Fix support for Minecraft 20w06a to 1.16.2 (protocols 701 to 751)

This commit is contained in:
joo 2020-08-17 06:13:24 +02:00
parent b79f8b30eb
commit 4c35517157
20 changed files with 654 additions and 256 deletions

View File

@ -60,6 +60,7 @@ Requirements
------------
- `cryptography <https://github.com/pyca/cryptography#cryptography>`_
- `requests <http://docs.python-requests.org/en/latest/>`_
- `pynbt <https://github.com/TkTech/PyNBT>`_
The requirements are also stored in ``requirements.txt``

View File

@ -242,13 +242,28 @@ SUPPORTED_MINECRAFT_VERSIONS = {
'20w19a': 715,
'20w20a': 716,
'20w20b': 717,
'1.16 Pre-release 2': 722,
'1.16 Pre-release 3': 725,
'1.16 Pre-release 4': 727,
'1.16 Pre-release 5': 729,
'1.16 Release Candidate 1':734,
'20w21a': 718,
'20w22a': 719,
'1.16-pre1': 721,
'1.16-pre2': 722,
'1.16-pre3': 725,
'1.16-pre4': 727,
'1.16-pre5': 729,
'1.16-pre6': 730,
'1.16-pre7': 732,
'1.16-pre8': 733,
'1.16-rc1': 734,
'1.16': 735,
'1.16.1': 736,
'20w27a': 738,
'20w28a': 740,
'20w29a': 741,
'20w30a': 743,
'1.16.2-pre1': 744,
'1.16.2-pre2': 746,
'1.16.2-pre3': 748,
'1.16.2-rc1': 749,
'1.16.2-rc2': 750,
'1.16.2': 751,
}
@ -270,9 +285,11 @@ def initglobals():
This allows the SUPPORTED_MINECRAFT_VERSIONS dict to be updated, then the
other globals can be updated as well, to allow for dynamic version support.
All updates are done by reference to allow this to work else where in the code.
All updates are done by reference to allow this to work else where in the
code.
'''
global RELEASE_MINECRAFT_VERSIONS, SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS
global RELEASE_MINECRAFT_VERSIONS, SUPPORTED_PROTOCOL_VERSIONS
global RELEASE_PROTOCOL_VERSIONS
import re

View File

@ -3,10 +3,9 @@ from minecraft.networking.packets import (
)
from minecraft.networking.types import (
Integer, FixedPointInteger, Angle, UnsignedByte, Byte, Boolean, UUID,
Short, VarInt, Double, Float, String, Enum, Difficulty, Dimension,
GameMode, Long, Vector, Direction, PositionAndLook,
multi_attribute_alias,
FixedPointInteger, Angle, UnsignedByte, Byte, Boolean, UUID, Short, VarInt,
Double, Float, String, Enum, Difficulty, Long, Vector, Direction,
PositionAndLook, multi_attribute_alias,
)
from .combat_event_packet import CombatEventPacket
@ -18,6 +17,7 @@ from .block_change_packet import BlockChangePacket, MultiBlockChangePacket
from .explosion_packet import ExplosionPacket
from .sound_effect_packet import SoundEffectPacket
from .face_player_packet import FacePlayerPacket
from .join_game_and_respawn_packets import JoinGamePacket, RespawnPacket
# Formerly known as state_playing_clientbound.
@ -64,8 +64,8 @@ def get_packets(context):
class KeepAlivePacket(AbstractKeepAlivePacket):
@staticmethod
def get_id(context):
return 0x1F if context.protocol_version >= 751 else \
0x20 if context.protocol_version >= 722 else \
return 0x1F if context.protocol_version >= 741 else \
0x20 if context.protocol_version >= 721 else \
0x21 if context.protocol_version >= 550 else \
0x20 if context.protocol_version >= 471 else \
0x21 if context.protocol_version >= 389 else \
@ -76,47 +76,10 @@ class KeepAlivePacket(AbstractKeepAlivePacket):
0x00
class JoinGamePacket(Packet):
@staticmethod
def get_id(context):
return 0x24 if context.protocol_version >= 751 else \
0x25 if context.protocol_version >= 722 else \
0x26 if context.protocol_version >= 550 else \
0x25 if context.protocol_version >= 389 else \
0x24 if context.protocol_version >= 345 else \
0x23 if context.protocol_version >= 332 else \
0x24 if context.protocol_version >= 318 else \
0x23 if context.protocol_version >= 107 else \
0x01
packet_name = "join game"
get_definition = staticmethod(lambda context: [
{'entity_id': Integer},
{'is_hardcore': Boolean} if context.protocol_version >= 751 else {},
{'game_mode': UnsignedByte},
{'previous_game_mode': UnsignedByte} if context.protocol_version >= 722 else {},
{'dimension': Integer if context.protocol_version >= 108 else Byte},
{'hashed_seed': Long} if context.protocol_version >= 552 else {},
{'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'max_players': UnsignedByte},
{'level_type': String} if context.protocol_version < 716 else {},
{'render_distance': VarInt} if context.protocol_version >= 468 else {},
{'reduced_debug_info': Boolean},
{'respawn_screen': Boolean} if context.protocol_version >= 571 else {},
{'is_debug': Boolean} if context.protocol_version >= 716 else {},
{'is_flat': Boolean} if context.protocol_version >= 716 else {},
])
# These aliases declare the Enum type corresponding to each field:
Difficulty = Difficulty
GameMode = GameMode
Dimension = Dimension
class ServerDifficultyPacket(Packet):
@staticmethod
def get_id(context):
return 0x0D if context.protocol_version >= 722 else \
return 0x0D if context.protocol_version >= 721 else \
0x0E if context.protocol_version >= 550 else \
0x0D if context.protocol_version >= 332 else \
0x0E if context.protocol_version >= 318 else \
@ -136,7 +99,7 @@ class ServerDifficultyPacket(Packet):
class ChatMessagePacket(Packet):
@staticmethod
def get_id(context):
return 0x0E if context.protocol_version >= 722 else \
return 0x0E if context.protocol_version >= 721 else \
0x0F if context.protocol_version >= 550 else \
0x0E if context.protocol_version >= 343 else \
0x0F if context.protocol_version >= 332 else \
@ -145,9 +108,11 @@ class ChatMessagePacket(Packet):
0x02
packet_name = "chat message"
definition = [
get_definition = staticmethod(lambda context: [
{'json_data': String},
{'position': Byte}]
{'position': Byte},
{'sender': UUID} if context.protocol_version >= 718 else {},
])
class Position(Enum):
CHAT = 0 # A player-initiated chat message.
@ -158,8 +123,8 @@ class ChatMessagePacket(Packet):
class DisconnectPacket(Packet):
@staticmethod
def get_id(context):
return 0x19 if context.protocol_version >= 751 else \
0x1A if context.protocol_version >= 722 else \
return 0x19 if context.protocol_version >= 741 else \
0x1A if context.protocol_version >= 721 else \
0x1B if context.protocol_version >= 550 else \
0x1A if context.protocol_version >= 471 else \
0x1B if context.protocol_version >= 345 else \
@ -185,7 +150,7 @@ class SetCompressionPacket(Packet):
class SpawnPlayerPacket(Packet):
@staticmethod
def get_id(context):
return 0x04 if context.protocol_version >= 722 else \
return 0x04 if context.protocol_version >= 721 else \
0x05 if context.protocol_version >= 67 else \
0x0C
@ -221,7 +186,7 @@ class SpawnPlayerPacket(Packet):
class EntityVelocityPacket(Packet):
@staticmethod
def get_id(context):
return 0x46 if context.protocol_version >= 722 else \
return 0x46 if context.protocol_version >= 721 else \
0x47 if context.protocol_version >= 707 else \
0x46 if context.protocol_version >= 550 else \
0x45 if context.protocol_version >= 471 else \
@ -249,10 +214,15 @@ class EntityVelocityPacket(Packet):
class EntityPositionDeltaPacket(Packet):
@staticmethod
def get_id(context):
return 0x27 if context.protocol_version >= 751 else \
0x28 if context.protocol_version >= 722 else \
0x29 if context.protocol_version >= 578 else \
0xFF
return 0x27 if context.protocol_version >= 741 else \
0x28 if context.protocol_version >= 721 else \
0x29 if context.protocol_version >= 550 else \
0x28 if context.protocol_version >= 389 else \
0x27 if context.protocol_version >= 345 else \
0x26 if context.protocol_version >= 318 else \
0x25 if context.protocol_version >= 94 else \
0x26 if context.protocol_version >= 70 else \
0x15
packet_name = "entity position delta"
get_definition = staticmethod(lambda context: [
@ -267,9 +237,19 @@ class EntityPositionDeltaPacket(Packet):
class TimeUpdatePacket(Packet):
@staticmethod
def get_id(context):
return 0x4E if context.protocol_version >= 722 else \
0x4F if context.protocol_version >= 578 else \
0xFF
return 0x4E if context.protocol_version >= 721 else \
0x4F if context.protocol_version >= 550 else \
0x4E if context.protocol_version >= 471 else \
0x4A if context.protocol_version >= 461 else \
0x4B if context.protocol_version >= 451 else \
0x4A if context.protocol_version >= 389 else \
0x49 if context.protocol_version >= 352 else \
0x48 if context.protocol_version >= 345 else \
0x47 if context.protocol_version >= 336 else \
0x46 if context.protocol_version >= 318 else \
0x44 if context.protocol_version >= 94 else \
0x43 if context.protocol_version >= 70 else \
0x03
packet_name = "time update"
get_definition = staticmethod(lambda context: [
@ -281,7 +261,7 @@ class TimeUpdatePacket(Packet):
class UpdateHealthPacket(Packet):
@staticmethod
def get_id(context):
return 0x49 if context.protocol_version >= 722 else \
return 0x49 if context.protocol_version >= 721 else \
0x4A if context.protocol_version >= 707 else \
0x49 if context.protocol_version >= 550 else \
0x48 if context.protocol_version >= 471 else \
@ -305,47 +285,11 @@ class UpdateHealthPacket(Packet):
])
class RespawnPacket(Packet):
@staticmethod
def get_id(context):
return 0x39 if context.protocol_version >= 751 else \
0x3A if context.protocol_version >= 722 else \
0x3B if context.protocol_version >= 550 else \
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 {},
{'hashed_seed': Long} if context.protocol_version >= 552 else {},
{'game_mode': UnsignedByte},
{'level_type': String} if context.protocol_version < 716 else {},
{'is_debug': Boolean} if context.protocol_version >= 716 else {},
{'is_flat': Boolean} if context.protocol_version >= 716 else {},
{'copy_metadata': Boolean},
])
# These aliases declare the Enum type corresponding to each field:
Difficulty = Difficulty
Dimension = Dimension
GameMode = GameMode
class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod
def get_id(context):
return 0x17 if context.protocol_version >= 751 else \
0x18 if context.protocol_version >= 722 else \
return 0x17 if context.protocol_version >= 741 else \
0x18 if context.protocol_version >= 721 else \
0x19 if context.protocol_version >= 550 else \
0x18 if context.protocol_version >= 471 else \
0x19 if context.protocol_version >= 345 else \
@ -358,7 +302,7 @@ class PluginMessagePacket(AbstractPluginMessagePacket):
class PlayerListHeaderAndFooterPacket(Packet):
@staticmethod
def get_id(context):
return 0x53 if context.protocol_version >= 722 else \
return 0x53 if context.protocol_version >= 721 else \
0x54 if context.protocol_version >= 550 else \
0x53 if context.protocol_version >= 471 else \
0x5F if context.protocol_version >= 461 else \
@ -380,8 +324,8 @@ class PlayerListHeaderAndFooterPacket(Packet):
class EntityLookPacket(Packet):
@staticmethod
def get_id(context):
return 0x29 if context.protocol_version >= 751 else \
0x2A if context.protocol_version >= 722 else \
return 0x29 if context.protocol_version >= 741 else \
0x2A if context.protocol_version >= 721 else \
0x2B if context.protocol_version >= 550 else \
0x2A if context.protocol_version >= 389 else \
0x29 if context.protocol_version >= 345 else \

View File

@ -1,14 +1,15 @@
from minecraft.networking.packets import Packet
from minecraft.networking.types import (
VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord,
attribute_alias, multi_attribute_alias,
Type, VarInt, VarLong, Long, Integer, UnsignedByte, Position, Vector,
MutableRecord, PrefixedArray, Boolean, attribute_alias,
multi_attribute_alias,
)
class BlockChangePacket(Packet):
@staticmethod
def get_id(context):
return 0x0B if context.protocol_version >= 722 else \
return 0x0B if context.protocol_version >= 721 else \
0x0C if context.protocol_version >= 550 else \
0x0B if context.protocol_version >= 332 else \
0x0C if context.protocol_version >= 318 else \
@ -47,8 +48,8 @@ class BlockChangePacket(Packet):
class MultiBlockChangePacket(Packet):
@staticmethod
def get_id(context):
return 0x3B if context.protocol_version >= 751 else \
0x0F if context.protocol_version >= 722 else \
return 0x3B if context.protocol_version >= 741 else \
0x0F if context.protocol_version >= 721 else \
0x10 if context.protocol_version >= 550 else \
0x0F if context.protocol_version >= 343 else \
0x10 if context.protocol_version >= 332 else \
@ -58,12 +59,23 @@ class MultiBlockChangePacket(Packet):
packet_name = 'multi block change'
fields = 'chunk_x', 'chunk_z', 'records'
# Only used in protocol 741 and later.
class ChunkSectionPos(Vector, Type):
@classmethod
def read(cls, file_object):
value = Long.read(file_object)
x = value >> 42
z = (value >> 20) & 0x3FFFFF
y = value & 0xFFFFF
return cls(x, y, z)
# Access the 'chunk_x' and 'chunk_z' fields as a tuple.
chunk_pos = multi_attribute_alias(tuple, 'chunk_x', 'chunk_z')
@classmethod
def send(cls, pos, socket):
x, y, z = pos
value = (x & 0x3FFFFF) << 42 | (z & 0x3FFFFF) << 20 | y & 0xFFFFF
Long.send(value, socket)
class Record(MutableRecord):
class Record(MutableRecord, Type):
__slots__ = 'x', 'y', 'z', 'block_state_id'
def __init__(self, **kwds):
@ -94,30 +106,47 @@ class MultiBlockChangePacket(Packet):
# This alias is retained for backward compatibility.
blockStateId = attribute_alias('block_state_id')
def read(self, file_object):
h_position = UnsignedByte.read(file_object)
self.x, self.z = h_position >> 4, h_position & 0xF
self.y = UnsignedByte.read(file_object)
self.block_state_id = VarInt.read(file_object)
@classmethod
def read_with_context(cls, file_object, context):
record = cls()
if context.protocol_version >= 741:
value = VarLong.read(file_object)
record.block_state_id = value >> 12
record.x = (value >> 8) & 0xF
record.z = (value >> 4) & 0xF
record.y = value & 0xF
else:
h_position = UnsignedByte.read(file_object)
record.x = h_position >> 4
record.z = h_position & 0xF
record.y = UnsignedByte.read(file_object)
record.block_state_id = VarInt.read(file_object)
return record
def write(self, packet_buffer):
UnsignedByte.send(self.x << 4 | self.z & 0xF, packet_buffer)
UnsignedByte.send(self.y, packet_buffer)
VarInt.send(self.block_state_id, packet_buffer)
@classmethod
def send_with_context(self, record, socket, context):
if context.protocol_version >= 741:
value = record.block_state_id << 12 | \
(record.x & 0xF) << 8 | \
(record.z & 0xF) << 4 | \
record.y & 0xF
VarLong.send(value, socket)
else:
UnsignedByte.send(record.x << 4 | record.z & 0xF, socket)
UnsignedByte.send(record.y, socket)
VarInt.send(record.block_state_id, socket)
def read(self, file_object):
self.chunk_x = Integer.read(file_object)
self.chunk_z = Integer.read(file_object)
records_count = VarInt.read(file_object)
self.records = []
for i in range(records_count):
record = self.Record()
record.read(file_object)
self.records.append(record)
get_definition = staticmethod(lambda context: [
{'chunk_section_pos': MultiBlockChangePacket.ChunkSectionPos},
{'invert_trust_edges': Boolean}
if context.protocol_version >= 748 else {}, # Provisional field name.
{'records': PrefixedArray(VarInt, MultiBlockChangePacket.Record)},
] if context.protocol_version >= 741 else [
{'chunk_x': Integer},
{'chunk_z': Integer},
{'records': PrefixedArray(VarInt, MultiBlockChangePacket.Record)},
])
def write_fields(self, packet_buffer):
Integer.send(self.chunk_x, packet_buffer)
Integer.send(self.chunk_z, packet_buffer)
VarInt.send(len(self.records), packet_buffer)
for record in self.records:
record.write(packet_buffer)
# Access the 'chunk_x' and 'chunk_z' fields as a tuple.
# Only used prior to protocol 741.
chunk_pos = multi_attribute_alias(tuple, 'chunk_x', 'chunk_z')

View File

@ -8,8 +8,8 @@ from minecraft.networking.types import (
class CombatEventPacket(Packet):
@staticmethod
def get_id(context):
return 0x31 if context.protocol_version >= 751 else \
0x32 if context.protocol_version >= 722 else \
return 0x31 if context.protocol_version >= 741 else \
0x32 if context.protocol_version >= 721 else \
0x33 if context.protocol_version >= 550 else \
0x32 if context.protocol_version >= 471 else \
0x30 if context.protocol_version >= 451 else \

View File

@ -1,5 +1,5 @@
from minecraft.networking.types import (
Vector, Float, Byte, Integer, multi_attribute_alias,
Vector, Float, Byte, Integer, PrefixedArray, multi_attribute_alias, Type,
)
from minecraft.networking.packets import Packet
@ -7,8 +7,8 @@ from minecraft.networking.packets import Packet
class ExplosionPacket(Packet):
@staticmethod
def get_id(context):
return 0x1B if context.protocol_version >= 751 else \
0x1C if context.protocol_version >= 722 else \
return 0x1B if context.protocol_version >= 741 else \
0x1C if context.protocol_version >= 721 else \
0x1D if context.protocol_version >= 550 else \
0x1C if context.protocol_version >= 471 else \
0x1E if context.protocol_version >= 389 else \
@ -21,8 +21,27 @@ class ExplosionPacket(Packet):
packet_name = 'explosion'
fields = 'x', 'y', 'z', 'radius', 'records', \
'player_motion_x', 'player_motion_y', 'player_motion_z'
class Record(Vector, Type):
__slots__ = ()
@classmethod
def read(cls, file_object):
return cls(*(Byte.read(file_object) for i in range(3)))
@classmethod
def send(cls, record, socket):
for coord in record:
Byte.send(coord, socket)
definition = [
{'x': Float},
{'y': Float},
{'z': Float},
{'radius': Float},
{'records': PrefixedArray(Integer, Record)},
{'player_motion_x': Float},
{'player_motion_y': Float},
{'player_motion_z': Float}]
# Access the 'x', 'y', 'z' fields as a Vector tuple.
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
@ -30,37 +49,3 @@ class ExplosionPacket(Packet):
# Access the 'player_motion_{x,y,z}' fields as a Vector tuple.
player_motion = multi_attribute_alias(
Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z')
class Record(Vector):
__slots__ = ()
def read(self, file_object):
self.x = Float.read(file_object)
self.y = Float.read(file_object)
self.z = Float.read(file_object)
self.radius = Float.read(file_object)
records_count = Integer.read(file_object)
self.records = []
for i in range(records_count):
rec_x = Byte.read(file_object)
rec_y = Byte.read(file_object)
rec_z = Byte.read(file_object)
record = ExplosionPacket.Record(rec_x, rec_y, rec_z)
self.records.append(record)
self.player_motion_x = Float.read(file_object)
self.player_motion_y = Float.read(file_object)
self.player_motion_z = Float.read(file_object)
def write_fields(self, packet_buffer):
Float.send(self.x, packet_buffer)
Float.send(self.y, packet_buffer)
Float.send(self.z, packet_buffer)
Float.send(self.radius, packet_buffer)
Integer.send(len(self.records), packet_buffer)
for record in self.records:
Byte.send(record.x, packet_buffer)
Byte.send(record.y, packet_buffer)
Byte.send(record.z, packet_buffer)
Float.send(self.player_motion_x, packet_buffer)
Float.send(self.player_motion_y, packet_buffer)
Float.send(self.player_motion_z, packet_buffer)

View File

@ -8,8 +8,8 @@ from minecraft.networking.packets import Packet
class FacePlayerPacket(Packet):
@staticmethod
def get_id(context):
return 0x33 if context.protocol_version >= 751 else \
0x34 if context.protocol_version >= 722 else \
return 0x33 if context.protocol_version >= 741 else \
0x34 if context.protocol_version >= 721 else \
0x35 if context.protocol_version >= 550 else \
0x34 if context.protocol_version >= 471 else \
0x32 if context.protocol_version >= 451 else \

View File

@ -0,0 +1,208 @@
import pynbt
from minecraft.networking.packets import Packet
from minecraft.networking.types import (
NBT, Integer, Boolean, UnsignedByte, String, Byte, Long, VarInt,
PrefixedArray, Difficulty, GameMode, Dimension,
)
def nbt_to_snbt(tag):
'''Convert a pyNBT tag to SNBT ("stringified NBT") format.'''
scalars = {
pynbt.TAG_Byte: 'b',
pynbt.TAG_Short: 's',
pynbt.TAG_Int: '',
pynbt.TAG_Long: 'l',
pynbt.TAG_Float: 'f',
pynbt.TAG_Double: 'd',
}
if type(tag) in scalars:
return repr(tag.value) + scalars[type(tag)]
arrays = {
pynbt.TAG_Byte_Array: 'B',
pynbt.TAG_Int_Array: 'I',
pynbt.TAG_Long_Array: 'L',
}
if type(tag) in arrays:
return '[' + arrays[type(tag)] + ';' + \
','.join(map(repr, tag.value)) + ']'
if isinstance(tag, pynbt.TAG_String):
return repr(tag.value)
if isinstance(tag, pynbt.TAG_List):
return '[' + ','.join(map(nbt_to_snbt, tag.value)) + ']'
if isinstance(tag, pynbt.TAG_Compound):
return '{' + ','.join(n + ':' + nbt_to_snbt(v)
for (n, v) in tag.items()) + '}'
raise TypeError('Unknown NBT tag type: %r' % type(tag))
class AbstractDimensionPacket(Packet):
''' The abstract superclass of JoinGamePacket and RespawnPacket, containing
common definitions relating to their 'dimension' field.
'''
def field_string(self, field):
# pylint: disable=no-member
if self.context.protocol_version >= 748 and field == 'dimension':
return nbt_to_snbt(self.dimension)
elif self.context.protocol_version < 718 and field == 'dimension':
return Dimension.name_from_value(self.dimension)
return super(AbstractDimensionPacket, self).field_string(field)
class JoinGamePacket(AbstractDimensionPacket):
@staticmethod
def get_id(context):
return 0x24 if context.protocol_version >= 741 else \
0x25 if context.protocol_version >= 721 else \
0x26 if context.protocol_version >= 550 else \
0x25 if context.protocol_version >= 389 else \
0x24 if context.protocol_version >= 345 else \
0x23 if context.protocol_version >= 332 else \
0x24 if context.protocol_version >= 318 else \
0x23 if context.protocol_version >= 107 else \
0x01
packet_name = "join game"
get_definition = staticmethod(lambda context: [
{'entity_id': Integer},
{'is_hardcore': Boolean} if context.protocol_version >= 738 else {},
{'game_mode': UnsignedByte},
{'previous_game_mode': UnsignedByte}
if context.protocol_version >= 730 else {},
{'world_names': PrefixedArray(VarInt, String)}
if context.protocol_version >= 722 else {},
{'dimension_codec': NBT}
if context.protocol_version >= 718 else {},
{'dimension':
NBT if context.protocol_version >= 748 else
String if context.protocol_version >= 718 else
Integer if context.protocol_version >= 108 else
Byte},
{'world_name': String} if context.protocol_version >= 722 else {},
{'hashed_seed': Long} if context.protocol_version >= 552 else {},
{'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'max_players':
VarInt if context.protocol_version >= 749 else UnsignedByte},
{'level_type': String} if context.protocol_version < 716 else {},
{'render_distance': VarInt} if context.protocol_version >= 468 else {},
{'reduced_debug_info': Boolean},
{'respawn_screen': Boolean} if context.protocol_version >= 571 else {},
{'is_debug': Boolean} if context.protocol_version >= 716 else {},
{'is_flat': Boolean} if context.protocol_version >= 716 else {},
])
# These aliases declare the Enum type corresponding to each field:
Difficulty = Difficulty
GameMode = GameMode
# Accesses the 'game_mode' field appropriately depending on the protocol.
# Can be set or deleted when 'context' is undefined.
@property
def game_mode(self):
if self.context.protocol_version >= 738:
return self._game_mode_738
else:
return self._game_mode_0
@game_mode.setter
def game_mode(self, value):
self._game_mode_738 = value
self._game_mode_0 = value
@game_mode.deleter
def game_mode(self):
del self._game_mode_738
del self._game_mode_0
# Accesses the 'is_hardcore' field, or its equivalent in older protocols.
# Can be set or deleted when 'context' is undefined.
@property
def is_hardcore(self):
if self.context.protocol_version >= 738:
return self._is_hardcore
else:
return bool(self._game_mode_0 & GameMode.HARDCORE)
@is_hardcore.setter
def is_hardcore(self, value):
self._is_hardcore = value
self._game_mode_0 = \
getattr(self, '_game_mode_0', 0) | GameMode.HARDCORE \
if value else \
getattr(self, '_game_mode_0', 0) & ~GameMode.HARDCORE
@is_hardcore.deleter
def is_hardcore(self):
if hasattr(self, '_is_hardcore'):
del self._is_hardcore
if hasattr(self, '_game_mode_0'):
self._game_mode_0 &= ~GameMode.HARDCORE
# Accesses the component of the 'game_mode' field without any hardcore bit,
# version-independently. Can be set or deleted when 'context' is undefined.
@property
def pure_game_mode(self):
if self.context.protocol_version >= 738:
return self._game_mode_738
else:
return self._game_mode_0 & ~GameMode.HARDCORE
@pure_game_mode.setter
def pure_game_mode(self, value):
self._game_mode_738 = value
self._game_mode_0 = \
value & ~GameMode.HARDCORE | \
getattr(self, '_game_mode_0', 0) & GameMode.HARDCORE
def field_string(self, field):
if field == 'dimension_codec':
# pylint: disable=no-member
return nbt_to_snbt(self.dimension_codec)
return super(JoinGamePacket, self).field_string(field)
class RespawnPacket(AbstractDimensionPacket):
@staticmethod
def get_id(context):
return 0x39 if context.protocol_version >= 741 else \
0x3A if context.protocol_version >= 721 else \
0x3B if context.protocol_version >= 550 else \
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':
NBT if context.protocol_version >= 748 else
String if context.protocol_version >= 718 else
Integer},
{'world_name': String} if context.protocol_version >= 719 else {},
{'difficulty': UnsignedByte} if context.protocol_version < 464 else {},
{'hashed_seed': Long} if context.protocol_version >= 552 else {},
{'game_mode': UnsignedByte},
{'previous_game_mode': UnsignedByte}
if context.protocol_version >= 730 else {},
{'level_type': String} if context.protocol_version < 716 else {},
{'is_debug': Boolean} if context.protocol_version >= 716 else {},
{'is_flat': Boolean} if context.protocol_version >= 716 else {},
{'copy_metadata': Boolean} if context.protocol_version >= 714 else {},
])
# These aliases declare the Enum type corresponding to each field:
Difficulty = Difficulty
GameMode = GameMode

View File

@ -8,8 +8,8 @@ from minecraft.networking.types import (
class MapPacket(Packet):
@staticmethod
def get_id(context):
return 0x25 if context.protocol_version >= 751 else \
0x26 if context.protocol_version >= 722 else \
return 0x25 if context.protocol_version >= 741 else \
0x26 if context.protocol_version >= 721 else \
0x27 if context.protocol_version >= 550 else \
0x26 if context.protocol_version >= 389 else \
0x25 if context.protocol_version >= 345 else \

View File

@ -9,8 +9,8 @@ from minecraft.networking.types import (
class PlayerListItemPacket(Packet):
@staticmethod
def get_id(context):
return 0x32 if context.protocol_version >= 751 else \
0x33 if context.protocol_version >= 722 else \
return 0x32 if context.protocol_version >= 741 else \
0x33 if context.protocol_version >= 721 else \
0x34 if context.protocol_version >= 550 else \
0x33 if context.protocol_version >= 471 else \
0x31 if context.protocol_version >= 451 else \

View File

@ -9,8 +9,8 @@ from minecraft.networking.types import (
class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
@staticmethod
def get_id(context):
return 0x34 if context.protocol_version >= 751 else \
0x35 if context.protocol_version >= 722 else \
return 0x34 if context.protocol_version >= 741 else \
0x35 if context.protocol_version >= 721 else \
0x36 if context.protocol_version >= 550 else \
0x35 if context.protocol_version >= 471 else \
0x33 if context.protocol_version >= 451 else \

View File

@ -9,7 +9,7 @@ __all__ = 'SoundEffectPacket',
class SoundEffectPacket(Packet):
@staticmethod
def get_id(context):
return 0x51 if context.protocol_version >= 722 else \
return 0x51 if context.protocol_version >= 721 else \
0x52 if context.protocol_version >= 550 else \
0x51 if context.protocol_version >= 471 else \
0x4D if context.protocol_version >= 461 else \

View File

@ -5,7 +5,7 @@ from minecraft.networking.packets import (
from minecraft.networking.types import (
Double, Float, Boolean, VarInt, String, Byte, Position, Enum,
RelativeHand, BlockFace, Vector, Direction, PositionAndLook,
multi_attribute_alias, Short
multi_attribute_alias,
)
from .client_settings_packet import ClientSettingsPacket
@ -126,7 +126,8 @@ class TeleportConfirmPacket(Packet):
class AnimationPacket(Packet):
@staticmethod
def get_id(context):
return 0x2B if context.protocol_version >= 719 else \
return 0x2C if context.protocol_version >= 738 else \
0x2B if context.protocol_version >= 712 else \
0x2A if context.protocol_version >= 468 else \
0x29 if context.protocol_version >= 464 else \
0x27 if context.protocol_version >= 389 else \
@ -200,7 +201,8 @@ class PlayerBlockPlacementPacket(Packet):
@staticmethod
def get_id(context):
return 0x2D if context.protocol_version >= 712 else \
return 0x2E if context.protocol_version >= 738 else \
0x2D if context.protocol_version >= 712 else \
0x2C if context.protocol_version >= 468 else \
0x2B if context.protocol_version >= 464 else \
0x29 if context.protocol_version >= 389 else \
@ -238,7 +240,8 @@ class PlayerBlockPlacementPacket(Packet):
class UseItemPacket(Packet):
@staticmethod
def get_id(context):
return 0x2E if context.protocol_version >= 719 else \
return 0x2F if context.protocol_version >= 738 else \
0x2E if context.protocol_version >= 712 else \
0x2D if context.protocol_version >= 468 else \
0x2C if context.protocol_version >= 464 else \
0x2A if context.protocol_version >= 389 else \

View File

@ -4,29 +4,33 @@ These definitions and methods are used by the packet definitions
"""
import struct
import uuid
import io
from .utility import Vector
import pynbt
from .utility import Vector, class_and_instancemethod
__all__ = (
'Type', 'Boolean', 'UnsignedByte', 'Byte', 'Short', 'UnsignedShort',
'Integer', 'FixedPointInteger', 'Angle', 'VarInt', 'Long',
'Integer', 'FixedPointInteger', 'Angle', 'VarInt', 'VarLong', 'Long',
'UnsignedLong', 'Float', 'Double', 'ShortPrefixedByteArray',
'VarIntPrefixedByteArray', 'TrailingByteArray', 'String', 'UUID',
'Position',
'Position', 'NBT', 'PrefixedArray',
)
class Type(object):
# pylint: disable=no-self-argument
__slots__ = ()
@classmethod
def read_with_context(cls, file_object, _context):
return cls.read(file_object)
@class_and_instancemethod
def read_with_context(cls_or_self, file_object, _context):
return cls_or_self.read(file_object)
@classmethod
def send_with_context(cls, value, socket, _context):
return cls.send(value, socket)
@class_and_instancemethod
def send_with_context(cls_or_self, value, socket, _context):
return cls_or_self.send(value, socket)
@classmethod
def read(cls, file_object):
@ -130,12 +134,13 @@ class Angle(Type):
class VarInt(Type):
@staticmethod
def read(file_object):
max_bytes = 5
@classmethod
def read(cls, file_object):
number = 0
# Limit of 5 bytes, otherwise its possible to cause
# a DOS attack by sending VarInts that just keep
# going
# Limit of 'cls.max_bytes' bytes, otherwise its possible to cause
# a DOS attack by sending VarInts that just keep going
bytes_encountered = 0
while True:
byte = file_object.read(1)
@ -148,7 +153,7 @@ class VarInt(Type):
break
bytes_encountered += 1
if bytes_encountered > 5:
if bytes_encountered > cls.max_bytes:
raise ValueError("Tried to read too long of a VarInt")
return number
@ -171,6 +176,10 @@ class VarInt(Type):
raise ValueError("Integer too large")
class VarLong(VarInt):
max_bytes = 10
# Maps (maximum integer value -> size of VarInt in bytes)
VARINT_SIZE_TABLE = {
2 ** 7: 1,
@ -323,3 +332,48 @@ class Position(Type, Vector):
if context.protocol_version >= 443 else
(x & 0x3FFFFFF) << 38 | (y & 0xFFF) << 26 | (z & 0x3FFFFFF))
UnsignedLong.send(value, socket)
class NBT(Type):
@staticmethod
def read(file_object):
return pynbt.NBTFile(io=file_object)
@staticmethod
def send(value, socket):
buffer = io.BytesIO()
pynbt.NBTFile(value=value).save(buffer)
socket.send(buffer.getvalue())
class PrefixedArray(Type):
__slots__ = 'length_type', 'element_type'
def __init__(self, length_type, element_type):
self.length_type = length_type
self.element_type = element_type
def read(self, file_object):
return self.__read(file_object, self.element_type.read)
def send(self, value, socket):
return self.__send(value, socket, self.element_type.send)
def read_with_context(self, file_object, context):
def element_read(file_object):
return self.element_type.read_with_context(file_object, context)
return self.__read(file_object, element_read)
def send_with_context(self, value, socket, context):
def element_send(value, socket):
return self.element_type.send_with_context(value, socket, context)
return self.__send(value, socket, element_send)
def __read(self, file_object, element_read):
length = self.length_type.read(file_object)
return [element_read(file_object) for i in range(length)]
def __send(self, value, socket, element_send):
self.length_type.send(len(value), socket)
for element in value:
element_send(element, socket)

View File

@ -99,13 +99,22 @@ class Dimension(Enum):
OVERWORLD = 0
END = 1
from_identifier_dict = {
'minecraft:the_nether': NETHER,
'minecraft:overworld': OVERWORLD,
'minecraft:the_end': END,
}
to_identifier_dict = {e: i for (i, e) in from_identifier_dict.items()}
# Designation of a player's gamemode.
class GameMode(Enum):
class GameMode(BitFieldEnum):
SURVIVAL = 0
CREATIVE = 1
ADVENTURE = 2
SPECTATOR = 3
HARDCORE = 8 # Only used prior to protocol 738.
# Currently designates an entity's feet or eyes.

View File

@ -1,13 +1,16 @@
"""Minecraft data types that are used by packets, but don't have a specific
network representation.
"""
import types
from collections import namedtuple
from itertools import chain
__all__ = (
'Vector', 'MutableRecord', 'Direction', 'PositionAndLook', 'descriptor',
'attribute_alias', 'multi_attribute_alias',
'overridable_descriptor', 'overridable_property', 'attribute_alias',
'multi_attribute_alias',
)
@ -142,23 +145,58 @@ def multi_attribute_alias(container, *arg_names, **kwd_names):
return alias
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.
class overridable_descriptor:
"""As 'descriptor' (defined below), except that only a getter can be
defined, and the resulting descriptor has no '__set__' or '__delete__'
methods defined; hence, attributes defined via this class can be
overridden by attributes of instances of the class in which it occurs.
"""
__slots__ = '_fget', '_fset', '_fdel'
__slots__ = '_fget',
def __init__(self, fget=None, fset=None, fdel=None):
def __init__(self, fget=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
@staticmethod
def _default_get(instance, owner):
raise AttributeError('unreadable attribute')
def __get__(self, instance, owner):
return self._fget(self, instance, owner)
class overridable_property(overridable_descriptor):
"""As the builtin 'property' decorator of Python, except that only
a getter is defined and the resulting descriptor is a non-data
descriptor, overridable by attributes of instances of the class
in which the property occurs. See also 'overridable_descriptor' above.
"""
def __get__(self, instance, _owner):
return self._fget(instance)
class descriptor(overridable_descriptor):
"""Behaves identically to the builtin 'property' decorator 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.
Since an instance of this class always havs '__set__' and '__delete__'
defined, it is a "data descriptor", so its binding behaviour cannot be
overridden in instances of the class in which it occurs. See
https://docs.python.org/3/reference/datamodel.html#descriptor-invocation
for more information. See also 'overridable_descriptor' above.
"""
__slots__ = '_fset', '_fdel'
def __init__(self, fget=None, fset=None, fdel=None):
super(descriptor, self).__init__(fget=fget)
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 setter(self, fset):
self._fset = fset
return self
@ -167,10 +205,6 @@ class descriptor(object):
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")
@ -179,9 +213,6 @@ class descriptor(object):
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)
@ -189,6 +220,26 @@ class descriptor(object):
return self._fdel(self, instance)
class class_and_instancemethod:
""" A decorator for functions defined in a class namespace which are to be
accessed as both class and instance methods: retrieving the method from
a class will return a bound class method (like the built-in
'classmethod' decorator), but retrieving the method from an instance
will return a bound instance method (as if the function were not
decorated). Therefore, the first argument of the decorated function may
be either a class or an instance, depending on how it was called.
"""
__slots__ = '_func',
def __init__(self, func):
self._func = func
def __get__(self, inst, owner=None):
bind_to = owner if inst is None else inst
return types.MethodType(self._func, bind_to)
Direction = namedtuple('Direction', ('yaw', 'pitch'))

View File

@ -1,2 +1,3 @@
cryptography>=1.5
requests
pynbt

View File

@ -1,3 +1,5 @@
import pynbt
from minecraft import SUPPORTED_MINECRAFT_VERSIONS
from minecraft.networking import connection
from minecraft.networking import types
@ -98,10 +100,46 @@ class FakeClientHandler(object):
def handle_play_start(self):
# Called upon entering the play state.
self.write_packet(clientbound.play.JoinGamePacket(
entity_id=0, game_mode=0, dimension=0, hashed_seed=12345,
difficulty=2, max_players=1, level_type='default',
reduced_debug_info=False, render_distance=9, respawn_screen=False))
packet = clientbound.play.JoinGamePacket(
entity_id=0, is_hardcore=False, game_mode=0, previous_game_mode=0,
world_names=['minecraft:overworld'],
world_name='minecraft:overworld',
hashed_seed=12345, difficulty=2, max_players=1,
level_type='default', reduced_debug_info=False, render_distance=9,
respawn_screen=False, is_debug=False, is_flat=False)
if self.server.context.protocol_version >= 748:
packet.dimension = pynbt.TAG_Compound({
'natural': pynbt.TAG_Byte(1),
'effects': pynbt.TAG_String('minecraft:overworld'),
}, '')
packet.dimension_codec = pynbt.TAG_Compound({
'minecraft:dimension_type': pynbt.TAG_Compound({
'type': pynbt.TAG_String('minecraft:dimension_type'),
'value': pynbt.TAG_List(pynbt.TAG_Compound, [
pynbt.TAG_Compound(packet.dimension),
]),
}),
'minecraft:worldgen/biome': pynbt.TAG_Compound({
'type': pynbt.TAG_String('minecraft:worldgen/biome'),
'value': pynbt.TAG_List(pynbt.TAG_Compound, [
pynbt.TAG_Compound({
'id': pynbt.TAG_Int(1),
'name': pynbt.TAG_String('minecraft:plains'),
}),
pynbt.TAG_Compound({
'id': pynbt.TAG_Int(2),
'name': pynbt.TAG_String('minecraft:desert'),
}),
]),
}),
}, '')
elif self.server.context.protocol_version >= 718:
packet.dimension = 'minecraft:overworld'
else:
packet.dimension = types.Dimension.OVERWORLD
self.write_packet(packet)
def handle_play_packet(self, packet):
# Called upon each packet received after handle_play_start() returns.
@ -178,7 +216,8 @@ class FakeClientHandler(object):
try:
self.handle_connection()
packet = self.read_packet()
assert isinstance(packet, serverbound.handshake.HandShakePacket)
assert isinstance(packet, serverbound.handshake.HandShakePacket), \
type(packet)
self.handle_handshake(packet)
if packet.next_state == 1:
self._run_status()

View File

@ -101,3 +101,56 @@ class ClassMemberAliasesTest(unittest.TestCase):
packet = clientbound.play.BlockChangePacket(blockId=bi, blockMeta=bm)
self.assertEqual((packet.blockId, packet.blockMeta), (bi, bm))
self.assertEqual(packet.blockStateId, packet.block_state_id)
def test_join_game_packet(self):
GameMode = types.GameMode
context = ConnectionContext()
for pure_game_mode in (GameMode.SURVIVAL, GameMode.CREATIVE,
GameMode.ADVENTURE, GameMode.SPECTATOR):
for is_hardcore in (False, True):
context.protocol_version = 70
game_mode = \
pure_game_mode | GameMode.HARDCORE \
if is_hardcore else pure_game_mode
packet = clientbound.play.JoinGamePacket()
packet.game_mode = game_mode
packet.context = context
self.assertEqual(packet.pure_game_mode, pure_game_mode)
self.assertEqual(packet.is_hardcore, is_hardcore)
del packet.context
del packet.is_hardcore
packet.context = context
self.assertEqual(packet.game_mode, packet.pure_game_mode)
del packet.context
del packet.game_mode
packet.context = context
self.assertFalse(hasattr(packet, 'is_hardcore'))
packet = clientbound.play.JoinGamePacket()
packet.pure_game_mode = game_mode
packet.is_hardcore = is_hardcore
packet.context = context
self.assertEqual(packet.game_mode, game_mode)
context.protocol_version = 738
game_mode = pure_game_mode | GameMode.HARDCORE
packet = clientbound.play.JoinGamePacket()
packet.game_mode = game_mode
packet.is_hardcore = is_hardcore
packet.context = context
self.assertEqual(packet.game_mode, game_mode)
self.assertEqual(packet.pure_game_mode, game_mode)
self.assertEqual(packet.is_hardcore, is_hardcore)
del packet.context
packet.is_hardcore = is_hardcore
packet.context = context
self.assertEqual(packet.game_mode, game_mode)
self.assertEqual(packet.pure_game_mode, game_mode)
with self.assertRaises(AttributeError):
del packet.pure_game_mode

View File

@ -186,11 +186,9 @@ class TestReadWritePackets(unittest.TestCase):
packet = clientbound.play.CombatEventPacket(
event=clientbound.play.CombatEventPacket.EndCombatEvent(
duration=415, entity_id=91063502))
self.assertEqual(
str(packet),
'CombatEventPacket(event=EndCombatEvent('
'duration=415, entity_id=91063502))'
)
self.assertEqual(str(packet),
'CombatEventPacket(event=EndCombatEvent('
'duration=415, entity_id=91063502))')
self._test_read_write_packet(packet)
packet = clientbound.play.CombatEventPacket(
@ -205,26 +203,32 @@ class TestReadWritePackets(unittest.TestCase):
def test_multi_block_change_packet(self):
Record = clientbound.play.MultiBlockChangePacket.Record
packet = clientbound.play.MultiBlockChangePacket(
chunk_x=167, chunk_z=15, records=[
Record(x=1, y=2, z=3, blockId=56, blockMeta=13),
Record(position=Vector(1, 2, 3), block_state_id=909),
Record(position=(1, 2, 3), blockStateId=909)])
self.assertEqual(packet.records[0].blockId, 56)
self.assertEqual(packet.records[0].blockMeta, 13)
self.assertEqual(packet.records[0].blockStateId, 909)
self.assertEqual(packet.records[0].position, Vector(1, 2, 3))
self.assertEqual(packet.chunk_pos, (packet.chunk_x, packet.chunk_z))
self.assertEqual(
str(packet),
'MultiBlockChangePacket(chunk_x=167, chunk_z=15, records=['
'Record(x=1, y=2, z=3, block_state_id=909), '
'Record(x=1, y=2, z=3, block_state_id=909), '
'Record(x=1, y=2, z=3, block_state_id=909)])'
)
for protocol_version in TEST_VERSIONS:
context = ConnectionContext()
context.protocol_version = protocol_version
packet = clientbound.play.MultiBlockChangePacket(context)
self._test_read_write_packet(packet)
if protocol_version >= 741:
packet.chunk_section_pos = Vector(167, 17, 33)
packet.invert_trust_edges = False
else:
packet.chunk_x, packet.chunk_z = 167, 17
self.assertEqual(packet.chunk_pos, (167, 17))
packet.records = [
Record(x=1, y=2, z=3, blockId=56, blockMeta=13),
Record(position=Vector(1, 2, 3), block_state_id=909),
Record(position=(1, 2, 3), blockStateId=909),
]
for i in range(3):
self.assertEqual(packet.records[i].blockId, 56)
self.assertEqual(packet.records[i].blockMeta, 13)
self.assertEqual(packet.records[i].blockStateId, 909)
self.assertEqual(packet.records[i].position, Vector(1, 2, 3))
self._test_read_write_packet(packet, context)
def test_spawn_object_packet(self):
for protocol_version in TEST_VERSIONS: