From 0729bf10b7540444d33e9e6dd4b69e883b02ea0d Mon Sep 17 00:00:00 2001 From: Kelketek Rritaa Date: Sun, 29 Jun 2014 10:55:29 -0500 Subject: [PATCH 01/19] More consistent arrangement of base templates. --- .../{prosimii => evennia_general}/evennia_admin.html | 0 src/web/templates/{prosimii => evennia_general}/index.html | 0 src/web/templates/{prosimii => evennia_general}/tbi.html | 0 src/web/views.py | 7 +++---- 4 files changed, 3 insertions(+), 4 deletions(-) rename src/web/templates/{prosimii => evennia_general}/evennia_admin.html (100%) rename src/web/templates/{prosimii => evennia_general}/index.html (100%) rename src/web/templates/{prosimii => evennia_general}/tbi.html (100%) diff --git a/src/web/templates/prosimii/evennia_admin.html b/src/web/templates/evennia_general/evennia_admin.html similarity index 100% rename from src/web/templates/prosimii/evennia_admin.html rename to src/web/templates/evennia_general/evennia_admin.html diff --git a/src/web/templates/prosimii/index.html b/src/web/templates/evennia_general/index.html similarity index 100% rename from src/web/templates/prosimii/index.html rename to src/web/templates/evennia_general/index.html diff --git a/src/web/templates/prosimii/tbi.html b/src/web/templates/evennia_general/tbi.html similarity index 100% rename from src/web/templates/prosimii/tbi.html rename to src/web/templates/evennia_general/tbi.html diff --git a/src/web/views.py b/src/web/views.py index 078af83045..127b61360b 100644 --- a/src/web/views.py +++ b/src/web/views.py @@ -23,7 +23,6 @@ def page_index(request): # Some misc. configurable stuff. # TODO: Move this to either SQL or settings.py based configuration. fpage_player_limit = 4 - fpage_news_entries = 2 # A QuerySet of the most recently connected players. recent_users = PlayerDB.objects.get_recently_connected_players()[:fpage_player_limit] @@ -52,7 +51,7 @@ def page_index(request): "num_others": nothers or "no" } - return render(request, 'index.html', pagevars) + return render(request, 'evennia_general/index.html', pagevars) def to_be_implemented(request): @@ -65,7 +64,7 @@ def to_be_implemented(request): "page_title": "To Be Implemented...", } - return render(request, 'tbi.html', pagevars) + return render(request, 'evennia_general/tbi.html', pagevars) @staff_member_required @@ -74,7 +73,7 @@ def evennia_admin(request): Helpful Evennia-specific admin page. """ return render( - request, 'evennia_admin.html', { + request, 'evennia_general/evennia_admin.html', { 'playerdb': PlayerDB}) From df85edc295feddd61e061a56a9abb386425d65b4 Mon Sep 17 00:00:00 2001 From: Kelketek Rritaa Date: Sun, 29 Jun 2014 11:07:11 -0500 Subject: [PATCH 02/19] Made template overriding more consistent and created template_overrides dir. --- game/gamesrc/web/template_overrides/README.md | 4 ++++ src/settings_default.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 game/gamesrc/web/template_overrides/README.md diff --git a/game/gamesrc/web/template_overrides/README.md b/game/gamesrc/web/template_overrides/README.md new file mode 100644 index 0000000000..8fb5f1fdb8 --- /dev/null +++ b/game/gamesrc/web/template_overrides/README.md @@ -0,0 +1,4 @@ +Place your own version of templates into this file to override the default ones. +For instance, if there's a template at: `src/web/templates/evennia_general/index.html` +and you want to replace it, create the file `game/gamesrc/web/template_overrides/evennia_general/index.html` +and it will be loaded instead. \ No newline at end of file diff --git a/src/settings_default.py b/src/settings_default.py index 76dc73ad0e..10dc7cde28 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -514,7 +514,7 @@ STATICFILES_IGNORE_PATTERNS = ('README.md',) ACTIVE_TEMPLATE = 'prosimii' # We setup the location of the website template as well as the admin site. TEMPLATE_DIRS = ( - os.path.join(GAME_DIR, "gamesrc", "web", "templates"), + os.path.join(GAME_DIR, "gamesrc", "web", "template_overrides"), os.path.join(SRC_DIR, "web", "templates", ACTIVE_TEMPLATE), os.path.join(SRC_DIR, "web", "templates"),) # List of callables that know how to import templates from various sources. From 39def4efa9ab54c98fa97b1fd99b3b5aacd7dd8d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Jun 2014 21:58:30 +0200 Subject: [PATCH 03/19] Made cmdsethandler report syntax-errors in a more informative way. --- src/commands/cmdsethandler.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/commands/cmdsethandler.py b/src/commands/cmdsethandler.py index a32ebf859e..2056d1c931 100644 --- a/src/commands/cmdsethandler.py +++ b/src/commands/cmdsethandler.py @@ -103,7 +103,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): """ python_paths = [path] + ["%s.%s" % (prefix, path) - for prefix in _CMDSET_PATHS] + for prefix in _CMDSET_PATHS if not path.startswith(prefix)] errstring = "" for python_path in python_paths: try: @@ -123,16 +123,19 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): cmdsetclass = cmdsetclass(cmdsetobj) return cmdsetclass - except ImportError: - errstring += _("Error loading cmdset: Couldn't import module '%s'.") - errstring = errstring % modulepath + except ImportError, e: + errstring += _("Error loading cmdset: Couldn't import module '%s': %s.") + errstring = errstring % (modulepath, e) except KeyError: errstring += _("Error in loading cmdset: No cmdset class '%(classname)s' in %(modulepath)s.") errstring = errstring % {"classname": classname, "modulepath": modulepath} + except SyntaxError, e: + errstring += _("SyntaxError encountered when loading cmdset '%s': %s.") + errstring = errstring % (modulepath, e) except Exception: - errstring += _("Compile/Run error when loading cmdset '%s'. Error was logged.") - errstring = errstring % (python_path) + errstring += _("Compile/Run error when loading cmdset '%s': %s.") + errstring = errstring % (python_path, e) if errstring: # returning an empty error cmdset From 115587aa1b91a16546296cd83a3ec7fdfc367a44 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 30 Jun 2014 00:53:30 +0200 Subject: [PATCH 04/19] Fixed a bug with creating Player from admin. It is hard to add permissions (these are Tags with a special subsetting). This makes the admin not very useful for creating players at the moment. --- src/commands/cmdparser.py | 1 - src/players/admin.py | 42 +++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/commands/cmdparser.py b/src/commands/cmdparser.py index cc2292d748..8b48bb84eb 100644 --- a/src/commands/cmdparser.py +++ b/src/commands/cmdparser.py @@ -262,7 +262,6 @@ def at_multimatch_cmd(caller, matches): id1 = "" id2 = "" - print "cmd.obj:", cmd, cmd.obj if (not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller) and hasattr(cmd.obj, "key")): diff --git a/src/players/admin.py b/src/players/admin.py index 4dd89e684d..b96bab4e78 100644 --- a/src/players/admin.py +++ b/src/players/admin.py @@ -197,23 +197,31 @@ class PlayerDBAdmin(BaseUserAdmin): 'description': "These account details are shared by the admin " "system and the game."},),) - # TODO! Remove User reference! - def save_formset(self, request, form, formset, change): - """ - Run all hooks on the player object - """ - super(PlayerDBAdmin, self).save_formset(request, form, formset, change) - userobj = form.instance - userobj.name = userobj.username + def save_model(self, request, obj, form, change): + obj.save() if not change: - # uname, passwd, email = str(request.POST.get(u"username")), \ - # str(request.POST.get(u"password1")), \ - # str(request.POST.get(u"email")) - typeclass = str(request.POST.get( - u"playerdb_set-0-db_typeclass_path")) - create.create_player("", "", "", - user=userobj, - typeclass=typeclass, - player_dbobj=userobj) + #calling hooks for new player + ply = obj.typeclass + ply.basetype_setup() + ply.at_player_creation() + + ## TODO! Remove User reference! + #def save_formset(self, request, form, formset, change): + # """ + # Run all hooks on the player object + # """ + # super(PlayerDBAdmin, self).save_formset(request, form, formset, change) + # userobj = form.instance + # userobj.name = userobj.username + # if not change: + # # uname, passwd, email = str(request.POST.get(u"username")), \ + # # str(request.POST.get(u"password1")), \ + # # str(request.POST.get(u"email")) + # typeclass = str(request.POST.get( + # u"playerdb_set-0-db_typeclass_path")) + # create.create_player("", "", "", + # user=userobj, + # typeclass=typeclass, + # player_dbobj=userobj) admin.site.register(PlayerDB, PlayerDBAdmin) From ea059e9874b6a43b8531f1c799dcbd345d7fc6cd Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 30 Jun 2014 10:00:43 +0200 Subject: [PATCH 05/19] Renamed evennia_webclient.js to evennia_ajax_webclient.js to match the script name in the html file. --- .../js/{evennia_webclient.js => evennia_ajax_webclient.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/web/webclient/static/webclient/js/{evennia_webclient.js => evennia_ajax_webclient.js} (100%) diff --git a/src/web/webclient/static/webclient/js/evennia_webclient.js b/src/web/webclient/static/webclient/js/evennia_ajax_webclient.js similarity index 100% rename from src/web/webclient/static/webclient/js/evennia_webclient.js rename to src/web/webclient/static/webclient/js/evennia_ajax_webclient.js From cda13989f675f672ab50e1dd5d28521d7b0f667c Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 30 Jun 2014 20:14:58 +0200 Subject: [PATCH 06/19] Run Migrations! Made Tags unique based on the combination of their db_key, db_category AND their db_tagtype fields. --- ...category__add_unique_tag_db_key_db_cate.py | 62 +++++++++++++++++++ src/typeclasses/models.py | 4 +- src/utils/create.py | 56 +++++++---------- 3 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 src/typeclasses/migrations/0009_auto__del_unique_tag_db_key_db_category__add_unique_tag_db_key_db_cate.py diff --git a/src/typeclasses/migrations/0009_auto__del_unique_tag_db_key_db_category__add_unique_tag_db_key_db_cate.py b/src/typeclasses/migrations/0009_auto__del_unique_tag_db_key_db_category__add_unique_tag_db_key_db_cate.py new file mode 100644 index 0000000000..a3dc3557df --- /dev/null +++ b/src/typeclasses/migrations/0009_auto__del_unique_tag_db_key_db_category__add_unique_tag_db_key_db_cate.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Removing unique constraint on 'Tag', fields ['db_key', 'db_category'] + db.delete_unique(u'typeclasses_tag', ['db_key', 'db_category']) + + # Adding unique constraint on 'Tag', fields ['db_key', 'db_category', 'db_tagtype'] + db.create_unique(u'typeclasses_tag', ['db_key', 'db_category', 'db_tagtype']) + + # Removing index on 'Tag', fields ['db_key', 'db_category'] + db.delete_index(u'typeclasses_tag', ['db_key', 'db_category']) + + # Adding index on 'Tag', fields ['db_key', 'db_category', 'db_tagtype'] + db.create_index(u'typeclasses_tag', ['db_key', 'db_category', 'db_tagtype']) + + + def backwards(self, orm): + # Removing index on 'Tag', fields ['db_key', 'db_category', 'db_tagtype'] + db.delete_index(u'typeclasses_tag', ['db_key', 'db_category', 'db_tagtype']) + + # Adding index on 'Tag', fields ['db_key', 'db_category'] + db.create_index(u'typeclasses_tag', ['db_key', 'db_category']) + + # Removing unique constraint on 'Tag', fields ['db_key', 'db_category', 'db_tagtype'] + db.delete_unique(u'typeclasses_tag', ['db_key', 'db_category', 'db_tagtype']) + + # Adding unique constraint on 'Tag', fields ['db_key', 'db_category'] + db.create_unique(u'typeclasses_tag', ['db_key', 'db_category']) + + + models = { + u'typeclasses.attribute': { + 'Meta': {'object_name': 'Attribute'}, + 'db_attrtype': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'db_category': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_model': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'db_strvalue': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'db_value': ('src.utils.picklefield.PickledObjectField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'typeclasses.tag': { + 'Meta': {'unique_together': "(('db_key', 'db_category', 'db_tagtype'),)", 'object_name': 'Tag', 'index_together': "(('db_key', 'db_category', 'db_tagtype'),)"}, + 'db_category': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'db_index': 'True'}), + 'db_data': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'db_model': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'db_index': 'True'}), + 'db_tagtype': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'db_index': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + } + } + + complete_apps = ['typeclasses'] \ No newline at end of file diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 2d96558fae..6e93da41c9 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -539,8 +539,8 @@ class Tag(models.Model): class Meta: "Define Django meta options" verbose_name = "Tag" - unique_together = (('db_key', 'db_category'),) - index_together = (('db_key', 'db_category'),) + unique_together = (('db_key', 'db_category', 'db_tagtype'),) + index_together = (('db_key', 'db_category', 'db_tagtype'),) def __unicode__(self): return u"%s" % self.db_key diff --git a/src/utils/create.py b/src/utils/create.py index cfcf8a903a..bce3c76f62 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -116,28 +116,32 @@ def create_object(typeclass=None, key=None, location=None, elif isinstance(typeclass, _Object) or utils.inherits_from(typeclass, _Object): # this is already an object typeclass, extract its path typeclass = typeclass.path + typeclass = utils.to_unicode(typeclass) + + # Setup input for the create command - # handle eventual #dbref input location = handle_dbref(location, _ObjectDB) - home = handle_dbref(home, _ObjectDB) destination = handle_dbref(destination, _ObjectDB) report_to = handle_dbref(report_to, _ObjectDB) + home = handle_dbref(home, _ObjectDB) + if not home: + try: + home = handle_dbref(settings.DEFAULT_HOME, _ObjectDB) if not nohome else None + except _ObjectDB.DoesNotExist: + raise _ObjectDB.DoesNotExist("settings.DEFAULT_HOME (= '%s') does not exist, or the setting is malformed." % + settings.DEFAULT_HOME) - # create new database object - new_db_object = _ObjectDB() - - # assign the typeclass - typeclass = utils.to_unicode(typeclass) - new_db_object.typeclass_path = typeclass + # create new database object all in one go + new_db_object = _ObjectDB(db_key=key, db_location=location, + db_destination=destination, db_home=home, + db_typeclass_path=typeclass) # the name/key is often set later in the typeclass. This # is set here as a failsafe. - if key: - new_db_object.key = key - else: + if not key: new_db_object.key = "#%i" % new_db_object.dbid - # this will either load the typeclass or the default one + # this will either load the typeclass or the default one (will also save object) new_object = new_db_object.typeclass if not _GA(new_object, "is_typeclass")(typeclass, exact=True): @@ -154,15 +158,15 @@ def create_object(typeclass=None, key=None, location=None, # from now on we can use the typeclass object # as if it was the database object. - new_object.destination = destination - - # call the hook method. This is where all at_creation + # call the hook methods. This is where all at_creation # customization happens as the typeclass stores custom # things on its database object. + + # note - this will override eventual custom keys, locations etc! new_object.basetype_setup() # setup the basics of Exits, Characters etc. new_object.at_object_creation() - # custom-given perms/locks overwrite hooks + # custom-given perms/locks do overwrite hooks if permissions: new_object.permissions.add(permissions) if locks: @@ -170,28 +174,14 @@ def create_object(typeclass=None, key=None, location=None, if aliases: new_object.aliases.add(aliases) - if home: - new_object.home = home - else: - # we shouldn't need to handle dbref here (home handler should fix it), but some have - # reported issues here (issue 446). - try: - new_object.home = handle_dbref(settings.DEFAULT_HOME, _ObjectDB) if not nohome else None - except _ObjectDB.DoesNotExist: - raise _ObjectDB.DoesNotExist("settings.DEFAULT_HOME (= '%s') does not exist, or the setting is malformed." % - settings.DEFAULT_HOME) - - # perform a move_to in order to display eventual messages. + # trigger relevant move_to hooks in order to display eventual messages. if location: - new_object.move_to(location, quiet=True) - else: - # rooms would have location=None. - new_object.location = None + new_object.at_object_receive(new_object, None) + new_object.at_after_move(new_object) # post-hook setup (mainly used by Exits) new_object.basetype_posthook_setup() - new_object.save() return new_object #alias for create_object From 05d21ef4f7da46b616019a7ddb363a7235b01788 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 1 Jul 2014 00:46:20 +0200 Subject: [PATCH 07/19] Updated create.create_object to be faster. Made AttributeHandler.add() handle bulk-adding of Attributes and with a slightly more efficient way to create new Attributes. --- src/typeclasses/models.py | 71 +++++++++++++++++++++++++++------------ src/utils/create.py | 31 ++++++++++++----- src/utils/picklefield.py | 2 ++ 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 6e93da41c9..e2b30917b2 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -321,6 +321,12 @@ class AttributeHandler(object): return ret if len(key) > 1 else default return ret[0] if len(ret)==1 else ret + def batch_add(self, keys, values, categories=None, lockstrings=None, + stratts=None, accessing_obj=None, default_access=True): + """ + Batch version supporting the addition of more than one + """ + def add(self, key, value, category=None, lockstring="", strattr=False, accessing_obj=None, default_access=True): """ @@ -332,6 +338,12 @@ class AttributeHandler(object): If accessing_obj is given, self.obj's 'attrcreate' lock access will be checked against it. If no accessing_obj is given, no check will be done. + + The method also accepts multiple attributes (this is a faster way + to add attributes since it allows for some optimizations). + If so, key and value (or strvalue) must be iterables of the same length. + All batch-added Attributes will use the same category and lockstring. + """ if accessing_obj and not self.obj.access(accessing_obj, self._attrcreate, default=default_access): @@ -341,28 +353,41 @@ class AttributeHandler(object): self._recache() if not key: return - key = key.strip().lower() + + keys, values= make_iter(key), make_iter(value) + + if len(keys) != len(values): + raise RuntimeError("AttributeHandler.add(): key and value of different length: %s vs %s" % key, value) category = category.strip().lower() if category is not None else None - cachekey = "%s-%s" % (key, category) - attr_obj = self._cache.get(cachekey) - if not attr_obj: - # no old attr available; create new. - attr_obj = Attribute(db_key=key, db_category=category, - db_model=self._model, db_attrtype=self._attrtype) - attr_obj.save() # important - getattr(self.obj, self._m2m_fieldname).add(attr_obj) - self._cache[cachekey] = attr_obj - if lockstring: - attr_obj.locks.add(lockstring) - # we shouldn't need to fear stale objects, the field signalling - # should catch all cases - if strattr: - # store as a simple string - attr_obj.db_strvalue = value - attr_obj.save(update_fields=["db_strvalue"]) - else: - # pickle arbitrary data - attr_obj.value = value + new_attrobjs = [] + for ikey, keystr in enumerate(keys): + keystr = keystr.strip().lower() + new_value = values[ikey] + cachekey = "%s-%s" % (keystr, category) + attr_obj = self._cache.get(cachekey) + + if attr_obj: + # update an existing attribute object + if strattr: + # store as a simple string (will not notify OOB handlers) + attr_obj.db_strvalue = new_value + attr_obj.save(update_fields=["db_strvalue"]) + else: + # store normally (this will also notify OOB handlers) + attr_obj.value = new_value + else: + # create a new Attribute (no OOB handlers can be notified) + kwargs = {"db_key" : keystr, "db_category" : category, + "db_model" : self._model, "db_attrtype" : self._attrtype, + "db_value" : None if strattr else to_pickle(new_value), + "db_strvalue" : value if strattr else None} + new_attr = Attribute(**kwargs) + new_attr.save() + new_attrobjs.append(new_attr) + if new_attrobjs: + # Add new objects to m2m field all at once + getattr(self.obj, self._m2m_fieldname).add(*new_attrobjs) + self._recache() def remove(self, key, raise_exception=False, category=None, accessing_obj=None, default_access=True): @@ -581,8 +606,10 @@ class TagHandler(object): self.obj, self._m2m_fieldname).filter( db_model=self._model).filter(tagtype)) - def add(self, tag, category=None, data=None): + def add(self, tag=None, category=None, data=None): "Add a new tag to the handler. Tag is a string or a list of strings." + if not tag: + return for tagstr in make_iter(tag): if not tagstr: continue diff --git a/src/utils/create.py b/src/utils/create.py index bce3c76f62..13052d207f 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -59,7 +59,12 @@ def handle_dbref(inp, objclass, raise_errors=True): objects. """ if not (isinstance(inp, basestring) and inp.startswith("#")): - return inp + try: + return inp.dbobj + except AttributeError: + return inp + + # a string, analyze it inp = inp.lstrip('#') try: if int(inp) < 0: @@ -67,7 +72,7 @@ def handle_dbref(inp, objclass, raise_errors=True): except ValueError: return None - # if we get to this point, inp is an integer dbref + # if we get to this point, inp is an integer dbref; get the matching object try: return objclass.objects.get(id=inp) except Exception: @@ -98,7 +103,7 @@ def create_object(typeclass=None, key=None, location=None, containing the error message. If set, this method will return None upon errors. nohome - this allows the creation of objects without a default home location; - this only used when creating default location itself or during unittests + this only used when creating the default location itself or during unittests """ global _Object, _ObjectDB if not _Object: @@ -122,7 +127,6 @@ def create_object(typeclass=None, key=None, location=None, location = handle_dbref(location, _ObjectDB) destination = handle_dbref(destination, _ObjectDB) - report_to = handle_dbref(report_to, _ObjectDB) home = handle_dbref(home, _ObjectDB) if not home: try: @@ -136,9 +140,8 @@ def create_object(typeclass=None, key=None, location=None, db_destination=destination, db_home=home, db_typeclass_path=typeclass) - # the name/key is often set later in the typeclass. This - # is set here as a failsafe. if not key: + # the object should always have a key, so if not set we give a default new_db_object.key = "#%i" % new_db_object.dbid # this will either load the typeclass or the default one (will also save object) @@ -149,6 +152,7 @@ def create_object(typeclass=None, key=None, location=None, # gave us a default SharedMemoryModel.delete(new_db_object) if report_to: + report_to = handle_dbref(report_to, _ObjectDB) _GA(report_to, "msg")("Error creating %s (%s).\n%s" % (new_db_object.key, typeclass, _GA(new_db_object, "typeclass_last_errmsg"))) return None @@ -162,10 +166,21 @@ def create_object(typeclass=None, key=None, location=None, # customization happens as the typeclass stores custom # things on its database object. - # note - this will override eventual custom keys, locations etc! + # note - this may override input keys, locations etc! new_object.basetype_setup() # setup the basics of Exits, Characters etc. new_object.at_object_creation() + # we want the input to override that set in the hooks, so + # we re-apply those if needed + if new_object.key != key: + new_object.key = key + if new_object.location != location: + new_object.location = location + if new_object.home != home: + new_object.home = home + if new_object.destination != destination: + new_object.destination = destination + # custom-given perms/locks do overwrite hooks if permissions: new_object.permissions.add(permissions) @@ -174,7 +189,7 @@ def create_object(typeclass=None, key=None, location=None, if aliases: new_object.aliases.add(aliases) - # trigger relevant move_to hooks in order to display eventual messages. + # trigger relevant move_to hooks in order to display messages. if location: new_object.at_object_receive(new_object, None) new_object.at_after_move(new_object) diff --git a/src/utils/picklefield.py b/src/utils/picklefield.py index 3c59ad31f9..49da9f0ebe 100644 --- a/src/utils/picklefield.py +++ b/src/utils/picklefield.py @@ -43,6 +43,8 @@ from django.forms import CharField, Textarea from django.forms.util import flatatt from django.utils.html import format_html +from src.utils.dbserialize import from_pickle, to_pickle + try: from django.utils.encoding import force_text except ImportError: From d05c92792c7cd9183936002f37bbda481a4cc003 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 1 Jul 2014 01:10:44 +0200 Subject: [PATCH 08/19] Some cleanup and making unittests succeed again --- src/commands/default/tests.py | 10 +++---- src/typeclasses/models.py | 54 +++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/commands/default/tests.py b/src/commands/default/tests.py index 4b31cc8cd1..05ef9b60c7 100644 --- a/src/commands/default/tests.py +++ b/src/commands/default/tests.py @@ -21,9 +21,9 @@ from src.players.player import Player from src.utils import create, ansi from src.server.sessionhandler import SESSIONS -from django.db.models.signals import pre_save -from src.server.caches import field_pre_save -pre_save.connect(field_pre_save, dispatch_uid="fieldcache") +from django.db.models.signals import post_save +from src.server.caches import field_post_save +post_save.connect(field_post_save, dispatch_uid="fieldcache") # set up signal here since we are not starting the server @@ -78,12 +78,12 @@ class CommandTest(TestCase): CID = 0 # we must set a different CID in every test to avoid unique-name collisions creating the objects def setUp(self): "sets up testing environment" - settings.DEFAULT_HOME = "#2" #print "creating player %i: %s" % (self.CID, self.__class__.__name__) self.player = create.create_player("TestPlayer%i" % self.CID, "test@test.com", "testpassword", typeclass=TestPlayerClass) self.player2 = create.create_player("TestPlayer%ib" % self.CID, "test@test.com", "testpassword", typeclass=TestPlayerClass) self.room1 = create.create_object("src.objects.objects.Room", key="Room%i"%self.CID, nohome=True) self.room1.db.desc = "room_desc" + settings.DEFAULT_HOME = "#%i" % self.room1.id # we must have a default home self.room2 = create.create_object("src.objects.objects.Room", key="Room%ib" % self.CID) self.obj1 = create.create_object(TestObjectClass, key="Obj%i" % self.CID, location=self.room1, home=self.room1) self.obj2 = create.create_object(TestObjectClass, key="Obj%ib" % self.CID, location=self.room1, home=self.room1) @@ -272,7 +272,7 @@ class TestComms(CommandTest): self.call(comms.CmdCdesc(), "testchan = Test Channel", "Description of channel 'testchan' set to 'Test Channel'.") self.call(comms.CmdCemit(), "testchan = Test Message", "[testchan] Test Message|Sent to channel testchan: Test Message") self.call(comms.CmdCWho(), "testchan", "Channel subscriptions\ntestchan:\n TestPlayer7") - self.call(comms.CmdPage(), "TestPlayer7b = Test", "You paged TestPlayer7b with: 'Test'.") + self.call(comms.CmdPage(), "TestPlayer7b = Test", "TestPlayer7b is offline. They will see your message if they list their pages later.|You paged TestPlayer7b with: 'Test'.") self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] = [:reason]") # noone else connected to boot self.call(comms.CmdCdestroy(), "testchan" ,"[testchan] TestPlayer7: testchan is being destroyed. Make sure to change your aliases.|Channel 'testchan' was destroyed.") diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index e2b30917b2..f31634ab6c 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -321,11 +321,6 @@ class AttributeHandler(object): return ret if len(key) > 1 else default return ret[0] if len(ret)==1 else ret - def batch_add(self, keys, values, categories=None, lockstrings=None, - stratts=None, accessing_obj=None, default_access=True): - """ - Batch version supporting the addition of more than one - """ def add(self, key, value, category=None, lockstring="", strattr=False, accessing_obj=None, default_access=True): @@ -338,11 +333,50 @@ class AttributeHandler(object): If accessing_obj is given, self.obj's 'attrcreate' lock access will be checked against it. If no accessing_obj is given, no check will be done. + """ + if accessing_obj and not self.obj.access(accessing_obj, + self._attrcreate, default=default_access): + # check create access + return + if self._cache is None: + self._recache() + if not key: + return - The method also accepts multiple attributes (this is a faster way - to add attributes since it allows for some optimizations). - If so, key and value (or strvalue) must be iterables of the same length. - All batch-added Attributes will use the same category and lockstring. + category = category.strip().lower() if category is not None else None + keystr = key.strip().lower() + cachekey = "%s-%s" % (keystr, category) + attr_obj = self._cache.get(cachekey) + + if attr_obj: + # update an existing attribute object + if strattr: + # store as a simple string (will not notify OOB handlers) + attr_obj.db_strvalue = value + attr_obj.save(update_fields=["db_strvalue"]) + else: + # store normally (this will also notify OOB handlers) + attr_obj.value = value + else: + # create a new Attribute (no OOB handlers can be notified) + kwargs = {"db_key" : keystr, "db_category" : category, + "db_model" : self._model, "db_attrtype" : self._attrtype, + "db_value" : None if strattr else to_pickle(value), + "db_strvalue" : value if strattr else None} + new_attr = Attribute(**kwargs) + new_attr.save() + getattr(self.obj, self._m2m_fieldname).add(new_attr) + self._cache[cachekey] = new_attr + + + def batch_add(self, key, value, category=None, lockstring="", + strattr=False, accessing_obj=None, default_access=True): + """ + Batch-version of add(). This is more efficient than + repeat-calling add. + + key and value must be sequences of the same length, each + representing a key-value pair. """ if accessing_obj and not self.obj.access(accessing_obj, @@ -389,6 +423,7 @@ class AttributeHandler(object): getattr(self.obj, self._m2m_fieldname).add(*new_attrobjs) self._recache() + def remove(self, key, raise_exception=False, category=None, accessing_obj=None, default_access=True): """Remove attribute or a list of attributes from object. @@ -441,6 +476,7 @@ class AttributeHandler(object): else: return self._cache.values() + class NickHandler(AttributeHandler): """ Handles the addition and removal of Nicks From 6eafe650762181f43b80a8a629483e74b2ed38ec Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 1 Jul 2014 02:14:48 +0200 Subject: [PATCH 09/19] Implemented src.utils.spawner along with a test command @spawn. This allows for spawning individualized objects based on a prototype dictionary rather than having to make a new Typeclass for small changes. Allows for setting basid properties as well as Attributes and NAttributes. Supports prototype multiple inheritance (see header of src/utils/spawner.py) --- src/commands/default/building.py | 195 ++++++++++++------- src/commands/default/cmdset_character.py | 1 + src/utils/spawner.py | 229 +++++++++++++++++++++++ 3 files changed, 357 insertions(+), 68 deletions(-) create mode 100644 src/utils/spawner.py diff --git a/src/commands/default/building.py b/src/commands/default/building.py index f040ba843c..909bcd756f 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -1169,6 +1169,74 @@ class CmdOpen(ObjManipCommand): back_exit_typeclass) +def _convert_from_string(cmd, strobj): + """ + Converts a single object in *string form* to its equivalent python + type. + + Python earlier than 2.6: + Handles floats, ints, and limited nested lists and dicts + (can't handle lists in a dict, for example, this is mainly due to + the complexity of parsing this rather than any technical difficulty - + if there is a need for @set-ing such complex structures on the + command line we might consider adding it). + Python 2.6 and later: + Supports all Python structures through literal_eval as long as they + are valid Python syntax. If they are not (such as [test, test2], ie + withtout the quotes around the strings), the entire structure will + be converted to a string and a warning will be given. + + We need to convert like this since all data being sent over the + telnet connection by the Player is text - but we will want to + store it as the "real" python type so we can do convenient + comparisons later (e.g. obj.db.value = 2, if value is stored as a + string this will always fail). + """ + + def rec_convert(obj): + """ + Helper function of recursive conversion calls. This is only + used for Python <=2.5. After that literal_eval is available. + """ + # simple types + try: + return int(obj) + except ValueError: + pass + try: + return float(obj) + except ValueError: + pass + # iterables + if obj.startswith('[') and obj.endswith(']'): + "A list. Traverse recursively." + return [rec_convert(val) for val in obj[1:-1].split(',')] + if obj.startswith('(') and obj.endswith(')'): + "A tuple. Traverse recursively." + return tuple([rec_convert(val) for val in obj[1:-1].split(',')]) + if obj.startswith('{') and obj.endswith('}') and ':' in obj: + "A dict. Traverse recursively." + return dict([(rec_convert(pair.split(":", 1)[0]), + rec_convert(pair.split(":", 1)[1])) + for pair in obj[1:-1].split(',') if ":" in pair]) + # if nothing matches, return as-is + return obj + + if _LITERAL_EVAL: + # Use literal_eval to parse python structure exactly. + try: + return _LITERAL_EVAL(strobj) + except (SyntaxError, ValueError): + # treat as string + string = "{RNote: Value was converted to string. If you don't want this, " + string += "use proper Python syntax, like enclosing strings in quotes.{n" + cmd.caller.msg(string) + return utils.to_str(strobj) + else: + # fall back to old recursive solution (does not support + # nested lists/dicts) + return rec_convert(strobj.strip()) + class CmdSetAttribute(ObjManipCommand): """ set attribute on an object or player @@ -1202,73 +1270,6 @@ class CmdSetAttribute(ObjManipCommand): locks = "cmd:perm(set) or perm(Builders)" help_category = "Building" - def convert_from_string(self, strobj): - """ - Converts a single object in *string form* to its equivalent python - type. - - Python earlier than 2.6: - Handles floats, ints, and limited nested lists and dicts - (can't handle lists in a dict, for example, this is mainly due to - the complexity of parsing this rather than any technical difficulty - - if there is a need for @set-ing such complex structures on the - command line we might consider adding it). - Python 2.6 and later: - Supports all Python structures through literal_eval as long as they - are valid Python syntax. If they are not (such as [test, test2], ie - withtout the quotes around the strings), the entire structure will - be converted to a string and a warning will be given. - - We need to convert like this since all data being sent over the - telnet connection by the Player is text - but we will want to - store it as the "real" python type so we can do convenient - comparisons later (e.g. obj.db.value = 2, if value is stored as a - string this will always fail). - """ - - def rec_convert(obj): - """ - Helper function of recursive conversion calls. This is only - used for Python <=2.5. After that literal_eval is available. - """ - # simple types - try: - return int(obj) - except ValueError: - pass - try: - return float(obj) - except ValueError: - pass - # iterables - if obj.startswith('[') and obj.endswith(']'): - "A list. Traverse recursively." - return [rec_convert(val) for val in obj[1:-1].split(',')] - if obj.startswith('(') and obj.endswith(')'): - "A tuple. Traverse recursively." - return tuple([rec_convert(val) for val in obj[1:-1].split(',')]) - if obj.startswith('{') and obj.endswith('}') and ':' in obj: - "A dict. Traverse recursively." - return dict([(rec_convert(pair.split(":", 1)[0]), - rec_convert(pair.split(":", 1)[1])) - for pair in obj[1:-1].split(',') if ":" in pair]) - # if nothing matches, return as-is - return obj - - if _LITERAL_EVAL: - # Use literal_eval to parse python structure exactly. - try: - return _LITERAL_EVAL(strobj) - except (SyntaxError, ValueError): - # treat as string - string = "{RNote: Value was converted to string. If you don't want this, " - string += "use proper Python syntax, like enclosing strings in quotes.{n" - self.caller.msg(string) - return utils.to_str(strobj) - else: - # fall back to old recursive solution (does not support - # nested lists/dicts) - return rec_convert(strobj.strip()) def func(self): "Implement the set attribute - a limited form of @py." @@ -1318,7 +1319,7 @@ class CmdSetAttribute(ObjManipCommand): # setting attribute(s). Make sure to convert to real Python type before saving. for attr in attrs: try: - obj.attributes.add(attr, self.convert_from_string(value)) + obj.attributes.add(attr, _convert_from_string(self, value)) string += "\nCreated attribute %s/%s = %s" % (obj.name, attr, value) except SyntaxError: # this means literal_eval tried to parse a faulty string @@ -2242,3 +2243,61 @@ class CmdTag(MuxCommand): else: string = "No tags attached to %s." % obj self.caller.msg(string) + +class CmdSpawn(MuxCommand): + """ + spawn objects from prototype + + Usage: + @spawn {prototype dictionary} + + Example: + @spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"} + + Dictionary keys: + {wkey {n - string, the main object identifier + {wtypeclass {n - string, if not set, will use settings.BASE_OBJECT_TYPECLASS + {wlocation {n - this should be a valid object or #dbref + {whome {n - valid object or #dbref + {wdestination{n - only valid for exits (object or dbref) + {wpermissions{n - string or list of permission strings + {wlocks {n - a lock-string + {waliases {n - string or list of strings + {wndb_{n - value of a nattribute (ndb_ is stripped) + any other keywords are interpreted as Attributes and their values. + + This command can't access prototype inheritance. + """ + + key = "@spawn" + locks = "cmd:perm(spawn) or perm(Builders)" + help_category = "Building" + + def func(self): + "Implements the spawn" + if not self.args: + self.caller.msg("Usage: @spawn {key:value, key, value, ...}") + return + from src.utils.spawner import spawn + + try: + # make use of _convert_from_string from the SetAttribute command + prototype = _convert_from_string(self, self.args) + except SyntaxError: + # this means literal_eval tried to parse a faulty string + string = "{RCritical Python syntax error in argument. " + string += "Only primitive Python structures are allowed. " + string += "\nYou also need to use correct Python syntax. " + string += "Remember especially to put quotes around all " + string += "strings inside lists and dicts.{n" + self.caller.msg(string) + return + + if not isinstance(prototype, dict): + self.caller.msg("The prototype must be a Python dictionary.") + return + + for obj in spawn(prototype): + self.caller.msg("Spawned %s." % obj.key) + + diff --git a/src/commands/default/cmdset_character.py b/src/commands/default/cmdset_character.py index 35292bdd8b..b7d533c139 100644 --- a/src/commands/default/cmdset_character.py +++ b/src/commands/default/cmdset_character.py @@ -80,6 +80,7 @@ class CharacterCmdSet(CmdSet): self.add(building.CmdScript()) self.add(building.CmdSetHome()) self.add(building.CmdTag()) + self.add(building.CmdSpawn()) # Batchprocessor commands self.add(batchprocess.CmdBatchCommands()) diff --git a/src/utils/spawner.py b/src/utils/spawner.py new file mode 100644 index 0000000000..ba3005e09a --- /dev/null +++ b/src/utils/spawner.py @@ -0,0 +1,229 @@ +""" +Spawner + +The spawner takes input files containing object definitions in +dictionary forms. These use a prototype architechture to define +unique objects without having to make a Typeclass for each. + +The main function is spawn(*prototype), where the prototype +is a dictionary like this: + +GOBLIN = { + "typeclass": "game.gamesrc.objects.objects.Monster", + "key": "goblin grunt", + "health": lambda: randint(20,30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + } + +Possible keywords are: + prototype - dict, parent prototype of this structure (see below) + key - string, the main object identifier + typeclass - string, if not set, will use settings.BASE_OBJECT_TYPECLASS + location - this should be a valid object or #dbref + home - valid object or #dbref + destination - only valid for exits (object or dbref) + + permissions - string or list of permission strings + locks - a lock-string + aliases - string or list of strings + + ndb_ - value of a nattribute (ndb_ is stripped) + any other keywords are interpreted as Attributes and their values. + +Each value can also be a callable that takes no arguments. It should +return the value to enter into the field and will be called every time +the prototype is used to spawn an object. + +By specifying a prototype, the child will inherit all prototype slots +it does not explicitly define itself, while overloading those that it +does specify. + +GOBLIN_WIZARD = { + "prototype": GOBLIN, + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + } + +GOBLIN_ARCHER = { + "prototype": GOBLIN, + "key": "goblin archer", + "attacks": ["short bow"] +} + +One can also have multiple prototypes. These are inherited from the +left, with the ones further to the right taking precedence. + +ARCHWIZARD = { + "attack": ["archwizard staff", "eye of doom"] + +GOBLIN_ARCHWIZARD = { + "key" : "goblin archwizard" + "prototype": (GOBLIN_WIZARD, ARCHWIZARD), +} + +The goblin archwizard will have some different attacks, but will +otherwise have the same spells as a goblin wizard who in turn shares +many traits with a normal goblin. + +""" + +import os, sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' + +from django.conf import settings +from random import randint +from src.objects.models import ObjectDB +from src.utils.create import handle_dbref + +_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") + +_handle_dbref = lambda inp: handle_dbref(inp, ObjectDB) + + +def _get_prototype(dic, prot): + "Recursively traverse a prototype dictionary, including multiple inheritance" + if "prototype" in dic: + # move backwards through the inheritance + prototypes = dic["prototype"] + if isinstance(prototypes, dict): + prototypes = (prototypes,) + for prototype in prototypes: + # Build the prot dictionary in reverse order, overloading + new_prot = _get_prototype(prototype, prot) + prot.update(new_prot) + prot.update(dic) + prot.pop("prototype", None) # we don't need this anymore + return prot + + +def _batch_create_object(*objparams): + """ + This is a cut-down version of the create_object() function, + optimized for speed. It does NOT check and convert various input + so make sure the spawned Typeclass works before using this! + + Input: + objsparams - each argument should be a tuple of arguments for the respective + creation/add handlers in the following order: + (create, permissions, locks, aliases, nattributes, attributes) + Returns: + A list of created objects + """ + + # bulk create all objects in one go + dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] + # unfortunately this doesn't work since bulk_create don't creates pks; + # the result are double objects at the next stage + #dbobjs = _ObjectDB.objects.bulk_create(dbobjs) + + objs = [] + for iobj, dbobj in enumerate(dbobjs): + # call all setup hooks on each object + objparam = objparams[iobj] + obj = dbobj.typeclass # this saves dbobj if not done already + obj.basetype_setup() + obj.at_object_creation() + + if objparam[1]: + # permissions + obj.permissions.add(objparam[1]) + if objparam[2]: + # locks + obj.locks.add(objparam[2]) + if objparam[3]: + # aliases + obj.aliases.add(objparam[3]) + if objparam[4]: + # nattributes + for key, value in objparam[4].items(): + obj.nattributes.add(key, value) + if objparam[5]: + # attributes + keys, values = objparam[5].keys(), objparam[5].values() + obj.attributes.batch_add(keys, values) + + obj.basetype_posthook_setup() + objs.append(obj) + return objs + + +def spawn(*prototypes): + """ + Spawn a number of prototyped objects. Each argument should be a + prototype dictionary. + """ + objsparams = [] + + for prototype in prototypes: + + prot = _get_prototype(prototype, {}) + if not prot: + continue + + # extract the keyword args we need to create the object itself + create_kwargs = {} + create_kwargs["db_key"] = prot.pop("key", "Spawned Object %06i" % randint(1,100000)) + create_kwargs["db_location"] = _handle_dbref(prot.pop("location", None)) + create_kwargs["db_home"] = _handle_dbref(prot.pop("home", settings.DEFAULT_HOME)) + create_kwargs["db_destination"] = _handle_dbref(prot.pop("destination", None)) + create_kwargs["db_typeclass_path"] = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) + + # extract calls to handlers + permission_string = prot.pop("permissions", "") + lock_string = prot.pop("locks", "") + alias_string = prot.pop("aliases", "") + + # extract ndb assignments + nattributes = dict((key.split("_", 1)[1], value if callable(value) else value) + for key, value in prot.items() if key.startswith("ndb_")) + + # the rest are attributes + attributes = dict((key, value() if callable(value) else value) + for key, value in prot.items() + if not (key in _CREATE_OBJECT_KWARGS or key in nattributes)) + + # pack for call into _batch_create_object + objsparams.append( (create_kwargs, permission_string, lock_string, + alias_string, nattributes, attributes) ) + + return _batch_create_object(*objsparams) + + +if __name__ == "__main__": + # testing + + NOBODY = {} + + GOBLIN = { + "key": "goblin grunt", + "health": lambda: randint(20,30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + } + + GOBLIN_WIZARD = { + "prototype": GOBLIN, + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + } + + GOBLIN_ARCHER = { + "prototype": GOBLIN, + "key": "goblin archer", + "attacks": ["short bow"] + } + + ARCHWIZARD = { + "attacks": ["archwizard staff"], + } + + GOBLIN_ARCHWIZARD = { + "key": "goblin archwizard", + "prototype" : (GOBLIN_WIZARD, ARCHWIZARD) + } + # test + print [o.key for o in spawn(GOBLIN, GOBLIN_ARCHWIZARD)] From 221d56fecd3a2e778e27714a236d1141f804b0f5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 1 Jul 2014 04:04:54 +0200 Subject: [PATCH 10/19] Made the spawner more streamlined, and allowed the @spawn command to access prototypes in a file specified by the optional PROTOTYPE_MODULES setting. --- game/gamesrc/world/examples/prototypes.py | 56 +++++++++++++++++ src/commands/default/building.py | 47 +++++++++++--- src/utils/spawner.py | 76 ++++++++++++----------- src/utils/utils.py | 2 + 4 files changed, 137 insertions(+), 44 deletions(-) create mode 100644 game/gamesrc/world/examples/prototypes.py diff --git a/game/gamesrc/world/examples/prototypes.py b/game/gamesrc/world/examples/prototypes.py new file mode 100644 index 0000000000..537616f103 --- /dev/null +++ b/game/gamesrc/world/examples/prototypes.py @@ -0,0 +1,56 @@ +""" +Example prototypes read by the @spawn command but is also easily +available to use from code. Each prototype should be a dictionary. Use +the same name as the variable to refer to other prototypes. + +Possible keywords are: + prototype - string pointing to parent prototype of this structure + key - string, the main object identifier + typeclass - string, if not set, will use settings.BASE_OBJECT_TYPECLASS + location - this should be a valid object or #dbref + home - valid object or #dbref + destination - only valid for exits (object or dbref) + + permissions - string or list of permission strings + locks - a lock-string + aliases - string or list of strings + + ndb_ - value of a nattribute (ndb_ is stripped) + any other keywords are interpreted as Attributes and their values. + +See the @spawn command and src.utils.spawner for more info. + +""" + +from random import randint + +NOBODY = {} + +GOBLIN = { + "key": "goblin grunt", + "health": lambda: randint(20,30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + } + +GOBLIN_WIZARD = { + "prototype": "GOBLIN", + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + } + +GOBLIN_ARCHER = { + "prototype": "GOBLIN", + "key": "goblin archer", + "attacks": ["short bow"] +} + +ARCHWIZARD = { + "attacks": ["archwizard staff"], +} + +GOBLIN_ARCHWIZARD = { + "key": "goblin archwizard", + "prototype" : ("GOBLIN_WIZARD", "ARCHWIZARD") +} diff --git a/src/commands/default/building.py b/src/commands/default/building.py index 909bcd756f..cf21b03148 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -19,7 +19,7 @@ __all__ = ("ObjManipCommand", "CmdSetObjAlias", "CmdCopy", "CmdUnLink", "CmdSetHome", "CmdListCmdSets", "CmdName", "CmdOpen", "CmdSetAttribute", "CmdTypeclass", "CmdWipe", "CmdLock", "CmdExamine", "CmdFind", "CmdTeleport", - "CmdScript", "CmdTag") + "CmdScript", "CmdTag", "CmdSpawn") try: # used by @set @@ -30,7 +30,7 @@ except ImportError: # used by @find CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS - +_PROTOTYPE_PARENTS = None class ObjManipCommand(MuxCommand): """ @@ -2244,17 +2244,33 @@ class CmdTag(MuxCommand): string = "No tags attached to %s." % obj self.caller.msg(string) +# +# To use the prototypes with the @spawn function, copy +# game/gamesrc/world/examples/prototypes.py up one level +# to game/gamesrc/world. Then add to game/settings.py the +# line +# PROTOTYPE_MODULES = ["game.gamesrc.commands.prototypes"] +# Reload the server and the prototypes should be available. +# + class CmdSpawn(MuxCommand): """ spawn objects from prototype Usage: - @spawn {prototype dictionary} + @spawn[/switches] {prototype dictionary} + + Switches: + noloc - allow location to None. Otherwise, location will default to + caller's current location + parents - show all available prototype parents Example: @spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"} Dictionary keys: + {wprototype {n - name of parent prototype to use. Can be a list for + multiple inheritance (inherits left to right) {wkey {n - string, the main object identifier {wtypeclass {n - string, if not set, will use settings.BASE_OBJECT_TYPECLASS {wlocation {n - this should be a valid object or #dbref @@ -2266,7 +2282,8 @@ class CmdSpawn(MuxCommand): {wndb_{n - value of a nattribute (ndb_ is stripped) any other keywords are interpreted as Attributes and their values. - This command can't access prototype inheritance. + The parent prototypes are taken as dictionaries defined globally in + the settings.PROTOTYPE_MODULES. """ key = "@spawn" @@ -2274,9 +2291,22 @@ class CmdSpawn(MuxCommand): help_category = "Building" def func(self): - "Implements the spawn" + "Implements the spawner" + + global _PROTOTYPE_PARENTS + if _PROTOTYPE_PARENTS is None: + if hasattr(settings, "PROTOTYPE_MODULES"): + # read prototype parents from setting + _PROTOTYPE_PARENTS = {} + for prototype_module in utils.make_iter(settings.PROTOTYPE_MODULES): + _PROTOTYPE_PARENTS.update(dict((key, val) + for key, val in utils.all_from_module(prototype_module).items() if isinstance(val, dict))) + if not self.args: - self.caller.msg("Usage: @spawn {key:value, key, value, ...}") + string = "Usage: @spawn {key:value, key, value, ...}\n" \ + "Available prototypes: %s" + self.caller.msg(string % ", ".join(_PROTOTYPE_PARENTS.keys()) + if _PROTOTYPE_PARENTS else None) return from src.utils.spawner import spawn @@ -2297,7 +2327,10 @@ class CmdSpawn(MuxCommand): self.caller.msg("The prototype must be a Python dictionary.") return - for obj in spawn(prototype): + if not "noloc" in self.switches and not "location" in prototype: + prototype["location"] = self.caller.location + + for obj in spawn(prototype, prototype_parents=_PROTOTYPE_PARENTS): self.caller.msg("Spawned %s." % obj.key) diff --git a/src/utils/spawner.py b/src/utils/spawner.py index ba3005e09a..09a63b645c 100644 --- a/src/utils/spawner.py +++ b/src/utils/spawner.py @@ -18,7 +18,7 @@ GOBLIN = { } Possible keywords are: - prototype - dict, parent prototype of this structure (see below) + prototype - string parent prototype key - string, the main object identifier typeclass - string, if not set, will use settings.BASE_OBJECT_TYPECLASS location - this should be a valid object or #dbref @@ -83,16 +83,16 @@ _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") _handle_dbref = lambda inp: handle_dbref(inp, ObjectDB) -def _get_prototype(dic, prot): +def _get_prototype(dic, prot, protparents): "Recursively traverse a prototype dictionary, including multiple inheritance" if "prototype" in dic: # move backwards through the inheritance prototypes = dic["prototype"] - if isinstance(prototypes, dict): + if not hasattr(prototypes, "__iter__"): prototypes = (prototypes,) for prototype in prototypes: # Build the prot dictionary in reverse order, overloading - new_prot = _get_prototype(prototype, prot) + new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents) prot.update(new_prot) prot.update(dic) prot.pop("prototype", None) # we don't need this anymore @@ -150,16 +150,21 @@ def _batch_create_object(*objparams): return objs -def spawn(*prototypes): +def spawn(*prototypes, **kwargs): """ Spawn a number of prototyped objects. Each argument should be a prototype dictionary. + + The keyword argument "prototype_parents" holds a dictionary of + prototype dictionaries, each with a unique key. The given + prototypes may call these as parents using the "prototype" key. """ objsparams = [] for prototype in prototypes: + protparents = kwargs.get("prototype_parents", None) - prot = _get_prototype(prototype, {}) + prot = _get_prototype(prototype, {}, protparents) if not prot: continue @@ -195,35 +200,32 @@ def spawn(*prototypes): if __name__ == "__main__": # testing - NOBODY = {} - - GOBLIN = { - "key": "goblin grunt", - "health": lambda: randint(20,30), - "resists": ["cold", "poison"], - "attacks": ["fists"], - "weaknesses": ["fire", "light"] - } - - GOBLIN_WIZARD = { - "prototype": GOBLIN, - "key": "goblin wizard", - "spells": ["fire ball", "lighting bolt"] - } - - GOBLIN_ARCHER = { - "prototype": GOBLIN, - "key": "goblin archer", - "attacks": ["short bow"] - } - - ARCHWIZARD = { - "attacks": ["archwizard staff"], - } - - GOBLIN_ARCHWIZARD = { - "key": "goblin archwizard", - "prototype" : (GOBLIN_WIZARD, ARCHWIZARD) - } + protparents = { + "NOBODY": {}, + "GOBLIN" : { + "key": "goblin grunt", + "health": lambda: randint(20,30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + }, + "GOBLIN_WIZARD" : { + "prototype": "GOBLIN", + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + }, + "GOBLIN_ARCHER" : { + "prototype": "GOBLIN", + "key": "goblin archer", + "attacks": ["short bow"] + }, + "ARCHWIZARD" : { + "attacks": ["archwizard staff"], + }, + "GOBLIN_ARCHWIZARD" : { + "key": "goblin archwizard", + "prototype" : ("GOBLIN_WIZARD", "ARCHWIZARD") + } + } # test - print [o.key for o in spawn(GOBLIN, GOBLIN_ARCHWIZARD)] + print [o.key for o in spawn(protparents["GOBLIN"], protparents["GOBLIN_ARCHWIZARD"], prototype_parents=protparents)] diff --git a/src/utils/utils.py b/src/utils/utils.py index 5d640751d7..24474b1676 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -792,6 +792,8 @@ def all_from_module(module): Return all global-level variables from a module as a dict """ mod = mod_import(module) + if not mod: + return {} return dict((key, val) for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))) From b9c8c1695ca737e561d6ca86d917f8bacc5a998a Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 1 Jul 2014 19:39:12 +0200 Subject: [PATCH 11/19] Finalized the spawner API and made the @spawn command also take prototypes directly, e.g. @spawn GOBLIN. --- src/commands/default/building.py | 59 ++++++++++++++++++-------------- src/utils/spawner.py | 27 ++++++++++++--- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/src/commands/default/building.py b/src/commands/default/building.py index cf21b03148..291c076baa 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -6,11 +6,12 @@ Building and world design commands """ from django.conf import settings from src.objects.models import ObjectDB -from src.utils import create, utils, search -from src.utils.ansi import raw from src.locks.lockhandler import LockException from src.commands.default.muxcommand import MuxCommand from src.commands.cmdhandler import get_and_merge_cmdsets +from src.utils import create, utils, search +from src.utils.spawner import spawn +from src.utils.ansi import raw # limit symbol import for API __all__ = ("ObjManipCommand", "CmdSetObjAlias", "CmdCopy", @@ -2258,14 +2259,16 @@ class CmdSpawn(MuxCommand): spawn objects from prototype Usage: - @spawn[/switches] {prototype dictionary} + @spawn + @spawn[/switch] prototype_name + @spawn[/switch] {prototype dictionary} - Switches: - noloc - allow location to None. Otherwise, location will default to - caller's current location - parents - show all available prototype parents + Switch: + noloc - allow location to be None if not specified explicitly. Otherwise, + location will default to caller's current location. Example: + @spawn GOBLIN @spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"} Dictionary keys: @@ -2282,34 +2285,30 @@ class CmdSpawn(MuxCommand): {wndb_{n - value of a nattribute (ndb_ is stripped) any other keywords are interpreted as Attributes and their values. - The parent prototypes are taken as dictionaries defined globally in - the settings.PROTOTYPE_MODULES. + The available prototypes are defined globally in modules set in + settings.PROTOTYPE_MODULES. If @spawn is used without arguments it + displays a list of available prototypes. """ key = "@spawn" + aliases = ["spawn"] locks = "cmd:perm(spawn) or perm(Builders)" help_category = "Building" def func(self): "Implements the spawner" - global _PROTOTYPE_PARENTS - if _PROTOTYPE_PARENTS is None: - if hasattr(settings, "PROTOTYPE_MODULES"): - # read prototype parents from setting - _PROTOTYPE_PARENTS = {} - for prototype_module in utils.make_iter(settings.PROTOTYPE_MODULES): - _PROTOTYPE_PARENTS.update(dict((key, val) - for key, val in utils.all_from_module(prototype_module).items() if isinstance(val, dict))) + def _show_prototypes(prototypes): + "Helper to show a list of available prototypes" + string = "\nAvailable prototypes:\n %s" + string = string % utils.fill(", ".join(sorted(prototypes.keys()))) + return string + prototypes = spawn(return_prototypes=True) if not self.args: - string = "Usage: @spawn {key:value, key, value, ...}\n" \ - "Available prototypes: %s" - self.caller.msg(string % ", ".join(_PROTOTYPE_PARENTS.keys()) - if _PROTOTYPE_PARENTS else None) + string = "Usage: @spawn {key:value, key, value, ... }" + self.caller.msg(string + _show_prototypes(prototypes)) return - from src.utils.spawner import spawn - try: # make use of _convert_from_string from the SetAttribute command prototype = _convert_from_string(self, self.args) @@ -2323,14 +2322,22 @@ class CmdSpawn(MuxCommand): self.caller.msg(string) return - if not isinstance(prototype, dict): - self.caller.msg("The prototype must be a Python dictionary.") + if isinstance(prototype, basestring): + # A prototype key + keystr = prototype + prototype = prototypes.get(prototype, None) + if not prototype: + string = "No prototype named '%s'." % keystr + self.caller.msg(string + _show_prototypes(prototypes)) + return + elif not isinstance(prototype, dict): + self.caller.msg("The prototype must be a prototype key or a Python dictionary.") return if not "noloc" in self.switches and not "location" in prototype: prototype["location"] = self.caller.location - for obj in spawn(prototype, prototype_parents=_PROTOTYPE_PARENTS): + for obj in spawn(prototype): self.caller.msg("Spawned %s." % obj.key) diff --git a/src/utils/spawner.py b/src/utils/spawner.py index 09a63b645c..155aa5e37a 100644 --- a/src/utils/spawner.py +++ b/src/utils/spawner.py @@ -77,6 +77,7 @@ from django.conf import settings from random import randint from src.objects.models import ObjectDB from src.utils.create import handle_dbref +from src.utils.utils import make_iter, all_from_module _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") @@ -155,14 +156,30 @@ def spawn(*prototypes, **kwargs): Spawn a number of prototyped objects. Each argument should be a prototype dictionary. - The keyword argument "prototype_parents" holds a dictionary of - prototype dictionaries, each with a unique key. The given - prototypes may call these as parents using the "prototype" key. + keyword args: + prototype_modules - a python-path to a + prototype module, or a list of such paths. These will be used + to build the global protparents dictionary accessible by the + input prototypes. If not given, it will instead look for modules + defined by settings.PROTOTYPE_MODULES. + return_prototypes - only return a list of the prototype-parents + (no object creation happens) """ - objsparams = [] + protparents = {} + protmodules = make_iter(kwargs.get("prototype_modules", [])) + if not protmodules and hasattr(settings, "PROTOTYPE_MODULES"): + protmodules = make_iter(settings.PROTOTYPE_MODULES) + for prototype_module in protmodules: + protparents.update(dict((key, val) + for key, val in all_from_module(prototype_module).items() if isinstance(val, dict))) + + if "return_prototypes" in kwargs: + # only return the parents + return protparents + + objsparams = [] for prototype in prototypes: - protparents = kwargs.get("prototype_parents", None) prot = _get_prototype(prototype, {}, protparents) if not prot: From fb8cf41b4ec181f0a8389985f887f19414edf5ba Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 1 Jul 2014 20:16:43 +0200 Subject: [PATCH 12/19] Added recursive loop-detection to the spawner. This should avoid accidentally creating infinite self-references in prototype structures. --- src/utils/spawner.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/utils/spawner.py b/src/utils/spawner.py index 155aa5e37a..cccbc030de 100644 --- a/src/utils/spawner.py +++ b/src/utils/spawner.py @@ -84,16 +84,21 @@ _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") _handle_dbref = lambda inp: handle_dbref(inp, ObjectDB) -def _get_prototype(dic, prot, protparents): - "Recursively traverse a prototype dictionary, including multiple inheritance" +def _get_prototype(dic, prot, protparents, visited): + """ + Recursively traverse a prototype dictionary, + including multiple inheritance and self-reference + detection + """ + visited.append(id(dic)) if "prototype" in dic: # move backwards through the inheritance - prototypes = dic["prototype"] - if not hasattr(prototypes, "__iter__"): - prototypes = (prototypes,) - for prototype in prototypes: + for prototype in make_iter(dic["prototype"]): + if id(prototype) in visited: + # a loop was detected. Don't self-reference. + continue # Build the prot dictionary in reverse order, overloading - new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents) + new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents, visited) prot.update(new_prot) prot.update(dic) prot.pop("prototype", None) # we don't need this anymore @@ -181,7 +186,7 @@ def spawn(*prototypes, **kwargs): objsparams = [] for prototype in prototypes: - prot = _get_prototype(prototype, {}, protparents) + prot = _get_prototype(prototype, {}, protparents, []) if not prot: continue From 169d0a78f881f7ebf01ed90c8440fd5ca66f0183 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 1 Jul 2014 20:30:35 +0200 Subject: [PATCH 13/19] Made WHO display Player info, not Character info to un-privileged users, and give more info to privileged users. Resolves #518. --- src/commands/default/player.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/commands/default/player.py b/src/commands/default/player.py index 68716d0126..c0c0980384 100644 --- a/src/commands/default/player.py +++ b/src/commands/default/player.py @@ -378,9 +378,11 @@ class CmdWho(MuxPlayerCommand): nplayers = (SESSIONS.player_count()) if show_session_data: + # privileged info table = prettytable.PrettyTable(["{wPlayer Name", "{wOn for", "{wIdle", + "{wPuppeting", "{wRoom", "{wCmds", "{wProtocol", @@ -389,25 +391,27 @@ class CmdWho(MuxPlayerCommand): if not session.logged_in: continue delta_cmd = time.time() - session.cmd_last_visible delta_conn = time.time() - session.conn_time - plr_pobject = session.get_puppet() - plr_pobject = plr_pobject or session.get_player() - table.add_row([utils.crop(plr_pobject.name, width=25), + player = session.get_player() + puppet = session.get_puppet() + location = puppet.location.key if puppet else "None" + table.add_row([utils.crop(player.name, width=25), utils.time_format(delta_conn, 0), utils.time_format(delta_cmd, 1), - hasattr(plr_pobject, "location") and plr_pobject.location and plr_pobject.location.key or "None", + utils.crop(puppet.key if puppet else "None", width=25), + utils.crop(location, width=25), session.cmd_total, session.protocol_key, isinstance(session.address, tuple) and session.address[0] or session.address]) else: + # unprivileged table = prettytable.PrettyTable(["{wPlayer name", "{wOn for", "{wIdle"]) for session in session_list: if not session.logged_in: continue delta_cmd = time.time() - session.cmd_last_visible delta_conn = time.time() - session.conn_time - plr_pobject = session.get_puppet() - plr_pobject = plr_pobject or session.get_player() - table.add_row([utils.crop(plr_pobject.name, width=25), + player = session.get_player() + table.add_row([utils.crop(player.key, width=25), utils.time_format(delta_conn, 0), utils.time_format(delta_cmd, 1)]) From 680e603c4d802df8ce72f8ec7ea96d2884997753 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 5 Jul 2014 20:32:08 +0200 Subject: [PATCH 14/19] Changed how Tags and Attribues cache and track which objects they use, as suggested in #529. --- contrib/tutorial_world/objects.py | 1 + src/typeclasses/managers.py | 10 +++---- src/typeclasses/models.py | 47 +++++++++++++++---------------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/contrib/tutorial_world/objects.py b/contrib/tutorial_world/objects.py index 1e7561ff99..978de148f3 100644 --- a/contrib/tutorial_world/objects.py +++ b/contrib/tutorial_world/objects.py @@ -141,6 +141,7 @@ class CmdClimb(Command): obj = self.caller.search(self.args.strip()) if not obj: return + print "obj", "self.obj", obj, self if obj != self.obj: self.caller.msg("Try as you might, you cannot climb that.") return diff --git a/src/typeclasses/managers.py b/src/typeclasses/managers.py index 5cfd34d623..a638947a42 100644 --- a/src/typeclasses/managers.py +++ b/src/typeclasses/managers.py @@ -70,24 +70,24 @@ class AttributeManager(models.Manager): @_attr_pickled def get(self, *args, **kwargs): return super(AttributeManager, self).get(*args, **kwargs) - @_attr_pickled + @_attr_pickled def filter(self,*args, **kwargs): return super(AttributeManager, self).filter(*args, **kwargs) - @_attr_pickled + @_attr_pickled def exclude(self,*args, **kwargs): return super(AttributeManager, self).exclude(*args, **kwargs) - @_attr_pickled + @_attr_pickled def values(self,*args, **kwargs): return super(AttributeManager, self).values(*args, **kwargs) - @_attr_pickled + @_attr_pickled def values_list(self,*args, **kwargs): return super(AttributeManager, self).values_list(*args, **kwargs) - @_attr_pickled + @_attr_pickled def exists(self,*args, **kwargs): return super(AttributeManager, self).exists(*args, **kwargs) diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index f31634ab6c..9c0eded5e5 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -38,7 +38,7 @@ from django.db.models import Q from django.utils.encoding import smart_str from django.contrib.contenttypes.models import ContentType -from src.utils.idmapper.models import SharedMemoryModel, WeakSharedMemoryModel +from src.utils.idmapper.models import SharedMemoryModel from src.server.caches import get_prop_cache, set_prop_cache #from src.server.caches import set_attr_cache @@ -237,19 +237,18 @@ class AttributeHandler(object): def __init__(self, obj): "Initialize handler" self.obj = obj - self._model = "%s.%s" % ContentType.objects.get_for_model(obj).natural_key() + self._objid = obj.id + self._model = to_str(ContentType.objects.get_for_model(obj).natural_key()[1]) self._cache = None def _recache(self): - if not self._attrtype: - attrtype = Q(db_attrtype=None) | Q(db_attrtype='') - else: - attrtype = Q(db_attrtype=self._attrtype) + "Cache all attributes of this object" + query = {"%s__id" % self._model : self._objid, + "attribute__db_attrtype" : self._attrtype} + attrs = [conn.attribute for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] self._cache = dict(("%s-%s" % (to_str(attr.db_key).lower(), - attr.db_category.lower() if attr.db_category else None), attr) - for attr in getattr(self.obj, self._m2m_fieldname).filter( - db_model=self._model).filter(attrtype)) - #set_attr_cache(self.obj, self._cache) # currently only for testing + attr.db_category.lower() if conn.attribute.db_category else None), + attr) for attr in attrs) def has(self, key, category=None): """ @@ -412,7 +411,7 @@ class AttributeHandler(object): else: # create a new Attribute (no OOB handlers can be notified) kwargs = {"db_key" : keystr, "db_category" : category, - "db_model" : self._model, "db_attrtype" : self._attrtype, + "db_attrtype" : self._attrtype, "db_value" : None if strattr else to_pickle(new_value), "db_strvalue" : value if strattr else None} new_attr = Attribute(**kwargs) @@ -628,19 +627,18 @@ class TagHandler(object): and with a tagtype given by self.handlertype """ self.obj = obj - self._model = "%s.%s" % ContentType.objects.get_for_model(obj).natural_key() + self._objid = obj.id + self._model = ContentType.objects.get_for_model(obj).natural_key()[1] self._cache = None def _recache(self): - "Update cache from database field" - if not self._tagtype: - tagtype = Q(db_tagtype='') | Q(db_tagtype__isnull=True) - else: - tagtype = Q(db_tagtype=self._tagtype) - self._cache = dict(("%s-%s" % (tag.db_key, tag.db_category), tag) - for tag in getattr( - self.obj, self._m2m_fieldname).filter( - db_model=self._model).filter(tagtype)) + "Cache all tags of this object" + query = {"%s__id" % self._model : self._objid, + "tag__db_tagtype" : self._tagtype} + tagobjs = [conn.tag for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] + self._cache = dict(("%s-%s" % (to_str(tagobj.db_key).lower(), + tagobj.db_category.lower() if tagobj.db_category else None), + tagobj) for tagobj in tagobjs) def add(self, tag=None, category=None, data=None): "Add a new tag to the handler. Tag is a string or a list of strings." @@ -656,7 +654,7 @@ class TagHandler(object): # will overload data on an existing tag since that is not # considered part of making the tag unique) tagobj = Tag.objects.create_tag(key=tagstr, category=category, data=data, - model=self._model, tagtype=self._tagtype) + tagtype=self._tagtype) getattr(self.obj, self._m2m_fieldname).add(tagobj) if self._cache is None: self._recache() @@ -709,11 +707,10 @@ class TagHandler(object): self._recache() if category: category = category.strip().lower() if category is not None else None - matches = getattr(self.obj, self._m2m_fieldname).filter(db_category=category, - db_tagtype=self._tagtype, - db_model=self._model) + matches = [tag for tag in self._cache.values() if tag.db_category == category] else: matches = self._cache.values() + if matches: if return_key_and_category: # return tuple (key, category) From e6950aadf288a2b2adec06277f1210e98271e2dd Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 6 Jul 2014 13:10:03 +0200 Subject: [PATCH 15/19] Changed how lazy-loading of handlers work, using a werkzeug recipe. Much more efficient now. --- contrib/tutorial_world/objects.py | 1 - src/commands/command.py | 7 ++- src/comms/models.py | 14 ++--- src/help/models.py | 16 ++++-- src/objects/models.py | 32 +++++------ src/players/models.py | 27 +++++---- src/scripts/models.py | 11 +--- src/server/serversession.py | 11 ++-- src/typeclasses/models.py | 57 +++++++++++-------- src/utils/utils.py | 93 +++++++++++-------------------- 10 files changed, 125 insertions(+), 144 deletions(-) diff --git a/contrib/tutorial_world/objects.py b/contrib/tutorial_world/objects.py index 978de148f3..1e7561ff99 100644 --- a/contrib/tutorial_world/objects.py +++ b/contrib/tutorial_world/objects.py @@ -141,7 +141,6 @@ class CmdClimb(Command): obj = self.caller.search(self.args.strip()) if not obj: return - print "obj", "self.obj", obj, self if obj != self.obj: self.caller.msg("Try as you might, you cannot climb that.") return diff --git a/src/commands/command.py b/src/commands/command.py index 8cd332e5c1..c37eb0bd5d 100644 --- a/src/commands/command.py +++ b/src/commands/command.py @@ -7,7 +7,7 @@ All commands in Evennia inherit from the 'Command' class in this module. import re from src.locks.lockhandler import LockHandler -from src.utils.utils import is_iter, fill, LazyLoadHandler +from src.utils.utils import is_iter, fill, lazy_property def _init_command(mcs, **kwargs): @@ -155,7 +155,10 @@ class Command(object): overloading evential same-named class properties.""" if kwargs: _init_command(self, **kwargs) - self.lockhandler = LazyLoadHandler(self, "lockhandler", LockHandler) + + @lazy_property + def lockhandler(self): + return LockHandler(self) def __str__(self): "Print the command" diff --git a/src/comms/models.py b/src/comms/models.py index 688898f7b5..f37d7c5057 100644 --- a/src/comms/models.py +++ b/src/comms/models.py @@ -27,7 +27,7 @@ from src.utils.idmapper.models import SharedMemoryModel from src.comms import managers from src.comms.managers import identify_object from src.locks.lockhandler import LockHandler -from src.utils.utils import crop, make_iter, LazyLoadHandler +from src.utils.utils import crop, make_iter, lazy_property __all__ = ("Msg", "TempMsg", "ChannelDB") @@ -103,7 +103,6 @@ class Msg(SharedMemoryModel): def __init__(self, *args, **kwargs): SharedMemoryModel.__init__(self, *args, **kwargs) - #_SA(self, "locks", LazyLoadHandler(self, "locks", LockHandler)) self.extra_senders = [] class Meta: @@ -299,10 +298,13 @@ class TempMsg(object): self.header = header self.message = message self.lock_storage = lockstring - self.locks = LazyLoadHandler(self, "locks", LockHandler) self.hide_from = hide_from and make_iter(hide_from) or [] self.date_sent = datetime.now() + @lazy_property + def locks(self): + return LockHandler(self) + def __str__(self): "This handles what is shown when e.g. printing the message" senders = ",".join(obj.key for obj in self.senders) @@ -359,12 +361,6 @@ class ChannelDB(TypedObject): _typeclass_paths = settings.CHANNEL_TYPECLASS_PATHS _default_typeclass_path = settings.BASE_CHANNEL_TYPECLASS or "src.comms.comms.Channel" - def __init__(self, *args, **kwargs): - TypedObject.__init__(self, *args, **kwargs) - _SA(self, "tags", LazyLoadHandler(self, "tags", TagHandler)) - _SA(self, "attributes", LazyLoadHandler(self, "attributes", AttributeHandler)) - _SA(self, "aliases", LazyLoadHandler(self, "aliases", AliasHandler)) - class Meta: "Define Django meta options" verbose_name = "Channel" diff --git a/src/help/models.py b/src/help/models.py index 8f97fa4069..d81f7a284f 100644 --- a/src/help/models.py +++ b/src/help/models.py @@ -14,7 +14,7 @@ from src.utils.idmapper.models import SharedMemoryModel from src.help.manager import HelpEntryManager from src.typeclasses.models import Tag, TagHandler from src.locks.lockhandler import LockHandler -from src.utils.utils import LazyLoadHandler +from src.utils.utils import lazy_property __all__ = ("HelpEntry",) @@ -66,10 +66,16 @@ class HelpEntry(SharedMemoryModel): objects = HelpEntryManager() _is_deleted = False - def __init__(self, *args, **kwargs): - SharedMemoryModel.__init__(self, *args, **kwargs) - self.locks = LazyLoadHandler(self, "locks", LockHandler) - self.tags = LazyLoadHandler(self, "tags", TagHandler) + # lazy-loaded handlers + + @lazy_property + def locks(self): + return LockHandler(self) + + @lazy_property + def tags(self): + return TagHandler(self) + class Meta: "Define Django meta options" diff --git a/src/objects/models.py b/src/objects/models.py index 7ad13c661b..9aeb8ea658 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -19,16 +19,15 @@ from django.db import models from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from src.typeclasses.models import (TypedObject, TagHandler, NickHandler, - AliasHandler, AttributeHandler) +from src.typeclasses.models import TypedObject, NickHandler from src.objects.manager import ObjectManager from src.players.models import PlayerDB from src.commands.cmdsethandler import CmdSetHandler from src.commands import cmdhandler from src.scripts.scripthandler import ScriptHandler from src.utils import logger -from src.utils.utils import (make_iter, to_str, to_unicode, - variable_from_module, dbref, LazyLoadHandler) +from src.utils.utils import (make_iter, to_str, to_unicode, lazy_property, + variable_from_module, dbref) from django.utils.translation import ugettext as _ @@ -130,19 +129,18 @@ class ObjectDB(TypedObject): _typeclass_paths = settings.OBJECT_TYPECLASS_PATHS _default_typeclass_path = settings.BASE_OBJECT_TYPECLASS or "src.objects.objects.Object" - # Add the object-specific handlers - def __init__(self, *args, **kwargs): - "Parent must be initialized first." - TypedObject.__init__(self, *args, **kwargs) - # handlers - _SA(self, "cmdset", LazyLoadHandler(self, "cmdset", CmdSetHandler, True)) - _SA(self, "scripts", LazyLoadHandler(self, "scripts", ScriptHandler)) - _SA(self, "nicks", LazyLoadHandler(self, "nicks", NickHandler)) - #_SA(self, "attributes", LazyLoadHandler(self, "attributes", AttributeHandler)) - #_SA(self, "tags", LazyLoadHandler(self, "tags", TagHandler)) - #_SA(self, "aliases", LazyLoadHandler(self, "aliases", AliasHandler)) - # make sure to sync the contents cache when initializing - #_GA(self, "contents_update")() + # lazy-load handlers + @lazy_property + def cmdset(self): + return CmdSetHandler(self, True) + + @lazy_property + def scripts(self): + return ScriptHandler(self) + + @lazy_property + def nicks(self): + return NickHandler(self) def _at_db_player_postsave(self): """ diff --git a/src/players/models.py b/src/players/models.py index cd2b633300..23d2b5363e 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -23,13 +23,12 @@ from django.utils.encoding import smart_str from src.players import manager from src.scripts.models import ScriptDB -from src.typeclasses.models import (TypedObject, TagHandler, NickHandler, - AliasHandler, AttributeHandler) +from src.typeclasses.models import (TypedObject, NickHandler) from src.scripts.scripthandler import ScriptHandler from src.commands.cmdsethandler import CmdSetHandler from src.commands import cmdhandler from src.utils import utils, logger -from src.utils.utils import to_str, make_iter, LazyLoadHandler +from src.utils.utils import to_str, make_iter, lazy_property from django.utils.translation import ugettext as _ @@ -111,15 +110,19 @@ class PlayerDB(TypedObject, AbstractUser): app_label = 'players' verbose_name = 'Player' - def __init__(self, *args, **kwargs): - "Parent must be initiated first" - TypedObject.__init__(self, *args, **kwargs) - # handlers - _SA(self, "cmdset", LazyLoadHandler(self, "cmdset", CmdSetHandler, True)) - _SA(self, "scripts", LazyLoadHandler(self, "scripts", ScriptHandler)) - _SA(self, "nicks", LazyLoadHandler(self, "nicks", NickHandler)) - #_SA(self, "tags", LazyLoadHandler(self, "tags", TagHandler)) - #_SA(self, "aliases", LazyLoadHandler(self, "aliases", AliasHandler)) + # lazy-loading of handlers + @lazy_property + def cmdset(self): + return CmdSetHandler(self, True) + + @lazy_property + def scripts(self): + return ScriptHandler(self) + + @lazy_property + def nicks(self): + return NickHandler(self) + # alias to the objs property def __characters_get(self): diff --git a/src/scripts/models.py b/src/scripts/models.py index 2942d2d43c..45347cff9a 100644 --- a/src/scripts/models.py +++ b/src/scripts/models.py @@ -27,9 +27,9 @@ Common examples of uses of Scripts: from django.conf import settings from django.db import models from django.core.exceptions import ObjectDoesNotExist -from src.typeclasses.models import TypedObject, TagHandler, AttributeHandler +from src.typeclasses.models import TypedObject from src.scripts.manager import ScriptManager -from src.utils.utils import dbref, to_str, LazyLoadHandler +from src.utils.utils import dbref, to_str __all__ = ("ScriptDB",) _GA = object.__getattribute__ @@ -108,13 +108,6 @@ class ScriptDB(TypedObject): "Define Django meta options" verbose_name = "Script" - def __init__(self, *args, **kwargs): - super(ScriptDB, self).__init__(*args, **kwargs) - _SA(self, "attributes", LazyLoadHandler(self, "attributes", AttributeHandler)) - _SA(self, "tags", LazyLoadHandler(self, "tags", TagHandler)) - #_SA(self, "aliases", AliasHandler(self)) - - # # # ScriptDB class properties diff --git a/src/server/serversession.py b/src/server/serversession.py index 00551f4b42..4cdd2375a5 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -13,8 +13,9 @@ from django.conf import settings #from src.scripts.models import ScriptDB from src.comms.models import ChannelDB from src.utils import logger, utils -from src.utils.utils import make_iter, to_unicode, LazyLoadHandler -from src.commands import cmdhandler, cmdsethandler +from src.utils.utils import make_iter, to_unicode +from src.commands.cmdhandler import cmdhandler +from src.commands.cmdsethandler import CmdSetHandler from src.server.session import Session IDLE_COMMAND = settings.IDLE_COMMAND @@ -49,7 +50,7 @@ class ServerSession(Session): self.puppet = None self.player = None self.cmdset_storage_string = "" - self.cmdset = LazyLoadHandler(self, "cmdset", cmdsethandler.CmdSetHandler, True) + self.cmdset = CmdSetHandler(self, True) def __cmdset_storage_get(self): return [path.strip() for path in self.cmdset_storage_string.split(',')] @@ -103,7 +104,7 @@ class ServerSession(Session): self.player.save() # add the session-level cmdset - self.cmdset = LazyLoadHandler(self, "cmdset", cmdsethandler.CmdSetHandler, True) + self.cmdset = CmdSetHandler(self, True) def at_disconnect(self): """ @@ -198,7 +199,7 @@ class ServerSession(Session): else: text = self.player.nicks.nickreplace(text, categories=("inputline", "channels"), include_player=False) - cmdhandler.cmdhandler(self, text, callertype="session", sessid=self.sessid) + cmdhandler(self, text, callertype="session", sessid=self.sessid) self.update_session_counters() if "oob" in kwargs: # handle oob instructions diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 9c0eded5e5..3c77fbfe3e 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -34,7 +34,6 @@ import weakref from django.db import models from django.core.exceptions import ObjectDoesNotExist from django.conf import settings -from django.db.models import Q from django.utils.encoding import smart_str from django.contrib.contenttypes.models import ContentType @@ -48,7 +47,7 @@ from src.typeclasses import managers from src.locks.lockhandler import LockHandler from src.utils import logger from src.utils.utils import ( - make_iter, is_iter, to_str, inherits_from, LazyLoadHandler) + make_iter, is_iter, to_str, inherits_from, lazy_property) from src.utils.dbserialize import to_pickle, from_pickle from src.utils.picklefield import PickledObjectField @@ -132,12 +131,9 @@ class Attribute(SharedMemoryModel): # Database manager objects = managers.AttributeManager() - # Lock handler self.locks - def __init__(self, *args, **kwargs): - "Initializes the parent first -important!" - #SharedMemoryModel.__init__(self, *args, **kwargs) - super(Attribute, self).__init__(*args, **kwargs) - self.locks = LazyLoadHandler(self, "locks", LockHandler) + @lazy_property + def locks(self): + return LockHandler(self) class Meta: "Define Django meta options" @@ -801,15 +797,33 @@ class TypedObject(SharedMemoryModel): def __init__(self, *args, **kwargs): "We must initialize the parent first - important!" super(TypedObject, self).__init__(*args, **kwargs) - #SharedMemoryModel.__init__(self, *args, **kwargs) _SA(self, "dbobj", self) # this allows for self-reference - _SA(self, "locks", LazyLoadHandler(self, "locks", LockHandler)) - _SA(self, "tags", LazyLoadHandler(self, "tags", TagHandler)) - _SA(self, "aliases", LazyLoadHandler(self, "aliases", AliasHandler)) - _SA(self, "permissions", LazyLoadHandler(self, "permissions", PermissionHandler)) - _SA(self, "attributes", LazyLoadHandler(self, "attributes", AttributeHandler)) - _SA(self, "nattributes", NAttributeHandler(self)) - #_SA(self, "nattributes", LazyLoadHandler(self, "nattributes", NAttributeHandler)) + + # initialize all handlers in a lazy fashion + @lazy_property + def attributes(self): + return AttributeHandler(self) + + @lazy_property + def locks(self): + return LockHandler(self) + + @lazy_property + def tags(self): + return TagHandler(self) + + @lazy_property + def aliases(self): + return AliasHandler(self) + + @lazy_property + def permissions(self): + return PermissionHandler(self) + + @lazy_property + def nattributes(self): + return NAttributeHandler(self) + class Meta: """ @@ -1276,13 +1290,10 @@ class TypedObject(SharedMemoryModel): if not TICKER_HANDLER: from src.scripts.tickerhandler import TICKER_HANDLER TICKER_HANDLER.remove(self) # removes objects' all ticker subscriptions - if not isinstance(_GA(self, "permissions"), LazyLoadHandler): - _GA(self, "permissions").clear() - if not isinstance(_GA(self, "attributes"), LazyLoadHandler): - _GA(self, "attributes").clear() - if not isinstance(_GA(self, "aliases"), LazyLoadHandler): - _GA(self, "aliases").clear() - if hasattr(self, "nicks") and not isinstance(_GA(self, "nicks"), LazyLoadHandler): + _GA(self, "permissions").clear() + _GA(self, "attributes").clear() + _GA(self, "aliases").clear() + if hasattr(self, "nicks"): _GA(self, "nicks").clear() _SA(self, "_cached_typeclass", None) _GA(self, "flush_from_cache")() diff --git a/src/utils/utils.py b/src/utils/utils.py index 24474b1676..c72274d32e 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -1060,67 +1060,38 @@ def deepsize(obj, max_depth=4): size = getsizeof(obj) + sum([p[1] for p in sizedict.values()]) return size -# lazy load handlers - -import weakref -class LazyLoadHandler(object): +# lazy load handler +_missing = object() +class lazy_property(object): """ - Load handlers only when they are actually accessed + Delays loading of property until first access. Credit goes to + the Implementation in the werkzeug suite: + http://werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property + + This should be used as a decorator in a class and is in Evennia + mainly used to lazy-load handlers: + + @lazy_property + def attributes(self): + return AttributeHandler(self) + + Once initialized, the AttributeHandler will be available + as a property "attributes" on the object. + """ - def __init__(self, obj, name, cls, *args): - """ - Set up a delayed load of a class. The 'name' must be named the - same as the variable to which the LazyLoadHandler is assigned. - """ - _SA(self, "obj", weakref.ref(obj)) - _SA(self, "name", name) - _SA(self, "cls", cls) - _SA(self, "args", args) + def __init__(self, func, name=None, doc=None): + "Store all properties for now" + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func - def _instantiate(self): - """ - Initialize handler as cls(obj, *args) - """ - obj = _GA(self, "obj")() - instance = _GA(self, "cls")(weakref.proxy(obj), *_GA(self, "args")) - _SA(obj, _GA(self, "name"), instance) - return instance - - def __getattribute__(self, name): - """ - Access means loading the handler - """ - return getattr(_GA(self, "_instantiate")(), name) - - def __setattr__(self, name, value): - """ - Setting means loading the handler - """ - setattr(_GA(self, "_instantiate")(), name, value) - - def __delattr__(self, name): - """ - Deleting also triggers loading of handler - """ - delattr(_GA(self, "_instantiate")(), name) - - def __repr__(self): - return repr(_GA(self, "_instantiate")()) - def __str__(self): - return str(_GA(self, "_instantiate")()) - def __unicode__(self): - return str(_GA(self, "_instantiate")()) - -class NonWeakLazyLoadHandler(LazyLoadHandler): - """ - Variation of LazyLoadHandler that does not - create a weak reference when initiating. - """ - def _instantiate(self): - """ - Initialize handler as cls(obj, *args) - """ - obj = _GA(self, "obj")() - instance = _GA(self, "cls")(obj, *_GA(self, "args")) - _SA(obj, _GA(self, "name"), instance) - return instance + def __get__(self, obj, type=None): + "Triggers initialization" + if obj is None: + return self + value = obj.__dict__.get(self.__name__, _missing) + if value is _missing: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value From 9dfe620b2b84bf1b4a38a3bb165a5a7aa3ae1eae Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 6 Jul 2014 16:03:27 +0200 Subject: [PATCH 16/19] Started moving tag/attribute handlers to TypeObjectManager as per #529 --- src/typeclasses/managers.py | 50 +++++++++++++++++++++++++++++++++++++ src/typeclasses/models.py | 6 ++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/typeclasses/managers.py b/src/typeclasses/managers.py index a638947a42..55429b6be8 100644 --- a/src/typeclasses/managers.py +++ b/src/typeclasses/managers.py @@ -217,6 +217,56 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): Common ObjectManager for all dbobjects. """ + # Attribute manager methods + + # Tag manager methods + + def get_tag(self, key=None, category=None, obj=None, tagtype=None): + """ + Return Tag objects by key, by category, by object or + with a combination of those criteria. + + tagtype - one of None (normal tags), "alias" or "permission" + """ + query = [("tag__db_tagtype", tagtype)] + if obj: + query.append(("%s__id" % self.model.__name__.lower(), obj.id)) + if key: + query.append(("tag__db_key", key)) + if category: + query.append(("tag__db_category", category)) + return self.model.db_tags.through.objects.filter(**dict(query)) + + def get_permission(self, key=None, category=None, obj=None): + return self.get_tag(key=key, category=category, obj=obj, tagtype="permission") + + def get_alias(self, key=None, category=None, obj=None): + return self.get_tag(key=key, category=category, obj=obj, tagtype="alias") + + @returns_typeclass + def get_by_tag(self, key=None, category=None, tagtype=None): + """ + Return objects having tags with a given key or category or + combination of the two. + + tagtype = None, alias or permission + """ + query = [("db_tags__db_tagtype", tagtype)] + if key: + query.append(("db_tags__db_key", key)) + if category: + query.append(("db_tags__db_category", category)) + return self.filter(**dict(query)) + + def get_by_permission(self, key=None, category=None): + return self.get_by_tag(key=key, category=category, tagtype="permission") + + def get_by_alias(self, key=None, category=None): + return self.get_by_tag(key=key, category=category, tagtype="alias") + + + # object-manager methods + def dbref(self, dbref, reqhash=True): """ Valid forms of dbref (database reference number) diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 3c77fbfe3e..9cf09b4388 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -35,7 +35,6 @@ from django.db import models from django.core.exceptions import ObjectDoesNotExist from django.conf import settings from django.utils.encoding import smart_str -from django.contrib.contenttypes.models import ContentType from src.utils.idmapper.models import SharedMemoryModel from src.server.caches import get_prop_cache, set_prop_cache @@ -58,7 +57,6 @@ TICKER_HANDLER = None _PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] _TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE -_CTYPEGET = ContentType.objects.get _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ @@ -234,7 +232,7 @@ class AttributeHandler(object): "Initialize handler" self.obj = obj self._objid = obj.id - self._model = to_str(ContentType.objects.get_for_model(obj).natural_key()[1]) + self._model = to_str(obj.__class__.__name__.lower()) self._cache = None def _recache(self): @@ -624,7 +622,7 @@ class TagHandler(object): """ self.obj = obj self._objid = obj.id - self._model = ContentType.objects.get_for_model(obj).natural_key()[1] + self._model = obj.__class__.__name__.lower() self._cache = None def _recache(self): From 88efc50054bd5fe8c313a0ff601423dcb83a7da7 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 6 Jul 2014 16:08:29 +0200 Subject: [PATCH 17/19] Fixed outbut listing bug in @set. --- src/commands/default/building.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/default/building.py b/src/commands/default/building.py index 291c076baa..4b176c3b73 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -1297,7 +1297,7 @@ class CmdSetAttribute(ObjManipCommand): if self.rhs is None: # no = means we inspect the attribute(s) if not attrs: - attrs = [attr.key for attr in obj.get_all_attributes()] + attrs = [attr.key for attr in obj.attributes.all()] for attr in attrs: if obj.attributes.has(attr): string += "\nAttribute %s/%s = %s" % (obj.name, attr, @@ -1305,7 +1305,7 @@ class CmdSetAttribute(ObjManipCommand): else: string += "\n%s has no attribute '%s'." % (obj.name, attr) # we view it without parsing markup. - self.caller.msg(string.strip(), data={"raw": True}) + self.caller.msg(string.strip(), raw=True) return else: # deleting the attribute(s) From 1874300ad1fa9c8f02d13e9e8fbef3a0be7487d1 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 6 Jul 2014 16:20:50 +0200 Subject: [PATCH 18/19] Added at_post_disconnect() hook to Player, to allow for clean deletion of players at disconnect (for example for Guest-account implementation) --- src/players/player.py | 10 ++++++++++ src/server/serversession.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/players/player.py b/src/players/player.py index 8c15b74486..9935a6c6fe 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -383,6 +383,16 @@ class Player(TypeClass): reason = reason and "(%s)" % reason or "" self._send_to_connect_channel("{R%s disconnected %s{n" % (self.key, reason)) + def at_post_disconnect(self): + """ + This is called after disconnection is complete. No messages + can be relayed to the player from here. After this call, the + player should not be accessed any more, making this a good + spot for deleting it (in the case of a guest player account, + for example). + """ + pass + def at_message_receive(self, message, from_obj=None): """ Called when any text is emitted to this diff --git a/src/server/serversession.py b/src/server/serversession.py index 4cdd2375a5..531add1fe9 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -123,6 +123,8 @@ class ServerSession(Session): if not self.sessionhandler.sessions_from_player(player): # no more sessions connected to this player player.is_connected = False + # this may be used to e.g. delete player after disconnection etc + _GA(player.typeclass, "at_post_disconnect")() def get_player(self): """ From ef8e52302a5e3c94a25c75c1b6a24443774c73ad Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 6 Jul 2014 17:01:09 +0200 Subject: [PATCH 19/19] Added spawner.spawn to ev.py --- ev.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ev.py b/ev.py index 0a3555c223..3d996b0231 100644 --- a/ev.py +++ b/ev.py @@ -132,6 +132,7 @@ from src.utils import logger from src.utils import utils from src.utils import gametime from src.utils import ansi +from src.utils.spawner import spawn ######################################################################