pyCraft/minecraft/networking/types/utility.py

255 lines
8.8 KiB
Python

"""Minecraft data types that are used by packets, but don't have a specific
network representation.
"""
import types
from collections import namedtuple
from itertools import chain
__all__ = (
'Vector', 'MutableRecord', 'Direction', 'PositionAndLook', 'descriptor',
'overridable_descriptor', 'overridable_property', 'attribute_alias',
'multi_attribute_alias',
)
class Vector(namedtuple('BaseVector', ('x', 'y', 'z'))):
"""An immutable type usually used to represent 3D spatial coordinates,
supporting elementwise vector addition, subtraction, and negation; and
scalar multiplication and (right) division.
NOTE: subclasses of 'Vector' should have '__slots__ = ()' to avoid the
creation of a '__dict__' attribute, which would waste space.
"""
__slots__ = ()
def __add__(self, other):
return NotImplemented if not isinstance(other, Vector) else \
type(self)(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(self, other):
return NotImplemented if not isinstance(other, Vector) else \
type(self)(self.x - other.x, self.y - other.y, self.z - other.z)
def __neg__(self):
return type(self)(-self.x, -self.y, -self.z)
def __mul__(self, other):
return type(self)(self.x*other, self.y*other, self.z*other)
def __rmul__(self, other):
return type(self)(other*self.x, other*self.y, other*self.z)
def __truediv__(self, other):
return type(self)(self.x/other, self.y/other, self.z/other)
def __floordiv__(self, other):
return type(self)(self.x//other, self.y//other, self.z//other)
__div__ = __floordiv__
def __repr__(self):
return '%s(%r, %r, %r)' % (type(self).__name__, self.x, self.y, self.z)
class MutableRecord(object):
"""An abstract base class providing namedtuple-like repr(), ==, hash(), and
iter(), implementations for types containing mutable fields given by
__slots__.
"""
__slots__ = ()
def __init__(self, **kwds):
for attr, value in kwds.items():
setattr(self, attr, value)
def __repr__(self):
return '%s(%s)' % (type(self).__name__, ', '.join(
'%s=%r' % (a, getattr(self, a)) for a in self._all_slots()
if hasattr(self, a)))
def __eq__(self, other):
return type(self) is type(other) and all(
getattr(self, a) == getattr(other, a) for a in self._all_slots())
def __ne__(self, other):
return not (self == other)
def __hash__(self):
values = tuple(getattr(self, a, None) for a in self._all_slots())
return hash((type(self), values))
def __iter__(self):
return iter(getattr(self, a) for a in self._all_slots())
@classmethod
def _all_slots(cls):
for supcls in reversed(cls.__mro__):
slots = supcls.__dict__.get('__slots__', ())
slots = (slots,) if isinstance(slots, str) else slots
for slot in slots:
yield slot
def attribute_alias(name):
"""An attribute descriptor that redirects access to a different attribute
with a given name.
"""
return property(fget=(lambda self: getattr(self, name)),
fset=(lambda self, value: setattr(self, name, value)),
fdel=(lambda self: delattr(self, name)))
def multi_attribute_alias(container, *arg_names, **kwd_names):
"""A descriptor for an attribute whose value is a container of a given type
with several fields, each of which is aliased to a different attribute
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.
As a special case, 'tuple' may be given as the 'container' when there
are positional arguments, and (even though the tuple constructor does
not take positional arguments), the arguments will be aliased to the
corresponding positions in a tuple.
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.
"""
if container is tuple:
container = lambda *args: args # noqa: E731
@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 overridable_descriptor:
"""As 'descriptor' (defined below), except that only a getter can be
defined, and the resulting descriptor has no '__set__' or '__delete__'
methods defined; hence, attributes defined via this class can be
overridden by attributes of instances of the class in which it occurs.
"""
__slots__ = '_fget',
def __init__(self, fget=None):
self._fget = fget if fget is not None else self._default_get
def getter(self, fget):
self._fget = fget
return self
@staticmethod
def _default_get(instance, owner):
raise AttributeError('unreadable attribute')
def __get__(self, instance, owner):
return self._fget(self, instance, owner)
class overridable_property(overridable_descriptor):
"""As the builtin 'property' decorator of Python, except that only
a getter is defined and the resulting descriptor is a non-data
descriptor, overridable by attributes of instances of the class
in which the property occurs. See also 'overridable_descriptor' above.
"""
def __get__(self, instance, _owner):
return self._fget(instance)
class descriptor(overridable_descriptor):
"""Behaves identically to the builtin 'property' decorator of Python,
except that the getter, setter and deleter functions given by the
user are used as the raw __get__, __set__ and __delete__ functions
as defined in Python's descriptor protocol.
Since an instance of this class always havs '__set__' and '__delete__'
defined, it is a "data descriptor", so its binding behaviour cannot be
overridden in instances of the class in which it occurs. See
https://docs.python.org/3/reference/datamodel.html#descriptor-invocation
for more information. See also 'overridable_descriptor' above.
"""
__slots__ = '_fset', '_fdel'
def __init__(self, fget=None, fset=None, fdel=None):
super(descriptor, self).__init__(fget=fget)
self._fset = fset if fset is not None else self._default_set
self._fdel = fdel if fdel is not None else self._default_del
def setter(self, fset):
self._fset = fset
return self
def deleter(self, fdel):
self._fdel = fdel
return self
@staticmethod
def _default_set(instance, value):
raise AttributeError("can't set attribute")
@staticmethod
def _default_del(instance):
raise AttributeError("can't delete attribute")
def __set__(self, instance, value):
return self._fset(self, instance, value)
def __delete__(self, instance):
return self._fdel(self, instance)
class class_and_instancemethod:
""" A decorator for functions defined in a class namespace which are to be
accessed as both class and instance methods: retrieving the method from
a class will return a bound class method (like the built-in
'classmethod' decorator), but retrieving the method from an instance
will return a bound instance method (as if the function were not
decorated). Therefore, the first argument of the decorated function may
be either a class or an instance, depending on how it was called.
"""
__slots__ = '_func',
def __init__(self, func):
self._func = func
def __get__(self, inst, owner=None):
bind_to = owner if inst is None else inst
return types.MethodType(self._func, bind_to)
Direction = namedtuple('Direction', ('yaw', 'pitch'))
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')