"""
General Character commands usually available to all characters
"""
import re
from django.conf import settings
import evennia
from evennia.typeclasses.attributes import NickTemplateInvalid
from evennia.utils import utils
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
# limit symbol import for API
__all__ = (
"CmdHome",
"CmdLook",
"CmdNick",
"CmdInventory",
"CmdSetDesc",
"CmdGet",
"CmdDrop",
"CmdGive",
"CmdSay",
"CmdWhisper",
"CmdPose",
"CmdAccess",
)
[docs]class CmdHome(COMMAND_DEFAULT_CLASS):
"""
move to your character's home location
Usage:
home
Teleports you to your home location.
"""
key = "home"
locks = "cmd:perm(home) or perm(Builder)"
arg_regex = r"$"
[docs] def func(self):
"""Implement the command"""
caller = self.caller
home = caller.home
if not home:
caller.msg("You have no home!")
elif home == caller.location:
caller.msg("You are already home!")
else:
caller.msg("There's no place like home ...")
caller.move_to(home, move_type="teleport")
[docs]class CmdLook(COMMAND_DEFAULT_CLASS):
"""
look at location or object
Usage:
look
look <obj>
look *<account>
Observes your location or objects in your vicinity.
"""
key = "look"
aliases = ["l", "ls"]
locks = "cmd:all()"
arg_regex = r"\s|$"
[docs] def func(self):
"""
Handle the looking.
"""
caller = self.caller
if not self.args:
target = caller.location
if not target:
caller.msg("You have no location to look at!")
return
else:
target = caller.search(self.args)
if not target:
return
desc = caller.at_look(target)
# add the type=look to the outputfunc to make it
# easy to separate this output in client.
self.msg(text=(desc, {"type": "look"}), options=None)
[docs]class CmdNick(COMMAND_DEFAULT_CLASS):
"""
define a personal alias/nick by defining a string to
match and replace it with another on the fly
Usage:
nick[/switches] <string> [= [replacement_string]]
nick[/switches] <template> = <replacement_template>
nick/delete <string> or number
nicks
Switches:
inputline - replace on the inputline (default)
object - replace on object-lookup
account - replace on account-lookup
list - show all defined aliases (also "nicks" works)
delete - remove nick by index in /list
clearall - clear all nicks
Examples:
nick hi = say Hello, I'm Sarah!
nick/object tom = the tall man
nick build $1 $2 = create/drop $1;$2
nick tell $1 $2=page $1=$2
nick tm?$1=page tallman=$1
nick tm\=$1=page tallman=$1
A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
Put the last $-marker without an ending space to catch all remaining text. You
can also use unix-glob matching for the left-hand side <string>:
* - matches everything
? - matches 0 or 1 single characters
[abcd] - matches these chars in any order
[!abcd] - matches everything not among these chars
\= - escape literal '=' you want in your <string>
Note that no objects are actually renamed or changed by this command - your nicks
are only available to you. If you want to permanently add keywords to an object
for everyone to use, you need build privileges and the alias command.
"""
key = "nick"
switch_options = ("inputline", "object", "account", "list", "delete", "clearall")
aliases = ["nickname", "nicks"]
locks = "cmd:all()"
[docs] def parse(self):
"""
Support escaping of = with \=
"""
super().parse()
args = (self.lhs or "") + (" = %s" % self.rhs if self.rhs else "")
parts = re.split(r"(?<!\\)=", args, 1)
self.rhs = None
if len(parts) < 2:
self.lhs = parts[0].strip()
else:
self.lhs, self.rhs = [part.strip() for part in parts]
self.lhs = self.lhs.replace("\=", "=")
[docs] def func(self):
"""Create the nickname"""
def _cy(string):
"add color to the special markers"
return re.sub(r"(\$[0-9]+|\*|\?|\[.+?\])", r"|Y\1|n", string)
caller = self.caller
switches = self.switches
nicktypes = [switch for switch in switches if switch in ("object", "account", "inputline")]
specified_nicktype = bool(nicktypes)
nicktypes = nicktypes if specified_nicktype else ["inputline"]
nicklist = (
utils.make_iter(caller.nicks.get(category="inputline", return_obj=True) or [])
+ utils.make_iter(caller.nicks.get(category="object", return_obj=True) or [])
+ utils.make_iter(caller.nicks.get(category="account", return_obj=True) or [])
)
if "list" in switches or self.cmdstring in ("nicks",):
if not nicklist:
string = "|wNo nicks defined.|n"
else:
table = self.styled_table("#", "Type", "Nick match", "Replacement")
for inum, nickobj in enumerate(nicklist):
_, _, nickvalue, replacement = nickobj.value
table.add_row(
str(inum + 1), nickobj.db_category, _cy(nickvalue), _cy(replacement)
)
string = "|wDefined Nicks:|n\n%s" % table
caller.msg(string)
return
if "clearall" in switches:
caller.nicks.clear()
caller.account.nicks.clear()
caller.msg("Cleared all nicks.")
return
if "delete" in switches or "del" in switches:
if not self.args or not self.lhs:
caller.msg("usage nick/delete <nick> or <#num> ('nicks' for list)")
return
# see if a number was given
arg = self.args.lstrip("#")
oldnicks = []
if arg.isdigit():
# we are given a index in nicklist
delindex = int(arg)
if 0 < delindex <= len(nicklist):
oldnicks.append(nicklist[delindex - 1])
else:
caller.msg("Not a valid nick index. See 'nicks' for a list.")
return
else:
if not specified_nicktype:
nicktypes = ("object", "account", "inputline")
for nicktype in nicktypes:
oldnicks.append(caller.nicks.get(arg, category=nicktype, return_obj=True))
oldnicks = [oldnick for oldnick in oldnicks if oldnick]
if oldnicks:
for oldnick in oldnicks:
nicktype = oldnick.category
nicktypestr = "%s-nick" % nicktype.capitalize()
_, _, old_nickstring, old_replstring = oldnick.value
caller.nicks.remove(old_nickstring, category=nicktype)
caller.msg(
f"{nicktypestr} removed: '|w{old_nickstring}|n' -> |w{old_replstring}|n."
)
else:
caller.msg("No matching nicks to remove.")
return
if not self.rhs and self.lhs:
# check what a nick is set to
strings = []
if not specified_nicktype:
nicktypes = ("object", "account", "inputline")
for nicktype in nicktypes:
nicks = [
nick
for nick in utils.make_iter(
caller.nicks.get(category=nicktype, return_obj=True)
)
if nick
]
for nick in nicks:
_, _, nick, repl = nick.value
if nick.startswith(self.lhs):
strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'")
if strings:
caller.msg("\n".join(strings))
else:
caller.msg(f"No nicks found matching '{self.lhs}'")
return
if not self.rhs and self.lhs:
# check what a nick is set to
strings = []
if not specified_nicktype:
nicktypes = ("object", "account", "inputline")
for nicktype in nicktypes:
if nicktype == "account":
obj = account
else:
obj = caller
nicks = utils.make_iter(obj.nicks.get(category=nicktype, return_obj=True))
for nick in nicks:
_, _, nick, repl = nick.value
if nick.startswith(self.lhs):
strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'")
if strings:
caller.msg("\n".join(strings))
else:
caller.msg(f"No nicks found matching '{self.lhs}'")
return
if not self.rhs and self.lhs:
# check what a nick is set to
strings = []
if not specified_nicktype:
nicktypes = ("object", "account", "inputline")
for nicktype in nicktypes:
if nicktype == "account":
obj = account
else:
obj = caller
nicks = utils.make_iter(obj.nicks.get(category=nicktype, return_obj=True))
for nick in nicks:
_, _, nick, repl = nick.value
if nick.startswith(self.lhs):
strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'")
if strings:
caller.msg("\n".join(strings))
else:
caller.msg(f"No nicks found matching '{self.lhs}'")
return
if not self.args or not self.lhs:
caller.msg("Usage: nick[/switches] nickname = [realname]")
return
# setting new nicks
nickstring = self.lhs
replstring = self.rhs
if replstring == nickstring:
caller.msg("No point in setting nick same as the string to replace...")
return
# check so we have a suitable nick type
errstring = ""
string = ""
for nicktype in nicktypes:
nicktypestr = f"{nicktype.capitalize()}-nick"
old_nickstring = None
old_replstring = None
oldnick = caller.nicks.get(key=nickstring, category=nicktype, return_obj=True)
if oldnick:
_, _, old_nickstring, old_replstring = oldnick.value
if replstring:
# creating new nick
errstring = ""
if oldnick:
if replstring == old_replstring:
string += f"\nIdentical {nicktypestr.lower()} already set."
else:
string += (
f"\n{nicktypestr} '|w{old_nickstring}|n' updated to map to"
f" '|w{replstring}|n'."
)
else:
string += f"\n{nicktypestr} '|w{nickstring}|n' mapped to '|w{replstring}|n'."
try:
caller.nicks.add(nickstring, replstring, category=nicktype)
except NickTemplateInvalid:
caller.msg(
"You must use the same $-markers both in the nick and in the replacement."
)
return
elif old_nickstring and old_replstring:
# just looking at the nick
string += f"\n{nicktypestr} '|w{old_nickstring}|n' maps to '|w{old_replstring}|n'."
errstring = ""
string = errstring if errstring else string
caller.msg(_cy(string))
[docs]class CmdInventory(COMMAND_DEFAULT_CLASS):
"""
view inventory
Usage:
inventory
inv
Shows your inventory.
"""
key = "inventory"
aliases = ["inv", "i"]
locks = "cmd:all()"
arg_regex = r"$"
[docs] def func(self):
"""check inventory"""
items = self.caller.contents
if not items:
string = "You are not carrying anything."
else:
from evennia.utils.ansi import raw as raw_ansi
table = self.styled_table(border="header")
for key, desc, _ in utils.group_objects_by_key_and_desc(items, caller=self.caller):
table.add_row(
f"|C{key}|n",
"{}|n".format(utils.crop(raw_ansi(desc or ""), width=50) or ""),
)
string = f"|wYou are carrying:\n{table}"
self.msg(text=(string, {"type": "inventory"}))
class NumberedTargetCommand(COMMAND_DEFAULT_CLASS):
"""
A class that parses out an optional number component from the input string. This
class is intended to be inherited from to provide additional functionality, rather
than used on its own.
"""
def parse(self):
"""
Parser that extracts a `.number` property from the beginning of the input string.
For example, if the input string is "3 apples", this parser will set `self.number = 3` and
`self.args = "apples"`. If the input string is "apples", this parser will set
`self.number = 0` and `self.args = "apples"`.
"""
super().parse()
self.number = 0
if getattr(self, "lhs", None):
# handle self.lhs but don't require it
count, *args = self.lhs.split(maxsplit=1)
# we only use the first word as a count if it's a number and
# there is more text afterwards
if args and count.isdecimal():
self.number = int(count)
self.lhs = args[0]
if self.args:
# check for numbering
count, *args = self.args.split(maxsplit=1)
# we only use the first word as a count if it's a number and
# there is more text afterwards
if args and count.isdecimal():
self.args = args[0]
# we only re-assign self.number if it wasn't already taken from self.lhs
if not self.number:
self.number = int(count)
[docs]class CmdGet(NumberedTargetCommand):
"""
pick up something
Usage:
get <obj>
Picks up an object from your location and puts it in your inventory.
"""
key = "get"
aliases = "grab"
locks = "cmd:all()"
arg_regex = r"\s|$"
[docs] def func(self):
"""implements the command."""
caller = self.caller
if not self.args:
self.msg("Get what?")
return
objs = caller.search(self.args, location=caller.location, stacked=self.number)
if not objs:
return
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
# NOTE: this behavior may be a bug, see issue #3432
objs = utils.make_iter(objs)
if len(objs) == 1 and caller == objs[0]:
self.msg("You can't get yourself.")
return
# if we aren't allowed to get any of the objects, cancel the get
for obj in objs:
# check the locks
if not obj.access(caller, "get"):
if obj.db.get_err_msg:
self.msg(obj.db.get_err_msg)
else:
self.msg("You can't get that.")
return
# calling at_pre_get hook method
if not obj.at_pre_get(caller):
return
moved = []
# attempt to move all of the objects
for obj in objs:
if obj.move_to(caller, quiet=True, move_type="get"):
moved.append(obj)
# calling at_get hook method
obj.at_get(caller)
if not moved:
# none of the objects were successfully moved
self.msg("That can't be picked up.")
else:
obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True)
caller.location.msg_contents(f"$You() $conj(pick) up {obj_name}.", from_obj=caller)
[docs]class CmdDrop(NumberedTargetCommand):
"""
drop something
Usage:
drop <obj>
Lets you drop an object from your inventory into the
location you are currently in.
"""
key = "drop"
locks = "cmd:all()"
arg_regex = r"\s|$"
[docs] def func(self):
"""Implement command"""
caller = self.caller
if not self.args:
caller.msg("Drop what?")
return
# Because the DROP command by definition looks for items
# in inventory, call the search function using location = caller
objs = caller.search(
self.args,
location=caller,
nofound_string=f"You aren't carrying {self.args}.",
multimatch_string=f"You carry more than one {self.args}:",
stacked=self.number,
)
if not objs:
return
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
# NOTE: this behavior may be a bug, see issue #3432
objs = utils.make_iter(objs)
# if any objects fail the drop permission check, cancel the drop
for obj in objs:
# Call the object's at_pre_drop() method.
if not obj.at_pre_drop(caller):
return
# do the actual dropping
moved = []
for obj in objs:
if obj.move_to(caller.location, quiet=True, move_type="drop"):
moved.append(obj)
# Call the object's at_drop() method.
obj.at_drop(caller)
if not moved:
# none of the objects were successfully moved
self.msg("That can't be dropped.")
else:
obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True)
caller.location.msg_contents(f"$You() $conj(drop) {obj_name}.", from_obj=caller)
[docs]class CmdGive(NumberedTargetCommand):
"""
give away something to someone
Usage:
give <inventory obj> <to||=> <target>
Gives an item from your inventory to another person,
placing it in their inventory.
"""
key = "give"
rhs_split = ("=", " to ") # Prefer = delimiter, but allow " to " usage.
locks = "cmd:all()"
arg_regex = r"\s|$"
[docs] def func(self):
"""Implement give"""
caller = self.caller
if not self.args or not self.rhs:
caller.msg("Usage: give <inventory object> = <target>")
return
# find the thing(s) to give away
to_give = caller.search(
self.lhs,
location=caller,
nofound_string=f"You aren't carrying {self.lhs}.",
multimatch_string=f"You carry more than one {self.lhs}:",
stacked=self.number,
)
if not to_give:
return
# find the target to give to
target = caller.search(self.rhs)
if not target:
return
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
# NOTE: this behavior may be a bug, see issue #3432
to_give = utils.make_iter(to_give)
singular, plural = to_give[0].get_numbered_name(len(to_give), caller)
if target == caller:
caller.msg(f"You keep {plural if len(to_give) > 1 else singular} to yourself.")
return
# if any of the objects aren't allowed to be given, cancel the give
for obj in to_give:
# calling at_pre_give hook method
if not obj.at_pre_give(caller, target):
return
# do the actual moving
moved = []
for obj in to_give:
if obj.move_to(target, quiet=True, move_type="give"):
moved.append(obj)
# Call the object's at_give() method.
obj.at_give(caller, target)
if not moved:
caller.msg(f"You could not give that to {target.get_display_name(caller)}.")
else:
obj_name = to_give[0].get_numbered_name(len(moved), caller, return_string=True)
caller.msg(f"You give {obj_name} to {target.get_display_name(caller)}.")
target.msg(f"{caller.get_display_name(target)} gives you {obj_name}.")
[docs]class CmdSetDesc(COMMAND_DEFAULT_CLASS):
"""
describe yourself
Usage:
setdesc <description>
Add a description to yourself. This
will be visible to people when they
look at you.
"""
key = "setdesc"
locks = "cmd:all()"
arg_regex = r"\s|$"
[docs] def func(self):
"""add the description"""
if not self.args:
self.msg("You must add a description.")
return
self.caller.db.desc = self.args.strip()
self.msg("You set your description.")
[docs]class CmdSay(COMMAND_DEFAULT_CLASS):
"""
speak as your character
Usage:
say <message>
Talk to those in your current location.
"""
key = "say"
aliases = ['"', "'"]
locks = "cmd:all()"
# don't require a space after `say/'/"`
arg_regex = None
[docs] def func(self):
"""Run the say command"""
caller = self.caller
if not self.args:
caller.msg("Say what?")
return
speech = self.args
# Calling the at_pre_say hook on the character
speech = caller.at_pre_say(speech)
# If speech is empty, stop here
if not speech:
return
# Call the at_post_say hook on the character
caller.at_say(speech, msg_self=True)
[docs]class CmdWhisper(COMMAND_DEFAULT_CLASS):
"""
Speak privately as your character to another
Usage:
whisper <character> = <message>
whisper <char1>, <char2> = <message>
Talk privately to one or more characters in your current location, without
others in the room being informed.
"""
key = "whisper"
locks = "cmd:all()"
[docs] def func(self):
"""Run the whisper command"""
caller = self.caller
if not self.lhs or not self.rhs:
caller.msg("Usage: whisper <character> = <message>")
return
receivers = [recv.strip() for recv in self.lhs.split(",")]
receivers = [caller.search(receiver) for receiver in set(receivers)]
receivers = [recv for recv in receivers if recv]
speech = self.rhs
# If the speech is empty, abort the command
if not speech or not receivers:
return
# Call a hook to change the speech before whispering
speech = caller.at_pre_say(speech, whisper=True, receivers=receivers)
# no need for self-message if we are whispering to ourselves (for some reason)
msg_self = None if caller in receivers else True
caller.at_say(speech, msg_self=msg_self, receivers=receivers, whisper=True)
[docs]class CmdPose(COMMAND_DEFAULT_CLASS):
"""
strike a pose
Usage:
pose <pose text>
pose's <pose text>
Example:
pose is standing by the wall, smiling.
-> others will see:
Tom is standing by the wall, smiling.
Describe an action being taken. The pose text will
automatically begin with your name.
"""
key = "pose"
aliases = [":", "emote"]
locks = "cmd:all()"
arg_regex = ""
# we want to be able to pose without whitespace between
# the command/alias and the pose (e.g. :pose)
arg_regex = None
[docs] def parse(self):
"""
Custom parse the cases where the emote
starts with some special letter, such
as 's, at which we don't want to separate
the caller's name and the emote with a
space.
"""
args = self.args
if args and not args[0] in ["'", ",", ":"]:
args = " %s" % args.strip()
self.args = args
[docs] def func(self):
"""Hook function"""
if not self.args:
msg = "What do you want to do?"
self.msg(msg)
else:
msg = f"{self.caller.name}{self.args}"
self.caller.location.msg_contents(text=(msg, {"type": "pose"}), from_obj=self.caller)
[docs]class CmdAccess(COMMAND_DEFAULT_CLASS):
"""
show your current game access
Usage:
access
This command shows you the permission hierarchy and
which permission groups you are a member of.
"""
key = "access"
aliases = ["groups", "hierarchy"]
locks = "cmd:all()"
arg_regex = r"$"
[docs] def func(self):
"""Load the permission groups"""
caller = self.caller
hierarchy_full = settings.PERMISSION_HIERARCHY
string = "\n|wPermission Hierarchy|n (climbing):\n %s" % ", ".join(hierarchy_full)
if self.caller.account.is_superuser:
cperms = "<Superuser>"
pperms = "<Superuser>"
else:
cperms = ", ".join(caller.permissions.all())
pperms = ", ".join(caller.account.permissions.all())
string += "\n|wYour access|n:"
string += f"\nCharacter |c{caller.key}|n: {cperms}"
if utils.inherits_from(caller, evennia.DefaultObject):
string += f"\nAccount |c{caller.account.key}|n: {pperms}"
caller.msg(string)