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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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 From f47beb675154c75378b8b96998e8d9eacbc79e55 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Mon, 6 Jun 2022 16:23:36 -0400 Subject: [PATCH 49/77] Changed parser to escape properly using alternating " or ' --- evennia/utils/funcparser.py | 61 +++++++++++++++++++------- evennia/utils/tests/test_funcparser.py | 46 +++++++++++-------- 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index c3455f6774..54fc64b0c8 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -84,8 +84,8 @@ class _ParsedFunc: # state storage fullstr: str = "" infuncstr: str = "" - single_quoted: bool = False - double_quoted: bool = False + single_quoted: int = -1 + double_quoted: int = -1 current_kwarg: str = "" open_lparens: int = 0 open_lsquate: int = 0 @@ -318,8 +318,8 @@ class FuncParser: # parsing state callstack = [] - single_quoted = False - double_quoted = False + single_quoted = -1 + double_quoted = -1 open_lparens = 0 # open ( open_lsquare = 0 # open [ open_lcurly = 0 # open { @@ -330,6 +330,7 @@ class FuncParser: curr_func = None fullstr = "" # final string infuncstr = "" # string parts inside the current level of $funcdef (including $) + literal_infuncstr = False for char in string: @@ -373,12 +374,13 @@ class FuncParser: curr_func.open_lcurly = open_lcurly current_kwarg = "" infuncstr = "" - single_quoted = False - double_quoted = False + single_quoted = -1 + double_quoted = -1 open_lparens = 0 open_lsquare = 0 open_lcurly = 0 exec_return = "" + literal_infuncstr = False callstack.append(curr_func) # start a new func @@ -401,19 +403,41 @@ class FuncParser: infuncstr += str(exec_return) exec_return = "" - if char == "'": # note that this is the same as "\'" + if char == "'" and double_quoted < 0: # note that this is the same as "\'" # a single quote - flip status - single_quoted = not single_quoted - infuncstr += char + if single_quoted == 0: + infuncstr = infuncstr[1:] + single_quoted = -1 + elif single_quoted > 0: + prefix = infuncstr[0:single_quoted] + infuncstr = prefix + infuncstr[single_quoted+1:] + single_quoted = -1 + else: + infuncstr += char + infuncstr = infuncstr.strip() + single_quoted = len(infuncstr) - 1 + literal_infuncstr = True + continue - if char == '"': # note that this is the same as '\"' + if char == '"' and single_quoted < 0: # note that this is the same as '\"' # a double quote = flip status - double_quoted = not double_quoted - infuncstr += char + if double_quoted == 0: + infuncstr = infuncstr[1:] + double_quoted = -1 + elif double_quoted > 0: + prefix = infuncstr[0:double_quoted] + infuncstr = prefix + infuncstr[double_quoted + 1:] + double_quoted = -1 + else: + infuncstr += char + infuncstr = infuncstr.strip() + double_quoted = len(infuncstr) - 1 + literal_infuncstr = True + continue - if double_quoted or single_quoted: + if double_quoted >= 0 or single_quoted >= 0: # inside a string definition - this escapes everything else infuncstr += char continue @@ -477,12 +501,15 @@ class FuncParser: else: curr_func.args.append(exec_return) else: + if not literal_infuncstr: + infuncstr = infuncstr.strip() + # store a string instead if current_kwarg: - curr_func.kwargs[current_kwarg] = infuncstr.strip() - elif infuncstr.strip(): + curr_func.kwargs[current_kwarg] = infuncstr + elif literal_infuncstr or infuncstr.strip(): # don't store the empty string - curr_func.args.append(infuncstr.strip()) + curr_func.args.append(infuncstr) # note that at this point either exec_return or infuncstr will # be empty. We need to store the full string so we can print @@ -493,6 +520,7 @@ class FuncParser: current_kwarg = "" exec_return = "" infuncstr = "" + literal_infuncstr = False if char == ")": # closing the function list - this means we have a @@ -536,6 +564,7 @@ class FuncParser: if return_str: exec_return = "" infuncstr = "" + literal_infuncstr = False continue infuncstr += char diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index c62928ff53..3f6e6eb2f9 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -44,6 +44,7 @@ def _double_callable(*args, **kwargs): def _eval_callable(*args, **kwargs): if args: return simple_eval(args[0]) + return "" @@ -113,25 +114,25 @@ class TestFuncParser(TestCase): ("$foo() Test noargs5", "_test() Test noargs5"), ("Test args1 $foo(a,b,c)", "Test args1 _test(a, b, c)"), ("Test args2 $bar(foo, bar, too)", "Test args2 _test(foo, bar, too)"), - ("Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, ' too')"), - ("Test args4 $foo('')", "Test args4 _test('')"), - ('Test args4 $foo("")', 'Test args4 _test("")'), + (r"Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, too)"), + ("Test args4 $foo('')", "Test args4 _test()"), + ('Test args4 $foo("")', 'Test args4 _test()'), ("Test args5 $foo(\(\))", "Test args5 _test(())"), ("Test args6 $foo(\()", "Test args6 _test(()"), ("Test args7 $foo(())", "Test args7 _test(())"), ("Test args8 $foo())", "Test args8 _test())"), ("Test args9 $foo(=)", "Test args9 _test(=)"), ("Test args10 $foo(\,)", "Test args10 _test(,)"), - ("Test args10 $foo(',')", "Test args10 _test(',')"), + ("Test args10 $foo(',')", "Test args10 _test(,)"), ("Test args11 $foo(()", "Test args11 $foo(()"), # invalid syntax ( "Test kwarg1 $bar(foo=1, bar='foo', too=ere)", - "Test kwarg1 _test(foo=1, bar='foo', too=ere)", + "Test kwarg1 _test(foo=1, bar=foo, too=ere)", ), ("Test kwarg2 $bar(foo,bar,too=ere)", "Test kwarg2 _test(foo, bar, too=ere)"), ("test kwarg3 $foo(foo = bar, bar = ere )", "test kwarg3 _test(foo=bar, bar=ere)"), ( - "test kwarg4 $foo(foo =' bar ',\" bar \"= ere )", + r"test kwarg4 $foo(foo =\' bar \',\" bar \"= ere )", "test kwarg4 _test(foo=' bar ', \" bar \"=ere)", ), ( @@ -180,22 +181,24 @@ class TestFuncParser(TestCase): ("Test clr $clr(r, This is a red string!)", "Test clr |rThis is a red string!|n"), ("Test eval1 $eval(21 + 21 - 10)", "Test eval1 32"), ("Test eval2 $eval((21 + 21) / 2)", "Test eval2 21.0"), - ("Test eval3 $eval('21' + 'foo' + 'bar')", "Test eval3 21foobar"), - ("Test eval4 $eval('21' + '$repl()' + '' + str(10 // 2))", "Test eval4 21rr5"), - ("Test eval5 $eval('21' + '\$repl()' + '' + str(10 // 2))", "Test eval5 21$repl()5"), - ("Test eval6 $eval('$repl(a)' + '$repl(b)')", "Test eval6 rarrbr"), + ("Test eval3 $eval(\"'21' + 'foo' + 'bar'\")", "Test eval3 21foobar"), + (r"Test eval4 $eval(\'21\' + \'$repl()\' + \"''\" + str(10 // 2))", "Test eval4 21rr5"), + (r"Test eval5 $eval(\'21\' + \'\$repl()\' + \'\' + str(10 // 2))", "Test eval5 21$repl()5"), + ("Test eval6 $eval(\"'$repl(a)' + '$repl(b)'\")", "Test eval6 rarrbr"), ("Test type1 $typ([1,2,3,4])", "Test type1 "), ("Test type2 $typ((1,2,3,4))", "Test type2 "), ("Test type3 $typ({1,2,3,4})", "Test type3 "), ("Test type4 $typ({1:2,3:4})", "Test type4 "), ("Test type5 $typ(1), $typ(1.0)", "Test type5 , "), - ("Test type6 $typ('1'), $typ(\"1.0\")", "Test type6 , "), + ("Test type6 $typ(\"'1'\"), $typ('\"1.0\"')", "Test type6 , "), ("Test add1 $add(1, 2)", "Test add1 3"), ("Test add2 $add([1,2,3,4], [5,6])", "Test add2 [1, 2, 3, 4, 5, 6]"), ("Test literal1 $sum($lit([1,2,3,4,5,6]))", "Test literal1 21"), ("Test literal2 $typ($lit(1))", "Test literal2 "), ("Test literal3 $typ($lit(1)aaa)", "Test literal3 "), ("Test literal4 $typ(aaa$lit(1))", "Test literal4 "), + ("Test spider's thread", "Test spider's thread"), + ] ) def test_parse(self, string, expected): @@ -258,7 +261,11 @@ class TestFuncParser(TestCase): self.assertEqual([1, 2, 3, 4], ret) self.assertTrue(isinstance(ret, list)) - ret = self.parser.parse_to_any("$lit('')") + ret = self.parser.parse_to_any("$lit(\"''\")") + self.assertEqual("", ret) + self.assertTrue(isinstance(ret, str)) + + ret = self.parser.parse_to_any(r"$lit(\'\')") self.assertEqual("", ret) self.assertTrue(isinstance(ret, str)) @@ -390,7 +397,8 @@ class TestDefaultCallables(TestCase): ("Some $rjust(Hello, 30)", "Some Hello"), ("Some $rjust(Hello, width=30)", "Some Hello"), ("Some $cjust(Hello, 30)", "Some Hello "), - ("Some $eval('-'*20)Hello", "Some --------------------Hello"), + ("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"), + ("$crop(\"spider's silk\", 5)", "spide"), ] ) def test_other_callables(self, string, expected): @@ -455,18 +463,18 @@ class TestDefaultCallables(TestCase): self.parser.parse( "this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)" ), - "this should be '''escaped,''' and '''instead,''' cropped with text. ", + "this should be escaped, and instead, cropped with text. ", ) def test_escaped2(self): + raw_str = 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)' + expected = 'this should be escaped, and instead, cropped with text. ' + result = self.parser.parse(raw_str) self.assertEqual( - self.parser.parse( - 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)' - ), - 'this should be """escaped,""" and """instead,""" cropped with text. ', + result, + expected, ) - class TestCallableSearch(test_resources.BaseEvenniaTest): """ Test the $search(query) callable From f676a7aac93885f47d5a5c22c6d427a48ccaa450 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Thu, 9 Jun 2022 15:50:16 -0600 Subject: [PATCH 50/77] fix ANSI_NORMAL override in text2html resets states on ANSI_NORMAL flag regardless of `clean` status --- evennia/utils/text2html.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index ab3f2930d6..1a0b2bf6cc 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -234,9 +234,9 @@ class TextToHTMLparser(object): 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] = "" + if substr == ANSI_NORMAL: + # close any existing span if necessary + str_list[i] = "" if not clean else "" # reset to defaults classes = [] clean = True From ff877b967128316b3245280cfaedee1862f14b6a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Jun 2022 00:00:12 +0200 Subject: [PATCH 51/77] Fix bug in 'Attribute-with-hidden-object' deserializer --- evennia/utils/dbserialize.py | 14 +++--- evennia/utils/tests/test_dbserialize.py | 58 +++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index a12f986e04..0fbec4c9e3 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -23,7 +23,7 @@ from collections import deque, OrderedDict, defaultdict from collections.abc import MutableSequence, MutableSet, MutableMapping try: - from pickle import dumps, loads + from pickle import dumps, loads, UnpicklingError except ImportError: from pickle import dumps, loads from django.core.exceptions import ObjectDoesNotExist @@ -633,12 +633,12 @@ def to_pickle(data): # 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 + # the item that Evennia will otherwise not find (these would # otherwise lead to an error). Use the dbserialize helper from # this method. try: item.__serialize_dbobjs__() - except TypeError: + except TypeError as err: # we catch typerrors so we can handle both classes (requiring # classmethods) and instances pass @@ -725,9 +725,13 @@ def from_pickle(data, db_obj=None): # use the dbunserialize helper in this module. try: item.__deserialize_dbobjs__() - except TypeError: + except (TypeError, UnpicklingError): # handle recoveries both of classes (requiring classmethods - # or instances + # or instances. Unpickling errors can happen when re-loading the + # data from cache (because the hidden entity was already + # deserialized and stored back on the object, unpickling it + # again fails). TODO: Maybe one could avoid this retry in a + # more graceful way? pass return item diff --git a/evennia/utils/tests/test_dbserialize.py b/evennia/utils/tests/test_dbserialize.py index 480893c466..5cd5ffceda 100644 --- a/evennia/utils/tests/test_dbserialize.py +++ b/evennia/utils/tests/test_dbserialize.py @@ -15,9 +15,7 @@ class TestDbSerialize(TestCase): """ def setUp(self): - self.obj = DefaultObject( - db_key="Tester", - ) + self.obj = DefaultObject(db_key="Tester") self.obj.save() def test_constants(self): @@ -117,3 +115,57 @@ class TestDbSerialize(TestCase): 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]}) + + +class _InvalidContainer: + """Container not saveable in Attribute (if obj is dbobj, it 'hides' it)""" + def __init__(self, obj): + self.hidden_obj = obj + + +class _ValidContainer(_InvalidContainer): + """Container possible to save in Attribute (handles hidden dbobj explicitly)""" + def __serialize_dbobjs__(self): + self.hidden_obj = dbserialize.dbserialize(self.hidden_obj) + def __deserialize_dbobjs__(self): + self.hidden_obj = dbserialize.dbunserialize(self.hidden_obj) + + +class DbObjWrappers(TestCase): + """ + Test the `__serialize_dbobjs__` and `__deserialize_dbobjs__` methods. + + """ + def setUp(self): + super().setUp() + self.dbobj1 = DefaultObject(db_key="Tester1") + self.dbobj1.save() + self.dbobj2 = DefaultObject(db_key="Tester2") + self.dbobj2.save() + + def test_dbobj_hidden_obj__fail(self): + with self.assertRaises(TypeError): + self.dbobj1.db.testarg = _InvalidContainer(self.dbobj1) + + def test_consecutive_fetch(self): + con =_ValidContainer(self.dbobj2) + self.dbobj1.db.testarg = con + attrobj = self.dbobj1.attributes.get("testarg", return_obj=True) + + self.assertEqual(attrobj.value, con) + self.assertEqual(attrobj.value, con) + self.assertEqual(attrobj.value.hidden_obj, self.dbobj2) + + def test_dbobj_hidden_obj__success(self): + con =_ValidContainer(self.dbobj2) + self.dbobj1.db.testarg = con + + # accessing the same data twice + res1 = self.dbobj1.db.testarg + res2 = self.dbobj1.db.testarg + + self.assertEqual(res1, res2) + self.assertEqual(res1, con) + self.assertEqual(res2, con) + self.assertEqual(res1.hidden_obj, self.dbobj2) + self.assertEqual(res2.hidden_obj, self.dbobj2) From 517ec5d4d5ce150ea8ad723de49261d33a221c28 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Jun 2022 00:40:36 +0200 Subject: [PATCH 52/77] Update FuncParser docs --- docs/Makefile | 4 +- docs/deploy.sh | 1 + docs/source/Components/FuncParser.md | 109 ++++++++++++++++++++------- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index f7d7feae4b..c79a48bdad 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -152,8 +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! +# note - don't run deploy/release manually, the result will clash with the +# result of the github actions! deploy: make _multiversion-deploy @echo "Documentation deployed." diff --git a/docs/deploy.sh b/docs/deploy.sh index 9ab8706804..e35af73b94 100644 --- a/docs/deploy.sh +++ b/docs/deploy.sh @@ -17,6 +17,7 @@ git checkout gh-pages # with the build/ directory available since this is not in git # remove all but the build dir +# TODO don't delete old branches after 1.0 release; they will get harder and harder to rebuild ls -Q | grep -v build | xargs rm -Rf cp -Rf build/html/* . diff --git a/docs/source/Components/FuncParser.md b/docs/source/Components/FuncParser.md index cb51d80c6b..3bc8260bd2 100644 --- a/docs/source/Components/FuncParser.md +++ b/docs/source/Components/FuncParser.md @@ -10,14 +10,15 @@ the return from the function. from evennia.utils.funcparser import FuncParser def _power_callable(*args, **kwargs): - """This will be callable as $square(number, power=) in string""" + """This will be callable as $pow(number, power=) in string""" pow = int(kwargs.get('power', 2)) return float(args[0]) ** pow +# create a parser and tell it that '$pow' means using _power_callable parser = FuncParser({"pow": _power_callable}) ``` -Next, just pass a string into the parser, optionally containing `$func(...)` markers: +Next, just pass a string into the parser, containing `$func(...)` markers: ```python parser.parse("We have that 4 x 4 x 4 is $pow(4, power=3).") @@ -71,7 +72,7 @@ You can apply inline function parsing to any string. The from evennia.utils import funcparser parser = FuncParser(callables, **default_kwargs) -parsed_string = parser.parser(input_string, raise_errors=False, +parsed_string = parser.parse(input_string, raise_errors=False, escape=False, strip=False, return_str=True, **reserved_kwargs) @@ -90,8 +91,12 @@ available to the parser as you parse strings with it. It can either be an underscore `_`) will be considered a suitable callable. The name of the function will be the `$funcname` by which it can be called. - A `list` of modules/paths. This allows you to pull in modules from many sources for your parsing. +- The `**default` kwargs are optional kwargs that will be passed to _all_ + callables every time this parser is used - unless the user overrides it explicitly in + their call. This is great for providing sensible standards that the user can + tweak as needed. -The other arguments to the parser: +`FuncParser.parse` takes further arguments, and can vary for every string parsed. - `raise_errors` - By default, any errors from a callable will be quietly ignored and the result will be that the failing function call will show verbatim. If `raise_errors` is set, @@ -102,12 +107,14 @@ The other arguments to the parser: - `return_str` - When `True` (default), `parser` always returns a string. If `False`, it may return the return value of a single function call in the string. This is the same as using the `.parse_to_any` method. -- The `**default/reserved_keywords` are optional and allow you to pass custom data into _every_ function - call. This is great for including things like the current session or config options. Defaults can be - replaced if the user gives the same-named kwarg in the string's function call. Reserved kwargs are always passed, - ignoring defaults or what the user passed. In addition, the `funcparser` and `raise_errors` - reserved kwargs are always passed - the first is a back-reference to the `FuncParser` instance and the second - is the `raise_errors` boolean passed into `FuncParser.parse`. +- The `**reserved_keywords` are _always_ passed to every callable in the string. + They override any `**defaults` given when instantiating the parser and cannot + be overridden by the user - if they enter the same kwarg it will be ignored. + This is great for providing the current session, settings etc. +- The `funcparser` and `raise_errors` + are always added as reserved keywords - the first is a + back-reference to the `FuncParser` instance and the second + is the `raise_errors` boolean given to `FuncParser.parse`. Here's an example of using the default/reserved keywords: @@ -158,7 +165,8 @@ created the parser. However, if you _nest_ functions, the return of the innermost function may be something other than a string. Let's introduce the `$eval` function, which evaluates simple expressions using -Python's `literal_eval` and/or `simple_eval`. +Python's `literal_eval` and/or `simple_eval`. It returns whatever data type it +evaluates to. "There's a $toint($eval(10 * 2.2))% chance of survival." @@ -177,23 +185,66 @@ will be a string: "There's a 22% chance of survival." ``` -However, if you use the `parse_to_any` (or `parse(..., return_str=True)`) and _don't add any extra string around the outermost function call_, +However, if you use the `parse_to_any` (or `parse(..., return_str=False)`) and +_don't add any extra string around the outermost function call_, you'll get the return type of the outermost callable back: ```python -parser.parse_to_any("$toint($eval(10 * 2.2)%") -"22%" parser.parse_to_any("$toint($eval(10 * 2.2)") 22 +parser.parse_to_any("the number $toint($eval(10 * 2.2).") +"the number 22" +parser.parse_to_any("$toint($eval(10 * 2.2)%") +"22%" ``` +### Escaping special character + +When entering funcparser callables in strings, it looks like a regular +function call inside a string: + +```python +"This is a $myfunc(arg1, arg2, kwarg=foo)." +``` + +Commas (`,`) and equal-signs (`=`) are considered to separate the arguments and +kwargs. In the same way, the right parenthesis (`)`) closes the argument list. +Sometimes you want to include commas in the argument without it breaking the +argument list. + +```python +"There is a $format(beautiful meadow, with dandelions) to the west." +``` + +You can escape in various ways. + +- Prepending with the escape character `\` + + ```python + "There is a $format(beautiful meadow\, with dandelions) to the west." + ``` +- Wrapping your strings in quotes. This works like Python, and you can nest + double and single quotes inside each other if so needed. The result will + be a verbatim string that contains everything but the outermost quotes. + + ```python + "There is a $format('beautiful meadow, with dandelions') to the west." + ``` +- If you want verbatim quotes in your string, you can escape them too. + + ```python + "There is a $format('beautiful meadow, with \'dandelions\'') to the west." + ``` + ### Safe convertion of inputs -Since you don't know in which order users may use your callables, they should always check the types -of its inputs and convert to the type the callable needs. Note also that when converting from strings, -there are limits what inputs you can support. This is because FunctionParser strings are often used by -non-developer players/builders and some things (such as complex classes/callables etc) are just not -safe/possible to convert from string representation. +Since you don't know in which order users may use your callables, they should +always check the types of its inputs and convert to the type the callable needs. +Note also that when converting from strings, there are limits what inputs you +can support. This is because FunctionParser strings can be used by +non-developer players/builders and some things (such as complex +classes/callables etc) are just not safe/possible to convert from string +representation. In `evennia.utils.utils` is a helper called [safe_convert_to_types](evennia.utils.utils.safe_convert_to_types). This function @@ -204,19 +255,24 @@ from evennia.utils.utils import safe_convert_to_types def _process_callable(*args, **kwargs): """ - A callable with a lot of custom options - - $process(expression, local, extra=34, extra2=foo) + $process(expression, local, extra1=34, extra2=foo) """ args, kwargs = safe_convert_to_type( - (('py', 'py'), {'extra1': int, 'extra2': str}), + (('py', str), {'extra1': int, 'extra2': str}), *args, **kwargs) # args/kwargs should be correct types now ``` +In other words, in the callable `$process(expression, local, extra1=.., +extra2=...)`, the first argument will be handled by the 'py' converter +(described below), the second will passed through regular Python `str`, +kwargs will be handled by `int` and `str` respectively. You can supply +your own converter function as long as it takes one argument and returns +the converted result. + In other words, ```python @@ -224,8 +280,7 @@ args, kwargs = safe_convert_to_type( (tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs) ``` -Each converter should be a callable taking one argument - this will be the arg/kwarg-value to convert. The -special converter `"py"` will try to convert a string argument to a Python structure with the help of the +The special converter `"py"` will try to convert a string argument to a Python structure with the help of the following tools (which you may also find useful to experiment with on your own): - [ast.literal_eval](https://docs.python.org/3.8/library/ast.html#ast.literal_eval) is an in-built Python @@ -339,12 +394,12 @@ references to other objects accessible via these callables. result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently depending on who sees it, and also to reference other people in the same way. - `$You([key])` - same as `$you` but always capitalized. -- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb +- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb between 2nd person presens to 3rd person presence depending on who sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](evennia.utils.verb_conjugation) to do this, and only works for English verbs. -- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically +- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically map pronouns (like his, herself, you, its etc) between 1st/2nd person to 3rd person. ### Example From 9c09d3a8936ca9b40bdb6f59cbae2359eda91d91 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Jun 2022 00:55:43 +0200 Subject: [PATCH 53/77] Update to readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 73bb35181c..81c8733a5d 100644 --- a/README.md +++ b/README.md @@ -60,14 +60,14 @@ introduction][introduction] to read. To learn how to get your hands on the code base, the [Getting started][gettingstarted] page is the way to go. Otherwise you could -browse the [Documentation][wiki] or why not come join the [Evennia +browse the [Documentation][docs] or why not come join the [Evennia Community forum][group] or join us in our [development chat][chat]. Welcome! -[homepage]: http://www.evennia.com +[homepage]: https://www.evennia.com [gettingstarted]: http://github.com/evennia/evennia/wiki/Getting-Started -[wiki]: https://github.com/evennia/evennia/wiki +[docs]: https://www.evennia.com/docs/latest/ [screenshot]: https://user-images.githubusercontent.com/294267/30773728-ea45afb6-a076-11e7-8820-49be2168a6b8.png [logo]: https://github.com/evennia/evennia/blob/master/evennia/web/website/static/website/images/evennia_logo.png [unittestciimg]: https://github.com/evennia/evennia/workflows/test-suite/badge.svg @@ -77,5 +77,5 @@ Welcome! [introduction]: https://github.com/evennia/evennia/wiki/Evennia-Introduction [license]: https://github.com/evennia/evennia/wiki/Licensing [group]: https://groups.google.com/forum/#!forum/evennia -[chat]: http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb +[chat]: https://discord.gg/AJJpcRUhtF [wikimudpage]: http://en.wikipedia.org/wiki/MUD From 45ed27c7f12ea52c78a803f4d3c333624c28b38f Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Jun 2022 01:05:00 +0200 Subject: [PATCH 54/77] Another update to the readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 81c8733a5d..9b32773135 100644 --- a/README.md +++ b/README.md @@ -66,16 +66,16 @@ Welcome! [homepage]: https://www.evennia.com -[gettingstarted]: http://github.com/evennia/evennia/wiki/Getting-Started -[docs]: https://www.evennia.com/docs/latest/ +[gettingstarted]: https://www.evennia.com/docs/latest/Getting-Started.html +[docs]: https://www.evennia.com/docs/latest [screenshot]: https://user-images.githubusercontent.com/294267/30773728-ea45afb6-a076-11e7-8820-49be2168a6b8.png [logo]: https://github.com/evennia/evennia/blob/master/evennia/web/website/static/website/images/evennia_logo.png [unittestciimg]: https://github.com/evennia/evennia/workflows/test-suite/badge.svg [unittestcilink]: https://github.com/evennia/evennia/actions?query=workflow%3Atest-suite [coverimg]: https://coveralls.io/repos/github/evennia/evennia/badge.svg?branch=master [coverlink]: https://coveralls.io/github/evennia/evennia?branch=master -[introduction]: https://github.com/evennia/evennia/wiki/Evennia-Introduction -[license]: https://github.com/evennia/evennia/wiki/Licensing -[group]: https://groups.google.com/forum/#!forum/evennia +[introduction]: https://www.evennia.com/docs/latest/Evennia-Introduction.html +[license]: https://www.evennia.com/docs/latest/Licensing.html +[group]: https://github.com/evennia/evennia/discussions [chat]: https://discord.gg/AJJpcRUhtF [wikimudpage]: http://en.wikipedia.org/wiki/MUD From a83f21ef2f075f0cbb28a32e9d7f9be2c9deed12 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Jun 2022 09:46:48 +0200 Subject: [PATCH 55/77] Apply black to cleanup code --- evennia/commands/cmdsethandler.py | 4 +- evennia/commands/default/building.py | 14 +-- evennia/commands/default/tests.py | 10 +- .../base_systems/components/__init__.py | 6 +- .../base_systems/components/component.py | 1 + .../base_systems/components/dbfield.py | 7 +- .../contrib/base_systems/components/holder.py | 10 +- .../base_systems/components/signals.py | 12 ++- .../contrib/base_systems/components/tests.py | 31 +++--- .../custom_gametime/custom_gametime.py | 2 +- evennia/contrib/rpg/rpsystem/rpsystem.py | 100 ++++++++++-------- evennia/contrib/rpg/rpsystem/tests.py | 18 ++-- evennia/contrib/rpg/traits/tests.py | 19 +++- evennia/contrib/rpg/traits/traits.py | 30 +++--- evennia/help/utils.py | 10 +- evennia/locks/lockfuncs.py | 6 +- evennia/prototypes/prototypes.py | 4 +- evennia/server/evennia_launcher.py | 8 +- evennia/server/server.py | 1 + evennia/server/tests/test_server.py | 1 - evennia/server/tests/testrunner.py | 4 + evennia/typeclasses/attributes.py | 16 +-- evennia/typeclasses/tags.py | 24 +++-- evennia/typeclasses/tests.py | 4 +- evennia/utils/evmenu.py | 6 +- evennia/utils/funcparser.py | 4 +- evennia/utils/gametime.py | 2 +- evennia/utils/logger.py | 14 ++- evennia/utils/test_resources.py | 1 - evennia/utils/tests/test_dbserialize.py | 8 +- evennia/utils/tests/test_funcparser.py | 18 ++-- evennia/utils/tests/test_text2html.py | 54 +++------- evennia/utils/utils.py | 1 + 33 files changed, 256 insertions(+), 194 deletions(-) diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 1930aa3bce..d6338eac75 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -450,9 +450,7 @@ class CmdSetHandler(object): """ if "permanent" in kwargs: - logger.log_dep( - "obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'." - ) + logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'.") persistent = kwargs["permanent"] if persistent is False else persistent if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)): diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index e8d3b6a5b6..582c6434e4 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2219,11 +2219,13 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): old_typeclass_path = obj.typeclass_path if reset: - answer = yield("|yNote that this will reset the object back to its typeclass' default state, " - "removing any custom locks/perms/attributes etc that may have been added " - "by an explicit create_object call. Use `update` or type/force instead in order " - "to keep such data. " - "Continue [Y]/N?|n") + answer = yield ( + "|yNote that this will reset the object back to its typeclass' default state, " + "removing any custom locks/perms/attributes etc that may have been added " + "by an explicit create_object call. Use `update` or type/force instead in order " + "to keep such data. " + "Continue [Y]/N?|n" + ) if answer.upper() in ("N", "NO"): caller.msg("Aborted.") return @@ -3473,7 +3475,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS): caller.msg("\n".join(msgs)) if "delete" not in self.switches: if script and script.pk: - ScriptEvMore(caller, [script], session=self.session) + ScriptEvMore(caller, [script], session=self.session) else: caller.msg("Script was deleted automatically.") else: diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index cfcc622689..32f4a882f5 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -107,8 +107,7 @@ class TestGeneral(BaseEvenniaCommandTest): def test_nick_list(self): self.call(general.CmdNick(), "/list", "No nicks defined.") - self.call(general.CmdNick(), "test1 = Hello", - "Inputline-nick 'test1' mapped to 'Hello'.") + self.call(general.CmdNick(), "test1 = Hello", "Inputline-nick 'test1' mapped to 'Hello'.") self.call(general.CmdNick(), "/list", "Defined Nicks:") def test_get_and_drop(self): @@ -1295,7 +1294,8 @@ class TestBuilding(BaseEvenniaCommandTest): "Obj2 = evennia.objects.objects.DefaultExit", "Obj2 changed typeclass from evennia.objects.objects.DefaultObject " "to evennia.objects.objects.DefaultExit.", - cmdstring="swap", inputs=["yes"], + cmdstring="swap", + inputs=["yes"], ) self.call(building.CmdTypeclass(), "/list Obj", "Core typeclasses") self.call( @@ -1332,7 +1332,7 @@ class TestBuilding(BaseEvenniaCommandTest): "/reset/force Obj=evennia.objects.objects.DefaultObject", "Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n" "All object creation hooks were run. All old attributes where deleted before the swap.", - inputs=["yes"] + inputs=["yes"], ) from evennia.prototypes.prototypes import homogenize_prototype @@ -1359,7 +1359,7 @@ class TestBuilding(BaseEvenniaCommandTest): "typeclasses.objects.Object.\nOnly the at_object_creation hook was run " "(update mode). Attributes set before swap were not removed\n" "(use `swap` or `type/reset` to clear all). Prototype 'replaced_obj' was " - "successfully applied over the object type." + "successfully applied over the object type.", ) assert self.obj1.db.desc == "protdesc" diff --git a/evennia/contrib/base_systems/components/__init__.py b/evennia/contrib/base_systems/components/__init__.py index 1aa94a1df1..21bb6bf936 100644 --- a/evennia/contrib/base_systems/components/__init__.py +++ b/evennia/contrib/base_systems/components/__init__.py @@ -17,8 +17,10 @@ def get_component_class(component_name): subclasses = Component.__subclasses__() component_class = next((sc for sc in subclasses if sc.name == component_name), None) if component_class is None: - message = f"Component named {component_name} has not been found. " \ - f"Make sure it has been imported before being used." + message = ( + f"Component named {component_name} has not been found. " + f"Make sure it has been imported before being used." + ) raise Exception(message) return component_class diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index 4b1697da38..4be5b86d15 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -13,6 +13,7 @@ class Component: Each Component must supply the name, it is used as a slot name but also part of the attribute key. """ + name = "" def __init__(self, host=None): diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 9adbf8197f..fc3a104ffb 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -26,7 +26,7 @@ class DBField(AttributeProperty): db_fields = getattr(owner, "_db_fields", None) if db_fields is None: db_fields = {} - setattr(owner, '_db_fields', db_fields) + setattr(owner, "_db_fields", db_fields) db_fields[name] = self @@ -50,7 +50,7 @@ class NDBField(NAttributeProperty): ndb_fields = getattr(owner, "_ndb_fields", None) if ndb_fields is None: ndb_fields = {} - setattr(owner, '_ndb_fields', ndb_fields) + setattr(owner, "_ndb_fields", ndb_fields) ndb_fields[name] = self @@ -64,6 +64,7 @@ class TagField: Default value of a tag is added when the component is registered. Tags are removed if the component itself is removed. """ + def __init__(self, default=None, enforce_single=False): self._category_key = None self._default = default @@ -78,7 +79,7 @@ class TagField: tag_fields = getattr(owner, "_tag_fields", None) if tag_fields is None: tag_fields = {} - setattr(owner, '_tag_fields', tag_fields) + setattr(owner, "_tag_fields", tag_fields) tag_fields[name] = self def __get__(self, instance, owner): diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index d5112083cd..91d4330f58 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -16,6 +16,7 @@ class ComponentProperty: Defaults can be overridden for this typeclass by passing kwargs """ + def __init__(self, component_name, **kwargs): """ Initializes the descriptor @@ -49,6 +50,7 @@ class ComponentHandler: It lets you add or remove components and will load components as needed. It stores the list of registered components on the host .db with component_names as key. """ + def __init__(self, host): self.host = host self._loaded_components = {} @@ -124,7 +126,9 @@ class ComponentHandler: self.host.signals.remove_object_listeners_and_responders(component) del self._loaded_components[component_name] else: - message = f"Cannot remove {component_name} from {self.host.name} as it is not registered." + message = ( + f"Cannot remove {component_name} from {self.host.name} as it is not registered." + ) raise ComponentIsNotRegistered(message) def remove_by_name(self, name): @@ -199,7 +203,9 @@ class ComponentHandler: self._set_component(component_instance) self.host.signals.add_object_listeners_and_responders(component_instance) else: - message = f"Could not initialize runtime component {component_name} of {self.host.name}" + message = ( + f"Could not initialize runtime component {component_name} of {self.host.name}" + ) raise ComponentDoesNotExist(message) def _set_component(self, component): diff --git a/evennia/contrib/base_systems/components/signals.py b/evennia/contrib/base_systems/components/signals.py index 23ace839dd..eff5137c64 100644 --- a/evennia/contrib/base_systems/components/signals.py +++ b/evennia/contrib/base_systems/components/signals.py @@ -15,9 +15,11 @@ def as_listener(func=None, signal_name=None): signal_name (str): The name of the signal to listen to, defaults to function name. """ if not func and signal_name: + def wrapper(func): func._listener_signal_name = signal_name return func + return wrapper signal_name = func.__name__ @@ -35,9 +37,11 @@ def as_responder(func=None, signal_name=None): signal_name (str): The name of the signal to respond to, defaults to function name. """ if not func and signal_name: + def wrapper(func): func._responder_signal_name = signal_name return func + return wrapper signal_name = func.__name__ @@ -177,12 +181,12 @@ class SignalsHandler(object): """ type_host = type(obj) for att_name, att_obj in type_host.__dict__.items(): - listener_signal_name = getattr(att_obj, '_listener_signal_name', None) + listener_signal_name = getattr(att_obj, "_listener_signal_name", None) if listener_signal_name: callback = getattr(obj, att_name) self.add_listener(signal_name=listener_signal_name, callback=callback) - responder_signal_name = getattr(att_obj, '_responder_signal_name', None) + responder_signal_name = getattr(att_obj, "_responder_signal_name", None) if responder_signal_name: callback = getattr(obj, att_name) self.add_responder(signal_name=responder_signal_name, callback=callback) @@ -196,12 +200,12 @@ class SignalsHandler(object): """ type_host = type(obj) for att_name, att_obj in type_host.__dict__.items(): - listener_signal_name = getattr(att_obj, '_listener_signal_name', None) + listener_signal_name = getattr(att_obj, "_listener_signal_name", None) if listener_signal_name: callback = getattr(obj, att_name) self.remove_listener(signal_name=listener_signal_name, callback=callback) - responder_signal_name = getattr(att_obj, '_responder_signal_name', None) + responder_signal_name = getattr(att_obj, "_responder_signal_name", None) if responder_signal_name: callback = getattr(obj, att_name) self.remove_responder(signal_name=responder_signal_name, callback=callback) diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index 413b964c18..6251575af6 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -56,7 +56,7 @@ class TestComponents(EvenniaTest): def test_character_can_register_runtime_component(self): rct = RuntimeComponentTestC.create(self.char1) self.char1.components.add(rct) - test_c = self.char1.components.get('test_c') + test_c = self.char1.components.get("test_c") assert test_c assert test_c.my_int == 6 @@ -110,7 +110,7 @@ class TestComponents(EvenniaTest): assert handler.get("test_c") is rct def test_can_access_component_regular_get(self): - assert self.char1.cmp.test_a is self.char1.components.get('test_a') + assert self.char1.cmp.test_a is self.char1.components.get("test_a") def test_returns_none_with_regular_get_when_no_attribute(self): assert self.char1.cmp.does_not_exist is None @@ -127,7 +127,7 @@ class TestComponents(EvenniaTest): def test_host_has_added_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) self.char1.components.add(rct) - test_c = self.char1.components.get('test_c') + test_c = self.char1.components.get("test_c") assert self.char1.tags.has(key="test_c", category="components") assert self.char1.tags.has(key="added_value", category="test_c::added_tag") @@ -162,7 +162,7 @@ class TestComponents(EvenniaTest): assert not self.char1.tags.has(key="added_value", category="test_c::added_tag") def test_component_tags_only_hold_one_value_when_enforce_single(self): - test_b = self.char1.components.get('test_b') + test_b = self.char1.components.get("test_b") test_b.single_tag = "first_value" test_b.single_tag = "second value" @@ -171,7 +171,7 @@ class TestComponents(EvenniaTest): assert not self.char1.tags.has(key="first_value", category="test_b::single_tag") def test_component_tags_default_value_is_overridden_when_enforce_single(self): - test_b = self.char1.components.get('test_b') + test_b = self.char1.components.get("test_b") test_b.default_single_tag = "second value" assert self.char1.tags.has(key="second value", category="test_b::default_single_tag") @@ -179,12 +179,14 @@ class TestComponents(EvenniaTest): assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag") def test_component_tags_support_multiple_values_by_default(self): - test_b = self.char1.components.get('test_b') + test_b = self.char1.components.get("test_b") test_b.multiple_tags = "first value" test_b.multiple_tags = "second value" test_b.multiple_tags = "third value" - assert all(val in test_b.multiple_tags for val in ("first value", "second value", "third value")) + assert all( + val in test_b.multiple_tags for val in ("first value", "second value", "third value") + ) assert self.char1.tags.has(key="first value", category="test_b::multiple_tags") assert self.char1.tags.has(key="second value", category="test_b::multiple_tags") assert self.char1.tags.has(key="third value", category="test_b::multiple_tags") @@ -193,11 +195,11 @@ class TestComponents(EvenniaTest): class CharWithSignal(ComponentHolderMixin, DefaultCharacter): @signals.as_listener def my_signal(self): - setattr(self, 'my_signal_is_called', True) + setattr(self, "my_signal_is_called", True) @signals.as_listener def my_other_signal(self): - setattr(self, 'my_other_signal_is_called', True) + setattr(self, "my_other_signal_is_called", True) @signals.as_responder def my_response(self): @@ -213,11 +215,11 @@ class ComponentWithSignal(Component): @signals.as_listener def my_signal(self): - setattr(self, 'my_signal_is_called', True) + setattr(self, "my_signal_is_called", True) @signals.as_listener def my_other_signal(self): - setattr(self, 'my_other_signal_is_called', True) + setattr(self, "my_other_signal_is_called", True) @signals.as_responder def my_response(self): @@ -236,14 +238,15 @@ class TestComponentSignals(BaseEvenniaTest): def setUp(self): super().setUp() self.char1 = create.create_object( - CharWithSignal, key="Char", + CharWithSignal, + key="Char", ) def test_host_can_register_as_listener(self): self.char1.signals.trigger("my_signal") assert self.char1.my_signal_is_called - assert not getattr(self.char1, 'my_other_signal_is_called', None) + assert not getattr(self.char1, "my_other_signal_is_called", None) def test_host_can_register_as_responder(self): responses = self.char1.signals.query("my_response") @@ -258,7 +261,7 @@ class TestComponentSignals(BaseEvenniaTest): component = char.cmp.test_signal_a assert component.my_signal_is_called - assert not getattr(component, 'my_other_signal_is_called', None) + assert not getattr(component, "my_other_signal_is_called", None) def test_component_can_register_as_responder(self): char = self.char1 diff --git a/evennia/contrib/base_systems/custom_gametime/custom_gametime.py b/evennia/contrib/base_systems/custom_gametime/custom_gametime.py index 33588deca9..eac9020b8a 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.start(interval=seconds,force_restart=True) + self.start(interval=seconds, force_restart=True) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index c191d72d93..69f29280c6 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: @@ -339,18 +339,18 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ """ # build a list of candidates with all possible referrable names # include 'me' keyword for self-ref - candidate_map = [(sender, 'me')] + 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)) + 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.extend( [(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". @@ -374,31 +374,40 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ 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:] - + tail = string[match_index + 1 :] + if search_mode: # 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) + 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 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 + # 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) + tail = re.split("(\W)", tail) iend = 0 for i, item in enumerate(tail): # don't add non-word characters to the search query if not item.isalpha(): - continue + continue 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(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 @@ -411,7 +420,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_ # save search string matched_text = "".join(tail[1:iend]) # recombine remainder of emote back into a string - tail = "".join(tail[iend+1:]) + tail = "".join(tail[iend + 1 :]) nmatches = len(bestmatches) @@ -549,18 +558,18 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **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','~','')] + 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 it if anonymous_add == "first": # add case flag for initial caps - skey += 't' + skey += "t" # 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 femote = "{emote} [{key}]" - emote = femote.format( key="{{"+ skey +"}}", emote=emote ) + emote = femote.format(key="{{" + skey + "}}", emote=emote) obj_mapping[skey] = sender # broadcast emote to everyone @@ -663,7 +672,9 @@ class SdescHandler: if len(cleaned_sdesc) > max_length: raise SdescError( - "Short desc can max be {} chars long (was {} chars).".format(max_length, len(cleaned_sdesc)) + "Short desc can max be {} chars long (was {} chars).".format( + max_length, len(cleaned_sdesc) + ) ) # store to attributes @@ -682,7 +693,6 @@ class SdescHandler: return self.sdesc or self.obj.key - class RecogHandler: """ This handler manages the recognition mapping @@ -758,7 +768,9 @@ class RecogHandler: if len(cleaned_recog) > max_length: raise RecogError( - "Recog string cannot be longer than {} chars (was {} chars)".format(max_length, len(cleaned_recog)) + "Recog string cannot be longer than {} chars (was {} chars)".format( + max_length, len(cleaned_recog) + ) ) # mapping #dbref:obj @@ -866,7 +878,7 @@ class CmdEmote(RPCommand): # replaces the main emote emote = self.args targets = self.caller.location.contents if not emote.endswith((".", "?", "!", '"')): # If emote is not punctuated or speech, - emote += "." # add a full-stop for good measure. + emote += "." # add a full-stop for good measure. send_emote(self.caller, targets, emote, anonymous_add="first") @@ -1132,7 +1144,11 @@ class CmdRecog(RPCommand): # assign personal alias to object in room if forget_mode: # remove existing recog caller.recog.remove(obj) - caller.msg("You will now know them only as '{}'.".format( 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 @@ -1216,9 +1232,10 @@ 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) + return SdescHandler(self) def at_object_creation(self): """ @@ -1409,19 +1426,18 @@ class ContribRPObject(DefaultObject): 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. @@ -1448,8 +1464,8 @@ class ContribRPObject(DefaultObject): is privileged to control said object. """ - ref = kwargs.get("ref","~") - + ref = kwargs.get("ref", "~") + if looker == self: # always show your own key sdesc = self.key @@ -1460,13 +1476,12 @@ class ContribRPObject(DefaultObject): except AttributeError: # use own sdesc as a fallback sdesc = self.sdesc.get() - - # 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 + # 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): """ @@ -1475,7 +1490,7 @@ class ContribRPObject(DefaultObject): Args: looker (Object): Object doing the looking. - + Returns: string (str): A string containing the name, appearance and contents of the object. @@ -1553,11 +1568,11 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): characters stand out from other objects. """ - ref = kwargs.get("ref","~") - + ref = kwargs.get("ref", "~") + if looker == self: # process your key as recog since you recognize yourself - sdesc = self.process_recog(self.key,self) + sdesc = self.process_recog(self.key, self) else: try: # get the sdesc looker should see, with formatting @@ -1567,12 +1582,11 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): sdesc = self.sdesc.get() # 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): + 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): """ Called at initial creation. @@ -1631,7 +1645,6 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): return sdesc - def process_sdesc(self, sdesc, obj, **kwargs): """ Allows to customize how your sdesc is displayed (primarily by @@ -1713,7 +1726,4 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject): the evennia.contrib.rpg.rplanguage module. """ - return "{label}|w{text}|n".format( - label=f"|W({language})" if language else "", - text=text - ) + return "{label}|w{text}|n".format(label=f"|W({language})" if language else "", text=text) diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py index f0d040d6f7..bd2ea32896 100644 --- a/evennia/contrib/rpg/rpsystem/tests.py +++ b/evennia/contrib/rpg/rpsystem/tests.py @@ -165,17 +165,19 @@ class TestRPSystem(BaseEvenniaTest): ) 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. + 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 + 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") @@ -233,7 +235,7 @@ class TestRPSystem(BaseEvenniaTest): self.out1, "|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." + "and |bAnother nice colliding sdesc-guy for tests|n twice.", ) self.assertEqual( self.out2, diff --git a/evennia/contrib/rpg/traits/tests.py b/evennia/contrib/rpg/traits/tests.py index a6bc405357..612fd8840d 100644 --- a/evennia/contrib/rpg/traits/tests.py +++ b/evennia/contrib/rpg/traits/tests.py @@ -321,7 +321,6 @@ class TestTraitStatic(_TraitHandlerBase): self.trait.mult = 0.75 self.assertEqual(self._get_values(), (5, 1, 0.75, 4.5)) - def test_delete(self): """Deleting resets to default.""" self.trait.mult = 2.0 @@ -362,7 +361,14 @@ class TestTraitCounter(_TraitHandlerBase): def _get_values(self): """Get (base, mod, mult, value, min, max).""" - return (self.trait.base, self.trait.mod, self.trait.mult, self.trait.value, self.trait.min, self.trait.max) + return ( + self.trait.base, + self.trait.mod, + self.trait.mult, + self.trait.value, + self.trait.min, + self.trait.max, + ) def test_init(self): self.assertEqual( @@ -634,7 +640,14 @@ class TestTraitGauge(_TraitHandlerBase): def _get_values(self): """Get (base, mod, mult, value, min, max).""" - return (self.trait.base, self.trait.mod, self.trait.mult, self.trait.value, self.trait.min, self.trait.max) + return ( + self.trait.base, + self.trait.mod, + self.trait.mult, + self.trait.value, + self.trait.min, + self.trait.max, + ) def test_init(self): self.assertEqual( diff --git a/evennia/contrib/rpg/traits/traits.py b/evennia/contrib/rpg/traits/traits.py index 303fe89bf7..cd759fa8c0 100644 --- a/evennia/contrib/rpg/traits/traits.py +++ b/evennia/contrib/rpg/traits/traits.py @@ -1148,7 +1148,7 @@ class Trait: class StaticTrait(Trait): """ - Static Trait. This is a single value with a modifier, + Static Trait. This is a single value with a modifier, multiplier, and no concept of a 'current' value or min/max etc. value = (base + mod) * mult @@ -1161,7 +1161,9 @@ class StaticTrait(Trait): def __str__(self): status = "{value:11}".format(value=self.value) - return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(name=self.name, status=status, mod=self.mod, mult=self.mult) + return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format( + name=self.name, status=status, mod=self.mod, mult=self.mult + ) # Helpers @property @@ -1189,7 +1191,7 @@ class StaticTrait(Trait): def mult(self): """The trait's multiplier.""" return self._data["mult"] - + @mult.setter def mult(self, amount): if type(amount) in (int, float): @@ -1322,16 +1324,16 @@ class CounterTrait(Trait): now = time() tdiff = now - self._data["last_update"] current += rate * tdiff - value = (current + self.mod) + value = current + self.mod # we must make sure so we don't overstep our bounds # even if .mod is included if self._passed_ratetarget(value): - current = (self._data["ratetarget"] - self.mod) + current = self._data["ratetarget"] - self.mod self._stop_timer() elif not self._within_boundaries(value): - current = (self._enforce_boundaries(value) - self.mod) + current = self._enforce_boundaries(value) - self.mod self._stop_timer() else: self._data["last_update"] = now @@ -1378,7 +1380,7 @@ class CounterTrait(Trait): @property def mult(self): return self._data["mult"] - + @mult.setter def mult(self, amount): if type(amount) in (int, float): @@ -1571,7 +1573,9 @@ class GaugeTrait(CounterTrait): def __str__(self): status = "{value:4} / {base:4}".format(value=self.value, base=self.base) - return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(name=self.name, status=status, mod=self.mod, mult=self.mult) + return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format( + name=self.name, status=status, mod=self.mod, mult=self.mult + ) @property def base(self): @@ -1596,11 +1600,11 @@ class GaugeTrait(CounterTrait): if value + self.base < self.min: value = self.min - self.base self._data["mod"] = value - + @property def mult(self): return self._data["mult"] - + @mult.setter def mult(self, amount): if type(amount) in (int, float): @@ -1621,7 +1625,7 @@ class GaugeTrait(CounterTrait): if value is None: self._data["min"] = self.default_keys["min"] elif type(value) in (int, float): - self._data["min"] = min(value, (self.base + self.mod) * self.mult) + self._data["min"] = min(value, (self.base + self.mod) * self.mult) @property def max(self): @@ -1644,7 +1648,7 @@ class GaugeTrait(CounterTrait): def current(self): """The `current` value of the gauge.""" return self._update_current( - self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult)) + self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult)) ) @current.setter @@ -1655,7 +1659,7 @@ class GaugeTrait(CounterTrait): @current.deleter def current(self): "Resets current back to 'full'" - self._data["current"] = (self.base + self.mod) * self.mult + self._data["current"] = (self.base + self.mod) * self.mult @property def value(self): diff --git a/evennia/help/utils.py b/evennia/help/utils.py index 981aea1fd0..85a254ac9e 100644 --- a/evennia/help/utils.py +++ b/evennia/help/utils.py @@ -12,9 +12,13 @@ import re # since we use them (e.g. as command names). # Lunr's default ignore-word list is found here: # https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py -_LUNR_STOP_WORD_FILTER_EXCEPTIONS = ( - ["about", "might", "get", "who", "say"] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS -) +_LUNR_STOP_WORD_FILTER_EXCEPTIONS = [ + "about", + "might", + "get", + "who", + "say", +] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS _LUNR = None diff --git a/evennia/locks/lockfuncs.py b/evennia/locks/lockfuncs.py index 8f2cdb70a8..b3d3de0a5e 100644 --- a/evennia/locks/lockfuncs.py +++ b/evennia/locks/lockfuncs.py @@ -473,6 +473,7 @@ def tag(accessing_obj, accessed_obj, *args, **kwargs): category = args[1] if len(args) > 1 else None return bool(accessing_obj.tags.get(tagkey, category=category)) + def is_ooc(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -489,13 +490,14 @@ def is_ooc(accessing_obj, accessed_obj, *args, **kwargs): session = accessed_obj.session except AttributeError: session = account.sessions.get()[0] # note-this doesn't work well - # for high multisession mode. We may need - # to change to sessiondb to resolve this + # for high multisession mode. We may need + # to change to sessiondb to resolve this try: return not account.get_puppet(session) except TypeError: return not session.get_puppet() + def objtag(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index f9b6c564d3..8c002c6145 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -528,10 +528,10 @@ def search_prototype( """ # This will load the prototypes the first time they are searched - loaded = getattr(load_module_prototypes, '_LOADED', False) + loaded = getattr(load_module_prototypes, "_LOADED", False) if not loaded: load_module_prototypes() - setattr(load_module_prototypes, '_LOADED', True) + setattr(load_module_prototypes, "_LOADED", True) # prototype keys are always in lowecase if key: diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 846e674ad0..110e4d218a 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -2316,9 +2316,11 @@ def main(): if option in ("makemessages", "compilemessages"): # some commands don't require the presence of a game directory to work need_gamedir = False - if CURRENT_DIR != EVENNIA_LIB: - print("You must stand in the evennia/evennia/ folder (where the 'locale/' " - "folder is located) to run this command.") + if CURRENT_DIR != EVENNIA_LIB: + print( + "You must stand in the evennia/evennia/ folder (where the 'locale/' " + "folder is located) to run this command." + ) sys.exit() if option in ("shell", "check", "makemigrations", "createsuperuser", "shell_plus"): diff --git a/evennia/server/server.py b/evennia/server/server.py index 4f653ffad5..4739d43ff3 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -423,6 +423,7 @@ class Evennia: logger.log_msg("Evennia Server successfully restarted in 'reset' mode.") elif mode == "shutdown": from evennia.objects.models import ObjectDB + self.at_server_cold_start() # clear eventual lingering session storages ObjectDB.objects.clear_all_sessids() diff --git a/evennia/server/tests/test_server.py b/evennia/server/tests/test_server.py index d0c79064a0..a5bf8a825a 100644 --- a/evennia/server/tests/test_server.py +++ b/evennia/server/tests/test_server.py @@ -197,7 +197,6 @@ class TestServer(TestCase): class TestInitHooks(TestCase): - def setUp(self): from evennia.utils import create diff --git a/evennia/server/tests/testrunner.py b/evennia/server/tests/testrunner.py index 9199164c58..69bd632461 100644 --- a/evennia/server/tests/testrunner.py +++ b/evennia/server/tests/testrunner.py @@ -16,15 +16,18 @@ class EvenniaTestSuiteRunner(DiscoverRunner): avoid running the large number of tests defined by Django """ + def setup_test_environment(self, **kwargs): # the portal looping call starts before the unit-test suite so we # can't mock it - instead we stop it before starting the test - otherwise # we'd get unclean reactor errors across test boundaries. from evennia.server.portal.portal import PORTAL + PORTAL.maintenance_task.stop() # initialize evennia itself import evennia + evennia._init() from django.conf import settings @@ -37,6 +40,7 @@ class EvenniaTestSuiteRunner(DiscoverRunner): # remove testing flag after suite has run from django.conf import settings + settings._TEST_ENVIRONMENT = False super().teardown_test_environment(**kwargs) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 7c5a537c19..9830befa7f 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -218,13 +218,15 @@ class AttributeProperty: """ value = self._default try: - value = self.at_get(getattr(instance, self.attrhandler_name).get( - key=self._key, - default=self._default, - category=self._category, - strattr=self._strattr, - raise_exception=self._autocreate, - )) + value = self.at_get( + getattr(instance, self.attrhandler_name).get( + key=self._key, + default=self._default, + category=self._category, + strattr=self._strattr, + raise_exception=self._autocreate, + ) + ) except AttributeError: if self._autocreate: # attribute didn't exist and autocreate is set diff --git a/evennia/typeclasses/tags.py b/evennia/typeclasses/tags.py index 7e0926156b..2803c8de26 100644 --- a/evennia/typeclasses/tags.py +++ b/evennia/typeclasses/tags.py @@ -96,6 +96,7 @@ class Tag(models.Model): # Handlers making use of the Tags model # + class TagProperty: """ Tag property descriptor. Allows for setting tags on an object as Django-like 'fields' @@ -112,6 +113,7 @@ class TagProperty: mytag2 = TagProperty(category="tagcategory") """ + taghandler_name = "tags" def __init__(self, category=None, data=None): @@ -134,10 +136,7 @@ class TagProperty: """ try: return getattr(instance, self.taghandler_name).get( - key=self._key, - category=self._category, - return_list=False, - raise_exception=True + key=self._key, category=self._category, return_list=False, raise_exception=True ) except AttributeError: self.__set__(instance, self._category) @@ -150,9 +149,7 @@ class TagProperty: self._category = category ( getattr(instance, self.taghandler_name).add( - key=self._key, - category=self._category, - data=self._data + key=self._key, category=self._category, data=self._data ) ) @@ -430,8 +427,15 @@ class TagHandler(object): return ret[0] if len(ret) == 1 else ret - def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False, - raise_exception=False): + def get( + self, + key=None, + default=None, + category=None, + return_tagobj=False, + return_list=False, + raise_exception=False, + ): """ Get the tag for the given key, category or combination of the two. @@ -613,6 +617,7 @@ class AliasProperty(TagProperty): bob = AliasProperty() """ + taghandler_name = "aliases" @@ -636,6 +641,7 @@ class PermissionProperty(TagProperty): myperm = PermissionProperty() """ + taghandler_name = "permissions" diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index 90c4945898..d97e7c3b5e 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -145,7 +145,9 @@ class TestTypedObjectManager(BaseEvenniaTest): 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._manager( + "get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any" + ), [self.obj1], ) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 4dae1b700d..060d47c140 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1255,12 +1255,12 @@ class EvMenu: min_rows = 4 # split the items into columns - split = max(min_rows, ceil(len(table)/ncols)) + 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) + start = icol * split + end = min(start + split, max_end) cols_list.append(EvColumn(*table[start:end])) return str(EvTable(table=cols_list, border="none")) diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 54fc64b0c8..7186ff47b0 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -410,7 +410,7 @@ class FuncParser: single_quoted = -1 elif single_quoted > 0: prefix = infuncstr[0:single_quoted] - infuncstr = prefix + infuncstr[single_quoted+1:] + infuncstr = prefix + infuncstr[single_quoted + 1 :] single_quoted = -1 else: infuncstr += char @@ -427,7 +427,7 @@ class FuncParser: double_quoted = -1 elif double_quoted > 0: prefix = infuncstr[0:double_quoted] - infuncstr = prefix + infuncstr[double_quoted + 1:] + infuncstr = prefix + infuncstr[double_quoted + 1 :] double_quoted = -1 else: infuncstr += char diff --git a/evennia/utils/gametime.py b/evennia/utils/gametime.py index ab14c77847..9b915b368b 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.start(interval=seconds,force_restart=True) + self.start(interval=seconds, force_restart=True) # Access functions diff --git a/evennia/utils/logger.py b/evennia/utils/logger.py index ea0728a501..98bc5a499d 100644 --- a/evennia/utils/logger.py +++ b/evennia/utils/logger.py @@ -50,6 +50,7 @@ def _log(msg, logfunc, prefix="", **kwargs): # log call functions (each has legacy aliases) + def log_info(msg, **kwargs): """ Logs any generic debugging/informative info that should appear in the log. @@ -62,6 +63,7 @@ def log_info(msg, **kwargs): """ _log(msg, log.info, **kwargs) + info = log_info log_infomsg = log_info log_msg = log_info @@ -79,6 +81,7 @@ def log_warn(msg, **kwargs): """ _log(msg, log.warn, **kwargs) + warn = log_warn warning = log_warn log_warnmsg = log_warn @@ -120,6 +123,7 @@ def log_trace(msg=None, **kwargs): if msg: _log(msg, log.error, prefix="!!", **kwargs) + log_tracemsg = log_trace exception = log_trace critical = log_trace @@ -156,6 +160,7 @@ def log_sec(msg, **kwargs): """ _log(msg, log.info, prefix="SS", **kwargs) + sec = log_sec security = log_sec log_secmsg = log_sec @@ -174,12 +179,12 @@ def log_server(msg, **kwargs): _log(msg, log.info, prefix="Server", **kwargs) - class GetLogObserver: """ Sets up how the system logs are formatted. """ + component_prefix = "" event_levels = { twisted_logger.LogLevel.debug: "??", @@ -207,8 +212,7 @@ class GetLogObserver: event["log_format"] = str(event.get("log_format", "")) component_prefix = self.component_prefix or "" log_msg = twisted_logger.formatEventAsClassicLogText( - event, - formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT) + event, formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT) ) return f"{component_prefix}{log_msg}" @@ -218,14 +222,15 @@ class GetLogObserver: # Called by server/portal on startup + class GetPortalLogObserver(GetLogObserver): component_prefix = "|Portal| " + class GetServerLogObserver(GetLogObserver): component_prefix = "" - # logging overrides @@ -352,6 +357,7 @@ class WeeklyLogFile(logfile.DailyLogFile): self.lastDate = max(self.lastDate, self.toDate()) self.size += len(data) + # Arbitrary file logger diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py index 65b62ea633..53d054de37 100644 --- a/evennia/utils/test_resources.py +++ b/evennia/utils/test_resources.py @@ -96,7 +96,6 @@ DEFAULT_SETTING_RESETS = dict( "evennia.game_template.server.conf.prototypefuncs", ], BASE_GUEST_TYPECLASS="evennia.accounts.accounts.DefaultGuest", - # a special setting boolean _TEST_ENVIRONMENT is set by the test runner # while the test suite is running. ) diff --git a/evennia/utils/tests/test_dbserialize.py b/evennia/utils/tests/test_dbserialize.py index 5cd5ffceda..d4fd101ef0 100644 --- a/evennia/utils/tests/test_dbserialize.py +++ b/evennia/utils/tests/test_dbserialize.py @@ -119,14 +119,17 @@ class TestDbSerialize(TestCase): class _InvalidContainer: """Container not saveable in Attribute (if obj is dbobj, it 'hides' it)""" + def __init__(self, obj): self.hidden_obj = obj class _ValidContainer(_InvalidContainer): """Container possible to save in Attribute (handles hidden dbobj explicitly)""" + def __serialize_dbobjs__(self): self.hidden_obj = dbserialize.dbserialize(self.hidden_obj) + def __deserialize_dbobjs__(self): self.hidden_obj = dbserialize.dbunserialize(self.hidden_obj) @@ -136,6 +139,7 @@ class DbObjWrappers(TestCase): Test the `__serialize_dbobjs__` and `__deserialize_dbobjs__` methods. """ + def setUp(self): super().setUp() self.dbobj1 = DefaultObject(db_key="Tester1") @@ -148,7 +152,7 @@ class DbObjWrappers(TestCase): self.dbobj1.db.testarg = _InvalidContainer(self.dbobj1) def test_consecutive_fetch(self): - con =_ValidContainer(self.dbobj2) + con = _ValidContainer(self.dbobj2) self.dbobj1.db.testarg = con attrobj = self.dbobj1.attributes.get("testarg", return_obj=True) @@ -157,7 +161,7 @@ class DbObjWrappers(TestCase): self.assertEqual(attrobj.value.hidden_obj, self.dbobj2) def test_dbobj_hidden_obj__success(self): - con =_ValidContainer(self.dbobj2) + con = _ValidContainer(self.dbobj2) self.dbobj1.db.testarg = con # accessing the same data twice diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 3f6e6eb2f9..1296f7899b 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -116,7 +116,7 @@ class TestFuncParser(TestCase): ("Test args2 $bar(foo, bar, too)", "Test args2 _test(foo, bar, too)"), (r"Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, too)"), ("Test args4 $foo('')", "Test args4 _test()"), - ('Test args4 $foo("")', 'Test args4 _test()'), + ('Test args4 $foo("")', "Test args4 _test()"), ("Test args5 $foo(\(\))", "Test args5 _test(())"), ("Test args6 $foo(\()", "Test args6 _test(()"), ("Test args7 $foo(())", "Test args7 _test(())"), @@ -183,14 +183,20 @@ class TestFuncParser(TestCase): ("Test eval2 $eval((21 + 21) / 2)", "Test eval2 21.0"), ("Test eval3 $eval(\"'21' + 'foo' + 'bar'\")", "Test eval3 21foobar"), (r"Test eval4 $eval(\'21\' + \'$repl()\' + \"''\" + str(10 // 2))", "Test eval4 21rr5"), - (r"Test eval5 $eval(\'21\' + \'\$repl()\' + \'\' + str(10 // 2))", "Test eval5 21$repl()5"), + ( + r"Test eval5 $eval(\'21\' + \'\$repl()\' + \'\' + str(10 // 2))", + "Test eval5 21$repl()5", + ), ("Test eval6 $eval(\"'$repl(a)' + '$repl(b)'\")", "Test eval6 rarrbr"), ("Test type1 $typ([1,2,3,4])", "Test type1 "), ("Test type2 $typ((1,2,3,4))", "Test type2 "), ("Test type3 $typ({1,2,3,4})", "Test type3 "), ("Test type4 $typ({1:2,3:4})", "Test type4 "), ("Test type5 $typ(1), $typ(1.0)", "Test type5 , "), - ("Test type6 $typ(\"'1'\"), $typ('\"1.0\"')", "Test type6 , "), + ( + "Test type6 $typ(\"'1'\"), $typ('\"1.0\"')", + "Test type6 , ", + ), ("Test add1 $add(1, 2)", "Test add1 3"), ("Test add2 $add([1,2,3,4], [5,6])", "Test add2 [1, 2, 3, 4, 5, 6]"), ("Test literal1 $sum($lit([1,2,3,4,5,6]))", "Test literal1 21"), @@ -198,7 +204,6 @@ class TestFuncParser(TestCase): ("Test literal3 $typ($lit(1)aaa)", "Test literal3 "), ("Test literal4 $typ(aaa$lit(1))", "Test literal4 "), ("Test spider's thread", "Test spider's thread"), - ] ) def test_parse(self, string, expected): @@ -398,7 +403,7 @@ class TestDefaultCallables(TestCase): ("Some $rjust(Hello, width=30)", "Some Hello"), ("Some $cjust(Hello, 30)", "Some Hello "), ("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"), - ("$crop(\"spider's silk\", 5)", "spide"), + ('$crop("spider\'s silk", 5)', "spide"), ] ) def test_other_callables(self, string, expected): @@ -468,13 +473,14 @@ class TestDefaultCallables(TestCase): def test_escaped2(self): raw_str = 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)' - expected = 'this should be escaped, and instead, cropped with text. ' + expected = "this should be escaped, and instead, cropped with text. " result = self.parser.parse(raw_str) self.assertEqual( result, expected, ) + class TestCallableSearch(test_resources.BaseEvenniaTest): """ Test the $search(query) callable diff --git a/evennia/utils/tests/test_text2html.py b/evennia/utils/tests/test_text2html.py index 361ab086cf..cb99ab8bd7 100644 --- a/evennia/utils/tests/test_text2html.py +++ b/evennia/utils/tests/test_text2html.py @@ -12,7 +12,9 @@ class TestText2Html(TestCase): self.assertEqual("foo", parser.format_styles("foo")) self.assertEqual( 'redfoo', - parser.format_styles(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', @@ -31,33 +33,15 @@ class TestText2Html(TestCase): ) self.assertEqual( 'a redfoo', - parser.format_styles( - "a " - + ansi.ANSI_UNDERLINE - + "red" - + ansi.ANSI_NORMAL - + "foo" - ), + parser.format_styles("a " + ansi.ANSI_UNDERLINE + "red" + ansi.ANSI_NORMAL + "foo"), ) self.assertEqual( 'a redfoo', - parser.format_styles( - "a " - + ansi.ANSI_BLINK - + "red" - + ansi.ANSI_NORMAL - + "foo" - ), + parser.format_styles("a " + ansi.ANSI_BLINK + "red" + ansi.ANSI_NORMAL + "foo"), ) self.assertEqual( 'a redfoo', - parser.format_styles( - "a " - + ansi.ANSI_INVERSE - + "red" - + ansi.ANSI_NORMAL - + "foo" - ), + parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"), ) def test_remove_bells(self): @@ -65,13 +49,7 @@ class TestText2Html(TestCase): self.assertEqual("foo", parser.remove_bells("foo")) self.assertEqual( "a red" + ansi.ANSI_NORMAL + "foo", - parser.remove_bells( - "a " - + ansi.ANSI_BEEP - + "red" - + ansi.ANSI_NORMAL - + "foo" - ), + parser.remove_bells("a " + ansi.ANSI_BEEP + "red" + ansi.ANSI_NORMAL + "foo"), ) def test_remove_backspaces(self): @@ -160,20 +138,20 @@ class TestText2Html(TestCase): self.assertEqual( text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"), '' - 'Hello' + "Hello" '' - 'W' + "W" '' - 'o' + "o" '' - 'r' + "r" '' - 'l' + "l" '' - 'd' + "d" '' - '!' + "!" '' - '!' - '', + "!" + "", ) diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index d603a93bd2..10f8073e31 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -2695,6 +2695,7 @@ def copy_word_case(base_word, new_word): + excess ) + def run_in_main_thread(function_or_method, *args, **kwargs): """ Force a callable to execute in the main Evennia thread. This is only relevant when From 87d77c6105b0574e4785046916b4c1e3e9ae2a93 Mon Sep 17 00:00:00 2001 From: Avalon Hope <96471977+avalonhope@users.noreply.github.com> Date: Fri, 17 Jun 2022 09:35:02 +0100 Subject: [PATCH 56/77] Adding unit tests for script search --- evennia/utils/tests/test_search.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 evennia/utils/tests/test_search.py diff --git a/evennia/utils/tests/test_search.py b/evennia/utils/tests/test_search.py new file mode 100644 index 0000000000..6bacfb4cb3 --- /dev/null +++ b/evennia/utils/tests/test_search.py @@ -0,0 +1,29 @@ +from evennia.scripts.scripts import DefaultScript +from evennia.utils.test_resources import EvenniaTest +from evennia.utils.search import search_script_tag + +class TestSearch(EvenniaTest): + + def test_search_script_tag(self): + """Check that a script can be found by its tag.""" + script, errors = DefaultScript.create("a-script") + script.tags.add("a-tag") + found = search_script_tag("a-tag") + self.assertEqual(len(found), 1, errors) + self.assertEqual(script.key, found[0].key, errors) + + def test_search_script_tag_category(self): + """Check that a script can be found by its tag and category.""" + script, errors = DefaultScript.create("a-script") + script.tags.add("a-tag", category="a-category") + found = search_script_tag("a-tag", category="a-category") + self.assertEqual(len(found), 1, errors) + self.assertEqual(script.key, found[0].key, errors) + + def test_search_script_tag_wrong_category(self): + """Check that a script can be found by its tag and category.""" + script, errors = DefaultScript.create("a-script") + script.tags.add("a-tag", category="a-category") + found = search_script_tag("a-tag", category="wrong-category") + self.assertEqual(len(found), 0, errors) + From ef6080ea43ff5edbad8d15f58521ad3bf4bb6dd1 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 17 Jun 2022 13:17:36 -0600 Subject: [PATCH 57/77] Update and reorganize Links --- docs/source/Links.md | 216 ++++++++++++++++++------------------------- 1 file changed, 92 insertions(+), 124 deletions(-) diff --git a/docs/source/Links.md b/docs/source/Links.md index 514f225a92..89cfd176c8 100644 --- a/docs/source/Links.md +++ b/docs/source/Links.md @@ -2,127 +2,121 @@ *A list of resources that may be useful for Evennia users and developers.* -## Official Evennia links +## Official Evennia resources - [evennia.com](https://www.evennia.com) - Main Evennia portal page. Links to all corners of Evennia. -- [Evennia github page](https://github.com/evennia/evennia) - Download code and read documentation. -- [Evennia official chat -channel](https://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb) -- Our official IRC chat #evennia at irc.freenode.net:6667. -- [Evennia forums/mailing list](https://groups.google.com/group/evennia) - Web interface to our -google group. -- [Evennia development blog](https://evennia.blogspot.com/) - Musings from the lead developer. - [Evennia's manual on ReadTheDocs](https://readthedocs.org/projects/evennia/) - Read and download offline in html, PDF or epub formats. -- [Evennia Game Index](http://games.evennia.com/) - An automated listing of Evennia games. ----- +- [Evennia development blog](https://evennia.blogspot.com/) - Musings from the lead developer. +- [Evennia on GitHub](https://github.com/evennia/evennia) - Download code and read documentation. - [Evennia on Open Hub](https://www.openhub.net/p/6906) - [Evennia on OpenHatch](https://openhatch.org/projects/Evennia) - [Evennia on PyPi](https://pypi.python.org/pypi/Evennia-MUD-Server/) -- [Evennia subreddit](https://www.reddit.com/r/Evennia/) (not much there yet though) -## Third-party Evennia utilities and resources +## Evennia Community -*For publicly available games running on Evennia, add and find those in the [Evennia game -index](http://games.evennia.com) instead!* +- [Evennia Game Index](http://games.evennia.com/) - An automated listing of Evennia games. +- [Evennia official Discord channel](https://discord.gg/AJJpcRUhtF) +- [Evennia official IRC +channel](https://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb) ( #evennia at [irc.freenode.net:6667](http://irc.freenode.net:6667/) ) +- [Evennia official forums](https://github.com/evennia/evennia/discussions) on Github Discussions. +- [Evennia subreddit](https://www.reddit.com/r/Evennia/) -- [Discord Evennia channel](https://discord.gg/NecFePw) - This is a fan-driven Discord channel with -a bridge to the official Evennia IRC channel. +## Third-party Evennia tools ---- - -- [Discord live blog](https://discordapp.com/channels/517176782357528616/517176782781415434) of the -_Blackbirds_ Evennia game project. -- [Unreal Engine Evennia plugin](https://www.unrealengine.com/marketplace/en-US/slug/evennia-plugin) - an in-progress Unreal plugin for integrating Evennia with Epic Games' Unreal Engine. -- [The dark net/March Hare MUD](https://github.com/thedarknet/evennia) from the 2019 [DEF CON -27](https://www.defcon.org/html/defcon-27/dc-27-index.html) hacker conference in Paris. This is an -Evennia game dir with batchcode to build the custom _Hackers_ style cyberspace zone with puzzles and -challenges [used during the conference](https://dcdark.net/home#). -- [Arx sources](https://github.com/Arx-Game/arxcode) - Open-source code release of the very popular -[Arx](https://play.arxmush.org/) Evennia game. [Here are instructions for installing](Arxcode- -installing-help) -- [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your -website. -- [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional -coloration for Evennia unit-test output. -- [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for -telnet/web). -- [Encarnia sources](https://github.com/whitehorse-io/encarnia) - An open-sourced game dir for -Evennia with things like races, combat etc. [Summary -here](https://www.reddit.com/r/MUD/comments/6z6s3j/encarnia_an_evennia_python_mud_code_base_with/). -- [The world of Cool battles sources](https://github.com/FlutterSprite/coolbattles) - Open source -turn-based battle system for Evennia. It also has a [live demo](http://wcb.battlestudio.com/). -- [nextRPI](https://github.com/cluebyte/nextrpi) - A github project for making a toolbox for people -to make [RPI](https://www.topmudsites.com/forums/showthread.php?t=4804)-style Evennia games. -- [Muddery](https://github.com/muddery/muddery) - A mud framework under development, based on an -older fork of Evennia. It has some specific design goals for building and extending the game based -on input files. -- [vim-evennia](https://github.com/amfl/vim-evennia) - A mode for editing batch-build files (`.ev`) -files in the [vim](https://www.vim.org/) text editor (Emacs users can use [evennia- +- [Evennia-docker](https://github.com/gtaylor/evennia-docker) - Evennia in a [Docker container](https://www.docker.com/) for quick install and deployment in just a few commands. +- [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your website. +- [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional coloration for Evennia unit-test output. +- [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for telnet/web). +- [The world of Cool battles sources](https://github.com/FlutterSprite/coolbattles) - Open source turn-based battle system for an older version of Evennia. +- [nextRPI](https://github.com/cluebyte/nextrpi) - A github project for making a toolbox for people to make [RPI](https://www.topmudsites.com/forums/showthread.php?t=4804)-style Evennia games. +- [Unreal Engine Evennia plugin](https://www.unrealengine.com/marketplace/en-US/slug/evennia-plugin) an in-progress Unreal plugin for integrating Evennia with Epic Games' Unreal Engine. +- [vim-evennia](https://github.com/amfl/vim-evennia) - A mode for editing batch-build files (`.ev`) files in the [vim](https://www.vim.org/) text editor (Emacs users can use [evennia- mode.el](https://github.com/evennia/evennia/blob/master/evennia/utils/evennia-mode.el)). - [Other Evennia-related repos on github](https://github.com/search?p=1&q=evennia) + ---- -- [EvCast video series](https://www.youtube.com/playlist?list=PLyYMNttpc-SX1hvaqlUNmcxrhmM64pQXl) - -Tutorial videos explaining installing Evennia, basic Python etc. -- [Evennia-docker](https://github.com/gtaylor/evennia-docker) - Evennia in a [Docker -container](https://www.docker.com/) for quick install and deployment in just a few commands. -- [Evennia's docs in Chinese](http://www.evenniacn.com/) - A translated mirror of a slightly older -Evennia version. Announcement [here](https://groups.google.com/forum/#!topic/evennia/3AXS8ZTzJaA). -- [Evennia for MUSHers](https://musoapbox.net/topic/1150/evennia-for-mushers) - An article describing -Evennia for those used to the MUSH way of doing things. -- *[Language Understanding for Text games using Deep reinforcement -learning](http://news.mit.edu/2015/learning-language-playing-computer-games-0924#_msocom_1)* -([PDF](https://people.csail.mit.edu/karthikn/pdfs/mud-play15.pdf)) - MIT research paper using Evennia -to train AIs. -## Other useful mud development resources +## Evennia-Based Projects -- [ROM area reader](https://github.com/ctoth/area_reader) - Parser for converting ROM area files to -Python objects. -- [Gossip MUD chat network](https://gossip.haus/) +### Code bases +- [Arx sources](https://github.com/Arx-Game/arxcode) - Open-source code release of the very popular [Arx](https://play.arxmush.org/) Evennia game. [Here are instructions for installing](https://www.evennia.com/docs/1.0-dev/Howtos/Arxcode-Installation.html) +- [The dark net/March Hare MUD](https://github.com/thedarknet/evennia) from the 2019 [DEF CON 27](https://www.defcon.org/html/defcon-27/dc-27-index.html) hacker conference in Paris. This is an Evennia game dir with batchcode to build the custom _Hackers_ style cyberspace zone with puzzles and challenges [used during the conference](https://dcdark.net/home#). +- [Muddery](https://github.com/muddery/muddery) - A mud framework under development, based on an older fork of Evennia. It has some specific design goals for building and extending the game based on input files. +- [Encarnia sources](https://github.com/whitehorse-io/encarnia) - An open-sourced game dir for an older version of Evennia with things like races, combat etc. [Summary here](https://www.reddit.com/r/MUD/comments/6z6s3j/encarnia_an_evennia_python_mud_code_base_with/). -## General MUD forums and discussions +### Other -- [MUD Coder's Guild](https://mudcoders.com/) - A blog and [associated Slack - channel](https://slack.mudcoders.com/) with discussions on MUD development. -- [MuSoapbox](https://www.musoapbox.net/) - Very active Mu* game community mainly focused on MUSH-type gaming. -- [Imaginary Realities](http://journal.imaginary-realities.com/) - An e-magazine on game and MUD - design that has several articles about Evennia. There is also an - [archive of older issues](http://disinterest.org/resource/imaginary-realities/) - from 1998-2001 that are still very relevant. -- [Optional Realities](http://optionalrealities.com/) - Mud development discussion forums that has - regular articles on MUD development focused on roleplay-intensive games. After a HD crash it's not - as content-rich as it once was. +- [Discord live blog](https://discordapp.com/channels/517176782357528616/517176782781415434) of the _Blackbirds_ Evennia game project. +- [Evennia for MUSHers](https://musoapbox.net/topic/1150/evennia-for-mushers) - An article describing Evennia for those used to the MUSH way of doing things. +- *[Language Understanding for Text games using Deep reinforcement learning](http://news.mit.edu/2015/learning-language-playing-computer-games-0924#_msocom_1)* +([PDF](https://people.csail.mit.edu/karthikn/pdfs/mud-play15.pdf)) - MIT research paper using Evennia to train AIs. + +---- + +## General MU* resources + +### Tools + +- [ROM area reader](https://github.com/ctoth/area_reader) - Parser for converting ROM area files to Python objects. + +### Informational + +- [Mud-dev wiki](http://mud-dev.wikidot.com/) - A (very) slowly growing resource on MUD creation. +- [Mud Client/Server Interaction](http://cryosphere.net/mud-protocol.html) - A page on classic MUD telnet protocols. +- [Mud Tech's fun/cool but ...](https://gc-taylor.com/blog/2013/01/08/mud-tech-funcool-dont-forget-ship-damned-thing/) - Greg Taylor gives good advice on mud design. +- [Lost Library of MOO](https://www.hayseed.net/MOO/) - Archive of scientific articles on mudding (in particular moo). +- [Nick Gammon's hints thread](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) - Contains a very useful list of things to think about when starting your new MUD. +- Mud Dev mailing list archive ([mirror](http://www.disinterest.org/resource/MUD-Dev/)) - Influential mailing list active 1996-2004. Advanced game design discussions. +- [Raph Koster's laws of game design](https://www.raphkoster.com/games/laws-of-online-world-design/the-laws-of-online-world-design/) - thought-provoking guidelines and things to think about when designing a virtual multiplayer world (Raph is known for *Ultima Online* among other things). +- [Imaginary Realities unofficial archive](http://tharsis-gate.org/articles/imaginary.html) - An e-magazine on game and MUD design that has several articles about Evennia. + +### Community + +- [Grapevine MUD community and chat network](https://grapevine.haus/) +- [MUD Coder's Guild](https://mudcoders.com/) - A blog and [associated Slack channel](https://slack.mudcoders.com/) with discussions on MUD development. +- [MuSoapbox](https://musoapbox.net/) - MU* forum mainly focused on MUSH-type gaming. - [MudLab](http://mudlab.org/) - Mud design discussion forum - [MudConnector](http://www.mudconnect.com/) - Mud listing and forums - [MudBytes](http://www.mudbytes.net/) - Mud listing and forums - [Top Mud Sites](http://www.topmudsites.com/) - Mud listing and forums -- [Planet Mud-Dev](http://planet-muddev.disinterest.org/) - A blog aggregator following blogs of - current MUD development (including Evennia) around the 'net. Worth to put among your RSS - subscriptions. -- Mud Dev mailing list archive ([mirror](http://www.disinterest.org/resource/MUD-Dev/)) - - Influential mailing list active 1996-2004. Advanced game design discussions. -- [Mud-dev wiki](http://mud-dev.wikidot.com/) - A (very) slowly growing resource on MUD creation. -- [Mud Client/Server Interaction](http://cryosphere.net/mud-protocol.html) - A page on classic MUD - telnet protocols. -- [Mud Tech's fun/cool but ...](https://gc-taylor.com/blog/2013/01/08/mud-tech-funcool-dont-forget-ship-damned-thing/) - - Greg Taylor gives good advice on mud design. -- [Lost Library of MOO](https://www.hayseed.net/MOO/) - Archive of scientific articles on mudding (in -particular moo). -- [Nick Gammon's hints thread](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) - -Contains a very useful list of things to think about when starting your new MUD. -- [Lost Garden](http://www.lostgarden.com/) - A game development blog with long and interesting - articles (not MUD-specific) -- [What Games Are](http://whatgamesare.com/) - A blog about general game design (not MUD-specific) -- [The Alexandrian](https://thealexandrian.net/) - A blog about tabletop roleplaying and board games, - but with lots of general discussion about rule systems and game balance that could be applicable - also for MUDs. -- [Raph Koster's laws of game design](https://www.raphkoster.com/games/laws-of-online-world-design/the-laws-of-online-world-design/) - - thought-provoking guidelines and things to think about when designing a virtual multiplayer world - (Raph is known for *Ultima Online* among other things). -## Literature +---- + +## General Game-Dev Resources + +### Tools + +- [GIT](https://git-scm.com/) + - [Documentation](https://git-scm.com/documentation) + - [Learn GIT in 15 minutes](https://try.github.io/levels/1/challenges/1) (interactive tutorial) + +### Frameworks + +- [Django's homepage](https://www.djangoproject.com/) + - [Documentation](https://docs.djangoproject.com/en) + - [Code](https://code.djangoproject.com/) +- [Twisted homepage](https://twistedmatrix.com/) + - [Documentation](https://twistedmatrix.com/documents/current/core/howto/index.html) + - [Code](https://twistedmatrix.com/trac/browser) + +### Learning Python + +- [Python Website](https://www.python.org/) + - [Documentation](https://www.python.org/doc/) + - [Tutorial](https://docs.python.org/tut/tut.html) + - [Library Reference](https://docs.python.org/lib/lib.html) + - [Language Reference](https://docs.python.org/ref/ref.html) +- [Python tips and tricks](https://www.siafoo.net/article/52) +- [Jetbrains Python academy](https://hyperskill.org/onboarding?track=python) - free online programming curriculum for different skill levels + +### Blogs + +- [Lost Garden](https://lostgarden.home.blog/) - A game development blog with long and interesting articles (not MUD-specific) +- [What Games Are](http://whatgamesare.com/) - A blog about general game design (not MUD-specific) +- [The Alexandrian](https://thealexandrian.net/) - A blog about tabletop roleplaying and board games, but with lots of general discussion about rule systems and game balance that could be applicable also for MUDs. + +### Literature - Richard Bartle *Designing Virtual Worlds* ([amazon page](https://www.amazon.com/Designing-Virtual-Worlds-Richard-Bartle/dp/0131018167)) - @@ -148,29 +142,3 @@ Contains a very useful list of things to think about when starting your new MUD. economic theory. Written in 1730 but the translation is annotated and the essay is actually very easy to follow also for a modern reader. Required reading if you think of implementing a sane game economic system. - -## Frameworks - -- [Django's homepage](https://www.djangoproject.com/) - - [Documentation](https://docs.djangoproject.com/en) - - [Code](https://code.djangoproject.com/) -- [Twisted homepage](https://twistedmatrix.com/) - - [Documentation](https://twistedmatrix.com/documents/current/core/howto/index.html) - - [Code](https://twistedmatrix.com/trac/browser) - -## Tools - -- [GIT](https://git-scm.com/) - - [Documentation](https://git-scm.com/documentation) - - [Learn GIT in 15 minutes](https://try.github.io/levels/1/challenges/1) (interactive tutorial) - -## Python Info - -- [Python Website](https://www.python.org/) - - [Documentation](https://www.python.org/doc/) - - [Tutorial](https://docs.python.org/tut/tut.html) - - [Library Reference](https://docs.python.org/lib/lib.html) - - [Language Reference](https://docs.python.org/ref/ref.html) -- [Python tips and tricks](https://www.siafoo.net/article/52) -- [Jetbrains Python academy](https://hyperskill.org/onboarding?track=python) - - free online programming curriculum for different skill levels From 342c8a6218b2ba53c6b0528f79ed16164eafd746 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 17 Jun 2022 13:22:32 -0600 Subject: [PATCH 58/77] match line-item syntax, alphabetize --- docs/source/Links.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/source/Links.md b/docs/source/Links.md index 89cfd176c8..75f3cfe2ba 100644 --- a/docs/source/Links.md +++ b/docs/source/Links.md @@ -24,15 +24,15 @@ channel](https://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUm ## Third-party Evennia tools +- [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional coloration for Evennia unit-test output. - [Evennia-docker](https://github.com/gtaylor/evennia-docker) - Evennia in a [Docker container](https://www.docker.com/) for quick install and deployment in just a few commands. - [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your website. -- [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional coloration for Evennia unit-test output. - [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for telnet/web). -- [The world of Cool battles sources](https://github.com/FlutterSprite/coolbattles) - Open source turn-based battle system for an older version of Evennia. - [nextRPI](https://github.com/cluebyte/nextrpi) - A github project for making a toolbox for people to make [RPI](https://www.topmudsites.com/forums/showthread.php?t=4804)-style Evennia games. - [Unreal Engine Evennia plugin](https://www.unrealengine.com/marketplace/en-US/slug/evennia-plugin) an in-progress Unreal plugin for integrating Evennia with Epic Games' Unreal Engine. - [vim-evennia](https://github.com/amfl/vim-evennia) - A mode for editing batch-build files (`.ev`) files in the [vim](https://www.vim.org/) text editor (Emacs users can use [evennia- mode.el](https://github.com/evennia/evennia/blob/master/evennia/utils/evennia-mode.el)). +- [The world of Cool battles sources](https://github.com/FlutterSprite/coolbattles) - Open source turn-based battle system for an older version of Evennia. - [Other Evennia-related repos on github](https://github.com/search?p=1&q=evennia) ---- @@ -41,9 +41,9 @@ mode.el](https://github.com/evennia/evennia/blob/master/evennia/utils/evennia-mo ### Code bases - [Arx sources](https://github.com/Arx-Game/arxcode) - Open-source code release of the very popular [Arx](https://play.arxmush.org/) Evennia game. [Here are instructions for installing](https://www.evennia.com/docs/1.0-dev/Howtos/Arxcode-Installation.html) +- [Encarnia sources](https://github.com/whitehorse-io/encarnia) - An open-sourced game dir for an older version of Evennia with things like races, combat etc. [Summary here](https://www.reddit.com/r/MUD/comments/6z6s3j/encarnia_an_evennia_python_mud_code_base_with/). - [The dark net/March Hare MUD](https://github.com/thedarknet/evennia) from the 2019 [DEF CON 27](https://www.defcon.org/html/defcon-27/dc-27-index.html) hacker conference in Paris. This is an Evennia game dir with batchcode to build the custom _Hackers_ style cyberspace zone with puzzles and challenges [used during the conference](https://dcdark.net/home#). - [Muddery](https://github.com/muddery/muddery) - A mud framework under development, based on an older fork of Evennia. It has some specific design goals for building and extending the game based on input files. -- [Encarnia sources](https://github.com/whitehorse-io/encarnia) - An open-sourced game dir for an older version of Evennia with things like races, combat etc. [Summary here](https://www.reddit.com/r/MUD/comments/6z6s3j/encarnia_an_evennia_python_mud_code_base_with/). ### Other @@ -62,24 +62,23 @@ mode.el](https://github.com/evennia/evennia/blob/master/evennia/utils/evennia-mo ### Informational -- [Mud-dev wiki](http://mud-dev.wikidot.com/) - A (very) slowly growing resource on MUD creation. -- [Mud Client/Server Interaction](http://cryosphere.net/mud-protocol.html) - A page on classic MUD telnet protocols. -- [Mud Tech's fun/cool but ...](https://gc-taylor.com/blog/2013/01/08/mud-tech-funcool-dont-forget-ship-damned-thing/) - Greg Taylor gives good advice on mud design. -- [Lost Library of MOO](https://www.hayseed.net/MOO/) - Archive of scientific articles on mudding (in particular moo). -- [Nick Gammon's hints thread](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) - Contains a very useful list of things to think about when starting your new MUD. -- Mud Dev mailing list archive ([mirror](http://www.disinterest.org/resource/MUD-Dev/)) - Influential mailing list active 1996-2004. Advanced game design discussions. -- [Raph Koster's laws of game design](https://www.raphkoster.com/games/laws-of-online-world-design/the-laws-of-online-world-design/) - thought-provoking guidelines and things to think about when designing a virtual multiplayer world (Raph is known for *Ultima Online* among other things). - [Imaginary Realities unofficial archive](http://tharsis-gate.org/articles/imaginary.html) - An e-magazine on game and MUD design that has several articles about Evennia. +- [Lost Library of MOO](https://www.hayseed.net/MOO/) - Archive of scientific articles on mudding (in particular moo). +- [Mud Client/Server Interaction](http://cryosphere.net/mud-protocol.html) - A page on classic MUD telnet protocols. +- [Mud-dev wiki](http://mud-dev.wikidot.com/) - A (very) slowly growing resource on MUD creation. +- [Mud Tech's fun/cool but ...](https://gc-taylor.com/blog/2013/01/08/mud-tech-funcool-dont-forget-ship-damned-thing/) - Greg Taylor gives good advice on mud design. +- [Nick Gammon's hints thread](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) - Contains a very useful list of things to think about when starting your new MUD. +- [Raph Koster's laws of game design](https://www.raphkoster.com/games/laws-of-online-world-design/the-laws-of-online-world-design/) - thought-provoking guidelines and things to think about when designing a virtual multiplayer world (Raph is known for *Ultima Online* among other things). ### Community -- [Grapevine MUD community and chat network](https://grapevine.haus/) +- [Grapevine](https://grapevine.haus/) - MUD listings and inter-game chat network - [MUD Coder's Guild](https://mudcoders.com/) - A blog and [associated Slack channel](https://slack.mudcoders.com/) with discussions on MUD development. +- [MudBytes](http://www.mudbytes.net/) - MUD listing and forums +- [MudConnector](http://www.mudconnect.com/) - MUD listing and forums +- [MudLab](http://mudlab.org/) - MUD design discussion forum - [MuSoapbox](https://musoapbox.net/) - MU* forum mainly focused on MUSH-type gaming. -- [MudLab](http://mudlab.org/) - Mud design discussion forum -- [MudConnector](http://www.mudconnect.com/) - Mud listing and forums -- [MudBytes](http://www.mudbytes.net/) - Mud listing and forums -- [Top Mud Sites](http://www.topmudsites.com/) - Mud listing and forums +- [Top Mud Sites](http://www.topmudsites.com/) - MUD listing and forums ---- From 8c612fd3e3214d5caf0cc6eca131866c36f3d233 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 17 Jun 2022 13:25:09 -0600 Subject: [PATCH 59/77] add discord relay --- docs/source/Links.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/Links.md b/docs/source/Links.md index 75f3cfe2ba..d5204156e3 100644 --- a/docs/source/Links.md +++ b/docs/source/Links.md @@ -24,6 +24,7 @@ channel](https://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUm ## Third-party Evennia tools +- [Discord relay](https://github.com/InspectorCaracal/evennia-things/tree/main/discord_relay) - Two-way chat relays between Evennia channels and Discord channels. - [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional coloration for Evennia unit-test output. - [Evennia-docker](https://github.com/gtaylor/evennia-docker) - Evennia in a [Docker container](https://www.docker.com/) for quick install and deployment in just a few commands. - [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your website. From 1e66de58aba8895bad9e00ddab40a50a6ea7ed7b Mon Sep 17 00:00:00 2001 From: Hellgheast Date: Mon, 20 Jun 2022 18:45:20 +0200 Subject: [PATCH 60/77] Update Building-Quickstart.md Changing the description command to desc (which exist in dev-1.0) --- .../Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md index 3407ece825..f4fa3f3821 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md @@ -115,7 +115,7 @@ Try to `look` at the box to see the (default) description. The description you get is not very exciting. Let's add some flavor. - describe box = This is a large and very heavy box. + desc box = This is a large and very heavy box. If you try the `get` command we will pick up the box. So far so good, but if we really want this to be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent From 4fdbae04c22739e571f115cf7f5cf11444d3c5b1 Mon Sep 17 00:00:00 2001 From: Hellgheast Date: Mon, 20 Jun 2022 19:05:18 +0200 Subject: [PATCH 61/77] doc: Fixing bodyfunctions script call --- .../Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md index f4fa3f3821..49748d4812 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md @@ -155,10 +155,10 @@ later, in the [Commands tutorial](./Adding-Commands.md). [Scripts](../../../Components/Scripts.md) are powerful out-of-character objects useful for many "under the hood" things. One of their optional abilities is to do things on a timer. To try out a first script, let's put one -on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py` +on ourselves. There is an example script in `evennia/contrib/tutorials/bodyfunctions/bodyfunctions.py` that is called `BodyFunctions`. To add this to us we will use the `script` command: - script self = tutorial_examples.bodyfunctions.BodyFunctions + script self = tutorials.bodyfunctions.BodyFunctions This string will tell Evennia to dig up the Python code at the place we indicate. It already knows to look in the `contrib/` folder, so we don't have to give the full path. @@ -179,7 +179,7 @@ output every time it fires. When you are tired of your character's "insights", kill the script with - script/stop self = tutorial_examples.bodyfunctions.BodyFunctions + script/stop self = tutorials.bodyfunctions.BodyFunctions You create your own scripts in Python, outside the game; the path you give to `script` is literally the Python path to your script file. The [Scripts](../../../Components/Scripts.md) page explains more details. From 87402e1c8af7ebdc8452fe7c1ee07f794ae8aeda Mon Sep 17 00:00:00 2001 From: Hellgheast Date: Mon, 20 Jun 2022 19:18:16 +0200 Subject: [PATCH 62/77] doc: Correction of red_button calls --- .../Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md index 49748d4812..4140edff96 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md @@ -199,7 +199,7 @@ named simply `Object`. Let's create an object that is a little more interesting. Let's make us one of _those_! - create/drop button:tutorial_examples.red_button.RedButton + create/drop button:tutorials.red_button.RedButton The same way we did with the Script Earler, we specify a "Python-path" to the Python code we want Evennia to use for creating the object. There you go - one red button. From ac3830fdf82a9bd6aa92902233411b5ab7797db0 Mon Sep 17 00:00:00 2001 From: Hellgheast Date: Mon, 20 Jun 2022 19:38:20 +0200 Subject: [PATCH 63/77] doc : sethelp removed the /add switch --- .../Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md index 4140edff96..c1f8daa202 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Building-Quickstart.md @@ -301,7 +301,7 @@ The Command-help is something you modify in Python code. We'll get to that when add Commands. But you can also add regular help entries, for example to explain something about the history of your game world: - sethelp/add History = At the dawn of time ... + sethelp History = At the dawn of time ... You will now find your new `History` entry in the `help` list and read your help-text with `help History`. From c10121bde21178fafce9a0f424b5ad8353c8f0ac Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 20 Jun 2022 13:32:33 -0600 Subject: [PATCH 64/77] remove IRC, update docker-compose --- docs/source/Links.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/Links.md b/docs/source/Links.md index d5204156e3..088ba072c6 100644 --- a/docs/source/Links.md +++ b/docs/source/Links.md @@ -17,19 +17,17 @@ offline in html, PDF or epub formats. - [Evennia Game Index](http://games.evennia.com/) - An automated listing of Evennia games. - [Evennia official Discord channel](https://discord.gg/AJJpcRUhtF) -- [Evennia official IRC -channel](https://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb) ( #evennia at [irc.freenode.net:6667](http://irc.freenode.net:6667/) ) - [Evennia official forums](https://github.com/evennia/evennia/discussions) on Github Discussions. - [Evennia subreddit](https://www.reddit.com/r/Evennia/) ## Third-party Evennia tools - [Discord relay](https://github.com/InspectorCaracal/evennia-things/tree/main/discord_relay) - Two-way chat relays between Evennia channels and Discord channels. +- [docker-compose for Evennia](https://github.com/gtaylor/evennia-docker) - A quick-install setup for running Evennia in a [Docker container](https://www.docker.com/). (See [the official Evennia docs](https://www.evennia.com/docs/latest/Running-Evennia-in-Docker.html) for more details on running Evennia with Docker.) - [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional coloration for Evennia unit-test output. -- [Evennia-docker](https://github.com/gtaylor/evennia-docker) - Evennia in a [Docker container](https://www.docker.com/) for quick install and deployment in just a few commands. - [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your website. -- [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for telnet/web). - [nextRPI](https://github.com/cluebyte/nextrpi) - A github project for making a toolbox for people to make [RPI](https://www.topmudsites.com/forums/showthread.php?t=4804)-style Evennia games. +- [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for telnet/web). - [Unreal Engine Evennia plugin](https://www.unrealengine.com/marketplace/en-US/slug/evennia-plugin) an in-progress Unreal plugin for integrating Evennia with Epic Games' Unreal Engine. - [vim-evennia](https://github.com/amfl/vim-evennia) - A mode for editing batch-build files (`.ev`) files in the [vim](https://www.vim.org/) text editor (Emacs users can use [evennia- mode.el](https://github.com/evennia/evennia/blob/master/evennia/utils/evennia-mode.el)). From 66b3770f6e7a318a23b51b33677ba4384e80ad26 Mon Sep 17 00:00:00 2001 From: Hellgheast Date: Tue, 21 Jun 2022 11:28:21 +0200 Subject: [PATCH 65/77] Testing to fix the jinja2 problem --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index ab4da74f57..7b2280b898 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ autobahn >= 20.7.1, < 21.0.0 lunr == 0.6.0 simpleeval <= 1.0 uritemplate == 4.1.1 +Jinja2<3.1 # try to resolve dependency issue in py3.7 attrs >= 19.2.0 From 2922c0c7d9cee2474b3bcb2068a355159d884302 Mon Sep 17 00:00:00 2001 From: Avalon Hope <96471977+avalonhope@users.noreply.github.com> Date: Tue, 21 Jun 2022 12:23:49 +0100 Subject: [PATCH 66/77] Update test_search.py --- evennia/utils/tests/test_search.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/evennia/utils/tests/test_search.py b/evennia/utils/tests/test_search.py index 6bacfb4cb3..ae9b9a9694 100644 --- a/evennia/utils/tests/test_search.py +++ b/evennia/utils/tests/test_search.py @@ -21,9 +21,16 @@ class TestSearch(EvenniaTest): self.assertEqual(script.key, found[0].key, errors) def test_search_script_tag_wrong_category(self): - """Check that a script can be found by its tag and category.""" + """Check that a script cannot be found by the wrong category.""" script, errors = DefaultScript.create("a-script") script.tags.add("a-tag", category="a-category") found = search_script_tag("a-tag", category="wrong-category") self.assertEqual(len(found), 0, errors) + + def test_search_script_tag_wrong(self): + """Check that a script cannot be found by the wrong tag.""" + script, errors = DefaultScript.create("a-script") + script.tags.add("a-tag", category="a-category") + found = search_script_tag("wrong-tag", category="a-category") + self.assertEqual(len(found), 0, errors) From a05ec09f51bb0d47998a93a848deb28349515ae7 Mon Sep 17 00:00:00 2001 From: "Cory F. Cohen" Date: Tue, 21 Jun 2022 21:07:11 -0400 Subject: [PATCH 67/77] Fix various typos in several files. All are in comments and docstrings, and none should be controversial in any way (e.g. British versus American spelling). --- evennia/accounts/accounts.py | 10 +++++----- evennia/commands/default/building.py | 6 +++--- evennia/commands/default/comms.py | 2 +- evennia/commands/default/help.py | 2 +- evennia/game_template/world/README.md | 2 +- evennia/game_template/world/prototypes.py | 4 ++-- evennia/server/sessionhandler.py | 2 +- evennia/utils/evmore.py | 4 ++-- evennia/utils/utils.py | 12 ++++++------ 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index f779a4281f..4b566c7238 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -678,7 +678,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): typeclass (str, optional): Typeclass to use for this character. If not given, use settings.BASE_CHARACTER_TYPECLASS. permissions (list, optional): If not given, use the account's permissions. - ip (str, optiona): The client IP creating this character. Will fall back to the + ip (str, optional): The client IP creating this character. Will fall back to the one stored for the account if not given. kwargs (any): Other kwargs will be used in the create_call. Returns: @@ -955,7 +955,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): kwargs (any): Other keyword arguments will be added to the found command object instance as variables before it executes. This is unused by default Evennia but may be - used to set flags and change operating paramaters for + used to set flags and change operating parameters for commands at run-time. """ @@ -1433,7 +1433,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key)) if _MULTISESSION_MODE == 0: # in this mode we should have only one character available. We - # try to auto-connect to our last conneted object, if any + # try to auto-connect to our last connected object, if any try: self.puppet_object(session, self.db._last_puppet) except RuntimeError: @@ -1460,7 +1460,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ Called by the login process if a user account is targeted correctly but provided with an invalid password. By default it does nothing, - but exists to be overriden. + but exists to be overridden. Args: session (session): Session logging in. @@ -1703,7 +1703,7 @@ class DefaultGuest(DefaultAccount): Gets or creates a Guest account object. Keyword Args: - ip (str, optional): IP address of requestor; used for ban checking, + ip (str, optional): IP address of requester; used for ban checking, throttling and logging Returns: diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 582c6434e4..56ee7b033d 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1071,7 +1071,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS): exitname, backshort = self.directions[exitshort] backname = self.directions[backshort][0] - # if we recieved a typeclass for the exit, add it to the alias(short name) + # if we received a typeclass for the exit, add it to the alias(short name) if ":" in self.lhs: # limit to only the first : character exit_typeclass = ":" + self.lhs.split(":", 1)[-1] @@ -1665,7 +1665,7 @@ class CmdSetAttribute(ObjManipCommand): def split_nested_attr(self, attr): """ Yields tuples of (possible attr name, nested keys on that attr). - For performance, this is biased to the deepest match, but allows compatability + For performance, this is biased to the deepest match, but allows compatibility with older attrs that might have been named with `[]`'s. > list(split_nested_attr("nested['asdf'][0]")) @@ -4031,7 +4031,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): ) return try: - # we homogenize the protoype first, to be more lenient with free-form + # we homogenize the prototype first, to be more lenient with free-form protlib.validate_prototype(protlib.homogenize_prototype(prototype)) except RuntimeError as err: self.caller.msg(str(err)) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index b150bdaf84..6552e5c745 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -1818,7 +1818,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS): class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS): """ - Link an Evennia channel to an exteral Grapevine channel + Link an Evennia channel to an external Grapevine channel Usage: grapevine2chan[/switches] = diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 3ac6c4924a..e70fda3968 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -138,7 +138,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): click_topics=True, ): """This visually formats the help entry. - This method can be overriden to customize the way a help + This method can be overridden to customize the way a help entry is displayed. Args: diff --git a/evennia/game_template/world/README.md b/evennia/game_template/world/README.md index 0f3862dad4..0e4a2fe18f 100644 --- a/evennia/game_template/world/README.md +++ b/evennia/game_template/world/README.md @@ -1,6 +1,6 @@ # world/ -This folder is meant as a miscellanous folder for all that other stuff +This folder is meant as a miscellaneous folder for all that other stuff related to the game. Code which are not commands or typeclasses go here, like custom economy systems, combat code, batch-files etc. diff --git a/evennia/game_template/world/prototypes.py b/evennia/game_template/world/prototypes.py index 04aba091f3..8a05ed5f6c 100644 --- a/evennia/game_template/world/prototypes.py +++ b/evennia/game_template/world/prototypes.py @@ -28,7 +28,7 @@ Possible keywords are: - `prototype_key` - the name of the prototype. This is required for db-prototypes, for module-prototypes, the global variable name of the dict is used instead - `prototype_parent` - string pointing to parent prototype if any. Prototype inherits - in a similar way as classes, with children overriding values in their partents. + in a similar way as classes, with children overriding values in their parents. - `key` - string, the main object identifier. - `typeclass` - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`. - `location` - this should be a valid object or #dbref. @@ -42,7 +42,7 @@ Possible keywords are: of the shorter forms, defaults are used for the rest. - `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`. - Any other keywords are interpreted as Attributes with no category or lock. - These will internally be added to `attrs` (eqivalent to `(attrname, value)`. + These will internally be added to `attrs` (equivalent to `(attrname, value)`. See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info. diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 7ccb302720..5e1bcf0830 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -683,7 +683,7 @@ class ServerSessionHandler(SessionHandler): Get a unique list of connected and logged-in Accounts. Returns: - accounts (list): All conected Accounts (which may be fewer than the + accounts (list): All connected Accounts (which may be fewer than the amount of Sessions due to multi-playing). """ diff --git a/evennia/utils/evmore.py b/evennia/utils/evmore.py index 9129db1298..da8f04ad5a 100644 --- a/evennia/utils/evmore.py +++ b/evennia/utils/evmore.py @@ -181,7 +181,7 @@ class EvMore(object): justify (bool, optional): If set, auto-justify long lines. This must be turned off for fixed-width or formatted output, like tables. It's force-disabled if `inp` is an EvTable. - justify_kwargs (dict, optional): Keywords for the justifiy function. Used only + justify_kwargs (dict, optional): Keywords for the justify function. Used only if `justify` is True. If this is not set, default arguments will be used. exit_on_lastpage (bool, optional): If reaching the last page without the page being completely filled, exit pager immediately. If unset, @@ -507,7 +507,7 @@ class EvMore(object): def page_formatter(self, page): """ Page formatter. Every page passes through this method. Override - it to customize behvaior per-page. A common use is to generate a new + it to customize behavior per-page. A common use is to generate a new EvTable for every page (this is more efficient than to generate one huge EvTable across many pages and feed it into EvMore all at once). diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 10f8073e31..b25e8da447 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -819,7 +819,7 @@ def latinify(string, default="?", pure_ascii=False): This is used as a last resort when normal encoding does not work. Arguments: - string (str): A string to convert to 'safe characters' convertable + string (str): A string to convert to 'safe characters' convertible to an latin-1 bytestring later. default (str, optional): Characters resisting mapping will be replaced with this character or string. The intent is to apply an encode operation @@ -1078,7 +1078,7 @@ def delay(timedelay, callback, *args, **kwargs): Keep in mind that persistent tasks arguments and callback should not use memory references. If persistent is set to True the delay function will return an int - which is the task's id itended for use with TASK_HANDLER's do_task + which is the task's id intended for use with TASK_HANDLER's do_task and remove methods. All persistent tasks whose time delays have passed will be called on server startup. @@ -1531,12 +1531,12 @@ def class_from_module(path, defaultpaths=None, fallback=None): defaultpaths (iterable, optional): If a direct import from `path` fails, try subsequent imports by prepending those paths to `path`. fallback (str): If all other attempts fail, use this path as a fallback. - This is intended as a last-resport. In the example of Evennia + This is intended as a last-resort. In the example of Evennia loading, this would be a path to a default parent class in the evennia repo itself. Returns: - class (Class): An uninstatiated class recovered from path. + class (Class): An uninstantiated class recovered from path. Raises: ImportError: If all loading failed. @@ -1675,7 +1675,7 @@ def string_partial_matching(alternatives, inp, ret_index=True): Matching is made from the start of each subword in each alternative. Case is not important. So e.g. "bi sh sw" or just "big" or "shiny" or "sw" will match "Big shiny sword". Scoring is - done to allow to separate by most common demoninator. You will get + done to allow to separate by most common denominator. You will get multiple matches returned if appropriate. Args: @@ -1749,7 +1749,7 @@ def format_table(table, extra_space=1): ftable = format_table([[1,2,3], [4,5,6]]) string = "" - for ir, row in enumarate(ftable): + for ir, row in enumerate(ftable): if ir == 0: # make first row white string += "\\n|w" + "".join(row) + "|n" From 7321173d3b16bc46bbcc7f898b086e3593d9b83e Mon Sep 17 00:00:00 2001 From: "Cory F. Cohen" Date: Tue, 21 Jun 2022 21:13:06 -0400 Subject: [PATCH 68/77] A couple more typos that are only on develop. --- evennia/commands/default/building.py | 2 +- evennia/commands/default/help.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 56ee7b033d..cecc701fe4 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2832,7 +2832,7 @@ class CmdExamine(ObjManipCommand): objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj) objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset) objdata[ - f"Commands vailable to {obj.key} (result of Merged Cmdset(s))" + f"Commands available to {obj.key} (result of Merged Cmdset(s))" ] = self.format_current_cmds(obj, current_cmdset) if self.object_type == "script": objdata["Description"] = self.format_script_desc(obj) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index e70fda3968..bdbc62d2ad 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -67,7 +67,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): help // ... Use the 'help' command alone to see an index of all help topics, organized - by category.eSome big topics may offer additional sub-topics. + by category. Some big topics may offer additional sub-topics. """ From c619d03639c1981d1b9a1c072012f4c620d43bc3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 22 Jun 2022 08:25:17 +0200 Subject: [PATCH 69/77] Limit jinja2 version for sphinx error --- docs/requirements.txt | 1 + requirements.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 683117213d..9f5d80a334 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,6 +3,7 @@ sphinx==3.2.1 myst-parser==0.15.2 myst-parser[linkify]==0.15.2 +Jinja2 < 3.1 # sphinx-multiversion with evennia fixes git+https://github.com/evennia/sphinx-multiversion.git@evennia-mods#egg=sphinx-multiversion diff --git a/requirements.txt b/requirements.txt index 7b2280b898..a08725076e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ autobahn >= 20.7.1, < 21.0.0 lunr == 0.6.0 simpleeval <= 1.0 uritemplate == 4.1.1 -Jinja2<3.1 +Jinja2 < 3.1 # try to resolve dependency issue in py3.7 attrs >= 19.2.0 From f8c6ca2797eefec6a013c802d5185842893b9cbb Mon Sep 17 00:00:00 2001 From: Avalon Hope <96471977+avalonhope@users.noreply.github.com> Date: Wed, 22 Jun 2022 09:38:57 +0100 Subject: [PATCH 70/77] Unit test for search_script_attribute --- evennia/utils/tests/test_search.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/evennia/utils/tests/test_search.py b/evennia/utils/tests/test_search.py index ae9b9a9694..f9ab2e2e4a 100644 --- a/evennia/utils/tests/test_search.py +++ b/evennia/utils/tests/test_search.py @@ -1,6 +1,6 @@ from evennia.scripts.scripts import DefaultScript from evennia.utils.test_resources import EvenniaTest -from evennia.utils.search import search_script_tag +from evennia.utils.search import search_script_attribute, search _script_tag class TestSearch(EvenniaTest): @@ -33,4 +33,21 @@ class TestSearch(EvenniaTest): script.tags.add("a-tag", category="a-category") found = search_script_tag("wrong-tag", category="a-category") self.assertEqual(len(found), 0, errors) + + def test_search_script_attribute(self): + """Check that a script can be found by its attributes.""" + script, errors = DefaultScript.create("a-script") + script.db.an_attribute = "some value" + found = search_script_attribute(key="an_attribute", value="some value") + self.assertEqual(len(found), 1, errors) + self.assertEqual(script.key, found[0].key, errors) + + def test_search_script_attribute_wrong(self): + """Check that a script can be found by its attributes.""" + script, errors = DefaultScript.create("a-script") + script.db.an_attribute = "some value" + found = search_script_attribute(key="an_attribute", value="wrong value") + self.assertEqual(len(found), 1, errors) + self.assertEqual(script.key, found[0].key, errors) + From 5db14e8c4de810e762d466034cf958558d212670 Mon Sep 17 00:00:00 2001 From: Avalon Hope <96471977+avalonhope@users.noreply.github.com> Date: Wed, 22 Jun 2022 09:39:45 +0100 Subject: [PATCH 71/77] Update test_search.py --- evennia/utils/tests/test_search.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/evennia/utils/tests/test_search.py b/evennia/utils/tests/test_search.py index f9ab2e2e4a..e1380f5402 100644 --- a/evennia/utils/tests/test_search.py +++ b/evennia/utils/tests/test_search.py @@ -43,11 +43,9 @@ class TestSearch(EvenniaTest): self.assertEqual(script.key, found[0].key, errors) def test_search_script_attribute_wrong(self): - """Check that a script can be found by its attributes.""" + """Check that a script cannot be found by wrong value of its attributes.""" script, errors = DefaultScript.create("a-script") script.db.an_attribute = "some value" found = search_script_attribute(key="an_attribute", value="wrong value") self.assertEqual(len(found), 1, errors) self.assertEqual(script.key, found[0].key, errors) - - From 975c38c0af4102f9e32f6f29bd41546d1128218e Mon Sep 17 00:00:00 2001 From: Avalon Hope <96471977+avalonhope@users.noreply.github.com> Date: Wed, 22 Jun 2022 10:06:31 +0100 Subject: [PATCH 72/77] Update test_search.py --- evennia/utils/tests/test_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/utils/tests/test_search.py b/evennia/utils/tests/test_search.py index e1380f5402..55b57d0add 100644 --- a/evennia/utils/tests/test_search.py +++ b/evennia/utils/tests/test_search.py @@ -1,6 +1,6 @@ from evennia.scripts.scripts import DefaultScript from evennia.utils.test_resources import EvenniaTest -from evennia.utils.search import search_script_attribute, search _script_tag +from evennia.utils.search import search_script_attribute, search_script_tag class TestSearch(EvenniaTest): From 10a543d0239eea84c42e5449e5c89267bee8491e Mon Sep 17 00:00:00 2001 From: Avalon Hope <96471977+avalonhope@users.noreply.github.com> Date: Wed, 22 Jun 2022 10:30:13 +0100 Subject: [PATCH 73/77] Update test_search.py --- evennia/utils/tests/test_search.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/evennia/utils/tests/test_search.py b/evennia/utils/tests/test_search.py index 55b57d0add..63515d6754 100644 --- a/evennia/utils/tests/test_search.py +++ b/evennia/utils/tests/test_search.py @@ -47,5 +47,4 @@ class TestSearch(EvenniaTest): script, errors = DefaultScript.create("a-script") script.db.an_attribute = "some value" found = search_script_attribute(key="an_attribute", value="wrong value") - self.assertEqual(len(found), 1, errors) - self.assertEqual(script.key, found[0].key, errors) + self.assertEqual(len(found), 0, errors) From 42718e21d61d0c3260b6d871b18c76f77400704f Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Tue, 28 Jun 2022 11:53:46 -0600 Subject: [PATCH 74/77] include aliases in candidates --- 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 69f29280c6..d8fbe89af0 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -350,7 +350,8 @@ 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.extend([(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()]) + candidate_map.append((obj, obj.key) + candidate_map.extend([(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". From 5140cf36c2cdb22450dd48e2552ad9fb56154626 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Tue, 28 Jun 2022 12:00:51 -0600 Subject: [PATCH 75/77] 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 d8fbe89af0..f50ad88326 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -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.append((obj, obj.key) + candidate_map.append((obj, obj.key)) candidate_map.extend([(obj, alias) for alias in obj.aliases.all()]) # escape mapping syntax on the form {#id} if it exists already in emote, From c82ad50f9eb383a779400d19ecad3d4548ae98aa Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 3 Jul 2022 11:11:52 +0200 Subject: [PATCH 76/77] Set echo option, probably no effect --- evennia/server/portal/telnet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 4d25696c50..5d764e34f7 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -226,6 +226,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): or option == naws.NAWS or option == MCCP or option == mssp.MSSP + or option == ECHO or option == suppress_ga.SUPPRESS_GA ) @@ -236,6 +237,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): or option == naws.NAWS or option == MCCP or option == mssp.MSSP + or option == ECHO or option == suppress_ga.SUPPRESS_GA ) From 58148839491b0548e6fee3ceb10d78bdc9c2d86b Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 5 Jul 2022 19:24:53 +0200 Subject: [PATCH 77/77] Add vale linter config for docs --- docs/source/.vale.ini | 8 + docs/source/.vale/Vocab/docs/accept.txt | 19 + docs/source/.vale/Vocab/docs/reject.txt | 0 docs/source/.vale/write-good/Cliches.yml | 702 +++++++++++++++++++++ docs/source/.vale/write-good/E-Prime.yml | 32 + docs/source/.vale/write-good/Illusions.yml | 11 + docs/source/.vale/write-good/Passive.yml | 183 ++++++ docs/source/.vale/write-good/README.md | 27 + docs/source/.vale/write-good/So.yml | 5 + docs/source/.vale/write-good/ThereIs.yml | 6 + docs/source/.vale/write-good/TooWordy.yml | 221 +++++++ docs/source/.vale/write-good/Weasel.yml | 207 ++++++ docs/source/.vale/write-good/meta.json | 4 + docs/source/Contributing.md | 6 +- 14 files changed, 1428 insertions(+), 3 deletions(-) create mode 100644 docs/source/.vale.ini create mode 100644 docs/source/.vale/Vocab/docs/accept.txt create mode 100644 docs/source/.vale/Vocab/docs/reject.txt create mode 100644 docs/source/.vale/write-good/Cliches.yml create mode 100644 docs/source/.vale/write-good/E-Prime.yml create mode 100644 docs/source/.vale/write-good/Illusions.yml create mode 100644 docs/source/.vale/write-good/Passive.yml create mode 100644 docs/source/.vale/write-good/README.md create mode 100644 docs/source/.vale/write-good/So.yml create mode 100644 docs/source/.vale/write-good/ThereIs.yml create mode 100644 docs/source/.vale/write-good/TooWordy.yml create mode 100644 docs/source/.vale/write-good/Weasel.yml create mode 100644 docs/source/.vale/write-good/meta.json diff --git a/docs/source/.vale.ini b/docs/source/.vale.ini new file mode 100644 index 0000000000..5bf3f4ecf5 --- /dev/null +++ b/docs/source/.vale.ini @@ -0,0 +1,8 @@ +StylesPath = .vale + +Vocab = docs +Packages = write-good +MinAlertLevel = error + +[*.md] +BasedOnStyles = Vale, write-good diff --git a/docs/source/.vale/Vocab/docs/accept.txt b/docs/source/.vale/Vocab/docs/accept.txt new file mode 100644 index 0000000000..05b0ba7c42 --- /dev/null +++ b/docs/source/.vale/Vocab/docs/accept.txt @@ -0,0 +1,19 @@ +Evennia +Pastebin +[Cc]ontrib(s)* +Patreon +[Rr]epo(s)* +(?i)readme +[Ss]ubfolder(s)* +[Dd]ev(s)* +Github +[Dd]ocstring(s)* +[Mm]ygame(s)* +[Gg]amedir(s)* +[Vv]irtualenv(s)* +Python +API +[Tt]ypeclass(es)*? +[Bb]ullet point(s)* +CommonMark +[Pp]reparser(s)* diff --git a/docs/source/.vale/Vocab/docs/reject.txt b/docs/source/.vale/Vocab/docs/reject.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/source/.vale/write-good/Cliches.yml b/docs/source/.vale/write-good/Cliches.yml new file mode 100644 index 0000000000..c95314387b --- /dev/null +++ b/docs/source/.vale/write-good/Cliches.yml @@ -0,0 +1,702 @@ +extends: existence +message: "Try to avoid using clichés like '%s'." +ignorecase: true +level: warning +tokens: + - a chip off the old block + - a clean slate + - a dark and stormy night + - a far cry + - a fine kettle of fish + - a loose cannon + - a penny saved is a penny earned + - a tough row to hoe + - a word to the wise + - ace in the hole + - acid test + - add insult to injury + - against all odds + - air your dirty laundry + - all fun and games + - all in a day's work + - all talk, no action + - all thumbs + - all your eggs in one basket + - all's fair in love and war + - all's well that ends well + - almighty dollar + - American as apple pie + - an axe to grind + - another day, another dollar + - armed to the teeth + - as luck would have it + - as old as time + - as the crow flies + - at loose ends + - at my wits end + - avoid like the plague + - babe in the woods + - back against the wall + - back in the saddle + - back to square one + - back to the drawing board + - bad to the bone + - badge of honor + - bald faced liar + - ballpark figure + - banging your head against a brick wall + - baptism by fire + - barking up the wrong tree + - bat out of hell + - be all and end all + - beat a dead horse + - beat around the bush + - been there, done that + - beggars can't be choosers + - behind the eight ball + - bend over backwards + - benefit of the doubt + - bent out of shape + - best thing since sliced bread + - bet your bottom dollar + - better half + - better late than never + - better mousetrap + - better safe than sorry + - between a rock and a hard place + - beyond the pale + - bide your time + - big as life + - big cheese + - big fish in a small pond + - big man on campus + - bigger they are the harder they fall + - bird in the hand + - bird's eye view + - birds and the bees + - birds of a feather flock together + - bit the hand that feeds you + - bite the bullet + - bite the dust + - bitten off more than he can chew + - black as coal + - black as pitch + - black as the ace of spades + - blast from the past + - bleeding heart + - blessing in disguise + - blind ambition + - blind as a bat + - blind leading the blind + - blood is thicker than water + - blood sweat and tears + - blow off steam + - blow your own horn + - blushing bride + - boils down to + - bolt from the blue + - bone to pick + - bored stiff + - bored to tears + - bottomless pit + - boys will be boys + - bright and early + - brings home the bacon + - broad across the beam + - broken record + - brought back to reality + - bull by the horns + - bull in a china shop + - burn the midnight oil + - burning question + - burning the candle at both ends + - burst your bubble + - bury the hatchet + - busy as a bee + - by hook or by crook + - call a spade a spade + - called onto the carpet + - calm before the storm + - can of worms + - can't cut the mustard + - can't hold a candle to + - case of mistaken identity + - cat got your tongue + - cat's meow + - caught in the crossfire + - caught red-handed + - checkered past + - chomping at the bit + - cleanliness is next to godliness + - clear as a bell + - clear as mud + - close to the vest + - cock and bull story + - cold shoulder + - come hell or high water + - cool as a cucumber + - cool, calm, and collected + - cost a king's ransom + - count your blessings + - crack of dawn + - crash course + - creature comforts + - cross that bridge when you come to it + - crushing blow + - cry like a baby + - cry me a river + - cry over spilt milk + - crystal clear + - curiosity killed the cat + - cut and dried + - cut through the red tape + - cut to the chase + - cute as a bugs ear + - cute as a button + - cute as a puppy + - cuts to the quick + - dark before the dawn + - day in, day out + - dead as a doornail + - devil is in the details + - dime a dozen + - divide and conquer + - dog and pony show + - dog days + - dog eat dog + - dog tired + - don't burn your bridges + - don't count your chickens + - don't look a gift horse in the mouth + - don't rock the boat + - don't step on anyone's toes + - don't take any wooden nickels + - down and out + - down at the heels + - down in the dumps + - down the hatch + - down to earth + - draw the line + - dressed to kill + - dressed to the nines + - drives me up the wall + - dull as dishwater + - dyed in the wool + - eagle eye + - ear to the ground + - early bird catches the worm + - easier said than done + - easy as pie + - eat your heart out + - eat your words + - eleventh hour + - even the playing field + - every dog has its day + - every fiber of my being + - everything but the kitchen sink + - eye for an eye + - face the music + - facts of life + - fair weather friend + - fall by the wayside + - fan the flames + - feast or famine + - feather your nest + - feathered friends + - few and far between + - fifteen minutes of fame + - filthy vermin + - fine kettle of fish + - fish out of water + - fishing for a compliment + - fit as a fiddle + - fit the bill + - fit to be tied + - flash in the pan + - flat as a pancake + - flip your lid + - flog a dead horse + - fly by night + - fly the coop + - follow your heart + - for all intents and purposes + - for the birds + - for what it's worth + - force of nature + - force to be reckoned with + - forgive and forget + - fox in the henhouse + - free and easy + - free as a bird + - fresh as a daisy + - full steam ahead + - fun in the sun + - garbage in, garbage out + - gentle as a lamb + - get a kick out of + - get a leg up + - get down and dirty + - get the lead out + - get to the bottom of + - get your feet wet + - gets my goat + - gilding the lily + - give and take + - go against the grain + - go at it tooth and nail + - go for broke + - go him one better + - go the extra mile + - go with the flow + - goes without saying + - good as gold + - good deed for the day + - good things come to those who wait + - good time was had by all + - good times were had by all + - greased lightning + - greek to me + - green thumb + - green-eyed monster + - grist for the mill + - growing like a weed + - hair of the dog + - hand to mouth + - happy as a clam + - happy as a lark + - hasn't a clue + - have a nice day + - have high hopes + - have the last laugh + - haven't got a row to hoe + - head honcho + - head over heels + - hear a pin drop + - heard it through the grapevine + - heart's content + - heavy as lead + - hem and haw + - high and dry + - high and mighty + - high as a kite + - hit paydirt + - hold your head up high + - hold your horses + - hold your own + - hold your tongue + - honest as the day is long + - horns of a dilemma + - horse of a different color + - hot under the collar + - hour of need + - I beg to differ + - icing on the cake + - if the shoe fits + - if the shoe were on the other foot + - in a jam + - in a jiffy + - in a nutshell + - in a pig's eye + - in a pinch + - in a word + - in hot water + - in the gutter + - in the nick of time + - in the thick of it + - in your dreams + - it ain't over till the fat lady sings + - it goes without saying + - it takes all kinds + - it takes one to know one + - it's a small world + - it's only a matter of time + - ivory tower + - Jack of all trades + - jockey for position + - jog your memory + - joined at the hip + - judge a book by its cover + - jump down your throat + - jump in with both feet + - jump on the bandwagon + - jump the gun + - jump to conclusions + - just a hop, skip, and a jump + - just the ticket + - justice is blind + - keep a stiff upper lip + - keep an eye on + - keep it simple, stupid + - keep the home fires burning + - keep up with the Joneses + - keep your chin up + - keep your fingers crossed + - kick the bucket + - kick up your heels + - kick your feet up + - kid in a candy store + - kill two birds with one stone + - kiss of death + - knock it out of the park + - knock on wood + - knock your socks off + - know him from Adam + - know the ropes + - know the score + - knuckle down + - knuckle sandwich + - knuckle under + - labor of love + - ladder of success + - land on your feet + - lap of luxury + - last but not least + - last hurrah + - last-ditch effort + - law of the jungle + - law of the land + - lay down the law + - leaps and bounds + - let sleeping dogs lie + - let the cat out of the bag + - let the good times roll + - let your hair down + - let's talk turkey + - letter perfect + - lick your wounds + - lies like a rug + - life's a bitch + - life's a grind + - light at the end of the tunnel + - lighter than a feather + - lighter than air + - like clockwork + - like father like son + - like taking candy from a baby + - like there's no tomorrow + - lion's share + - live and learn + - live and let live + - long and short of it + - long lost love + - look before you leap + - look down your nose + - look what the cat dragged in + - looking a gift horse in the mouth + - looks like death warmed over + - loose cannon + - lose your head + - lose your temper + - loud as a horn + - lounge lizard + - loved and lost + - low man on the totem pole + - luck of the draw + - luck of the Irish + - make hay while the sun shines + - make money hand over fist + - make my day + - make the best of a bad situation + - make the best of it + - make your blood boil + - man of few words + - man's best friend + - mark my words + - meaningful dialogue + - missed the boat on that one + - moment in the sun + - moment of glory + - moment of truth + - money to burn + - more power to you + - more than one way to skin a cat + - movers and shakers + - moving experience + - naked as a jaybird + - naked truth + - neat as a pin + - needle in a haystack + - needless to say + - neither here nor there + - never look back + - never say never + - nip and tuck + - nip it in the bud + - no guts, no glory + - no love lost + - no pain, no gain + - no skin off my back + - no stone unturned + - no time like the present + - no use crying over spilled milk + - nose to the grindstone + - not a hope in hell + - not a minute's peace + - not in my backyard + - not playing with a full deck + - not the end of the world + - not written in stone + - nothing to sneeze at + - nothing ventured nothing gained + - now we're cooking + - off the top of my head + - off the wagon + - off the wall + - old hat + - older and wiser + - older than dirt + - older than Methuselah + - on a roll + - on cloud nine + - on pins and needles + - on the bandwagon + - on the money + - on the nose + - on the rocks + - on the spot + - on the tip of my tongue + - on the wagon + - on thin ice + - once bitten, twice shy + - one bad apple doesn't spoil the bushel + - one born every minute + - one brick short + - one foot in the grave + - one in a million + - one red cent + - only game in town + - open a can of worms + - open and shut case + - open the flood gates + - opportunity doesn't knock twice + - out of pocket + - out of sight, out of mind + - out of the frying pan into the fire + - out of the woods + - out on a limb + - over a barrel + - over the hump + - pain and suffering + - pain in the + - panic button + - par for the course + - part and parcel + - party pooper + - pass the buck + - patience is a virtue + - pay through the nose + - penny pincher + - perfect storm + - pig in a poke + - pile it on + - pillar of the community + - pin your hopes on + - pitter patter of little feet + - plain as day + - plain as the nose on your face + - play by the rules + - play your cards right + - playing the field + - playing with fire + - pleased as punch + - plenty of fish in the sea + - point with pride + - poor as a church mouse + - pot calling the kettle black + - pretty as a picture + - pull a fast one + - pull your punches + - pulling your leg + - pure as the driven snow + - put it in a nutshell + - put one over on you + - put the cart before the horse + - put the pedal to the metal + - put your best foot forward + - put your foot down + - quick as a bunny + - quick as a lick + - quick as a wink + - quick as lightning + - quiet as a dormouse + - rags to riches + - raining buckets + - raining cats and dogs + - rank and file + - rat race + - reap what you sow + - red as a beet + - red herring + - reinvent the wheel + - rich and famous + - rings a bell + - ripe old age + - ripped me off + - rise and shine + - road to hell is paved with good intentions + - rob Peter to pay Paul + - roll over in the grave + - rub the wrong way + - ruled the roost + - running in circles + - sad but true + - sadder but wiser + - salt of the earth + - scared stiff + - scared to death + - sealed with a kiss + - second to none + - see eye to eye + - seen the light + - seize the day + - set the record straight + - set the world on fire + - set your teeth on edge + - sharp as a tack + - shoot for the moon + - shoot the breeze + - shot in the dark + - shoulder to the wheel + - sick as a dog + - sigh of relief + - signed, sealed, and delivered + - sink or swim + - six of one, half a dozen of another + - skating on thin ice + - slept like a log + - slinging mud + - slippery as an eel + - slow as molasses + - smart as a whip + - smooth as a baby's bottom + - sneaking suspicion + - snug as a bug in a rug + - sow wild oats + - spare the rod, spoil the child + - speak of the devil + - spilled the beans + - spinning your wheels + - spitting image of + - spoke with relish + - spread like wildfire + - spring to life + - squeaky wheel gets the grease + - stands out like a sore thumb + - start from scratch + - stick in the mud + - still waters run deep + - stitch in time + - stop and smell the roses + - straight as an arrow + - straw that broke the camel's back + - strong as an ox + - stubborn as a mule + - stuff that dreams are made of + - stuffed shirt + - sweating blood + - sweating bullets + - take a load off + - take one for the team + - take the bait + - take the bull by the horns + - take the plunge + - takes one to know one + - takes two to tango + - the more the merrier + - the real deal + - the real McCoy + - the red carpet treatment + - the same old story + - there is no accounting for taste + - thick as a brick + - thick as thieves + - thin as a rail + - think outside of the box + - third time's the charm + - this day and age + - this hurts me worse than it hurts you + - this point in time + - three sheets to the wind + - through thick and thin + - throw in the towel + - tie one on + - tighter than a drum + - time and time again + - time is of the essence + - tip of the iceberg + - tired but happy + - to coin a phrase + - to each his own + - to make a long story short + - to the best of my knowledge + - toe the line + - tongue in cheek + - too good to be true + - too hot to handle + - too numerous to mention + - touch with a ten foot pole + - tough as nails + - trial and error + - trials and tribulations + - tried and true + - trip down memory lane + - twist of fate + - two cents worth + - two peas in a pod + - ugly as sin + - under the counter + - under the gun + - under the same roof + - under the weather + - until the cows come home + - unvarnished truth + - up the creek + - uphill battle + - upper crust + - upset the applecart + - vain attempt + - vain effort + - vanquish the enemy + - vested interest + - waiting for the other shoe to drop + - wakeup call + - warm welcome + - watch your p's and q's + - watch your tongue + - watching the clock + - water under the bridge + - weather the storm + - weed them out + - week of Sundays + - went belly up + - wet behind the ears + - what goes around comes around + - what you see is what you get + - when it rains, it pours + - when push comes to shove + - when the cat's away + - when the going gets tough, the tough get going + - white as a sheet + - whole ball of wax + - whole hog + - whole nine yards + - wild goose chase + - will wonders never cease? + - wisdom of the ages + - wise as an owl + - wolf at the door + - words fail me + - work like a dog + - world weary + - worst nightmare + - worth its weight in gold + - wrong side of the bed + - yanking your chain + - yappy as a dog + - years young + - you are what you eat + - you can run but you can't hide + - you only live once + - you're the boss + - young and foolish + - young and vibrant diff --git a/docs/source/.vale/write-good/E-Prime.yml b/docs/source/.vale/write-good/E-Prime.yml new file mode 100644 index 0000000000..074a102b25 --- /dev/null +++ b/docs/source/.vale/write-good/E-Prime.yml @@ -0,0 +1,32 @@ +extends: existence +message: "Try to avoid using '%s'." +ignorecase: true +level: suggestion +tokens: + - am + - are + - aren't + - be + - been + - being + - he's + - here's + - here's + - how's + - i'm + - is + - isn't + - it's + - she's + - that's + - there's + - they're + - was + - wasn't + - we're + - were + - weren't + - what's + - where's + - who's + - you're diff --git a/docs/source/.vale/write-good/Illusions.yml b/docs/source/.vale/write-good/Illusions.yml new file mode 100644 index 0000000000..b4f1321859 --- /dev/null +++ b/docs/source/.vale/write-good/Illusions.yml @@ -0,0 +1,11 @@ +extends: repetition +message: "'%s' is repeated!" +level: warning +alpha: true +action: + name: edit + params: + - truncate + - " " +tokens: + - '[^\s]+' diff --git a/docs/source/.vale/write-good/Passive.yml b/docs/source/.vale/write-good/Passive.yml new file mode 100644 index 0000000000..f472cb9049 --- /dev/null +++ b/docs/source/.vale/write-good/Passive.yml @@ -0,0 +1,183 @@ +extends: existence +message: "'%s' may be passive voice. Use active voice if you can." +ignorecase: true +level: warning +raw: + - \b(am|are|were|being|is|been|was|be)\b\s* +tokens: + - '[\w]+ed' + - awoken + - beat + - become + - been + - begun + - bent + - beset + - bet + - bid + - bidden + - bitten + - bled + - blown + - born + - bought + - bound + - bred + - broadcast + - broken + - brought + - built + - burnt + - burst + - cast + - caught + - chosen + - clung + - come + - cost + - crept + - cut + - dealt + - dived + - done + - drawn + - dreamt + - driven + - drunk + - dug + - eaten + - fallen + - fed + - felt + - fit + - fled + - flown + - flung + - forbidden + - foregone + - forgiven + - forgotten + - forsaken + - fought + - found + - frozen + - given + - gone + - gotten + - ground + - grown + - heard + - held + - hidden + - hit + - hung + - hurt + - kept + - knelt + - knit + - known + - laid + - lain + - leapt + - learnt + - led + - left + - lent + - let + - lighted + - lost + - made + - meant + - met + - misspelt + - mistaken + - mown + - overcome + - overdone + - overtaken + - overthrown + - paid + - pled + - proven + - put + - quit + - read + - rid + - ridden + - risen + - run + - rung + - said + - sat + - sawn + - seen + - sent + - set + - sewn + - shaken + - shaven + - shed + - shod + - shone + - shorn + - shot + - shown + - shrunk + - shut + - slain + - slept + - slid + - slit + - slung + - smitten + - sold + - sought + - sown + - sped + - spent + - spilt + - spit + - split + - spoken + - spread + - sprung + - spun + - stolen + - stood + - stridden + - striven + - struck + - strung + - stuck + - stung + - stunk + - sung + - sunk + - swept + - swollen + - sworn + - swum + - swung + - taken + - taught + - thought + - thrived + - thrown + - thrust + - told + - torn + - trodden + - understood + - upheld + - upset + - wed + - wept + - withheld + - withstood + - woken + - won + - worn + - wound + - woven + - written + - wrung diff --git a/docs/source/.vale/write-good/README.md b/docs/source/.vale/write-good/README.md new file mode 100644 index 0000000000..3edcc9b376 --- /dev/null +++ b/docs/source/.vale/write-good/README.md @@ -0,0 +1,27 @@ +Based on [write-good](https://github.com/btford/write-good). + +> Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too. + +``` +The MIT License (MIT) + +Copyright (c) 2014 Brian Ford + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/docs/source/.vale/write-good/So.yml b/docs/source/.vale/write-good/So.yml new file mode 100644 index 0000000000..e57f099dc0 --- /dev/null +++ b/docs/source/.vale/write-good/So.yml @@ -0,0 +1,5 @@ +extends: existence +message: "Don't start a sentence with '%s'." +level: error +raw: + - '(?:[;-]\s)so[\s,]|\bSo[\s,]' diff --git a/docs/source/.vale/write-good/ThereIs.yml b/docs/source/.vale/write-good/ThereIs.yml new file mode 100644 index 0000000000..8b82e8f6cc --- /dev/null +++ b/docs/source/.vale/write-good/ThereIs.yml @@ -0,0 +1,6 @@ +extends: existence +message: "Don't start a sentence with '%s'." +ignorecase: false +level: error +raw: + - '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b' diff --git a/docs/source/.vale/write-good/TooWordy.yml b/docs/source/.vale/write-good/TooWordy.yml new file mode 100644 index 0000000000..275701b196 --- /dev/null +++ b/docs/source/.vale/write-good/TooWordy.yml @@ -0,0 +1,221 @@ +extends: existence +message: "'%s' is too wordy." +ignorecase: true +level: warning +tokens: + - a number of + - abundance + - accede to + - accelerate + - accentuate + - accompany + - accomplish + - accorded + - accrue + - acquiesce + - acquire + - additional + - adjacent to + - adjustment + - admissible + - advantageous + - adversely impact + - advise + - aforementioned + - aggregate + - aircraft + - all of + - all things considered + - alleviate + - allocate + - along the lines of + - already existing + - alternatively + - amazing + - ameliorate + - anticipate + - apparent + - appreciable + - as a matter of fact + - as a means of + - as far as I'm concerned + - as of yet + - as to + - as yet + - ascertain + - assistance + - at the present time + - at this time + - attain + - attributable to + - authorize + - because of the fact that + - belated + - benefit from + - bestow + - by means of + - by virtue of + - by virtue of the fact that + - cease + - close proximity + - commence + - comply with + - concerning + - consequently + - consolidate + - constitutes + - demonstrate + - depart + - designate + - discontinue + - due to the fact that + - each and every + - economical + - eliminate + - elucidate + - employ + - endeavor + - enumerate + - equitable + - equivalent + - evaluate + - evidenced + - exclusively + - expedite + - expend + - expiration + - facilitate + - factual evidence + - feasible + - finalize + - first and foremost + - for all intents and purposes + - for the most part + - for the purpose of + - forfeit + - formulate + - have a tendency to + - honest truth + - however + - if and when + - impacted + - implement + - in a manner of speaking + - in a timely manner + - in a very real sense + - in accordance with + - in addition + - in all likelihood + - in an effort to + - in between + - in excess of + - in lieu of + - in light of the fact that + - in many cases + - in my opinion + - in order to + - in regard to + - in some instances + - in terms of + - in the case of + - in the event that + - in the final analysis + - in the nature of + - in the near future + - in the process of + - inception + - incumbent upon + - indicate + - indication + - initiate + - irregardless + - is applicable to + - is authorized to + - is responsible for + - it is + - it is essential + - it seems that + - it was + - magnitude + - maximum + - methodology + - minimize + - minimum + - modify + - monitor + - multiple + - necessitate + - nevertheless + - not certain + - not many + - not often + - not unless + - not unlike + - notwithstanding + - null and void + - numerous + - objective + - obligate + - obtain + - on the contrary + - on the other hand + - one particular + - optimum + - overall + - owing to the fact that + - participate + - particulars + - pass away + - pertaining to + - point in time + - portion + - possess + - preclude + - previously + - prior to + - prioritize + - procure + - proficiency + - provided that + - purchase + - put simply + - readily apparent + - refer back + - regarding + - relocate + - remainder + - remuneration + - requirement + - reside + - residence + - retain + - satisfy + - shall + - should you wish + - similar to + - solicit + - span across + - strategize + - subsequent + - substantial + - successfully complete + - sufficient + - terminate + - the month of + - the point I am trying to make + - therefore + - time period + - took advantage of + - transmit + - transpire + - type of + - until such time as + - utilization + - utilize + - validate + - various different + - what I mean to say is + - whether or not + - with respect to + - with the exception of + - witnessed diff --git a/docs/source/.vale/write-good/Weasel.yml b/docs/source/.vale/write-good/Weasel.yml new file mode 100644 index 0000000000..e29391444b --- /dev/null +++ b/docs/source/.vale/write-good/Weasel.yml @@ -0,0 +1,207 @@ +extends: existence +message: "'%s' is a weasel word!" +ignorecase: true +level: warning +tokens: + - absolutely + - accidentally + - additionally + - allegedly + - alternatively + - angrily + - anxiously + - approximately + - awkwardly + - badly + - barely + - beautifully + - blindly + - boldly + - bravely + - brightly + - briskly + - bristly + - bubbly + - busily + - calmly + - carefully + - carelessly + - cautiously + - cheerfully + - clearly + - closely + - coldly + - completely + - consequently + - correctly + - courageously + - crinkly + - cruelly + - crumbly + - cuddly + - currently + - daily + - daringly + - deadly + - definitely + - deliberately + - doubtfully + - dumbly + - eagerly + - early + - easily + - elegantly + - enormously + - enthusiastically + - equally + - especially + - eventually + - exactly + - exceedingly + - exclusively + - extremely + - fairly + - faithfully + - fatally + - fiercely + - finally + - fondly + - few + - foolishly + - fortunately + - frankly + - frantically + - generously + - gently + - giggly + - gladly + - gracefully + - greedily + - happily + - hardly + - hastily + - healthily + - heartily + - helpfully + - honestly + - hourly + - hungrily + - hurriedly + - immediately + - impatiently + - inadequately + - ingeniously + - innocently + - inquisitively + - interestingly + - irritably + - jiggly + - joyously + - justly + - kindly + - largely + - lately + - lazily + - likely + - literally + - lonely + - loosely + - loudly + - loudly + - luckily + - madly + - many + - mentally + - mildly + - monthly + - mortally + - mostly + - mysteriously + - neatly + - nervously + - nightly + - noisily + - normally + - obediently + - occasionally + - only + - openly + - painfully + - particularly + - patiently + - perfectly + - politely + - poorly + - powerfully + - presumably + - previously + - promptly + - punctually + - quarterly + - quickly + - quietly + - rapidly + - rarely + - really + - recently + - recklessly + - regularly + - remarkably + - relatively + - reluctantly + - repeatedly + - rightfully + - roughly + - rudely + - sadly + - safely + - selfishly + - sensibly + - seriously + - sharply + - shortly + - shyly + - significantly + - silently + - simply + - sleepily + - slowly + - smartly + - smelly + - smoothly + - softly + - solemnly + - sparkly + - speedily + - stealthily + - sternly + - stupidly + - substantially + - successfully + - suddenly + - surprisingly + - suspiciously + - swiftly + - tenderly + - tensely + - thoughtfully + - tightly + - timely + - truthfully + - unexpectedly + - unfortunately + - usually + - very + - victoriously + - violently + - vivaciously + - warmly + - waverly + - weakly + - wearily + - weekly + - wildly + - wisely + - worldly + - wrinkly + - yearly diff --git a/docs/source/.vale/write-good/meta.json b/docs/source/.vale/write-good/meta.json new file mode 100644 index 0000000000..a115d2886a --- /dev/null +++ b/docs/source/.vale/write-good/meta.json @@ -0,0 +1,4 @@ +{ + "feed": "https://github.com/errata-ai/write-good/releases.atom", + "vale_version": ">=1.0.0" +} diff --git a/docs/source/Contributing.md b/docs/source/Contributing.md index 0d83bc5c40..564795acc1 100644 --- a/docs/source/Contributing.md +++ b/docs/source/Contributing.md @@ -25,7 +25,7 @@ Evennia depends heavily on good documentation and we are always looking for extra eyes and hands to improve it. Even small things such as fixing typos are a great help! -- Easiest is to just [report dicumentation issues][issues] as you find them. If +- Easiest is to just [report documentation issues][issues] as you find them. If we don't know about them, we can't fix them! - If you want to help edit the docs directly, [check here](./Contributing-Docs.md) on how to do it. @@ -83,7 +83,7 @@ like [Pastebin](https://pastebin.com/) and just supply the link. Evennia has a [contrib](Contribs/Contribs-Overview.md) directory which contains user-shared code organized by category. You can contribute anything that you think may be useful to another dev, also highly game-specific code. A contrib -must always be added via a forked respository. +must always be added via a forked repository. #### Guidelines for making a contrib @@ -101,7 +101,7 @@ must always be added via a forked respository. documented and part of the installation instructions. - The contrib must be contained within a separate folder under one of the contrib categories (`game_systems`, `rpg`, `utils` etc). Ask if you are - unsuare which category to put your contrib under. + unsure which category to put your contrib under. - The folder (package) should be on the following form: ```