From 543d5f03dbc7acecb4004dcf2abdc7eca3f5d246 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:51:05 -0600 Subject: [PATCH 01/48] initial patch --- evennia/contrib/rpg/rpsystem/rpsystem.py | 109 ++++++++++++----------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 2c431280c0..7a8caf05f8 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -442,7 +442,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ + [ regex_tuple_from_key_alias(obj) # handle objects without sdescs for obj in candidates - if not (hasattr(obj, "recog") and hasattr(obj, "sdesc")) + if not hasattr(obj, "recog") and not hasattr(obj, "sdesc") ] ) @@ -643,42 +643,13 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): # the form {{#num}} to {#num} markers ready to sdesc-map in the next step. sendemote = emote.format(**receiver_lang_mapping) - # handle sdesc mappings. we make a temporary copy that we can modify - try: - process_sdesc = receiver.process_sdesc - except AttributeError: - process_sdesc = _dummy_process - - try: - process_recog = receiver.process_recog - except AttributeError: - process_recog = _dummy_process - - try: - recog_get = receiver.recog.get - receiver_sdesc_mapping = dict( - (ref, process_recog(recog_get(obj), obj, ref=ref, **kwargs)) - for ref, obj in obj_mapping.items() - ) - except AttributeError: - receiver_sdesc_mapping = dict( - ( - ref, - process_sdesc(obj.sdesc.get(), obj, ref=ref) - if hasattr(obj, "sdesc") - else process_sdesc(obj.key, obj, ref=ref), - ) - for ref, obj in obj_mapping.items() - ) - # make sure receiver always sees their real name - rkey_start = "#%i" % receiver.id - rkey_keep_case = rkey_start + "~" # signifies keeping the case - for rkey in (key for key in receiver_sdesc_mapping if key.startswith(rkey_start)): - # we could have #%i^, #%it etc depending on input case - we want the - # self-reference to retain case. - receiver_sdesc_mapping[rkey] = process_sdesc( - receiver.key, receiver, ref=rkey_keep_case, **kwargs + receiver_sdesc_mapping = dict( + ( + ref, + obj.get_display_name(receiver, ref=ref), ) + for ref, obj in obj_mapping.items() + ) # do the template replacement of the sdesc/recog {#num} markers receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs) @@ -719,11 +690,15 @@ class SdescHandler: def _cache(self): """ Cache data from storage - """ self.sdesc = self.obj.attributes.get("_sdesc", default="") sdesc_regex = self.obj.attributes.get("_sdesc_regex", default="") - self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS) + if self.sdesc: + self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS) + else: + permutation_string = " ".join([self.key] + self.aliases.all()) + self.sdesc_regex = re.compile(ordered_permutation_regex(permutation_string), _RE_FLAGS) + def add(self, sdesc, max_length=60): """ @@ -1346,6 +1321,9 @@ class ContribRPObject(DefaultObject): This class is meant as a mix-in or parent for objects in an rp-heavy game. It implements the base functionality for poses. """ + @lazy_property + def sdesc(self): + return SdescHandler(self) def at_object_creation(self): """ @@ -1539,6 +1517,7 @@ class ContribRPObject(DefaultObject): Keyword Args: pose (bool): Include the pose (if available) in the return. + ref (str): Specifies the capitalization for the displayed name. Returns: name (str): A string of the sdesc containing the name of the object, @@ -1546,20 +1525,18 @@ class ContribRPObject(DefaultObject): including the DBREF if this user is privileged to control said object. - Notes: - The RPObject version doesn't add color to its display. - """ idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else "" + ref = kwargs.get("ref","~") + if looker == self: sdesc = self.key else: try: - recog = looker.recog.get(self) + sdesc = looker.get_sdesc(self, process=True, ref=ref) except AttributeError: - recog = None - sdesc = recog or (hasattr(self, "sdesc") and self.sdesc.get()) or self.key - pose = " %s" % (self.db.pose or "") if kwargs.get("pose", False) else "" + sdesc = self.sdesc.get() + pose = " %s" % (self.db.pose or "is here.") if kwargs.get("pose", False) else "" return "%s%s%s" % (sdesc, idstr, pose) def return_appearance(self, looker): @@ -1635,21 +1612,23 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): said object. Notes: - The RPCharacter version of this method colors its display to make + The RPCharacter version adds additional processing to sdescs to make characters stand out from other objects. """ idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else "" + ref = kwargs.get("ref","~") + if looker == self: - sdesc = self.key + sdesc = self.process_recog(self.key,self) else: try: - recog = looker.recog.get(self) + sdesc = looker.get_sdesc(self, process=True, ref=ref) except AttributeError: - recog = None - sdesc = recog or (hasattr(self, "sdesc") and self.sdesc.get()) or self.key + sdesc = self.sdesc.get() pose = " %s" % (self.db.pose or "is here.") if kwargs.get("pose", False) else "" - return "|c%s|n%s%s" % (sdesc, idstr, pose) + return "%s%s%s" % (sdesc, idstr, pose) + def at_object_creation(self): """ @@ -1682,6 +1661,34 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): return f'/me whispers "{message}"' return f'/me says, "{message}"' + def get_sdesc(self, obj, process=False, **kwargs): + """ + Single hook method to handle getting recogs with sdesc fallback in an + aware manner, to allow separate processing of recogs from sdescs. + Gets the sdesc or recog for obj from the view of self. + + Args: + obj (Object): the object whose sdesc or recog is being gotten + Keyword Args: + process (bool): If True, the sdesc/recog is run through the + appropriate process_X method. + """ + if obj == self: + recog = self.key + sdesc = self.key + else: + try: + recog = self.recog.get(obj) + except AttributeError: + recog = None + sdesc = recog or (hasattr(obj, "sdesc") and obj.sdesc.get()) or obj.key + + if process: + sdesc = (self.process_recog if recog else self.process_sdesc)(sdesc, obj, **kwargs) + + return sdesc + + def process_sdesc(self, sdesc, obj, **kwargs): """ Allows to customize how your sdesc is displayed (primarily by From e0a9310b1bef6eb571fa871cfd53386c02a27667 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 15 Apr 2022 13:00:09 -0600 Subject: [PATCH 02/48] split recog and sdesc colors --- evennia/contrib/rpg/rpsystem/rpsystem.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 7a8caf05f8..33a070b30a 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -1585,11 +1585,6 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): This is a character class that has poses, sdesc and recog. """ - # Handlers - @lazy_property - def sdesc(self): - return SdescHandler(self) - @lazy_property def recog(self): return RecogHandler(self) @@ -1739,14 +1734,15 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): translated from the original sdesc at this point. obj (Object): The object the recog:ed string belongs to. This is not used by default. - Kwargs: - ref (str): See process_sdesc. Returns: recog (str): The modified recog string. """ - return self.process_sdesc(recog, obj, **kwargs) + if not sdesc: + return "" + + return "|m%s|n" % sdesc def process_language(self, text, speaker, language, **kwargs): """ From e7292955efee234065535c07251290396da0c5f1 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 15 Apr 2022 13:54:41 -0600 Subject: [PATCH 03/48] fix a couple typos/tweaks --- evennia/contrib/rpg/rpsystem/rpsystem.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 33a070b30a..a49e9a17cd 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -644,8 +644,8 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): sendemote = emote.format(**receiver_lang_mapping) receiver_sdesc_mapping = dict( - ( - ref, + ( + ref, obj.get_display_name(receiver, ref=ref), ) for ref, obj in obj_mapping.items() @@ -693,12 +693,10 @@ class SdescHandler: """ self.sdesc = self.obj.attributes.get("_sdesc", default="") sdesc_regex = self.obj.attributes.get("_sdesc_regex", default="") - if self.sdesc: - self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS) - else: + if not sdesc_regex: permutation_string = " ".join([self.key] + self.aliases.all()) - self.sdesc_regex = re.compile(ordered_permutation_regex(permutation_string), _RE_FLAGS) - + sdesc_regex = ordered_permutation_regex(permutation_string) + self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS) def add(self, sdesc, max_length=60): """ @@ -1739,10 +1737,10 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): recog (str): The modified recog string. """ - if not sdesc: + if not recog: return "" - return "|m%s|n" % sdesc + return "|m%s|n" % recog def process_language(self, text, speaker, language, **kwargs): """ From 2418594742d78760b9c1af3d398c0d1a88388e5b Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 15 Apr 2022 14:08:48 -0600 Subject: [PATCH 04/48] don't process non-character names --- evennia/contrib/rpg/rpsystem/rpsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index a49e9a17cd..901d731882 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -1531,7 +1531,7 @@ class ContribRPObject(DefaultObject): sdesc = self.key else: try: - sdesc = looker.get_sdesc(self, process=True, ref=ref) + sdesc = looker.get_sdesc(self, ref=ref) except AttributeError: sdesc = self.sdesc.get() pose = " %s" % (self.db.pose or "is here.") if kwargs.get("pose", False) else "" From f1b329c1becd7038be9d4eecee9ed727469d4c50 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 15 Apr 2022 14:56:59 -0600 Subject: [PATCH 05/48] default sdesc is key --- evennia/contrib/rpg/rpsystem/rpsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 901d731882..fa3a5f6097 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -691,7 +691,7 @@ class SdescHandler: """ Cache data from storage """ - self.sdesc = self.obj.attributes.get("_sdesc", default="") + self.sdesc = self.obj.attributes.get("_sdesc", default=obj.key) sdesc_regex = self.obj.attributes.get("_sdesc_regex", default="") if not sdesc_regex: permutation_string = " ".join([self.key] + self.aliases.all()) From 08257e54416fb5be5eae36db812a5a8a83083633 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 15 Apr 2022 14:58:12 -0600 Subject: [PATCH 06/48] Update rpsystem.py --- evennia/contrib/rpg/rpsystem/rpsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index fa3a5f6097..31e4adbb01 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -691,7 +691,7 @@ class SdescHandler: """ Cache data from storage """ - self.sdesc = self.obj.attributes.get("_sdesc", default=obj.key) + self.sdesc = self.obj.attributes.get("_sdesc", default=self.obj.key) sdesc_regex = self.obj.attributes.get("_sdesc_regex", default="") if not sdesc_regex: permutation_string = " ".join([self.key] + self.aliases.all()) From 4127297a3a88fa3bea14297728dc430853e11a40 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 21 Apr 2022 10:37:30 -0600 Subject: [PATCH 07/48] fix typo --- evennia/contrib/rpg/rpsystem/rpsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 31e4adbb01..2edd82b148 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -694,7 +694,7 @@ class SdescHandler: self.sdesc = self.obj.attributes.get("_sdesc", default=self.obj.key) sdesc_regex = self.obj.attributes.get("_sdesc_regex", default="") if not sdesc_regex: - permutation_string = " ".join([self.key] + self.aliases.all()) + permutation_string = " ".join([self.obj.key] + self.obj.aliases.all()) sdesc_regex = ordered_permutation_regex(permutation_string) self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS) From 969e9d9ae5d7cc83b65fe40b0b958a5221c0ca01 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 21 Apr 2022 18:44:48 -0600 Subject: [PATCH 08/48] change how regex is used also a couple other fixes i found; still needs some cleanup --- evennia/contrib/rpg/rpsystem/rpsystem.py | 106 ++++++++++------------- 1 file changed, 47 insertions(+), 59 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 2edd82b148..0cc9f66258 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -151,12 +151,13 @@ Extra Installation Instructions: import re from re import escape as re_escape import itertools +from string import punctuation from django.conf import settings from evennia.objects.objects import DefaultObject, DefaultCharacter from evennia.objects.models import ObjectDB from evennia.commands.command import Command from evennia.commands.cmdset import CmdSet -from evennia.utils import ansi +from evennia.utils import ansi, logger from evennia.utils.utils import lazy_property, make_iter, variable_from_module _REGEX_TUPLE_CACHE = {} @@ -430,24 +431,11 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ - says, "..." are """ - # Load all candidate regex tuples [(regex, obj, sdesc/recog),...] - candidate_regexes = ( - ([(_RE_SELF_REF, sender, sender.sdesc.get())] if hasattr(sender, "sdesc") else []) - + ( - [sender.recog.get_regex_tuple(obj) for obj in candidates] - if hasattr(sender, "recog") - else [] - ) - + [obj.sdesc.get_regex_tuple() for obj in candidates if hasattr(obj, "sdesc")] - + [ - regex_tuple_from_key_alias(obj) # handle objects without sdescs - for obj in candidates - if not hasattr(obj, "recog") and not hasattr(obj, "sdesc") - ] - ) - - # filter out non-found data - candidate_regexes = [tup for tup in candidate_regexes if tup] + candidate_map = [(sender, 'me')] + candidate_map += [ (obj, sender.recog.get(obj)) for obj in candidates if sender.recog.get(obj)] if hasattr(sender, "recog") else [] + candidate_map += [ (obj, obj.sdesc.get()) for obj in candidates if hasattr(obj, "sdesc") ] + for obj in candidates: + candidate_map += [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] # escape mapping syntax on the form {#id} if it exists already in emote, # if so it is replaced with just "id". @@ -469,17 +457,29 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # first see if there is a number given (e.g. 1-tall) num_identifier, _ = marker_match.groups("") # return "" if no match, rather than None istart0 = marker_match.start() - istart = istart0 + istart = istart0 + 1 - # loop over all candidate regexes and match against the string following the match - matches = ((reg.match(string[istart:]), obj, text) for reg, obj, text in candidate_regexes) - - # score matches by how long part of the string was matched - matches = [(match.end() if match else -1, obj, text) for match, obj, text in matches] - maxscore = max(score for score, obj, text in matches) + if search_mode: + rquery = "".join([r"\b(" + re.escape(word.strip(punctuation)) + r").*" for word in iter(string[istart:].split())]) + rquery = re.compile(rquery, _RE_FLAGS) + matches = ((rquery.search(text), obj, text) for obj, text in candidate_map) + bestmatches = [(obj, match.group()) for match, obj, text in matches if match] + else: + word_list = [] + bestmatches = [] + for next_word in iter(string[istart:].split()): + word_list.append(next_word.strip(punctuation)) + rquery = "".join([r"\b(" + re.escape(word) + r").*" for word in word_list]) + rquery = re.compile(rquery, _RE_FLAGS) + matches = ((rquery.search(text), obj, text) for obj, text in candidate_map) + matches = [(obj, match.group()) for match, obj, text in matches if match] + if len(matches) == 0: + # no matches at this length, keep previous iteration + break + # set latest match set as best matches + bestmatches = matches # we have a valid maxscore, extract all matches with this value - bestmatches = [(obj, text) for score, obj, text in matches if maxscore == score != -1] nmatches = len(bestmatches) if not nmatches: @@ -488,12 +488,11 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ nmatches = 0 elif nmatches == 1: # an exact match. - obj = bestmatches[0][0] - nmatches = 1 + obj, match_str = bestmatches[0] elif all(bestmatches[0][0].id == obj.id for obj, text in bestmatches): # multi-match but all matches actually reference the same # obj (could happen with clashing recogs + sdescs) - obj = bestmatches[0][0] + obj, match_str = bestmatches[0] nmatches = 1 else: # multi-match. @@ -501,7 +500,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ inum = min(max(0, int(num_identifier) - 1), nmatches - 1) if num_identifier else None if inum is not None: # A valid inum is given. Use this to separate data. - obj = bestmatches[inum][0] + obj, match_str = bestmatches[inum] nmatches = 1 else: # no identifier given - a real multimatch. @@ -522,19 +521,16 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # - ^ for all upercase input (likle /NAME) # - v for lower-case input (like /name) # - ~ for mixed case input (like /nAmE) - matchtext = marker_match.group() - if not _RE_SELF_REF.match(matchtext): - # self-refs are kept as-is, others are parsed by case - matchtext = marker_match.group().lstrip(_PREFIX) - if matchtext.istitle(): - case = "t" - elif matchtext.isupper(): - case = "^" - elif matchtext.islower(): - case = "v" + matchtext = marker_match.group().lstrip(_PREFIX) + if matchtext.istitle(): + case = "t" + elif matchtext.isupper(): + case = "^" + elif matchtext.islower(): + case = "v" key = "#%i%s" % (obj.id, case) - string = string[:istart0] + "{%s}" % key + string[istart + maxscore :] + string = string[:istart0] + "{%s}" % key + string[istart + len(match_str) :] mapping[key] = obj else: @@ -619,14 +615,16 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): # if anonymous_add is passed as a kwarg, collect and remove it from kwargs if "anonymous_add" in kwargs: anonymous_add = kwargs.pop("anonymous_add") - if anonymous_add and not any(1 for tag in obj_mapping if tag.startswith(skey)): + self_refs = (f"{skey}{ref}" for ref in ('t','^','v','~','')) + if anonymous_add and not any(1 for tag in obj_mapping if tag in self_refs): # no self-reference in the emote - add to the end - obj_mapping[skey] = sender if anonymous_add == "first": + skey = skey + 't' possessive = "" if emote.startswith("'") else " " emote = "%s%s%s" % ("{{%s}}" % skey, possessive, emote) else: emote = "%s [%s]" % (emote, "{{%s}}" % skey) + obj_mapping[skey] = sender # broadcast emote to everyone for receiver in receivers: @@ -684,7 +682,6 @@ class SdescHandler: """ self.obj = obj self.sdesc = "" - self.sdesc_regex = "" self._cache() def _cache(self): @@ -692,11 +689,6 @@ class SdescHandler: Cache data from storage """ self.sdesc = self.obj.attributes.get("_sdesc", default=self.obj.key) - sdesc_regex = self.obj.attributes.get("_sdesc_regex", default="") - if not sdesc_regex: - permutation_string = " ".join([self.obj.key] + self.obj.aliases.all()) - sdesc_regex = ordered_permutation_regex(permutation_string) - self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS) def add(self, sdesc, max_length=60): """ @@ -737,12 +729,9 @@ class SdescHandler: ) # store to attributes - sdesc_regex = ordered_permutation_regex(cleaned_sdesc) self.obj.attributes.add("_sdesc", sdesc) - self.obj.attributes.add("_sdesc_regex", sdesc_regex) # local caching self.sdesc = sdesc - self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS) return sdesc @@ -881,10 +870,10 @@ class RecogHandler: # check an eventual recog_masked lock on the object # to avoid revealing masked characters. If lock # does not exist, pass automatically. - return self.obj2recog.get(obj, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key) + return self.obj2recog.get(obj, None) else: - # recog_mask log not passed, disable recog - return obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key + # recog_mask lock not passed, disable recog + return None def all(self): """ @@ -998,7 +987,6 @@ class CmdSay(RPCommand): # replaces standard say # calling the speech modifying hook speech = caller.at_pre_say(self.args) - # preparing the speech with sdesc/speech parsing. targets = self.caller.location.contents send_emote(self.caller, targets, speech, anonymous_add=None) @@ -1651,8 +1639,8 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): """ if kwargs.get("whisper"): - return f'/me whispers "{message}"' - return f'/me says, "{message}"' + return f'/Me whispers "{message}"' + return f'/Me says, "{message}"' def get_sdesc(self, obj, process=False, **kwargs): """ From b5ec52a51acf0ac52cbe01ddb620d7c2c407a01b Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 21 Apr 2022 21:09:46 -0600 Subject: [PATCH 09/48] strip obj id; other cleanup --- evennia/contrib/rpg/rpsystem/rpsystem.py | 152 +++-------------------- 1 file changed, 19 insertions(+), 133 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 0cc9f66258..2589d67798 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -150,7 +150,6 @@ Extra Installation Instructions: """ import re from re import escape as re_escape -import itertools from string import punctuation from django.conf import settings from evennia.objects.objects import DefaultObject, DefaultCharacter @@ -160,8 +159,6 @@ from evennia.commands.cmdset import CmdSet from evennia.utils import ansi, logger from evennia.utils.utils import lazy_property, make_iter, variable_from_module -_REGEX_TUPLE_CACHE = {} - _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1)) # ------------------------------------------------------------ # Emote parser @@ -240,97 +237,7 @@ class LanguageError(Exception): pass -def _dummy_process(text, *args, **kwargs): - "Pass-through processor" - return text - - # emoting mechanisms - - -def ordered_permutation_regex(sentence): - """ - Builds a regex that matches 'ordered permutations' of a sentence's - words. - - Args: - sentence (str): The sentence to build a match pattern to - - Returns: - regex (re object): Compiled regex object represented the - possible ordered permutations of the sentence, from longest to - shortest. - Example: - The sdesc_regex for an sdesc of " very tall man" will - result in the following allowed permutations, - regex-matched in inverse order of length (case-insensitive): - "the very tall man", "the very tall", "very tall man", - "very tall", "the very", "tall man", "the", "very", "tall", - and "man". - We also add regex to make sure it also accepts num-specifiers, - like /2-tall. - - """ - # escape {#nnn} markers from sentence, replace with nnn - sentence = _RE_REF.sub(r"\1", sentence) - # escape {##nnn} markers, replace with nnn - sentence = _RE_REF_LANG.sub(r"\1", sentence) - # escape self-ref marker from sentence - sentence = _RE_SELF_REF.sub(r"", sentence) - - # ordered permutation algorithm - words = sentence.split() - combinations = itertools.product((True, False), repeat=len(words)) - solution = [] - for combination in combinations: - comb = [] - for iword, word in enumerate(words): - if combination[iword]: - comb.append(word) - elif comb: - break - if comb: - solution.append( - _PREFIX - + r"[0-9]*%s*%s(?=\W|$)+" % (_NUM_SEP, re_escape(" ".join(comb)).rstrip("\\")) - ) - - # combine into a match regex, first matching the longest down to the shortest components - regex = r"|".join(sorted(set(solution), key=lambda item: (-len(item), item))) - return regex - - -def regex_tuple_from_key_alias(obj): - """ - This will build a regex tuple for any object, not just from those - with sdesc/recog handlers. It's used as a legacy mechanism for - being able to mix this contrib with objects not using sdescs, but - note that creating the ordered permutation regex dynamically for - every object will add computational overhead. - - Args: - obj (Object): This object's key and eventual aliases will - be used to build the tuple. - - Returns: - regex_tuple (tuple): A tuple - (ordered_permutation_regex, obj, key/alias) - - - """ - global _REGEX_TUPLE_CACHE - permutation_string = " ".join([obj.key] + obj.aliases.all()) - cache_key = f"{obj.id} {permutation_string}" - - if cache_key not in _REGEX_TUPLE_CACHE: - _REGEX_TUPLE_CACHE[cache_key] = ( - re.compile(ordered_permutation_regex(permutation_string), _RE_FLAGS), - obj, - obj.key, - ) - return _REGEX_TUPLE_CACHE[cache_key] - - def parse_language(speaker, emote): """ Parse the emote for language. This is @@ -432,10 +339,14 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ """ candidate_map = [(sender, 'me')] - candidate_map += [ (obj, sender.recog.get(obj)) for obj in candidates if sender.recog.get(obj)] if hasattr(sender, "recog") else [] - candidate_map += [ (obj, obj.sdesc.get()) for obj in candidates if hasattr(obj, "sdesc") ] for obj in candidates: - candidate_map += [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] + if hasattr(sender, "recog"): + if recog := sender.recog.get(obj): + candidate_map.append((obj, recog)) + if hasattr(obj, "sdesc"): + candidate_map.append((obj, obj.sdesc.get())) + else: + candidate_map += [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] # escape mapping syntax on the form {#id} if it exists already in emote, # if so it is replaced with just "id". @@ -630,13 +541,15 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): for receiver in receivers: # first handle the language mapping, which always produce different keys ##nn receiver_lang_mapping = {} - try: - process_language = receiver.process_language - except AttributeError: - process_language = _dummy_process - for key, (langname, saytext) in language_mapping.items(): - # color says - receiver_lang_mapping[key] = process_language(saytext, sender, langname) + if hasattr(receiver, "process_language") and callable(receiver.process_language): + receiver_lang_mapping = { + key: receiver.process_language(saytext, sender, langname) + for key, (langname, saytext) in language_mapping.items() + } + else: + receiver_lang_mapping = { + key: saytext for key, (langname, saytext) in language_mapping.items() + } # map the language {##num} markers. This will convert the escaped sdesc markers on # the form {{#num}} to {#num} markers ready to sdesc-map in the next step. sendemote = emote.format(**receiver_lang_mapping) @@ -644,7 +557,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): receiver_sdesc_mapping = dict( ( ref, - obj.get_display_name(receiver, ref=ref), + obj.get_display_name(receiver, ref=ref, no_id=True), ) for ref, obj in obj_mapping.items() ) @@ -743,15 +656,6 @@ class SdescHandler: """ return self.sdesc or self.obj.key - def get_regex_tuple(self): - """ - Return data for sdesc/recog handling - - Returns: - tup (tuple): tuple (sdesc_regex, obj, sdesc) - - """ - return self.sdesc_regex, self.obj, self.sdesc class RecogHandler: @@ -779,7 +683,6 @@ class RecogHandler: self.obj = obj # mappings self.ref2recog = {} - self.obj2regex = {} self.obj2recog = {} self._cache() @@ -788,11 +691,7 @@ class RecogHandler: Load data to handler cache """ self.ref2recog = self.obj.attributes.get("_recog_ref2recog", default={}) - obj2regex = self.obj.attributes.get("_recog_obj2regex", default={}) obj2recog = self.obj.attributes.get("_recog_obj2recog", default={}) - self.obj2regex = dict( - (obj, re.compile(regex, _RE_FLAGS)) for obj, regex in obj2regex.items() if obj - ) self.obj2recog = dict((obj, recog) for obj, recog in obj2recog.items() if obj) def add(self, obj, recog, max_length=60): @@ -843,12 +742,9 @@ class RecogHandler: key = "#%i" % obj.id self.obj.attributes.get("_recog_ref2recog", default={})[key] = recog self.obj.attributes.get("_recog_obj2recog", default={})[obj] = recog - regex = ordered_permutation_regex(cleaned_recog) - self.obj.attributes.get("_recog_obj2regex", default={})[obj] = regex # local caching self.ref2recog[key] = recog self.obj2recog[obj] = recog - self.obj2regex[obj] = re.compile(regex, _RE_FLAGS) return recog def get(self, obj): @@ -895,18 +791,8 @@ class RecogHandler: if obj in self.obj2recog: del self.obj.db._recog_obj2recog[obj] del self.obj.db._recog_obj2regex[obj] - del self.obj.db._recog_ref2recog["#%i" % obj.id] self._cache() - def get_regex_tuple(self, obj): - """ - Returns: - rec (tuple): Tuple (recog_regex, obj, recog) - """ - if obj in self.obj2recog and obj.access(self.obj, "enable_recog", default=True): - return self.obj2regex[obj], obj, self.obj2regex[obj] - return None - # ------------------------------------------------------------ # RP Commands @@ -1512,7 +1398,7 @@ class ContribRPObject(DefaultObject): said object. """ - idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else "" + idstr = "(#%s)" % self.id if self.access(looker, access_type="control") and not kwargs.get("no_id", False) else "" ref = kwargs.get("ref","~") if looker == self: @@ -1597,7 +1483,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): characters stand out from other objects. """ - idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else "" + idstr = "(#%s)" % self.id if self.access(looker, access_type="control") and not kwargs.get("no_id",False) else "" ref = kwargs.get("ref","~") if looker == self: From a49caf9f93fe6f878751e1f7cceece68a3e345aa Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 21 Apr 2022 21:10:12 -0600 Subject: [PATCH 10/48] don't need those any more --- evennia/contrib/rpg/rpsystem/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/__init__.py b/evennia/contrib/rpg/rpsystem/__init__.py index 9affd32487..395aef660b 100644 --- a/evennia/contrib/rpg/rpsystem/__init__.py +++ b/evennia/contrib/rpg/rpsystem/__init__.py @@ -4,7 +4,6 @@ Roleplaying emotes and language - Griatch, 2015 """ from .rpsystem import EmoteError, SdescError, RecogError, LanguageError # noqa -from .rpsystem import ordered_permutation_regex, regex_tuple_from_key_alias # noqa from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa from .rpsystem import SdescHandler, RecogHandler # noqa from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa From 180b3f222fe7ce3001807934e86f86050f8a6af1 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 21 Apr 2022 21:14:08 -0600 Subject: [PATCH 11/48] Update tests.py --- evennia/contrib/rpg/rpsystem/tests.py | 48 +-------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py index cd96ac43b1..d603b6fbff 100644 --- a/evennia/contrib/rpg/rpsystem/tests.py +++ b/evennia/contrib/rpg/rpsystem/tests.py @@ -113,41 +113,11 @@ class TestRPSystem(BaseEvenniaTest): rpsystem.ContribRPCharacter, key="Receiver2", location=self.room ) - def test_ordered_permutation_regex(self): - self.assertEqual( - rpsystem.ordered_permutation_regex(sdesc0), - "/[0-9]*-*A\\ nice\\ sender\\ of\\ emotes(?=\\W|$)+|" - "/[0-9]*-*nice\\ sender\\ of\\ emotes(?=\\W|$)+|" - "/[0-9]*-*A\\ nice\\ sender\\ of(?=\\W|$)+|" - "/[0-9]*-*sender\\ of\\ emotes(?=\\W|$)+|" - "/[0-9]*-*nice\\ sender\\ of(?=\\W|$)+|" - "/[0-9]*-*A\\ nice\\ sender(?=\\W|$)+|" - "/[0-9]*-*nice\\ sender(?=\\W|$)+|" - "/[0-9]*-*of\\ emotes(?=\\W|$)+|" - "/[0-9]*-*sender\\ of(?=\\W|$)+|" - "/[0-9]*-*A\\ nice(?=\\W|$)+|" - "/[0-9]*-*emotes(?=\\W|$)+|" - "/[0-9]*-*sender(?=\\W|$)+|" - "/[0-9]*-*nice(?=\\W|$)+|" - "/[0-9]*-*of(?=\\W|$)+|" - "/[0-9]*-*A(?=\\W|$)+", - ) - def test_sdesc_handler(self): self.speaker.sdesc.add(sdesc0) self.assertEqual(self.speaker.sdesc.get(), sdesc0) self.speaker.sdesc.add("This is {#324} ignored") self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored") - self.speaker.sdesc.add("Testing three words") - self.assertEqual( - self.speaker.sdesc.get_regex_tuple()[0].pattern, - "/[0-9]*-*Testing\ three\ words(?=\W|$)+|" - "/[0-9]*-*Testing\ three(?=\W|$)+|" - "/[0-9]*-*three\ words(?=\W|$)+|" - "/[0-9]*-*Testing(?=\W|$)+|" - "/[0-9]*-*three(?=\W|$)+|" - "/[0-9]*-*words(?=\W|$)+", - ) def test_recog_handler(self): self.speaker.sdesc.add(sdesc0) @@ -156,12 +126,8 @@ class TestRPSystem(BaseEvenniaTest): self.speaker.recog.add(self.receiver2, recog02) self.assertEqual(self.speaker.recog.get(self.receiver1), recog01) self.assertEqual(self.speaker.recog.get(self.receiver2), recog02) - self.assertEqual( - self.speaker.recog.get_regex_tuple(self.receiver1)[0].pattern, - "/[0-9]*-*Mr\\ Receiver(?=\\W|$)+|/[0-9]*-*Receiver(?=\\W|$)+|/[0-9]*-*Mr(?=\\W|$)+", - ) self.speaker.recog.remove(self.receiver1) - self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1) + self.assertEqual(self.speaker.recog.get(self.receiver1), None) self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2}) @@ -265,18 +231,6 @@ class TestRPSystem(BaseEvenniaTest): self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1) self.assertEqual(self.speaker.search("colliding"), self.receiver2) - def test_regex_tuple_from_key_alias(self): - self.speaker.aliases.add("foo bar") - self.speaker.aliases.add("this thing is a long thing") - t0 = time.time() - result = rpsystem.regex_tuple_from_key_alias(self.speaker) - t1 = time.time() - result = rpsystem.regex_tuple_from_key_alias(self.speaker) - t2 = time.time() - # print(f"t1: {t1 - t0}, t2: {t2 - t1}") - self.assertLess(t2 - t1, 10**-4) - self.assertEqual(result, (Anything, self.speaker, self.speaker.key)) - class TestRPSystemCommands(BaseEvenniaCommandTest): def setUp(self): From 2047c64e9db29c82daf243d582e0d54ed22b1bf5 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 21 Apr 2022 21:46:06 -0600 Subject: [PATCH 12/48] Update rpsystem.py --- evennia/contrib/rpg/rpsystem/rpsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 2589d67798..cc10b0b99d 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -526,7 +526,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): # if anonymous_add is passed as a kwarg, collect and remove it from kwargs if "anonymous_add" in kwargs: anonymous_add = kwargs.pop("anonymous_add") - self_refs = (f"{skey}{ref}" for ref in ('t','^','v','~','')) + self_refs = [f"{skey}{ref}" for ref in ('t','^','v','~','')] if anonymous_add and not any(1 for tag in obj_mapping if tag in self_refs): # no self-reference in the emote - add to the end if anonymous_add == "first": From 55e7b3c93df80b5939ec7ed4da9a39dd6ddeeab8 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 21 Apr 2022 21:57:51 -0600 Subject: [PATCH 13/48] deleted the wrong line --- evennia/contrib/rpg/rpsystem/rpsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index cc10b0b99d..b79a1974ec 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -790,7 +790,7 @@ class RecogHandler: """ if obj in self.obj2recog: del self.obj.db._recog_obj2recog[obj] - del self.obj.db._recog_obj2regex[obj] + del self.obj.db._recog_ref2recog["#%i" % obj.id] self._cache() From 7568cb29b96c5978705b839aa748cb2ede04f811 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 22 Apr 2022 11:39:49 -0600 Subject: [PATCH 14/48] update format color for recog vs sdesc --- evennia/contrib/rpg/rpsystem/tests.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py index d603b6fbff..2c45e9a8a0 100644 --- a/evennia/contrib/rpg/rpsystem/tests.py +++ b/evennia/contrib/rpg/rpsystem/tests.py @@ -96,7 +96,7 @@ recog01 = "Mr Receiver" recog02 = "Mr Receiver2" recog10 = "Mr Sender" emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."' -case_emote = "/me looks at /first, then /FIRST, /First and /Colliding twice." +case_emote = "/Me looks at /first, then /FIRST, /First and /Colliding twice." class TestRPSystem(BaseEvenniaTest): @@ -178,18 +178,18 @@ class TestRPSystem(BaseEvenniaTest): rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False) self.assertEqual( self.out0, - "With a flair, |bSender|n looks at |bThe first receiver of emotes.|n " + "With a flair, |mSender|n looks at |bThe first receiver of emotes.|n " 'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n', ) self.assertEqual( self.out1, - "With a flair, |bA nice sender of emotes|n looks at |bReceiver1|n and " + "With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and " '|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n', ) self.assertEqual( self.out2, "With a flair, |bA nice sender of emotes|n looks at |bThe first " - 'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n', + 'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n', ) def test_send_case_sensitive_emote(self): @@ -207,20 +207,20 @@ class TestRPSystem(BaseEvenniaTest): rpsystem.send_emote(speaker, receivers, case_emote) self.assertEqual( self.out0, - "|bSender|n looks at |bthe first receiver of emotes.|n, then " + "|mSender|n looks at |bthe first receiver of emotes.|n, then " "|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and " "|bAnother nice colliding sdesc-guy for tests|n twice.", ) self.assertEqual( self.out1, - "|bA nice sender of emotes|n looks at |bReceiver1|n, then |bReceiver1|n, " - "|bReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.", + "|bA nice sender of emotes|n looks at |mReceiver1|n, then |mReceiver1|n, " + "|mReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.", ) self.assertEqual( self.out2, "|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, " "then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of " - "emotes.|n and |bReceiver2|n twice.", + "emotes.|n and |mReceiver2|n twice.", ) def test_rpsearch(self): From 0769ffbef1fbcbf820ba6f2acae9de016c96813a Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 22 Apr 2022 11:40:47 -0600 Subject: [PATCH 15/48] Update rpsystem.py --- evennia/contrib/rpg/rpsystem/rpsystem.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index b79a1974ec..836e7ea4d9 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -367,11 +367,12 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # first see if there is a number given (e.g. 1-tall) num_identifier, _ = marker_match.groups("") # return "" if no match, rather than None - istart0 = marker_match.start() - istart = istart0 + 1 + match_index = marker_match.start() + head = string[:match_index] + tail = string[match_index+1:] if search_mode: - rquery = "".join([r"\b(" + re.escape(word.strip(punctuation)) + r").*" for word in iter(string[istart:].split())]) + rquery = "".join([r"\b(" + re.escape(word.strip(punctuation)) + r").*" for word in iter(tail.split())]) rquery = re.compile(rquery, _RE_FLAGS) matches = ((rquery.search(text), obj, text) for obj, text in candidate_map) bestmatches = [(obj, match.group()) for match, obj, text in matches if match] @@ -379,8 +380,12 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ else: word_list = [] bestmatches = [] - for next_word in iter(string[istart:].split()): - word_list.append(next_word.strip(punctuation)) + tail = re.split('(\W)', tail) + istart = 0 + for i, item in enumerate(tail): + if not item.isalpha(): + continue + word_list.append(item) rquery = "".join([r"\b(" + re.escape(word) + r").*" for word in word_list]) rquery = re.compile(rquery, _RE_FLAGS) matches = ((rquery.search(text), obj, text) for obj, text in candidate_map) @@ -390,7 +395,10 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ break # set latest match set as best matches bestmatches = matches - # we have a valid maxscore, extract all matches with this value + istart = i + + tail = "".join(tail[istart+1:]) + nmatches = len(bestmatches) if not nmatches: @@ -441,7 +449,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ case = "v" key = "#%i%s" % (obj.id, case) - string = string[:istart0] + "{%s}" % key + string[istart + len(match_str) :] + string = f"{head}{{{key}}}{tail}" mapping[key] = obj else: @@ -1109,7 +1117,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room if forget_mode: # remove existing recog caller.recog.remove(obj) - caller.msg("%s will now know them only as '%s'." % (caller.key, obj.recog.get(obj))) + caller.msg("%s will now know them only as '%s'." % (caller.key, obj.get_display_name(caller, no_id=True))) else: # set recog sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key From e86b81917bc915a7c0a5668f56402273b642b72b Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 25 Apr 2022 11:43:07 -0600 Subject: [PATCH 16/48] added more comments, cleaned up a couple lines --- evennia/contrib/rpg/rpsystem/rpsystem.py | 104 ++++++++++++++++------- 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 836e7ea4d9..295aa2a55b 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -149,7 +149,6 @@ Extra Installation Instructions: """ import re -from re import escape as re_escape from string import punctuation from django.conf import settings from evennia.objects.objects import DefaultObject, DefaultCharacter @@ -338,13 +337,18 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ - says, "..." are """ + # build a list of candidates with all possible referrable names + # include 'me' keyword for self-ref candidate_map = [(sender, 'me')] for obj in candidates: + # check if sender has any recogs for obj and add if hasattr(sender, "recog"): if recog := sender.recog.get(obj): candidate_map.append((obj, recog)) + # check if obj has an sdesc and add if hasattr(obj, "sdesc"): candidate_map.append((obj, obj.sdesc.get())) + # if no sdesc, include key plus aliases instead else: candidate_map += [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] @@ -370,34 +374,41 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ match_index = marker_match.start() head = string[:match_index] tail = string[match_index+1:] - + if search_mode: + # match the candidates against the whole search string rquery = "".join([r"\b(" + re.escape(word.strip(punctuation)) + r").*" for word in iter(tail.split())]) - rquery = re.compile(rquery, _RE_FLAGS) - matches = ((rquery.search(text), obj, text) for obj, text in candidate_map) + matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map) + # filter out any non-matching candidates bestmatches = [(obj, match.group()) for match, obj, text in matches if match] else: + # to find the longest match, we start from the marker and lengthen the + # match query one word at a time. word_list = [] bestmatches = [] + # preserve punctuation when splitting tail = re.split('(\W)', tail) - istart = 0 + iend = 0 for i, item in enumerate(tail): + # don't add non-word characters to the search query if not item.isalpha(): continue word_list.append(item) rquery = "".join([r"\b(" + re.escape(word) + r").*" for word in word_list]) - rquery = re.compile(rquery, _RE_FLAGS) - matches = ((rquery.search(text), obj, text) for obj, text in candidate_map) + # match candidates against the current set of words + matches = ((re.search(re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map) matches = [(obj, match.group()) for match, obj, text in matches if match] if len(matches) == 0: - # no matches at this length, keep previous iteration + # no matches at this length, keep previous iteration as best break - # set latest match set as best matches + # since this is the longest match so far, set latest match set as best matches bestmatches = matches - istart = i + # save current index as end point of matched text + iend = i - tail = "".join(tail[istart+1:]) + # recombine remainder of emote back into a string + tail = "".join(tail[iend+1:]) nmatches = len(bestmatches) @@ -448,7 +459,8 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ elif matchtext.islower(): case = "v" - key = "#%i%s" % (obj.id, case) + key = f"#{obj.id}{case}" + # recombine emote with matched text replaced by ref string = f"{head}{{{key}}}{tail}" mapping[key] = obj @@ -534,14 +546,17 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): # if anonymous_add is passed as a kwarg, collect and remove it from kwargs if "anonymous_add" in kwargs: anonymous_add = kwargs.pop("anonymous_add") + # make sure to catch all possible self-refs self_refs = [f"{skey}{ref}" for ref in ('t','^','v','~','')] if anonymous_add and not any(1 for tag in obj_mapping if tag in self_refs): - # no self-reference in the emote - add to the end + # no self-reference in the emote - add it if anonymous_add == "first": - skey = skey + 't' + # add case flag for initial caps + skey += 't' possessive = "" if emote.startswith("'") else " " emote = "%s%s%s" % ("{{%s}}" % skey, possessive, emote) else: + # add it to the end emote = "%s [%s]" % (emote, "{{%s}}" % skey) obj_mapping[skey] = sender @@ -562,10 +577,11 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): # the form {{#num}} to {#num} markers ready to sdesc-map in the next step. sendemote = emote.format(**receiver_lang_mapping) + # map the ref keys to sdescs receiver_sdesc_mapping = dict( ( ref, - obj.get_display_name(receiver, ref=ref, no_id=True), + obj.get_display_name(receiver, noid=True), ) for ref, obj in obj_mapping.items() ) @@ -1117,7 +1133,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room if forget_mode: # remove existing recog caller.recog.remove(obj) - caller.msg("%s will now know them only as '%s'." % (caller.key, obj.get_display_name(caller, no_id=True))) + caller.msg("%s will now know them only as '%s'." % (caller.key, obj.get_display_name(caller, noid=True))) else: # set recog sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key @@ -1397,24 +1413,34 @@ class ContribRPObject(DefaultObject): Keyword Args: pose (bool): Include the pose (if available) in the return. - ref (str): Specifies the capitalization for the displayed name. + ref (str): The reference marker found in string to replace. + This is on the form #{num}{case}, like '#12^', where + the number is a processing location in the string and the + case symbol indicates the case of the original tag input + - `t` - input was Titled, like /Tall + - `^` - input was all uppercase, like /TALL + - `v` - input was all lowercase, like /tall + - `~` - input case should be kept, or was mixed-case + noid (bool): Don't show DBREF even if viewer has control access. Returns: name (str): A string of the sdesc containing the name of the object, - if this is defined. - including the DBREF if this user is privileged to control - said object. + if this is defined. By default, included the DBREF if this user + is privileged to control said object. """ - idstr = "(#%s)" % self.id if self.access(looker, access_type="control") and not kwargs.get("no_id", False) else "" + idstr = "(#%s)" % self.id if self.access(looker, access_type="control") and not kwargs.get("noid",False) else "" ref = kwargs.get("ref","~") if looker == self: + # always show your own key sdesc = self.key else: try: + # get the sdesc looker should see sdesc = looker.get_sdesc(self, ref=ref) except AttributeError: + # use own sdesc as a fallback sdesc = self.sdesc.get() pose = " %s" % (self.db.pose or "is here.") if kwargs.get("pose", False) else "" return "%s%s%s" % (sdesc, idstr, pose) @@ -1426,6 +1452,10 @@ class ContribRPObject(DefaultObject): Args: looker (Object): Object doing the looking. + + Returns: + string (str): A string containing the name, appearance and contents + of the object. """ if not looker: return "" @@ -1449,6 +1479,7 @@ class ContribRPObject(DefaultObject): string += "\n|wExits:|n " + ", ".join(exits) if users or things: string += "\n " + "\n ".join(users + things) + return string @@ -1479,19 +1510,27 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): Keyword Args: pose (bool): Include the pose (if available) in the return. + ref (str): The reference marker found in string to replace. + This is on the form #{num}{case}, like '#12^', where + the number is a processing location in the string and the + case symbol indicates the case of the original tag input + - `t` - input was Titled, like /Tall + - `^` - input was all uppercase, like /TALL + - `v` - input was all lowercase, like /tall + - `~` - input case should be kept, or was mixed-case + noid (bool): Don't show DBREF even if viewer has control access. Returns: name (str): A string of the sdesc containing the name of the object, - if this is defined. - including the DBREF if this user is privileged to control - said object. + if this is defined. By default, included the DBREF if this user + is privileged to control said object. Notes: The RPCharacter version adds additional processing to sdescs to make characters stand out from other objects. """ - idstr = "(#%s)" % self.id if self.access(looker, access_type="control") and not kwargs.get("no_id",False) else "" + idstr = "(#%s)" % self.id if self.access(looker, access_type="control") and not kwargs.get("noid",False) else "" ref = kwargs.get("ref","~") if looker == self: @@ -1512,10 +1551,8 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): super().at_object_creation() self.db._sdesc = "" - self.db._sdesc_regex = "" self.db._recog_ref2recog = {} - self.db._recog_obj2regex = {} self.db._recog_obj2recog = {} self.cmdset.add(RPSystemCmdSet, persistent=True) @@ -1538,7 +1575,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): def get_sdesc(self, obj, process=False, **kwargs): """ - Single hook method to handle getting recogs with sdesc fallback in an + Single method to handle getting recogs with sdesc fallback in an aware manner, to allow separate processing of recogs from sdescs. Gets the sdesc or recog for obj from the view of self. @@ -1546,19 +1583,20 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): obj (Object): the object whose sdesc or recog is being gotten Keyword Args: process (bool): If True, the sdesc/recog is run through the - appropriate process_X method. + appropriate process method (process_sdesc or process_recog) """ + # always see own key if obj == self: recog = self.key sdesc = self.key else: - try: - recog = self.recog.get(obj) - except AttributeError: - recog = None + # first check if we have a recog for this object + recog = self.recog.get(obj) + # set sdesc to recog, using sdesc as a fallback, or the object's key if no sdesc sdesc = recog or (hasattr(obj, "sdesc") and obj.sdesc.get()) or obj.key if process: + # process the sdesc as a recog if a recog was found, else as an sdesc sdesc = (self.process_recog if recog else self.process_sdesc)(sdesc, obj, **kwargs) return sdesc From 97b83ef2410435ef318d1cedb729335322bf92ac Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 25 Apr 2022 11:52:13 -0600 Subject: [PATCH 17/48] update RecogHandler.get docstring --- evennia/contrib/rpg/rpsystem/rpsystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 295aa2a55b..bfa638af64 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -773,13 +773,13 @@ class RecogHandler: def get(self, obj): """ - Get recog replacement string, if one exists, otherwise - get sdesc and as a last resort, the object's key. + Get recog replacement string, if one exists. Args: obj (Object): The object, whose sdesc to replace Returns: - recog (str): The replacement string to use. + recog (str or None): The replacement string to use, or + None if there is no recog for this object. Notes: This method will respect a "enable_recog" lock set on From 052714f82b7edc20d680347fabc45f59ded49dfb Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 25 Apr 2022 11:56:08 -0600 Subject: [PATCH 18/48] correct .get_sdesc docstring --- evennia/contrib/rpg/rpsystem/rpsystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index bfa638af64..5227e73dc0 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -1583,7 +1583,8 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): obj (Object): the object whose sdesc or recog is being gotten Keyword Args: process (bool): If True, the sdesc/recog is run through the - appropriate process method (process_sdesc or process_recog) + appropriate process method for self - .process_sdesc or + .process_recog """ # always see own key if obj == self: From 15be72489af441dd3d2fc0bf75be17534ac51248 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 25 Apr 2022 12:58:49 -0600 Subject: [PATCH 19/48] fix typo, add comment --- evennia/contrib/rpg/rpsystem/rpsystem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 5227e73dc0..a14fc29d0a 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -372,11 +372,12 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # first see if there is a number given (e.g. 1-tall) num_identifier, _ = marker_match.groups("") # return "" if no match, rather than None match_index = marker_match.start() + # split the emote string at the reference marker, to process everything after it head = string[:match_index] tail = string[match_index+1:] if search_mode: - # match the candidates against the whole search string + # match the candidates against the whole search string after the marker rquery = "".join([r"\b(" + re.escape(word.strip(punctuation)) + r").*" for word in iter(tail.split())]) matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map) # filter out any non-matching candidates @@ -397,7 +398,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ word_list.append(item) rquery = "".join([r"\b(" + re.escape(word) + r").*" for word in word_list]) # match candidates against the current set of words - matches = ((re.search(re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map) + matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map) matches = [(obj, match.group()) for match, obj, text in matches if match] if len(matches) == 0: # no matches at this length, keep previous iteration as best From 9b114b0bea7c0d26d66ebb4cb1821d470d9867ac Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:07:44 -0600 Subject: [PATCH 20/48] implement `get_posed_sdesc` --- evennia/contrib/rpg/rpsystem/rpsystem.py | 30 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index a14fc29d0a..2b0265d165 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -1232,6 +1232,10 @@ class ContribRPObject(DefaultObject): self.db.pose = "" self.db.pose_default = "is here." + # initializing sdesc + self.db._sdesc = "" + self.sdesc.add("Something") + def search( self, searchdata, @@ -1404,6 +1408,22 @@ class ContribRPObject(DefaultObject): multimatch_string=multimatch_string, ) + def get_posed_sdesc(self, sdesc, **kwargs): + """ + Displays the object with its current pose string. + + Returns: + pose (str): A string containing the object's sdesc and + current or default pose. + """ + + # get the current pose, or default if no pose is set + pose = self.db.pose or self.db.pose_default + + # return formatted string, or sdesc as fallback + return f"{sdesc} {pose}" if pose else sdesc + + def get_display_name(self, looker, **kwargs): """ Displays the name of the object in a viewer-aware manner. @@ -1430,7 +1450,6 @@ class ContribRPObject(DefaultObject): is privileged to control said object. """ - idstr = "(#%s)" % self.id if self.access(looker, access_type="control") and not kwargs.get("noid",False) else "" ref = kwargs.get("ref","~") if looker == self: @@ -1443,8 +1462,13 @@ class ContribRPObject(DefaultObject): except AttributeError: # use own sdesc as a fallback sdesc = self.sdesc.get() - pose = " %s" % (self.db.pose or "is here.") if kwargs.get("pose", False) else "" - return "%s%s%s" % (sdesc, idstr, pose) + + # add dbref is looker has control access and `noid` is not set + if self.access(looker, access_type="control") and not kwargs.get("noid",False): + sdesc = f"{sdesc}{self.id}" + + return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc + def return_appearance(self, looker): """ From 865fc14ef82cae8ed476d6d22976f14b83ee392a Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:12:53 -0600 Subject: [PATCH 21/48] update character display name too --- evennia/contrib/rpg/rpsystem/rpsystem.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 2b0265d165..777e4507e1 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -1555,18 +1555,24 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): characters stand out from other objects. """ - idstr = "(#%s)" % self.id if self.access(looker, access_type="control") and not kwargs.get("noid",False) else "" ref = kwargs.get("ref","~") if looker == self: + # process your key as recog since you recognize yourself sdesc = self.process_recog(self.key,self) else: try: + # get the sdesc looker should see, with formatting sdesc = looker.get_sdesc(self, process=True, ref=ref) except AttributeError: + # use own sdesc as a fallback sdesc = self.sdesc.get() - pose = " %s" % (self.db.pose or "is here.") if kwargs.get("pose", False) else "" - return "%s%s%s" % (sdesc, idstr, pose) + + # add dbref is looker has control access and `noid` is not set + if self.access(looker, access_type="control") and not kwargs.get("noid",False): + sdesc = f"{sdesc}{self.id}" + + return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc def at_object_creation(self): From 344cde81e18d62324b843a43c2aa0d2f8fb7a484 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:20:42 -0600 Subject: [PATCH 22/48] remove unnecessary declaration --- evennia/contrib/rpg/rpsystem/rpsystem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 777e4507e1..1e311260aa 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -564,7 +564,6 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): # broadcast emote to everyone for receiver in receivers: # first handle the language mapping, which always produce different keys ##nn - receiver_lang_mapping = {} if hasattr(receiver, "process_language") and callable(receiver.process_language): receiver_lang_mapping = { key: receiver.process_language(saytext, sender, langname) From 8b612a9e9cba13ef9ad71972b00920521a70eaed Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 28 Apr 2022 13:58:05 -0600 Subject: [PATCH 23/48] first pass on switching to format/fstring --- evennia/contrib/rpg/rpsystem/rpsystem.py | 90 ++++++++++++------------ 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 1e311260aa..069f1196a4 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -282,9 +282,9 @@ def parse_language(speaker, emote): langname, saytext = say_match.groups() istart, iend = say_match.start(), say_match.end() # the key is simply the running match in the emote - key = "##%i" % imatch + key = f"##{imatch}" # replace say with ref markers in emote - emote = emote[:istart] + "{%s}" % key + emote[iend:] + emote = "{start}{key}{end}".format( start=emote[:istart], key=key, end=emote[iend:] ) mapping[key] = (langname, saytext) if errors: @@ -449,7 +449,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # case sensitive mode # internal flags for the case used for the original /query # - t for titled input (like /Name) - # - ^ for all upercase input (likle /NAME) + # - ^ for all upercase input (like /NAME) # - v for lower-case input (like /name) # - ~ for mixed case input (like /nAmE) matchtext = marker_match.group().lstrip(_PREFIX) @@ -469,13 +469,12 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # multimatch error refname = marker_match.group() reflist = [ - "%s%s%s (%s%s)" - % ( - inum + 1, - _NUM_SEP, - _RE_PREFIX.sub("", refname), - text, - " (%s)" % sender.key if sender == ob else "", + "{name}{sep}{num} ({text}{key})".format( + num=inum + 1, + sep=_NUM_SEP, + name=_RE_PREFIX.sub("", refname), + text=text, + key=f" ({sender.key})" if sender == ob else "", ) for inum, (ob, text) in enumerate(obj) ] @@ -539,7 +538,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): sender.msg(str(err)) return - skey = "#%i" % sender.id + skey = f"#{sender.id}" # we escape the object mappings since we'll do the language ones first # (the text could have nested object mappings). @@ -554,11 +553,12 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): if anonymous_add == "first": # add case flag for initial caps skey += 't' - possessive = "" if emote.startswith("'") else " " - emote = "%s%s%s" % ("{{%s}}" % skey, possessive, emote) + # don't put a space after the self-ref if it's a possessive emote + femote = "{key}{emote}" if emote.startswith("'") else "{key} {emote}" else: # add it to the end - emote = "%s [%s]" % (emote, "{{%s}}" % skey) + femote = "{emote} [{key}]" + emote = femote.format( key="{{"+ skey +"}}", emote=emote ) obj_mapping[skey] = sender # broadcast emote to everyone @@ -661,8 +661,7 @@ class SdescHandler: if len(cleaned_sdesc) > max_length: raise SdescError( - "Short desc can max be %i chars long (was %i chars)." - % (max_length, len(cleaned_sdesc)) + "Short desc can max be {} chars long (was {} chars).".format(max_length, len(cleaned_sdesc)) ) # store to attributes @@ -692,7 +691,6 @@ class RecogHandler: _recog_ref2recog _recog_obj2recog - _recog_obj2regex """ @@ -758,12 +756,11 @@ class RecogHandler: if len(cleaned_recog) > max_length: raise RecogError( - "Recog string cannot be longer than %i chars (was %i chars)" - % (max_length, len(cleaned_recog)) + "Recog string cannot be longer than {} chars (was {} chars)".format(max_length, len(cleaned_recog)) ) # mapping #dbref:obj - key = "#%i" % obj.id + key = f"#{obj.id}" self.obj.attributes.get("_recog_ref2recog", default={})[key] = recog self.obj.attributes.get("_recog_obj2recog", default={})[obj] = recog # local caching @@ -814,7 +811,7 @@ class RecogHandler: """ if obj in self.obj2recog: del self.obj.db._recog_obj2recog[obj] - del self.obj.db._recog_ref2recog["#%i" % obj.id] + del self.obj.db._recog_ref2recog[f"#{obj.id}"] self._cache() @@ -866,8 +863,8 @@ class CmdEmote(RPCommand): # replaces the main emote # we also include ourselves here. emote = self.args targets = self.caller.location.contents - if not emote.endswith((".", "?", "!")): # If emote is not punctuated, - emote = "%s." % emote # add a full-stop for good measure. + if not emote.endswith((".", "?", "!", '"')): # If emote is not punctuated or speech, + emote += "." # add a full-stop for good measure. send_emote(self.caller, targets, emote, anonymous_add="first") @@ -932,7 +929,7 @@ class CmdSdesc(RPCommand): # set/look at own sdesc except AttributeError: caller.msg(f"Cannot set sdesc on {caller.key}.") return - caller.msg("%s's sdesc was set to '%s'." % (caller.key, sdesc)) + caller.msg(f"{caller.key}'s sdesc was set to '{sdesc}'.") class CmdPose(RPCommand): # set current pose and default pose @@ -992,8 +989,8 @@ class CmdPose(RPCommand): # set current pose and default pose caller.msg("Usage: pose OR pose obj = ") return - if not pose.endswith("."): - pose = "%s." % pose + if not pose.endswith((".", "?", "!", '"')): + pose += "." if target: # affect something else target = caller.search(target) @@ -1005,18 +1002,18 @@ class CmdPose(RPCommand): # set current pose and default pose else: target = caller + target_name = target.sdesc.get() if hasattr(target, "sdesc") else target.key if not target.attributes.has("pose"): - caller.msg("%s cannot be posed." % target.key) + caller.msg(f"{target_name} cannot be posed.") return - target_name = target.sdesc.get() if hasattr(target, "sdesc") else target.key # set the pose if self.reset: pose = target.db.pose_default target.db.pose = pose elif self.default: target.db.pose_default = pose - caller.msg("Default pose is now '%s %s'." % (target_name, pose)) + caller.msg(f"Default pose is now '{target_name} {pose}'.") return else: # set the pose. We do one-time ref->sdesc mapping here. @@ -1028,12 +1025,12 @@ class CmdPose(RPCommand): # set current pose and default pose pose = parsed.format(**mapping) if len(target_name) + len(pose) > 60: - caller.msg("Your pose '%s' is too long." % pose) + caller.msg(f"'{pose}' is too long.") return target.db.pose = pose - caller.msg("Pose will read '%s %s'." % (target_name, pose)) + caller.msg(f"Pose will read '{target_name} {pose}'.") class CmdRecog(RPCommand): # assign personal alias to object in room @@ -1112,12 +1109,12 @@ class CmdRecog(RPCommand): # assign personal alias to object in room caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc)) elif nmatches > 1: reflist = [ - "{}{}{} ({}{})".format( - inum + 1, - _NUM_SEP, - _RE_PREFIX.sub("", sdesc), - caller.recog.get(obj), - " (%s)" % caller.key if caller == obj else "", + "{sdesc}{sep}{num} ({recog}{key})".format( + num=inum + 1, + sep=_NUM_SEP, + sdesc=_RE_PREFIX.sub("", sdesc), + recog=caller.recog.get(obj) or "no recog", + key=f" ({caller.key})" if caller == obj else "", ) for inum, obj in enumerate(matches) ] @@ -1133,7 +1130,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room if forget_mode: # remove existing recog caller.recog.remove(obj) - caller.msg("%s will now know them only as '%s'." % (caller.key, obj.get_display_name(caller, noid=True))) + caller.msg("You will now know them only as '{}'.".format( obj.get_display_name(caller, noid=True) )) else: # set recog sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key @@ -1142,7 +1139,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room except RecogError as err: caller.msg(err) return - caller.msg("%s will now remember |w%s|n as |w%s|n." % (caller.key, sdesc, alias)) + caller.msg("You will now remember |w{}|n as |w{}|n.".format(sdesc, alias)) class CmdMask(RPCommand): @@ -1173,14 +1170,14 @@ class CmdMask(RPCommand): caller.msg("You are already wearing a mask.") return sdesc = _RE_CHAREND.sub("", self.args) - sdesc = "%s |H[masked]|n" % sdesc + sdesc = f"{sdesc} |H[masked]|n" if len(sdesc) > 60: caller.msg("Your masked sdesc is too long.") return caller.db.unmasked_sdesc = caller.sdesc.get() caller.locks.add("enable_recog:false()") caller.sdesc.add(sdesc) - caller.msg("You wear a mask as '%s'." % sdesc) + caller.msg(f"You wear a mask as '{sdesc}'.") else: # unmask old_sdesc = caller.db.unmasked_sdesc @@ -1190,7 +1187,7 @@ class CmdMask(RPCommand): del caller.db.unmasked_sdesc caller.locks.remove("enable_recog") caller.sdesc.add(old_sdesc) - caller.msg("You remove your mask and are again '%s'." % old_sdesc) + caller.msg(f"You remove your mask and are again '{old_sdesc}'.") class RPSystemCmdSet(CmdSet): @@ -1672,7 +1669,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): sdesc = sdesc.upper() elif "v" in ref: sdesc = sdesc.lower() - return "|b%s|n" % sdesc + return f"|b{sdesc}|n" def process_recog(self, recog, obj, **kwargs): """ @@ -1691,7 +1688,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): if not recog: return "" - return "|m%s|n" % recog + return f"|m{recog}|n" def process_language(self, text, speaker, language, **kwargs): """ @@ -1714,4 +1711,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): the evennia.contrib.rpg.rplanguage module. """ - return "%s|w%s|n" % ("|W(%s)" % language if language else "", text) + return "{label}|w{text}|n".format( + label=f"|W({language})" if language else "", + text=text + ) From 369eab1963872f249e0620a6299b2a1e801c7235 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:50:09 -0600 Subject: [PATCH 24/48] update docstring module paths --- evennia/contrib/rpg/rpsystem/rpsystem.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 069f1196a4..6520c6a9f1 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -53,7 +53,7 @@ Add `RPSystemCmdSet` from this module to your CharacterCmdSet: # ... -from evennia.contrib.rpg.rpsystem import RPSystemCmdSet <--- +from evennia.contrib.rpg.rpsystem.rpsystem import RPSystemCmdSet <--- class CharacterCmdSet(default_cmds.CharacterCmdset): # ... @@ -69,7 +69,7 @@ the typeclasses in this module: ```python # in mygame/typeclasses/characters.py -from evennia.contrib.rpg import ContribRPCharacter +from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPCharacter class Character(ContribRPCharacter): # ... @@ -79,7 +79,7 @@ class Character(ContribRPCharacter): ```python # in mygame/typeclasses/objects.py -from evennia.contrib.rpg import ContribRPObject +from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject class Object(ContribRPObject): # ... @@ -89,7 +89,7 @@ class Object(ContribRPObject): ```python # in mygame/typeclasses/rooms.py -from evennia.contrib.rpg import ContribRPRoom +from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPRoom class Room(ContribRPRoom): # ... @@ -125,7 +125,7 @@ Extra Installation Instructions: 1. In typeclasses/character.py: Import the `ContribRPCharacter` class: - `from evennia.contrib.rpg.rpsystem import ContribRPCharacter` + `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPCharacter` Inherit ContribRPCharacter: Change "class Character(DefaultCharacter):" to `class Character(ContribRPCharacter):` @@ -133,13 +133,13 @@ Extra Installation Instructions: Add `super().at_object_creation()` as the top line. 2. In `typeclasses/rooms.py`: Import the `ContribRPRoom` class: - `from evennia.contrib.rpg.rpsystem import ContribRPRoom` + `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPRoom` Inherit `ContribRPRoom`: Change `class Room(DefaultRoom):` to `class Room(ContribRPRoom):` 3. In `typeclasses/objects.py` Import the `ContribRPObject` class: - `from evennia.contrib.rpg.rpsystem import ContribRPObject` + `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject` Inherit `ContribRPObject`: Change `class Object(DefaultObject):` to `class Object(ContribRPObject):` From b3aa869ac68c3ffa4bd20d52be92846ab05a7210 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 29 Apr 2022 12:11:29 -0600 Subject: [PATCH 25/48] fixes --- evennia/contrib/rpg/rpsystem/rpsystem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 6520c6a9f1..1296da6f62 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -284,7 +284,7 @@ def parse_language(speaker, emote): # the key is simply the running match in the emote key = f"##{imatch}" # replace say with ref markers in emote - emote = "{start}{key}{end}".format( start=emote[:istart], key=key, end=emote[iend:] ) + emote = "{start}{{{key}}}{end}".format( start=emote[:istart], key=key, end=emote[iend:] ) mapping[key] = (langname, saytext) if errors: @@ -408,6 +408,8 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # save current index as end point of matched text iend = i + # save search string + matched_text = "".join(tail[1:iend]) # recombine remainder of emote back into a string tail = "".join(tail[iend+1:]) @@ -469,7 +471,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # multimatch error refname = marker_match.group() reflist = [ - "{name}{sep}{num} ({text}{key})".format( + "{num}{sep}{name} ({text}{key})".format( num=inum + 1, sep=_NUM_SEP, name=_RE_PREFIX.sub("", refname), @@ -581,7 +583,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): receiver_sdesc_mapping = dict( ( ref, - obj.get_display_name(receiver, noid=True), + obj.get_display_name(receiver, ref=ref, noid=True), ) for ref, obj in obj_mapping.items() ) @@ -1109,7 +1111,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc)) elif nmatches > 1: reflist = [ - "{sdesc}{sep}{num} ({recog}{key})".format( + "{num}{sep}{sdesc} ({recog}{key})".format( num=inum + 1, sep=_NUM_SEP, sdesc=_RE_PREFIX.sub("", sdesc), @@ -1461,7 +1463,7 @@ class ContribRPObject(DefaultObject): # add dbref is looker has control access and `noid` is not set if self.access(looker, access_type="control") and not kwargs.get("noid",False): - sdesc = f"{sdesc}{self.id}" + sdesc = f"{sdesc}(#{self.id})" return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc From deb7d9f69a8025f632c55f73af76c56ffe84ef34 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 29 Apr 2022 12:26:00 -0600 Subject: [PATCH 26/48] add test for `.get_sdesc` --- evennia/contrib/rpg/rpsystem/tests.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py index 2c45e9a8a0..45f5a669a4 100644 --- a/evennia/contrib/rpg/rpsystem/tests.py +++ b/evennia/contrib/rpg/rpsystem/tests.py @@ -164,6 +164,24 @@ class TestRPSystem(BaseEvenniaTest): result, ) + def test_get_sdesc(self): + looker = self.speaker # Sender + target = self.receiver1 # Receiver1 + looker.sdesc.add(sdesc0) # A nice sender of emotes + target.sdesc.add(sdesc1) # The first receiver of emotes. + + # sdesc with no processing + self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.") + # sdesc with processing + self.assertEqual(looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n") + + looker.recog.add(target, recog01) # Mr Receiver + + # recog with no processing + self.assertEqual(looker.get_sdesc(target), "Mr Receiver") + # recog with processing + self.assertEqual(looker.get_sdesc(target, process=True), "|mMr Receiver|n") + def test_send_emote(self): speaker = self.speaker receiver1 = self.receiver1 @@ -259,7 +277,7 @@ class TestRPSystemCommands(BaseEvenniaCommandTest): self.call( rpsystem.CmdRecog(), "barfoo as friend", - "Char will now remember BarFoo Character as friend.", + "You will now remember BarFoo Character as friend.", ) self.call( rpsystem.CmdRecog(), @@ -270,6 +288,6 @@ class TestRPSystemCommands(BaseEvenniaCommandTest): self.call( rpsystem.CmdRecog(), "friend", - "Char will now know them only as 'BarFoo Character'", + "You will now know them only as 'BarFoo Character'", cmdstring="forget", ) From a76ba1d92c24c14c800e8300177a67cbca3fc6cf Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 29 Apr 2022 12:33:27 -0600 Subject: [PATCH 27/48] add /Me vs /me to case sensitive test --- evennia/contrib/rpg/rpsystem/tests.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py index 45f5a669a4..f0d040d6f7 100644 --- a/evennia/contrib/rpg/rpsystem/tests.py +++ b/evennia/contrib/rpg/rpsystem/tests.py @@ -96,7 +96,7 @@ recog01 = "Mr Receiver" recog02 = "Mr Receiver2" recog10 = "Mr Sender" emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."' -case_emote = "/Me looks at /first, then /FIRST, /First and /Colliding twice." +case_emote = "/Me looks at /first. Then, /me looks at /FIRST, /First and /Colliding twice." class TestRPSystem(BaseEvenniaTest): @@ -225,20 +225,21 @@ class TestRPSystem(BaseEvenniaTest): rpsystem.send_emote(speaker, receivers, case_emote) self.assertEqual( self.out0, - "|mSender|n looks at |bthe first receiver of emotes.|n, then " - "|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and " - "|bAnother nice colliding sdesc-guy for tests|n twice.", + "|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n " + "looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n " + "and |bAnother nice colliding sdesc-guy for tests|n twice.", ) self.assertEqual( self.out1, - "|bA nice sender of emotes|n looks at |mReceiver1|n, then |mReceiver1|n, " - "|mReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.", + "|bA nice sender of emotes|n looks at |mReceiver1|n. Then, " + "|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n " + "and |bAnother nice colliding sdesc-guy for tests|n twice." ) self.assertEqual( self.out2, - "|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, " - "then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of " - "emotes.|n and |mReceiver2|n twice.", + "|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. " + "Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, " + "|bThe first receiver of emotes.|n and |mReceiver2|n twice.", ) def test_rpsearch(self): From bbee58c8541353fd3c06a44447874450432e5320 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 29 Apr 2022 12:43:17 -0600 Subject: [PATCH 28/48] replace % subs on regex strings --- evennia/contrib/rpg/rpsystem/rpsystem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 1296da6f62..c191d72d93 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -186,13 +186,13 @@ _EMOTE_MULTIMATCH_ERROR = """|RMultiple possibilities for {ref}: _RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE -_RE_PREFIX = re.compile(r"^%s" % _PREFIX, re.UNICODE) +_RE_PREFIX = re.compile(rf"^{_PREFIX}", re.UNICODE) # This regex will return groups (num, word), where num is an optional counter to # separate multimatches from one another and word is the first word in the # marker. So entering "/tall man" will return groups ("", "tall") # and "/2-tall man" will return groups ("2", "tall"). -_RE_OBJ_REF_START = re.compile(r"%s(?:([0-9]+)%s)*(\w+)" % (_PREFIX, _NUM_SEP), _RE_FLAGS) +_RE_OBJ_REF_START = re.compile(rf"{_PREFIX}(?:([0-9]+){_NUM_SEP})*(\w+)", _RE_FLAGS) _RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS) _RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS) @@ -350,7 +350,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ candidate_map.append((obj, obj.sdesc.get())) # if no sdesc, include key plus aliases instead else: - candidate_map += [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] + candidate_map.extend( [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] ) # escape mapping syntax on the form {#id} if it exists already in emote, # if so it is replaced with just "id". @@ -1568,7 +1568,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): # add dbref is looker has control access and `noid` is not set if self.access(looker, access_type="control") and not kwargs.get("noid",False): - sdesc = f"{sdesc}{self.id}" + sdesc = f"{sdesc}(#{self.id})" return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc From dd771fddd0512f6d8e134ac9358dfa73cbc29a79 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Wed, 4 May 2022 10:36:53 -0600 Subject: [PATCH 29/48] move newline to before indent --- evennia/commands/default/building.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9f0f6359cc..e8d3b6a5b6 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2732,7 +2732,7 @@ class CmdExamine(ObjManipCommand): return if ndb_attr and ndb_attr[0]: - return "\n " + " \n".join( + return "\n " + "\n ".join( sorted(self.format_single_attribute(attr) for attr in ndb_attr) ) From 684e80bca0890b71761a00eb11d6cdb8aec5783d Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 9 May 2022 16:20:34 -0600 Subject: [PATCH 30/48] rewrite color/style replacement --- evennia/utils/text2html.py | 340 ++++++++++++++++++------------------- 1 file changed, 170 insertions(+), 170 deletions(-) diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index be8f459c87..613cdad38a 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -12,11 +12,12 @@ import re from html import escape as html_escape from .ansi import * +from evennia.utils import logger # All xterm256 RGB equivalents -XTERM256_FG = "\033[38;5;%sm" -XTERM256_BG = "\033[48;5;%sm" +XTERM256_FG = "\033[38;5;{}m" +XTERM256_BG = "\033[48;5;{}m" class TextToHTMLparser(object): @@ -25,77 +26,55 @@ class TextToHTMLparser(object): """ tabstop = 4 - # mapping html color name <-> ansi code. - hilite = ANSI_HILITE - unhilite = ANSI_UNHILITE # this will be stripped - there is no css equivalent. - normal = ANSI_NORMAL # " - underline = ANSI_UNDERLINE - blink = ANSI_BLINK - inverse = ANSI_INVERSE # this will produce an outline; no obvious css equivalent? - colorcodes = [ - ("color-000", unhilite + ANSI_BLACK), # pure black - ("color-001", unhilite + ANSI_RED), - ("color-002", unhilite + ANSI_GREEN), - ("color-003", unhilite + ANSI_YELLOW), - ("color-004", unhilite + ANSI_BLUE), - ("color-005", unhilite + ANSI_MAGENTA), - ("color-006", unhilite + ANSI_CYAN), - ("color-007", unhilite + ANSI_WHITE), # light grey - ("color-008", hilite + ANSI_BLACK), # dark grey - ("color-009", hilite + ANSI_RED), - ("color-010", hilite + ANSI_GREEN), - ("color-011", hilite + ANSI_YELLOW), - ("color-012", hilite + ANSI_BLUE), - ("color-013", hilite + ANSI_MAGENTA), - ("color-014", hilite + ANSI_CYAN), - ("color-015", hilite + ANSI_WHITE), # pure white - ] + [("color-%03i" % (i + 16), XTERM256_FG % ("%i" % (i + 16))) for i in range(240)] - colorback = [ - ("bgcolor-000", ANSI_BACK_BLACK), # pure black - ("bgcolor-001", ANSI_BACK_RED), - ("bgcolor-002", ANSI_BACK_GREEN), - ("bgcolor-003", ANSI_BACK_YELLOW), - ("bgcolor-004", ANSI_BACK_BLUE), - ("bgcolor-005", ANSI_BACK_MAGENTA), - ("bgcolor-006", ANSI_BACK_CYAN), - ("bgcolor-007", ANSI_BACK_WHITE), # light grey - ("bgcolor-008", hilite + ANSI_BACK_BLACK), # dark grey - ("bgcolor-009", hilite + ANSI_BACK_RED), - ("bgcolor-010", hilite + ANSI_BACK_GREEN), - ("bgcolor-011", hilite + ANSI_BACK_YELLOW), - ("bgcolor-012", hilite + ANSI_BACK_BLUE), - ("bgcolor-013", hilite + ANSI_BACK_MAGENTA), - ("bgcolor-014", hilite + ANSI_BACK_CYAN), - ("bgcolor-015", hilite + ANSI_BACK_WHITE), # pure white - ] + [("bgcolor-%03i" % (i + 16), XTERM256_BG % ("%i" % (i + 16))) for i in range(240)] + style_codes = [ + # non-color style markers + ANSI_NORMAL, + ANSI_UNDERLINE, + ANSI_HILITE, + ANSI_UNHILITE, + ANSI_INVERSE, + ANSI_BLINK, + ANSI_INV_HILITE, + ANSI_BLINK_HILITE, + ANSI_INV_BLINK, + ANSI_INV_BLINK_HILITE, + ] + + ansi_color_codes = [ + # Foreground colors + ANSI_BLACK, + ANSI_RED, + ANSI_GREEN, + ANSI_YELLOW, + ANSI_BLUE, + ANSI_MAGENTA, + ANSI_CYAN, + ANSI_WHITE, + ] + + xterm_fg_codes = [ XTERM256_FG.format(i + 16) for i in range(240) ] - # make sure to escape [ - # colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes] - # colorback = [(c, code.replace("[", r"\[")) for c, code in colorback] - fg_colormap = dict((code, clr) for clr, code in colorcodes) - bg_colormap = dict((code, clr) for clr, code in colorback) + ansi_bg_codes = [ + # Background colors + ANSI_BACK_BLACK, + ANSI_BACK_RED, + ANSI_BACK_GREEN, + ANSI_BACK_YELLOW, + ANSI_BACK_BLUE, + ANSI_BACK_MAGENTA, + ANSI_BACK_CYAN, + ANSI_BACK_WHITE, + ] + + xterm_bg_codes = [ XTERM256_BG.format(i + 16) for i in range(240) ] + + re_style = re.compile(r"({})".format('|'.join(style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes).replace("[",r"\["))) - # create stop markers - fgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m|\033\[0m|$" - bgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m|\033\[0m|$" - bgfgstop = bgstop[:-2] + fgstop + colorlist = [ ANSI_UNHILITE + code for code in ansi_color_codes ] + [ ANSI_HILITE + code for code in ansi_color_codes ] + xterm_fg_codes - fgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m)" - bgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m)" - bgfgstart = bgstart + r"((?:\033\[1m|\033\[22m){0,1}\033\[[3-4][0-8].*?m){0,1}" + bglist = ansi_bg_codes + [ ANSI_HILITE + code for code in ansi_bg_codes ] + xterm_bg_codes - # extract color markers, tagging the start marker and the text marked - re_fgs = re.compile(fgstart + "(.*?)(?=" + fgstop + ")") - re_bgs = re.compile(bgstart + "(.*?)(?=" + bgstop + ")") - re_bgfg = re.compile(bgfgstart + "(.*?)(?=" + bgfgstop + ")") - - re_normal = re.compile(normal.replace("[", r"\[")) - re_hilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (hilite.replace("[", r"\["), fgstop, bgstop)) - re_unhilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (unhilite.replace("[", r"\["), fgstop, bgstop)) - re_uline = re.compile("(?:%s)(.*?)(?=%s|%s)" % (underline.replace("[", r"\["), fgstop, bgstop)) - re_blink = re.compile("(?:%s)(.*?)(?=%s|%s)" % (blink.replace("[", r"\["), fgstop, bgstop)) - re_inverse = re.compile("(?:%s)(.*?)(?=%s|%s)" % (inverse.replace("[", r"\["), fgstop, bgstop)) re_string = re.compile( r"(?P[<&>])|(?P[\t]+)|(?P\r\n|\r|\n)", re.S | re.M | re.I, @@ -106,100 +85,6 @@ class TextToHTMLparser(object): re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL) re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL) - def _sub_bgfg(self, colormatch): - # print("colormatch.groups()", colormatch.groups()) - bgcode, fgcode, text = colormatch.groups() - if not fgcode: - ret = r"""%s""" % ( - self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")), - text, - ) - else: - ret = r"""%s""" % ( - self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")), - self.fg_colormap.get(fgcode, self.bg_colormap.get(fgcode, "err")), - text, - ) - return ret - - def _sub_fg(self, colormatch): - code, text = colormatch.groups() - return r"""%s""" % (self.fg_colormap.get(code, "err"), text) - - def _sub_bg(self, colormatch): - code, text = colormatch.groups() - return r"""%s""" % (self.bg_colormap.get(code, "err"), text) - - def re_color(self, text): - """ - Replace ansi colors with html color class names. Let the - client choose how it will display colors, if it wishes to. - - Args: - text (str): the string with color to replace. - - Returns: - text (str): Re-colored text. - - """ - text = self.re_bgfg.sub(self._sub_bgfg, text) - text = self.re_fgs.sub(self._sub_fg, text) - text = self.re_bgs.sub(self._sub_bg, text) - text = self.re_normal.sub("", text) - return text - - def re_bold(self, text): - """ - Clean out superfluous hilights rather than set to make - it match the look of telnet. - - Args: - text (str): Text to process. - - Returns: - text (str): Processed text. - - """ - text = self.re_hilite.sub(r"\1", text) - return self.re_unhilite.sub(r"\1", text) # strip unhilite - there is no equivalent in css. - - def re_underline(self, text): - """ - Replace ansi underline with html underline class name. - - Args: - text (str): Text to process. - - Returns: - text (str): Processed text. - - """ - return self.re_uline.sub(r'\1', text) - - def re_blinking(self, text): - """ - Replace ansi blink with custom blink css class - - Args: - text (str): Text to process. - - Returns: - text (str): Processed text. - """ - return self.re_blink.sub(r'\1', text) - - def re_inversing(self, text): - """ - Replace ansi inverse with custom inverse css class - - Args: - text (str): Text to process. - - Returns: - text (str): Processed text. - """ - return self.re_inverse.sub(r'\1', text) - def remove_bells(self, text): """ Remove ansi specials @@ -211,7 +96,7 @@ class TextToHTMLparser(object): text (str): Processed text. """ - return text.replace("\07", "") + return text.replace(ANSI_BEEP, "") def remove_backspaces(self, text): """ @@ -292,7 +177,7 @@ class TextToHTMLparser(object): url=url, text=text ) return val - + def sub_text(self, match): """ Helper method to be passed to re.sub, @@ -314,6 +199,126 @@ class TextToHTMLparser(object): text = cdict["tab"].replace("\t", " " * (self.tabstop)) return text return None + + def format_styles(self, text): + """ + Takes a string with parsed ANSI codes and replaces them with + HTML spans and CSS classes. + + Args: + text (str): The string to process. + + Returns: + text (str): Processed text. + """ + + # split out the ANSI codes and clean out any empty items + str_list = [substr for substr in self.re_style.split(text) if substr] + # initialize all the flags and classes + classes = [] + clean = True + inverse = False + # default color is light grey - unhilite + white + hilight = ANSI_UNHILITE + fg = ANSI_WHITE + # default bg is black + bg = ANSI_BACK_BLACK + + for i, substr in enumerate(str_list): + # reset all current styling + if substr == ANSI_NORMAL and not clean: + # replace with close existing tag + str_list[i] = "" + # reset to defaults + classes = [] + clean = True + inverse = False + hilight = ANSI_UNHILITE + fg = ANSI_WHITE + bg = ANSI_BACK_BLACK + + # change color + elif substr in self.ansi_color_codes + self.xterm_fg_codes: + # erase ANSI code from output + str_list[i] = "" + # set new color + fg = substr + + # change bg color + elif substr in self.ansi_bg_codes + self.xterm_bg_codes: + # erase ANSI code from output + str_list[i] = "" + # set new bg + bg = substr + + # non-color codes + elif substr in self.style_codes: + # erase ANSI code from output + str_list[i] = "" + + # hilight codes + if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE): + # set new hilight status + hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE + + # inversion codes + if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE): + inverse = True + + # blink codes + if substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE) and "blink" not in classes: + classes.append("blink") + + # underline + if substr == ANSI_UNDERLINE and "underline" not in classes: + classes.append("underline") + + else: + # normal text, add text back to list + if not str_list[i-1]: + # prior entry was cleared, which means style change + # get indices for the fg and bg codes + bg_index = self.bglist.index(bg) + try: + color_index = self.colorlist.index(hilight + fg) + except ValueError: + # xterm256 colors don't have the hilight codes + color_index = self.colorlist.index(fg) + + if inverse: + # inverse means swap fg and bg indices + bg_class = "bgcolor-{}".format(str(color_index).rjust(3,"0")) + color_class = "color-{}".format(str(bg_index).rjust(3,"0")) + else: + # use fg and bg indices for classes + bg_class = "bgcolor-{}".format(str(bg_index).rjust(3,"0")) + color_class = "color-{}".format(str(color_index).rjust(3,"0")) + + # black bg is the default, don't explicitly style + if bg_class != "bgcolor-000": + classes.append(bg_class) + # light grey text is the default, don't explicitly style + if color_class != "color-007": + classes.append(color_class) + # define the new style span + prefix = ''.format(" ".join(classes)) + # close any prior span + if not clean: + prefix = '' + prefix + # add span to output + str_list[i-1] = prefix + + # clean out color classes to easily update next time + classes = [cls for cls in classes if "color" not in cls] + # flag as currently being styled + clean = False + + # close span if necessary + if not clean: + str_list.append("") + # recombine back into string + return "".join(str_list) + def parse(self, text, strip_ansi=False): """ @@ -328,19 +333,14 @@ class TextToHTMLparser(object): text (str): Parsed text. """ - # print(f"incoming text:\n{text}") # parse everything to ansi first text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True) # convert all ansi to html result = re.sub(self.re_string, self.sub_text, text) result = re.sub(self.re_mxplink, self.sub_mxp_links, result) result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result) - result = self.re_color(result) - result = self.re_bold(result) - result = self.re_underline(result) - result = self.re_blinking(result) - result = self.re_inversing(result) result = self.remove_bells(result) + result = self.format_styles(result) result = self.convert_linebreaks(result) result = self.remove_backspaces(result) result = self.convert_urls(result) From 6046f279eb6e0921d48ac17d5175f4ef3440d25e Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 9 May 2022 16:22:16 -0600 Subject: [PATCH 31/48] updat text2html tests --- evennia/utils/tests/test_text2html.py | 94 ++++++++++----------------- 1 file changed, 33 insertions(+), 61 deletions(-) diff --git a/evennia/utils/tests/test_text2html.py b/evennia/utils/tests/test_text2html.py index 3b67cd426e..361ab086cf 100644 --- a/evennia/utils/tests/test_text2html.py +++ b/evennia/utils/tests/test_text2html.py @@ -7,20 +7,20 @@ import mock class TestText2Html(TestCase): - def test_re_color(self): + def test_format_styles(self): parser = text2html.HTML_PARSER - self.assertEqual("foo", parser.re_color("foo")) + self.assertEqual("foo", parser.format_styles("foo")) self.assertEqual( 'redfoo', - parser.re_color(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"), + parser.format_styles(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"), ) self.assertEqual( 'redfoo', - parser.re_color(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"), + parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"), ) self.assertEqual( - 'redfoo', - parser.re_color( + 'redfoo', + parser.format_styles( ansi.ANSI_BACK_RED + ansi.ANSI_UNHILITE + ansi.ANSI_GREEN @@ -29,63 +29,37 @@ class TestText2Html(TestCase): + "foo" ), ) - - @unittest.skip("parser issues") - def test_re_bold(self): - parser = text2html.HTML_PARSER - self.assertEqual("foo", parser.re_bold("foo")) self.assertEqual( - # "a redfoo", # TODO: why not? - "a redfoo", - parser.re_bold("a " + ansi.ANSI_HILITE + "red" + ansi.ANSI_UNHILITE + "foo"), - ) - - @unittest.skip("parser issues") - def test_re_underline(self): - parser = text2html.HTML_PARSER - self.assertEqual("foo", parser.re_underline("foo")) - self.assertEqual( - 'a red' + ansi.ANSI_NORMAL + "foo", - parser.re_underline( + 'a redfoo', + parser.format_styles( "a " + ansi.ANSI_UNDERLINE + "red" - + ansi.ANSI_NORMAL # TODO: why does it keep it? + + ansi.ANSI_NORMAL + "foo" ), ) - - @unittest.skip("parser issues") - def test_re_blinking(self): - parser = text2html.HTML_PARSER - self.assertEqual("foo", parser.re_blinking("foo")) self.assertEqual( - 'a red' + ansi.ANSI_NORMAL + "foo", - parser.re_blinking( + 'a redfoo', + parser.format_styles( "a " + ansi.ANSI_BLINK + "red" - + ansi.ANSI_NORMAL # TODO: why does it keep it? + + ansi.ANSI_NORMAL + "foo" ), ) - - @unittest.skip("parser issues") - def test_re_inversing(self): - parser = text2html.HTML_PARSER - self.assertEqual("foo", parser.re_inversing("foo")) self.assertEqual( - 'a red' + ansi.ANSI_NORMAL + "foo", - parser.re_inversing( + 'a redfoo', + parser.format_styles( "a " + ansi.ANSI_INVERSE + "red" - + ansi.ANSI_NORMAL # TODO: why does it keep it? + + ansi.ANSI_NORMAL + "foo" ), ) - @unittest.skip("parser issues") def test_remove_bells(self): parser = text2html.HTML_PARSER self.assertEqual("foo", parser.remove_bells("foo")) @@ -95,7 +69,7 @@ class TestText2Html(TestCase): "a " + ansi.ANSI_BEEP + "red" - + ansi.ANSI_NORMAL # TODO: why does it keep it? + + ansi.ANSI_NORMAL + "foo" ), ) @@ -110,7 +84,6 @@ class TestText2Html(TestCase): self.assertEqual("foo", parser.convert_linebreaks("foo")) self.assertEqual("a
redfoo
", parser.convert_linebreaks("a\n redfoo\n")) - @unittest.skip("parser issues") def test_convert_urls(self): parser = text2html.HTML_PARSER self.assertEqual("foo", parser.convert_urls("foo")) @@ -118,7 +91,6 @@ class TestText2Html(TestCase): 'a http://redfoo runs', parser.convert_urls("a http://redfoo runs"), ) - # TODO: doesn't URL encode correctly def test_sub_mxp_links(self): parser = text2html.HTML_PARSER @@ -186,22 +158,22 @@ class TestText2Html(TestCase): self.assertEqual("foo", text2html.parse_html("foo")) self.maxDiff = None self.assertEqual( - # TODO: note that the blink is currently *not* correctly aborted - # with |n here! This is probably not possible to correctly handle - # with regex - a stateful parser may be needed. - # blink back-cyan normal underline red green yellow blue magenta cyan back-green text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"), - '' - 'Hello' # noqa - '' - 'W' # noqa - 'o' - 'r' - 'l' - 'd' - '!' - '!' # noqa - "" - "" - "", + '' + 'Hello' + '' + 'W' + '' + 'o' + '' + 'r' + '' + 'l' + '' + 'd' + '' + '!' + '' + '!' + '', ) From 4db12d15c057bb6e81bb6fbcc31bd6b141a0005d Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 9 May 2022 16:57:46 -0600 Subject: [PATCH 32/48] don't need logger anymore --- evennia/utils/text2html.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index 613cdad38a..af066fec68 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -12,8 +12,6 @@ import re from html import escape as html_escape from .ansi import * -from evennia.utils import logger - # All xterm256 RGB equivalents XTERM256_FG = "\033[38;5;{}m" From 97c0d7eb30ac3614d7ac553ec203ae2ccc3073c0 Mon Sep 17 00:00:00 2001 From: Owllex Date: Sun, 22 May 2022 00:22:12 -0700 Subject: [PATCH 33/48] Implement dict update operator (|) for savers. --- evennia/utils/dbserialize.py | 17 ++++++++++--- evennia/utils/tests/test_dbserialize.py | 33 ++++++++++++++----------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index ce6743bc5b..9e417c4e9f 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -239,6 +239,9 @@ class _SaverMutable(object): def __gt__(self, other): return self._data > other + def __or__(self, other): + return self._data | other + @_save def __setitem__(self, key, value): self._data.__setitem__(key, self._convert_mutables(value)) @@ -450,7 +453,9 @@ def deserialize(obj): elif tname in ("_SaverOrderedDict", "OrderedDict"): return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()]) elif tname in ("_SaverDefaultDict", "defaultdict"): - return defaultdict(obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()}) + return defaultdict( + obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()} + ) elif tname in _DESERIALIZE_MAPPING: return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj) elif is_iter(obj): @@ -612,7 +617,10 @@ def to_pickle(data): elif dtype in (dict, _SaverDict): return dict((process_item(key), process_item(val)) for key, val in item.items()) elif dtype in (defaultdict, _SaverDefaultDict): - return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items())) + return defaultdict( + item.default_factory, + ((process_item(key), process_item(val)) for key, val in item.items()), + ) elif dtype in (set, _SaverSet): return set(process_item(val) for val in item) elif dtype in (OrderedDict, _SaverOrderedDict): @@ -678,7 +686,10 @@ def from_pickle(data, db_obj=None): elif dtype == dict: return dict((process_item(key), process_item(val)) for key, val in item.items()) elif dtype == defaultdict: - return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items())) + return defaultdict( + item.default_factory, + ((process_item(key), process_item(val)) for key, val in item.items()), + ) elif dtype == set: return set(process_item(val) for val in item) elif dtype == OrderedDict: diff --git a/evennia/utils/tests/test_dbserialize.py b/evennia/utils/tests/test_dbserialize.py index 028d6d1f72..480893c466 100644 --- a/evennia/utils/tests/test_dbserialize.py +++ b/evennia/utils/tests/test_dbserialize.py @@ -62,10 +62,12 @@ class TestDbSerialize(TestCase): self.obj.db.test.sort(key=lambda d: str(d)) self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}]) - def test_dict(self): + def test_saverdict(self): self.obj.db.test = {"a": True} self.obj.db.test.update({"b": False}) self.assertEqual(self.obj.db.test, {"a": True, "b": False}) + self.obj.db.test |= {"c": 5} + self.assertEqual(self.obj.db.test, {"a": True, "b": False, "c": 5}) @parameterized.expand( [ @@ -88,27 +90,30 @@ class TestDbSerialize(TestCase): self.assertIsInstance(value, base_type) self.assertNotIsInstance(value, saver_type) self.assertEqual(value, default_value) - self.obj.db.test = {'a': True} - self.obj.db.test.update({'b': False}) - self.assertEqual(self.obj.db.test, {'a': True, 'b': False}) + self.obj.db.test = {"a": True} + self.obj.db.test.update({"b": False}) + self.assertEqual(self.obj.db.test, {"a": True, "b": False}) def test_defaultdict(self): from collections import defaultdict + # baseline behavior for a defaultdict _dd = defaultdict(list) - _dd['a'] - self.assertEqual(_dd, {'a': []}) + _dd["a"] + self.assertEqual(_dd, {"a": []}) # behavior after defaultdict is set as attribute dd = defaultdict(list) self.obj.db.test = dd - self.obj.db.test['a'] - self.assertEqual(self.obj.db.test, {'a': []}) + self.obj.db.test["a"] + self.assertEqual(self.obj.db.test, {"a": []}) - self.obj.db.test['a'].append(1) - self.assertEqual(self.obj.db.test, {'a': [1]}) - self.obj.db.test['a'].append(2) - self.assertEqual(self.obj.db.test, {'a': [1, 2]}) - self.obj.db.test['a'].append(3) - self.assertEqual(self.obj.db.test, {'a': [1, 2, 3]}) + self.obj.db.test["a"].append(1) + self.assertEqual(self.obj.db.test, {"a": [1]}) + self.obj.db.test["a"].append(2) + self.assertEqual(self.obj.db.test, {"a": [1, 2]}) + self.obj.db.test["a"].append(3) + self.assertEqual(self.obj.db.test, {"a": [1, 2, 3]}) + self.obj.db.test |= {"b": [5, 6]} + self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]}) From 96835fa445b6b7c2b137c025b1834845a8b2842e Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Wed, 25 May 2022 21:09:00 -0600 Subject: [PATCH 34/48] fix obsolete `.restart` calls --- evennia/contrib/base_systems/custom_gametime/custom_gametime.py | 2 +- evennia/utils/gametime.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/base_systems/custom_gametime/custom_gametime.py b/evennia/contrib/base_systems/custom_gametime/custom_gametime.py index 5611580187..33588deca9 100644 --- a/evennia/contrib/base_systems/custom_gametime/custom_gametime.py +++ b/evennia/contrib/base_systems/custom_gametime/custom_gametime.py @@ -328,4 +328,4 @@ class GametimeScript(DefaultScript): callback() seconds = real_seconds_until(**self.db.gametime) - self.restart(interval=seconds) + self.start(interval=seconds,force_restart=True) diff --git a/evennia/utils/gametime.py b/evennia/utils/gametime.py index 0e6358285c..ab14c77847 100644 --- a/evennia/utils/gametime.py +++ b/evennia/utils/gametime.py @@ -67,7 +67,7 @@ class TimeScript(DefaultScript): callback(*args, **kwargs) seconds = real_seconds_until(**self.db.gametime) - self.restart(interval=seconds) + self.start(interval=seconds,force_restart=True) # Access functions From 44a53e3750a2ea081aafeab5f3b73745502dc040 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Wed, 25 May 2022 21:10:53 -0600 Subject: [PATCH 35/48] start all global scripts --- evennia/utils/containers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index 85678ee03e..9a97e03ed0 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -167,7 +167,6 @@ class GlobalScriptContainer(Container): # store a hash representation of the setup script.attributes.add("_global_script_settings", compare_hash, category="settings_hash") - script.start() return script @@ -183,9 +182,12 @@ class GlobalScriptContainer(Container): # populate self.typeclass_storage self.load_data() - # start registered scripts + # make sure settings-defined scripts are loaded for key in self.loaded_data: self._load_script(key) + # start all global scripts + for script in self._get_scripts(): + script.start() def load_data(self): """ From a39472476e4b1b77e795b8a2f4a16f351428acfb Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 27 May 2022 10:49:46 +0200 Subject: [PATCH 36/48] Update Link page (sync from master pages) --- docs/source/Links.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/Links.md b/docs/source/Links.md index ed9cdb5714..514f225a92 100644 --- a/docs/source/Links.md +++ b/docs/source/Links.md @@ -129,10 +129,11 @@ Contains a very useful list of things to think about when starting your new MUD. Essential reading for the design of any persistent game world, written by the co-creator of the original game *MUD*. Published in 2003 but it's still as relevant now as when it came out. Covers everything you need to know and then some. -- Zed A. Shaw *Learn Python the Hard way* ([homepage](https://learnpythonthehardway.org/)) - Despite - the imposing name this book is for the absolute Python/programming beginner. One learns the language - by gradually creating a small text game! It has been used by multiple users before moving on to - Evennia. *Update: This used to be free to read online, this is no longer the case.* + + When the rights to Designing Virtual Worlds returned to him, Richard Bartle + made the PDF of his Designing Virtual Worlds freely available through his own + website ([Designing Virtual Worlds](https://mud.co.uk/dvw/)). A direct link to + the PDF can be found [here](https://mud.co.uk/richard/DesigningVirtualWorlds.pdf). - David M. Beazley *Python Essential Reference (4th ed)* ([amazon page](https://www.amazon.com/Python-Essential-Reference-David-Beazley/dp/0672329786/)) - Our recommended book on Python; it not only efficiently summarizes the language but is also From 796d12a9066a58dbb0e0e8760fce37ef1b05a86d Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 31 May 2022 12:08:16 -0400 Subject: [PATCH 37/48] Add failing test case about searching with none categories --- evennia/typeclasses/tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index aea6ce7518..90c4945898 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -142,6 +142,13 @@ class TestTypedObjectManager(BaseEvenniaTest): [self.obj1], ) + def test_get_tag_with_any_including_nones(self): + self.obj1.tags.add("tagA", "categoryA") + self.assertEqual( + self._manager("get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any"), + [self.obj1], + ) + def test_get_tag_withnomatch(self): self.obj1.tags.add("tagC", "categoryC") self.assertEqual( From f961db6e293c7f61a6955ad2a97b9bbf3258236d Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 31 May 2022 12:08:50 -0400 Subject: [PATCH 38/48] Remove sorted from unique categories as the order is not important --- evennia/typeclasses/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/typeclasses/managers.py b/evennia/typeclasses/managers.py index c8ad666e23..e5d1130f12 100644 --- a/evennia/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -286,7 +286,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): categories = make_iter(category) if category else [] n_keys = len(keys) n_categories = len(categories) - unique_categories = sorted(set(categories)) + unique_categories = set(categories) n_unique_categories = len(unique_categories) dbmodel = self.model.__dbclass__.__name__.lower() From a5c6a3ece7c2d2d27db4680b258920fac753f015 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Tue, 31 May 2022 22:59:11 -0600 Subject: [PATCH 39/48] update and simplify `options_formatter` --- evennia/utils/evmenu.py | 49 ++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index a3f34f47b2..4dae1b700d 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -274,12 +274,13 @@ import inspect from ast import literal_eval from fnmatch import fnmatch +from math import ceil from inspect import isfunction, getargspec from django.conf import settings from evennia import Command, CmdSet from evennia.utils import logger -from evennia.utils.evtable import EvTable +from evennia.utils.evtable import EvTable, EvColumn from evennia.utils.ansi import strip_ansi from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop from evennia.commands import cmdhandler @@ -1210,7 +1211,6 @@ class EvMenu: Args: optionlist (list): List of (key, description) tuples for every option related to this node. - caller (Object, Account or None, optional): The caller of the node. Returns: options (str): The formatted option display. @@ -1229,7 +1229,7 @@ class EvMenu: table = [] for key, desc in optionlist: if key or desc: - desc_string = ": %s" % desc if desc else "" + desc_string = f": {desc}" if desc else "" table_width_max = max( table_width_max, max(m_len(p) for p in key.split("\n")) @@ -1239,42 +1239,31 @@ class EvMenu: raw_key = strip_ansi(key) if raw_key != key: # already decorations in key definition - table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string)) + table.append(f" |lc{raw_key}|lt{key}|le{desc_string}") else: # add a default white color to key - table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string)) - ncols = _MAX_TEXT_WIDTH // table_width_max # number of ncols + table.append(f" |lc{raw_key}|lt|w{key}|n|le{desc_string}") + ncols = _MAX_TEXT_WIDTH // table_width_max # number of columns if ncols < 0: - # no visible option at all + # no visible options at all return "" - ncols = ncols + 1 if ncols == 0 else ncols - # get the amount of rows needed (start with 4 rows) - nrows = 4 - while nrows * ncols < nlist: - nrows += 1 - ncols = nlist // nrows # number of full columns - nlastcol = nlist % nrows # number of elements in last column + ncols = 1 if ncols == 0 else ncols - # get the final column count - ncols = ncols + 1 if nlastcol > 0 else ncols - if ncols > 1: - # only extend if longer than one column - table.extend([" " for i in range(nrows - nlastcol)]) + # minimum number of rows in a column + min_rows = 4 - # build the actual table grid - table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)] + # split the items into columns + split = max(min_rows, ceil(len(table)/ncols)) + max_end = len(table) + cols_list = [] + for icol in range(ncols): + start = icol*split + end = min(start+split,max_end) + cols_list.append(EvColumn(*table[start:end])) - # adjust the width of each column - for icol in range(len(table)): - col_width = ( - max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep - ) - table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]] - - # format the table into columns - return str(EvTable(table=table, border="none")) + return str(EvTable(table=cols_list, border="none")) def node_formatter(self, nodetext, optionstext): """ From 232d80041ae021716ac281b97392022d812df1fb Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:48:18 -0600 Subject: [PATCH 40/48] check if cmdid has callback --- evennia/web/static/webclient/js/evennia.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/web/static/webclient/js/evennia.js b/evennia/web/static/webclient/js/evennia.js index fb740c966e..51da36c468 100644 --- a/evennia/web/static/webclient/js/evennia.js +++ b/evennia/web/static/webclient/js/evennia.js @@ -149,7 +149,7 @@ An "emitter" object must have a function // kwargs (obj): keyword-args to listener // emit: function (cmdname, args, kwargs) { - if (kwargs.cmdid) { + if (kwargs.cmdid && (kwargs.cmdid in cmdmap)) { cmdmap[kwargs.cmdid].apply(this, [args, kwargs]); delete cmdmap[kwargs.cmdid]; } From 2269a9b1ef28a6ffa636312235f62b5c40f442a9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 1 Jun 2022 22:04:54 +0200 Subject: [PATCH 41/48] Add custom de/serializer methods for embedded dbobjs in Attribute pickling --- evennia/utils/dbserialize.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index ce6743bc5b..e18eee9d18 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -602,7 +602,9 @@ def to_pickle(data): def process_item(item): """Recursive processor and identification of data""" + dtype = type(item) + if dtype in (str, int, float, bool, bytes, SafeString): return item elif dtype == tuple: @@ -620,7 +622,20 @@ def to_pickle(data): elif dtype in (deque, _SaverDeque): return deque(process_item(val) for val in item) - elif hasattr(item, "__iter__"): + # not one of the base types + if hasattr(item, "__serialize_dbobjs__"): + # Allows custom serialization of any dbobjects embedded in + # the item that Evennia will otherwise not found (these would + # otherwise lead to an error). Use the dbserialize helper from + # this method. + try: + item.__serialize_dbobjs__() + except TypeError: + # we catch typerrors so we can handle both classes (requiring + # classmethods) and instances + pass + + if hasattr(item, "__iter__"): # we try to conserve the iterable class, if not convert to list try: return item.__class__([process_item(val) for val in item]) @@ -692,6 +707,18 @@ def from_pickle(data, db_obj=None): return item.__class__(process_item(val) for val in item) except (AttributeError, TypeError): return [process_item(val) for val in item] + + if hasattr(item, "__deserialize_dbobjs__"): + # this allows the object to custom-deserialize any embedded dbobjs + # that we previously serialized with __serialize_dbobjs__. + # use the dbunserialize helper in this module. + try: + item.__deserialize_dbobjs__() + except TypeError: + # handle recoveries both of classes (requiring classmethods + # or instances + pass + return item def process_tree(item, parent): From d9cd9e59f3bad4d6facaf664f2916ebc18de3e75 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 1 Jun 2022 22:08:37 +0200 Subject: [PATCH 42/48] Update changelog with pickle improvement; update Attribute docs --- CHANGELOG.md | 2 + docs/source/Components/Attributes.md | 235 ++++++++++++++++----------- 2 files changed, 142 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e2870179..192b0d58e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,6 +162,8 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10 way to override features on all ObjectDB-inheriting objects easily. - Add `TagProperty`, `AliasProperty` and `PermissionProperty` to assign these data in a similar way to django fields. +- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` + to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute. ## Evennia 0.9.5 diff --git a/docs/source/Components/Attributes.md b/docs/source/Components/Attributes.md index 18291ff774..978fcdf39d 100644 --- a/docs/source/Components/Attributes.md +++ b/docs/source/Components/Attributes.md @@ -3,7 +3,7 @@ ```{code-block} :caption: In-game > set obj/myattr = "test" -``` +``` ```{code-block} python :caption: In-code, using the .db wrapper obj.db.foo = [1, 2, 3, "bar"] @@ -16,8 +16,8 @@ value = attributes.get("myattr", category="bar") ``` ```{code-block} python :caption: In-code, using `AttributeProperty` at class level -from evennia import DefaultObject -from evennia import AttributeProperty +from evennia import DefaultObject +from evennia import AttributeProperty class MyObject(DefaultObject): foo = AttributeProperty(default=[1, 2, 3, "bar"]) @@ -25,20 +25,20 @@ class MyObject(DefaultObject): ``` -_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any -Python data structure and data type, like numbers, strings, lists, dicts etc. You can also +_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any +Python data structure and data type, like numbers, strings, lists, dicts etc. You can also store (references to) database objects like characters and rooms. - [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read to avoid being surprised, also for experienced developers. Attributes can store _almost_ everything but you need to know the quirks. -- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent +- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent siblings of Attributes. - [Managing Attributes In-game](#managing-attributes-in-game) for in-game builder commands. -## Managing Attributes in Code +## Managing Attributes in Code -Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities -([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and +Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities +([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and [Channels](./Channels.md)) can (and usually do) have Attributes associated with them. There are three ways to manage Attributes, all of which can be mixed. @@ -50,8 +50,8 @@ are three ways to manage Attributes, all of which can be mixed. The simplest way to get/set Attributes is to use the `.db` shortcut. This allows for setting and getting Attributes that lack a _category_ (having category `None`) -```python -import evennia +```python +import evennia obj = evennia.create_object(key="Foo") @@ -64,10 +64,10 @@ obj.db.self_reference = obj # stores a reference to the obj rose = evennia.search_object(key="rose")[0] # returns a list, grab 0th element rose.db.has_thorns = True -# retrieving +# retrieving val1 = obj.db.foo1 val2 = obj.db.foo2 -weap = obj.db.weapon +weap = obj.db.weapon myself = obj.db.self_reference # retrieve reference from db, get object back is_ouch = rose.db.has_thorns @@ -75,25 +75,25 @@ is_ouch = rose.db.has_thorns # this will return None, not AttributeError! not_found = obj.db.jiwjpowiwwerw -# returns all Attributes on the object -obj.db.all +# returns all Attributes on the object +obj.db.all # delete an Attribute del obj.db.foo2 ``` -Trying to access a non-existing Attribute will never lead to an `AttributeError`. Instead -you will get `None` back. The special `.db.all` will return a list of all Attributes on -the object. You can replace this with your own Attribute `all` if you want, it will replace the +Trying to access a non-existing Attribute will never lead to an `AttributeError`. Instead +you will get `None` back. The special `.db.all` will return a list of all Attributes on +the object. You can replace this with your own Attribute `all` if you want, it will replace the default `all` functionality until you delete it again. ### Using .attributes -If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of -the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally): +If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of +the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally): + +```python +is_ouch = rose.attributes.get("has_thorns") -```python -is_ouch = rose.attributes.get("has_thorns") - obj.attributes.add("helmet", "Knight's helmet") helmet = obj.attributes.get("helmet") @@ -103,7 +103,7 @@ obj.attributes.add("my game log", "long text about ...") By using a category you can separate same-named Attributes on the same object to help organization. -```python +```python # store (let's say we have gold_necklace and ringmail_armor from before) obj.attributes.add("neck", gold_necklace, category="clothing") obj.attributes.add("neck", ringmail_armor, category="armor") @@ -113,19 +113,19 @@ neck_clothing = obj.attributes.get("neck", category="clothing") neck_armor = obj.attributes.get("neck", category="armor") ``` -If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories. +If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories. -Here are the methods of the `AttributeHandler`. See +Here are the methods of the `AttributeHandler`. See the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for more details. - `has(...)` - this checks if the object has an Attribute with this key. This is equivalent to doing `obj.db.attrname` except you can also check for a specific `category. -- `get(...)` - this retrieves the given Attribute. You can also provide a `default` value to return +- `get(...)` - this retrieves the given Attribute. You can also provide a `default` value to return if the Attribute is not defined (instead of None). By supplying an `accessing_object` to the call one can also make sure to check permissions before modifying - anything. The `raise_exception` kwarg allows you to raise an `AttributeError` instead of returning - `None` when you access a non-existing `Attribute`. The `strattr` kwarg tells the system to store - the Attribute as a raw string rather than to pickle it. While an optimization this should usually + anything. The `raise_exception` kwarg allows you to raise an `AttributeError` instead of returning + `None` when you access a non-existing `Attribute`. The `strattr` kwarg tells the system to store + the Attribute as a raw string rather than to pickle it. While an optimization this should usually not be used unless the Attribute is used for some particular, limited purpose. - `add(...)` - this adds a new Attribute to the object. An optional [lockstring](./Locks.md) can be supplied here to restrict future access and also the call itself may be checked against locks. @@ -135,30 +135,30 @@ the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for Examples: -```python +```python try: - # raise error if Attribute foo does not exist + # raise error if Attribute foo does not exist val = obj.attributes.get("foo", raise_exception=True): except AttributeError: # ... - + # return default value if foo2 doesn't exist -val2 = obj.attributes.get("foo2", default=[1, 2, 3, "bar"]) +val2 = obj.attributes.get("foo2", default=[1, 2, 3, "bar"]) # delete foo if it exists (will silently fail if unset, unless # raise_exception is set) obj.attributes.remove("foo") - + # view all clothes on obj -all_clothes = obj.attributes.all(category="clothes") +all_clothes = obj.attributes.all(category="clothes") ``` -### Using AttributeProperty +### Using AttributeProperty -The third way to set up an Attribute is to use an `AttributeProperty`. This +The third way to set up an Attribute is to use an `AttributeProperty`. This is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django database Fields. Unlike using `.db` and `.attributes`, an `AttributeProperty` can't be created on the fly, you must assign it in the class code. -```python +```python # mygame/typeclasses/characters.py from evennia import DefaultCharacter @@ -173,16 +173,16 @@ class Character(DefaultCharacter): sleepy = AttributeProperty(False, autocreate=False) poisoned = AttributeProperty(False, autocreate=False) - - def at_object_creation(self): - # ... -``` + + def at_object_creation(self): + # ... +``` When a new instance of the class is created, new `Attributes` will be created with the value and category given. -With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object: +With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object: -```python +```python char = create_object(Character) char.strength # returns 10 @@ -195,15 +195,15 @@ char.db.sleepy # returns None because autocreate=False (see below) ``` -```{warning} +```{warning} Be careful to not assign AttributeProperty's to names of properties and methods already existing on the class, like 'key' or 'at_object_creation'. That could lead to very confusing errors. ``` -The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set. -The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default. -The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database): +The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set. +The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default. +The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database): -```python +```python char.sleepy # returns False, no db access char.db.sleepy # returns None - no Attribute exists @@ -217,39 +217,39 @@ char.sleepy # now returns True, involves db access ``` -You can e.g. `del char.strength` to set the value back to the default (the value defined -in the `AttributeProperty`). +You can e.g. `del char.strength` to set the value back to the default (the value defined +in the `AttributeProperty`). See the [AttributeProperty API](evennia.typeclasses.attributes.AttributeProperty) for more details on how to create it with special options, like giving access-restrictions. ## Managing Attributes in-game -Attributes are mainly used by code. But one can also allow the builder to use Attributes to -'turn knobs' in-game. For example a builder could want to manually tweak the "level" Attribute of an +Attributes are mainly used by code. But one can also allow the builder to use Attributes to +'turn knobs' in-game. For example a builder could want to manually tweak the "level" Attribute of an enemy NPC to lower its difficuly. -When setting Attributes this way, you are severely limited in what can be stored - this is because +When setting Attributes this way, you are severely limited in what can be stored - this is because giving players (even builders) the ability to store arbitrary Python would be a severe security -problem. +problem. -In game you can set an Attribute like this: +In game you can set an Attribute like this: set myobj/foo = "bar" -To view, do +To view, do - set myobj/foo + set myobj/foo -or see them together with all object-info with +or see them together with all object-info with examine myobj -The first `set`-example will store a new Attribute `foo` on the object `myobj` and give it the +The first `set`-example will store a new Attribute `foo` on the object `myobj` and give it the value "bar". -You can store numbers, booleans, strings, tuples, lists and dicts this way. But if +You can store numbers, booleans, strings, tuples, lists and dicts this way. But if you store a list/tuple/dict they must be proper Python structures and may _only_ contain strings -or numbers. If you try to insert an unsupported structure, the input will be converted to a +or numbers. If you try to insert an unsupported structure, the input will be converted to a string. set myobj/mybool = True @@ -263,8 +263,8 @@ For the last line you'll get a warning and the value instead will be saved as a ## Locking and checking Attributes -While the `set` command is limited to builders, individual Attributes are usually not -locked down. You may want to lock certain sensitive Attributes, in particular for games +While the `set` command is limited to builders, individual Attributes are usually not +locked down. You may want to lock certain sensitive Attributes, in particular for games where you allow player building. You can add such limitations by adding a [lock string](./Locks.md) to your Attribute. A NAttribute have no locks. @@ -273,7 +273,7 @@ The relevant lock types are - `attrread` - limits who may read the value of the Attribute - `attredit` - limits who may set/change this Attribute -You must use the `AttributeHandler` to assign the lockstring to the Attribute: +You must use the `AttributeHandler` to assign the lockstring to the Attribute: ```python lockstring = "attread:all();attredit:perm(Admins)" @@ -281,7 +281,7 @@ obj.attributes.add("myattr", "bar", lockstring=lockstring)" ``` If you already have an Attribute and want to add a lock in-place you can do so -by having the `AttributeHandler` return the `Attribute` object itself (rather than +by having the `AttributeHandler` return the `Attribute` object itself (rather than its value) and then assign the lock to it directly: ```python @@ -293,8 +293,8 @@ Note the `return_obj` keyword which makes sure to return the `Attribute` object could be accessed. A lock is no good if nothing checks it -- and by default Evennia does not check locks on Attributes. -To check the `lockstring` you provided, make sure you include `accessing_obj` and set -`default_access=False` as you make a `get` call. +To check the `lockstring` you provided, make sure you include `accessing_obj` and set +`default_access=False` as you make a `get` call. ```python # in some command code where we want to limit @@ -328,13 +328,13 @@ values into a string representation before storing it to the database. This is d With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class instances without the `__iter__` method. -* You can generally store any non-iterable Python entity that can be pickled. -* Single database objects/typeclasses can be stored, despite them normally not being possible - to pickle. Evennia wil convert them to an internal representation using their classname, - database-id and creation-date with a microsecond precision. When retrieving, the object +* You can generally store any non-iterable Python entity that can be _pickled_. +* Single database objects/typeclasses can be stored, despite them normally not being possible + to pickle. Evennia will convert them to an internal representation using theihr classname, + database-id and creation-date with a microsecond precision. When retrieving, the object instance will be re-fetched from the database using this information. -* To convert the database object, Evennia must know it's there. If you *hide* a database object - inside a non-iterable class, you will run into errors - this is not supported! +* If you 'hide' a db-obj as a property on a custom class, Evennia will not be + able to find it to serialize it. For that you need to help it out (see below). ```{code-block} python :caption: Valid assignments @@ -345,16 +345,55 @@ obj.db.test1 = False # a database object (will be stored as an internal representation) obj.db.test2 = myobj ``` + +As mentioned, Evennia will not be able to automatically serialize db-objects +'hidden' in arbitrary properties on an object. This will lead to an error +when saving the Attribute. + ```{code-block} python :caption: Invalid, 'hidden' dbobject - -# example of an invalid, "hidden" dbobject +# example of storing an invalid, "hidden" dbobject in Attribute class Container: def __init__(self, mydbobj): # no way for Evennia to know this is a database object! self.mydbobj = mydbobj + +# let's assume myobj is a db-object container = Container(myobj) -obj.db.invalid = container # will cause error! +obj.db.mydata = container # will raise error! + +``` + +By adding two methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to the +object you want to save, you can pre-serialize and post-deserialize all 'hidden' +objects before Evennia's main serializer gets to work. Inside these methods, use Evennia's +[evennia.utils.dbserialize.dbserialize](api:evennia.utils.dbserialize.dbserialize) and +[dbunserialize](api:evennia.utils.dbserialize.dbunserialize) functions to safely +serialize the db-objects you want to store. + +```{code-block} python +:caption: Fixing an invalid 'hidden' dbobj for storing in Attribute + +from evennia.utils import dbserialize # important + +class Container: + def __init__(self, mydbobj): + # A 'hidden' db-object + self.mydbobj = mydbobj + + def __serialize_dbobjs__(self): + """This is called before serialization and allows + us to custom-handle those 'hidden' dbobjs""" + self.mydbobj = dbserialize.dbserialize(self.mydbobj + + def __deserialize_dbobjs__(self): + """This is called after deserialization and allows you to + restore the 'hidden' dbobjs you serialized before""" + self.mydbobj = dbserialize.dbunserialize(self.mydbobj) + +# let's assume myobj is a db-object +container = Container(myobj) +obj.db.mydata = container # will now work fine! ``` ### Storing multiple objects @@ -404,6 +443,12 @@ obj.db.test8[2]["test"] = 5 # test8 is now [4,2,{"test":5}] ``` +Note that if make some advanced iterable object, and store an db-object on it in +a way such that it is _not_ returned by iterating over it, you have created a +'hidden' db-object. See [the previous section](#storing-single-objects) for how +to tell Evennia how to serialize such hidden objects safely. + + ### Retrieving Mutable objects A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can @@ -429,41 +474,41 @@ print(obj.db.mylist) # now also [1, 2, 3, 5] ``` When you extract your mutable Attribute data into a variable like `mylist`, think of it as getting a _snapshot_ -of the variable. If you update the snapshot, it will save to the database, but this change _will not propagate to +of the variable. If you update the snapshot, it will save to the database, but this change _will not propagate to any other snapshots you may have done previously_. -```python +```python obj.db.mylist = [1, 2, 3, 4] -mylist1 = obj.db.mylist -mylist2 = obj.db.mylist -mylist1[3] = 5 +mylist1 = obj.db.mylist +mylist2 = obj.db.mylist +mylist1[3] = 5 print(mylist1) # this is now [1, 2, 3, 5] -print(obj.db.mylist) # also updated to [1, 2, 3, 5] +print(obj.db.mylist) # also updated to [1, 2, 3, 5] -print(mylist2) # still [1, 2, 3, 4] ! +print(mylist2) # still [1, 2, 3, 4] ! ``` ```{sidebar} Remember, the complexities of this section only relate to *mutable* iterables - things you can update -in-place, like lists and dicts. [Immutable](https://en.wikipedia.org/wiki/Immutable) objects (strings, +in-place, like lists and dicts. [Immutable](https://en.wikipedia.org/wiki/Immutable) objects (strings, numbers, tuples etc) are already disconnected from the database from the onset. ``` -To avoid confusion with mutable Attributes, only work with one variable (snapshot) at a time and save -back the results as needed. +To avoid confusion with mutable Attributes, only work with one variable (snapshot) at a time and save +back the results as needed. You can also choose to "disconnect" the Attribute entirely from the database with the help of the `.deserialize()` method: ```python obj.db.mylist = [1, 2, 3, 4, {1: 2}] -mylist = obj.db.mylist.deserialize() +mylist = obj.db.mylist.deserialize() ``` The result of this operation will be a structure only consisting of normal Python mutables (`list` -instead of `_SaverList`, `dict` instead of `_SaverDict` and so on). If you update it, you need to +instead of `_SaverList`, `dict` instead of `_SaverDict` and so on). If you update it, you need to explicitly save it back to the Attribute for it to save. ## Properties of Attributes @@ -518,7 +563,7 @@ are **non-persistent** - they will _not_ survive a server reload. Differences between `Attributes` and `NAttributes`: - `NAttribute`s are always wiped on a server reload. -- They only exist in memory and never involve the database at all, making them faster to +- They only exist in memory and never involve the database at all, making them faster to access and edit than `Attribute`s. - `NAttribute`s can store _any_ Python structure (and database object) without limit. - They can _not_ be set with the standard `set` command (but they are visible with `examine`) @@ -526,10 +571,10 @@ Differences between `Attributes` and `NAttributes`: There are some important reasons we recommend using `ndb` to store temporary data rather than the simple alternative of just storing a variable directly on an object: -- NAttributes are tracked by Evennia and will not be purged in various cache-cleanup operations - the server may do. So using them guarantees that they'll remain available at least as long as +- NAttributes are tracked by Evennia and will not be purged in various cache-cleanup operations + the server may do. So using them guarantees that they'll remain available at least as long as the server lives. -- It's a consistent style - `.db/.attributes` and `.ndb/.nattributes` makes for clean-looking code +- It's a consistent style - `.db/.attributes` and `.ndb/.nattributes` makes for clean-looking code where it's clear how long-lived (or not) your data is to be. ### Persistent vs non-persistent @@ -557,4 +602,4 @@ useful in a few situations though. - `NAttribute`s have no restrictions at all on what they can store, since they don't need to worry about being saved to the database - they work very well for temporary storage. - You want to implement a fully or partly *non-persistent world*. Who are we to argue with your - grand vision! \ No newline at end of file + grand vision! From c39845c43b9915414351b68d13872d009c90a8ae Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 2 Jun 2022 00:09:51 +0200 Subject: [PATCH 43/48] Fix doc build for no-db case --- docs/Makefile | 2 ++ evennia/utils/containers.py | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index eed5098f2f..f7d7feae4b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -152,6 +152,8 @@ mv-local: @echo "Documentation built (multiversion + autodocs)." @echo "To see result, open evennia/docs/build/html//index.html in a browser." +# note - don't run the following manually, the result will clash with the result +# of the github actions! deploy: make _multiversion-deploy @echo "Documentation deployed." diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index 9a97e03ed0..ae3236a2b3 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -12,6 +12,7 @@ evennia.OPTION_CLASSES from pickle import dumps +from django.db.utils import OperationalError, ProgrammingError from django.conf import settings from evennia.utils.utils import class_from_module, callables_from_module from evennia.utils import logger @@ -186,8 +187,12 @@ class GlobalScriptContainer(Container): for key in self.loaded_data: self._load_script(key) # start all global scripts - for script in self._get_scripts(): - script.start() + try: + for script in self._get_scripts(): + script.start() + except (OperationalError, ProgrammingError): + # this can happen if db is not loaded yet (such as when building docs) + pass def load_data(self): """ From 8f0329fbaf10c939754a8e3af4953f9361e7c04a Mon Sep 17 00:00:00 2001 From: Bruno Briante Date: Wed, 1 Jun 2022 22:29:12 -0300 Subject: [PATCH 44/48] fix google style URL on CODING_STYLE.md --- CODING_STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index 3bc2dd346f..800c1e463d 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -193,7 +193,7 @@ or in the chat. [pep8]: http://www.python.org/dev/peps/pep-0008 [pep8tool]: https://pypi.python.org/pypi/pep8 -[googlestyle]: http://www.sphinx-doc.org/en/stable/ext/example_google.html +[googlestyle]: https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html [githubmarkdown]: https://help.github.com/articles/github-flavored-markdown/ [markdown-hilight]: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting [command-docstrings]: https://github.com/evennia/evennia/wiki/Using%20MUX%20As%20a%20Standard#documentation-policy From 379f2b63ab3abe2a95119dba0cbc071c98541edb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 4 Jun 2022 13:03:42 +0200 Subject: [PATCH 45/48] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 192b0d58e2..15f727f0c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,6 +164,8 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10 data in a similar way to django fields. - The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute. +- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will + now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal) ## Evennia 0.9.5 From cd503cd9fd2d24a68117fdfd9893d327ffa81de7 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 4 Jun 2022 13:14:37 +0200 Subject: [PATCH 46/48] Ran black on text2html file for PEP8 cleanup --- evennia/utils/text2html.py | 64 ++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index af066fec68..ab3f2930d6 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -38,7 +38,7 @@ class TextToHTMLparser(object): ANSI_INV_BLINK, ANSI_INV_BLINK_HILITE, ] - + ansi_color_codes = [ # Foreground colors ANSI_BLACK, @@ -50,8 +50,8 @@ class TextToHTMLparser(object): ANSI_CYAN, ANSI_WHITE, ] - - xterm_fg_codes = [ XTERM256_FG.format(i + 16) for i in range(240) ] + + xterm_fg_codes = [XTERM256_FG.format(i + 16) for i in range(240)] ansi_bg_codes = [ # Background colors @@ -64,14 +64,24 @@ class TextToHTMLparser(object): ANSI_BACK_CYAN, ANSI_BACK_WHITE, ] - - xterm_bg_codes = [ XTERM256_BG.format(i + 16) for i in range(240) ] - - re_style = re.compile(r"({})".format('|'.join(style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes).replace("[",r"\["))) - colorlist = [ ANSI_UNHILITE + code for code in ansi_color_codes ] + [ ANSI_HILITE + code for code in ansi_color_codes ] + xterm_fg_codes + xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)] - bglist = ansi_bg_codes + [ ANSI_HILITE + code for code in ansi_bg_codes ] + xterm_bg_codes + re_style = re.compile( + r"({})".format( + "|".join( + style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes + ).replace("[", r"\[") + ) + ) + + colorlist = ( + [ANSI_UNHILITE + code for code in ansi_color_codes] + + [ANSI_HILITE + code for code in ansi_color_codes] + + xterm_fg_codes + ) + + bglist = ansi_bg_codes + [ANSI_HILITE + code for code in ansi_bg_codes] + xterm_bg_codes re_string = re.compile( r"(?P[<&>])|(?P[\t]+)|(?P\r\n|\r|\n)", @@ -175,7 +185,7 @@ class TextToHTMLparser(object): url=url, text=text ) return val - + def sub_text(self, match): """ Helper method to be passed to re.sub, @@ -197,15 +207,15 @@ class TextToHTMLparser(object): text = cdict["tab"].replace("\t", " " * (self.tabstop)) return text return None - + def format_styles(self, text): """ Takes a string with parsed ANSI codes and replaces them with HTML spans and CSS classes. - + Args: text (str): The string to process. - + Returns: text (str): Processed text. """ @@ -221,7 +231,7 @@ class TextToHTMLparser(object): fg = ANSI_WHITE # default bg is black bg = ANSI_BACK_BLACK - + for i, substr in enumerate(str_list): # reset all current styling if substr == ANSI_NORMAL and not clean: @@ -258,13 +268,16 @@ class TextToHTMLparser(object): if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE): # set new hilight status hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE - + # inversion codes if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE): inverse = True - + # blink codes - if substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE) and "blink" not in classes: + if ( + substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE) + and "blink" not in classes + ): classes.append("blink") # underline @@ -273,7 +286,7 @@ class TextToHTMLparser(object): else: # normal text, add text back to list - if not str_list[i-1]: + if not str_list[i - 1]: # prior entry was cleared, which means style change # get indices for the fg and bg codes bg_index = self.bglist.index(bg) @@ -285,12 +298,12 @@ class TextToHTMLparser(object): if inverse: # inverse means swap fg and bg indices - bg_class = "bgcolor-{}".format(str(color_index).rjust(3,"0")) - color_class = "color-{}".format(str(bg_index).rjust(3,"0")) + bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0")) + color_class = "color-{}".format(str(bg_index).rjust(3, "0")) else: # use fg and bg indices for classes - bg_class = "bgcolor-{}".format(str(bg_index).rjust(3,"0")) - color_class = "color-{}".format(str(color_index).rjust(3,"0")) + bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0")) + color_class = "color-{}".format(str(color_index).rjust(3, "0")) # black bg is the default, don't explicitly style if bg_class != "bgcolor-000": @@ -302,10 +315,10 @@ class TextToHTMLparser(object): prefix = ''.format(" ".join(classes)) # close any prior span if not clean: - prefix = '' + prefix + prefix = "" + prefix # add span to output - str_list[i-1] = prefix - + str_list[i - 1] = prefix + # clean out color classes to easily update next time classes = [cls for cls in classes if "color" not in cls] # flag as currently being styled @@ -316,7 +329,6 @@ class TextToHTMLparser(object): str_list.append("") # recombine back into string return "".join(str_list) - def parse(self, text, strip_ansi=False): """ From e752db07d29ac404af1fea94096f3dcb75f309c9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 4 Jun 2022 13:15:44 +0200 Subject: [PATCH 47/48] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f727f0c5..e44c634c5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,6 +166,7 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10 to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute. - Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal) +- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal) ## Evennia 0.9.5 From f8e29f6f109db58d2ce77700088833eb6cf1a3ae Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 4 Jun 2022 13:24:06 +0200 Subject: [PATCH 48/48] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e44c634c5f..be0527668a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,7 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10 - Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal) - Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal) +- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal) ## Evennia 0.9.5