mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Expand RPSystem to respect case of references. Resolves #1620.
This commit is contained in:
parent
ff2733fd93
commit
cb5830bbc5
3 changed files with 139 additions and 30 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue