Expand RPSystem to respect case of references. Resolves #1620.

This commit is contained in:
Griatch 2021-09-08 23:41:33 +02:00
parent ff2733fd93
commit cb5830bbc5
3 changed files with 139 additions and 30 deletions

View file

@ -86,6 +86,8 @@ Up requirements to Django 3.2+
but `Cmd_nAmE` -> `Cmd.nAmE`). This helps e.g Mudlet's legacy `Client_GUI` implementation) but `Cmd_nAmE` -> `Cmd.nAmE`). This helps e.g Mudlet's legacy `Client_GUI` implementation)
- Prototypes now allow setting `prototype_parent` directly to a prototype-dict. - Prototypes now allow setting `prototype_parent` directly to a prototype-dict.
This makes it easier when dynamically building in-module prototypes. This makes it easier when dynamically building in-module prototypes.
- RPSystem contrib was expanded to support case, so /tall becomes 'tall man'
while /Tall becomes 'Tall man'. One can turn this off if wanting the old style.
### Evennia 0.9.5 (2019-2020) ### Evennia 0.9.5 (2019-2020)

View file

@ -27,7 +27,7 @@ to a game, common to many RP-centric games:
/alias to reference objects in the room. You can use any /alias to reference objects in the room. You can use any
number of sdesc sub-parts to differentiate a local sdesc, or number of sdesc sub-parts to differentiate a local sdesc, or
use /1-sdesc etc to differentiate them. The emote also use /1-sdesc etc to differentiate them. The emote also
identifies nested says. identifies nested says and separates case.
- sdesc obscuration of real character names for use in emotes - sdesc obscuration of real character names for use in emotes
and in any referencing such as object.search(). This relies and in any referencing such as object.search(). This relies
on an SdescHandler `sdesc` being set on the Character and on an SdescHandler `sdesc` being set on the Character and
@ -60,13 +60,18 @@ an example of a static *pose*: The "standing by the bar" has been set
by the player of the tall man, so that people looking at him can tell by the player of the tall man, so that people looking at him can tell
at a glance what is going on. at a glance what is going on.
> emote /me looks at /tall and says "Hello!" > emote /me looks at /Tall and says "Hello!"
I see: I see:
Griatch looks at Tall man and says "Hello". Griatch looks at Tall man and says "Hello".
Tall man (assuming his name is Tom) sees: Tall man (assuming his name is Tom) sees:
The godlike figure looks at Tom and says "Hello". The godlike figure looks at Tom and says "Hello".
Note that by default, the case of the tag matters, so `/tall` will
lead to 'tall man' while `/Tall` will become 'Tall man' and /TALL
becomes /TALL MAN. If you don't want this behavior, you can pass
case_sensitive=False to the `send_emote` function.
Verbose Installation Instructions: Verbose Installation Instructions:
1. In typeclasses/character.py: 1. In typeclasses/character.py:
@ -89,9 +94,9 @@ Verbose Installation Instructions:
Inherit `ContribRPObject`: Inherit `ContribRPObject`:
Change `class Object(DefaultObject):` to Change `class Object(DefaultObject):` to
`class Object(ContribRPObject):` `class Object(ContribRPObject):`
4. Reload the server (@reload or from console: "evennia reload") 4. Reload the server (`reload` or from console: "evennia reload")
5. Force typeclass updates as required. Example for your character: 5. Force typeclass updates as required. Example for your character:
@type/reset/force me = typeclasses.characters.Character `type/reset/force me = typeclasses.characters.Character`
""" """
import re import re
@ -146,8 +151,9 @@ _RE_OBJ_REF_START = re.compile(r"%s(?:([0-9]+)%s)*(\w+)" % (_PREFIX, _NUM_SEP),
_RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS) _RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS)
_RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS) _RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS)
# Reference markers are used internally when distributing the emote to # Reference markers are used internally when distributing the emote to
# all that can see it. They are never seen by players and are on the form {#dbref}. # all that can see it. They are never seen by players and are on the form {#dbref<char>}
_RE_REF = re.compile(r"\{+\#([0-9]+)\}+") # with the <char> indicating case of the original reference query (like ^ for uppercase)
_RE_REF = re.compile(r"\{+\#([0-9]+[\^\~tv]{0,1})\}+")
# This regex is used to quickly reference one self in an emote. # This regex is used to quickly reference one self in an emote.
_RE_SELF_REF = re.compile(r"/me|@", _RE_FLAGS) _RE_SELF_REF = re.compile(r"/me|@", _RE_FLAGS)
@ -333,7 +339,7 @@ def parse_language(speaker, emote):
return emote, mapping return emote, mapping
def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False): def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_sensitive=True):
""" """
Read a raw emote and parse it into an intermediary Read a raw emote and parse it into an intermediary
format for distributing to all observers. format for distributing to all observers.
@ -346,6 +352,11 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False):
string (str): The string (like an emote) we want to analyze for keywords. string (str): The string (like an emote) we want to analyze for keywords.
search_mode (bool, optional): If `True`, the "emote" is a query string search_mode (bool, optional): If `True`, the "emote" is a query string
we want to analyze. If so, the return value is changed. we want to analyze. If so, the return value is changed.
case_sensitive (bool, optional); If set, the case of /refs matter, so that
/tall will come out as 'tall man' while /Tall will become 'Tall man'.
This allows for more grammatically correct emotes at the cost of being
a little more to learn for players. If disabled, the original sdesc case
is always kept and are inserted as-is.
Returns: Returns:
(emote, mapping) (tuple): If `search_mode` is `False` (emote, mapping) (tuple): If `search_mode` is `False`
@ -452,10 +463,32 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False):
elif nmatches == 0: elif nmatches == 0:
errors.append(_EMOTE_NOMATCH_ERROR.format(ref=marker_match.group())) errors.append(_EMOTE_NOMATCH_ERROR.format(ref=marker_match.group()))
elif nmatches == 1: elif nmatches == 1:
key = "#%i" % obj.id # a unique match - parse into intermediary representation
case = '~' # retain original case of sdesc
if case_sensitive:
# 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)
# - 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'
key = "#%i%s" % (obj.id, case)
string = string[:istart0] + "{%s}" % key + string[istart + maxscore:] string = string[:istart0] + "{%s}" % key + string[istart + maxscore:]
mapping[key] = obj mapping[key] = obj
else: else:
# multimatch error
refname = marker_match.group() refname = marker_match.group()
reflist = [ reflist = [
"%s%s%s (%s%s)" "%s%s%s (%s%s)"
@ -507,30 +540,42 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
- None: No auto-add at anonymous emote - None: No auto-add at anonymous emote
- 'last': Add sender to the end of emote as [sender] - 'last': Add sender to the end of emote as [sender]
- 'first': Prepend sender to start of emote. - 'first': Prepend sender to start of emote.
Kwargs:
case_sensitive (bool): Defaults to True, but can be unset
here. When enabled, /tall will lead to a lowercase
'tall man' while /Tall will lead to 'Tall man' and
/TALL will lead to 'TALL MAN'. If disabled, the sdesc's
case will always be used, regardless of the /ref case used.
any: Other kwargs will be passed on into the receiver's process_sdesc and
process_recog methods, and can thus be used to customize those.
""" """
case_sensitive = kwargs.pop("case_sensitive", True)
try: try:
emote, obj_mapping = parse_sdescs_and_recogs(sender, receivers, emote) emote, obj_mapping = parse_sdescs_and_recogs(sender, receivers, emote,
case_sensitive=case_sensitive)
emote, language_mapping = parse_language(sender, emote) emote, language_mapping = parse_language(sender, emote)
except (EmoteError, LanguageError) as err: except (EmoteError, LanguageError) as err:
# handle all error messages, don't hide actual coding errors # handle all error messages, don't hide actual coding errors
sender.msg(str(err)) sender.msg(str(err))
return return
skey = "#%i" % sender.id
# we escape the object mappings since we'll do the language ones first # we escape the object mappings since we'll do the language ones first
# (the text could have nested object mappings). # (the text could have nested object mappings).
emote = _RE_REF.sub(r"{{#\1}}", emote) emote = _RE_REF.sub(r"{{#\1}}", emote)
# if anonymous_add is passed as a kwarg, collect and remove it from kwargs # if anonymous_add is passed as a kwarg, collect and remove it from kwargs
if 'anonymous_add' in kwargs: if 'anonymous_add' in kwargs:
anonymous_add = kwargs.pop('anonymous_add') anonymous_add = kwargs.pop('anonymous_add')
if anonymous_add and not "#%i" % sender.id in obj_mapping: 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 # no self-reference in the emote - add to the end
key = "#%i" % sender.id obj_mapping[skey] = sender
obj_mapping[key] = sender
if anonymous_add == "first": if anonymous_add == "first":
possessive = "" if emote.startswith("'") else " " possessive = "" if emote.startswith("'") else " "
emote = "%s%s%s" % ("{{%s}}" % key, possessive, emote) emote = "%s%s%s" % ("{{%s}}" % skey, possessive, emote)
else: else:
emote = "%s [%s]" % (emote, "{{%s}}" % key) emote = "%s [%s]" % (emote, "{{%s}}" % skey)
# broadcast emote to everyone # broadcast emote to everyone
for receiver in receivers: for receiver in receivers:
@ -544,7 +589,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
# color says # color says
receiver_lang_mapping[key] = process_language(saytext, sender, langname) receiver_lang_mapping[key] = process_language(saytext, sender, langname)
# map the language {##num} markers. This will convert the escaped sdesc markers on # map the language {##num} markers. This will convert the escaped sdesc markers on
# the form {{#num}} to {#num} markers ready to sdescmat in the next step. # the form {{#num}} to {#num} markers ready to sdesc-map in the next step.
sendemote = emote.format(**receiver_lang_mapping) sendemote = emote.format(**receiver_lang_mapping)
# handle sdesc mappings. we make a temporary copy that we can modify # handle sdesc mappings. we make a temporary copy that we can modify
@ -561,22 +606,27 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
try: try:
recog_get = receiver.recog.get recog_get = receiver.recog.get
receiver_sdesc_mapping = dict( receiver_sdesc_mapping = dict(
(ref, process_recog(recog_get(obj), obj)) for ref, obj in obj_mapping.items() (ref, process_recog(recog_get(obj), obj, ref=ref, **kwargs))
for ref, obj in obj_mapping.items()
) )
except AttributeError: except AttributeError:
receiver_sdesc_mapping = dict( receiver_sdesc_mapping = dict(
( (
ref, ref,
process_sdesc(obj.sdesc.get(), obj) process_sdesc(obj.sdesc.get(), obj, ref=ref)
if hasattr(obj, "sdesc") if hasattr(obj, "sdesc")
else process_sdesc(obj.key, obj), else process_sdesc(obj.key, obj, ref=ref),
) )
for ref, obj in obj_mapping.items() for ref, obj in obj_mapping.items()
) )
# make sure receiver always sees their real name # make sure receiver always sees their real name
rkey = "#%i" % receiver.id rkey_start = "#%i" % receiver.id
if rkey in receiver_sdesc_mapping: rkey_keep_case = rkey_start + '~' # signifies keeping the case
receiver_sdesc_mapping[rkey] = process_sdesc(receiver.key, receiver) 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)
# do the template replacement of the sdesc/recog {#num} markers # do the template replacement of the sdesc/recog {#num} markers
receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs) receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs)
@ -587,7 +637,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
# ------------------------------------------------------------ # ------------------------------------------------------------
class SdescHandler(object): class SdescHandler:
""" """
This Handler wraps all operations with sdescs. We This Handler wraps all operations with sdescs. We
need to use this since we do a lot preparations on need to use this since we do a lot preparations on
@ -690,7 +740,7 @@ class SdescHandler(object):
return self.sdesc_regex, self.obj, self.sdesc return self.sdesc_regex, self.obj, self.sdesc
class RecogHandler(object): class RecogHandler:
""" """
This handler manages the recognition mapping This handler manages the recognition mapping
of an Object. of an Object.
@ -1590,11 +1640,33 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
you are viewing yourself (and sdesc is your key). you are viewing yourself (and sdesc is your key).
This is not used by default. This is not used by default.
Kwargs:
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
Returns: Returns:
sdesc (str): The processed sdesc ready sdesc (str): The processed sdesc ready
for display. for display.
""" """
if not sdesc:
return ""
ref = kwargs.get('ref', '~') # ~ to keep sdesc unchanged
if 't' in ref:
# we only want to capitalize the first letter if there are many words
sdesc = sdesc.lower()
sdesc = sdesc[0].upper() + sdesc[1:] if len(sdesc) > 1 else sdesc.upper()
elif '^' in ref:
sdesc = sdesc.upper()
elif 'v' in ref:
sdesc = sdesc.lower()
return "|b%s|n" % sdesc return "|b%s|n" % sdesc
def process_recog(self, recog, obj, **kwargs): def process_recog(self, recog, obj, **kwargs):
@ -1606,12 +1678,14 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
translated from the original sdesc at this point. translated from the original sdesc at this point.
obj (Object): The object the recog:ed string belongs to. obj (Object): The object the recog:ed string belongs to.
This is not used by default. This is not used by default.
Kwargs:
ref (str): See process_sdesc.
Returns: Returns:
recog (str): The modified recog string. recog (str): The modified recog string.
""" """
return self.process_sdesc(recog, obj) return self.process_sdesc(recog, obj, **kwargs)
def process_language(self, text, speaker, language, **kwargs): def process_language(self, text, speaker, language, **kwargs):
""" """

View file

@ -104,7 +104,7 @@ recog01 = "Mr Receiver"
recog02 = "Mr Receiver2" recog02 = "Mr Receiver2"
recog10 = "Mr Sender" recog10 = "Mr Sender"
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."' 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."
class TestRPSystem(EvenniaTest): class TestRPSystem(EvenniaTest):
maxDiff = None maxDiff = None
@ -195,9 +195,11 @@ class TestRPSystem(EvenniaTest):
"#9": "A nice sender of emotes", "#9": "A nice sender of emotes",
}, },
) )
self.assertEqual(rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote), result) self.assertEqual(rpsystem.parse_sdescs_and_recogs(
speaker, candidates, emote, case_sensitive=False), result)
self.speaker.recog.add(self.receiver1, recog01) self.speaker.recog.add(self.receiver1, recog01)
self.assertEqual(rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote), result) self.assertEqual(rpsystem.parse_sdescs_and_recogs(
speaker, candidates, emote, case_sensitive=False), result)
def test_send_emote(self): def test_send_emote(self):
speaker = self.speaker speaker = self.speaker
@ -210,7 +212,7 @@ class TestRPSystem(EvenniaTest):
speaker.msg = lambda text, **kwargs: setattr(self, "out0", text) speaker.msg = lambda text, **kwargs: setattr(self, "out0", text)
receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text) receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text)
receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text) receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text)
rpsystem.send_emote(speaker, receivers, emote) rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
self.assertEqual( self.assertEqual(
self.out0, self.out0,
"With a flair, |bSender|n looks at |bThe first receiver of emotes.|n " "With a flair, |bSender|n looks at |bThe first receiver of emotes.|n "
@ -227,6 +229,37 @@ class TestRPSystem(EvenniaTest):
'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n', 'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n',
) )
def test_send_case_sensitive_emote(self):
"""Test new case-sensitive rp-parsing"""
speaker = self.speaker
receiver1 = self.receiver1
receiver2 = self.receiver2
receivers = [speaker, receiver1, receiver2]
speaker.sdesc.add(sdesc0)
receiver1.sdesc.add(sdesc1)
receiver2.sdesc.add(sdesc2)
speaker.msg = lambda text, **kwargs: setattr(self, "out0", text)
receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text)
receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text)
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."
)
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."
)
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."
)
def test_rpsearch(self): def test_rpsearch(self):
self.speaker.sdesc.add(sdesc0) self.speaker.sdesc.add(sdesc0)
self.receiver1.sdesc.add(sdesc1) self.receiver1.sdesc.add(sdesc1)
@ -244,7 +277,7 @@ class TestRPSystem(EvenniaTest):
result = rpsystem.regex_tuple_from_key_alias(self.speaker) result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t2 = time.time() t2 = time.time()
# print(f"t1: {t1 - t0}, t2: {t2 - t1}") # print(f"t1: {t1 - t0}, t2: {t2 - t1}")
self.assertLess(t2-t1, t1-t0) self.assertLess(t2-t1, 10**-4)
self.assertEqual(result, (Anything, self.speaker, self.speaker.key)) self.assertEqual(result, (Anything, self.speaker, self.speaker.key))
@ -266,7 +299,7 @@ class TestRPSystemCommands(CommandTest):
caller=self.char2, caller=self.char2,
) )
self.call(rpsystem.CmdSay(), "Hello!", 'Char says, "Hello!"') self.call(rpsystem.CmdSay(), "Hello!", 'Char says, "Hello!"')
self.call(rpsystem.CmdEmote(), "/me smiles to /barfoo.", "Char smiles to BarFoo Character") self.call(rpsystem.CmdEmote(), "/me smiles to /BarFoo.", "Char smiles to BarFoo Character")
self.call( self.call(
rpsystem.CmdPose(), rpsystem.CmdPose(),
"stands by the bar", "stands by the bar",