diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index edef289962..0178910755 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -59,7 +59,7 @@ def _get_flat_menu_prototype(caller, refresh=False, validate=False): def _get_unchanged_inherited(caller, protname): """Return prototype values inherited from parent(s), which are not replaced in child""" - protototype = _get_menu_prototype(caller) + prototype = _get_menu_prototype(caller) if protname in prototype: return protname[protname], False else: @@ -1968,6 +1968,7 @@ def node_apply_diff(caller, **kwargs): obj_prototype = kwargs.get("obj_prototype", None) base_obj = kwargs.get("base_obj", None) diff = kwargs.get("diff", None) + custom_location = kwargs.get("custom_location", None) if not update_objects: text = "There are no existing objects to update." @@ -1978,24 +1979,36 @@ def node_apply_diff(caller, **kwargs): if not diff: # use one random object as a reference to calculate a diff base_obj = choice(update_objects) - diff, obj_prototype = spawner.prototype_diff_from_object(prototype, base_obj) + + # from evennia import set_trace + diff, obj_prototype = spawner.prototype_diff_from_object( + prototype, base_obj, exceptions={"location": "KEEP"}) text = ["Suggested changes to {} objects. ".format(len(update_objects)), "Showing random example obj to change: {name} ({dbref}))\n".format( name=base_obj.key, dbref=base_obj.dbref)] helptext = """ + This will go through all existing objects and apply the changes you accept. + Be careful with this operation! The upgrade mechanism will try to automatically estimate what changes need to be applied. But the estimate is |wonly based on the analysis of one randomly selected object|n among all objects spawned by this prototype. If that object happens to be unusual in some way the estimate will be off and may lead to unexpected - results for other objects. Always test your objects carefully after an upgrade and - consider being conservative (switch to KEEP) or even do the update manually if you are - unsure that the results will be acceptable. """ + results for other objects. Always test your objects carefully after an upgrade and consider + being conservative (switch to KEEP) for things you are unsure of. For complex upgrades it + may be better to get help from an administrator with access to the `@py` command for doing + this manually. + + Note that the `location` will never be auto-adjusted because it's so rare to want to + homogenize the location of all object instances.""" options = [] ichanges = 0 + + # convert diff to a menu text + options to edit + for (key, inst) in sorted(((key, val) for key, val in diff.items()), key=lambda tup: tup[0]): if key in protlib._PROTOTYPE_META_NAMES: diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index 6dff62ef96..a13aa7e532 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -36,7 +36,7 @@ prototype key (this value must be possible to serialize in an Attribute). """ from ast import literal_eval -from random import randint as base_randint, random as base_random +from random import randint as base_randint, random as base_random, choice as base_choice from evennia.utils import search from evennia.utils.utils import justify as base_justify, is_iter, to_str @@ -101,6 +101,16 @@ def center_justify(*args, **kwargs): return "" +def choice(*args, **kwargs): + """ + Usage: $choice(val, val, val, ...) + Returns one of the values randomly + """ + if args: + return base_choice(args) + return "" + + def full_justify(*args, **kwargs): """ diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 5ead6239e7..08803797b3 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -128,6 +128,7 @@ import hashlib import time from django.conf import settings + import evennia from evennia.objects.models import ObjectDB from evennia.utils.utils import make_iter, is_iter @@ -138,6 +139,8 @@ from evennia.prototypes.prototypes import ( _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") _PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") +_PROTOTYPE_ROOT_NAMES = ('typeclass', 'key', 'aliases', 'attrs', 'tags', 'locks', 'permissions', + 'location', 'home', 'destination') _NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES @@ -240,10 +243,10 @@ def prototype_from_object(obj): locks = obj.locks.all() if locks: prot['locks'] = ";".join(locks) - perms = obj.permissions.get() + perms = obj.permissions.get(return_list=True) if perms: prot['permissions'] = make_iter(perms) - aliases = obj.aliases.get() + aliases = obj.aliases.get(return_list=True) if aliases: prot['aliases'] = aliases tags = [(tag.db_key, tag.db_category, tag.db_data) @@ -258,7 +261,160 @@ def prototype_from_object(obj): return prot -def prototype_diff_from_object(prototype, obj): +def get_detailed_prototype_diff(prototype1, prototype2): + """ + A 'detailed' diff specifies differences down to individual sub-sectiions + of the prototype, like individual attributes, permissions etc. It is used + by the menu to allow a user to customize what should be kept. + + Args: + prototype1 (dict): Original prototype. + prototype2 (dict): Comparison prototype. + + Returns: + diff (dict): A structure detailing how to convert prototype1 to prototype2. + + Notes: + A detailed diff has instructions REMOVE, ADD, UPDATE and KEEP. + + """ + def _recursive_diff(old, new): + + old_type = type(old) + new_type = type(new) + + if old_type != new_type: + if old and not new: + return (old, new, "REMOVE") + elif not old and new: + return (old, new, "ADD") + else: + return (old, new, "UPDATE") + elif new_type == dict: + all_keys = set(old.keys() + new.keys()) + return {key: _recursive_diff(old.get(key), new.get(key)) for key in all_keys} + elif is_iter(new): + old_map = {part[0] if is_iter(part) else part: part for part in old} + new_map = {part[0] if is_iter(part) else part: part for part in new} + all_keys = set(old_map.keys() + new_map.keys()) + return new_type(_recursive_diff(old_map.get(key), new_map.get(key)) + for key in all_keys) + elif old != new: + return (old, new, "UPDATE") + else: + return (old, new, "KEEP") + + diff = _recursive_diff(prototype1, prototype2) + + return diff + + +def flatten_diff(detailed_diff): + """ + For spawning, a 'detailed' diff is not necessary, rather we just + want instructions on how to handle each root key. + + Args: + detailed_diff (dict): Diff produced by `get_detailed_prototype_diff` and + possibly modified by the user. + + Returns: + flattened_diff (dict): A flat structure detailing how to operate on each + root component of the prototype. + + Notes: + The flattened diff has the following possible instructions: + UPDATE, REPLACE, REMOVE + Many of the detailed diff's values can hold nested structures with their own + individual instructions. A detailed diff can have the following instructions: + REMOVE, ADD, UPDATE, KEEP + Here's how they are translated: + - All REMOVE -> REMOVE + - All ADD|UPDATE -> UPDATE + - All KEEP -> (remove from flattened diff) + - Mix KEEP, UPDATE, ADD -> UPDATE + - Mix REMOVE, KEEP, UPDATE, ADD -> REPLACE + """ + + typ = type(diffpart) + if typ == tuple and _is_diff_instruction(diffpart): + key = args[0] + _, val, inst = diffpart + elif typ == dict: + for key, subdiffpart in diffpart: + _apply_diff(subdiffpart, obj, *(args + (key, ))) + else: + # all other types in the diff are iterables (tups or lists) and + # are identified by their first element. + for tup in diffpart: + _apply_diff(tup, obj, *(args + (tup[0], ))) + + + + +def _is_diff_instruction(obj): + return (isinstance(obj, tuple) and + len(obj) == 3 and + obj[2] in ('KEEP', 'REMOVE', 'ADD', 'UPDATE')) + + +def apply_diff_to_prototype(prototype, diff): + """ + When spawning we don't need the full details of the diff; we have (in the menu) had our + chance to customize we just want to know if the + current root key should be + + """ + + +def menu_format_diff(diff): + """ + Reformat the diff in a way suitable for the olc menu. + + Args: + diff (dict): A diff as produced by `prototype_diff`. The root level of this diff + (which is always a dict) is used to group sub-changes. + + Returns: + + + """ + + def _apply_diff(diffpart, obj, *args): + """ + Recursively apply the diff for a given rootname. + + Args: + diffpart (tuple or dict): Part of diff to apply. + obj (Object): Object to apply diff to. + args (str): Listing of identifiers for the part to apply, + starting from the root. + + """ + typ = type(diffpart) + if typ == tuple and _is_diff_instruction(diffpart): + key = args[0] + _, val, inst = diffpart + elif typ == dict: + for key, subdiffpart in diffpart: + _apply_diff(subdiffpart, obj, *(args + (key, ))) + else: + # all other types in the diff are iterables (tups or lists) and + # are identified by their first element. + for tup in diffpart: + _apply_diff(tup, obj, *(args + (tup[0], ))) + + + def _iter_diff(obj): + if _is_diff_instruction(obj): + old, new, inst = obj + + out_dict = {} + for root_key, root_val in diff.items(): + pass + + +def prototype_diff_from_object(prototype, obj, exceptions=None): """ Get a simple diff for a prototype compared to an object which may or may not already have a prototype (or has one but changed locally). For more complex migratations a manual diff may be @@ -266,32 +422,25 @@ def prototype_diff_from_object(prototype, obj): Args: prototype (dict): Prototype. - obj (Object): Object to + obj (Object): Object to compare prototype against. + exceptions (dict, optional): A mapping {"key": "KEEP|REPLACE|UPDATE|REMOVE" for + enforcing a specific outcome for that key regardless of the diff. Returns: diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} other_prototype (dict): The prototype for the given object. The diff is a how to convert this prototype into the new prototype. + diff = {"key": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), + "attrs": {"attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), + "attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), ...}, + "aliases": {"aliasname": (old, new, "KEEP...", ...}, + ... } + + """ - prot1 = prototype prot2 = prototype_from_object(obj) - - diff = {} - for key, value in prot1.items(): - diff[key] = "KEEP" - if key in prot2: - if callable(prot2[key]) or value != prot2[key]: - 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" - + diff = prototype_diff(prototype, prot2) return diff, prot2 @@ -589,17 +738,12 @@ def spawn(*prototypes, **kwargs): simple_attributes = [] for key, value in ((key, value) for key, value in prot.items() if not (key.startswith("ndb_"))): + # we don't support categories, nor locks for simple attributes if key in _PROTOTYPE_META_NAMES: continue - - if is_iter(value) and len(value) > 1: - # (value, category) - simple_attributes.append((key, - init_spawn_value(value[0], value_to_obj_or_any), - init_spawn_value(value[1], str))) else: - simple_attributes.append((key, - init_spawn_value(value, value_to_obj_or_any))) + simple_attributes.append( + (key, init_spawn_value(value, value_to_obj_or_any), None, None)) attributes = attributes + simple_attributes attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS]