From cb989934baae94c5ce00a08ead3019098ae2fa50 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 23 Mar 2020 23:19:46 +0100 Subject: [PATCH] Fixing protoype spawning --- evennia/commands/default/building.py | 478 ++++++++++++++++----------- evennia/prototypes/prototypes.py | 27 +- evennia/prototypes/spawner.py | 101 +++++- evennia/server/webserver.py | 17 +- 4 files changed, 411 insertions(+), 212 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 1ab8e8714a..0f8756440b 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -13,13 +13,12 @@ from evennia.utils.utils import ( class_from_module, get_all_typeclasses, variable_from_module, - dbref, + dbref, interactive ) from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus from evennia.utils.ansi import raw -from evennia.prototypes.menus import _format_diff_text_and_options COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2099,10 +2098,10 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): # to confirm changes. if "prototype" in self.switches: diff, _ = spawner.prototype_diff_from_object(prototype, obj) - txt, options = _format_diff_text_and_options(diff, objects=[obj]) + txt = spawner.format_diff(diff) prompt = ( "Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n" - % (prototype["key"], obj.name, "\n".join(txt)) + % (prototype["key"], obj.name, txt) ) if not reset: prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state." @@ -3227,6 +3226,10 @@ class CmdTag(COMMAND_DEFAULT_CLASS): self.caller.msg(string) +# helper functions for spawn + + + class CmdSpawn(COMMAND_DEFAULT_CLASS): """ spawn objects from prototype @@ -3256,7 +3259,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): them with latest version of given prototype. If given with /save, will auto-update all objects with the old version of the prototype without asking first. - edit, olc - create/manipulate prototype in a menu interface. + edit, menu, olc - create/manipulate prototype in a menu interface. Example: spawn GOBLIN @@ -3309,56 +3312,203 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" + def _search_prototype(self, prototype_key, quiet=False): + """ + Search for prototype and handle no/multi-match and access. + + Returns a single found prototype or None - in the + case, the caller has already been informed of the + search error we need not do any further action. + + """ + prototypes = protlib.search_prototype(prototype_key) + nprots = len(prototypes) + + # handle the search result + err = None + if not prototypes: + err = f"No prototype named '{prototype_key}'." + elif nprots > 1: + err = "Found {} prototypes matching '{}':\n {}".format( + nprots, + prototype_key, + ", ".join(proto.get("prototype_key", "") for proto in prototypes), + ) + else: + # we have a single prototype, check access + prototype = prototypes[0] + if not self.caller.locks.check_lockstring( + self.caller, prototype.get("prototype_locks", ""), + access_type="spawn", default=True): + err = "You don't have access to use this prototype." + + if err: + # return None on any error + if not quiet: + self.caller.msg(err) + return + return prototype + + def _parse_prototype(self, inp, expect=dict): + """ + Parse a prototype dict or key from the input and convert it safely + into a dict if appropriate. + + Args: + inp (str): The input from user. + expect (type, optional): + Returns: + prototype (dict, str or None): The parsed prototype. If None, the error + was already reported. + + """ + eval_err = None + try: + prototype = _LITERAL_EVAL(inp) + except (SyntaxError, ValueError) as err: + # treat as string + eval_err = err + prototype = utils.to_str(inp) + finally: + # it's possible that the input was a prototype-key, in which case + # it's okay for the LITERAL_EVAL to fail. Only if the result does not + # match the expected type do we have a problem. + if not isinstance(prototype, expect): + if eval_err: + string = ( + f"{inp}\n{eval_err}\n|RCritical Python syntax error in argument. Only primitive " + "Python structures are allowed. \nMake sure to use correct " + "Python syntax. Remember especially to put quotes around all " + "strings inside lists and dicts.|n For more advanced uses, embed " + "inlinefuncs in the strings." + ) + else: + string = "Expected {}, got {}.".format(expect, type(prototype)) + self.caller.msg(string) + return + + if expect == dict: + # an actual prototype. We need to make sure it's safe, + # so don't allow exec. + # TODO: Exec support is deprecated. Remove completely for 1.0. + if "exec" in prototype and not self.caller.check_permstring("Developer"): + self.caller.msg( + "Spawn aborted: You are not allowed to " "use the 'exec' prototype key." + ) + return + try: + # we homogenize the protoype first, to be more lenient with free-form + protlib.validate_prototype(protlib.homogenize_prototype(prototype)) + except RuntimeError as err: + self.caller.msg(str(err)) + return + return prototype + + def _get_prototype_detail(self, query=None, prototypes=None): + """ + Display the detailed specs of one or more prototypes. + + Args: + query (str, optional): If this is given and `prototypes` is not, search for + the prototype(s) by this query. This may be a partial query which + may lead to multiple matches, all being displayed. + prototypes (list, optional): If given, ignore `query` and only show these + prototype-details. + Returns: + display (str, None): A formatted string of one or more prototype details. + If None, the caller was already informed of the error. + + + """ + if not prototypes: + # we need to query. Note that if query is None, all prototypes will + # be returned. + prototypes = protlib.search_prototype(key=query) + if prototypes: + return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes) + elif query: + self.caller.msg(f"No prototype found to match the query '{query}'.") + else: + self.caller.msg(f"No prototypes found.") + + def _list_prototypes(self, key=None, tags=None): + """Display prototypes as a list, optionally limited by key/tags. """ + EvMore( + self.caller, + str(protlib.list_prototypes(self.caller, key=key, tags=tags)), + exit_on_lastpage=True, + justify_kwargs=False, + ) + + @interactive + def _update_existing_objects(self, caller, prototype_key, quiet=False): + """ + Update existing objects (if any) with this prototype-key to the latest + prototype version. + + Args: + caller (Object): This is necessary for @interactive to work. + prototype_key (str): The prototype to update. + quiet (bool, optional): If set, don't report to user if no + old objects were found to update. + Returns: + n_updated (int): Number of updated objects. + + """ + prototype = self._search_prototype(prototype_key) + if not prototype: + return + + existing_objects = protlib.search_objects_with_prototype(prototype_key) + if not existing_objects: + if not quiet: + caller.msg("No existing objects found with an older version of this prototype.") + return + + if existing_objects: + n_existing = len(existing_objects) + slow = " (note that this may be slow)" if n_existing > 10 else "" + string = ( + f"There are {n_existing} existing object(s) with an older version " + f"of prototype '{prototype_key}'. Should it be re-applied to them{slow}? [Y]/N" + ) + answer = yield (string) + if answer.lower() in ["n", "no"]: + caller.msg( + "|rNo update was done of existing objects. " + "Use spawn/update to apply later as needed.|n" + ) + return + n_updated = spawner.batch_update_objects_with_prototype( + prototype, objects=existing_objects) + caller.msg(f"{n_updated} objects were updated.") + return + + def _parse_key_desc_tags(self, argstring, desc=True): + """ + Parse ;-separated input list. + """ + key, desc, tags = "", "", [] + if ";" in self.args: + parts = (part.strip().lower() for part in self.args.split(";")) + if len(parts) > 1 and desc: + key = parts[0] + desc = parts[1] + tags = parts[2:] + else: + key = parts[0] + tags = parts[1:] + else: + key = argstring.strip().lower() + return key, desc, tags + def func(self): """Implements the spawner""" - def _parse_prototype(inp, expect=dict): - err = None - try: - prototype = _LITERAL_EVAL(inp) - except (SyntaxError, ValueError) as err: - # treat as string - prototype = utils.to_str(inp) - finally: - if not isinstance(prototype, expect): - if err: - string = ( - "{}\n|RCritical Python syntax error in argument. Only primitive " - "Python structures are allowed. \nYou also need to use correct " - "Python syntax. Remember especially to put quotes around all " - "strings inside lists and dicts.|n For more advanced uses, embed " - "inline functions in the strings.".format(err) - ) - else: - string = "Expected {}, got {}.".format(expect, type(prototype)) - self.caller.msg(string) - return None - if expect == dict: - # an actual prototype. We need to make sure it's safe. Don't allow exec - if "exec" in prototype and not self.caller.check_permstring("Developer"): - self.caller.msg( - "Spawn aborted: You are not allowed to " "use the 'exec' prototype key." - ) - return None - try: - # we homogenize first, to be more lenient - protlib.validate_prototype(protlib.homogenize_prototype(prototype)) - except RuntimeError as err: - self.caller.msg(str(err)) - return - return prototype - - def _search_show_prototype(query, prototypes=None): - # prototype detail - if not prototypes: - prototypes = protlib.search_prototype(key=query) - if prototypes: - return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes) - else: - return False - caller = self.caller + noloc = "noloc" in self.switches + # run the menu/olc if ( self.cmdstring == "olc" or "menu" in self.switches @@ -3368,63 +3518,42 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # OLC menu mode prototype = None if self.lhs: - key = self.lhs - prototype = protlib.search_prototype(key=key) - if len(prototype) > 1: - caller.msg( - "More than one match for {}:\n{}".format( - key, "\n".join(proto.get("prototype_key", "") for proto in prototype) - ) - ) - return - elif prototype: - # one match - prototype = prototype[0] - else: - # no match - caller.msg("No prototype '{}' was found.".format(key)) + prototype_key = self.lhs + prototype = self._search_prototype(prototype_key) + if not prototype: return olc_menus.start_olc(caller, session=self.session, prototype=prototype) return if "search" in self.switches: - # query for a key match + # query for a key match. The arg is a search query or nothing. + if not self.args: - self.switches.append("list") - else: - key, tags = self.args.strip(), None - if ";" in self.args: - key, tags = (part.strip().lower() for part in self.args.split(";", 1)) - tags = [tag.strip() for tag in tags.split(",")] if tags else None - EvMore( - caller, - str(protlib.list_prototypes(caller, key=key, tags=tags)), - exit_on_lastpage=True, - ) + # an empty search returns the full list + self._list_prototypes() return + # search for key;tag combinations + key, _, tags = self._parse_key_desc_tags(self.args, desc=False) + self._list_prototypes(key, tags) + return + if "show" in self.switches or "examine" in self.switches: - # the argument is a key in this case (may be a partial key) + # show a specific prot detail. The argument is a search query or empty. if not self.args: - self.switches.append("list") - else: - matchstring = _search_show_prototype(self.args) - if matchstring: - caller.msg(matchstring) - else: - caller.msg("No prototype '{}' was found.".format(self.args)) + # we don't show the list of all details, that's too spammy. + caller.msg("You need to specify a prototype-key to show.") return - if "list" in self.switches: - # for list, all optional arguments are tags - # import pudb; pudb.set_trace() + detail_string = self._get_prototype_detail(self.args) + if not detail_string: + return + caller.msg(detail_string) + return - EvMore( - caller, - str(protlib.list_prototypes(caller, tags=self.lhslist)), - exit_on_lastpage=True, - justify_kwargs=False, - ) + if "list" in self.switches: + # for list, all optional arguments are tags. + self._list_prototypes(tags=self.lhslist) return if "save" in self.switches: @@ -3435,27 +3564,41 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): ) return + prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs) + # handle rhs: - prototype = _parse_prototype(self.lhs.strip()) + prototype = self._parse_prototype(self.rhs.strip()) if not prototype: return - # present prototype to save - new_matchstring = _search_show_prototype("", prototypes=[prototype]) - string = "|yCreating new prototype:|n\n{}".format(new_matchstring) - question = "\nDo you want to continue saving? [Y]/N" + if prototype.get("prototype_key") != prototype_key: + caller.msg("(Replacing `prototype_key` in prototype with given key.)") + prototype['prototype_key'] = prototype_key + if prototype_desc and prototype.get("prototype_desc") != prototype_desc: + caller.msg("(Replacing `prototype_desc` in prototype with given desc.)") + prototype['prototype_desc'] = prototype_desc + if prototype_tags and prototype.get("prototype_tags") != prototype_tags: + caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))" ) + prototype['prototype_tags'] = prototype_tags - prototype_key = prototype.get("prototype_key") - if not prototype_key: - caller.msg("\n|yTo save a prototype it must have the 'prototype_key' set.") - return + string = "" + # check for existing prototype (exact match) + old_prototype = self._search_prototype(prototype_key, quiet=True) - # check for existing prototype, - old_matchstring = _search_show_prototype(prototype_key) + print("old_prototype", old_prototype) + print("new_prototype", prototype) + diff = spawner.prototype_diff(old_prototype, prototype, homogenize=True) + print("diff", diff) + diffstr = spawner.format_diff(diff) + new_prototype_detail = self._get_prototype_detail(prototypes=[prototype]) - if old_matchstring: - string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring) - question = "\n|yDo you want to replace the existing prototype?|n [Y]/N" + if old_prototype: + string = (f"|yExisting prototype \"{prototype_key}\" found. Change:|n\n{diffstr}\n" + f"|yNew changed prototype:|n\n{new_prototype_detail}") + question = "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N" + else: + string = f"|yCreating new prototype:|n\n{new_prototype_detail}" + question = "\nDo you want to continue saving? [Y]/N" answer = yield (string + question) if answer.lower() in ["n", "no"]: @@ -3474,82 +3617,52 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): caller.msg("|gSaved prototype:|n {}".format(prototype_key)) # check if we want to update existing objects - existing_objects = protlib.search_objects_with_prototype(prototype_key) - if existing_objects: - if "update" not in self.switches: - n_existing = len(existing_objects) - slow = " (note that this may be slow)" if n_existing > 10 else "" - string = ( - "There are {} objects already created with an older version " - "of prototype {}. Should it be re-applied to them{}? [Y]/N".format( - n_existing, prototype_key, slow - ) - ) - answer = yield (string) - if answer.lower() in ["n", "no"]: - caller.msg( - "|rNo update was done of existing objects. " - "Use spawn/update to apply later as needed.|n" - ) - return - n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key) - caller.msg("{} objects were updated.".format(n_updated)) + + self._update_existing_objects(self.caller, prototype_key, quiet=True) return if not self.args: + # all switches beyond this point gets a common non-arg return ncount = len(protlib.search_prototype()) caller.msg( "Usage: spawn or {{key: value, ...}}" - "\n ({} existing prototypes. Use /list to inspect)".format(ncount) + f"\n ({ncount} existing prototypes. Use /list to inspect)" ) return if "delete" in self.switches: # remove db-based prototype - matchstring = _search_show_prototype(self.args) - if matchstring: - string = "|rDeleting prototype:|n\n{}".format(matchstring) - question = "\nDo you want to continue deleting? [Y]/N" - answer = yield (string + question) - if answer.lower() in ["n", "no"]: - caller.msg("|rDeletion cancelled.|n") - return - try: - success = protlib.delete_prototype(self.args) - except protlib.PermissionError as err: - caller.msg("|rError deleting:|R {}|n".format(err)) - caller.msg( - "Deletion {}.".format( - "successful" if success else "failed (does the prototype exist?)" - ) - ) + prototype_detail = self._get_prototype_detail(self.args) + if not prototype_detail: return + + string = f"|rDeleting prototype:|n\n{prototype_detail}" + question = "\nDo you want to continue deleting? [Y]/N" + answer = yield (string + question) + if answer.lower() in ["n", "no"]: + caller.msg("|rDeletion cancelled.|n") + return + + try: + success = protlib.delete_prototype(self.args) + except protlib.PermissionError as err: + retmsg = f"|rError deleting:|R {err}|n" else: - caller.msg("Could not find prototype '{}'".format(key)) + retmsg = ("Deletion successful" if success else + "Deletion failed (does the prototype exist?)") + caller.msg(retmsg) + return if "update" in self.switches: # update existing prototypes - key = self.args.strip().lower() - existing_objects = protlib.search_objects_with_prototype(key) - if existing_objects: - n_existing = len(existing_objects) - slow = " (note that this may be slow)" if n_existing > 10 else "" - string = ( - "There are {} objects already created with an older version " - "of prototype {}. Should it be re-applied to them{}? [Y]/N".format( - n_existing, key, slow - ) - ) - answer = yield (string) - if answer.lower() in ["n", "no"]: - caller.msg("|rUpdate cancelled.") - return - n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key) - caller.msg("{} objects were updated.".format(n_updated)) + prototype_key = self.args.strip().lower() + self._update_existing_objects(self.caller, prototype_key) + return - # A direct creation of an object from a given prototype + # If we get to this point, we use not switches but are trying a + # direct creation of an object from a given prototype or -key - prototype = _parse_prototype( + prototype = self._parse_prototype( self.args, expect=dict if self.args.strip().startswith("{") else str ) if not prototype: @@ -3559,35 +3672,20 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): key = "" if isinstance(prototype, str): # A prototype key we are looking to apply - key = prototype - prototypes = protlib.search_prototype(prototype) - nprots = len(prototypes) - if not prototypes: - caller.msg("No prototype named '%s'." % prototype) - return - elif nprots > 1: - caller.msg( - "Found {} prototypes matching '{}':\n {}".format( - nprots, - prototype, - ", ".join(proto.get("prototype_key", "") for proto in prototypes), - ) - ) - return - # we have a prototype, check access - prototype = prototypes[0] - if not caller.locks.check_lockstring( - caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True - ): - caller.msg("You don't have access to use this prototype.") - return + prototype_key = prototype + prototype = self._search_prototype(prototype_key) - if "noloc" not in self.switches and "location" not in prototype: - prototype["location"] = self.caller.location + if not prototype: + return # proceed to spawning try: for obj in spawner.spawn(prototype): self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) + if not noloc and "location" not in prototype: + # we don't hardcode the location in the prototype (unless the user + # did so manually) - that would lead to it having to be 'removed' every + # time we try to update objects with this prototype in the future. + obj.location = caller.location except RuntimeError as err: caller.msg(err) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 94a3e04eb0..26fd99f1fd 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -56,6 +56,8 @@ _PROTOTYPE_TAG_CATEGORY = "from_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" PROT_FUNCS = {} +_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:perm(Admin)" + class PermissionError(RuntimeError): pass @@ -84,8 +86,19 @@ def homogenize_prototype(prototype, custom_keys=None): homogenizations like adding missing prototype_keys and setting a default typeclass. """ + if not prototype or not isinstance(prototype, dict): + return {} + reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ()) + # correct cases of setting None for certain values + for protkey in prototype: + if prototype[protkey] is None: + if protkey in ("attrs", "tags", "prototype_tags"): + prototype[protkey] = [] + elif protkey in ("prototype_key", "prototype_desc"): + prototype[protkey] = "" + attrs = list(prototype.get("attrs", [])) # break reference tags = make_iter(prototype.get("tags", [])) homogenized_tags = [] @@ -111,12 +124,14 @@ def homogenize_prototype(prototype, custom_keys=None): # add required missing parts that had defaults before - if "prototype_key" not in prototype: + homogenized["prototype_key"] = homogenized.get("prototype_key", # assign a random hash as key - homogenized["prototype_key"] = "prototype-{}".format( - hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7] - ) - + "prototype-{}".format( + hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7])) + homogenized["prototype_tags"] = homogenized.get("prototype_tags", []) + homogenized["prototype_locks"] = homogenized.get( + "prototype_lock", _PROTOTYPE_FALLBACK_LOCK) + homogenized["prototype_desc"] = homogenized.get("prototype_desc", "") if "typeclass" not in prototype and "prototype_parent" not in prototype: homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS @@ -230,7 +245,7 @@ def save_prototype(prototype): "prototype_desc", prototype.get("prototype_desc", "") ) prototype_locks = in_prototype.get( - "prototype_locks", prototype.get("prototype_locks", "spawn:all();edit:perm(Admin)") + "prototype_locks", prototype.get("prototype_locks", _PROTOTYPE_FALLBACK_LOCK) ) is_valid, err = validate_lockstring(prototype_locks) if not is_valid: diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index fab4d2cfdc..98fe2526d7 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -322,9 +322,9 @@ def prototype_from_object(obj): return prot -def prototype_diff(prototype1, prototype2, maxdepth=2): +def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False): """ - A 'detailed' diff specifies differences down to individual sub-sectiions + A 'detailed' diff specifies differences down to individual sub-sections of the prototype, like individual attributes, permissions etc. It is used by the menu to allow a user to customize what should be kept. @@ -334,6 +334,8 @@ def prototype_diff(prototype1, prototype2, maxdepth=2): maxdepth (int, optional): The maximum depth into the diff we go before treating the elements of iterables as individual entities to compare. This is important since a single attr/tag (for example) are represented by a tuple. + homogenize (bool, optional): Auto-homogenize both prototypes for the best comparison. + This is most useful for displaying. Returns: diff (dict): A structure detailing how to convert prototype1 to prototype2. All @@ -350,7 +352,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2): old_type = type(old) new_type = type(new) - if old_type != new_type: + if old_type == new_type and not (old and new): + # both old and new are unset, like [] or None + return (old, new, "KEEP") + elif old_type != new_type: if old and not new: if depth < maxdepth and old_type == dict: return {key: (part, None, "REMOVE") for key, part in old.items()} @@ -387,7 +392,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2): else: return (old, new, "KEEP") - diff = _recursive_diff(prototype1, prototype2) + prot1 = protlib.homogenize_prototype(prototype1) if homogenize else prototype1 + prot2 = protlib.homogenize_prototype(prototype2) if homogenize else prototype2 + + diff = _recursive_diff(prot1, prot2) return diff @@ -490,6 +498,80 @@ def prototype_diff_from_object(prototype, obj): return diff, obj_prototype +def format_diff(diff, minimal=True): + """ + Reformat a diff for presentation. This is a shortened version + of the olc _format_diff_text_and_options without the options. + + Args: + diff (dict): A diff as produced by `prototype_diff`. + minimal (bool, optional): Only show changes (remove KEEPs) + + Returns: + texts (str): The formatted text. + + """ + valid_instructions = ("KEEP", "REMOVE", "ADD", "UPDATE") + + def _visualize(obj, rootname, get_name=False): + if is_iter(obj): + if not obj: + return str(obj) + if get_name: + return obj[0] if obj[0] else "" + if rootname == "attrs": + return "{} |w=|n {} |w(category:|n |n{}|w, locks:|n {}|w)|n".format(*obj) + elif rootname == "tags": + return "{} |w(category:|n {}|w)|n".format(obj[0], obj[1]) + return "{}".format(obj) + + def _parse_diffpart(diffpart, rootname): + typ = type(diffpart) + texts = [] + if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions: + old, new, instruction = diffpart + if instruction == "KEEP": + if not minimal: + texts.append(" |gKEEP|n: {old}".format(old=_visualize(old, rootname))) + elif instruction == "ADD": + texts.append(" |yADD|n: {new}".format(new=_visualize(new, rootname))) + elif instruction == "REMOVE" and not new: + texts.append(" |rREMOVE|n: {old}".format(old=_visualize(old, rootname))) + else: + vold = _visualize(old, rootname) + vnew = _visualize(new, rootname) + vsep = "" if len(vold) < 78 else "\n" + vinst = " |rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction) + varrow = "|r->|n" if instruction == "REMOVE" else "|y->|n" + texts.append( + " {inst}|W:|n {old} |W{varrow}|n{sep} {new}".format( + inst=vinst, old=vold, varrow=varrow, sep=vsep, new=vnew + ) + ) + else: + for key in sorted(list(diffpart.keys())): + subdiffpart = diffpart[key] + text = _parse_diffpart(subdiffpart, rootname) + texts.extend(text) + return texts + + texts = [] + + for root_key in sorted(diff): + diffpart = diff[root_key] + text = _parse_diffpart(diffpart, root_key) + if text or not minimal: + heading = "- |w{}:|n\n".format(root_key) + if text: + text = [heading + text[0]] + text[1:] + else: + text = [heading] + + texts.extend(text) + + return "\n ".join(line for line in texts if line) + + def batch_update_objects_with_prototype(prototype, diff=None, objects=None): """ Update existing objects with the latest version of the prototype. @@ -525,15 +607,13 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): # make sure the diff is flattened diff = flatten_diff(diff) + changed = 0 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(): if directive in ("UPDATE", "REPLACE"): @@ -623,6 +703,11 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): pass else: obj.attributes.remove(key) + + # we must always make sure to re-add the prototype tag + obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) + obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + if do_save: changed += 1 obj.save() @@ -707,7 +792,7 @@ def spawn(*prototypes, **kwargs): Args: prototypes (str or dict): Each argument should either be a prototype_key (will be used to find the prototype) or a full prototype - dictionary. These will be batched-spawned as one object each. + dictionary. These will be batched-spawned as one object each. Kwargs: prototype_modules (str or list): A python-path to a prototype module, or a list of such paths. These will be used to build diff --git a/evennia/server/webserver.py b/evennia/server/webserver.py index 4dc3f71e35..ff7f61baa0 100644 --- a/evennia/server/webserver.py +++ b/evennia/server/webserver.py @@ -70,14 +70,15 @@ class HTTPChannelWithXForwardedFor(http.HTTPChannel): Check to see if this is a reverse proxied connection. """ - CLIENT = 0 - http.HTTPChannel.allHeadersReceived(self) - req = self.requests[-1] - client_ip, port = self.transport.client - proxy_chain = req.getHeader("X-FORWARDED-FOR") - if proxy_chain and client_ip in _UPSTREAM_IPS: - forwarded = proxy_chain.split(", ", 1)[CLIENT] - self.transport.client = (forwarded, port) + if self.requests: + CLIENT = 0 + http.HTTPChannel.allHeadersReceived(self) + req = self.requests[-1] + client_ip, port = self.transport.client + proxy_chain = req.getHeader("X-FORWARDED-FOR") + if proxy_chain and client_ip in _UPSTREAM_IPS: + forwarded = proxy_chain.split(", ", 1)[CLIENT] + self.transport.client = (forwarded, port) # Monkey-patch Twisted to handle X-Forwarded-For.