Merge branch 'master' into packets

This commit is contained in:
joodicator 2019-05-13 21:28:02 +02:00
commit faf02acf62
25 changed files with 613 additions and 231 deletions

View File

@ -29,6 +29,7 @@ pyCraft is compatible with the following Minecraft releases:
* 1.11, 1.11.1, 1.11.2
* 1.12, 1.12.1, 1.12.2
* 1.13, 1.13.1, 1.13.2
* 1.14
In addition, some development snapshots and pre-release versions are supported:
`<minecraft/__init__.py>`_ contains a full list of supported Minecraft versions

View File

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

View File

@ -6,132 +6,170 @@ with a MineCraft server.
__version__ = "0.5.0"
SUPPORTED_MINECRAFT_VERSIONS = {
'1.8': 47,
'1.8.1': 47,
'1.8.2': 47,
'1.8.3': 47,
'1.8.4': 47,
'1.8.5': 47,
'1.8.6': 47,
'1.8.7': 47,
'1.8.8': 47,
'1.8.9': 47,
'1.9': 107,
'1.9.1': 108,
'1.9.2': 109,
'1.9.3': 110,
'1.9.4': 110,
'1.10': 210,
'1.10.1': 210,
'1.10.2': 210,
'16w32a': 301,
'16w32b': 302,
'16w33a': 303,
'16w35a': 304,
'16w36a': 305,
'16w38a': 306,
'16w39a': 307,
'16w39b': 308,
'16w39c': 309,
'16w40a': 310,
'16w41a': 311,
'16w42a': 312,
'16w43a': 313,
'16w44a': 313,
'1.11-pre1': 314,
'1.11': 315,
'16w50a': 316,
'1.11.1': 316,
'1.11.2': 316,
'17w06a': 317,
'17w13a': 318,
'17w13b': 319,
'17w14a': 320,
'17w15a': 321,
'17w16a': 322,
'17w16b': 323,
'17w17a': 324,
'17w17b': 325,
'17w18a': 326,
'17w18b': 327,
'1.12-pre1': 328,
'1.12-pre2': 329,
'1.12-pre3': 330,
'1.12-pre4': 331,
'1.12-pre5': 332,
'1.12-pre6': 333,
'1.12-pre7': 334,
'1.12': 335,
'17w31a': 336,
'1.12.1-pre1': 337,
'1.12.1': 338,
'1.12.2-pre1': 339,
'1.12.2-pre2': 339,
'1.12.2': 340,
'17w43a': 341,
'17w43b': 342,
'17w45a': 343,
'17w45b': 344,
'17w46a': 345,
'17w47a': 346,
'17w47b': 347,
'17w48a': 348,
'17w49a': 349,
'17w49b': 350,
'17w50a': 351,
'18w01a': 352,
'18w02a': 353,
'18w03a': 354,
'18w03b': 355,
'18w05a': 356,
'18w06a': 357,
'18w07a': 358,
'18w07b': 359,
'18w07c': 360,
'18w08a': 361,
'18w08b': 362,
'18w09a': 363,
'18w10a': 364,
'18w10b': 365,
'18w10c': 366,
'18w10d': 367,
'18w11a': 368,
'18w14a': 369,
'18w14b': 370,
'18w15a': 371,
'18w16a': 372,
'18w19a': 373,
'18w19b': 374,
'18w20a': 375,
'18w20b': 376,
'18w20c': 377,
'18w21a': 378,
'18w21b': 379,
'18w22a': 380,
'18w22b': 381,
'18w22c': 382,
'1.13-pre1': 383,
'1.13-pre2': 384,
'1.13-pre3': 385,
'1.13-pre4': 386,
'1.13-pre5': 387,
'1.13-pre6': 388,
'1.13-pre7': 389,
'1.13-pre8': 390,
'1.13-pre9': 391,
'1.13-pre10': 392,
'1.13': 393,
'18w30a': 394,
'18w30b': 395,
'18w31a': 396,
'18w32a': 397,
'18w33a': 398,
'1.13.1-pre1': 399,
'1.13.1-pre2': 400,
'1.13.1': 401,
'1.13.2-pre1': 402,
'1.13.2-pre2': 403,
'1.13.2': 404,
'1.8': 47,
'1.8.1': 47,
'1.8.2': 47,
'1.8.3': 47,
'1.8.4': 47,
'1.8.5': 47,
'1.8.6': 47,
'1.8.7': 47,
'1.8.8': 47,
'1.8.9': 47,
'1.9': 107,
'1.9.1': 108,
'1.9.2': 109,
'1.9.3': 110,
'1.9.4': 110,
'1.10': 210,
'1.10.1': 210,
'1.10.2': 210,
'16w32a': 301,
'16w32b': 302,
'16w33a': 303,
'16w35a': 304,
'16w36a': 305,
'16w38a': 306,
'16w39a': 307,
'16w39b': 308,
'16w39c': 309,
'16w40a': 310,
'16w41a': 311,
'16w42a': 312,
'16w43a': 313,
'16w44a': 313,
'1.11-pre1': 314,
'1.11': 315,
'16w50a': 316,
'1.11.1': 316,
'1.11.2': 316,
'17w06a': 317,
'17w13a': 318,
'17w13b': 319,
'17w14a': 320,
'17w15a': 321,
'17w16a': 322,
'17w16b': 323,
'17w17a': 324,
'17w17b': 325,
'17w18a': 326,
'17w18b': 327,
'1.12-pre1': 328,
'1.12-pre2': 329,
'1.12-pre3': 330,
'1.12-pre4': 331,
'1.12-pre5': 332,
'1.12-pre6': 333,
'1.12-pre7': 334,
'1.12': 335,
'17w31a': 336,
'1.12.1-pre1': 337,
'1.12.1': 338,
'1.12.2-pre1': 339,
'1.12.2-pre2': 339,
'1.12.2': 340,
'17w43a': 341,
'17w43b': 342,
'17w45a': 343,
'17w45b': 344,
'17w46a': 345,
'17w47a': 346,
'17w47b': 347,
'17w48a': 348,
'17w49a': 349,
'17w49b': 350,
'17w50a': 351,
'18w01a': 352,
'18w02a': 353,
'18w03a': 354,
'18w03b': 355,
'18w05a': 356,
'18w06a': 357,
'18w07a': 358,
'18w07b': 359,
'18w07c': 360,
'18w08a': 361,
'18w08b': 362,
'18w09a': 363,
'18w10a': 364,
'18w10b': 365,
'18w10c': 366,
'18w10d': 367,
'18w11a': 368,
'18w14a': 369,
'18w14b': 370,
'18w15a': 371,
'18w16a': 372,
'18w19a': 373,
'18w19b': 374,
'18w20a': 375,
'18w20b': 376,
'18w20c': 377,
'18w21a': 378,
'18w21b': 379,
'18w22a': 380,
'18w22b': 381,
'18w22c': 382,
'1.13-pre1': 383,
'1.13-pre2': 384,
'1.13-pre3': 385,
'1.13-pre4': 386,
'1.13-pre5': 387,
'1.13-pre6': 388,
'1.13-pre7': 389,
'1.13-pre8': 390,
'1.13-pre9': 391,
'1.13-pre10': 392,
'1.13': 393,
'18w30a': 394,
'18w30b': 395,
'18w31a': 396,
'18w32a': 397,
'18w33a': 398,
'1.13.1-pre1': 399,
'1.13.1-pre2': 400,
'1.13.1': 401,
'1.13.2-pre1': 402,
'1.13.2-pre2': 403,
'1.13.2': 404,
'18w43a': 441,
'18w43b': 441,
'18w43c': 442,
'18w44a': 443,
'18w45a': 444,
'18w46a': 445,
'18w47a': 446,
'18w47b': 447,
'18w48a': 448,
'18w48b': 449,
'18w49a': 450,
'18w50a': 451,
'19w02a': 452,
'19w03a': 453,
'19w03b': 454,
'19w03c': 455,
'19w04a': 456,
'19w04b': 457,
'19w05a': 458,
'19w06a': 459,
'19w07a': 460,
'19w08a': 461,
'19w08b': 462,
'19w09a': 463,
'19w11a': 464,
'19w11b': 465,
'19w12a': 466,
'19w12b': 467,
'19w13a': 468,
'19w13b': 469,
'19w14a': 470,
'19w14b': 471,
'1.14 Pre-Release 1': 472,
'1.14 Pre-Release 2': 473,
'1.14 Pre-Release 3': 474,
'1.14 Pre-Release 4': 475,
'1.14 Pre-Release 5': 476,
'1.14': 477,
}
SUPPORTED_PROTOCOL_VERSIONS = \

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

@ -187,6 +187,16 @@ class Connection(object):
else:
self._outgoing_packet_queue.append(packet)
def listener(self, *packet_types, **kwds):
"""
Shorthand decorator to register a function as a packet listener.
"""
def _method_func(method):
self.register_packet_listener(method, *packet_types, **kwds)
return method
return _method_func
def register_packet_listener(self, method, *packet_types, **kwds):
"""
Registers a listener method which will be notified when a packet of

View File

@ -4,7 +4,7 @@ from minecraft.networking.packets import (
from minecraft.networking.types import (
Integer, FixedPointInteger, UnsignedByte, Byte, Boolean, UUID, Short,
VarInt, Double, Float, String, Enum,
VarInt, Double, Float, String, Enum, Difficulty, Dimension, GameMode
)
from .combat_event_packet import CombatEventPacket
@ -21,6 +21,7 @@ def get_packets(context):
packets = {
KeepAlivePacket,
JoinGamePacket,
ServerDifficultyPacket,
ChatMessagePacket,
PlayerPositionAndLookPacket,
MapPacket,
@ -34,6 +35,7 @@ def get_packets(context):
SpawnObjectPacket,
BlockChangePacket,
MultiBlockChangePacket,
RespawnPacket,
PluginMessagePacket,
PlayerListHeaderAndFooterPacket,
SoundEffectPacket,
@ -48,7 +50,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 +74,39 @@ class JoinGamePacket(Packet):
{'entity_id': Integer},
{'game_mode': UnsignedByte},
{'dimension': Integer if context.protocol_version >= 108 else Byte},
{'difficulty': UnsignedByte},
{'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'max_players': UnsignedByte},
{'level_type': String},
{'reduced_debug_info': Boolean}])
{'render_distance': VarInt} if context.protocol_version >= 468 else {},
{'reduced_debug_info': Boolean},
])
# JoinGamePacket.Difficulty is an alias for Difficulty
Difficulty = Difficulty
# JoinGamePacket.Gamemode is an alias for Gamemode
GameMode = GameMode
# JoinGamePacket.Dimension is an alias for Dimension
Dimension = Dimension
class ServerDifficultyPacket(Packet):
@staticmethod
def get_id(context):
return 0x0D if context.protocol_version >= 332 else \
0x0E if context.protocol_version >= 318 else \
0x0D if context.protocol_version >= 70 else \
0x41
packet_name = 'server difficulty'
get_definition = staticmethod(lambda context: [
{'difficulty': UnsignedByte},
{'is_locked': Boolean} if context.protocol_version >= 464 else {},
])
# ServerDifficultyPacket.Difficulty is an alias for Difficulty
Difficulty = Difficulty
class ChatMessagePacket(Packet):
@ -100,7 +132,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 \
@ -146,7 +179,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 \
@ -168,7 +204,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 \
@ -186,10 +225,44 @@ class UpdateHealthPacket(Packet):
])
class RespawnPacket(Packet):
@staticmethod
def get_id(context):
return 0x3A if context.protocol_version >= 471 else \
0x38 if context.protocol_version >= 461 else \
0x39 if context.protocol_version >= 451 else \
0x38 if context.protocol_version >= 389 else \
0x37 if context.protocol_version >= 352 else \
0x36 if context.protocol_version >= 345 else \
0x35 if context.protocol_version >= 336 else \
0x34 if context.protocol_version >= 332 else \
0x35 if context.protocol_version >= 318 else \
0x33 if context.protocol_version >= 70 else \
0x07
packet_name = 'respawn'
get_definition = staticmethod(lambda context: [
{'dimension': Integer},
{'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'game_mode': UnsignedByte},
{'level_type': String},
])
# RespawnPacket.Difficulty is an alias for Difficulty.
Difficulty = Difficulty
# RespawnPacket.Dimension is an alias for Dimension.
Dimension = Dimension
# RespawnPacket.Gamemode is an alias for Gamemode.
GameMode = GameMode
class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod
def get_id(context):
return 0x19 if context.protocol_version >= 345 else \
return 0x18 if context.protocol_version >= 471 else \
0x19 if context.protocol_version >= 345 else \
0x18 if context.protocol_version >= 332 else \
0x19 if context.protocol_version >= 318 else \
0x18 if context.protocol_version >= 70 else \
@ -199,7 +272,11 @@ class PluginMessagePacket(AbstractPluginMessagePacket):
class PlayerListHeaderAndFooterPacket(Packet):
@staticmethod
def get_id(context):
return 0x4E if context.protocol_version >= 393 else \
return 0x53 if context.protocol_version >= 471 else \
0x5F if context.protocol_version >= 461 else \
0x50 if context.protocol_version >= 451 else \
0x4F if context.protocol_version >= 441 else \
0x4E if context.protocol_version >= 393 else \
0x4A if context.protocol_version >= 338 else \
0x49 if context.protocol_version >= 335 else \
0x47 if context.protocol_version >= 110 else \

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,40 +15,89 @@ class SpawnObjectPacket(Packet):
packet_name = 'spawn object'
class EntityType(Enum):
BOAT = 1
ITEM_STACK = 2
AREA_EFFECT_CLOUD = 3
MINECART = 10
ACTIVATED_TNT = 50
ENDERCRYSTAL = 51
ARROW = 60
SNOWBALL = 61
EGG = 62
FIREBALL = 63
FIRECHARGE = 64
ENDERPERL = 65
WITHER_SKULL = 66
SHULKER_BULLET = 67
LLAMA_SPIT = 68
FALLING_OBJECT = 70
ITEM_FRAMES = 71
EYE_OF_ENDER = 72
POTION = 73
EXP_BOTTLE = 75
FIREWORK_ROCKET = 76
LEASH_KNOT = 77
ARMORSTAND = 78
EVOCATION_FANGS = 79
FISHING_HOOK = 90
SPECTRAL_ARROW = 91
DRAGON_FIREBALL = 93
fields = ('entity_id', 'object_uuid', 'type_id',
'x', 'y', 'z', 'pitch', 'yaw')
@descriptor
def EntityType(desc, self, cls): # pylint: disable=no-self-argument
if self is None:
# EntityType is being accessed as a class attribute.
raise AttributeError(
'This interface is deprecated:\n\n'
'As of pyCraft\'s support for Minecraft 1.14, the nested '
'class "SpawnObjectPacket.EntityType" cannot be accessed as a '
'class attribute, because it depends on the protocol version. '
'There are two ways to access the correct version of the '
'class:\n\n'
'1. Access the "EntityType" attribute of a '
'"SpawnObjectPacket" instance with its "context" property '
'set.\n\n'
'2. Call "SpawnObjectPacket.field_enum(\'type_id\', '
'context)".')
else:
# EntityType is being accessed as an instance attribute.
return self.field_enum('type_id', self.context)
@classmethod
def field_enum(cls, field, context):
if field != 'type_id' or context is None:
return
pv = context.protocol_version
name = 'EntityType_%d' % pv
if hasattr(cls, name):
return getattr(cls, name)
class EntityType(Enum):
ACTIVATED_TNT = 50 if pv < 458 else 55 # PrimedTnt
AREA_EFFECT_CLOUD = 3 if pv < 458 else 0
ARMORSTAND = 78 if pv < 458 else 1
ARROW = 60 if pv < 458 else 2
BOAT = 1 if pv < 458 else 5
DRAGON_FIREBALL = 93 if pv < 458 else 13
EGG = 62 if pv < 458 else 74 # ThrownEgg
ENDERCRYSTAL = 51 if pv < 458 else 16
ENDERPEARL = 65 if pv < 458 else 75 # ThrownEnderpearl
EVOCATION_FANGS = 79 if pv < 458 else 20
EXP_BOTTLE = 75 if pv < 458 else 76 # ThrownExpBottle
EYE_OF_ENDER = 72 if pv < 458 else 23 # EyeOfEnderSignal
FALLING_OBJECT = 70 if pv < 458 else 24 # FallingSand
FIREBALL = 63 if pv < 458 else 34 # Fireball (ghast)
FIRECHARGE = 64 if pv < 458 else 65 # SmallFireball (blaze)
FIREWORK_ROCKET = 76 if pv < 458 else 25 # FireworksRocketEntity
FISHING_HOOK = 90 if pv < 458 else 93 # Fishing bobber
ITEM_FRAMES = 71 if pv < 458 else 33 # ItemFrame
ITEM_STACK = 2 if pv < 458 else 32 # Item
LEASH_KNOT = 77 if pv < 458 else 35
LLAMA_SPIT = 68 if pv < 458 else 37
MINECART = 10 if pv < 458 else 39 # MinecartRideable
POTION = 73 if pv < 458 else 77 # ThrownPotion
SHULKER_BULLET = 67 if pv < 458 else 60
SNOWBALL = 61 if pv < 458 else 67
SPECTRAL_ARROW = 91 if pv < 458 else 68
WITHER_SKULL = 66 if pv < 458 else 85
if pv >= 393:
TRIDENT = 94
if pv >= 458:
MINECART_CHEST = 40
MINECART_COMMAND_BLOCK = 41
MINECART_FURNACE = 42
MINECART_HOPPER = 43
MINECART_SPAWNER = 44
MINECART_TNT = 45
setattr(cls, name, EntityType)
return EntityType
def read(self, file_object):
self.entity_id = VarInt.read(file_object)
if self.context.protocol_version >= 49:
self.object_uuid = UUID.read(file_object)
self.type_id = Byte.read(file_object)
if self.context.protocol_version >= 458:
self.type_id = VarInt.read(file_object)
else:
self.type_id = Byte.read(file_object)
xyz_type = Double if self.context.protocol_version >= 100 else Integer
for attr in 'x', 'y', 'z':
@ -64,7 +114,11 @@ class SpawnObjectPacket(Packet):
VarInt.send(self.entity_id, packet_buffer)
if self.context.protocol_version >= 49:
UUID.send(self.object_uuid, packet_buffer)
Byte.send(self.type_id, packet_buffer)
if self.context.protocol_version >= 458:
VarInt.send(self.type_id, packet_buffer)
else:
Byte.send(self.type_id, packet_buffer)
xyz_type = Double if self.context.protocol_version >= 100 else Integer
for coord in self.x, self.y, self.z:
@ -78,9 +132,20 @@ class SpawnObjectPacket(Packet):
Short.send(coord, packet_buffer)
# Access the entity type as a string, according to the EntityType enum.
@property
def type(self):
if self.context is None:
raise ValueError('This packet must have a non-None "context" '
'in order to read the "type" property.')
# pylint: disable=no-member
return self.EntityType.name_from_value(self.type_id)
@type.setter
def type(self, type_name):
if self.context is None:
raise ValueError('This packet must have a non-None "context" '
'in order to set the "type" property.')
self.type_id = getattr(self.EntityType, type_name)
type = property(lambda p: p.EntityType.name_from_value(p.type_id), type)
# Access the fields 'x', 'y', 'z' as a Vector.
def position(self, position):

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__
@ -129,7 +129,7 @@ class Packet(object):
"""
value = getattr(self, field, None)
enum_class = self.field_enum(field)
enum_class = self.field_enum(field, self.context)
if enum_class is not None:
name = enum_class.name_from_value(value)
if name is not None:
@ -138,7 +138,7 @@ class Packet(object):
return repr(value)
@classmethod
def field_enum(cls, field):
def field_enum(cls, field, context=None):
""" The subclass of 'minecraft.networking.types.Enum' associated with
this field, or None if there is no such class.
"""

View File

@ -33,7 +33,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 \
@ -46,7 +48,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 \
@ -71,7 +74,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 \
@ -102,7 +107,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 \
@ -122,7 +129,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 \
@ -135,7 +143,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
@ -146,7 +154,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 \
@ -171,7 +180,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 \
@ -185,12 +196,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

@ -12,6 +12,7 @@ from .utility import Vector
__all__ = (
'Enum', 'BitFieldEnum', 'AbsoluteHand', 'RelativeHand', 'BlockFace',
'Difficulty', 'Dimension', 'GameMode'
)
@ -82,3 +83,26 @@ class BlockFace(Enum):
# >>> BlockFace.to_vector[BlockFace.NORTH]
# Position(x=0, y=0, z=-1)
to_vector = {fce: pos for (pos, fce) in from_vector.items()}
# Designation of a world's difficulty.
class Difficulty(Enum):
PEACEFUL = 0
EASY = 1
NORMAL = 2
HARD = 3
# Designation of a world's dimension.
class Dimension(Enum):
NETHER = -1
OVERWORLD = 0
END = 1
# Designation of a player's gamemode.
class GameMode(Enum):
SURVIVAL = 0
CREATIVE = 1
ADVENTURE = 2
SPECTATOR = 3

View File

@ -6,7 +6,7 @@ from collections import namedtuple
__all__ = (
'Vector', 'MutableRecord', 'PositionAndLook',
'Vector', 'MutableRecord', 'descriptor',
)
@ -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.

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

@ -224,26 +224,23 @@ class EarlyPacketListenerTest(ConnectTest):
the early packet listener is registered afterwards.
"""
def _start_client(self, client):
@client.listener(clientbound.play.JoinGamePacket)
def handle_join(packet):
assert early_handle_join.called, \
'Ordinary listener called before early listener.'
handle_join.called = True
handle_join.called = False
client.register_packet_listener(
handle_join, clientbound.play.JoinGamePacket)
@client.listener(clientbound.play.JoinGamePacket, early=True)
def early_handle_join(packet):
early_handle_join.called = True
client.register_packet_listener(
early_handle_join, clientbound.play.JoinGamePacket, early=True)
early_handle_join.called = False
@client.listener(clientbound.play.DisconnectPacket)
def handle_disconnect(packet):
assert early_handle_join.called, 'Early listener not called.'
assert handle_join.called, 'Ordinary listener not called.'
raise fake_server.FakeServerTestSuccess
client.register_packet_listener(
handle_disconnect, clientbound.play.DisconnectPacket)
client.connect()
@ -409,7 +406,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

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

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 =