diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 5a1196513a..f34fe8c854 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -177,27 +177,45 @@ def prototype_from_object(obj): # first, check if this object already has a prototype prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) - prot = protlib.search_prototype(prot) + if prot: + prot = protlib.search_prototype(prot[0]) if not prot or len(prot) > 1: # no unambiguous prototype found - build new prototype prot = {} prot['prototype_key'] = "From-Object-{}-{}".format( - obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) + obj.key, hashlib.md5(str(time.time())).hexdigest()[:7]) prot['prototype_desc'] = "Built from {}".format(str(obj)) prot['prototype_locks'] = "spawn:all();edit:all()" prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] - prot['location'] = obj.db_location - prot['home'] = obj.db_home - prot['destination'] = obj.db_destination prot['typeclass'] = obj.db_typeclass_path - prot['locks'] = obj.locks.all() - prot['permissions'] = obj.permissions.get() - prot['aliases'] = obj.aliases.get() - prot['tags'] = [(tag.key, tag.category, tag.data) - for tag in obj.tags.get(return_tagobj=True, return_list=True)] - prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) - for attr in obj.attributes.get(return_obj=True, return_list=True)] + + location = obj.db_location + if location: + prot['location'] = location + home = obj.db_home + if home: + prot['home'] = home + destination = obj.db_destination + if destination: + prot['destination'] = destination + locks = obj.locks.all() + if locks: + prot['locks'] = locks + perms = obj.permissions.get() + if perms: + prot['permissions'] = perms + aliases = obj.aliases.get() + if aliases: + prot['aliases'] = aliases + tags = [(tag.db_key, tag.db_category, tag.db_data) + for tag in obj.tags.get(return_tagobj=True, return_list=True) if tag] + if tags: + prot['tags'] = tags + attrs = [(attr.key, attr.value, attr.category, attr.locks.all()) + for attr in obj.attributes.get(return_obj=True, return_list=True) if attr] + if attrs: + prot['attrs'] = attrs return prot @@ -224,8 +242,14 @@ def prototype_diff_from_object(prototype, obj): diff[key] = "KEEP" if key in prot2: if callable(prot2[key]) or value != prot2[key]: - diff[key] = "UPDATE" + if key in ('attrs', 'tags', 'permissions', 'locks', 'aliases'): + diff[key] = 'REPLACE' + else: + diff[key] = "UPDATE" elif key not in prot2: + diff[key] = "UPDATE" + for key in prot2: + if key not in diff and key not in prot1: diff[key] = "REMOVE" return diff @@ -246,25 +270,42 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): changed (int): The number of objects that had changes applied to them. """ - prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] - prototype_obj = protlib.DbPrototype.objects.filter(db_key=prototype_key) - prototype_obj = prototype_obj[0] if prototype_obj else None - new_prototype = prototype_obj.db.prototype - objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + if isinstance(prototype, basestring): + new_prototype = protlib.search_prototype(prototype) + else: + new_prototype = prototype - if not objs: + prototype_key = new_prototype['prototype_key'] + + if not objects: + objects = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + + if not objects: return 0 if not diff: - diff = prototype_diff_from_object(new_prototype, objs[0]) + diff = prototype_diff_from_object(new_prototype, objects[0]) changed = 0 - for obj in objs: + for obj in objects: do_save = False + + old_prot_key = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) + old_prot_key = old_prot_key[0] if old_prot_key else None + if prototype_key != old_prot_key: + obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) + obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + for key, directive in diff.items(): - val = new_prototype[key] if directive in ('UPDATE', 'REPLACE'): + + if key in _PROTOTYPE_META_NAMES: + # prototype meta keys are not stored on-object + continue + + val = new_prototype[key] do_save = True + if key == 'key': obj.db_key = init_spawn_value(val, str) elif key == 'typeclass': @@ -282,19 +323,19 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): elif key == 'permissions': if directive == 'REPLACE': obj.permissions.clear() - obj.permissions.batch_add(init_spawn_value(val, make_iter)) + obj.permissions.batch_add(*init_spawn_value(val, make_iter)) elif key == 'aliases': if directive == 'REPLACE': obj.aliases.clear() - obj.aliases.batch_add(init_spawn_value(val, make_iter)) + obj.aliases.batch_add(*init_spawn_value(val, make_iter)) elif key == 'tags': if directive == 'REPLACE': obj.tags.clear() - obj.tags.batch_add(init_spawn_value(val, make_iter)) + obj.tags.batch_add(*init_spawn_value(val, make_iter)) elif key == 'attrs': if directive == 'REPLACE': obj.attributes.clear() - obj.attributes.batch_add(init_spawn_value(val, make_iter)) + obj.attributes.batch_add(*init_spawn_value(val, make_iter)) elif key == 'exec': # we don't auto-rerun exec statements, it would be huge security risk! pass @@ -328,9 +369,9 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): pass else: obj.attributes.remove(key) - if do_save: - changed += 1 - obj.save() + if do_save: + changed += 1 + obj.save() return changed diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index e9ef4bce9f..b358043e9d 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -4,6 +4,8 @@ Unit tests for the prototypes and spawner """ from random import randint +import mock +from anything import Anything, Something from evennia.utils.test_resources import EvenniaTest from evennia.prototypes import spawner, prototypes as protlib @@ -56,6 +58,99 @@ class TestSpawner(EvenniaTest): prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard']) +class TestUtils(EvenniaTest): + + def test_prototype_from_object(self): + self.maxDiff = None + self.obj1.attributes.add("test", "testval") + self.obj1.tags.add('foo') + new_prot = spawner.prototype_from_object(self.obj1) + self.assertEqual( + {'attrs': [('test', 'testval', None, [''])], + 'home': Something, + 'key': 'Obj', + 'location': Something, + 'locks': ['call:true()', + 'control:perm(Developer)', + 'delete:perm(Admin)', + 'edit:perm(Admin)', + 'examine:perm(Builder)', + 'get:all()', + 'puppet:pperm(Developer)', + 'tell:perm(Admin)', + 'view:all()'], + 'prototype_desc': 'Built from Obj', + 'prototype_key': Something, + 'prototype_locks': 'spawn:all();edit:all()', + 'tags': [(u'foo', None, None)], + 'typeclass': 'evennia.objects.objects.DefaultObject'}, new_prot) + + def test_update_objects_from_prototypes(self): + + self.maxDiff = None + self.obj1.attributes.add('oldtest', 'to_remove') + + old_prot = spawner.prototype_from_object(self.obj1) + + # modify object away from prototype + self.obj1.attributes.add('test', 'testval') + self.obj1.aliases.add('foo') + self.obj1.key = 'NewObj' + + # modify prototype + old_prot['new'] = 'new_val' + old_prot['test'] = 'testval_changed' + old_prot['permissions'] = 'Builder' + # this will not update, since we don't update the prototype on-disk + old_prot['prototype_desc'] = 'New version of prototype' + + # diff obj/prototype + pdiff = spawner.prototype_diff_from_object(old_prot, self.obj1) + + self.assertEqual( + pdiff, + {'aliases': 'REMOVE', + 'attrs': 'REPLACE', + 'home': 'KEEP', + 'key': 'UPDATE', + 'location': 'KEEP', + 'locks': 'KEEP', + 'new': 'UPDATE', + 'permissions': 'UPDATE', + 'prototype_desc': 'UPDATE', + 'prototype_key': 'UPDATE', + 'prototype_locks': 'KEEP', + 'test': 'UPDATE', + 'typeclass': 'KEEP'}) + + # apply diff + count = spawner.batch_update_objects_with_prototype( + old_prot, diff=pdiff, objects=[self.obj1]) + self.assertEqual(count, 1) + + new_prot = spawner.prototype_from_object(self.obj1) + self.assertEqual({'attrs': [('test', 'testval_changed', None, ['']), + ('new', 'new_val', None, [''])], + 'home': Something, + 'key': 'Obj', + 'location': Something, + 'locks': ['call:true()', + 'control:perm(Developer)', + 'delete:perm(Admin)', + 'edit:perm(Admin)', + 'examine:perm(Builder)', + 'get:all()', + 'puppet:pperm(Developer)', + 'tell:perm(Admin)', + 'view:all()'], + 'permissions': 'builder', + 'prototype_desc': 'Built from Obj', + 'prototype_key': Something, + 'prototype_locks': 'spawn:all();edit:all()', + 'typeclass': 'evennia.objects.objects.DefaultObject'}, + new_prot) + + class TestPrototypeStorage(EvenniaTest): def setUp(self): diff --git a/requirements.txt b/requirements.txt index 7f4b94726f..72df29b9d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,11 @@ django > 1.10, < 2.0 twisted == 16.0.0 -mock >= 1.0.1 pillow == 2.9.0 pytz future >= 0.15.2 django-sekizai inflect + +mock >= 1.0.1 +anything