pyCraft/minecraft/networking/packets.py

770 lines
21 KiB
Python
Raw Normal View History

from io import BytesIO
from zlib import compress
from .types import (
VarInt, Integer, Float, Double, UnsignedShort, Long, Byte, UnsignedByte,
2015-09-05 09:33:12 +02:00
String, VarIntPrefixedByteArray, Boolean, UUID
)
2014-10-08 19:12:37 +02:00
2014-10-08 19:12:37 +02:00
class PacketBuffer(object):
def __init__(self):
self.bytes = BytesIO()
def send(self, value):
"""
Writes the given bytes to the buffer, designed to emulate socket.send
:param value: The bytes to write
"""
self.bytes.write(value)
def read(self, length):
return self.bytes.read(length)
2015-04-03 05:51:53 +02:00
def recv(self, length):
return self.read(length)
def reset(self):
self.bytes = BytesIO()
def reset_cursor(self):
self.bytes.seek(0)
def get_writable(self):
return self.bytes.getvalue()
2014-10-08 19:12:37 +02:00
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)
2014-10-08 19:12:37 +02:00
class Packet(object):
packet_name = "base"
id = None
definition = None
# To define the packet ID, either:
# 1. Define the attribute `id', of type int, in a subclass; or
# 2. Override `get_id' in a subclass and return the correct packet ID
# for the given ConnectionContext. This is necessary if the packet ID
# has changed across protocol versions, for example.
@classmethod
def get_id(cls, context):
return cls.id
# To define the network data layout of a packet, either:
# 1. Define the attribute `definition', a list of fields, each of which
# is a dict mapping attribute names to data types; or
# 2. Override `get_definition' in a subclass and return the correct
# definition for the given ConnectionContext. This may be necessary
# if the layout has changed across protocol versions, for example; or
# 3. Override the methods `read' and/or `write' in a subclass. This may be
# necessary if the packet layout cannot be described as a list of
# fields.
@classmethod
def get_definition(cls, context):
return cls.definition
def __init__(self, context=None, **kwargs):
self.context = context
self.set_values(**kwargs)
@property
def context(self):
return self._context
@context.setter
def context(self, _context):
self._context = _context
self._context_changed()
def _context_changed(self):
if self._context is not None:
self.id = self.get_id(self._context)
self.definition = self.get_definition(self._context)
else:
self.id = None
self.definition = None
2014-10-08 19:12:37 +02:00
def set_values(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
return self
def read(self, file_object):
for field in self.definition:
2015-04-03 05:51:53 +02:00
for var_name, data_type in field.items():
value = data_type.read(file_object)
setattr(self, var_name, value)
def write(self, socket, compression_threshold=None):
# buffer the data since we need to know the length of each packet's
# payload
packet_buffer = PacketBuffer()
# write packet's id right off the bat in the header
VarInt.send(self.id, packet_buffer)
for field in self.definition:
2015-04-03 05:51:53 +02:00
for var_name, data_type in field.items():
data = getattr(self, var_name)
data_type.send(data, packet_buffer)
# compression_threshold of None means compression is disabled
if compression_threshold is not None:
if len(packet_buffer.get_writable()) > compression_threshold != -1:
# compress the current payload
compressed_data = compress(packet_buffer.get_writable())
packet_buffer.reset()
# write out the length of the compressed payload
VarInt.send(len(compressed_data), packet_buffer)
# write the compressed payload itself
packet_buffer.send(compressed_data)
else:
# write out a 0 to indicate uncompressed data
packet_data = packet_buffer.get_writable()
packet_buffer.reset()
VarInt.send(0, packet_buffer)
packet_buffer.send(packet_data)
2014-10-08 19:12:37 +02:00
VarInt.send(len(packet_buffer.get_writable()), socket) # Packet Size
socket.send(packet_buffer.get_writable()) # Packet Payload
def __str__(self):
str = type(self).__name__
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)
return str
# Handshake State
2014-10-08 19:12:37 +02:00
# ==============
class HandShakePacket(Packet):
id = 0x00
packet_name = "handshake"
definition = [
2014-10-08 19:12:37 +02:00
{'protocol_version': VarInt},
{'server_address': String},
{'server_port': UnsignedShort},
{'next_state': VarInt}]
def state_handshake_clientbound(context):
return {
}
def state_handshake_serverbound(context):
return {
HandShakePacket
}
2014-10-08 19:12:37 +02:00
# Status State
# ==============
class ResponsePacket(Packet):
id = 0x00
packet_name = "response"
definition = [
2014-10-08 19:12:37 +02:00
{'json_response': String}]
class PingPacketResponse(Packet):
id = 0x01
packet_name = "ping"
definition = [
2014-10-08 19:12:37 +02:00
{'time': Long}]
def state_status_clientbound(context):
return {
ResponsePacket,
PingPacketResponse,
}
2014-10-08 19:12:37 +02:00
class RequestPacket(Packet):
id = 0x00
packet_name = "request"
definition = []
2014-10-08 19:12:37 +02:00
class PingPacket(Packet):
id = 0x01
packet_name = "ping"
definition = [
2014-10-08 19:12:37 +02:00
{'time': Long}]
def state_status_serverbound(context):
return {
RequestPacket,
PingPacket
}
# Login State
# ==============
class DisconnectPacket(Packet):
id = 0x00
packet_name = "disconnect"
definition = [
2014-10-08 19:12:37 +02:00
{'json_data': String}]
2014-10-08 19:12:37 +02:00
class EncryptionRequestPacket(Packet):
id = 0x01
packet_name = "encryption request"
definition = [
2014-10-08 19:12:37 +02:00
{'server_id': String},
{'public_key': VarIntPrefixedByteArray},
{'verify_token': VarIntPrefixedByteArray}]
class LoginSuccessPacket(Packet):
id = 0x02
packet_name = "login success"
definition = [
2014-10-08 19:12:37 +02:00
{'UUID': String},
{'Username': String}]
class SetCompressionPacket(Packet):
id = 0x03
packet_name = "set compression"
definition = [
{'threshold': VarInt}]
def state_login_clientbound(context):
return {
DisconnectPacket,
EncryptionRequestPacket,
LoginSuccessPacket,
SetCompressionPacket
}
2014-10-08 19:12:37 +02:00
class LoginStartPacket(Packet):
id = 0x00
packet_name = "login start"
definition = [
2014-10-08 19:12:37 +02:00
{'name': String}]
2014-10-08 19:12:37 +02:00
class EncryptionResponsePacket(Packet):
id = 0x01
packet_name = "encryption response"
definition = [
{'shared_secret': VarIntPrefixedByteArray},
{'verify_token': VarIntPrefixedByteArray}]
def state_login_serverbound(context):
return {
LoginStartPacket,
EncryptionResponsePacket
}
# Playing State
# ==============
class KeepAlivePacket(Packet):
packet_name = "keep alive"
definition = [
{'keep_alive_id': VarInt}]
class KeepAlivePacketClientbound(KeepAlivePacket):
@staticmethod
def get_id(context):
return 0x1F if context.protocol_version >= 107 else \
0x00
class KeepAlivePacketServerbound(KeepAlivePacket):
@staticmethod
def get_id(context):
return 0x0B if context.protocol_version >= 107 else \
0x00
class JoinGamePacket(Packet):
@staticmethod
def get_id(context):
return 0x23 if context.protocol_version >= 107 else \
0x01
packet_name = "join game"
get_definition = staticmethod(lambda context: [
{'entity_id': Integer},
{'game_mode': UnsignedByte},
{'dimension': Integer if context.protocol_version >= 108 else Byte},
{'difficulty': UnsignedByte},
{'max_players': UnsignedByte},
{'level_type': String},
{'reduced_debug_info': Boolean}])
class ChatMessagePacket(Packet):
@staticmethod
def get_id(context):
return 0x0F if context.protocol_version >= 107 else \
0x02
packet_name = "chat message"
definition = [
{'json_data': String},
{'position': Byte}]
class PlayerPositionAndLookPacket(Packet):
@staticmethod
def get_id(context):
return 0x2E if context.protocol_version >= 107 else \
0x08
packet_name = "player position and look"
get_definition = staticmethod(lambda context: [
{'x': Double},
{'y': Double},
{'z': Double},
{'yaw': Float},
{'pitch': Float},
{'flags': Byte},
{'teleport_id': VarInt} if context.protocol_version >= 107 else {},
])
FLAG_REL_X = 0x01
FLAG_REL_Y = 0x02
FLAG_REL_Z = 0x04
FLAG_REL_YAW = 0x08
FLAG_REL_PITCH = 0x10
class PositionAndLook(object):
__slots__ = 'x', 'y', 'z', 'yaw', 'pitch'
def __init__(self, **kwds):
for attr in self.__slots__:
setattr(self, attr, kwds.get(attr))
# Update a PositionAndLook instance using this packet.
def apply(self, target):
# pylint: disable=no-member
if self.flags & self.FLAG_REL_X:
target.x += self.x
else:
target.x = self.x
if self.flags & self.FLAG_REL_Y:
target.y += self.y
else:
target.y = self.y
if self.flags & self.FLAG_REL_Z:
target.z += self.z
else:
target.z = self.z
if self.flags & self.FLAG_REL_YAW:
target.yaw += self.yaw
else:
target.yaw = self.yaw
if self.flags & self.FLAG_REL_PITCH:
target.pitch += self.pitch
else:
target.pitch = self.pitch
self.yaw %= 360
self.pitch %= 360
class DisconnectPacketPlayState(Packet):
@staticmethod
def get_id(context):
return 0x1A if context.protocol_version >= 107 else \
0x40
packet_name = "disconnect"
definition = [
{'json_data': String}]
class SetCompressionPacketPlayState(Packet):
# Note: removed between protocol versions 47 and 107.
id = 0x46
packet_name = "set compression"
definition = [
{'threshold': VarInt}]
2015-09-05 09:33:12 +02:00
class PlayerListItemPacket(Packet):
@staticmethod
def get_id(context):
return 0x2D if context.protocol_version >= 107 else \
0x38
2015-09-05 09:33:12 +02:00
packet_name = "player list item"
class PlayerList(object):
__slots__ = 'players_by_uuid'
2015-09-05 09:33:12 +02:00
def __init__(self):
self.players_by_uuid = dict()
class PlayerListItem(object):
__slots__ = (
'uuid', 'name', 'properties', 'gamemode', 'ping', 'display_name')
2015-09-05 09:33:12 +02:00
def __init__(self, **kwds):
for key, val in kwds.items():
2015-09-05 09:33:12 +02:00
setattr(self, key, val)
class PlayerProperty(object):
__slots__ = 'name', 'value', 'signature'
2015-09-05 09:33:12 +02:00
def read(self, file_object):
self.name = String.read(file_object)
self.value = String.read(file_object)
is_signed = Boolean.read(file_object)
if is_signed:
self.signature = String.read(file_object)
else:
self.signature = None
class Action(object):
__slots__ = 'uuid'
2015-09-05 09:33:12 +02:00
def read(self, file_object):
self.uuid = UUID.read(file_object)
self._read(file_object)
2016-06-17 23:54:21 +02:00
def _read(self, file_object):
raise NotImplementedError(
'This abstract method must be overridden in a subclass.')
2015-09-05 09:33:12 +02:00
@classmethod
def type_from_id(cls, action_id):
subcls = {
0: PlayerListItemPacket.AddPlayerAction,
1: PlayerListItemPacket.UpdateGameModeAction,
2: PlayerListItemPacket.UpdateLatencyAction,
3: PlayerListItemPacket.UpdateDisplayNameAction,
4: PlayerListItemPacket.RemovePlayerAction
}.get(action_id)
if subcls is None:
raise ValueError("Unknown player list action ID: %s."
% action_id)
2015-09-05 09:33:12 +02:00
return subcls
2015-09-05 09:33:12 +02:00
class AddPlayerAction(Action):
__slots__ = 'name', 'properties', 'gamemode', 'ping', 'display_name'
2015-09-05 09:33:12 +02:00
def _read(self, file_object):
self.name = String.read(file_object)
prop_count = VarInt.read(file_object)
self.properties = []
for i in range(prop_count):
property = PlayerListItemPacket.PlayerProperty()
property.read(file_object)
self.properties.append(property)
self.gamemode = VarInt.read(file_object)
self.ping = VarInt.read(file_object)
has_display_name = Boolean.read(file_object)
if has_display_name:
self.display_name = String.read(file_object)
else:
self.display_name = None
2015-09-05 09:33:12 +02:00
def apply(self, player_list):
player = PlayerListItemPacket.PlayerListItem(
uuid=self.uuid,
name=self.name,
properties=self.properties,
gamemode=self.gamemode,
ping=self.ping,
display_name=self.display_name)
2015-09-05 09:33:12 +02:00
player_list.players_by_uuid[self.uuid] = player
2015-09-05 09:33:12 +02:00
class UpdateGameModeAction(Action):
__slots__ = 'gamemode'
2015-09-05 09:33:12 +02:00
def _read(self, file_object):
self.gamemode = VarInt.read(file_object)
2015-09-05 09:33:12 +02:00
def apply(self, player_list):
player = player_list.players_by_uuid.get(self.uuid)
if player:
player.gamemode = self.gamemode
2015-09-05 09:33:12 +02:00
class UpdateLatencyAction(Action):
__slots__ = 'ping'
2015-09-05 09:33:12 +02:00
def _read(self, file_object):
self.ping = VarInt.read(file_object)
2015-09-05 09:33:12 +02:00
def apply(self, player_list):
player = player_list.players_by_uuid.get(self.uuid)
if player:
player.ping = self.ping
2015-09-05 09:33:12 +02:00
class UpdateDisplayNameAction(Action):
__slots__ = 'display_name'
2015-09-05 09:33:12 +02:00
def _read(self, file_object):
has_display_name = Boolean.read(file_object)
if has_display_name:
self.display_name = String.read(file_object)
else:
self.display_name = None
2015-09-05 09:33:12 +02:00
def apply(self, player_list):
player = player_list.players_by_uuid.get(self.uuid)
if player:
player.display_name = self.display_name
2015-09-05 09:33:12 +02:00
class RemovePlayerAction(Action):
def _read(self, file_object):
pass
2015-09-05 09:33:12 +02:00
def apply(self, player_list):
if self.uuid in player_list.players_by_uuid:
del player_list.players_by_uuid[self.uuid]
def read(self, file_object):
action_id = VarInt.read(file_object)
self.action_type = PlayerListItemPacket.Action.type_from_id(action_id)
action_count = VarInt.read(file_object)
self.actions = []
for i in range(action_count):
action = self.action_type()
action.read(file_object)
self.actions.append(action)
def apply(self, player_list):
for action in self.actions:
action.apply(player_list)
def write(self, socket, compression_threshold=None):
raise NotImplementedError
2015-09-20 14:04:37 +02:00
class MapPacket(Packet):
@staticmethod
def get_id(context):
return 0x24 if context.protocol_version >= 107 else \
0x34
2015-09-20 14:04:37 +02:00
packet_name = 'map'
2015-09-20 14:04:37 +02:00
class MapIcon(object):
__slots__ = 'type', 'direction', 'location'
def __init__(self, type, direction, location):
2015-09-20 14:04:37 +02:00
self.type = type
self.direction = direction
self.location = location
def __repr__(self):
return ('MapIcon(type=%s, direction=%s, location=%s)'
% (self.type, self.direction, self.location))
def __str__(self):
return self.__repr__()
2015-09-20 14:04:37 +02:00
class Map(object):
__slots__ = ('id', 'scale', 'icons', 'pixels', 'width', 'height',
'is_tracking_position')
2015-09-20 14:04:37 +02:00
def __init__(self, id=None, scale=None, width=128, height=128):
self.id = id
self.scale = scale
self.icons = []
self.width = width
self.height = height
self.pixels = bytearray(0 for i in range(width*height))
self.is_tracking_position = True
def __repr__(self):
return ('Map(id=%s, scale=%s, icons=%s, width=%s, height=%s)' % (
self.id, self.scale, self.icons, self.width, self.height))
def __str__(self):
return self.__repr__()
2015-09-20 14:04:37 +02:00
class MapSet(object):
__slots__ = 'maps_by_id'
2015-09-20 14:04:37 +02:00
def __init__(self):
self.maps_by_id = dict()
def __repr__(self):
return 'MapSet(%s)' % ', '.join(self.maps_by_id.values())
def __str__(self):
return self.__repr__()
2015-09-20 14:04:37 +02:00
def read(self, file_object):
self.map_id = VarInt.read(file_object)
self.scale = Byte.read(file_object)
if self.context.protocol_version >= 107:
self.is_tracking_position = Boolean.read(file_object)
else:
self.is_tracking_position = True
2015-09-20 14:04:37 +02:00
icon_count = VarInt.read(file_object)
self.icons = []
for i in range(icon_count):
type, direction = divmod(UnsignedByte.read(file_object), 16)
x = Byte.read(file_object)
z = Byte.read(file_object)
2015-09-20 14:04:37 +02:00
icon = MapPacket.MapIcon(type, direction, (x, z))
self.icons.append(icon)
self.width = UnsignedByte.read(file_object)
if self.width:
self.height = UnsignedByte.read(file_object)
x = Byte.read(file_object)
z = Byte.read(file_object)
2015-09-20 14:04:37 +02:00
self.offset = (x, z)
self.pixels = VarIntPrefixedByteArray.read(file_object)
else:
self.height = 0
self.offset = None
self.pixels = None
def apply_to_map(self, map):
map.id = self.map_id
map.scale = self.scale
map.icons[:] = self.icons
if self.pixels is not None:
for i in range(len(self.pixels)):
x = self.offset[0] + i % self.width
z = self.offset[1] + i / self.width
map.pixels[x + map.width * z] = self.pixels[i]
map.is_tracking_position = self.is_tracking_position
2015-09-20 14:04:37 +02:00
def apply_to_map_set(self, map_set):
map = map_set.maps_by_id.get(self.map_id)
if map is None:
map = MapPacket.Map(self.map_id)
map_set.maps_by_id[self.map_id] = map
self.apply_to_map(map)
def write(self, socket, compression_threshold=None):
raise NotImplementedError
def __repr__(self):
return 'MapPacket(%s)' % ', '.join(
'%s=%r' % (k, v)
for (k, v) in self.__dict__.items()
if k != 'pixels')
def __str__(self):
return self.__repr__()
def state_playing_clientbound(context):
packets = {
KeepAlivePacketClientbound,
JoinGamePacket,
ChatMessagePacket,
PlayerPositionAndLookPacket,
MapPacket,
PlayerListItemPacket,
DisconnectPacketPlayState,
}
if context.protocol_version <= 47:
packets |= {
SetCompressionPacketPlayState,
}
return packets
class ChatPacket(Packet):
@staticmethod
def get_id(context):
return 0x02 if context.protocol_version >= 107 else \
0x01
@staticmethod
def get_max_length(context):
return 256 if context.protocol_version >= 305 else \
100
@property
def max_length(self):
if self.context is not None:
return self.get_max_length(self.context)
packet_name = "chat"
definition = [
{'message': String}]
class PositionAndLookPacket(Packet):
@staticmethod
def get_id(context):
return 0x0D if context.protocol_version >= 107 else \
0x06
packet_name = "position and look"
definition = [
{'x': Double},
{'feet_y': Double},
{'z': Double},
{'yaw': Float},
{'pitch': Float},
{'on_ground': Boolean}]
class TeleportConfirmPacket(Packet):
# Note: added between protocol versions 47 and 107.
id = 0x00
packet_name = "teleport confirm"
definition = [
{'teleport_id': VarInt}]
class AnimationPacketServerbound(Packet):
@staticmethod
def get_id(context):
return 0x1A if context.protocol_version >= 107 else \
0x0A
packet_name = "animation"
get_definition = staticmethod(lambda context: [
{'hand': VarInt} if context.protocol_version >= 107 else {}])
HAND_MAIN = 0
HAND_OFF = 1
def state_playing_serverbound(context):
packets = {
KeepAlivePacketServerbound,
ChatPacket,
PositionAndLookPacket,
AnimationPacketServerbound,
}
if context.protocol_version >= 107:
packets |= {
TeleportConfirmPacket,
}
return packets