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 diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 2c431280c0..c191d72d93 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):` @@ -149,18 +149,15 @@ 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 = {} - _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1)) # ------------------------------------------------------------ # Emote parser @@ -189,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) @@ -239,97 +236,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 @@ -375,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: @@ -430,24 +337,20 @@ 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 hasattr(obj, "sdesc")) - ] - ) - - # filter out non-found data - candidate_regexes = [tup for tup in candidate_regexes if tup] + # 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.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". @@ -468,18 +371,48 @@ 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 + 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 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 + bestmatches = [(obj, match.group()) for match, obj, text in matches if match] - # 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) + 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) + 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]) + # match candidates against the current set of words + 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 + break + # since this is the longest match so far, set latest match set as best matches + bestmatches = matches + # save current index as end point of matched text + iend = i - # 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) + # save search string + matched_text = "".join(tail[1:iend]) + # recombine remainder of emote back into a string + tail = "".join(tail[iend+1:]) - # 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 +421,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 +433,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. @@ -519,35 +451,32 @@ 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() - 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 :] + key = f"#{obj.id}{case}" + # recombine emote with matched text replaced by ref + string = f"{head}{{{key}}}{tail}" mapping[key] = obj else: # 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 "", + "{num}{sep}{name} ({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) ] @@ -611,7 +540,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). @@ -619,66 +548,45 @@ 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)): - # no self-reference in the emote - add to the end - obj_mapping[skey] = sender + # 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 it if anonymous_add == "first": - possessive = "" if emote.startswith("'") else " " - emote = "%s%s%s" % ("{{%s}}" % skey, possessive, emote) + # add case flag for initial caps + 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: - emote = "%s [%s]" % (emote, "{{%s}}" % skey) + # add it to the end + femote = "{emote} [{key}]" + emote = femote.format( key="{{"+ skey +"}}", emote=emote ) + obj_mapping[skey] = sender # broadcast emote to everyone 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) - # 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 + # map the ref keys to sdescs + receiver_sdesc_mapping = dict( + ( + ref, + obj.get_display_name(receiver, ref=ref, noid=True), ) + 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) @@ -713,17 +621,13 @@ class SdescHandler: """ self.obj = obj self.sdesc = "" - self.sdesc_regex = "" self._cache() 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) + self.sdesc = self.obj.attributes.get("_sdesc", default=self.obj.key) def add(self, sdesc, max_length=60): """ @@ -759,17 +663,13 @@ 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 - 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 @@ -781,15 +681,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: @@ -802,7 +693,6 @@ class RecogHandler: _recog_ref2recog _recog_obj2recog - _recog_obj2regex """ @@ -817,7 +707,6 @@ class RecogHandler: self.obj = obj # mappings self.ref2recog = {} - self.obj2regex = {} self.obj2recog = {} self._cache() @@ -826,11 +715,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): @@ -873,31 +758,27 @@ 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 - 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): """ - 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 @@ -908,10 +789,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): """ @@ -932,19 +813,9 @@ 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] + del self.obj.db._recog_ref2recog[f"#{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 @@ -994,8 +865,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") @@ -1025,7 +896,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) @@ -1061,7 +931,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 @@ -1121,8 +991,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) @@ -1134,18 +1004,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. @@ -1157,12 +1027,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 @@ -1241,12 +1111,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 "", + "{num}{sep}{sdesc} ({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) ] @@ -1262,7 +1132,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("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 @@ -1271,7 +1141,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): @@ -1302,14 +1172,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 @@ -1319,7 +1189,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): @@ -1346,6 +1216,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): """ @@ -1357,6 +1230,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, @@ -1529,6 +1406,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. @@ -1539,28 +1432,41 @@ class ContribRPObject(DefaultObject): 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. - - Notes: - The RPObject version doesn't add color to its display. + 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") else "" + ref = kwargs.get("ref","~") + if looker == self: + # always show your own key sdesc = self.key else: try: - recog = looker.recog.get(self) + # get the sdesc looker should see + sdesc = looker.get_sdesc(self, 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 "" - return "%s%s%s" % (sdesc, idstr, pose) + # 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 + def return_appearance(self, looker): """ @@ -1569,6 +1475,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 "" @@ -1592,6 +1502,7 @@ class ContribRPObject(DefaultObject): string += "\n|wExits:|n " + ", ".join(exits) if users or things: string += "\n " + "\n ".join(users + things) + return string @@ -1608,11 +1519,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) @@ -1627,29 +1533,45 @@ 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 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 + # process your key as recog since you recognize yourself + sdesc = self.process_recog(self.key,self) else: try: - recog = looker.recog.get(self) + # get the sdesc looker should see, with formatting + 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 "is here.") if kwargs.get("pose", False) else "" - return "|c%s|n%s%s" % (sdesc, idstr, pose) + # 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 + def at_object_creation(self): """ @@ -1658,10 +1580,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) @@ -1679,8 +1599,38 @@ 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): + """ + 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. + + 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 method for self - .process_sdesc or + .process_recog + """ + # always see own key + if obj == self: + recog = self.key + sdesc = self.key + else: + # 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 + def process_sdesc(self, sdesc, obj, **kwargs): """ @@ -1721,7 +1671,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): """ @@ -1732,14 +1682,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 recog: + return "" + + return f"|m{recog}|n" def process_language(self, text, speaker, language, **kwargs): """ @@ -1762,4 +1713,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 + ) diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py index cd96ac43b1..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): @@ -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}) @@ -198,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 @@ -212,18 +196,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): @@ -241,20 +225,21 @@ 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 " - "|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 |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, " + "|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 |bReceiver2|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): @@ -265,18 +250,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): @@ -305,7 +278,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(), @@ -316,6 +289,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", )