diff --git a/evennia/__init__.py b/evennia/__init__.py index 139cda5e25..9ed25a2669 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -214,7 +214,7 @@ def _init(): from .server.sessionhandler import SESSION_HANDLER from .comms.channelhandler import CHANNEL_HANDLER from .scripts.monitorhandler import MONITOR_HANDLER - from .scripts.globalhandler import GLOBAL_SCRIPTS + from .utils.containers import GLOBAL_SCRIPTS # initialize the doc string global __doc__ diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index f1cee5321e..4a17bedf7a 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -645,7 +645,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): guest = kwargs.get('guest', False) permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT) - typeclass = kwargs.get('typeclass', settings.BASE_ACCOUNT_TYPECLASS) + typeclass = kwargs.get('typeclass', cls) ip = kwargs.get('ip', '') if ip and CREATION_THROTTLE.check(ip): diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index e3104bd686..8c885d3458 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -256,6 +256,7 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)): try: kwargs['desc'] = kwargs.pop('description', '') + kwargs['typeclass'] = kwargs.get('typeclass', cls) obj = create.create_channel(key, *args, **kwargs) # Record creator id and creation IP diff --git a/evennia/scripts/globalhandler.py b/evennia/scripts/globalhandler.py deleted file mode 100644 index 4cce262993..0000000000 --- a/evennia/scripts/globalhandler.py +++ /dev/null @@ -1,69 +0,0 @@ -from django.conf import settings -from evennia.utils.utils import class_from_module - - -class GlobalContainer(object): - """ - Simple Handler object loaded by the Evennia API to contain and manage a game's Global Scripts. - - This is accessed like a dictionary. Alternatively you can access Properties on it. - - Example: - import evennia - evennia.GLOBAL_SCRIPTS['key'] - """ - - def __init__(self): - self.script_data = dict() - self.script_storage = dict() - self.script_data.update(settings.GLOBAL_SCRIPTS) - self.typeclass_storage = dict() - - for key, data in settings.GLOBAL_SCRIPTS.items(): - self.typeclass_storage[key] = class_from_module(data['typeclass']) - for key in self.script_data.keys(): - self._load_script(key) - - def __getitem__(self, item): - - # Likely to only reach this if someone called the API wrong. - if item not in self.typeclass_storage: - return None - - # The most common outcome next! - if self.script_storage[item]: - return self.script_storage[item] - else: - # Oops, something happened to our Global Script. Let's re-create it. - return self._load_script(item) - - def __getattr__(self, item): - return self[item] - - def _load_script(self, item): - typeclass = self.typeclass_storage[item] - found = typeclass.objects.filter(db_key=item).first() - interval = self.script_data[item].get('interval', None) - start_delay = self.script_data[item].get('start_delay', None) - repeats = self.script_data[item].get('repeats', 0) - desc = self.script_data[item].get('desc', '') - - if not found: - new_script, errors = typeclass.create(key=item, persistent=True, interval=interval, start_delay=start_delay, - repeats=repeats, desc=desc) - new_script.start() - self.script_storage[item] = new_script - return new_script - - if (found.interval != interval) or (found.start_delay != start_delay) or (found.repeats != repeats): - found.restart(interval=interval, start_delay=start_delay, repeats=repeats) - if found.desc != desc: - found.desc = desc - self.script_storage[item] = found - return found - - - - -# Create singleton of the GlobalHandler for the API. -GLOBAL_SCRIPTS = GlobalContainer() diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index e611ede0e2..10f466c8d6 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -342,6 +342,9 @@ class DefaultScript(ScriptBase): kwargs['key'] = key + # If no typeclass supplied, use this class + kwargs['typeclass'] = kwargs.pop('typeclass', cls) + try: obj = create.create_script(**kwargs) except Exception as e: diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 67886cc21a..0aa0a60526 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -542,20 +542,13 @@ PROTOTYPEFUNC_MODULES = ["evennia.utils.prototypefuncs", # Global Scripts ###################################################################### -# While any script that is not attached to any object is considered -# Global, any listed here will be started by Evennia during boot -# and attached to its API for an easy-lookup. This ensures the Script -# is always accessible, and re-created if it is somehow deleted. Use -# this for Scripts that absolutely MUST be running for your game as a -# simple way to get them launched. - -# The 'key' is a way to quickly index them, and it will also be the -# Script Typeclasss's key so it can be quickly retrieved. - -# Values are a dictionary that uses the example format. Available keys -# are typeclass (required), interval, repeats, start_delay, and desc -# only typeclass is required. - +# Global scripts started here will be available through +# 'evennia.GLOBAL_SCRIPTS.key'. The scripts will survive a reload and be +# recreated automatically if deleted. Each entry must have the script keys, +# whereas all other fields in the specification are optional. If 'typeclass' is +# not given, BASE_SCRIPT_TYPECLASS will be assumed. Note that if you change +# typeclass for the same key, a new Script will replace the old one on +# `evennia.GLOBAL_SCRIPTS`. GLOBAL_SCRIPTS = { # 'key': {'typeclass': 'typeclass.path.here', # 'repeats': -1, 'interval': 50, 'desc': 'Example script'}, diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py new file mode 100644 index 0000000000..8a286b44eb --- /dev/null +++ b/evennia/utils/containers.py @@ -0,0 +1,110 @@ +""" +Containers + +""" + + +from django.conf import settings +from evennia.utils.utils import class_from_module +from evennia.utils import logger + + +class GlobalScriptContainer(object): + """ + Simple Handler object loaded by the Evennia API to contain and manage a + game's Global Scripts. Scripts to start are defined by + `settings.GLOBAL_SCRIPTS`. + + Example: + import evennia + evennia.GLOBAL_SCRIPTS.scriptname + + """ + + def __init__(self): + """ + Initialize the container by preparing scripts. Lazy-load only when the + script is requested. + + """ + self.script_data = {key: {} if data is None else data + for key, data in settings.GLOBAL_SCRIPTS.items()} + self.script_storage = {} + self.typeclass_storage = {} + + for key, data in self.script_data.items(): + try: + typeclass = data.get('typeclass', settings.BASE_SCRIPT_TYPECLASS) + self.typeclass_storage[key] = class_from_module(typeclass) + except ImportError as err: + logger.log_err(f"GlobalContainer could not start global script {key}: {err}") + + def __getitem__(self, key): + + if key not in self.typeclass_storage: + # this script is unknown to the container + return None + + # (re)create script on-demand + return self.script_storage.get(key) or self._load_script(key) + + def __getattr__(self, key): + return self[key] + + def _load_script(self, key): + typeclass = self.typeclass_storage[key] + found = typeclass.objects.filter(db_key=key).first() + interval = self.script_data[key].get('interval', None) + start_delay = self.script_data[key].get('start_delay', None) + repeats = self.script_data[key].get('repeats', 0) + desc = self.script_data[key].get('desc', '') + + if not found: + new_script, errors = typeclass.create(key=key, persistent=True, + interval=interval, + start_delay=start_delay, + repeats=repeats, desc=desc) + if errors: + logger.log_err("\n".join(errors)) + return None + + new_script.start() + self.script_storage[key] = new_script + return new_script + + if ((found.interval != interval) or + (found.start_delay != start_delay) or + (found.repeats != repeats)): + found.restart(interval=interval, start_delay=start_delay, repeats=repeats) + if found.desc != desc: + found.desc = desc + self.script_storage[key] = found + return found + + def get(self, key): + """ + Retrive script by key (in case of not knowing it beforehand). + + Args: + key (str): The name of the script. + + Returns: + script (Script): The named global script. + + """ + # note that this will recreate the script if it doesn't exist/was lost + return self[key] + + def all(self): + """ + Get all scripts. + + Returns: + scripts (list): All global script objects stored on the container. + + """ + return list(self.script_storage.values()) + + +# Create singleton of the GlobalHandler for the API. +GLOBAL_SCRIPTS = GlobalScriptContainer() diff --git a/evennia/utils/idmapper/models.py b/evennia/utils/idmapper/models.py index 5381f2d8cf..642fc28d63 100644 --- a/evennia/utils/idmapper/models.py +++ b/evennia/utils/idmapper/models.py @@ -20,6 +20,7 @@ from django.core.exceptions import ObjectDoesNotExist, FieldError from django.db.models.signals import post_save from django.db.models.base import Model, ModelBase from django.db.models.signals import pre_delete, post_migrate +from django.db.utils import DatabaseError from evennia.utils import logger from evennia.utils.utils import dbref, get_evennia_pids, to_str @@ -391,7 +392,16 @@ class SharedMemoryModel(with_metaclass(SharedMemoryModelBase, Model)): if _IS_MAIN_THREAD: # in main thread - normal operation - super().save(*args, **kwargs) + try: + super().save(*args, **kwargs) + except DatabaseError: + # we handle the 'update_fields did not update any rows' error that + # may happen due to timing issues with attributes + ufields_removed = kwargs.pop('update_fields', None) + if ufields_removed: + super().save(*args, **kwargs) + else: + raise else: # in another thread; make sure to save in reactor thread def _save_callback(cls, *args, **kwargs):