Add ClientSettingsPacket and PluginMessagePacket.

Improve Packet string representation.
This commit is contained in:
joo 2017-08-24 02:14:53 +01:00
parent 3269a022a8
commit e9f095de42
13 changed files with 291 additions and 65 deletions

View File

@ -8,20 +8,22 @@ Use the packet classes under packets.clientbound.* and
packets.serverbound.* instead.
'''
from .packet import Packet
# Packet-Related Utilities
from .packet_buffer import PacketBuffer
from .keep_alive_packet import KeepAlivePacket
from .packet_listener import PacketListener
# For backward compatibility, re-export any old names from before the change:
# Abstract Packet Classes
from .packet import Packet
from .keep_alive_packet import AbstractKeepAlivePacket
from .plugin_message_packet import AbstractPluginMessagePacket
# Handshake State
# ==============
# Legacy Packets (Handshake State)
from .clientbound.handshake import get_packets as state_handshake_clientbound
from .serverbound.handshake import HandShakePacket
from .serverbound.handshake import get_packets as state_handshake_serverbound
# Status State
# ==============
# Legacy Packets (Status State)
from .clientbound.status import ResponsePacket
from .clientbound.status import PingResponsePacket as PingPacketResponse
from .clientbound.status import get_packets as state_status_clientbound
@ -29,8 +31,7 @@ from .serverbound.status import RequestPacket
from .serverbound.status import PingPacket
from .serverbound.status import get_packets as state_status_serverbound
# Login State
# ==============
# Legacy Packets (Login State)
from .clientbound.login import DisconnectPacket
from .clientbound.login import EncryptionRequestPacket
from .clientbound.login import LoginSuccessPacket
@ -40,8 +41,8 @@ from .serverbound.login import LoginStartPacket
from .serverbound.login import EncryptionResponsePacket
from .serverbound.login import get_packets as state_login_serverbound
# Playing State
# ==============
# Legacy Packets (Playing State)
from .keep_alive_packet import KeepAlivePacket
from .clientbound.play import KeepAlivePacket as KeepAlivePacketClientbound
from .serverbound.play import KeepAlivePacket as KeepAlivePacketServerbound
from .clientbound.play import JoinGamePacket
@ -60,7 +61,7 @@ from .serverbound.play import TeleportConfirmPacket
from .serverbound.play import AnimationPacket as AnimationPacketServerbound
from .serverbound.play import get_packets as state_playing_serverbound
__all_legacy_packets__ = [
__all_legacy_packets__ = (
state_handshake_clientbound, HandShakePacket,
state_handshake_serverbound, ResponsePacket,
PingPacketResponse, state_status_clientbound,
@ -75,19 +76,10 @@ __all_legacy_packets__ = [
MapPacket, state_playing_clientbound, ChatPacket,
PositionAndLookPacket, TeleportConfirmPacket,
AnimationPacketServerbound, state_playing_serverbound,
Packet, PacketBuffer, KeepAlivePacket
]
KeepAlivePacket,
)
class PacketListener(object):
def __init__(self, callback, *args):
self.callback = callback
self.packets_to_listen = []
for arg in args:
if issubclass(arg, Packet):
self.packets_to_listen.append(arg)
def call_packet(self, packet):
for packet_type in self.packets_to_listen:
if isinstance(packet, packet_type):
self.callback(packet)
__all_other__ = (
Packet, PacketBuffer, PacketListener,
AbstractKeepAlivePacket, AbstractPluginMessagePacket,
)

View File

@ -1,10 +1,10 @@
from minecraft.networking.packets import (
Packet, PacketBuffer, KeepAlivePacket as AbstractKeepAlivePacket
Packet, PacketBuffer, AbstractKeepAlivePacket, AbstractPluginMessagePacket
)
from minecraft.networking.types import (
Integer, UnsignedByte, Byte, Boolean, UUID, Short, Position,
VarInt, Double, Float, String
VarInt, Double, Float, String, Enum,
)
from .combat_event_packet import CombatEventPacket
@ -32,6 +32,7 @@ def get_packets(context):
SpawnObjectPacket,
BlockChangePacket,
MultiBlockChangePacket,
PluginMessagePacket,
}
if context.protocol_version <= 47:
packets |= {
@ -81,6 +82,11 @@ class ChatMessagePacket(Packet):
{'json_data': String},
{'position': Byte}]
class Position(Enum):
CHAT = 0 # A player-initiated chat message.
SYSTEM = 1 # The result of running a command.
GAME_INFO = 2 # Displayed above the hotbar in vanilla clients.
class DisconnectPacket(Packet):
@staticmethod
@ -265,3 +271,12 @@ class MultiBlockChangePacket(Packet):
def write(self, socket, compression_threshold=None):
raise NotImplementedError
class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod
def get_id(context):
return 0x18 if context.protocol_version >= 332 else \
0x19 if context.protocol_version >= 318 else \
0x18 if context.protocol_version >= 70 else \
0x3F

View File

@ -1,11 +1,11 @@
from minecraft.networking.packets import Packet
from minecraft.networking.types import (
Double, Float, Byte, VarInt
Double, Float, Byte, VarInt, BitFieldEnum
)
class PlayerPositionAndLookPacket(Packet):
class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
@staticmethod
def get_id(context):
return 0x2F if context.protocol_version >= 336 else \
@ -25,6 +25,9 @@ class PlayerPositionAndLookPacket(Packet):
{'teleport_id': VarInt} if context.protocol_version >= 107 else {},
])
field_enum = classmethod(
lambda cls, field: cls if field == 'flags' else None)
FLAG_REL_X = 0x01
FLAG_REL_Y = 0x02
FLAG_REL_Z = 0x04

View File

@ -1,7 +1,7 @@
from minecraft.networking.packets import Packet
from minecraft.networking.types import (
VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short
VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum
)
@ -13,7 +13,7 @@ class SpawnObjectPacket(Packet):
packet_name = 'spawn object'
class EntityType:
class EntityType(Enum):
BOAT = 1
ITEM_STACK = 2
AREA_EFFECT_CLOUD = 3
@ -42,19 +42,12 @@ class SpawnObjectPacket(Packet):
SPECTRAL_ARROW = 91
DRAGON_FIREBALL = 93
@classmethod
def get_type_by_id(cls, type_id):
by_id = {id: entity for (entity, id) in
cls.__dict__.items() if entity.isupper()}
return by_id[type_id]
def read(self, file_object):
self.entity_id = VarInt.read(file_object)
if self._context.protocol_version >= 49:
self.objectUUID = UUID.read(file_object)
type_id = Byte.read(file_object)
self.type = SpawnObjectPacket.EntityType.get_type_by_id(type_id)
self.type = SpawnObjectPacket.EntityType.name_from_value(type_id)
if self._context.protocol_version >= 100:
self.x = Double.read(file_object)

View File

@ -5,7 +5,11 @@ from minecraft.networking.types import (
)
class KeepAlivePacket(Packet):
class AbstractKeepAlivePacket(Packet):
packet_name = "keep alive"
definition = [
{'keep_alive_id': VarInt}]
# This alias is retained for backward compatibility:
KeepAlivePacket = AbstractKeepAlivePacket

View File

@ -1,7 +1,7 @@
from .packet_buffer import PacketBuffer
from zlib import compress
from minecraft.networking.types import (
VarInt
VarInt, Enum
)
@ -107,6 +107,32 @@ class Packet(object):
if self.id is not None:
str = '0x%02X %s' % (self.id, str)
if self.definition is not None:
fields = {a: getattr(self, a) for d in self.definition for a in d}
str = '%s %s' % (str, fields)
str = '%s(%s)' % (str, ', '.join(
'%s=%s' % (a, self.field_string(a))
for d in self.definition for a in d))
return str
def field_string(self, field):
""" The string representation of the value of a the given named field
of this packet. Override to customise field value representation.
"""
value = getattr(self, field, None)
enum_class = self.field_enum(field)
if enum_class is not None:
name = enum_class.name_from_value(value)
if name is not None:
return name
return repr(value)
@classmethod
def field_enum(cls, field):
""" The subclass of 'minecraft.networking.types.Enum' associated with
this field, or None if there is no such class.
"""
enum_name = ''.join(s.capitalize() for s in field.split('_'))
if hasattr(cls, enum_name):
enum_class = getattr(cls, enum_name)
if isinstance(enum_class, type) and issubclass(enum_class, Enum):
return enum_class

View File

@ -0,0 +1,15 @@
from .packet import Packet
class PacketListener(object):
def __init__(self, callback, *args):
self.callback = callback
self.packets_to_listen = []
for arg in args:
if issubclass(arg, Packet):
self.packets_to_listen.append(arg)
def call_packet(self, packet):
for packet_type in self.packets_to_listen:
if isinstance(packet, packet_type):
self.callback(packet)

View File

@ -0,0 +1,8 @@
from .packet import Packet
from minecraft.networking.types import String, TrailingByteArray
class AbstractPluginMessagePacket(Packet):
definition = [
{'channel': String},
{'data': TrailingByteArray}]

View File

@ -1,11 +1,13 @@
from minecraft.networking.packets import (
Packet, KeepAlivePacket as AbstractKeepAlivePacket
Packet, AbstractKeepAlivePacket, AbstractPluginMessagePacket
)
from minecraft.networking.types import (
Double, Float, Boolean, VarInt, String
Double, Float, Boolean, VarInt, String, Enum
)
from .client_settings_packet import ClientSettingsPacket
# Formerly known as state_playing_serverbound.
def get_packets(context):
@ -15,6 +17,8 @@ def get_packets(context):
PositionAndLookPacket,
AnimationPacket,
ClientStatusPacket,
ClientSettingsPacket,
PluginMessagePacket,
}
if context.protocol_version >= 107:
packets |= {
@ -82,7 +86,7 @@ class TeleportConfirmPacket(Packet):
{'teleport_id': VarInt}]
class AnimationPacket(Packet):
class AnimationPacket(Packet, Enum):
@staticmethod
def get_id(context):
return 0x1D if context.protocol_version >= 332 else \
@ -93,11 +97,14 @@ class AnimationPacket(Packet):
packet_name = "animation"
get_definition = staticmethod(lambda context: [
{'hand': VarInt} if context.protocol_version >= 107 else {}])
field_enum = classmethod(
lambda cls, field: cls if field == 'hand' else None)
HAND_MAIN = 0
HAND_OFF = 1
class ClientStatusPacket(Packet):
class ClientStatusPacket(Packet, Enum):
@staticmethod
def get_id(context):
return 0x03 if context.protocol_version >= 336 else \
@ -110,8 +117,19 @@ class ClientStatusPacket(Packet):
packet_name = "client status"
get_definition = staticmethod(lambda context: [
{'action_id': VarInt}])
field_enum = classmethod(
lambda cls, field: cls if field == 'action_id' else None)
RESPAWN = 0
REQUEST_STATS = 1
# Note: Open Inventory (id 2) was removed in protocol version 319
OPEN_INVENTORY = 2
class PluginMessagePacket(AbstractPluginMessagePacket):
@staticmethod
def get_id(context):
return 0x09 if context.protocol_version >= 336 else \
0x0A if context.protocol_version >= 317 else \
0x09 if context.protocol_version >= 94 else \
0x17

View File

@ -0,0 +1,51 @@
from minecraft.networking.packets import Packet
from minecraft.networking.types import (
String, Byte, VarInt, Boolean, UnsignedByte, Enum, BitFieldEnum
)
class ClientSettingsPacket(Packet):
@staticmethod
def get_id(context):
return 0x04 if context.protocol_version >= 336 else \
0x05 if context.protocol_version >= 318 else \
0x04 if context.protocol_version >= 94 else \
0x15
packet_name = 'client settings'
get_definition = staticmethod(lambda context: [
{'locale': String},
{'view_distance': Byte},
{'chat_mode': VarInt if context.protocol_version > 47 else Byte},
{'chat_colors': Boolean},
{'displayed_skin_parts': UnsignedByte},
{'main_hand': VarInt}])
field_enum = classmethod(
lambda cls, field: {
'chat_mode': cls.ChatMode,
'displayed_skin_parts': cls.SkinParts,
'main_hand': cls.Hand,
}.get(field))
class ChatMode(Enum):
FULL = 0 # Receive all types of chat messages.
SYSTEM = 1 # Receive only command results and game information.
NONE = 2 # Receive only game information.
class SkinParts(BitFieldEnum):
CAPE = 0x01
JACKET = 0x02
LEFT_SLEEVE = 0x04
RIGHT_SLEEVE = 0x08
LEFT_PANTS_LEG = 0x10
RIGHT_PANTS_LEG = 0x20
HAT = 0x40
ALL = 0x7F
NONE = 0x00
class Hand(Enum):
LEFT = 0
RIGHT = 1

View File

@ -205,6 +205,19 @@ class VarIntPrefixedByteArray(Type):
socket.send(struct.pack(str(len(value)) + "s", value))
class TrailingByteArray(Type):
""" A byte array consisting of all remaining data. If present in a packet
definition, this should only be the type of the last field. """
@staticmethod
def read(file_object):
return file_object.read()
@staticmethod
def send(value, socket):
socket.send(value)
class String(Type):
@staticmethod
def read(file_object):
@ -253,3 +266,30 @@ class Position(Type, namedtuple('Position', ('x', 'y', 'z'))):
def send(x, y, z, socket):
value = ((x & 0x3FFFFFF) << 38) | ((y & 0xFFF) << 26) | (z & 0x3FFFFFF)
UnsignedLong.send(value, socket)
class Enum(object):
@classmethod
def name_from_value(cls, value):
for name, name_value in cls.__dict__.items():
if name.isupper() and name_value == value:
return name
class BitFieldEnum(Enum):
@classmethod
def name_from_value(cls, value):
if not isinstance(value, int):
return
ret_names = []
ret_value = 0
for cls_name, cls_value in sorted(
[(n, v) for (n, v) in cls.__dict__.items()
if isinstance(v, int) and n.isupper() and v | value == value],
reverse=True, key=lambda p: p[1]
):
if ret_value | cls_value != ret_value or cls_value == value:
ret_names.append(cls_name)
ret_value |= cls_value
if ret_value == value:
return '|'.join(reversed(ret_names)) if ret_names else '0'

View File

@ -7,6 +7,9 @@ from minecraft.networking.packets import serverbound
class LegacyPacketNamesTest(unittest.TestCase):
def test_legacy_packets_equal_current_packets(self):
self.assertEqual(packets.KeepAlivePacket,
packets.AbstractKeepAlivePacket)
self.assertEqual(packets.state_handshake_clientbound,
clientbound.handshake.get_packets)
self.assertEqual(packets.HandShakePacket,

View File

@ -6,9 +6,9 @@ from random import choice
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
from minecraft.networking.connection import ConnectionContext
from minecraft.networking.types import VarInt
from minecraft.networking.types import VarInt, Enum, BitFieldEnum
from minecraft.networking.packets import (
PacketBuffer, PacketListener, KeepAlivePacket, serverbound)
Packet, PacketBuffer, PacketListener, KeepAlivePacket, serverbound)
class PacketBufferTest(unittest.TestCase):
@ -115,3 +115,61 @@ class PacketListenerTest(unittest.TestCase):
listener.call_packet(packet)
listener.call_packet(uncalled_packet)
class PacketEnumTest(unittest.TestCase):
def test_packet_str(self):
class ExamplePacket(Packet):
id = 0x00
packet_name = 'example'
definition = [
{'alpha': VarInt},
{'beta': VarInt},
{'gamma': VarInt}]
class Alpha(Enum):
ZERO = 0
class Beta(Enum):
ONE = 1
self.assertEqual(
str(ExamplePacket(ConnectionContext(), alpha=0, beta=0, gamma=0)),
'0x00 ExamplePacket(alpha=ZERO, beta=0, gamma=0)')
class EnumTest(unittest.TestCase):
def test_enum(self):
class Example(Enum):
ONE = 1
TWO = 2
THREE = 3
self.assertEqual(
list(map(Example.name_from_value, range(5))),
[None, 'ONE', 'TWO', 'THREE', None])
class BitFieldEnumTest(unittest.TestCase):
def test_name_from_value(self):
class Example1(BitFieldEnum):
ONE = 1
TWO = 2
FOUR = 4
ALL = 7
NONE = 0
self.assertEqual(
list(map(Example1.name_from_value, range(9))),
['NONE', 'ONE', 'TWO', 'ONE|TWO', 'FOUR',
'ONE|FOUR', 'TWO|FOUR', 'ALL', None])
class Example2(BitFieldEnum):
ONE = 1
TWO = 2
FOUR = 4
self.assertEqual(
list(map(Example2.name_from_value, range(9))),
['0', 'ONE', 'TWO', 'ONE|TWO', 'FOUR',
'ONE|FOUR', 'TWO|FOUR', 'ONE|TWO|FOUR', None])