Add support for Minecraft 1.18 and 1.18.1.

This commit is contained in:
joodicator 2021-12-16 19:39:13 +01:00
parent 1ae9a2b48a
commit bf49006553
17 changed files with 322 additions and 75 deletions

View File

@ -1,11 +1,13 @@
language: python language: python
dist: focal dist: focal
python: 3.8 python: 3.9
matrix: matrix:
include: include:
- python: pypy3 - python: pypy3
env: TOX_ENV=pypy env: TOX_ENV=pypy
before_install:
- python -m pip install --upgrade virtualenv
- python: 3.5 - python: 3.5
env: TOX_ENV=py35 env: TOX_ENV=py35
- python: 3.6 - python: 3.6
@ -14,13 +16,15 @@ matrix:
env: TOX_ENV=py37 env: TOX_ENV=py37
- python: 3.8 - python: 3.8
env: TOX_ENV=py38 env: TOX_ENV=py38
- python: 3.8 - python: 3.9
env: TOX_ENV=py39
- python: 3.9
env: TOX_ENV=flake8 env: TOX_ENV=flake8
- python: 3.8 - python: 3.9
env: TOX_ENV=pylint-errors env: TOX_ENV=pylint-errors
- python: 3.8 - python: 3.9
env: TOX_ENV=pylint-full env: TOX_ENV=pylint-full
- python: 3.8 - python: 3.9
env: TOX_ENV=verify-manifest env: TOX_ENV=verify-manifest
before_install: before_install:
@ -32,6 +36,6 @@ install:
script: script:
- tox -e $TOX_ENV - tox -e $TOX_ENV
after_script: after_script:
- if [ "$TOX_ENV" = "py38" ]; then tox -e coveralls; fi - if [ "$TOX_ENV" = "py39" ]; then tox -e coveralls; fi
notifications: notifications:
email: false email: false

View File

@ -1,7 +1,7 @@
pyCraft pyCraft
======= =======
.. image:: https://travis-ci.org/ammaraskar/pyCraft.svg?branch=master .. image:: https://app.travis-ci.com/ammaraskar/pyCraft.svg?branch=master
:target: https://travis-ci.org/ammaraskar/pyCraft :target: https://app.travis-ci.com/github/ammaraskar/pyCraft
.. image:: https://readthedocs.org/projects/pycraft/badge/?version=latest .. image:: https://readthedocs.org/projects/pycraft/badge/?version=latest
:target: https://pycraft.readthedocs.org/en/latest :target: https://pycraft.readthedocs.org/en/latest
.. image:: https://coveralls.io/repos/ammaraskar/pyCraft/badge.svg?branch=master .. image:: https://coveralls.io/repos/ammaraskar/pyCraft/badge.svg?branch=master
@ -31,7 +31,9 @@ pyCraft is compatible with the following Minecraft releases:
* 1.13, 1.13.1, 1.13.2 * 1.13, 1.13.1, 1.13.2
* 1.14, 1.14.1, 1.14.2, 1.14.3, 1.14.4 * 1.14, 1.14.1, 1.14.2, 1.14.3, 1.14.4
* 1.15, 1.15.1, 1.15.2 * 1.15, 1.15.1, 1.15.2
* 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4 * 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5
* 1.17, 1.17.1
* 1.18, 1.18.1
In addition, some development snapshots and pre-release versions are supported: In addition, some development snapshots and pre-release versions are supported:
`<minecraft/__init__.py>`_ contains a full list of supported Minecraft versions `<minecraft/__init__.py>`_ contains a full list of supported Minecraft versions
@ -55,6 +57,7 @@ pyCraft is compatible with (at least) the following Python implementations:
* Python 3.6 * Python 3.6
* Python 3.7 * Python 3.7
* Python 3.8 * Python 3.8
* Python 3.9
* PyPy * PyPy
Requirements Requirements

View File

@ -460,8 +460,21 @@ KNOWN_MINECRAFT_VERSION_RECORDS = [
Version('20w45a', PRE | 5, True), Version('20w45a', PRE | 5, True),
Version('20w46a', PRE | 6, True), Version('20w46a', PRE | 6, True),
Version('20w48a', PRE | 7, True), Version('20w48a', PRE | 7, True),
Version('20w49a', PRE | 8, False),
Version('20w51a', PRE | 9, False),
Version('1.16.5', 754, False),
Version('21w03a', PRE | 11, False),
Version('21w05a', PRE | 12, False),
Version('21w05b', PRE | 13, False),
Version('21w06a', PRE | 14, False),
Version('21w07a', PRE | 15, False),
Version('1.17-rc2', PRE | 35, False),
Version('1.17', 755, True), Version('1.17', 755, True),
Version('1.17.1', 756, True), Version('1.17.1', 756, True),
Version('21w44a', PRE | 48, False),
Version('1.18-rc4', PRE | 60, False),
Version('1.18', 757, True),
Version('1.18.1', 757, True),
] ]
# An OrderedDict mapping the id string of each known Minecraft version to its # An OrderedDict mapping the id string of each known Minecraft version to its

View File

@ -53,6 +53,13 @@ class ConnectionContext(object):
later than, or is equal to, 'other_pv', or else False.""" later than, or is equal to, 'other_pv', or else False."""
return utility.protocol_earlier_eq(other_pv, self.protocol_version) return utility.protocol_earlier_eq(other_pv, self.protocol_version)
def protocol_in_range(self, start_pv, end_pv):
"""Returns True if the protocol version of this context was published
later than, or is equal to, 'start_pv' and was published earlier
than 'end_pv' (analogously to Python's 'range' function)."""
return (utility.protocol_earlier(self.protocol_version, end_pv) and
utility.protocol_earlier_eq(start_pv, self.protocol_version))
class _ConnectionOptions(object): class _ConnectionOptions(object):
def __init__(self, address=None, port=None, compression_threshold=-1, def __init__(self, address=None, port=None, compression_threshold=-1,

View File

@ -1,3 +1,4 @@
from minecraft import PRE
from minecraft.networking.packets import ( from minecraft.networking.packets import (
Packet, AbstractKeepAlivePacket, AbstractPluginMessagePacket Packet, AbstractKeepAlivePacket, AbstractPluginMessagePacket
) )
@ -8,7 +9,10 @@ from minecraft.networking.types import (
PositionAndLook, multi_attribute_alias, attribute_transform, PositionAndLook, multi_attribute_alias, attribute_transform,
) )
from .combat_event_packet import CombatEventPacket from .combat_event_packet import (
CombatEventPacket, EnterCombatEventPacket, EndCombatEventPacket,
DeathCombatEventPacket,
)
from .map_packet import MapPacket from .map_packet import MapPacket
from .player_list_item_packet import PlayerListItemPacket from .player_list_item_packet import PlayerListItemPacket
from .player_position_and_look_packet import PlayerPositionAndLookPacket from .player_position_and_look_packet import PlayerPositionAndLookPacket
@ -36,7 +40,6 @@ def get_packets(context):
EntityPositionDeltaPacket, EntityPositionDeltaPacket,
TimeUpdatePacket, TimeUpdatePacket,
UpdateHealthPacket, UpdateHealthPacket,
CombatEventPacket,
ExplosionPacket, ExplosionPacket,
SpawnObjectPacket, SpawnObjectPacket,
BlockChangePacket, BlockChangePacket,
@ -47,18 +50,33 @@ def get_packets(context):
EntityLookPacket, EntityLookPacket,
ResourcePackSendPacket ResourcePackSendPacket
} }
if context.protocol_earlier_eq(47): if context.protocol_earlier_eq(47):
packets |= { packets |= {
SetCompressionPacket, SetCompressionPacket,
} }
if context.protocol_earlier(PRE | 15):
packets |= {
CombatEventPacket,
}
else:
packets |= {
EnterCombatEventPacket,
EndCombatEventPacket,
DeathCombatEventPacket,
}
if context.protocol_later_eq(94): if context.protocol_later_eq(94):
packets |= { packets |= {
SoundEffectPacket, SoundEffectPacket,
} }
if context.protocol_later_eq(352): if context.protocol_later_eq(352):
packets |= { packets |= {
FacePlayerPacket FacePlayerPacket
} }
return packets return packets
@ -150,7 +168,7 @@ class SetCompressionPacket(Packet):
def get_id(context): def get_id(context):
return 0x03 if context.protocol_later_eq(755) else \ return 0x03 if context.protocol_later_eq(755) else \
0x46 0x46
packet_name = "set compression" packet_name = "set compression"
definition = [ definition = [
{'threshold': VarInt}] {'threshold': VarInt}]
@ -264,7 +282,8 @@ class EntityPositionDeltaPacket(Packet):
class TimeUpdatePacket(Packet): class TimeUpdatePacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x58 if context.protocol_later_eq(755) else \ return 0x59 if context.protocol_later_eq(PRE | 48) else \
0x58 if context.protocol_later_eq(755) else \
0x4E if context.protocol_later_eq(721) else \ 0x4E if context.protocol_later_eq(721) else \
0x4F if context.protocol_later_eq(550) else \ 0x4F if context.protocol_later_eq(550) else \
0x4E if context.protocol_later_eq(471) else \ 0x4E if context.protocol_later_eq(471) else \
@ -332,7 +351,8 @@ class PluginMessagePacket(AbstractPluginMessagePacket):
class PlayerListHeaderAndFooterPacket(Packet): class PlayerListHeaderAndFooterPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x5E if context.protocol_later_eq(755) else \ return 0x5F if context.protocol_later_eq(PRE | 48) else \
0x5E if context.protocol_later_eq(755) else \
0x53 if context.protocol_later_eq(721) else \ 0x53 if context.protocol_later_eq(721) else \
0x54 if context.protocol_later_eq(550) else \ 0x54 if context.protocol_later_eq(550) else \
0x53 if context.protocol_later_eq(471) else \ 0x53 if context.protocol_later_eq(471) else \
@ -374,16 +394,35 @@ class EntityLookPacket(Packet):
{'on_ground': Boolean} {'on_ground': Boolean}
] ]
class ResourcePackSendPacket(Packet): class ResourcePackSendPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x3C if context.protocol_later_eq(755) else \ return 0x3C if context.protocol_later_eq(PRE | 15) else \
0x38 0x39 if context.protocol_later_eq(PRE | 8) else \
0x38 if context.protocol_later_eq(741) else \
0x39 if context.protocol_later_eq(721) else \
0x3A if context.protocol_later_eq(550) else \
0x39 if context.protocol_later_eq(471) else \
0x37 if context.protocol_later_eq(461) else \
0x38 if context.protocol_later_eq(451) else \
0x37 if context.protocol_later_eq(389) else \
0x36 if context.protocol_later_eq(352) else \
0x35 if context.protocol_later_eq(345) else \
0x34 if context.protocol_later_eq(336) else \
0x33 if context.protocol_later_eq(332) else \
0x34 if context.protocol_later_eq(318) else \
0x32 if context.protocol_later_eq(70) else \
0x48
packet_name = "resource pack send" packet_name = "resource pack send"
definition = [
{"url": String}, @staticmethod
{"hash": String}, def get_definition(context):
{"forced": Boolean}, return [
{"forced_message": String} {"url": String},
] {"hash": String},
{"forced": Boolean} if context.protocol_later_eq(PRE | 5) else {},
{"forced_message": String}
if context.protocol_later_eq(PRE | 15) else {},
]

View File

@ -1,3 +1,6 @@
from abc import ABCMeta, abstractmethod
from minecraft import PRE
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types import ( from minecraft.networking.types import (
@ -5,10 +8,16 @@ from minecraft.networking.types import (
) )
# Note: this packet was removed in Minecraft 21w07a (protocol PRE|15)
# and replaced with the separate EnterCombatEvent, EndCombatEvent, and
# DeathCombatEvent packets. These are subclasses of CombatEventPacket, so
# that code written to listen for CombatEventPacket instances should in most
# cases continue to work without modification.
class CombatEventPacket(Packet): class CombatEventPacket(Packet):
@staticmethod @classmethod
def get_id(context): def get_id(cls, context):
return 0x31 if context.protocol_later_eq(741) else \ return cls.deprecated() if context.protocol_later_eq(PRE | 15) else \
0x31 if context.protocol_later_eq(741) else \
0x32 if context.protocol_later_eq(721) else \ 0x32 if context.protocol_later_eq(721) else \
0x33 if context.protocol_later_eq(550) else \ 0x33 if context.protocol_later_eq(550) else \
0x32 if context.protocol_later_eq(471) else \ 0x32 if context.protocol_later_eq(471) else \
@ -28,19 +37,19 @@ class CombatEventPacket(Packet):
fields = 'event', fields = 'event',
# The abstract type of the 'event' field of this packet. # The abstract type of the 'event' field of this packet.
class EventType(MutableRecord): class EventType(MutableRecord, metaclass=ABCMeta):
__slots__ = () __slots__ = ()
type_from_id_dict = {} type_from_id_dict = {}
# Read the fields of the event (not including the ID) from the file. # Read the fields of the event (not including the ID) from the file.
@abstractmethod
def read(self, file_object): def read(self, file_object):
raise NotImplementedError( pass
'This abstract method must be overridden in a subclass.')
# Write the fields of the event (not including the ID) to the buffer. # Write the fields of the event (not including the ID) to the buffer.
@abstractmethod
def write(self, packet_buffer): def write(self, packet_buffer):
raise NotImplementedError( pass
'This abstract method must be overridden in a subclass.')
@classmethod @classmethod
def type_from_id(cls, event_id): def type_from_id(cls, event_id):
@ -89,10 +98,72 @@ class CombatEventPacket(Packet):
EventType.type_from_id_dict[EntityDeadEvent.id] = EntityDeadEvent EventType.type_from_id_dict[EntityDeadEvent.id] = EntityDeadEvent
def read(self, file_object): def read(self, file_object):
if self.context and self.context.protocol_later_eq(PRE | 15):
self.deprecated()
event_id = VarInt.read(file_object) event_id = VarInt.read(file_object)
self.event = CombatEventPacket.EventType.type_from_id(event_id)() self.event = CombatEventPacket.EventType.type_from_id(event_id)()
self.event.read(file_object) self.event.read(file_object)
def write_fields(self, packet_buffer): def write_fields(self, packet_buffer):
if self.context and self.context.protocol_later_eq(PRE | 15):
self.deprecated()
VarInt.send(self.event.id, packet_buffer) VarInt.send(self.event.id, packet_buffer)
self.event.write(packet_buffer) self.event.write(packet_buffer)
@staticmethod
def deprecated():
raise NotImplementedError(
'`CombatEventPacket` was removed in Minecraft snapshot 21w07a '
'(protocol version 2**30 + 15). In this and later versions, one '
'of the subclasses '
+ repr(SpecialisedCombatEventPacket.__subclasses__()) + ' must be '
'used directly for usage like that which generates this message.')
# Contains the behaviour common to all concrete CombatEventPacket subclasses
class SpecialisedCombatEventPacket(CombatEventPacket):
def __init__(self, *args, **kwds):
super(SpecialisedCombatEventPacket, self).__init__(*args, **kwds)
# Prior to Minecraft 21w07a, instances of CombatEventPacket had a
# single 'event' field giving a 'MutableRecord' of one of three types
# corresponding to the type of combat event represented. For backward
# compatibility, we here present a similar interface, giving the packet
# object itself as the 'event', which should work identically in most
# use cases, since it is a virtual subclass of, and has attributes of
# the same names and contents as those of, the previous event records.
self.event = self
# The 'get_id', 'fields', 'read', and 'write_fields' attributes of the
# 'Packet' base class are all overridden in 'CombatEventPacket'. We desire
# the default behaviour of these attributes, so we restore them here:
get_id = Packet.__dict__['get_id']
fields = Packet.__dict__['fields']
read = Packet.__dict__['read']
write_fields = Packet.__dict__['write_fields']
@CombatEventPacket.EnterCombatEvent.register # virtual subclass
class EnterCombatEventPacket(SpecialisedCombatEventPacket):
packet_name = 'enter combat event'
id = 0x34
definition = []
@CombatEventPacket.EndCombatEvent.register # virtual subclass
class EndCombatEventPacket(SpecialisedCombatEventPacket):
packet_name = 'end combat event'
id = 0x33
definition = [
{'duration': VarInt},
{'entity_id': Integer}]
@CombatEventPacket.EntityDeadEvent.register # virtual subclass
class DeathCombatEventPacket(SpecialisedCombatEventPacket):
packet_name = 'death combat event'
id = 0x35
definition = [
{'player_id': VarInt},
{'entity_id': Integer},
{'message': String}]

View File

@ -1,5 +1,6 @@
from minecraft.networking.types import ( from minecraft.networking.types import (
Vector, Float, Byte, Integer, PrefixedArray, multi_attribute_alias, Type, Vector, Float, Byte, Integer, PrefixedArray, multi_attribute_alias, Type,
VarInt,
) )
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
@ -34,15 +35,22 @@ class ExplosionPacket(Packet):
for coord in record: for coord in record:
Byte.send(coord, socket) Byte.send(coord, socket)
definition = [ @staticmethod
{'x': Float}, def get_definition(context):
{'y': Float}, return [
{'z': Float}, {'x': Float},
{'radius': Float}, {'y': Float},
{'records': PrefixedArray(Integer, Record)}, {'z': Float},
{'player_motion_x': Float}, {'radius': Float},
{'player_motion_y': Float},
{'player_motion_z': Float}] {'records': PrefixedArray(VarInt, ExplosionPacket.Record)}
if context.protocol_later_eq(755) else
{'records': PrefixedArray(Integer, ExplosionPacket.Record)},
{'player_motion_x': Float},
{'player_motion_y': Float},
{'player_motion_z': Float},
]
# Access the 'x', 'y', 'z' fields as a Vector tuple. # Access the 'x', 'y', 'z' fields as a Vector tuple.
position = multi_attribute_alias(Vector, 'x', 'y', 'z') position = multi_attribute_alias(Vector, 'x', 'y', 'z')

View File

@ -92,6 +92,8 @@ class JoinGamePacket(AbstractDimensionPacket):
VarInt if context.protocol_later_eq(749) else UnsignedByte}, VarInt if context.protocol_later_eq(749) else UnsignedByte},
{'level_type': String} if context.protocol_earlier(716) else {}, {'level_type': String} if context.protocol_earlier(716) else {},
{'render_distance': VarInt} if context.protocol_later_eq(468) else {}, {'render_distance': VarInt} if context.protocol_later_eq(468) else {},
{'simulation_distance': VarInt}
if context.protocol_later_eq(757) else {},
{'reduced_debug_info': Boolean}, {'reduced_debug_info': Boolean},
{'respawn_screen': Boolean} if context.protocol_later_eq(571) else {}, {'respawn_screen': Boolean} if context.protocol_later_eq(571) else {},
{'is_debug': Boolean} if context.protocol_later_eq(716) else {}, {'is_debug': Boolean} if context.protocol_later_eq(716) else {},

View File

@ -1,3 +1,4 @@
from minecraft import PRE
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, Byte, Boolean, UnsignedByte, VarIntPrefixedByteArray, String, VarInt, Byte, Boolean, UnsignedByte, VarIntPrefixedByteArray, String,
@ -72,9 +73,9 @@ class MapPacket(Packet):
self.map_id = VarInt.read(file_object) self.map_id = VarInt.read(file_object)
self.scale = Byte.read(file_object) self.scale = Byte.read(file_object)
if self.context.protocol_later_eq(107): if self.context.protocol_in_range(107, PRE | 6):
self.is_tracking_position = Boolean.read(file_object) self.is_tracking_position = Boolean.read(file_object)
else: elif self.context.protocol_earlier(107):
self.is_tracking_position = True self.is_tracking_position = True
if self.context.protocol_later_eq(452): if self.context.protocol_later_eq(452):
@ -82,6 +83,9 @@ class MapPacket(Packet):
else: else:
self.is_locked = False self.is_locked = False
if self.context.protocol_later_eq(PRE | 6):
self.is_tracking_position = Boolean.read(file_object)
icon_count = VarInt.read(file_object) icon_count = VarInt.read(file_object)
self.icons = [] self.icons = []
for i in range(icon_count): for i in range(icon_count):
@ -101,10 +105,7 @@ class MapPacket(Packet):
icon = MapPacket.MapIcon(type, direction, (x, z), display_name) icon = MapPacket.MapIcon(type, direction, (x, z), display_name)
self.icons.append(icon) self.icons.append(icon)
try: self.width = UnsignedByte.read(file_object)
self.width = UnsignedByte.read(file_object)
except:
self.width = None
if self.width: if self.width:
self.height = UnsignedByte.read(file_object) self.height = UnsignedByte.read(file_object)
x = Byte.read(file_object) x = Byte.read(file_object)

View File

@ -1,3 +1,4 @@
from minecraft import PRE
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, String, Float, Byte, Type, Integer, Vector, Enum, VarInt, String, Float, Byte, Type, Integer, Vector, Enum,
@ -9,7 +10,8 @@ __all__ = 'SoundEffectPacket',
class SoundEffectPacket(Packet): class SoundEffectPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x5C if context.protocol_later_eq(755) else \ return 0x5D if context.protocol_later_eq(PRE | 48) else \
0x5C if context.protocol_later_eq(755) else \
0x51 if context.protocol_later_eq(721) else \ 0x51 if context.protocol_later_eq(721) else \
0x52 if context.protocol_later_eq(550) else \ 0x52 if context.protocol_later_eq(550) else \
0x51 if context.protocol_later_eq(471) else \ 0x51 if context.protocol_later_eq(471) else \

View File

@ -268,11 +268,12 @@ class UseItemPacket(Packet):
Hand = RelativeHand Hand = RelativeHand
class ResourcePackStatusPacket(Packet): class ResourcePackStatusPacket(Packet):
@staticmethod @staticmethod
def get_id(context): def get_id(context):
return 0x21 return 0x21
packet_name = "resource pask status" packet_name = "resource pack status"
definition = [ definition = [
{"result": VarInt} {"result": VarInt}
] ]

View File

@ -1,8 +1,11 @@
import operator
from minecraft.networking.packets import Packet from minecraft.networking.packets import Packet
from minecraft.networking.types import ( from minecraft.networking.types import (
String, Byte, VarInt, Boolean, UnsignedByte, Enum, BitFieldEnum, String, Byte, VarInt, Boolean, UnsignedByte, Enum, BitFieldEnum,
AbsoluteHand AbsoluteHand
) )
from minecraft.utility import attribute_transform
class ClientSettingsPacket(Packet): class ClientSettingsPacket(Packet):
@ -18,13 +21,40 @@ class ClientSettingsPacket(Packet):
packet_name = 'client settings' packet_name = 'client settings'
get_definition = staticmethod(lambda context: [ @staticmethod
{'locale': String}, def get_definition(context):
{'view_distance': Byte}, return [
{'chat_mode': VarInt if context.protocol_later(47) else Byte}, {'locale': String},
{'chat_colors': Boolean}, {'view_distance': Byte},
{'displayed_skin_parts': UnsignedByte}, {'chat_mode': VarInt if context.protocol_later(47) else Byte},
{'main_hand': VarInt} if context.protocol_later(49) else {}]) {'chat_colors': Boolean},
{'displayed_skin_parts': UnsignedByte},
{'main_hand': VarInt} if context.protocol_later(49) else {},
{'enable_text_filtering': Boolean}
if context.protocol_later_eq(757) else
{'disable_text_filtering': Boolean}
if context.protocol_later_eq(755) else {},
{'allow_server_listings': Boolean}
if context.protocol_later_eq(755) else {},
]
# Set a default value for 'enable_text_filtering', because most clients
# will probably want this value, and to avoid breaking old code.
enable_text_filtering = False
# To support the possibility of both 'enable_text_filtering' and
# 'disable_text_filtering' fields existing, make 'disable_text_filtering'
# (which is the less likely to be used of the two) into a property that
# stores the negation of its value in the ordinary attribute
# 'enable_text_filtering'.
disable_text_filtering = attribute_transform(
'enable_text_filtering', operator.not_, operator.not_)
# Set a default value for 'allow_server_listings', because most clients
# will probably want this value, and to avoid breaking old code.
allow_server_listings = False
field_enum = classmethod( field_enum = classmethod(
lambda cls, field, context: { lambda cls, field, context: {

View File

@ -33,7 +33,20 @@ def attribute_alias(name):
"""An attribute descriptor that redirects access to a different attribute """An attribute descriptor that redirects access to a different attribute
with a given name. with a given name.
""" """
return attribute_transform(name, lambda x: x, lambda x: x) return property(
fget=(lambda self: getattr(self, name)),
fset=(lambda self, value: setattr(self, name, value)),
fdel=(lambda self: delattr(self, name)))
def partial_attribute_alias(name, part):
"""An attribute descriptor that redirects access to a particular named
attribute, 'part', on a different attribute with a given name.
"""
return property(
fget=(lambda self: getattr(getattr(self, name), part)),
fset=(lambda self, value: setattr(getattr(self, name), part, value)),
fdel=(lambda self: delattr(getattr(self, name), part)))
def multi_attribute_alias(container, *arg_names, **kwd_names): def multi_attribute_alias(container, *arg_names, **kwd_names):

View File

@ -115,7 +115,8 @@ class FakeClientHandler(object):
world_name='minecraft:overworld', world_name='minecraft:overworld',
hashed_seed=12345, difficulty=2, max_players=1, hashed_seed=12345, difficulty=2, max_players=1,
level_type='default', reduced_debug_info=False, render_distance=9, level_type='default', reduced_debug_info=False, render_distance=9,
respawn_screen=False, is_debug=False, is_flat=False) simulation_distance=9, respawn_screen=False, is_debug=False,
is_flat=False)
if self.server.context.protocol_later_eq(748): if self.server.context.protocol_later_eq(748):
packet.dimension = pynbt.TAG_Compound({ packet.dimension = pynbt.TAG_Compound({

View File

@ -6,7 +6,10 @@ import struct
from zlib import decompress from zlib import decompress
from random import choice from random import choice
from minecraft import SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS from minecraft.utility import protocol_earlier
from minecraft import (
PRE, SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS,
)
from minecraft.networking.connection import ConnectionContext from minecraft.networking.connection import ConnectionContext
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, Enum, Vector, PositionAndLook, OriginPoint, VarInt, Enum, Vector, PositionAndLook, OriginPoint,
@ -157,19 +160,21 @@ class TestReadWritePackets(unittest.TestCase):
maxDiff = None maxDiff = None
def test_explosion_packet(self): def test_explosion_packet(self):
context = ConnectionContext(protocol_version=TEST_VERSIONS[-1])
Record = clientbound.play.ExplosionPacket.Record Record = clientbound.play.ExplosionPacket.Record
packet = clientbound.play.ExplosionPacket( packet = clientbound.play.ExplosionPacket(
position=Vector(787, -37, 0), radius=15, position=Vector(787, -37, 0), radius=15,
records=[Record(-14, -116, -5), Record(-77, 34, -36), records=[Record(-14, -116, -5), Record(-77, 34, -36),
Record(-35, -127, 95), Record(11, 113, -8)], Record(-35, -127, 95), Record(11, 113, -8)],
player_motion=Vector(4, 5, 0)) player_motion=Vector(4, 5, 0), context=context)
self.assertEqual( self.assertEqual(
str(packet), str(packet),
'ExplosionPacket(x=787, y=-37, z=0, radius=15, records=[' '0x%02X ExplosionPacket(x=787, y=-37, z=0, radius=15, records=['
'Record(-14, -116, -5), Record(-77, 34, -36), ' 'Record(-14, -116, -5), Record(-77, 34, -36), '
'Record(-35, -127, 95), Record(11, 113, -8)], ' 'Record(-35, -127, 95), Record(11, 113, -8)], '
'player_motion_x=4, player_motion_y=5, player_motion_z=0)' 'player_motion_x=4, player_motion_y=5, player_motion_z=0)'
% packet.id
) )
self._test_read_write_packet(packet) self._test_read_write_packet(packet)
@ -181,7 +186,16 @@ class TestReadWritePackets(unittest.TestCase):
str(packet), str(packet),
'CombatEventPacket(event=EnterCombatEvent())' 'CombatEventPacket(event=EnterCombatEvent())'
) )
self._test_read_write_packet(packet) self._test_read_write_packet(packet, vmax=PRE | 14)
with self.assertRaises(NotImplementedError):
self._test_read_write_packet(packet, vmin=PRE | 15)
specialised_packet = clientbound.play.EnterCombatEventPacket()
self.assertIsInstance(specialised_packet.event, type(packet.event))
for field in specialised_packet.fields:
value = getattr(packet.event, field)
setattr(specialised_packet, field, value)
self.assertEqual(getattr(specialised_packet.event, field), value)
packet = clientbound.play.CombatEventPacket( packet = clientbound.play.CombatEventPacket(
event=clientbound.play.CombatEventPacket.EndCombatEvent( event=clientbound.play.CombatEventPacket.EndCombatEvent(
@ -189,7 +203,16 @@ class TestReadWritePackets(unittest.TestCase):
self.assertEqual(str(packet), self.assertEqual(str(packet),
'CombatEventPacket(event=EndCombatEvent(' 'CombatEventPacket(event=EndCombatEvent('
'duration=415, entity_id=91063502))') 'duration=415, entity_id=91063502))')
self._test_read_write_packet(packet) self._test_read_write_packet(packet, vmax=PRE | 14)
with self.assertRaises(NotImplementedError):
self._test_read_write_packet(packet, vmin=PRE | 15)
specialised_packet = clientbound.play.EndCombatEventPacket()
self.assertIsInstance(specialised_packet.event, type(packet.event))
for field in specialised_packet.fields:
value = getattr(packet.event, field)
setattr(specialised_packet, field, value)
self.assertEqual(getattr(specialised_packet.event, field), value)
packet = clientbound.play.CombatEventPacket( packet = clientbound.play.CombatEventPacket(
event=clientbound.play.CombatEventPacket.EntityDeadEvent( event=clientbound.play.CombatEventPacket.EntityDeadEvent(
@ -199,7 +222,16 @@ class TestReadWritePackets(unittest.TestCase):
"CombatEventPacket(event=EntityDeadEvent(" "CombatEventPacket(event=EntityDeadEvent("
"player_id=178, entity_id=36, message='RIP'))" "player_id=178, entity_id=36, message='RIP'))"
) )
self._test_read_write_packet(packet) self._test_read_write_packet(packet, vmax=PRE | 14)
with self.assertRaises(NotImplementedError):
self._test_read_write_packet(packet, vmin=PRE | 15)
specialised_packet = clientbound.play.DeathCombatEventPacket()
self.assertIsInstance(specialised_packet.event, type(packet.event))
for field in specialised_packet.fields:
value = getattr(packet.event, field)
setattr(specialised_packet, field, value)
self.assertEqual(getattr(specialised_packet.event, field), value)
def test_multi_block_change_packet(self): def test_multi_block_change_packet(self):
Record = clientbound.play.MultiBlockChangePacket.Record Record = clientbound.play.MultiBlockChangePacket.Record
@ -348,16 +380,22 @@ class TestReadWritePackets(unittest.TestCase):
) )
self._test_read_write_packet(packet, context) self._test_read_write_packet(packet, context)
def _test_read_write_packet(self, packet_in, context=None, **kwargs): def _test_read_write_packet(
self, packet_in, context=None, vmin=None, vmax=None, **kwargs
):
""" """
If kwargs are specified, the key will be tested against the If kwargs are specified, the key will be tested against the
respective delta value. Useful for testing FixedPointNumbers respective delta value. Useful for testing FixedPointNumbers
where there is precision lost in the resulting value. where there is precision lost in the resulting value.
""" """
if context is None: if context is None:
for protocol_version in TEST_VERSIONS: for pv in TEST_VERSIONS:
logging.debug('protocol_version = %r' % protocol_version) if vmin is not None and protocol_earlier(pv, vmin):
context = ConnectionContext(protocol_version=protocol_version) continue
if vmax is not None and protocol_earlier(vmax, pv):
continue
logging.debug('protocol_version = %r' % pv)
context = ConnectionContext(protocol_version=pv)
self._test_read_write_packet(packet_in, context) self._test_read_write_packet(packet_in, context)
else: else:
packet_in.context = context packet_in.context = context

View File

@ -4,6 +4,7 @@ from minecraft.networking.packets import PacketBuffer
from minecraft.networking.packets.clientbound.play import ( from minecraft.networking.packets.clientbound.play import (
PlayerPositionAndLookPacket, PlayerListItemPacket, MapPacket PlayerPositionAndLookPacket, PlayerListItemPacket, MapPacket
) )
from minecraft.networking.packets import serverbound
from minecraft.networking.connection import ConnectionContext from minecraft.networking.connection import ConnectionContext
from tests.test_packets import TEST_VERSIONS from tests.test_packets import TEST_VERSIONS
@ -306,3 +307,16 @@ class PlayerListItemTest(unittest.TestCase):
packet.write_fields(packet_buffer) packet.write_fields(packet_buffer)
self.read_and_apply(context, packet_buffer, player_list) self.read_and_apply(context, packet_buffer, player_list)
self.assertNotIn(fake_uuid, player_list.players_by_uuid) self.assertNotIn(fake_uuid, player_list.players_by_uuid)
class ClientSettingsTest(unittest.TestCase):
def test_enable_disable_text_filtering(self):
packet = serverbound.play.ClientSettingsPacket()
self.assertEqual(packet.enable_text_filtering, False)
self.assertEqual(packet.disable_text_filtering, True)
packet.enable_text_filtering = True
self.assertEqual(packet.enable_text_filtering, True)
self.assertEqual(packet.disable_text_filtering, False)
packet.disable_text_filtering = True
self.assertEqual(packet.enable_text_filtering, False)
self.assertEqual(packet.disable_text_filtering, True)

14
tox.ini
View File

@ -4,7 +4,7 @@
# and then run "tox" from this directory. # and then run "tox" from this directory.
[tox] [tox]
envlist = py35, py36, py37, py38, pypy, flake8, pylint-errors, pylint-full, verify-manifest envlist = py35, py36, py37, py38, py39, pypy, flake8, pylint-errors, pylint-full, verify-manifest
[testenv] [testenv]
commands = nosetests --with-timer commands = nosetests --with-timer
@ -27,7 +27,7 @@ commands =
deps = deps =
coveralls coveralls
[testenv:py38] [testenv:py39]
setenv = setenv =
PYCRAFT_RUN_INTERNET_TESTS=1 PYCRAFT_RUN_INTERNET_TESTS=1
commands = commands =
@ -41,7 +41,7 @@ deps =
mock mock
[testenv:flake8] [testenv:flake8]
basepython = python3.8 basepython = python3.9
commands = commands =
flake8 minecraft tests setup.py start.py bin/generate_travis_yml.py flake8 minecraft tests setup.py start.py bin/generate_travis_yml.py
deps = deps =
@ -54,14 +54,14 @@ per-file-ignores =
minecraft/networking/packets/__init__.py:F401 minecraft/networking/packets/__init__.py:F401
[testenv:pylint-errors] [testenv:pylint-errors]
basepython = python3.8 basepython = python3.9
deps = deps =
{[testenv]deps} {[testenv]deps}
pylint pylint
commands = pylint minecraft -E commands = pylint minecraft -E
[testenv:pylint-full] [testenv:pylint-full]
basepython = python3.8 basepython = python3.9
deps = deps =
{[testenv]deps} {[testenv]deps}
pylint pylint
@ -69,7 +69,7 @@ commands =
- pylint minecraft --disable=E - pylint minecraft --disable=E
[testenv:docs] [testenv:docs]
basepython = python3.8 basepython = python3.9
deps = deps =
{[testenv:cover]deps} {[testenv:cover]deps}
sphinx sphinx
@ -78,7 +78,7 @@ commands =
{toxinidir}/bin/build_docs {toxinidir}/bin/build_docs
[testenv:verify-manifest] [testenv:verify-manifest]
basepython = python3.8 basepython = python3.9
deps = deps =
check-manifest check-manifest
commands = commands =