Add replacable Trait classes

This commit is contained in:
Griatch 2020-04-14 00:32:37 +02:00
parent 485ab5907c
commit e0717fd07c
5 changed files with 183 additions and 115 deletions

View file

@ -237,20 +237,59 @@ Examples:
``` ```
""" """
from django.conf import settings
from functools import total_ordering from functools import total_ordering
from evennia.utils.dbserialize import _SaverDict from evennia.utils.dbserialize import _SaverDict
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.utils import inherits_from from evennia.utils.utils import inherits_from, class_from_module
# This way the user can easily supply their own. Each
# class should have a class-property `trait_type` to
# identify the Trait class. The default ones are "static",
# "counter" and "gauge".
STATIC_TYPE = "static" _TRAIT_CLASS_PATHS = [
COUNTER_TYPE = "counter", "evennia.contrib.traits.StaticTrait",
GAUGE_TYPE = "gauge" "evennia.contrib.traits.CounterTrait",
"evennia.contrib.traits.GaugeTrait",
]
if hasattr(settings, "TRAIT_CLASS_PATHS"):
_TRAIT_CLASS_PATHS += settings.TRAIT_CLASS_PATHS
# delay trait-class import to avoid circular import
_TRAIT_CLASSES = None
TRAIT_TYPES = (STATIC_TYPE, COUNTER_TYPE, GAUGE_TYPE) def _delayed_import_trait_classes():
RANGE_TRAITS = (COUNTER_TYPE, GAUGE_TYPE) """
Import classes based on the given paths. Note that
imports from settings are last in the list, so if they
have the same trait_type set, they will replace the
default.
"""
global _TRAIT_CLASSES
if _TRAIT_CLASSES is None:
_TRAIT_CLASSES = {}
for classpath in _TRAIT_CLASS_PATHS:
try:
cls = class_from_module(classpath)
except ImportError:
logger.log_trace(f"Could not import Trait from {classpath}.")
else:
if hasattr(cls, "trait_type"):
trait_type = cls.trait_type
else:
trait_type = str(cls.__name___).lower()
_TRAIT_CLASSES[trait_type] = cls
_GA = object.__getattribute__
_SA = object.__setattr__
# this is the default we offer in TraitHandler.add
DEFAULT_TRAIT_TYPE = "static"
class TraitException(Exception): class TraitException(Exception):
@ -261,6 +300,7 @@ class TraitException(Exception):
msg (str): informative error message msg (str): informative error message
""" """
def __init__(self, msg): def __init__(self, msg):
self.msg = msg self.msg = msg
@ -270,7 +310,8 @@ class TraitHandler:
Factory class that instantiates Trait objects. Factory class that instantiates Trait objects.
""" """
def __init__(self, obj, db_attribute_key='traits', db_attribute_category="traits"):
def __init__(self, obj, db_attribute_key="traits", db_attribute_category="traits"):
""" """
Initialize the handler and set up its internal Attribute-based storage. Initialize the handler and set up its internal Attribute-based storage.
@ -279,6 +320,9 @@ class TraitHandler:
db_attribute_key (str): Name of the DB attribute for trait data storage db_attribute_key (str): Name of the DB attribute for trait data storage
""" """
# load the available classes, if necessary
_delayed_import_trait_classes()
# Note that this retains the connection to the database, meaning every # Note that this retains the connection to the database, meaning every
# update we do to .trait_data automatically syncs with database. # update we do to .trait_data automatically syncs with database.
self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category) self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category)
@ -294,8 +338,8 @@ class TraitHandler:
def __setattr__(self, key, value): def __setattr__(self, key, value):
"""Returns error message if trait objects are assigned directly.""" """Returns error message if trait objects are assigned directly."""
if key in ('trait_data', '_cache'): if key in ("trait_data", "_cache"):
super().__setattr__(key, value) _SA(self, key, value)
else: else:
raise TraitException( raise TraitException(
"Trait object not settable directly. Assign to one of " "Trait object not settable directly. Assign to one of "
@ -306,13 +350,18 @@ class TraitHandler:
"""Returns error message if trait objects are assigned directly.""" """Returns error message if trait objects are assigned directly."""
return self.__setattr__(key, value) return self.__setattr__(key, value)
def __getattr__(self, trait): def __getattr__(self, key):
"""Returns Trait instances accessed as attributes.""" """Returns Trait instances accessed as attributes."""
return self.get(trait) return self.get(key)
def __getitem__(self, trait): def __getitem__(self, key):
"""Returns `Trait` instances accessed as dict keys.""" """Returns `Trait` instances accessed as dict keys."""
return self.get(trait) return self.get(key)
def __repr__(self):
return "TraitHandler ({num} Trait(s) stored): {keys}".format(
num=len(self), keys=", ".join(self.all)
)
@property @property
def all(self): def all(self):
@ -325,7 +374,7 @@ class TraitHandler:
""" """
return list(self.trait_data.keys()) return list(self.trait_data.keys())
def get(self, trait): def get(self, key):
""" """
Args: Args:
trait (str): key from the traits dict containing config data trait (str): key from the traits dict containing config data
@ -336,14 +385,28 @@ class TraitHandler:
is not found in traits collection. is not found in traits collection.
""" """
trait = self._cache.get(trait) trait = self._cache.get(key)
if trait is None and trait in self.trait_data: if trait is None and key in self.trait_data:
trait = self.cache[trait] = Trait(self.trait_data[trait]) trait_type = self.trait_data[key]["trait_type"]
try:
trait_cls = _TRAIT_CLASSES[trait_type]
except KeyError:
raise TraitException("Trait class for {trait_type} could not be found.")
trait = self._cache[key] = trait_cls(self.trait_data[key])
return trait return trait
def add(self, key, name=None, trait_type=STATIC_TYPE, def add(
base=0, modifier=0, min_value=0, max_value=0, self,
force=False, **extra_properties): key,
name=None,
trait_type=DEFAULT_TRAIT_TYPE,
base=0,
modifier=0,
min_value=None,
max_value=None,
force=False,
**extra_properties,
):
""" """
Create a new Trait and add it to the handler. Create a new Trait and add it to the handler.
@ -369,13 +432,15 @@ class TraitHandler:
already exists (and `force` is unset). already exists (and `force` is unset).
""" """
# from evennia import set_trace;set_trace()
if key in self.trait_data: if key in self.trait_data:
if force: if force:
self.remove(key) self.remove(key)
else: else:
raise TraitException(f"Trait '{key}' already exists.") raise TraitException(f"Trait '{key}' already exists.")
if trait_type not in TRAIT_TYPES: if trait_type not in _TRAIT_CLASSES:
raise TraitException("Trait-type '{trait_type} is invalid.") raise TraitException("Trait-type '{trait_type} is invalid.")
trait_kwargs = dict( trait_kwargs = dict(
@ -385,7 +450,7 @@ class TraitHandler:
modifier=modifier, modifier=modifier,
min_value=min_value, min_value=min_value,
max_value=max_value, max_value=max_value,
extra_properties=extra_properties extra_properties=extra_properties,
) )
self.trait_data[key] = trait_kwargs self.trait_data[key] = trait_kwargs
@ -402,7 +467,7 @@ class TraitHandler:
raise TraitException(f"Trait '{key}' not found.") raise TraitException(f"Trait '{key}' not found.")
if key in self._cache: if key in self._cache:
del self.cache[key] del self._cache[key]
del self.trait_data[key] del self.trait_data[key]
def clear(self): def clear(self):
@ -415,6 +480,7 @@ class TraitHandler:
# Parent Trait class # Parent Trait class
@total_ordering @total_ordering
class Trait: class Trait:
"""Represents an object or Character trait. """Represents an object or Character trait.
@ -423,8 +489,16 @@ class Trait:
See module docstring for configuration details. See module docstring for configuration details.
""" """
_keys = set("name", "type", "base", "mod", "current", _keys = (
"min", "max", "extra_properties") "name",
"trait_type",
"base",
"modifier",
"current",
"min_value",
"max_value",
"extra_properties",
)
def __init__(self, trait_data): def __init__(self, trait_data):
""" """
@ -436,20 +510,16 @@ class Trait:
save itself the database when updating save itself the database when updating
""" """
if not all(key in trait_data for key in self.valid_keys):
raise TraitException(
f"Required keys missing from trait_data "
f"(input was {list(trait_data.keys())}, "
f"required are {self.valid_keys}).")
self._type = trait_data['trait_type'] self._type = trait_data["trait_type"]
self._data = trait_data self._data = trait_data
self._locked = True self._locked = True
if not isinstance(trait_data, _SaverDict): if not isinstance(trait_data, _SaverDict):
logger.log_warn( logger.log_warn(
f"Non-persistent Trait data (type(trait_data)) " f"Non-persistent Trait data (type(trait_data)) "
f"loaded for {type(self).__name__}.") f"loaded for {type(self).__name__}."
)
# Private helper members # Private helper members
@ -458,7 +528,7 @@ class Trait:
if self._type in RANGE_TRAITS: if self._type in RANGE_TRAITS:
if self.min is not None and value <= self.min: if self.min is not None and value <= self.min:
return self.min return self.min
if self._data['max'] == 'base' and value >= self.mod + self.base: if self._data["max"] == "base" and value >= self.mod + self.base:
return self.mod + self.base return self.mod + self.base
if self.max is not None and value >= self.max: if self.max is not None and value >= self.max:
return self.max return self.max
@ -474,15 +544,14 @@ class Trait:
"""Debug-friendly representation of this Trait.""" """Debug-friendly representation of this Trait."""
return "{}({{{}}})".format( return "{}({{{}}})".format(
type(self).__name__, type(self).__name__,
', '.join(["'{}': {!r}".format(k, self._data[k]) ", ".join(
for k in self._keys if k in self._data])) ["'{}': {!r}".format(k, self._data[k]) for k in self._keys if k in self._data]
),
)
def __str__(self): def __str__(self):
status = "{actual:11}".format(actual=self.actual) status = "{actual:11}".format(actual=self.actual)
return "{name:12} {status} ({mod:+3})".format( return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
name=self.name,
status=status,
mod=self.mod)
# Extra Properties - allow access to properties on Trait # Extra Properties - allow access to properties on Trait
@ -504,12 +573,11 @@ class Trait:
def __getattr__(self, key): def __getattr__(self, key):
"""Access extra parameters as attributes.""" """Access extra parameters as attributes."""
try: try:
return self._data['extra_properties'][key] return self._data["extra_properties"][key]
except KeyError: except KeyError:
raise AttributeError( raise AttributeError(
"{} '{}' has no attribute {!r}".format( "{} '{}' has no attribute {!r}".format(type(self).__name__, self.name, key)
type(self).__name__, self.name, key )
))
def __setattr__(self, key, value): def __setattr__(self, key, value):
"""Set extra parameters as attributes. """Set extra parameters as attributes.
@ -526,16 +594,15 @@ class Trait:
raise AttributeError(f"Can't set attribute {key}.") raise AttributeError(f"Can't set attribute {key}.")
propobj.fset(self, value) propobj.fset(self, value)
else: else:
if (self.__dict__.get('_locked', False) and if self.__dict__.get("_locked", False) and key not in ("_keys",):
key not in ('_keys',)): _GA(self, "_data")["extra_properties"][key] = value
self._data['extra_properties'][key] = value
else: else:
super().__setattr__(key, value) _SA(self, key, value)
def __delattr__(self, key): def __delattr__(self, key):
"""Delete extra parameters as attributes.""" """Delete extra parameters as attributes."""
if key in self._data['extra_parameters']: if key in self._data["extra_properties"]:
del self._data['extra_parameters'][key] del self._data["extra_properties"][key]
# Numeric operations # Numeric operations
@ -630,7 +697,8 @@ class Trait:
@property @property
def name(self): def name(self):
"""Display name for the trait.""" """Display name for the trait."""
return self._data['name'] return self._data["name"]
key = name key = name
@property @property
@ -646,24 +714,24 @@ class Trait:
The setter for this property will enforce any range bounds set The setter for this property will enforce any range bounds set
on this `Trait`. on this `Trait`.
""" """
return self._data['base'] return self._data["base"]
@base.setter @base.setter
def base(self, amount): def base(self, amount):
if self._data.get('max', None) == 'base': if self._data.get("max", None) == "base":
self._data['base'] = amount self._data["base"] = amount
if type(amount) in (int, float): if type(amount) in (int, float):
self._data['base'] = self._enforce_bounds(amount) self._data["base"] = self._enforce_bounds(amount)
@property @property
def mod(self): def mod(self):
"""The trait's modifier.""" """The trait's modifier."""
return self._data['modifier'] return self._data["modifier"]
@mod.setter @mod.setter
def mod(self, amount): def mod(self, amount):
if type(amount) in (int, float): if type(amount) in (int, float):
self._data['modifier'] = amount self._data["modifier"] = amount
@property @property
def min(self): def min(self):
@ -675,7 +743,7 @@ class Trait:
@property @property
def max(self): def max(self):
return self._data['max_value'] return self._data["max_value"]
@max.setter @max.setter
def max(self, value): def max(self, value):
@ -684,7 +752,7 @@ class Trait:
@property @property
def current(self): def current(self):
"""The `current` value of the `Trait`.""" """The `current` value of the `Trait`."""
return self._data.get('current', self.base) return self._data.get("current", self.base)
@current.setter @current.setter
def current(self, value): def current(self, value):
@ -693,7 +761,7 @@ class Trait:
@property @property
def extra(self): def extra(self):
"""Returns a list containing available extra data keys.""" """Returns a list containing available extra data keys."""
return self._data['extra'].keys() return self._data["extra"].keys()
def reset_mod(self): def reset_mod(self):
"""Clears any mod value to 0.""" """Clears any mod value to 0."""
@ -710,11 +778,15 @@ class Trait:
# Implementation of the respective Trait types # Implementation of the respective Trait types
class StaticTrait(Trait): class StaticTrait(Trait):
""" """
Static Trait. Static Trait.
""" """
trait_type = "static"
@property @property
def min(self): def min(self):
raise TraitException(f"Static Trait {self.key} has no minimum value.") raise TraitException(f"Static Trait {self.key} has no minimum value.")
@ -733,17 +805,16 @@ class StaticTrait(Trait):
@property @property
def current(self): def current(self):
"""The `current` value of the `Trait`.""" """The `current` value of the `Trait`. This is the same as base for a Static Trait."""
return super().current return self.base
@current.setter @current.setter
def current(self, value): def current(self, value):
raise TraitException( """Current == base for Static Traits."""
f"Cannot set 'current' property on static Trait {self.key}.") self.base = self.current = value
def reset(self): def reset(self):
raise TraitException( raise TraitException(f"Cannot reset static Trait {self.key}.")
f"Cannot reset static Trait {self.key}.")
class CounterTrait(Trait): class CounterTrait(Trait):
@ -751,6 +822,9 @@ class CounterTrait(Trait):
Counter Trait. Counter Trait.
""" """
trait_type = "counter"
@property @property
def actual(self): def actual(self):
"The actual value of the Trait" "The actual value of the Trait"
@ -764,13 +838,13 @@ class CounterTrait(Trait):
@min.setter @min.setter
def min(self, amount): def min(self, amount):
if amount is None: if amount is None:
self._data['min'] = amount self._data["min"] = amount
elif type(amount) in (int, float): elif type(amount) in (int, float):
self._data['min'] = amount if amount < self.base else self.base self._data["min"] = amount if amount < self.base else self.base
@property @property
def max(self): def max(self):
if self._data['max_value'] == 'base': if self._data["max_value"] == "base":
return self._mod_base() return self._mod_base()
return super().max return super().max
@ -783,16 +857,16 @@ class CounterTrait(Trait):
When set this way, the property returns the value of the When set this way, the property returns the value of the
`mod`+`base` properties. `mod`+`base` properties.
""" """
if self._data['max_value'] == 'base': if self._data["max_value"] == "base":
return self._mod_base() return self._mod_base()
return super().max return super().max
@max.setter @max.setter
def max(self, value): def max(self, value):
if value == 'base' or value is None: if value == "base" or value is None:
self._data['max_value'] = value self._data["max_value"] = value
elif type(value) in (int, float): elif type(value) in (int, float):
self._data['max_value'] = value if value > self.base else self.base self._data["max_value"] = value if value > self.base else self.base
@property @property
def current(self): def current(self):
@ -802,10 +876,9 @@ class CounterTrait(Trait):
@current.setter @current.setter
def current(self, value): def current(self, value):
if type(value) in (int, float): if type(value) in (int, float):
self._data['current'] = self._enforce_bounds(value) self._data["current"] = self._enforce_bounds(value)
else: else:
raise AttributeError( raise AttributeError("'current' property is read-only on static 'Trait'.")
"'current' property is read-only on static 'Trait'.")
def percent(self): def percent(self):
"""Returns the value formatted as a percentage.""" """Returns the value formatted as a percentage."""
@ -822,14 +895,12 @@ class GaugeTrait(CounterTrait):
Gauge Trait. Gauge Trait.
""" """
trait_type = "gauge"
def __str__(self): def __str__(self):
status = "{actual:4} / {base:4}".format( status = "{actual:4} / {base:4}".format(actual=self.actual, base=self.base)
actual=self.actual, return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
base=self.base)
return "{name:12} {status} ({mod:+3})".format(
name=self.name,
status=status,
mod=self.mod)
@property @property
def actual(self): def actual(self):
@ -844,8 +915,8 @@ class GaugeTrait(CounterTrait):
@mod.setter @mod.setter
def mod(self, amount): def mod(self, amount):
if type(amount) in (int, float): if type(amount) in (int, float):
self._data['modifier'] = amount self._data["modifier"] = amount
delta = amount - self._data['modifier'] delta = amount - self._data["modifier"]
if delta >= 0: if delta >= 0:
# apply increases to current # apply increases to current
self.current = self._enforce_bounds(self.current + delta) self.current = self._enforce_bounds(self.current + delta)
@ -856,7 +927,7 @@ class GaugeTrait(CounterTrait):
@property @property
def current(self): def current(self):
"""The `current` value of the `Trait`.""" """The `current` value of the `Trait`."""
return self._data.get('current', self._mod_base()) return self._data.get("current", self._mod_base())
@current.setter @current.setter
def current(self, value): def current(self, value):
@ -869,5 +940,4 @@ class GaugeTrait(CounterTrait):
Will honor the upper bound if set. Will honor the upper bound if set.
""" """
self.current = \ self.current = self._enforce_bounds(self.current + self._mod_base())
self._enforce_bounds(self.current + self._mod_base())

View file

@ -261,6 +261,7 @@ if TELNET_ENABLED:
# Start telnet game connections # Start telnet game connections
from evennia.server.portal import telnet from evennia.server.portal import telnet
_telnet_protocol = class_from_module(settings.TELNET_PROTOCOL_CLASS) _telnet_protocol = class_from_module(settings.TELNET_PROTOCOL_CLASS)
for interface in TELNET_INTERFACES: for interface in TELNET_INTERFACES:
@ -285,6 +286,7 @@ if SSL_ENABLED:
# Start Telnet+SSL game connection (requires PyOpenSSL). # Start Telnet+SSL game connection (requires PyOpenSSL).
from evennia.server.portal import telnet_ssl from evennia.server.portal import telnet_ssl
_ssl_protocol = class_from_module(settings.SSL_PROTOCOL_CLASS) _ssl_protocol = class_from_module(settings.SSL_PROTOCOL_CLASS)
for interface in SSL_INTERFACES: for interface in SSL_INTERFACES:
@ -319,6 +321,7 @@ if SSH_ENABLED:
# evennia/game if necessary. # evennia/game if necessary.
from evennia.server.portal import ssh from evennia.server.portal import ssh
_ssh_protocol = class_from_module(settings.SSH_PROTOCOL_CLASS) _ssh_protocol = class_from_module(settings.SSH_PROTOCOL_CLASS)
for interface in SSH_INTERFACES: for interface in SSH_INTERFACES:
@ -328,11 +331,7 @@ if SSH_ENABLED:
for port in SSH_PORTS: for port in SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port) pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory( factory = ssh.makeFactory(
{ {"protocolFactory": _ssh_protocol, "protocolArgs": (), "sessions": PORTAL_SESSIONS,}
"protocolFactory": _ssh_protocol,
"protocolArgs": (),
"sessions": PORTAL_SESSIONS,
}
) )
factory.noisy = False factory.noisy = False
ssh_service = internet.TCPServer(port, factory, interface=interface) ssh_service = internet.TCPServer(port, factory, interface=interface)

View file

@ -58,7 +58,6 @@ _HTTP_WARNING = bytes(
_BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS) _BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS)
class TelnetServerFactory(protocol.ServerFactory): class TelnetServerFactory(protocol.ServerFactory):
"This is only to name this better in logs" "This is only to name this better in logs"
noisy = False noisy = False

View file

@ -23,7 +23,7 @@ from evennia.utils.utils import (
make_iter, make_iter,
delay, delay,
callables_from_module, callables_from_module,
class_from_module class_from_module,
) )
from evennia.server.portal import amp from evennia.server.portal import amp
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT

View file

@ -972,7 +972,7 @@ REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 25, "PAGE_SIZE": 25,
# require logged in users to call API so that access checks can work on them # require logged in users to call API so that access checks can work on them
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated", ], "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated",],
# These are the different ways people can authenticate for API requests - via # These are the different ways people can authenticate for API requests - via
# session or with user/password. Other ways are possible, such as via tokens # session or with user/password. Other ways are possible, such as via tokens
# or oauth, but require additional dependencies. # or oauth, but require additional dependencies.
@ -1040,28 +1040,28 @@ PORTAL_SESSION_HANDLER_CLASS = "evennia.server.portal.portalsessionhandler.Porta
# so the additions have somewhere to go. These must be simple things that # so the additions have somewhere to go. These must be simple things that
# can be pickled - stuff you could serialize to JSON is best. # can be pickled - stuff you could serialize to JSON is best.
SESSION_SYNC_ATTRS = ( SESSION_SYNC_ATTRS = (
"protocol_key", "protocol_key",
"address", "address",
"suid", "suid",
"sessid", "sessid",
"uid", "uid",
"csessid", "csessid",
"uname", "uname",
"logged_in", "logged_in",
"puid", "puid",
"conn_time", "conn_time",
"cmd_last", "cmd_last",
"cmd_last_visible", "cmd_last_visible",
"cmd_total", "cmd_total",
"protocol_flags", "protocol_flags",
"server_data", "server_data",
"cmdset_storage_string" "cmdset_storage_string",
) )
# The following are used for the communications between the Portal and Server. # The following are used for the communications between the Portal and Server.
# Very dragons territory. # Very dragons territory.
AMP_SERVER_PROTOCOL_CLASS = 'evennia.server.portal.amp_server.AMPServerProtocol' AMP_SERVER_PROTOCOL_CLASS = "evennia.server.portal.amp_server.AMPServerProtocol"
AMP_CLIENT_PROTOCOL_CLASS = 'evennia.server.amp_client.AMPServerClientProtocol' AMP_CLIENT_PROTOCOL_CLASS = "evennia.server.amp_client.AMPServerClientProtocol"
###################################################################### ######################################################################