evennia/evennia/contrib/rpsystem.py
2015-10-19 23:31:19 +02:00

1241 lines
43 KiB
Python

"""
Roleplaying base system for Evennia
Contribution - Griatch, 2015
This module contains the ContribRPObject, ContribRPRoom and
ContribRPCharacter typeclasses. If you inherit your
objects/rooms/character from these (or make them the defaults) from
these you will get the following features:
- Objects/Rooms will get the ability to have poses and will report
the poses of items inside them (the latter most useful for Rooms).
- Characters will get poses and also sdescs (short descriptions)
that will be used instead of their keys. They will gain commands
for managing recognition (custom sdesc-replacement), masking
themselves as well as an advanced free-form emote command.
To use, simply import the typclasses you want from this module and use
them to create your objects, or set them to default.
In more detail, This RP base system introduces the following features
to a game, common to many RP-centric games:
- emote system using director stance emoting (names/sdescs).
This uses a customizable replacement noun (/me, @ etc) to
represent you in the emote. You can use /sdesc, /nick, /key or
/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.
- 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
makes use of a custom Character.get_display_name hook. If
sdesc is not set, the character's `key` is used instead. This
is particularly used in the emoting system.
- recog system to assign your own nicknames to characters, can then
be used for referencing. The user may recog a user and assign
any personal nick to them. This will be shown in descriptions
and used to reference them. This is making use of the nick
functionality of Evennia.
- masks to hide your identity (using a simple lock).
- pose system to set room-persistent poses, visible in room
descriptions and when looking at the person/object. This is a
simple Attribute that modifies how the characters is viewed when
in a room as sdesc + pose.
- in-emote says, including seamless integration with language
obscuration routine (such as contrib/rplanguage.py)
Examples:
> look
Tavern
The tavern is full of nice people
*A tall man* is standing by the bar.
Above is an example of a player with an sdesc "a tall man". It is also
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!"
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".
"""
import re
from re import escape as re_escape
import itertools
from evennia import DefaultObject, DefaultCharacter
from evennia import Command, CmdSet
from evennia import ansi
from evennia.utils.utils import lazy_property
#------------------------------------------------------------
# Emote parser
#------------------------------------------------------------
# Settings
# The prefix is the (single-character) symbol used to find the start
# of a object reference, such as /tall (note that
# the system will understand multi-word references like '/a tall man' too).
_PREFIX = "/"
# The num_sep is the (single-character) symbol used to separate the
# sdesc from the number when trying to separate identical sdescs from
# one another. This is the same syntax used in the rest of Evennia, so
# by default, multiple "tall" can be separated by entering 1-tall,
# 2-tall etc.
_NUM_SEP = "-"
# Texts
_EMOTE_NOMATCH_ERROR = \
"""{{RNo match for {{r{ref}{{R.{{n"""
_EMOTE_MULTIMATCH_ERROR = \
"""{{RMultiple possibilities for {ref}:
{{r{reflist}{{n"""
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE
_RE_PREFIX = re.compile(r"^%s" % _PREFIX, re.UNICODE)
# The num_sep is the (single-character) symbol used to separate the
# sdesc from the number when trying to separate identical sdescs from
# one another. This is the same syntax used in the rest of Evennia, so
# by default, multiple "tall" can be separated by entering 1-tall,
# 2-tall etc.
_NUM_SEP = "-"
# 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)
# 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]+)\}+")
# This regex is used to quickly reference one self in an emote.
_RE_SELF_REF = re.compile(r"/me|@", _RE_FLAGS)
# regex for non-alphanumberic end of a string
_RE_CHAREND = re.compile(r"\W+$", _RE_FLAGS)
# reference markers for language
_RE_REF_LANG = re.compile(r"\{+\##([0-9]+)\}+")
# language says in the emote are on the form "..." or langname"..." (no spaces).
# this regex returns in groups (langname, say), where langname can be empty.
_RE_LANGUAGE = re.compile(r"(?:\((\w+)\))*(\".+?\")")
# the emote parser works in two steps:
# 1) convert the incoming emote into an intermediary
# form with all object references mapped to ids.
# 2) for every person seeing the emote, parse this
# intermediary form into the one valid for that char.
class EmoteError(Exception):
pass
class SdescError(Exception):
pass
class RecogError(Exception):
pass
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 o:len(o), reverse=True))
return regex
def parse_language(speaker, emote):
"""
Parse the emote for language. This is
used with a plugin for handling languages.
Args:
speaker (Object): The object speaking.
emote (str): An emote possibly containing
language references.
Returns:
(emote, mapping) (tuple): A tuple where the
`emote` is the emote string with all says
(including quotes) replaced with reference
markers on the form {##n} where n is a running
number. The `mapping` is a dictionary between
the markers and a tuple (langname, saytext), where
langname can be None.
Raises:
LanguageError: If an invalid language was specified.
Notes:
Note that no errors are raised if the wrong language identifier
is given.
This data, together with the identity of the speaker, is
intended to be used by the "listener" later, since with this
information the language skill of the speaker can be offset to
the language skill of the listener to determine how much
information is actually conveyed.
"""
# escape mapping syntax on the form {##id} if it exists already in emote,
# if so it is replaced with just "id".
emote = _RE_REF_LANG.sub(r"\1", emote)
errors = []
mapping = {}
for imatch, say_match in enumerate(reversed(list(_RE_LANGUAGE.finditer(emote)))):
# process matches backwards to be able to replace
# in-place without messing up indexes for future matches
# note that saytext includes surrounding "...".
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
# replace say with ref markers in emote
emote = emote[:istart] + "{%s}" % key + emote[iend:]
mapping[key] = (langname, saytext)
if errors:
# catch errors and report
raise LanguageError("\n".join(errors))
# at this point all says have been replaced with {##nn} markers
# and mapping maps 1:1 to this.
return emote, mapping
def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False):
"""
Read a textraw emote and parse it into an intermediary
format for distributing to all observers.
Args:
sender (Object): The object sending the emote. This object's
recog data will be considered in the parsing.
candidates (iterable): A list of objects valid for referencing
in the emote.
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.
Returns:
(emote, mapping) (tuple): If `search_mode` is `False`
(default), a tuple where the emote is the emote string, with
all references replaced with internal-representation {#dbref}
markers and mapping is a dictionary `{"#dbref":obj, ...}`.
result (list): If `search_mode` is `True` we are
performing a search query on `string`, looking for a specific
object. A list with zero, one or more matches.
Raises:
EmoteException: For various ref-matching errors.
Notes:
The parser analyzes and should understand the following
_PREFIX-tagged structures in the emote:
- self-reference (/me)
- recogs (any part of it) stored on emoter, matching obj in `candidates`.
- sdesc (any part of it) from any obj in `candidates`.
- N-sdesc, N-recog separating multi-matches (1-tall, 2-tall)
- says, "..." are
"""
# Load all candidate regex tuples [(regex, obj, sdesc/recog),...]
candidate_regexes = \
[(_RE_SELF_REF, sender, sender.sdesc.get())] + \
[sender.recog.get_regex_tuple(obj)
for obj in candidates if hasattr(obj, "recog")] + \
[obj.sdesc.get_regex_tuple()
for obj in candidates if hasattr(obj, "sdesc")]
# filter out non-found data
candidate_regexes = [tup for tup in candidate_regexes if tup]
# escape mapping syntax on the form {#id} if it exists already in emote,
# if so it is replaced with just "id".
string = _RE_REF.sub(r"\1", string)
# we now loop over all references and analyze them
mapping = {}
errors = []
obj = None
nmatches = 0
for marker_match in reversed(list(_RE_OBJ_REF_START.finditer(string))):
# we scan backwards so we can replace in-situ without messing
# up later occurrences. Given a marker match, query from
# start index forward for all candidates.
# 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
# loop over all candidate regexes and match against the string following the match
matches = ((reg.match(string[istart:]), obj, text) for reg, obj, text in candidate_regexes)
# score matches by how long part of the string was matched
matches = [(match.end() if match else -1, obj, text) for match, obj, text in matches]
maxscore = max(score for score, obj, text in matches)
# 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:
# no matches
obj = None
nmatches = 0
elif nmatches == 1:
# an exact match.
obj = bestmatches[0][0]
nmatches = 1
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]
nmatches = 1
else:
# multi-match.
# was a numerical identifier given to help us separate the multi-match?
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]
nmatches = 1
else:
# no identifier given - a real multimatch.
obj = bestmatches
if search_mode:
# single-object search mode. Don't continue loop.
break
elif nmatches == 0:
errors.append(_EMOTE_NOMATCH_ERROR.format(ref=marker_match.group()))
elif nmatches == 1:
key = "#%i" % obj.id
string = string[:istart0] + "{%s}" % key + string[istart + maxscore:]
mapping[key] = obj
else:
refname = marker_match.group()
reflist = ["%s%s%s (%s%s)" % (inum+1, _NUM_SEP,
_RE_PREFIX.sub("", refname), text,
" (%s)" % sender.key if sender == obj else "")
for inum, (obj, text) in enumerate(bestmatches) if score == maxscore]
errors.append(_EMOTE_MULTIMATCH_ERROR.format(
ref=marker_match.group(), reflist="\n ".join(reflist)))
if search_mode:
# return list of object(s) matching
if nmatches == 0:
return []
elif nmatches == 1:
return [obj]
else:
return [tup[0] for tup in obj]
if errors:
# make sure to not let errors through.
raise EmoteError("\n".join(errors))
# at this point all references have been replaced with {#xxx} markers and the mapping contains
# a 1:1 mapping between those inline markers and objects.
return string, mapping
def send_emote(sender, receivers, emote, anonymous_add="first"):
"""
Main access function for distribute an emote.
Args:
sender (Object): The one sending the emote.
receivers (iterable): Receivers of the emote. These
will also form the basis for which sdescs are
'valid' to use in the emote.
emote (str): The raw emote string as input by emoter.
anonymous_add (str or None, optional): If `sender` is not
self-referencing in the emote, this will auto-add
`sender`'s data to the emote. Possible values are
- None: No auto-add at anonymous emote
- 'last': Add sender to the end of emote as [sender]
- 'first': Prepend sender to start of emote.
"""
try:
emote, obj_mapping = parse_sdescs_and_recogs(sender, receivers, emote)
emote, language_mapping = parse_language(sender, emote)
except (EmoteError, LanguageError) as err:
# handle all error messages, don't hide actual coding errors
sender.msg(err.message)
return
if anonymous_add and not "#%i" % sender.id in obj_mapping:
# no self-reference in the emote - add to the end
key = "#%i" % sender.id
obj_mapping[key] = sender
if anonymous_add == 'first':
possessive = "" if emote.startswith('\'') else " "
emote = "%s%s%s" % ("{%s}" % key, possessive, emote)
else:
emote = "%s [%s]" % (emote, "{%s}" % key)
# broadcast emote to everyone
for receiver in receivers:
# we make a temporary copy that we can modify
try:
recog_get = receiver.recog.get
mapping = dict((ref, recog_get(obj)) for ref, obj in obj_mapping.items())
except AttributeError:
mapping = dict((ref, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
for ref, obj in obj_mapping.items())
# handle the language mapping, which always produce different keys ##nn
try:
process_language = receiver.process_language
except AttributeError:
process_language = _dummy_process
for key, (langname, saytext) in language_mapping.iteritems():
# color says
mapping[key] = process_language(saytext, sender, langname)
# make sure receiver always sees their real name
rkey = "#%i" % receiver.id
if rkey in mapping:
mapping[rkey] = receiver.key
# add color to sdesc strings
try:
process_sdesc = receiver.process_sdesc
except AttributeError:
process_sdesc = _dummy_process
mapping = dict((key, process_sdesc(val, receiver))
for key, val in mapping.iteritems())
# do the template replacement
receiver.msg(emote.format(**mapping))
#------------------------------------------------------------
# Handlers for sdesc and recog
#------------------------------------------------------------
class SdescHandler(object):
"""
This Handler wraps all operations with sdescs. We
need to use this since we do a lot preparations on
sdescs when updating them, in order for them to be
efficient to search for and query.
"""
def __init__(self, obj):
"""
Initialize the handler
Args:
obj (Object): The entity on which this handler is stored.
"""
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)
def add(self, sdesc, max_length=60):
"""
Add a new sdesc to object, replacing the old one.
Args:
sdesc (str): The sdesc to set. This may be stripped
of control sequences before setting.
max_length (int, optional): The max limit of the sdesc.
Returns:
sdesc (str): The actually set sdesc.
Raises:
SdescError: If the sdesc can not be set or is longer than
`max_length`.
"""
# strip emote components from sdesc
sdesc = _RE_REF.sub(r"\1",
_RE_REF_LANG.sub(r"\1",
_RE_SELF_REF.sub(r"",
_RE_LANGUAGE.sub(r"",
_RE_OBJ_REF_START.sub(r"", sdesc)))))
# make an sdesc clean of ANSI codes
cleaned_sdesc = ansi.strip_ansi(sdesc)
if len(cleaned_sdesc) > max_length:
raise SdescError("Too long 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
def get(self):
"""
Simple getter.
"""
return self.sdesc
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(object):
"""
This handler manages the recognition mapping
of an Object.
"""
def __init__(self, obj):
"""
Initialize the handler
Args:
obj (Object): The entity on which this handler is stored.
"""
self.obj = obj
# mappings
self.ref2recog = {}
self.obj2regex = {}
self.obj2recog = {}
self._cache()
def _cache(self):
"""
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):
"""
Assign a custom recog (nick) to the given object.
Args:
obj (Object): The object ot associate with the recog
string. This is usually determined from the sdesc in the
room by a call to parse_sdescs_and_recogs, but can also be
given.
recog (str): The replacement string to use with this object.
max_length (int, optional): The max length of the recog string.
Returns:
recog (str): The (possibly cleaned up) recog string actually set.
Raises:
SdescError: When recog could not be set or sdesc longer
than `max_length`.
"""
# strip emote components from recog
recog = _RE_REF.sub(r"\1",
_RE_REF_LANG.sub(r"\1",
_RE_SELF_REF.sub(r"",
_RE_LANGUAGE.sub(r"",
_RE_OBJ_REF_START.sub(r"", recog)))))
# make an recog clean of ANSI codes
cleaned_recog = ansi.strip_ansi(recog)
if len(cleaned_recog) > max_length:
raise RecogError("Too long recog")
# mapping #dbref:obj
key = "#%i" % obj.id
self.obj.db._recog_ref2recog[key] = recog
self.obj.db._recog_obj2recog[obj] = recog
regex = ordered_permutation_regex(cleaned_recog)
self.obj.db._recog_obj2regex[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.
Args:
obj (Object): The object, whose sdesc to replace
Returns:
recog (str): The replacement string to use.
Notes:
This method will respect a "enable_recog" lock set on
`obj` (True by default) in order to turn off recog
mechanism. This is useful for adding masks/hoods etc.
"""
if obj.access(self.obj, "enable_recog", default=True):
# 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)
else:
# recog_mask log not passed, disable recog
return obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
def remove(self, obj):
"""
Clear recog for a given object.
Args:
obj (Object): The object for which to remove recog.
"""
if obj in self.obj2recog:
del self.obj.db._recog_obj2recog[obj]
del self.obj.db._recog_obj2regex[obj]
del self.obj.db._recog_ref2recog["#%i" % obj.id]
self._cache()
def get_regex_tuple(self, obj):
"""
Returns:
rec (tuple): Tuple (recog_regex, obj, recog)
"""
if obj in self.obj2recog and obj.access(self.obj, "enable_recog", default=True):
return self.obj2regex[obj], obj, self.obj2regex[obj]
return None
#------------------------------------------------------------
# RP Commands
#------------------------------------------------------------
class RPCommand(Command):
"simple parent"
def parse(self):
"strip extra whitespace"
self.args = self.args.strip()
class CmdEmote(RPCommand): # replaces the main emote
"""
Emote an action, allowing dynamic replacement of
text in the emote.
Usage:
emote text
Example:
emote /me looks around.
emote With a flurry /me attacks /tall man with his sword.
emote "Hello", /me says.
Describes an event in the world. This allows the use of /ref
markers to replace with the short descriptions or recognized
strings of objects in the same room. These will be translated to
emotes to match each person seeing it. Use "..." for saying
things and langcode"..." without spaces to say something in
a different language.
"""
key = "emote"
aliases = [":"]
locks = "cmd:all()"
def func(self):
"Perform the emote."
if not self.args:
self.caller.msg("What do you want to do?")
else:
# we also include ourselves here.
emote = self.args
targets = self.caller.location.contents
if not emote.endswith((".", "!")):
emote = "%s." % emote
send_emote(self.caller, targets, emote, anonymous_add='first')
class CmdSdesc(RPCommand): # set/look at own sdesc
"""
Assign yourself a short description (sdesc).
Usage:
sdesc <short description>
Assigns a short description to yourself.
"""
key = "sdesc"
locks = "cmd:all()"
def func(self):
"Assign the sdesc"
caller = self.caller
if not self.args:
caller.msg("Usage: sdesc <sdesc-text>")
return
else:
# strip non-alfanum chars from end of sdesc
sdesc = _RE_CHAREND.sub("", self.args)
sdesc = caller.sdesc.add(sdesc)
caller.msg("%s's sdesc was set to '%s'." % (caller.key, sdesc))
class CmdPose(RPCommand): # set current pose and default pose
"""
Set a static pose
Usage:
pose <pose>
pose default <pose>
pose reset
pose obj = <pose>
pose default obj = <pose>
pose reset obj =
Examples:
pose leans against the tree
pose is talking to the barkeep.
pose box = is sitting on the floor.
Set a static pose. This is the end of a full sentence that starts
with your sdesc. If no full stop is given, it will be added
automatically. The default pose is the pose you get when using
pose reset. Note that you can use sdescs/recogs to reference
people in your pose, but these always appear as that person's
sdesc in the emote, regardless of who is seeing it.
"""
key = "pose"
def parse(self):
"""
Extract the "default" alternative to the pose.
"""
args = self.args.strip()
default = args.startswith("default")
reset = args.startswith("reset")
if default:
args = re.sub(r"^default", "", args)
if reset:
args = re.sub(r"^reset", "", args)
target = None
if "=" in args:
target, args = [part.strip() for part in args.split("=", 1)]
self.target = target
self.reset = reset
self.default = default
self.args = args.strip()
def func(self):
"Create the pose"
caller = self.caller
pose = self.args
target = self.target
if not pose and not self.reset:
caller.msg("Usage: pose <pose-text> OR pose obj = <pose-text>")
return
if not pose.endswith("."):
pose = "%s." % pose
if target:
# affect something else
target = caller.search(target)
if not target:
return
if not target.access(caller, "edit"):
caller.msg("You can't pose that.")
return
else:
target = caller
if not target.attributes.has("pose"):
caller.msg("%s cannot be posed." % target.key)
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))
return
else:
# set the pose. We do one-time ref->sdesc mapping here.
parsed, mapping = parse_sdescs_and_recogs(caller, caller.location.contents, pose)
mapping = dict((ref, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
for ref, obj in mapping.iteritems())
pose = parsed.format(**mapping)
if len(target_name) + len(pose) > 60:
caller.msg("Your pose '%s' is too long." % pose)
return
target.db.pose = pose
caller.msg("Pose will read '%s %s'." % (target_name, pose))
class CmdRecog(RPCommand): # assign personal alias to object in room
"""
Recognize another person in the same room.
Usage:
recog sdesc as alias
Example:
recog tall man as Griatch
forget griatch
This will assign a personal alias for a person, or
forget said alias.
"""
key = "recog"
aliases = ["recognize", "forget"]
def parse(self):
"Parse for the sdesc as alias structure"
if "as" in self.args:
self.sdesc, self.alias = [part.strip() for part in self.args.split(" as ", 2)]
elif self.args:
self.sdesc = self.args.strip()
self.alias = ""
def func(self):
"Assign the recog"
caller = self.caller
if not self.args:
caller.msg("Usage: recog <sdesc> as <alias> or forget <alias>")
return
sdesc = self.sdesc
alias = self.alias.rstrip(".?!")
prefixed_sdesc = sdesc if sdesc.startswith(_PREFIX) else _PREFIX + sdesc
candidates = caller.location.contents
matches = parse_sdescs_and_recogs(caller, candidates, prefixed_sdesc, search_mode=True)
nmatches = len(matches)
# handle 0, 1 and >1 matches
if nmatches == 0:
caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc))
elif nmatches > 1:
reflist = ["%s%s%s (%s%s)" % (inum+1, _NUM_SEP,
_RE_PREFIX.sub("", sdesc), caller.recog.get(obj),
" (%s)" % caller.key if caller == obj else "")
for inum, obj in enumerate(matches)]
caller.msg(_EMOTE_MULTIMATCH_ERROR.format(ref=sdesc,reflist="\n ".join(reflist)))
else:
obj = matches[0]
if not obj.access(self.obj, "enable_recog", default=True):
# don't apply recog if object doesn't allow it (e.g. by being masked).
caller.msg("Can't recognize someone who is masked.")
return
if self.cmdstring == "forget":
# remove existing recog
caller.recog.remove(obj)
caller.msg("%s will know only '%s'." % (caller.key, obj.recog.get(obj)))
else:
sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
alias = caller.recog.add(obj, alias)
caller.msg("%s will now remember {w%s{n as {w%s{n." % (caller.key, sdesc, alias))
class CmdMask(RPCommand):
"""
Wear a mask
Usage:
mask <new sdesc>
unmask
This will put on a mask to hide your identity. When wearing
a mask, your sdesc will be replaced by the sdesc you pick and
people's recognitions of you will be disabled.
"""
key = "mask"
aliases = ["unmask"]
def func(self):
caller = self.caller
if self.cmdstring == "mask":
# wear a mask
if not self.args:
caller.msg("Usage: (un)wearmask sdesc")
return
if caller.db.unmasked_sdesc:
caller.msg("You are already wearing a mask.")
return
sdesc = _RE_CHAREND.sub("", self.args)
sdesc = "%s {H[masked]{n" % sdesc
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)
else:
#unmask
old_sdesc = caller.db.unmasked_sdesc
if not old_sdesc:
caller.msg("You are not wearing a mask.")
return
del caller.db.unmasked_sdesc
caller.locks.remove("enable_recog")
caller.sdesc.add(old_sdesc)
caller.msg("You remove your mask and is again '%s'." % old_sdesc)
class RPSystemCmdSet(CmdSet):
"""
Mix-in for adding rp-commands to default cmdset.
"""
def at_cmdset_creation(self):
self.add(CmdEmote())
self.add(CmdSdesc())
self.add(CmdPose())
self.add(CmdRecog())
self.add(CmdMask())
#------------------------------------------------------------
# RP typeclasses
#------------------------------------------------------------
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.
"""
def at_object_creation(self):
"""
Called at initial creation.
"""
super(ContribRPObject, self).at_object_creation
# emoting/recog data
self.db.pose = ""
self.db.pose_default = "is here."
def search(self, searchdata, **kwargs):
"""
This version of search will pre-parse searchdata for eventual
matches against recogs and sdescs of candidates in the same
location.
Args:
searchdata (str): Search string.
Notes:
Recog/sdesc matching is always turned off if the keyword
`global_search` is set or `candidates` are given.
"""
if (isinstance(searchdata, basestring) and not
(kwargs.get("global_search") or
kwargs.get("candidates"))):
# searchdata is a string; common self-references
if searchdata.lower() in ("here", ):
return [self.location] if "quiet" in kwargs else self.location
if searchdata.lower() in ("me", "self",):
return [self] if "quiet" in kwargs else self
if searchdata.lower() == self.key.lower():
return [self] if "quiet" in kwargs else self
# sdesc/recog matching
candidates = self.location.contents
matches = parse_sdescs_and_recogs(self, candidates,
_PREFIX + searchdata, search_mode=True)
nmatches = len(matches)
if nmatches == 1:
return matches[0]
elif nmatches > 1:
# multimatch
reflist = ["%s%s%s (%s%s)" % (inum+1, _NUM_SEP, searchdata, self.recog.get(obj),
" (%s)" % self.key if self == obj else "")
for inum, obj in enumerate(matches)]
self.msg(_EMOTE_MULTIMATCH_ERROR.format(ref=searchdata,reflist="\n ".join(reflist)))
return
if not self.locks.check_lockstring(self, "perm(Builders)"):
# we block lookup unless we have access to continue
if "nofound_string" in kwargs:
self.msg(kwargs["nofound_string"])
else:
self.msg("There is no '%s' here." % searchdata)
return
# fall back to normal search
return super(ContribRPObject, self).search(searchdata, **kwargs)
def get_display_name(self, looker, **kwargs):
"""
Displays the name of the object in a viewer-aware manner.
Args:
looker (TypedObject): The object or player that is looking
at/getting inforamtion for this object.
Kwargs:
pose (bool): Include the pose (if available) in the return.
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.
"""
idstr = "(#%s)" % self.id if self.access(looker, access_type='control') else ""
try:
recog = looker.recog.get(self)
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)
def return_appearance(self, looker):
"""
This formats a description. It is the hook a 'look' command
should call.
Args:
looker (Object): Object doing the looking.
"""
if not looker:
return
# get and identify all objects
visible = (con for con in self.contents if con != looker and
con.access(looker, "view"))
exits, users, things = [], [], []
for con in visible:
key = con.get_display_name(looker, pose=True)
if con.destination:
exits.append(key)
elif con.has_player:
users.append(key)
else:
things.append(key)
# get description, build string
string = "{c%s{n\n" % self.get_display_name(looker, pose=True)
desc = self.db.desc
if desc:
string += "%s" % desc
if exits:
string += "\n{wExits:{n " + ", ".join(exits)
if users or things:
string += "\n " + "\n ".join(users + things)
return string
class ContribRPRoom(ContribRPObject):
"""
Dummy inheritance for rooms.
"""
pass
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)
def get_display_name(self, looker, **kwargs):
"""
Displays the name of the object in a viewer-aware manner.
Args:
looker (TypedObject): The object or player that is looking
at/getting inforamtion for this object.
Kwargs:
pose (bool): Include the pose (if available) in the return.
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 RPCharacter version of this method colors its display to make
characters stand out from other objects.
"""
idstr = "(#%s)" % self.id if self.access(looker, access_type='control') else ""
try:
recog = looker.recog.get(self)
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 "{c%s{n%s%s" % (sdesc, idstr, pose)
def at_object_creation(self):
"""
Called at initial creation.
"""
super(ContribRPCharacter, self).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, permanent=True)
# initializing sdesc
self.sdesc.add("A normal person")
def process_sdesc(self, sdesc, obj, **kwargs):
"""
Allows to customize how your sdesc is displayed (primarily by
changing colors).
Args:
sdesc (str): The sdesc to display.
obj (Object): The object to which the adjoining sdesc
belongs (can be yourself).
Returns:
sdesc (str): The processed sdesc ready
for display.
"""
return "{b%s{n" % sdesc
def process_language(self, text, speaker, language, **kwargs):
"""
Allows to process the spoken text, for example
by obfuscating language based on your and the
speaker's language skills. Also a good place to
put coloring.
Args:
text (str): The text to process.
speaker (Object): The object delivering the text.
language (str): An identifier string for the language.
Return:
text (str): The optionally processed text.
Notes:
This is designed to work together with a string obfuscator
such as the `obfuscate_language` or `obfuscate_whisper` in
the evennia.contrib.rplanguage module.
"""
return "%s{w%s{n" % ("{W(%s)" % language if language else "", text)
#from evennia.contrib import rplanguage
#return "{w%s{n" % rplanguage.obfuscate_language(text, level=1.0)