mirror of
https://github.com/ammaraskar/pyCraft.git
synced 2024-11-25 03:35:29 +01:00
Improve test coverage wrt protocol versions; other fixes/improvements
Improvements to the test suite: * List release version names and numbers in minecraft/__init__.py. * Make some tests, which previously ran for *all* protocol versions, run only for release protocol versions (to improve test performance). * Make some tests, which previously ran only for the latest protocol version, run for all release protocol versions (to improve coverage). * Print each protocol version being tested to the debug log, to help identify sources of errors. * Use the `nose-timer' plugin to show the run time of each test. Fix errors revealed by increased test coverage: * Fix: SoundEffectPacket.Pitch is not serialised correctly for early protocol versions. * Fix: handleExceptionTest finishes later than necessary because the test overrode an exception handler used internally by `_FakeServerTest', causing the server thread to time out after 4s. * Add support for multiple exception handlers in `Connection' (required for the above). Improvements to data descriptors: * Make syntax of property declarations more consistent/Pythonic. * Factor the definition of several aliasing properties into the utility methods `attribute_alias' and `multi_attribute_alias', which construct suitable data descriptors. * Define and use the named tuple `Direction' for (pitch, yaw) values.
This commit is contained in:
parent
6ef868bc5b
commit
7b1567c352
@ -3,8 +3,11 @@ A modern, Python3-compatible, well-documented library for communicating
|
|||||||
with a MineCraft server.
|
with a MineCraft server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# The version number of the most recent pyCraft release.
|
||||||
__version__ = "0.5.0"
|
__version__ = "0.5.0"
|
||||||
|
|
||||||
|
# A dict mapping the name of each Minecraft version supported by pyCraft to
|
||||||
|
# the corresponding protocol version number.
|
||||||
SUPPORTED_MINECRAFT_VERSIONS = {
|
SUPPORTED_MINECRAFT_VERSIONS = {
|
||||||
'1.8': 47,
|
'1.8': 47,
|
||||||
'1.8.1': 47,
|
'1.8.1': 47,
|
||||||
@ -175,5 +178,18 @@ SUPPORTED_MINECRAFT_VERSIONS = {
|
|||||||
'1.14.1': 480,
|
'1.14.1': 480,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Those Minecraft versions supported by pyCraft which are "release" versions,
|
||||||
|
# i.e. not development snapshots or pre-release versions.
|
||||||
|
RELEASE_MINECRAFT_VERSIONS = {
|
||||||
|
name: protocol for (name, protocol) in SUPPORTED_MINECRAFT_VERSIONS.items()
|
||||||
|
if __import__('re').match(r'\d+(\.\d+)+$', name)}
|
||||||
|
|
||||||
|
# The protocol versions of SUPPORTED_MINECRAFT_VERSIONS, without duplicates,
|
||||||
|
# in ascending numerical (and hence chronological) order.
|
||||||
SUPPORTED_PROTOCOL_VERSIONS = \
|
SUPPORTED_PROTOCOL_VERSIONS = \
|
||||||
sorted(set(SUPPORTED_MINECRAFT_VERSIONS.values()))
|
sorted(set(SUPPORTED_MINECRAFT_VERSIONS.values()))
|
||||||
|
|
||||||
|
# The protocol versions of RELEASE_MINECRAFT_VERSIONS, without duplicates,
|
||||||
|
# in ascending numerical (and hence chronological) order.
|
||||||
|
RELEASE_PROTOCOL_VERSIONS = \
|
||||||
|
sorted(set(RELEASE_MINECRAFT_VERSIONS.values()))
|
||||||
|
@ -82,22 +82,26 @@ class Connection(object):
|
|||||||
version string or protocol version number,
|
version string or protocol version number,
|
||||||
restricting the versions that the client may
|
restricting the versions that the client may
|
||||||
use in connecting to the server.
|
use in connecting to the server.
|
||||||
:param handle_exception: A function to be called when an exception
|
:param handle_exception: The final exception handler. This is triggered
|
||||||
occurs in the client's networking thread,
|
when an exception occurs in the networking
|
||||||
taking 2 arguments: the exception object 'e'
|
thread that is not caught normally. After
|
||||||
as in 'except Exception as e', and a 3-tuple
|
any other user-registered exception handlers
|
||||||
given by sys.exc_info(); or None for the
|
are run, the final exception (which may be the
|
||||||
default behaviour of raising the exception
|
original exception or one raised by another
|
||||||
from its original context; or False for no
|
handler) is passed, regardless of whether or
|
||||||
action. In any case, the networking thread
|
not it was caught by another handler, to the
|
||||||
will terminate, the exception will be
|
final handler, which may be a function obeying
|
||||||
available via the 'exception' and 'exc_info'
|
the protocol of 'register_exception_handler';
|
||||||
attributes of the 'Connection' instance.
|
the value 'None', meaning that if the
|
||||||
|
exception was otherwise uncaught, it is
|
||||||
|
re-raised from the networking thread after
|
||||||
|
closing the connection; or the value 'False',
|
||||||
|
meaning that the exception is never re-raised.
|
||||||
:param handle_exit: A function to be called when a connection to a
|
:param handle_exit: A function to be called when a connection to a
|
||||||
server terminates, not caused by an exception,
|
server terminates, not caused by an exception,
|
||||||
and not with the intention to automatically
|
and not with the intention to automatically
|
||||||
reconnect. Exceptions raised in this handler
|
reconnect. Exceptions raised from this function
|
||||||
will be handled by handle_exception.
|
will be handled by any matching exception handlers.
|
||||||
""" # NOQA
|
""" # NOQA
|
||||||
|
|
||||||
# This lock is re-entrant because it may be acquired in a re-entrant
|
# This lock is re-entrant because it may be acquired in a re-entrant
|
||||||
@ -110,6 +114,7 @@ class Connection(object):
|
|||||||
self.early_packet_listeners = []
|
self.early_packet_listeners = []
|
||||||
self.outgoing_packet_listeners = []
|
self.outgoing_packet_listeners = []
|
||||||
self.early_outgoing_packet_listeners = []
|
self.early_outgoing_packet_listeners = []
|
||||||
|
self._exception_handlers = []
|
||||||
|
|
||||||
def proto_version(version):
|
def proto_version(version):
|
||||||
if isinstance(version, str):
|
if isinstance(version, str):
|
||||||
@ -191,11 +196,21 @@ class Connection(object):
|
|||||||
"""
|
"""
|
||||||
Shorthand decorator to register a function as a packet listener.
|
Shorthand decorator to register a function as a packet listener.
|
||||||
"""
|
"""
|
||||||
def _method_func(method):
|
def listener_decorator(handler_func):
|
||||||
self.register_packet_listener(method, *packet_types, **kwds)
|
self.register_packet_listener(handler_func, *packet_types, **kwds)
|
||||||
return method
|
return handler_func
|
||||||
|
|
||||||
return _method_func
|
return listener_decorator
|
||||||
|
|
||||||
|
def exception_handler(self, *exc_types, **kwds):
|
||||||
|
"""
|
||||||
|
Shorthand decorator to register a function as an exception handler.
|
||||||
|
"""
|
||||||
|
def exception_handler_decorator(handler_func):
|
||||||
|
self.register_exception_handler(handler_func, *exc_types, **kwds)
|
||||||
|
return handler_func
|
||||||
|
|
||||||
|
return exception_handler_decorator
|
||||||
|
|
||||||
def register_packet_listener(self, method, *packet_types, **kwds):
|
def register_packet_listener(self, method, *packet_types, **kwds):
|
||||||
"""
|
"""
|
||||||
@ -229,6 +244,44 @@ class Connection(object):
|
|||||||
else self.early_outgoing_packet_listeners
|
else self.early_outgoing_packet_listeners
|
||||||
target.append(packets.PacketListener(method, *packet_types, **kwds))
|
target.append(packets.PacketListener(method, *packet_types, **kwds))
|
||||||
|
|
||||||
|
def register_exception_handler(self, handler_func, *exc_types, **kwds):
|
||||||
|
"""
|
||||||
|
Register a function to be called when an unhandled exception occurs
|
||||||
|
in the networking thread.
|
||||||
|
|
||||||
|
When multiple exception handlers are registered, they act like 'except'
|
||||||
|
clauses in a Python 'try' clause, with the earliest matching handler
|
||||||
|
catching the exception, and any later handlers catching any uncaught
|
||||||
|
exception raised from within an earlier handler.
|
||||||
|
|
||||||
|
Regardless of the presence or absence of matching handlers, any such
|
||||||
|
exception will cause the connection and the networking thread to
|
||||||
|
terminate, the final exception handler will be called (see the
|
||||||
|
'handle_exception' argument of the 'Connection' contructor), and the
|
||||||
|
original exception - or the last exception raised by a handler - will
|
||||||
|
be set as the 'exception' and 'exc_info' attributes of the
|
||||||
|
'Connection'.
|
||||||
|
|
||||||
|
:param handler_func: A function taking two arguments: the exception
|
||||||
|
object 'e' as in 'except Exception as e:', and the corresponding
|
||||||
|
3-tuple given by 'sys.exc_info()'. The return value of the function is
|
||||||
|
ignored, but any exception raised in it replaces the original
|
||||||
|
exception, and may be passed to later exception handlers.
|
||||||
|
|
||||||
|
:param exc_types: The types of exceptions that this handler shall
|
||||||
|
catch, as in 'except (exc_type_1, exc_type_2, ...) as e:'. If this is
|
||||||
|
empty, the handler will catch all exceptions.
|
||||||
|
|
||||||
|
:param early: If 'True', the exception handler is registered before
|
||||||
|
any existing exception handlers in the handling order.
|
||||||
|
"""
|
||||||
|
early = kwds.pop('early', False)
|
||||||
|
assert not kwds, 'Unexpected keyword arguments: %r' % (kwds,)
|
||||||
|
if early:
|
||||||
|
self._exception_handlers.insert(0, (handler_func, exc_types))
|
||||||
|
else:
|
||||||
|
self._exception_handlers.append((handler_func, exc_types))
|
||||||
|
|
||||||
def _pop_packet(self):
|
def _pop_packet(self):
|
||||||
# Pops the topmost packet off the outgoing queue and writes it out
|
# Pops the topmost packet off the outgoing queue and writes it out
|
||||||
# through the socket
|
# through the socket
|
||||||
@ -400,25 +453,44 @@ class Connection(object):
|
|||||||
self.write_packet(handshake)
|
self.write_packet(handshake)
|
||||||
|
|
||||||
def _handle_exception(self, exc, exc_info):
|
def _handle_exception(self, exc, exc_info):
|
||||||
handle_exception = self.handle_exception
|
# Call the current PacketReactor's exception handler.
|
||||||
try:
|
try:
|
||||||
if self.reactor.handle_exception(exc, exc_info):
|
if self.reactor.handle_exception(exc, exc_info):
|
||||||
return
|
return
|
||||||
if handle_exception not in (None, False):
|
except Exception as new_exc:
|
||||||
self.handle_exception(exc, exc_info)
|
|
||||||
except BaseException as new_exc:
|
|
||||||
exc, exc_info = new_exc, sys.exc_info()
|
exc, exc_info = new_exc, sys.exc_info()
|
||||||
|
|
||||||
|
# Call the user-registered exception handlers in order.
|
||||||
|
for handler, exc_types in self._exception_handlers:
|
||||||
|
if not exc_types or isinstance(exc, exc_types):
|
||||||
|
try:
|
||||||
|
handler(exc, exc_info)
|
||||||
|
caught = True
|
||||||
|
break
|
||||||
|
except Exception as new_exc:
|
||||||
|
exc, exc_info = new_exc, sys.exc_info()
|
||||||
|
else:
|
||||||
|
caught = False
|
||||||
|
|
||||||
|
# Call the user-specified final exception handler.
|
||||||
|
if self.handle_exception not in (None, False):
|
||||||
|
try:
|
||||||
|
self.handle_exception(exc, exc_info)
|
||||||
|
except Exception as new_exc:
|
||||||
|
exc, exc_info = new_exc, sys.exc_info()
|
||||||
|
|
||||||
|
# For backward compatibility, try to set the 'exc_info' attribute.
|
||||||
try:
|
try:
|
||||||
exc.exc_info = exc_info # For backward compatibility.
|
exc.exc_info = exc_info
|
||||||
except (TypeError, AttributeError):
|
except (TypeError, AttributeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Record the exception and cleanly terminate the connection.
|
||||||
self.exception, self.exc_info = exc, exc_info
|
self.exception, self.exc_info = exc, exc_info
|
||||||
with self._write_lock:
|
self.disconnect(immediate=True)
|
||||||
if self.networking_thread and not self.networking_thread.interrupt:
|
|
||||||
self.disconnect(immediate=True)
|
# If allowed by the final exception handler, re-raise the exception.
|
||||||
if handle_exception is None:
|
if self.handle_exception is None and not caught:
|
||||||
raise_(*exc_info)
|
raise_(*exc_info)
|
||||||
|
|
||||||
def _version_mismatch(self, server_protocol=None, server_version=None):
|
def _version_mismatch(self, server_protocol=None, server_version=None):
|
||||||
@ -471,7 +543,7 @@ class NetworkingThread(threading.Thread):
|
|||||||
self.connection.new_networking_thread = None
|
self.connection.new_networking_thread = None
|
||||||
self._run()
|
self._run()
|
||||||
self.connection._handle_exit()
|
self.connection._handle_exit()
|
||||||
except BaseException as e:
|
except Exception as e:
|
||||||
self.interrupt = True
|
self.interrupt = True
|
||||||
self.connection._handle_exception(e, sys.exc_info())
|
self.connection._handle_exception(e, sys.exc_info())
|
||||||
finally:
|
finally:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from minecraft.networking.packets import Packet
|
from minecraft.networking.packets import Packet
|
||||||
from minecraft.networking.types import (
|
from minecraft.networking.types import (
|
||||||
VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord
|
VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord,
|
||||||
|
attribute_alias, multi_attribute_alias,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -20,19 +21,25 @@ class BlockChangePacket(Packet):
|
|||||||
block_state_id = 0
|
block_state_id = 0
|
||||||
|
|
||||||
# For protocols < 347: an accessor for (block_state_id >> 4).
|
# For protocols < 347: an accessor for (block_state_id >> 4).
|
||||||
|
@property
|
||||||
|
def blockId(self):
|
||||||
|
return self.block_state_id >> 4
|
||||||
|
|
||||||
|
@blockId.setter
|
||||||
def blockId(self, block_id):
|
def blockId(self, block_id):
|
||||||
self.block_state_id = (self.block_state_id & 0xF) | (block_id << 4)
|
self.block_state_id = (self.block_state_id & 0xF) | (block_id << 4)
|
||||||
blockId = property(lambda self: self.block_state_id >> 4, blockId)
|
|
||||||
|
|
||||||
# For protocols < 347: an accessor for (block_state_id & 0xF).
|
# For protocols < 347: an accessor for (block_state_id & 0xF).
|
||||||
|
@property
|
||||||
|
def blockMeta(self):
|
||||||
|
return self.block_state_id & 0xF
|
||||||
|
|
||||||
|
@blockMeta.setter
|
||||||
def blockMeta(self, meta):
|
def blockMeta(self, meta):
|
||||||
self.block_state_id = (self.block_state_id & ~0xF) | (meta & 0xF)
|
self.block_state_id = (self.block_state_id & ~0xF) | (meta & 0xF)
|
||||||
blockMeta = property(lambda self: self.block_state_id & 0xF, blockMeta)
|
|
||||||
|
|
||||||
# This alias is retained for backward compatibility.
|
# This alias is retained for backward compatibility.
|
||||||
def blockStateId(self, block_state_id):
|
blockStateId = attribute_alias('block_state_id')
|
||||||
self.block_state_id = block_state_id
|
|
||||||
blockStateId = property(lambda self: self.block_state_id, blockStateId)
|
|
||||||
|
|
||||||
|
|
||||||
class MultiBlockChangePacket(Packet):
|
class MultiBlockChangePacket(Packet):
|
||||||
@ -54,24 +61,28 @@ class MultiBlockChangePacket(Packet):
|
|||||||
super(MultiBlockChangePacket.Record, self).__init__(**kwds)
|
super(MultiBlockChangePacket.Record, self).__init__(**kwds)
|
||||||
|
|
||||||
# Access the 'x', 'y', 'z' fields as a Vector of ints.
|
# Access the 'x', 'y', 'z' fields as a Vector of ints.
|
||||||
def position(self, position):
|
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||||
self.x, self.y, self.z = position
|
|
||||||
position = property(lambda r: Vector(r.x, r.y, r.z), position)
|
|
||||||
|
|
||||||
# For protocols < 347: an accessor for (block_state_id >> 4).
|
# For protocols < 347: an accessor for (block_state_id >> 4).
|
||||||
|
@property
|
||||||
|
def blockId(self):
|
||||||
|
return self.block_state_id >> 4
|
||||||
|
|
||||||
|
@blockId.setter
|
||||||
def blockId(self, block_id):
|
def blockId(self, block_id):
|
||||||
self.block_state_id = self.block_state_id & 0xF | block_id << 4
|
self.block_state_id = self.block_state_id & 0xF | block_id << 4
|
||||||
blockId = property(lambda r: r.block_state_id >> 4, blockId)
|
|
||||||
|
|
||||||
# For protocols < 347: an accessor for (block_state_id & 0xF).
|
# For protocols < 347: an accessor for (block_state_id & 0xF).
|
||||||
|
@property
|
||||||
|
def blockMeta(self):
|
||||||
|
return self.block_state_id & 0xF
|
||||||
|
|
||||||
|
@blockMeta.setter
|
||||||
def blockMeta(self, meta):
|
def blockMeta(self, meta):
|
||||||
self.block_state_id = self.block_state_id & ~0xF | meta & 0xF
|
self.block_state_id = self.block_state_id & ~0xF | meta & 0xF
|
||||||
blockMeta = property(lambda r: r.block_state_id & 0xF, blockMeta)
|
|
||||||
|
|
||||||
# This alias is retained for backward compatibility.
|
# This alias is retained for backward compatibility.
|
||||||
def blockStateId(self, block_state_id):
|
blockStateId = attribute_alias('block_state_id')
|
||||||
self.block_state_id = block_state_id
|
|
||||||
blockStateId = property(lambda r: r.block_state_id, blockStateId)
|
|
||||||
|
|
||||||
def read(self, file_object):
|
def read(self, file_object):
|
||||||
h_position = UnsignedByte.read(file_object)
|
h_position = UnsignedByte.read(file_object)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
from minecraft.networking.types import Vector, Float, Byte, Integer
|
from minecraft.networking.types import (
|
||||||
|
Vector, Float, Byte, Integer, multi_attribute_alias,
|
||||||
|
)
|
||||||
from minecraft.networking.packets import Packet
|
from minecraft.networking.packets import Packet
|
||||||
|
|
||||||
|
|
||||||
@ -19,23 +21,10 @@ class ExplosionPacket(Packet):
|
|||||||
class Record(Vector):
|
class Record(Vector):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
@property
|
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||||
def position(self):
|
|
||||||
return Vector(self.x, self.y, self.x)
|
|
||||||
|
|
||||||
@position.setter
|
player_motion = multi_attribute_alias(
|
||||||
def position(self, new_position):
|
Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z')
|
||||||
self.x, self.y, self.z = new_position
|
|
||||||
|
|
||||||
@property
|
|
||||||
def player_motion(self):
|
|
||||||
return Vector(self.player_motion_x, self.player_motion_y,
|
|
||||||
self.player_motion_z)
|
|
||||||
|
|
||||||
@player_motion.setter
|
|
||||||
def player_motion(self, new_player_motion):
|
|
||||||
self.player_motion_x, self.player_motion_y, self.player_motion_z \
|
|
||||||
= new_player_motion
|
|
||||||
|
|
||||||
def read(self, file_object):
|
def read(self, file_object):
|
||||||
self.x = Float.read(file_object)
|
self.x = Float.read(file_object)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from minecraft.networking.packets import Packet
|
from minecraft.networking.packets import Packet
|
||||||
|
|
||||||
from minecraft.networking.types import (
|
from minecraft.networking.types import (
|
||||||
String, Boolean, UUID, VarInt
|
String, Boolean, UUID, VarInt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class SoundEffectPacket(Packet):
|
|||||||
if context.protocol_version >= 201:
|
if context.protocol_version >= 201:
|
||||||
value = Float.read(file_object)
|
value = Float.read(file_object)
|
||||||
else:
|
else:
|
||||||
value = Byte.read(file_object) / 63.5
|
value = Byte.read(file_object)
|
||||||
if context.protocol_version < 204:
|
if context.protocol_version < 204:
|
||||||
value /= 63.5
|
value /= 63.5
|
||||||
return value
|
return value
|
||||||
|
@ -3,7 +3,7 @@ from minecraft.networking.types.utility import descriptor
|
|||||||
|
|
||||||
from minecraft.networking.types import (
|
from minecraft.networking.types import (
|
||||||
VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector,
|
VarInt, UUID, Byte, Double, Integer, UnsignedByte, Short, Enum, Vector,
|
||||||
PositionAndLook
|
PositionAndLook, attribute_alias, multi_attribute_alias,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -120,6 +120,7 @@ class SpawnObjectPacket(Packet):
|
|||||||
else:
|
else:
|
||||||
Byte.send(self.type_id, packet_buffer)
|
Byte.send(self.type_id, packet_buffer)
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
xyz_type = Double if self.context.protocol_version >= 100 else Integer
|
xyz_type = Double if self.context.protocol_version >= 100 else Integer
|
||||||
for coord in self.x, self.y, self.z:
|
for coord in self.x, self.y, self.z:
|
||||||
xyz_type.send(coord, packet_buffer)
|
xyz_type.send(coord, packet_buffer)
|
||||||
@ -147,28 +148,19 @@ class SpawnObjectPacket(Packet):
|
|||||||
'in order to set the "type" property.')
|
'in order to set the "type" property.')
|
||||||
self.type_id = getattr(self.EntityType, type_name)
|
self.type_id = getattr(self.EntityType, type_name)
|
||||||
|
|
||||||
# Access the fields 'x', 'y', 'z' as a Vector.
|
@type.deleter
|
||||||
def position(self, position):
|
def type(self):
|
||||||
self.x, self.y, self.z = position
|
del self.type_id
|
||||||
position = property(lambda p: Vector(p.x, p.y, p.z), position)
|
|
||||||
|
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||||
|
|
||||||
# Access the fields 'x', 'y', 'z', 'yaw', 'pitch' as a PositionAndLook.
|
|
||||||
# NOTE: modifying the object retrieved from this property will not change
|
# NOTE: modifying the object retrieved from this property will not change
|
||||||
# the packet; it can only be changed by attribute or property assignment.
|
# the packet; it can only be changed by attribute or property assignment.
|
||||||
def position_and_look(self, position_and_look):
|
position_and_look = multi_attribute_alias(
|
||||||
self.x, self.y, self.z = position_and_look.position
|
PositionAndLook, x='x', y='y', z='z', yaw='yaw', pitch='pitch')
|
||||||
self.yaw, self.pitch = position_and_look.look
|
|
||||||
position_and_look = property(lambda p: PositionAndLook(
|
|
||||||
x=p.x, y=p.y, z=p.z, yaw=p.yaw, pitch=p.pitch),
|
|
||||||
position_and_look)
|
|
||||||
|
|
||||||
# Access the fields 'velocity_x', 'velocity_y', 'velocity_z' as a Vector.
|
velocity = multi_attribute_alias(
|
||||||
def velocity(self, velocity):
|
Vector, 'velocity_x', 'velocity_y', 'velocity_z')
|
||||||
self.velocity_x, self.velocity_y, self.velocity_z = velocity
|
|
||||||
velocity = property(lambda p: Vector(p.velocity_x, p.velocity_y,
|
|
||||||
p.velocity_z), velocity)
|
|
||||||
|
|
||||||
# This alias is retained for backward compatibility.
|
# This alias is retained for backward compatibility.
|
||||||
def objectUUID(self, object_uuid):
|
objectUUID = attribute_alias('object_uuid')
|
||||||
self.object_uuid = object_uuid
|
|
||||||
objectUUID = property(lambda self: self.object_uuid, objectUUID)
|
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
network representation.
|
network representation.
|
||||||
"""
|
"""
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Vector', 'MutableRecord', 'PositionAndLook', 'descriptor',
|
'Vector', 'MutableRecord', 'PositionAndLook', 'descriptor',
|
||||||
|
'attribute_alias', 'multi_attribute_alias',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -75,21 +78,49 @@ class MutableRecord(object):
|
|||||||
return hash((type(self), values))
|
return hash((type(self), values))
|
||||||
|
|
||||||
|
|
||||||
class PositionAndLook(MutableRecord):
|
def attribute_alias(name):
|
||||||
"""A mutable record containing 3 spatial position coordinates
|
"""An attribute descriptor that redirects access to a different attribute
|
||||||
and 2 rotational coordinates for a look direction.
|
with a given name.
|
||||||
"""
|
"""
|
||||||
__slots__ = 'x', 'y', 'z', 'yaw', 'pitch'
|
return property(fget=(lambda self: getattr(self, name)),
|
||||||
|
fset=(lambda self, value: setattr(self, name, value)),
|
||||||
|
fdel=(lambda self: delattr(self, name)))
|
||||||
|
|
||||||
# Access the fields 'x', 'y', 'z' as a Vector.
|
|
||||||
def position(self, position):
|
|
||||||
self.x, self.y, self.z = position
|
|
||||||
position = property(lambda self: Vector(self.x, self.y, self.z), position)
|
|
||||||
|
|
||||||
# Access the fields 'yaw', 'pitch' as a tuple.
|
def multi_attribute_alias(container, *arg_names, **kwd_names):
|
||||||
def look(self, look):
|
"""A descriptor for an attribute whose value is a container of a given type
|
||||||
self.yaw, self.pitch = look
|
with several fields, each of which is aliased to a different attribute
|
||||||
look = property(lambda self: (self.yaw, self.pitch), look)
|
of the parent object.
|
||||||
|
|
||||||
|
The 'n'th name in 'arg_names' is the parent attribute that will be
|
||||||
|
aliased to the field of 'container' settable by the 'n'th positional
|
||||||
|
argument to its constructor, and accessible as its 'n'th iterable
|
||||||
|
element.
|
||||||
|
|
||||||
|
The name in 'kwd_names' mapped to by the key 'k' is the parent attribute
|
||||||
|
that will be aliased to the field of 'container' settable by the keyword
|
||||||
|
argument 'k' to its constructor, and accessible as its 'k' attribute.
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def alias(self):
|
||||||
|
return container(
|
||||||
|
*(getattr(self, name) for name in arg_names),
|
||||||
|
**{kwd: getattr(self, name) for (kwd, name) in kwd_names.items()})
|
||||||
|
|
||||||
|
@alias.setter
|
||||||
|
def alias(self, values):
|
||||||
|
if arg_names:
|
||||||
|
for name, value in zip(arg_names, values):
|
||||||
|
setattr(self, name, value)
|
||||||
|
for kwd, name in kwd_names.items():
|
||||||
|
setattr(self, name, getattr(values, kwd))
|
||||||
|
|
||||||
|
@alias.deleter
|
||||||
|
def alias(self):
|
||||||
|
for name in chain(arg_names, kwd_names.values()):
|
||||||
|
delattr(self, name)
|
||||||
|
|
||||||
|
return alias
|
||||||
|
|
||||||
|
|
||||||
class descriptor(object):
|
class descriptor(object):
|
||||||
@ -137,3 +168,17 @@ class descriptor(object):
|
|||||||
|
|
||||||
def __delete__(self, instance):
|
def __delete__(self, instance):
|
||||||
return self._fdel(self, instance)
|
return self._fdel(self, instance)
|
||||||
|
|
||||||
|
|
||||||
|
Direction = namedtuple('Direction', ('yaw', 'pitch'))
|
||||||
|
|
||||||
|
|
||||||
|
class PositionAndLook(MutableRecord):
|
||||||
|
"""A mutable record containing 3 spatial position coordinates
|
||||||
|
and 2 rotational coordinates for a look direction.
|
||||||
|
"""
|
||||||
|
__slots__ = 'x', 'y', 'z', 'yaw', 'pitch'
|
||||||
|
|
||||||
|
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||||
|
|
||||||
|
look = multi_attribute_alias(Direction, 'yaw', 'pitch')
|
||||||
|
@ -351,15 +351,14 @@ class HandleExceptionTest(ConnectTest):
|
|||||||
def _start_client(self, client):
|
def _start_client(self, client):
|
||||||
message = 'Min skoldpadda ar inte snabb, men den ar en skoldpadda.'
|
message = 'Min skoldpadda ar inte snabb, men den ar en skoldpadda.'
|
||||||
|
|
||||||
|
@client.listener(clientbound.login.LoginSuccessPacket)
|
||||||
def handle_login_success(_packet):
|
def handle_login_success(_packet):
|
||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
client.register_packet_listener(
|
|
||||||
handle_login_success, clientbound.login.LoginSuccessPacket)
|
|
||||||
|
|
||||||
def handle_exception(exc, exc_info):
|
@client.exception_handler()
|
||||||
|
def handle_exception(exc, _exc_info):
|
||||||
assert isinstance(exc, Exception) and exc.args == (message,)
|
assert isinstance(exc, Exception) and exc.args == (message,)
|
||||||
raise fake_server.FakeServerTestSuccess
|
raise fake_server.FakeServerTestSuccess
|
||||||
client.handle_exception = handle_exception
|
|
||||||
|
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import unittest
|
import unittest
|
||||||
import string
|
import string
|
||||||
|
import logging
|
||||||
|
import struct
|
||||||
from zlib import decompress
|
from zlib import decompress
|
||||||
from random import choice
|
from random import choice
|
||||||
|
|
||||||
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
|
from minecraft import 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
|
VarInt, Enum, Vector, PositionAndLook
|
||||||
@ -14,6 +16,10 @@ from minecraft.networking.packets import (
|
|||||||
clientbound
|
clientbound
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TEST_VERSIONS = list(RELEASE_PROTOCOL_VERSIONS)
|
||||||
|
if SUPPORTED_PROTOCOL_VERSIONS[-1] not in TEST_VERSIONS:
|
||||||
|
TEST_VERSIONS.append(SUPPORTED_PROTOCOL_VERSIONS[-1])
|
||||||
|
|
||||||
|
|
||||||
class PacketBufferTest(unittest.TestCase):
|
class PacketBufferTest(unittest.TestCase):
|
||||||
def test_basic_read_write(self):
|
def test_basic_read_write(self):
|
||||||
@ -42,7 +48,8 @@ class PacketBufferTest(unittest.TestCase):
|
|||||||
class PacketSerializationTest(unittest.TestCase):
|
class PacketSerializationTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_packet(self):
|
def test_packet(self):
|
||||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
for protocol_version in TEST_VERSIONS:
|
||||||
|
logging.debug('protocol_version = %r' % protocol_version)
|
||||||
context = ConnectionContext(protocol_version=protocol_version)
|
context = ConnectionContext(protocol_version=protocol_version)
|
||||||
|
|
||||||
packet = serverbound.play.ChatPacket(context)
|
packet = serverbound.play.ChatPacket(context)
|
||||||
@ -63,7 +70,8 @@ class PacketSerializationTest(unittest.TestCase):
|
|||||||
self.assertEqual(packet.message, deserialized.message)
|
self.assertEqual(packet.message, deserialized.message)
|
||||||
|
|
||||||
def test_compressed_packet(self):
|
def test_compressed_packet(self):
|
||||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
for protocol_version in TEST_VERSIONS:
|
||||||
|
logging.debug('protocol_version = %r' % protocol_version)
|
||||||
context = ConnectionContext(protocol_version=protocol_version)
|
context = ConnectionContext(protocol_version=protocol_version)
|
||||||
|
|
||||||
msg = ''.join(choice(string.ascii_lowercase) for i in range(500))
|
msg = ''.join(choice(string.ascii_lowercase) for i in range(500))
|
||||||
@ -74,7 +82,8 @@ class PacketSerializationTest(unittest.TestCase):
|
|||||||
self.write_read_packet(packet, -1)
|
self.write_read_packet(packet, -1)
|
||||||
|
|
||||||
def write_read_packet(self, packet, compression_threshold):
|
def write_read_packet(self, packet, compression_threshold):
|
||||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
for protocol_version in TEST_VERSIONS:
|
||||||
|
logging.debug('protocol_version = %r' % protocol_version)
|
||||||
context = ConnectionContext(protocol_version=protocol_version)
|
context = ConnectionContext(protocol_version=protocol_version)
|
||||||
|
|
||||||
packet_buffer = PacketBuffer()
|
packet_buffer = PacketBuffer()
|
||||||
@ -108,7 +117,8 @@ class PacketListenerTest(unittest.TestCase):
|
|||||||
def test_packet(chat_packet):
|
def test_packet(chat_packet):
|
||||||
self.assertEqual(chat_packet.message, message)
|
self.assertEqual(chat_packet.message, message)
|
||||||
|
|
||||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
for protocol_version in TEST_VERSIONS:
|
||||||
|
logging.debug('protocol_version = %r' % protocol_version)
|
||||||
context = ConnectionContext(protocol_version=protocol_version)
|
context = ConnectionContext(protocol_version=protocol_version)
|
||||||
|
|
||||||
listener = PacketListener(test_packet, serverbound.play.ChatPacket)
|
listener = PacketListener(test_packet, serverbound.play.ChatPacket)
|
||||||
@ -145,13 +155,6 @@ class PacketEnumTest(unittest.TestCase):
|
|||||||
class TestReadWritePackets(unittest.TestCase):
|
class TestReadWritePackets(unittest.TestCase):
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.context = ConnectionContext()
|
|
||||||
self.context.protocol_version = SUPPORTED_PROTOCOL_VERSIONS[-1]
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
del self.context
|
|
||||||
|
|
||||||
def test_explosion_packet(self):
|
def test_explosion_packet(self):
|
||||||
Record = clientbound.play.ExplosionPacket.Record
|
Record = clientbound.play.ExplosionPacket.Record
|
||||||
packet = clientbound.play.ExplosionPacket(
|
packet = clientbound.play.ExplosionPacket(
|
||||||
@ -159,6 +162,7 @@ class TestReadWritePackets(unittest.TestCase):
|
|||||||
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))
|
||||||
|
|
||||||
self._test_read_write_packet(packet)
|
self._test_read_write_packet(packet)
|
||||||
|
|
||||||
def test_combat_event_packet(self):
|
def test_combat_event_packet(self):
|
||||||
@ -187,57 +191,85 @@ class TestReadWritePackets(unittest.TestCase):
|
|||||||
self._test_read_write_packet(packet)
|
self._test_read_write_packet(packet)
|
||||||
|
|
||||||
def test_spawn_object_packet(self):
|
def test_spawn_object_packet(self):
|
||||||
EntityType = clientbound.play.SpawnObjectPacket.field_enum(
|
for protocol_version in TEST_VERSIONS:
|
||||||
'type_id', self.context)
|
logging.debug('protocol_version = %r' % protocol_version)
|
||||||
|
context = ConnectionContext(protocol_version=protocol_version)
|
||||||
|
|
||||||
object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa'
|
EntityType = clientbound.play.SpawnObjectPacket.field_enum(
|
||||||
pos_look = PositionAndLook(x=68.0, y=38.0, z=76.0, yaw=16, pitch=23)
|
'type_id', context)
|
||||||
velocity = Vector(21, 55, 41)
|
|
||||||
entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
|
|
||||||
|
|
||||||
packet = clientbound.play.SpawnObjectPacket(
|
pos_look = PositionAndLook(
|
||||||
context=self.context,
|
position=(Vector(68.0, 38.0, 76.0) if context.protocol_version
|
||||||
x=pos_look.x, y=pos_look.y, z=pos_look.z,
|
>= 100 else Vector(68, 38, 76)),
|
||||||
yaw=pos_look.yaw, pitch=pos_look.pitch,
|
yaw=16, pitch=23)
|
||||||
velocity_x=velocity.x, velocity_y=velocity.y,
|
velocity = Vector(21, 55, 41)
|
||||||
velocity_z=velocity.z, object_uuid=object_uuid,
|
entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
|
||||||
entity_id=entity_id, type_id=type_id, data=1)
|
|
||||||
self.assertEqual(packet.position_and_look, pos_look)
|
|
||||||
self.assertEqual(packet.position, pos_look.position)
|
|
||||||
self.assertEqual(packet.velocity, velocity)
|
|
||||||
self.assertEqual(packet.objectUUID, object_uuid)
|
|
||||||
self.assertEqual(packet.type, type_name)
|
|
||||||
|
|
||||||
packet2 = clientbound.play.SpawnObjectPacket(
|
packet = clientbound.play.SpawnObjectPacket(
|
||||||
context=self.context, position_and_look=pos_look,
|
context=context,
|
||||||
velocity=velocity, type=type_name,
|
x=pos_look.x, y=pos_look.y, z=pos_look.z,
|
||||||
object_uuid=object_uuid, entity_id=entity_id, data=1)
|
yaw=pos_look.yaw, pitch=pos_look.pitch,
|
||||||
self.assertEqual(packet.__dict__, packet2.__dict__)
|
velocity_x=velocity.x, velocity_y=velocity.y,
|
||||||
|
velocity_z=velocity.z,
|
||||||
|
entity_id=entity_id, type_id=type_id, data=1)
|
||||||
|
if context.protocol_version >= 49:
|
||||||
|
object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa'
|
||||||
|
packet.object_uuid = object_uuid
|
||||||
|
self.assertEqual(packet.objectUUID, object_uuid)
|
||||||
|
self.assertEqual(packet.position_and_look, pos_look)
|
||||||
|
self.assertEqual(packet.position, pos_look.position)
|
||||||
|
self.assertEqual(packet.velocity, velocity)
|
||||||
|
self.assertEqual(packet.type, type_name)
|
||||||
|
|
||||||
packet2.position = pos_look.position
|
packet2 = clientbound.play.SpawnObjectPacket(
|
||||||
self.assertEqual(packet.position, packet2.position)
|
context=context, position_and_look=pos_look,
|
||||||
|
velocity=velocity, type=type_name,
|
||||||
|
entity_id=entity_id, data=1)
|
||||||
|
if context.protocol_version >= 49:
|
||||||
|
packet2.object_uuid = object_uuid
|
||||||
|
self.assertEqual(packet.__dict__, packet2.__dict__)
|
||||||
|
|
||||||
packet2.data = 0
|
packet2.position = pos_look.position
|
||||||
self._test_read_write_packet(packet)
|
self.assertEqual(packet.position, packet2.position)
|
||||||
self._test_read_write_packet(packet2)
|
|
||||||
|
packet2.data = 0
|
||||||
|
if context.protocol_version < 49:
|
||||||
|
del packet2.velocity
|
||||||
|
self._test_read_write_packet(packet, context)
|
||||||
|
self._test_read_write_packet(packet2, context)
|
||||||
|
|
||||||
def test_sound_effect_packet(self):
|
def test_sound_effect_packet(self):
|
||||||
SoundCategory = clientbound.play.SoundEffectPacket.SoundCategory
|
for protocol_version in TEST_VERSIONS:
|
||||||
packet = clientbound.play.SoundEffectPacket(
|
context = ConnectionContext(protocol_version=protocol_version)
|
||||||
sound_category=SoundCategory.NEUTRAL, sound_id=545,
|
|
||||||
effect_position=Vector(0.125, 300.0, 50.5), volume=0.75, pitch=1.5)
|
|
||||||
self._test_read_write_packet(packet)
|
|
||||||
|
|
||||||
def _test_read_write_packet(self, packet_in):
|
packet = clientbound.play.SoundEffectPacket(
|
||||||
packet_in.context = self.context
|
sound_id=545, effect_position=Vector(0.125, 300.0, 50.5),
|
||||||
packet_buffer = PacketBuffer()
|
volume=0.75)
|
||||||
packet_in.write(packet_buffer)
|
if context.protocol_version >= 201:
|
||||||
packet_buffer.reset_cursor()
|
packet.pitch = struct.unpack('f', struct.pack('f', 1.5))[0]
|
||||||
VarInt.read(packet_buffer)
|
else:
|
||||||
packet_id = VarInt.read(packet_buffer)
|
packet.pitch = int(1.5 / 63.5) * 63.5
|
||||||
self.assertEqual(packet_id, packet_in.id)
|
if context.protocol_version >= 95:
|
||||||
|
packet.sound_category = \
|
||||||
|
clientbound.play.SoundEffectPacket.SoundCategory.NEUTRAL
|
||||||
|
self._test_read_write_packet(packet, context)
|
||||||
|
|
||||||
packet_out = type(packet_in)(context=self.context)
|
def _test_read_write_packet(self, packet_in, context=None):
|
||||||
packet_out.read(packet_buffer)
|
if context is None:
|
||||||
self.assertIs(type(packet_in), type(packet_out))
|
for protocol_version in TEST_VERSIONS:
|
||||||
self.assertEqual(packet_in.__dict__, packet_out.__dict__)
|
logging.debug('protocol_version = %r' % protocol_version)
|
||||||
|
context = ConnectionContext(protocol_version=protocol_version)
|
||||||
|
self._test_read_write_packet(packet_in, context)
|
||||||
|
else:
|
||||||
|
packet_in.context = context
|
||||||
|
packet_buffer = PacketBuffer()
|
||||||
|
packet_in.write(packet_buffer)
|
||||||
|
packet_buffer.reset_cursor()
|
||||||
|
VarInt.read(packet_buffer)
|
||||||
|
packet_id = VarInt.read(packet_buffer)
|
||||||
|
self.assertEqual(packet_id, packet_in.id)
|
||||||
|
|
||||||
|
packet_out = type(packet_in)(context=context)
|
||||||
|
packet_out.read(packet_buffer)
|
||||||
|
self.assertIs(type(packet_in), type(packet_out))
|
||||||
|
self.assertEqual(packet_in.__dict__, packet_out.__dict__)
|
||||||
|
@ -9,9 +9,13 @@ from minecraft.networking.types import (
|
|||||||
)
|
)
|
||||||
from minecraft.networking.packets import PacketBuffer
|
from minecraft.networking.packets import PacketBuffer
|
||||||
from minecraft.networking.connection import ConnectionContext
|
from minecraft.networking.connection import ConnectionContext
|
||||||
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
|
from minecraft import SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS
|
||||||
|
|
||||||
|
|
||||||
|
TEST_VERSIONS = list(RELEASE_PROTOCOL_VERSIONS)
|
||||||
|
if SUPPORTED_PROTOCOL_VERSIONS[-1] not in TEST_VERSIONS:
|
||||||
|
TEST_VERSIONS.append(SUPPORTED_PROTOCOL_VERSIONS[-1])
|
||||||
|
|
||||||
TEST_DATA = {
|
TEST_DATA = {
|
||||||
Boolean: [True, False],
|
Boolean: [True, False],
|
||||||
UnsignedByte: [0, 125],
|
UnsignedByte: [0, 125],
|
||||||
@ -36,7 +40,7 @@ TEST_DATA = {
|
|||||||
|
|
||||||
class SerializationTest(unittest.TestCase):
|
class SerializationTest(unittest.TestCase):
|
||||||
def test_serialization(self):
|
def test_serialization(self):
|
||||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
for protocol_version in TEST_VERSIONS:
|
||||||
context = ConnectionContext(protocol_version=protocol_version)
|
context = ConnectionContext(protocol_version=protocol_version)
|
||||||
|
|
||||||
for data_type in Type.__subclasses__():
|
for data_type in Type.__subclasses__():
|
||||||
|
5
tox.ini
5
tox.ini
@ -7,10 +7,11 @@
|
|||||||
envlist = py27, py34, py35, py36, pypy, flake8, pylint-errors, pylint-full, verify-manifest
|
envlist = py27, py34, py35, py36, pypy, flake8, pylint-errors, pylint-full, verify-manifest
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = nosetests
|
commands = nosetests --with-timer
|
||||||
|
|
||||||
deps =
|
deps =
|
||||||
nose
|
nose
|
||||||
|
nose-timer
|
||||||
-r{toxinidir}/requirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
|
|
||||||
[testenv:cover]
|
[testenv:cover]
|
||||||
@ -35,7 +36,7 @@ deps =
|
|||||||
setenv =
|
setenv =
|
||||||
PYCRAFT_RUN_INTERNET_TESTS=1
|
PYCRAFT_RUN_INTERNET_TESTS=1
|
||||||
commands =
|
commands =
|
||||||
nosetests --with-xunit --with-xcoverage --cover-package=minecraft --cover-erase --cover-inclusive --cover-tests --cover-branches --cover-min-percentage=60
|
{[testenv]commands} --with-xunit --with-xcoverage --cover-package=minecraft --cover-erase --cover-inclusive --cover-tests --cover-branches --cover-min-percentage=60
|
||||||
deps =
|
deps =
|
||||||
{[testenv:cover]deps}
|
{[testenv:cover]deps}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user