Merge branch 'master' into patch-1

This commit is contained in:
joo 2019-05-13 17:49:20 +02:00 committed by joodicator
commit d627423949
26 changed files with 602 additions and 228 deletions

View File

@ -28,7 +28,8 @@ pyCraft is compatible with the following Minecraft releases:
* 1.10, 1.10.1, 1.10.2
* 1.11, 1.11.1, 1.11.2
* 1.12, 1.12.1, 1.12.2
* 1.13
* 1.13, 1.13.1, 1.13.2
* 1.14
In addition, some development snapshots and pre-release versions are supported:
`<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:
Arbitary Requests
Arbitrary Requests
~~~~~~~~~~~~~~~~~~~~
You may make any arbitary request to the Yggdrasil service with the _make_request
You may make any arbitrary request to the Yggdrasil service with the _make_request
method passing in the AUTH_SERVER as the server parameter.
.. automodule:: minecraft.authentication
@ -43,7 +43,7 @@ method passing in the AUTH_SERVER as the server parameter.
---------------
Example Usage
---------------
An example of making an arbitary request can be seen here::
An example of making an arbitrary request can be seen here::
payload = {'username': username,
'password': password}

View File

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

View File

@ -121,6 +121,55 @@ SUPPORTED_MINECRAFT_VERSIONS = {
'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 = \

View File

@ -1,5 +1,6 @@
import requests
import json
import uuid
from .exceptions import YggdrasilError
#: The base url for Ygdrassil requests
@ -84,7 +85,7 @@ class AuthenticationToken(object):
return True
def authenticate(self, username, password):
def authenticate(self, username, password, invalidate_previous=False):
"""
Authenticates the user against https://authserver.mojang.com using
`username` and `password` parameters.
@ -93,6 +94,8 @@ class AuthenticationToken(object):
username - An `str` object with the username (unmigrated accounts)
or email address for a Mojang account.
password - An `str` object with the password.
invalidate_previous - A `bool`. When `True`, invalidate
all previously acquired `access_token`s across all clients.
Returns:
Returns `True` if successful.
@ -110,6 +113,12 @@ class AuthenticationToken(object):
"password": password
}
if not invalidate_previous:
# Include a `client_token` in the payload to prevent existing
# `access_token`s from being invalidated. If `self.client_token`
# is `None` generate a `client_token` using uuid4
payload["clientToken"] = self.client_token or uuid.uuid4().hex
res = _make_request(AUTH_SERVER, "authenticate", payload)
_raise_from_response(res)
@ -269,7 +278,7 @@ def _make_request(server, endpoint, data):
A `requests.Request` object.
"""
res = requests.post(server + "/" + endpoint, data=json.dumps(data),
headers=HEADERS)
headers=HEADERS, timeout=15)
return res

View File

@ -352,6 +352,8 @@ class Connection(object):
self.socket = socket.socket(ai_faml, ai_type, ai_prot)
self.socket.connect(ai_addr)
self.file_object = self.socket.makefile("rb", 0)
self.options.compression_enabled = False
self.options.compression_threshold = -1
self.connected = True
def disconnect(self, immediate=False):
@ -500,7 +502,7 @@ class NetworkingThread(threading.Thread):
# Ignore the earlier exception if a disconnect packet is
# received, as it may have been caused by trying to write to
# thw closed socket, which does not represent a program error.
# the closed socket, which does not represent a program error.
if exc_info is not None and packet.packet_name == "disconnect":
exc_info = None

View File

@ -37,6 +37,7 @@ def get_packets(context):
MultiBlockChangePacket,
RespawnPacket,
PluginMessagePacket,
PlayerListHeaderAndFooterPacket,
}
if context.protocol_version <= 47:
packets |= {
@ -48,7 +49,8 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod
def get_id(context):
return 0x21 if context.protocol_version >= 389 else \
return 0x20 if context.protocol_version >= 471 else \
0x21 if context.protocol_version >= 389 else \
0x20 if context.protocol_version >= 345 else \
0x1F if context.protocol_version >= 332 else \
0x20 if context.protocol_version >= 318 else \
@ -71,10 +73,11 @@ class JoinGamePacket(Packet):
{'entity_id': Integer},
{'game_mode': UnsignedByte},
{'dimension': Integer if context.protocol_version >= 108 else Byte},
{'difficulty': UnsignedByte},
{'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'max_players': UnsignedByte},
{'level_type': String},
{'reduced_debug_info': Boolean}
{'render_distance': VarInt} if context.protocol_version >= 468 else {},
{'reduced_debug_info': Boolean},
])
# JoinGamePacket.Difficulty is an alias for Difficulty
@ -127,7 +130,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 \
@ -173,7 +177,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 \
@ -195,7 +202,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 \
@ -246,8 +256,29 @@ class RespawnPacket(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 \
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):
@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 \

View File

@ -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 \

View File

@ -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)

View File

@ -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 \

View File

@ -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

View File

@ -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,39 +15,88 @@ class SpawnObjectPacket(Packet):
packet_name = 'spawn object'
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):
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
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)
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
@ -64,6 +114,10 @@ class SpawnObjectPacket(Packet):
VarInt.send(self.entity_id, packet_buffer)
if self.context.protocol_version >= 49:
UUID.send(self.object_uuid, 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
@ -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):

View File

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

View File

@ -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.

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

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

View File

@ -300,6 +300,10 @@ class NormalConnectionProcedure(unittest.TestCase):
def mocked_make_request(server, endpoint, data):
if endpoint == "authenticate":
if "accessToken" in data:
response = successful_res.copy()
response.json["accessToken"] = data["accessToken"]
return response
return successful_res
if endpoint == "refresh" and data["accessToken"] == "token":
return successful_res
@ -323,6 +327,9 @@ class NormalConnectionProcedure(unittest.TestCase):
self.assertFalse(a.authenticated)
self.assertTrue(a.authenticate("username", "password"))
self.assertEqual(_make_request_mock.call_count, 1)
self.assertIn("clientToken", _make_request_mock.call_args[0][2])
self.assertTrue(a.authenticated)
self.assertTrue(a.refresh())
@ -337,6 +344,35 @@ class NormalConnectionProcedure(unittest.TestCase):
self.assertEqual(_make_request_mock.call_count, 6)
# Test that we send a provided clientToken if the authenticationToken
# is initialized with one
with mock.patch("minecraft.authentication._make_request",
side_effect=mocked_make_request) as _make_request_mock:
a = AuthenticationToken(client_token="existing_token")
self.assertTrue(a.authenticate("username", "password",
invalidate_previous=False))
self.assertEqual(_make_request_mock.call_count, 1)
self.assertEqual(
"existing_token",
_make_request_mock.call_args[0][2]["clientToken"]
)
# Test that we invalidate previous tokens properly
with mock.patch("minecraft.authentication._make_request",
side_effect=mocked_make_request) as _make_request_mock:
a = AuthenticationToken()
self.assertFalse(a.authenticated)
self.assertTrue(a.authenticate("username", "password",
invalidate_previous=True))
self.assertTrue(a.authenticated)
self.assertEqual(a.access_token, "token")
self.assertEqual(_make_request_mock.call_count, 1)
self.assertNotIn("clientToken", _make_request_mock.call_args[0][2])
a = AuthenticationToken(username="username",
access_token="token",
client_token="token")

View File

@ -32,6 +32,46 @@ class ConnectTest(fake_server._FakeServerTest):
raise fake_server.FakeServerDisconnect
class ReconnectTest(ConnectTest):
phase = 0
def _start_client(self, client):
def handle_login_disconnect(packet):
if 'Please reconnect' in packet.json_data:
# Override the default behaviour of raising a fatal exception.
client.disconnect()
client.connect()
raise IgnorePacket
client.register_packet_listener(
handle_login_disconnect, clientbound.login.DisconnectPacket,
early=True)
def handle_play_disconnect(packet):
if 'Please reconnect' in packet.json_data:
client.connect()
elif 'Test successful' in packet.json_data:
raise fake_server.FakeServerTestSuccess
client.register_packet_listener(
handle_play_disconnect, clientbound.play.DisconnectPacket)
client.connect()
class client_handler_type(fake_server.FakeClientHandler):
def handle_login(self, packet):
if self.server.test_case.phase == 0:
self.server.test_case.phase = 1
raise fake_server.FakeServerDisconnect('Please reconnect (0).')
super(ReconnectTest.client_handler_type, self).handle_login(packet)
def handle_play_start(self):
if self.server.test_case.phase == 1:
self.server.test_case.phase = 2
raise fake_server.FakeServerDisconnect('Please reconnect (1).')
else:
assert self.server.test_case.phase == 2
raise fake_server.FakeServerDisconnect('Test successful (2).')
class PingTest(ConnectTest):
def _start_client(self, client):
def handle_ping(latency_ms):
@ -369,7 +409,7 @@ class VersionNegotiationEdgeCases(fake_server._FakeServerTest):
status_response = '{"description": {"text": "FakeServer"}}'
class ClientHandler(fake_server.FakeClientHandler):
def _run_status(self):
def handle_status(self, request_packet):
packet = clientbound.status.ResponsePacket()
packet.json_response = status_response
self.write_packet(packet)

View File

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

View File

@ -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

View File

@ -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(

View File

@ -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,18 +35,22 @@ TEST_DATA = {
class SerializationTest(unittest.TestCase):
def test_serialization(self):
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
context = ConnectionContext(protocol_version=protocol_version)
for data_type in Type.__subclasses__():
if data_type in TEST_DATA:
test_cases = TEST_DATA[data_type]
for test_data in test_cases:
packet_buffer = PacketBuffer()
data_type.send(test_data, packet_buffer)
data_type.send_with_context(
test_data, packet_buffer, context)
packet_buffer.reset_cursor()
deserialized = data_type.read(packet_buffer)
deserialized = data_type.read_with_context(
packet_buffer, context)
if data_type is FixedPointInteger:
self.assertAlmostEqual(
test_data, deserialized, delta=1.0/32.0)
@ -58,9 +64,21 @@ class SerializationTest(unittest.TestCase):
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)

View File

@ -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 =