evennia/evennia/commands/default/player.py

680 lines
24 KiB
Python

"""
Player (OOC) commands. These are stored on the Player object
and self.caller is thus always a Player, not an Object/Character.
These commands go in the PlayerCmdset and are accessible also
when puppeting a Character (although with lower priority)
These commands use the MuxCommandOOC parent that makes sure
to setup caller correctly. They use self.player to make sure
to always use the player object rather than self.caller (which
change depending on the level you are calling from)
The property self.character can be used to
access the character when these commands are triggered with
a connected character (such as the case of the @ooc command), it
is None if we are OOC.
Note that under MULTISESSION_MODE > 2, Player- commands should use
self.msg() and similar methods to reroute returns to the correct
method. Otherwise all text will be returned to all connected sessions.
"""
from builtins import range
import time
from django.conf import settings
from evennia.server.sessionhandler import SESSIONS
from evennia.commands.default.muxcommand import MuxPlayerCommand
from evennia.utils import utils, create, search, prettytable
_MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS
_MULTISESSION_MODE = settings.MULTISESSION_MODE
# limit symbol import for API
__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit",
"CmdCharCreate", "CmdOption", "CmdSessions", "CmdWho",
"CmdColorTest", "CmdQuell")
class MuxPlayerLookCommand(MuxPlayerCommand):
"""
Custom parent (only) parsing for OOC looking, sets a "playable"
property on the command based on the parsing.
"""
def parse(self):
"Custom parsing"
super(MuxPlayerLookCommand, self).parse()
if _MULTISESSION_MODE < 2:
# only one character allowed - not used in this mode
self.playable = None
return
playable = self.player.db._playable_characters
if playable is not None:
# clean up list if character object was deleted in between
if None in playable:
playable = [character for character in playable if character]
self.player.db._playable_characters = playable
# store playable property
if self.args:
self.playable = dict((utils.to_str(char.key.lower()), char)
for char in playable).get(self.args.lower(), None)
else:
self.playable = playable
# Obs - these are all intended to be stored on the Player, and as such,
# use self.player instead of self.caller, just to be sure. Also self.msg()
# is used to make sure returns go to the right session
# note that this is inheriting from MuxPlayerLookCommand,
# and has the .playable property.
class CmdOOCLook(MuxPlayerLookCommand):
"""
look while out-of-character
Usage:
look
Look in the ooc state.
"""
#This is an OOC version of the look command. Since a
#Player doesn't have an in-game existence, there is no
#concept of location or "self". If we are controlling
#a character, pass control over to normal look.
key = "look"
aliases = ["l", "ls"]
locks = "cmd:all()"
help_category = "General"
def func(self):
"implement the ooc look command"
if _MULTISESSION_MODE < 2:
# only one character allowed
self.msg("You are out-of-character (OOC).\nUse {w@ic{n to get back into the game.")
return
# call on-player look helper method
self.msg(self.player.at_look(target=self.playable, session=self.session))
class CmdCharCreate(MuxPlayerCommand):
"""
create a new character
Usage:
@charcreate <charname> [= desc]
Create a new character, optionally giving it a description. You
may use upper-case letters in the name - you will nevertheless
always be able to access your character using lower-case letters
if you want.
"""
key = "@charcreate"
locks = "cmd:pperm(Players)"
help_category = "General"
def func(self):
"create the new character"
player = self.player
if not self.args:
self.msg("Usage: @charcreate <charname> [= description]")
return
key = self.lhs
desc = self.rhs
charmax = _MAX_NR_CHARACTERS if _MULTISESSION_MODE > 1 else 1
if not player.is_superuser and \
(player.db._playable_characters and
len(player.db._playable_characters) >= charmax):
self.msg("You may only create a maximum of %i characters." % charmax)
return
# create the character
from evennia.objects.models import ObjectDB
start_location = ObjectDB.objects.get_id(settings.START_LOCATION)
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
typeclass = settings.BASE_CHARACTER_TYPECLASS
permissions = settings.PERMISSION_PLAYER_DEFAULT
new_character = create.create_object(typeclass, key=key,
location=start_location,
home=default_home,
permissions=permissions)
# only allow creator (and immortals) to puppet this char
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" %
(new_character.id, player.id))
player.db._playable_characters.append(new_character)
if desc:
new_character.db.desc = desc
elif not new_character.db.desc:
new_character.db.desc = "This is a Player."
self.msg("Created new character %s. Use {w@ic %s{n to enter the game as this character." % (new_character.key, new_character.key))
class CmdIC(MuxPlayerCommand):
"""
control an object you have permission to puppet
Usage:
@ic <character>
Go in-character (IC) as a given Character.
This will attempt to "become" a different object assuming you have
the right to do so. Note that it's the PLAYER character that puppets
characters/objects and which needs to have the correct permission!
You cannot become an object that is already controlled by another
player. In principle <character> can be any in-game object as long
as you the player have access right to puppet it.
"""
key = "@ic"
# lock must be all() for different puppeted objects to access it.
locks = "cmd:all()"
aliases = "@puppet"
help_category = "General"
def func(self):
"""
Main puppet method
"""
player = self.player
session = self.session
new_character = None
if not self.args:
new_character = player.db._last_puppet
if not new_character:
self.msg("Usage: @ic <character>")
return
if not new_character:
# search for a matching character
new_character = search.object_search(self.args)
if new_character:
new_character = new_character[0]
else:
self.msg("That is not a valid character choice.")
return
try:
player.puppet_object(session, new_character)
player.db._last_puppet = new_character
except RuntimeError as exc:
self.msg("{rYou cannot become {C%s{n: %s" % (new_character.name, exc))
# note that this is inheriting from MuxPlayerLookCommand,
# and as such has the .playable property.
class CmdOOC(MuxPlayerLookCommand):
"""
stop puppeting and go ooc
Usage:
@ooc
Go out-of-character (OOC).
This will leave your current character and put you in a incorporeal OOC state.
"""
key = "@ooc"
locks = "cmd:pperm(Players)"
aliases = "@unpuppet"
help_category = "General"
def func(self):
"Implement function"
player = self.player
session = self.session
old_char = player.get_puppet(session)
if not old_char:
string = "You are already OOC."
self.msg(string)
return
player.db._last_puppet = old_char
# disconnect
try:
player.unpuppet_object(session)
self.msg("\n{GYou go OOC.{n\n")
if _MULTISESSION_MODE < 2:
# only one character allowed
self.msg("You are out-of-character (OOC).\nUse {w@ic{n to get back into the game.")
return
self.msg(player.at_look(target=self.playable, session=session))
except RuntimeError as exc:
self.msg("{rCould not unpuppet from {c%s{n: %s" % (old_char, exc))
class CmdSessions(MuxPlayerCommand):
"""
check your connected session(s)
Usage:
@sessions
Lists the sessions currently connected to your account.
"""
key = "@sessions"
locks = "cmd:all()"
help_category = "General"
def func(self):
"Implement function"
player = self.player
sessions = player.sessions.all()
table = prettytable.PrettyTable(["{wsessid",
"{wprotocol",
"{whost",
"{wpuppet/character",
"{wlocation"])
for sess in sorted(sessions, key=lambda x: x.sessid):
char = player.get_puppet(sess)
table.add_row([str(sess.sessid), str(sess.protocol_key),
type(sess.address) == tuple and sess.address[0] or sess.address,
char and str(char) or "None",
char and str(char.location) or "N/A"])
string = "{wYour current session(s):{n\n%s" % table
self.msg(string)
class CmdWho(MuxPlayerCommand):
"""
list who is currently online
Usage:
who
doing
Shows who is currently online. Doing is an alias that limits info
also for those with all permissions.
"""
key = "who"
aliases = "doing"
locks = "cmd:all()"
def func(self):
"""
Get all connected players by polling session.
"""
player = self.player
session_list = SESSIONS.get_sessions()
session_list = sorted(session_list, key=lambda o: o.player.key)
if self.cmdstring == "doing":
show_session_data = False
else:
show_session_data = player.check_permstring("Immortals") or player.check_permstring("Wizards")
nplayers = (SESSIONS.player_count())
if show_session_data:
# privileged info
table = prettytable.PrettyTable(["{wPlayer Name",
"{wOn for",
"{wIdle",
"{wPuppeting",
"{wRoom",
"{wCmds",
"{wProtocol",
"{wHost"])
for session in session_list:
if not session.logged_in: continue
delta_cmd = time.time() - session.cmd_last_visible
delta_conn = time.time() - session.conn_time
player = session.get_player()
puppet = session.get_puppet()
location = puppet.location.key if puppet else "None"
table.add_row([utils.crop(player.name, width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1),
utils.crop(puppet.key if puppet else "None", width=25),
utils.crop(location, width=25),
session.cmd_total,
session.protocol_key,
isinstance(session.address, tuple) and session.address[0] or session.address])
else:
# unprivileged
table = prettytable.PrettyTable(["{wPlayer name", "{wOn for", "{wIdle"])
for session in session_list:
if not session.logged_in:
continue
delta_cmd = time.time() - session.cmd_last_visible
delta_conn = time.time() - session.conn_time
player = session.get_player()
table.add_row([utils.crop(player.key, width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1)])
isone = nplayers == 1
string = "{wPlayers:{n\n%s\n%s unique account%s logged in." % (table, "One" if isone else nplayers, "" if isone else "s")
self.msg(string)
class CmdOption(MuxPlayerCommand):
"""
Set an account option
Usage:
@option [name = value]
This command allows for viewing and setting client interface
settings.
"""
key = "@option"
aliases = "@options"
locks = "cmd:all()"
def func(self):
"""
Implements the command
"""
if self.session is None:
return
flags = self.session.protocol_flags
# Display current options
if not self.args:
# list the option settings
options = dict(flags)
if "SCREENWIDTH" in options:
if len(options["SCREENWIDTH"]) == 1:
options["SCREENWIDTH"] = options["SCREENWIDTH"][0]
else:
options["SCREENWIDTH"] = " \n".join("%s : %s" % (screenid, size)
for screenid, size in options["SCREENWIDTH"].iteritems())
if "SCREENHEIGHT" in options:
if len(options["SCREENHEIGHT"]) == 1:
options["SCREENHEIGHT"] = options["SCREENHEIGHT"][0]
else:
options["SCREENHEIGHT"] = " \n".join("%s : %s" % (screenid, size)
for screenid, size in options["SCREENHEIGHT"].iteritems())
options.pop("TTYPE", None)
options = "\n".join(" {w%s{n: %s" % (key, options[key]) for key in sorted(options))
self.msg("{wClient settings (%s):{n\n%s{n" % (self.session.protocol_key, options))
return
if not self.rhs:
self.msg("Usage: @option [name = [value]]")
return
# Try to assign new values
def validate_encoding(val):
# helper: change encoding
try:
utils.to_str(utils.to_unicode("test-string"), encoding=val)
except LookupError:
raise RuntimeError("The encoding '|w%s|n' is invalid. " % val)
return val
def validate_size(val):
return {0: int(val)}
def validate_bool(val):
return True if val.lower() in ("true", "on", "1") else False
def update(name, val, validator):
# helper: update property and report errors
try:
old_val = flags[name]
new_val = validator(val)
flags[name] = new_val
self.msg("Option |w%s|n was changed from '|w%s|n' to '|w%s|n'." % (name, old_val, new_val))
return True
except Exception, err:
self.msg("|rCould not set option |w%s|r:|n %s" % (name, err))
return False
validators = {"ANSI": validate_bool,
"CLIENTNAME": utils.to_str,
"ENCODING": validate_encoding,
"MCCP": validate_bool,
"MXP": validate_bool,
"OOB": validate_bool,
"SCREENHEIGHT": validate_size,
"SCREENWIDTH": validate_size,
"SCREENREADER": validate_bool,
"TERM": utils.to_str,
"UTF-8": validate_bool,
"XTERM256": validate_bool,
"INPUTDEBUG": validate_bool}
name = self.lhs.upper()
val = self.rhs.strip()
do_sync = False
if val and name in validators:
do_sync = update(name, val, validators[name])
else:
self.session.msg("|rNo option named '|w%s|r'." % name)
if do_sync:
self.session.sessionhandler.session_portal_sync(self.session)
class CmdPassword(MuxPlayerCommand):
"""
change your password
Usage:
@password <old password> = <new password>
Changes your password. Make sure to pick a safe one.
"""
key = "@password"
locks = "cmd:pperm(Players)"
def func(self):
"hook function."
player = self.player
if not self.rhs:
self.msg("Usage: @password <oldpass> = <newpass>")
return
oldpass = self.lhslist[0] # this is already stripped by parse()
newpass = self.rhslist[0] # ''
if not player.check_password(oldpass):
self.msg("The specified old password isn't correct.")
elif len(newpass) < 3:
self.msg("Passwords must be at least three characters long.")
else:
player.set_password(newpass)
player.save()
self.msg("Password changed.")
class CmdQuit(MuxPlayerCommand):
"""
quit the game
Usage:
@quit
Switch:
all - disconnect all connected sessions
Gracefully disconnect your current session from the
game. Use the /all switch to disconnect from all sessions.
"""
key = "@quit"
aliases = "quit"
locks = "cmd:all()"
def func(self):
"hook function"
player = self.player
if 'all' in self.switches:
player.msg("{RQuitting{n all sessions. Hope to see you soon again.", session=self.session)
for session in player.sessions.all():
player.disconnect_session_from_player(session)
else:
nsess = len(player.sessions.all())
if nsess == 2:
player.msg("{RQuitting{n. One session is still connected.", session=self.session)
elif nsess > 2:
player.msg("{RQuitting{n. %i session are still connected." % (nsess-1), session=self.session)
else:
# we are quitting the last available session
player.msg("{RQuitting{n. Hope to see you again, soon.", session=self.session)
player.disconnect_session_from_player(self.session)
class CmdColorTest(MuxPlayerCommand):
"""
testing which colors your client support
Usage:
@color ansi|xterm256
Prints a color map along with in-mud color codes to use to produce
them. It also tests what is supported in your client. Choices are
16-color ansi (supported in most muds) or the 256-color xterm256
standard. No checking is done to determine your client supports
color - if not you will see rubbish appear.
"""
key = "@color"
aliases = "color"
locks = "cmd:all()"
help_category = "General"
def table_format(self, table):
"""
Helper method to format the ansi/xterm256 tables.
Takes a table of columns [[val,val,...],[val,val,...],...]
"""
if not table:
return [[]]
extra_space = 1
max_widths = [max([len(str(val)) for val in col]) for col in table]
ftable = []
for irow in range(len(table[0])):
ftable.append([str(col[irow]).ljust(max_widths[icol]) + " " * extra_space
for icol, col in enumerate(table)])
return ftable
def func(self):
"Show color tables"
if self.args.startswith("a"):
# show ansi 16-color table
from evennia.utils import ansi
ap = ansi.ANSI_PARSER
# ansi colors
# show all ansi color-related codes
col1 = ["%s%s|n" % (code, code.replace("|", "||")) for code, _ in ap.ext_ansi_map[48:56]]
col2 = ["%s%s|n" % (code, code.replace("|", "||")) for code, _ in ap.ext_ansi_map[56:64]]
col3 = ["%s%s|n" % (code.replace("\\",""), code.replace("|", "||").replace("\\", "")) for code, _ in ap.ext_ansi_map[-8:]]
col2.extend(["" for i in range(len(col1)-len(col2))])
table = utils.format_table([col1, col2, col3])
string = "ANSI colors:"
for row in table:
string += "\n " + " ".join(row)
self.msg(string)
self.msg("||X : black. ||/ : return, ||- : tab, ||_ : space, ||* : invert, ||u : underline")
self.msg("To combine background and foreground, add background marker last, e.g. ||r||[B.")
elif self.args.startswith("x"):
# show xterm256 table
table = [[], [], [], [], [], [], [], [], [], [], [], []]
for ir in range(6):
for ig in range(6):
for ib in range(6):
# foreground table
table[ir].append("|%i%i%i%s|n" % (ir, ig, ib, "||%i%i%i" % (ir, ig, ib)))
# background table
table[6+ir].append("|%i%i%i|[%i%i%i%s|n" % (5 - ir, 5 - ig, 5 - ib,
ir, ig, ib,
"||[%i%i%i" % (ir, ig, ib)))
table = self.table_format(table)
string = "Xterm256 colors (if not all hues show, your client might not report that it can handle xterm256):"
for row in table:
string += "\n" + "".join(row)
self.msg(string)
#self.msg("(e.g. %%123 and %%[123 also work)")
else:
# malformed input
self.msg("Usage: @color ansi||xterm256")
class CmdQuell(MuxPlayerCommand):
"""
use character's permissions instead of player's
Usage:
quell
unquell
Normally the permission level of the Player is used when puppeting a
Character/Object to determine access. This command will switch the lock
system to make use of the puppeted Object's permissions instead. This is
useful mainly for testing.
Hierarchical permission quelling only work downwards, thus a Player cannot
use a higher-permission Character to escalate their permission level.
Use the unquell command to revert back to normal operation.
"""
key = "@quell"
aliases = ["@unquell"]
locks = "cmd:pperm(Players)"
help_category = "General"
def _recache_locks(self, player):
"Helper method to reset the lockhandler on an already puppeted object"
if self.session:
char = self.session.puppet
if char:
# we are already puppeting an object. We need to reset
# the lock caches (otherwise the superuser status change
# won't be visible until repuppet)
char.locks.reset()
player.locks.reset()
def func(self):
"Perform the command"
player = self.player
permstr = player.is_superuser and " (superuser)" or " (%s)" % (", ".join(player.permissions.all()))
if self.cmdstring == '@unquell':
if not player.attributes.get('_quell'):
self.msg("Already using normal Player permissions%s." % permstr)
else:
player.attributes.remove('_quell')
self.msg("Player permissions%s restored." % permstr)
else:
if player.attributes.get('_quell'):
self.msg("Already quelling Player%s permissions." % permstr)
return
player.attributes.add('_quell', True)
puppet = self.session.puppet
if puppet:
cpermstr = " (%s)" % ", ".join(puppet.permissions.all())
cpermstr = "Quelling to current puppet's permissions%s." % cpermstr
cpermstr += "\n(Note: If this is higher than Player permissions%s, the lowest of the two will be used.)" % permstr
cpermstr += "\nUse @unquell to return to normal permission usage."
self.msg(cpermstr)
else:
self.msg("Quelling Player permissions%s. Use @unquell to get them back." % permstr)
self._recache_locks(player)