mirror of
https://github.com/evennia/evennia.git
synced 2025-10-29 19:35:56 +00:00
724 lines
22 KiB
Python
724 lines
22 KiB
Python
"""
|
|
General Character commands usually available to all characters
|
|
"""
|
|
import re
|
|
|
|
from django.conf import settings
|
|
|
|
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",
|
|
)
|
|
|
|
|
|
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"$"
|
|
|
|
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")
|
|
|
|
|
|
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|$"
|
|
|
|
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)
|
|
|
|
|
|
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()"
|
|
|
|
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("\=", "=")
|
|
|
|
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 '|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))
|
|
|
|
|
|
class CmdInventory(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
view inventory
|
|
|
|
Usage:
|
|
inventory
|
|
inv
|
|
|
|
Shows your inventory.
|
|
"""
|
|
|
|
key = "inventory"
|
|
aliases = ["inv", "i"]
|
|
locks = "cmd:all()"
|
|
arg_regex = r"$"
|
|
|
|
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 item in items:
|
|
table.add_row(
|
|
f"|C{item.name}|n",
|
|
"{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or ""),
|
|
)
|
|
string = f"|wYou are carrying:\n{table}"
|
|
self.caller.msg(text=(string, {"type": "inventory"}))
|
|
|
|
|
|
class CmdGet(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
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();view:perm(Developer);read:perm(Developer)"
|
|
arg_regex = r"\s|$"
|
|
|
|
def func(self):
|
|
"""implements the command."""
|
|
|
|
caller = self.caller
|
|
|
|
if not self.args:
|
|
caller.msg("Get what?")
|
|
return
|
|
obj = caller.search(self.args, location=caller.location)
|
|
if not obj:
|
|
return
|
|
if caller == obj:
|
|
caller.msg("You can't get yourself.")
|
|
return
|
|
if not obj.access(caller, "get"):
|
|
if obj.db.get_err_msg:
|
|
caller.msg(obj.db.get_err_msg)
|
|
else:
|
|
caller.msg("You can't get that.")
|
|
return
|
|
|
|
# calling at_pre_get hook method
|
|
if not obj.at_pre_get(caller):
|
|
return
|
|
|
|
success = obj.move_to(caller, quiet=True, move_type="get")
|
|
if not success:
|
|
caller.msg("This can't be picked up.")
|
|
else:
|
|
caller.msg(f"You pick up {obj.name}.")
|
|
caller.location.msg_contents(f"{caller.name} picks up {obj.name}.", exclude=caller)
|
|
# calling at_get hook method
|
|
obj.at_get(caller)
|
|
|
|
|
|
class CmdDrop(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
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|$"
|
|
|
|
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
|
|
obj = 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}:",
|
|
)
|
|
if not obj:
|
|
return
|
|
|
|
# Call the object script's at_pre_drop() method.
|
|
if not obj.at_pre_drop(caller):
|
|
return
|
|
|
|
success = obj.move_to(caller.location, quiet=True, move_type="drop")
|
|
if not success:
|
|
caller.msg("This couldn't be dropped.")
|
|
else:
|
|
caller.msg("You drop %s." % (obj.name,))
|
|
caller.location.msg_contents(f"{caller.name} drops {obj.name}.", exclude=caller)
|
|
# Call the object script's at_drop() method.
|
|
obj.at_drop(caller)
|
|
|
|
|
|
class CmdGive(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
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|$"
|
|
|
|
def func(self):
|
|
"""Implement give"""
|
|
|
|
caller = self.caller
|
|
if not self.args or not self.rhs:
|
|
caller.msg("Usage: give <inventory object> = <target>")
|
|
return
|
|
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}:",
|
|
)
|
|
target = caller.search(self.rhs)
|
|
if not (to_give and target):
|
|
return
|
|
if target == caller:
|
|
caller.msg(f"You keep {to_give.key} to yourself.")
|
|
return
|
|
if not to_give.location == caller:
|
|
caller.msg(f"You are not holding {to_give.key}.")
|
|
return
|
|
|
|
# calling at_pre_give hook method
|
|
if not to_give.at_pre_give(caller, target):
|
|
return
|
|
|
|
# give object
|
|
success = to_give.move_to(target, quiet=True, move_type="give")
|
|
if not success:
|
|
caller.msg(f"You could not give {to_give.key}.")
|
|
else:
|
|
caller.msg(f"You give {to_give.key} to {target.key}.")
|
|
target.msg(f"{caller.key} gives you {to_give.key}.")
|
|
# Call the object script's at_give() method.
|
|
to_give.at_give(caller, target)
|
|
|
|
|
|
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|$"
|
|
|
|
def func(self):
|
|
"""add the description"""
|
|
|
|
if not self.args:
|
|
self.caller.msg("You must add a description.")
|
|
return
|
|
|
|
self.caller.db.desc = self.args.strip()
|
|
self.caller.msg("You set your description.")
|
|
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
|
|
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()"
|
|
|
|
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)
|
|
|
|
|
|
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
|
|
|
|
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
|
|
|
|
def func(self):
|
|
"""Hook function"""
|
|
if not self.args:
|
|
msg = "What do you want to do?"
|
|
self.caller.msg(msg)
|
|
else:
|
|
msg = f"{self.caller.name}{self.args}"
|
|
self.caller.location.msg_contents(text=(msg, {"type": "pose"}), from_obj=self.caller)
|
|
|
|
|
|
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"$"
|
|
|
|
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 hasattr(caller, "account"):
|
|
string += f"\nAccount |c{caller.account.key}|n: {pperms}"
|
|
caller.msg(string)
|