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)
- Prototypes now allow setting `prototype_parent` directly to a prototype-dict.
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)

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
number of sdesc sub-parts to differentiate a local sdesc, or
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
and in any referencing such as object.search(). This relies
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
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:
Griatch looks at Tall man and says "Hello".
Tall man (assuming his name is Tom) sees:
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:
1. In typeclasses/character.py:
@ -89,9 +94,9 @@ Verbose Installation Instructions:
Inherit `ContribRPObject`:
Change `class Object(DefaultObject):` to
`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:
@type/reset/force me = typeclasses.characters.Character
`type/reset/force me = typeclasses.characters.Character`
"""
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_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS)
# 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}.
_RE_REF = re.compile(r"\{+\#([0-9]+)\}+")
# all that can see it. They are never seen by players and are on the form {#dbref<char>}
# 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.
_RE_SELF_REF = re.compile(r"/me|@", _RE_FLAGS)
@ -333,7 +339,7 @@ def parse_language(speaker, emote):
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
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.
search_mode (bool, optional): If `True`, the "emote" is a query string
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:
(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:
errors.append(_EMOTE_NOMATCH_ERROR.format(ref=marker_match.group()))
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:]
mapping[key] = obj
else:
# multimatch error
refname = marker_match.group()
reflist = [
"%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
- 'last': Add sender to the end of emote as [sender]
- '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:
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)
except (EmoteError, LanguageError) as err:
# handle all error messages, don't hide actual coding errors
sender.msg(str(err))
return
skey = "#%i" % sender.id
# we escape the object mappings since we'll do the language ones first
# (the text could have nested object mappings).
emote = _RE_REF.sub(r"{{#\1}}", emote)
# 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 "#%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
key = "#%i" % sender.id
obj_mapping[key] = sender
obj_mapping[skey] = sender
if anonymous_add == "first":
possessive = "" if emote.startswith("'") else " "
emote = "%s%s%s" % ("{{%s}}" % key, possessive, emote)
emote = "%s%s%s" % ("{{%s}}" % skey, possessive, emote)
else:
emote = "%s [%s]" % (emote, "{{%s}}" % key)
emote = "%s [%s]" % (emote, "{{%s}}" % skey)
# broadcast emote to everyone
for receiver in receivers:
@ -544,7 +589,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
# color says
receiver_lang_mapping[key] = process_language(saytext, sender, langname)
# 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)
# 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:
recog_get = receiver.recog.get
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:
receiver_sdesc_mapping = dict(
(
ref,
process_sdesc(obj.sdesc.get(), obj)
process_sdesc(obj.sdesc.get(), obj, ref=ref)
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()
)
# make sure receiver always sees their real name
rkey = "#%i" % receiver.id
if rkey in receiver_sdesc_mapping:
receiver_sdesc_mapping[rkey] = process_sdesc(receiver.key, receiver)
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)
# do the template replacement of the sdesc/recog {#num} markers
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
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
class RecogHandler(object):
class RecogHandler:
"""
This handler manages the recognition mapping
of an Object.
@ -1590,11 +1640,33 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
you are viewing yourself (and sdesc is your key).
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:
sdesc (str): The processed sdesc ready
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
def process_recog(self, recog, obj, **kwargs):
@ -1606,12 +1678,14 @@ 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)
return self.process_sdesc(recog, obj, **kwargs)
def process_language(self, text, speaker, language, **kwargs):
"""

View file

@ -104,7 +104,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."
class TestRPSystem(EvenniaTest):
maxDiff = None
@ -195,9 +195,11 @@ class TestRPSystem(EvenniaTest):
"#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.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):
speaker = self.speaker
@ -210,7 +212,7 @@ class TestRPSystem(EvenniaTest):
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, emote)
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 "
@ -227,6 +229,37 @@ class TestRPSystem(EvenniaTest):
'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):
self.speaker.sdesc.add(sdesc0)
self.receiver1.sdesc.add(sdesc1)
@ -244,7 +277,7 @@ class TestRPSystem(EvenniaTest):
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t2 = time.time()
# 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))
@ -266,7 +299,7 @@ class TestRPSystemCommands(CommandTest):
caller=self.char2,
)
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(
rpsystem.CmdPose(),
"stands by the bar",