Reshuffling the Evennia package into the new template paradigm.
This commit is contained in:
parent
2846e64833
commit
2b3a32e447
371 changed files with 17250 additions and 304 deletions
1
lib/commands/__init__.py
Normal file
1
lib/commands/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
479
lib/commands/cmdhandler.py
Normal file
479
lib/commands/cmdhandler.py
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
"""
|
||||
Command handler
|
||||
|
||||
This module contains the infrastructure for accepting commands on the
|
||||
command line. The process is as follows:
|
||||
|
||||
1) The calling object (caller) inputs a string and triggers the command parsing system.
|
||||
2) The system checks the state of the caller - loggedin or not
|
||||
3) If no command string was supplied, we search the merged cmdset for system command CMD_NOINPUT
|
||||
and branches to execute that. --> Finished
|
||||
4) Cmdsets are gathered from different sources (in order of dropping priority):
|
||||
channels - all available channel names are auto-created into a cmdset, to allow
|
||||
for giving the channel name and have the following immediately
|
||||
sent to the channel. The sending is performed by the CMD_CHANNEL
|
||||
system command.
|
||||
object cmdsets - all objects at caller's location are scanned for non-empty
|
||||
cmdsets. This includes cmdsets on exits.
|
||||
caller - the caller is searched for its own currently active cmdset.
|
||||
player - lastly the cmdsets defined on caller.player are added.
|
||||
5) All the gathered cmdsets (if more than one) are merged into one using the cmdset priority rules.
|
||||
6) If merged cmdset is empty, raise NoCmdSet exception (this should not happen, at least the
|
||||
player should have a default cmdset available at all times). --> Finished
|
||||
7) The raw input string is parsed using the parser defined by settings.COMMAND_PARSER. It
|
||||
uses the available commands from the merged cmdset to know which commands to look for and
|
||||
returns one or many matches.
|
||||
8) If match list is empty, branch to system command CMD_NOMATCH --> Finished
|
||||
9) If match list has more than one element, branch to system command CMD_MULTIMATCH --> Finished
|
||||
10) A single match was found. If this is a channel-command (i.e. the command name is that of a channel),
|
||||
branch to CMD_CHANNEL --> Finished
|
||||
11) At this point we have found a normal command. We assign useful variables to it that
|
||||
will be available to the command coder at run-time.
|
||||
12) We have a unique cmdobject, primed for use. Call all hooks:
|
||||
at_pre_cmd(), cmdobj.parse(), cmdobj.func() and finally at_post_cmd().
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from weakref import WeakValueDictionary
|
||||
from copy import copy
|
||||
from traceback import format_exc
|
||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||
from django.conf import settings
|
||||
from src.comms.channelhandler import CHANNELHANDLER
|
||||
from src.utils import logger, utils
|
||||
from src.commands.cmdparser import at_multimatch_cmd
|
||||
from src.utils.utils import string_suggestions, to_unicode
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
__all__ = ("cmdhandler",)
|
||||
_GA = object.__getattribute__
|
||||
_CMDSET_MERGE_CACHE = WeakValueDictionary()
|
||||
|
||||
# This decides which command parser is to be used.
|
||||
# You have to restart the server for changes to take effect.
|
||||
_COMMAND_PARSER = utils.variable_from_module(*settings.COMMAND_PARSER.rsplit('.', 1))
|
||||
|
||||
# System command names - import these variables rather than trying to
|
||||
# remember the actual string constants. If not defined, Evennia
|
||||
# hard-coded defaults are used instead.
|
||||
|
||||
# command to call if user just presses <return> with no input
|
||||
CMD_NOINPUT = "__noinput_command"
|
||||
# command to call if no command match was found
|
||||
CMD_NOMATCH = "__nomatch_command"
|
||||
# command to call if multiple command matches were found
|
||||
CMD_MULTIMATCH = "__multimatch_command"
|
||||
# command to call if found command is the name of a channel
|
||||
CMD_CHANNEL = "__send_to_channel_command"
|
||||
# command to call as the very first one when the user connects.
|
||||
# (is expected to display the login screen)
|
||||
CMD_LOGINSTART = "__unloggedin_look_command"
|
||||
|
||||
# custom Exceptions
|
||||
|
||||
|
||||
class NoCmdSets(Exception):
|
||||
"No cmdsets found. Critical error."
|
||||
pass
|
||||
|
||||
|
||||
class ExecSystemCommand(Exception):
|
||||
"Run a system command"
|
||||
def __init__(self, syscmd, sysarg):
|
||||
self.args = (syscmd, sysarg) # needed by exception error handling
|
||||
self.syscmd = syscmd
|
||||
self.sysarg = sysarg
|
||||
|
||||
# Helper function
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def get_and_merge_cmdsets(caller, session, player, obj,
|
||||
callertype, sessid=None):
|
||||
"""
|
||||
Gather all relevant cmdsets and merge them.
|
||||
|
||||
callertype is one of "session", "player" or "object" dependin
|
||||
on which level the cmdhandler is invoked. Session includes the
|
||||
cmdsets available to Session, Player and its eventual puppeted Object.
|
||||
Player-level include cmdsets on Player and Object, while calling
|
||||
the handler on an Object only includes cmdsets on itself.
|
||||
|
||||
The cdmsets are merged in order generality, so that the Object's
|
||||
cmdset is merged last (and will thus take precedence over
|
||||
same-named and same-prio commands on Player and Session).
|
||||
|
||||
Note that this function returns a deferred!
|
||||
"""
|
||||
local_obj_cmdsets = [None]
|
||||
|
||||
@inlineCallbacks
|
||||
def _get_channel_cmdsets(player, player_cmdset):
|
||||
"Channel-cmdsets"
|
||||
# Create cmdset for all player's available channels
|
||||
channel_cmdset = None
|
||||
if not player_cmdset.no_channels:
|
||||
channel_cmdset = yield CHANNELHANDLER.get_cmdset(player)
|
||||
returnValue(channel_cmdset)
|
||||
|
||||
@inlineCallbacks
|
||||
def _get_local_obj_cmdsets(obj, obj_cmdset):
|
||||
"Object-level cmdsets"
|
||||
# Gather cmdsets from location, objects in location or carried
|
||||
local_obj_cmdsets = [None]
|
||||
try:
|
||||
location = obj.location
|
||||
except Exception:
|
||||
location = None
|
||||
if location and not obj_cmdset.no_objs:
|
||||
# Gather all cmdsets stored on objects in the room and
|
||||
# also in the caller's inventory and the location itself
|
||||
local_objlist = yield (location.contents_get(exclude=obj) +
|
||||
obj.contents +
|
||||
[location])
|
||||
for lobj in local_objlist:
|
||||
try:
|
||||
# call hook in case we need to do dynamic changing to cmdset
|
||||
_GA(lobj, "at_cmdset_get")()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
# the call-type lock is checked here, it makes sure a player
|
||||
# is not seeing e.g. the commands on a fellow player (which is why
|
||||
# the no_superuser_bypass must be True)
|
||||
local_obj_cmdsets = \
|
||||
yield [lobj.cmdset.current for lobj in local_objlist
|
||||
if (lobj.cmdset.current and
|
||||
lobj.locks.check(caller, 'call', no_superuser_bypass=True))]
|
||||
for cset in local_obj_cmdsets:
|
||||
#This is necessary for object sets, or we won't be able to
|
||||
# separate the command sets from each other in a busy room.
|
||||
cset.old_duplicates = cset.duplicates
|
||||
cset.duplicates = True
|
||||
returnValue(local_obj_cmdsets)
|
||||
|
||||
@inlineCallbacks
|
||||
def _get_cmdset(obj):
|
||||
"Get cmdset, triggering all hooks"
|
||||
try:
|
||||
yield obj.at_cmdset_get()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
try:
|
||||
returnValue(obj.cmdset.current)
|
||||
except AttributeError:
|
||||
returnValue(None)
|
||||
|
||||
if callertype == "session":
|
||||
# we are calling the command from the session level
|
||||
report_to = session
|
||||
session_cmdset = yield _get_cmdset(session)
|
||||
cmdsets = [session_cmdset]
|
||||
if player: # this automatically implies logged-in
|
||||
player_cmdset = yield _get_cmdset(player)
|
||||
channel_cmdset = yield _get_channel_cmdsets(player, player_cmdset)
|
||||
cmdsets.extend([player_cmdset, channel_cmdset])
|
||||
if obj:
|
||||
obj_cmdset = yield _get_cmdset(obj)
|
||||
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj, obj_cmdset)
|
||||
cmdsets.extend([obj_cmdset] + local_obj_cmdsets)
|
||||
elif callertype == "player":
|
||||
# we are calling the command from the player level
|
||||
report_to = player
|
||||
player_cmdset = yield _get_cmdset(player)
|
||||
channel_cmdset = yield _get_channel_cmdsets(player, player_cmdset)
|
||||
cmdsets = [player_cmdset, channel_cmdset]
|
||||
if obj:
|
||||
obj_cmdset = yield _get_cmdset(obj)
|
||||
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj, obj_cmdset)
|
||||
cmdsets.extend([obj_cmdset] + local_obj_cmdsets)
|
||||
elif callertype == "object":
|
||||
# we are calling the command from the object level
|
||||
report_to = obj
|
||||
obj_cmdset = yield _get_cmdset(obj)
|
||||
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj, obj_cmdset)
|
||||
cmdsets = [obj_cmdset] + local_obj_cmdsets
|
||||
else:
|
||||
raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype)
|
||||
#cmdsets = yield [caller_cmdset] + [player_cmdset] +
|
||||
# [channel_cmdset] + local_obj_cmdsets
|
||||
|
||||
# weed out all non-found sets
|
||||
cmdsets = yield [cmdset for cmdset in cmdsets
|
||||
if cmdset and cmdset.key != "_EMPTY_CMDSET"]
|
||||
# report cmdset errors to user (these should already have been logged)
|
||||
yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets
|
||||
if cmdset.key == "_CMDSET_ERROR"]
|
||||
|
||||
if cmdsets:
|
||||
# faster to do tuple on list than to build tuple directly
|
||||
mergehash = tuple([id(cmdset) for cmdset in cmdsets])
|
||||
if mergehash in _CMDSET_MERGE_CACHE:
|
||||
# cached merge exist; use that
|
||||
cmdset = _CMDSET_MERGE_CACHE[mergehash]
|
||||
else:
|
||||
# we group and merge all same-prio cmdsets separately (this avoids
|
||||
# order-dependent clashes in certain cases, such as
|
||||
# when duplicates=True)
|
||||
tempmergers = {}
|
||||
for cmdset in cmdsets:
|
||||
prio = cmdset.priority
|
||||
#print cmdset.key, prio
|
||||
if prio in tempmergers:
|
||||
# merge same-prio cmdset together separately
|
||||
tempmergers[prio] = yield cmdset + tempmergers[prio]
|
||||
else:
|
||||
tempmergers[prio] = cmdset
|
||||
|
||||
# sort cmdsets after reverse priority (highest prio are merged in last)
|
||||
cmdsets = yield sorted(tempmergers.values(), key=lambda x: x.priority)
|
||||
|
||||
# Merge all command sets into one, beginning with the lowest-prio one
|
||||
cmdset = cmdsets[0]
|
||||
for merging_cmdset in cmdsets[1:]:
|
||||
#print "<%s(%s,%s)> onto <%s(%s,%s)>" % (merging_cmdset.key, merging_cmdset.priority, merging_cmdset.mergetype,
|
||||
# cmdset.key, cmdset.priority, cmdset.mergetype)
|
||||
cmdset = yield merging_cmdset + cmdset
|
||||
# store the full sets for diagnosis
|
||||
cmdset.merged_from = cmdsets
|
||||
# cache
|
||||
_CMDSET_MERGE_CACHE[mergehash] = cmdset
|
||||
else:
|
||||
cmdset = None
|
||||
|
||||
for cset in (cset for cset in local_obj_cmdsets if cset):
|
||||
cset.duplicates = cset.old_duplicates
|
||||
#print "merged set:", cmdset.key
|
||||
returnValue(cmdset)
|
||||
|
||||
|
||||
# Main command-handler function
|
||||
|
||||
@inlineCallbacks
|
||||
def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sessid=None, **kwargs):
|
||||
"""
|
||||
This is the main function to handle any string sent to the engine.
|
||||
|
||||
called_by - object on which this was called from. This is either a Session, a Player or an Object.
|
||||
raw_string - the command string given on the command line
|
||||
_testing - if we should actually execute the command or not.
|
||||
if True, the command instance will be returned instead.
|
||||
callertype - this is one of "session", "player" or "object", in decending
|
||||
order. So when the Session is the caller, it will merge its
|
||||
own cmdset into cmdsets from both Player and eventual puppeted
|
||||
Object (and cmdsets in its room etc). A Player will only
|
||||
include its own cmdset and the Objects and so on. Merge order
|
||||
is the same order, so that Object cmdsets are merged in last,
|
||||
giving them precendence for same-name and same-prio commands.
|
||||
sessid - Relevant if callertype is "player" - the session id will help
|
||||
retrieve the correct cmdsets from puppeted objects.
|
||||
**kwargs - other keyword arguments will be assigned as named variables on the
|
||||
retrieved command object *before* it is executed. This is unuesed
|
||||
in default Evennia but may be used by code to set custom flags or
|
||||
special operating conditions for a command as it executes.
|
||||
|
||||
Note that this function returns a deferred!
|
||||
"""
|
||||
|
||||
raw_string = to_unicode(raw_string, force_string=True)
|
||||
|
||||
session, player, obj = None, None, None
|
||||
if callertype == "session":
|
||||
session = called_by
|
||||
player = session.player
|
||||
if player:
|
||||
obj = yield player.get_puppet(session.sessid)
|
||||
elif callertype == "player":
|
||||
player = called_by
|
||||
if sessid:
|
||||
obj = yield player.get_puppet(sessid)
|
||||
elif callertype == "object":
|
||||
obj = called_by
|
||||
else:
|
||||
raise RuntimeError("cmdhandler: callertype %s is not valid." % callertype)
|
||||
|
||||
# the caller will be the one to receive messages and excert its permissions.
|
||||
# we assign the caller with preference 'bottom up'
|
||||
caller = obj or player or session
|
||||
|
||||
try: # catch bugs in cmdhandler itself
|
||||
try: # catch special-type commands
|
||||
|
||||
cmdset = yield get_and_merge_cmdsets(caller, session, player, obj,
|
||||
callertype, sessid)
|
||||
if not cmdset:
|
||||
# this is bad and shouldn't happen.
|
||||
raise NoCmdSets
|
||||
unformatted_raw_string = raw_string
|
||||
raw_string = raw_string.strip()
|
||||
if not raw_string:
|
||||
# Empty input. Test for system command instead.
|
||||
syscmd = yield cmdset.get(CMD_NOINPUT)
|
||||
sysarg = ""
|
||||
raise ExecSystemCommand(syscmd, sysarg)
|
||||
# Parse the input string and match to available cmdset.
|
||||
# This also checks for permissions, so all commands in match
|
||||
# are commands the caller is allowed to call.
|
||||
matches = yield _COMMAND_PARSER(raw_string, cmdset, caller)
|
||||
|
||||
# Deal with matches
|
||||
|
||||
if len(matches) > 1:
|
||||
# We have a multiple-match
|
||||
syscmd = yield cmdset.get(CMD_MULTIMATCH)
|
||||
sysarg = _("There were multiple matches.")
|
||||
if syscmd:
|
||||
# use custom CMD_MULTIMATCH
|
||||
syscmd.matches = matches
|
||||
else:
|
||||
# fall back to default error handling
|
||||
sysarg = yield at_multimatch_cmd(caller, matches)
|
||||
raise ExecSystemCommand(syscmd, sysarg)
|
||||
|
||||
if len(matches) == 1:
|
||||
# We have a unique command match. But it may still be invalid.
|
||||
match = matches[0]
|
||||
cmdname, args, cmd = match[0], match[1], match[2]
|
||||
|
||||
# check if we allow this type of command
|
||||
if cmdset.no_channels and hasattr(cmd, "is_channel") and cmd.is_channel:
|
||||
matches = []
|
||||
if cmdset.no_exits and hasattr(cmd, "is_exit") and cmd.is_exit:
|
||||
matches = []
|
||||
|
||||
if not matches:
|
||||
# No commands match our entered command
|
||||
syscmd = yield cmdset.get(CMD_NOMATCH)
|
||||
if syscmd:
|
||||
# use custom CMD_NOMATH command
|
||||
sysarg = raw_string
|
||||
else:
|
||||
# fallback to default error text
|
||||
sysarg = _("Command '%s' is not available.") % raw_string
|
||||
suggestions = string_suggestions(raw_string,
|
||||
cmdset.get_all_cmd_keys_and_aliases(caller),
|
||||
cutoff=0.7, maxnum=3)
|
||||
if suggestions:
|
||||
sysarg += _(" Maybe you meant %s?") % utils.list_to_string(suggestions, _('or'), addquote=True)
|
||||
else:
|
||||
sysarg += _(" Type \"help\" for help.")
|
||||
raise ExecSystemCommand(syscmd, sysarg)
|
||||
|
||||
# Check if this is a Channel-cmd match.
|
||||
if hasattr(cmd, 'is_channel') and cmd.is_channel:
|
||||
# even if a user-defined syscmd is not defined, the
|
||||
# found cmd is already a system command in its own right.
|
||||
syscmd = yield cmdset.get(CMD_CHANNEL)
|
||||
if syscmd:
|
||||
# replace system command with custom version
|
||||
cmd = syscmd
|
||||
cmd.sessid = session.sessid if session else None
|
||||
sysarg = "%s:%s" % (cmdname, args)
|
||||
raise ExecSystemCommand(cmd, sysarg)
|
||||
|
||||
# A normal command.
|
||||
|
||||
# Assign useful variables to the instance
|
||||
cmd.caller = caller
|
||||
cmd.cmdstring = cmdname
|
||||
cmd.args = args
|
||||
cmd.cmdset = cmdset
|
||||
cmd.sessid = session.sessid if session else sessid
|
||||
cmd.session = session
|
||||
cmd.player = player
|
||||
cmd.raw_string = unformatted_raw_string
|
||||
#cmd.obj # set via on-object cmdset handler for each command,
|
||||
# since this may be different for every command when
|
||||
# merging multuple cmdsets
|
||||
|
||||
if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'):
|
||||
# cmd.obj is automatically made available by the cmdhandler.
|
||||
# we make sure to validate its scripts.
|
||||
yield cmd.obj.scripts.validate()
|
||||
|
||||
if _testing:
|
||||
# only return the command instance
|
||||
returnValue(cmd)
|
||||
|
||||
# assign custom kwargs to found cmd object
|
||||
for key, val in kwargs.items():
|
||||
setattr(cmd, key, val)
|
||||
|
||||
# pre-command hook
|
||||
abort = yield cmd.at_pre_cmd()
|
||||
if abort:
|
||||
# abort sequence
|
||||
returnValue(abort)
|
||||
|
||||
# Parse and execute
|
||||
yield cmd.parse()
|
||||
# (return value is normally None)
|
||||
ret = yield cmd.func()
|
||||
|
||||
# post-command hook
|
||||
yield cmd.at_post_cmd()
|
||||
|
||||
if cmd.save_for_next:
|
||||
# store a reference to this command, possibly
|
||||
# accessible by the next command.
|
||||
caller.ndb.last_cmd = yield copy(cmd)
|
||||
else:
|
||||
caller.ndb.last_cmd = None
|
||||
|
||||
# Done! This returns a deferred. By default, Evennia does
|
||||
# not use this at all.
|
||||
returnValue(ret)
|
||||
|
||||
except ExecSystemCommand, exc:
|
||||
# Not a normal command: run a system command, if available,
|
||||
# or fall back to a return string.
|
||||
syscmd = exc.syscmd
|
||||
sysarg = exc.sysarg
|
||||
if syscmd:
|
||||
syscmd.caller = caller
|
||||
syscmd.cmdstring = syscmd.key
|
||||
syscmd.args = sysarg
|
||||
syscmd.cmdset = cmdset
|
||||
syscmd.sessid = session.sessid if session else None
|
||||
syscmd.raw_string = unformatted_raw_string
|
||||
|
||||
if hasattr(syscmd, 'obj') and hasattr(syscmd.obj, 'scripts'):
|
||||
# cmd.obj is automatically made available.
|
||||
# we make sure to validate its scripts.
|
||||
yield syscmd.obj.scripts.validate()
|
||||
|
||||
if _testing:
|
||||
# only return the command instance
|
||||
returnValue(syscmd)
|
||||
|
||||
# parse and run the command
|
||||
yield syscmd.parse()
|
||||
yield syscmd.func()
|
||||
elif sysarg:
|
||||
# return system arg
|
||||
caller.msg(exc.sysarg)
|
||||
|
||||
except NoCmdSets:
|
||||
# Critical error.
|
||||
string = "No command sets found! This is a sign of a critical bug.\n"
|
||||
string += "The error was logged.\n"
|
||||
string += "If logging out/in doesn't solve the problem, try to "
|
||||
string += "contact the server admin through some other means "
|
||||
string += "for assistance."
|
||||
caller.msg(_(string))
|
||||
logger.log_errmsg("No cmdsets found: %s" % caller)
|
||||
|
||||
except Exception:
|
||||
# We should not end up here. If we do, it's a programming bug.
|
||||
string = "%s\nAbove traceback is from an untrapped error."
|
||||
string += " Please file a bug report."
|
||||
logger.log_trace(_(string))
|
||||
caller.msg(string % format_exc())
|
||||
|
||||
except Exception:
|
||||
# This catches exceptions in cmdhandler exceptions themselves
|
||||
string = "%s\nAbove traceback is from a Command handler bug."
|
||||
string += " Please contact an admin and/or file a bug report."
|
||||
logger.log_trace(_(string))
|
||||
caller.msg(string % format_exc())
|
||||
281
lib/commands/cmdparser.py
Normal file
281
lib/commands/cmdparser.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
"""
|
||||
The default command parser. Use your own by assigning
|
||||
settings.ALTERNATE_PARSER to a Python path to a module containing the
|
||||
replacing cmdparser function. The replacement parser must matches
|
||||
on the sme form as the default cmdparser.
|
||||
"""
|
||||
|
||||
from src.utils.logger import log_trace
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
def cmdparser(raw_string, cmdset, caller, match_index=None):
|
||||
"""
|
||||
This function is called by the cmdhandler once it has
|
||||
gathered and merged all valid cmdsets valid for this particular parsing.
|
||||
|
||||
raw_string - the unparsed text entered by the caller.
|
||||
cmdset - the merged, currently valid cmdset
|
||||
caller - the caller triggering this parsing
|
||||
match_index - an optional integer index to pick a given match in a
|
||||
list of same-named command matches.
|
||||
|
||||
The cmdparser understand the following command combinations (where
|
||||
[] marks optional parts.
|
||||
|
||||
[cmdname[ cmdname2 cmdname3 ...] [the rest]
|
||||
|
||||
A command may consist of any number of space-separated words of any
|
||||
length, and contain any character. It may also be empty.
|
||||
|
||||
The parser makes use of the cmdset to find command candidates. The
|
||||
parser return a list of matches. Each match is a tuple with its
|
||||
first three elements being the parsed cmdname (lower case),
|
||||
the remaining arguments, and the matched cmdobject from the cmdset.
|
||||
"""
|
||||
|
||||
def create_match(cmdname, string, cmdobj):
|
||||
"""
|
||||
Evaluates the quality of a match by counting how many chars of cmdname
|
||||
matches string (counting from beginning of string). We also calculate
|
||||
a ratio from 0-1 describing how much cmdname matches string.
|
||||
We return a tuple (cmdname, count, ratio, args, cmdobj).
|
||||
|
||||
"""
|
||||
cmdlen, strlen = len(cmdname), len(string)
|
||||
mratio = 1 - (strlen - cmdlen) / (1.0 * strlen)
|
||||
args = string[cmdlen:]
|
||||
return (cmdname, args, cmdobj, cmdlen, mratio)
|
||||
|
||||
if not raw_string:
|
||||
return None
|
||||
|
||||
matches = []
|
||||
|
||||
# match everything that begins with a matching cmdname.
|
||||
l_raw_string = raw_string.lower()
|
||||
for cmd in cmdset:
|
||||
try:
|
||||
matches.extend([create_match(cmdname, raw_string, cmd)
|
||||
for cmdname in [cmd.key] + cmd.aliases
|
||||
if cmdname and l_raw_string.startswith(cmdname.lower())
|
||||
and (not cmd.arg_regex or
|
||||
cmd.arg_regex.match(l_raw_string[len(cmdname):]))])
|
||||
except Exception:
|
||||
log_trace("cmdhandler error. raw_input:%s" % raw_string)
|
||||
|
||||
if not matches:
|
||||
# no matches found.
|
||||
if '-' in raw_string:
|
||||
# This could be due to the user trying to identify the
|
||||
# command with a #num-<command> style syntax.
|
||||
mindex, new_raw_string = raw_string.split("-", 1)
|
||||
if mindex.isdigit():
|
||||
mindex = int(mindex) - 1
|
||||
# feed result back to parser iteratively
|
||||
return cmdparser(new_raw_string, cmdset,
|
||||
caller, match_index=mindex)
|
||||
|
||||
# only select command matches we are actually allowed to call.
|
||||
matches = [match for match in matches if match[2].access(caller, 'cmd')]
|
||||
|
||||
if len(matches) > 1:
|
||||
# See if it helps to analyze the match with preserved case but only if
|
||||
# it leaves at least one match.
|
||||
trimmed = [match for match in matches
|
||||
if raw_string.startswith(match[0])]
|
||||
if trimmed:
|
||||
matches = trimmed
|
||||
|
||||
if len(matches) > 1:
|
||||
# we still have multiple matches. Sort them by count quality.
|
||||
matches = sorted(matches, key=lambda m: m[3])
|
||||
# only pick the matches with highest count quality
|
||||
quality = [mat[3] for mat in matches]
|
||||
matches = matches[-quality.count(quality[-1]):]
|
||||
|
||||
if len(matches) > 1:
|
||||
# still multiple matches. Fall back to ratio-based quality.
|
||||
matches = sorted(matches, key=lambda m: m[4])
|
||||
# only pick the highest rated ratio match
|
||||
quality = [mat[4] for mat in matches]
|
||||
matches = matches[-quality.count(quality[-1]):]
|
||||
|
||||
if len(matches) > 1 and match_index != None and 0 <= match_index < len(matches):
|
||||
# We couldn't separate match by quality, but we have an
|
||||
# index argument to tell us which match to use.
|
||||
matches = [matches[match_index]]
|
||||
|
||||
# no matter what we have at this point, we have to return it.
|
||||
return matches
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Search parsers and support methods
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Default functions for formatting and processing searches.
|
||||
#
|
||||
# You can replace these from the settings file by setting the variables
|
||||
#
|
||||
# SEARCH_AT_RESULT
|
||||
# SEARCH_AT_MULTIMATCH_INPUT
|
||||
#
|
||||
# The the replacing functions must have the same inputs and outputs as
|
||||
# those in this module.
|
||||
#
|
||||
def at_search_result(msg_obj, ostring, results, global_search=False,
|
||||
nofound_string=None, multimatch_string=None, quiet=False):
|
||||
"""
|
||||
Called by search methods after a result of any type has been found.
|
||||
|
||||
Takes a search result (a list) and
|
||||
formats eventual errors.
|
||||
|
||||
msg_obj - object to receive feedback.
|
||||
ostring - original search string
|
||||
results - list of found matches (0, 1 or more)
|
||||
global_search - if this was a global_search or not
|
||||
(if it is, there might be an idea of supplying
|
||||
dbrefs instead of only numbers)
|
||||
nofound_string - optional custom string for not-found error message.
|
||||
multimatch_string - optional custom string for multimatch error header
|
||||
quiet - work normally, but don't echo to caller, just return the
|
||||
results.
|
||||
|
||||
Multiple matches are returned to the searching object
|
||||
as
|
||||
1-object
|
||||
2-object
|
||||
3-object
|
||||
etc
|
||||
|
||||
"""
|
||||
string = ""
|
||||
if not results:
|
||||
# no results.
|
||||
if nofound_string:
|
||||
# custom return string
|
||||
string = nofound_string
|
||||
else:
|
||||
string = _("Could not find '%s'." % ostring)
|
||||
results = None
|
||||
|
||||
elif len(results) > 1:
|
||||
# we have more than one match. We will display a
|
||||
# list of the form 1-objname, 2-objname etc.
|
||||
|
||||
# check if the msg_object may se dbrefs
|
||||
show_dbref = global_search
|
||||
|
||||
if multimatch_string:
|
||||
# custom header
|
||||
string = multimatch_string
|
||||
else:
|
||||
string = "More than one match for '%s'" % ostring
|
||||
string += " (please narrow target):"
|
||||
string = _(string)
|
||||
|
||||
for num, result in enumerate(results):
|
||||
invtext = ""
|
||||
dbreftext = ""
|
||||
if hasattr(result, _("location")) and result.location == msg_obj:
|
||||
invtext = _(" (carried)")
|
||||
if show_dbref:
|
||||
dbreftext = "(#%i)" % result.dbid
|
||||
string += "\n %i-%s%s%s" % (num + 1, result.name,
|
||||
dbreftext, invtext)
|
||||
results = None
|
||||
else:
|
||||
# we have exactly one match.
|
||||
results = results[0]
|
||||
|
||||
if string and not quiet:
|
||||
msg_obj.msg(string.strip())
|
||||
return results
|
||||
|
||||
|
||||
def at_multimatch_input(ostring):
|
||||
"""
|
||||
Parse number-identifiers.
|
||||
|
||||
This parser will be called by the engine when a user supplies
|
||||
a search term. The search term must be analyzed to determine
|
||||
if the user wants to differentiate between multiple matches
|
||||
(usually found during a previous search).
|
||||
|
||||
This method should separate out any identifiers from the search
|
||||
string used to differentiate between same-named objects. The
|
||||
result should be a tuple (index, search_string) where the index
|
||||
gives which match among multiple matches should be used (1 being
|
||||
the lowest number, rather than 0 as in Python).
|
||||
|
||||
This parser version will identify search strings on the following
|
||||
forms
|
||||
|
||||
2-object
|
||||
|
||||
This will be parsed to (2, "object") and, if applicable, will tell
|
||||
the engine to pick the second from a list of same-named matches of
|
||||
objects called "object".
|
||||
|
||||
Ex for use in a game session:
|
||||
|
||||
> look
|
||||
You see: ball, ball, ball and ball.
|
||||
> get ball
|
||||
There where multiple matches for ball:
|
||||
1-ball
|
||||
2-ball
|
||||
3-ball
|
||||
4-ball
|
||||
> get 3-ball
|
||||
You get the ball.
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(ostring, basestring):
|
||||
return (None, ostring)
|
||||
if not '-' in ostring:
|
||||
return (None, ostring)
|
||||
try:
|
||||
index = ostring.find('-')
|
||||
number = int(ostring[:index]) - 1
|
||||
return (number, ostring[index + 1:])
|
||||
except ValueError:
|
||||
#not a number; this is not an identifier.
|
||||
return (None, ostring)
|
||||
except IndexError:
|
||||
return (None, ostring)
|
||||
|
||||
|
||||
def at_multimatch_cmd(caller, matches):
|
||||
"""
|
||||
Format multiple command matches to a useful error.
|
||||
"""
|
||||
string = "There were multiple matches:"
|
||||
for num, match in enumerate(matches):
|
||||
# each match is a tuple (candidate, cmd)
|
||||
cmdname, arg, cmd, dum, dum = match
|
||||
|
||||
is_channel = hasattr(cmd, "is_channel") and cmd.is_channel
|
||||
if is_channel:
|
||||
is_channel = _(" (channel)")
|
||||
else:
|
||||
is_channel = ""
|
||||
if cmd.is_exit and cmd.destination:
|
||||
is_exit = (" (exit to %s)") % cmd.destination
|
||||
else:
|
||||
is_exit = ""
|
||||
|
||||
id1 = ""
|
||||
id2 = ""
|
||||
if (not (is_channel or is_exit) and
|
||||
(hasattr(cmd, 'obj') and cmd.obj != caller) and
|
||||
hasattr(cmd.obj, "key")):
|
||||
# the command is defined on some other object
|
||||
id1 = "%s-%s" % (num + 1, cmdname)
|
||||
id2 = " (%s)" % (cmd.obj.key)
|
||||
else:
|
||||
id1 = "%s-%s" % (num + 1, cmdname)
|
||||
id2 = ""
|
||||
string += "\n %s%s%s%s" % (id1, id2, is_channel, is_exit)
|
||||
return string
|
||||
462
lib/commands/cmdset.py
Normal file
462
lib/commands/cmdset.py
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
"""
|
||||
A cmdset holds a set of commands available to the object or to other
|
||||
objects near it. All the commands a player can give (look, @create etc)
|
||||
are stored as the default cmdset on the player object and managed using the
|
||||
CmdHandler object (see cmdhandler.py).
|
||||
|
||||
The power of having command sets in CmdSets like this is that CmdSets
|
||||
can be merged together according to individual rules to create a new
|
||||
on-the-fly CmdSet that is some combination of the
|
||||
previous ones. Their function are borrowed to a large parts from mathematical
|
||||
Set theory, it should not be much of a problem to understand.
|
||||
|
||||
See CmdHandler for practical examples on how to apply cmdsets
|
||||
together to create interesting in-game effects.
|
||||
"""
|
||||
|
||||
from weakref import WeakKeyDictionary
|
||||
from django.utils.translation import ugettext as _
|
||||
from src.utils.utils import inherits_from, is_iter
|
||||
__all__ = ("CmdSet",)
|
||||
|
||||
|
||||
class _CmdSetMeta(type):
|
||||
"""
|
||||
This metaclass makes some minor on-the-fly convenience fixes to
|
||||
the cmdset class.
|
||||
"""
|
||||
def __init__(mcs, *args, **kwargs):
|
||||
"""
|
||||
Fixes some things in the cmdclass
|
||||
"""
|
||||
# by default we key the cmdset the same as the
|
||||
# name of its class.
|
||||
if not hasattr(mcs, 'key') or not mcs.key:
|
||||
mcs.key = mcs.__name__
|
||||
mcs.path = "%s.%s" % (mcs.__module__, mcs.__name__)
|
||||
|
||||
if not type(mcs.key_mergetypes) == dict:
|
||||
mcs.key_mergetypes = {}
|
||||
|
||||
super(_CmdSetMeta, mcs).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class CmdSet(object):
|
||||
"""
|
||||
This class describes a unique cmdset that understands priorities. CmdSets
|
||||
can be merged and made to perform various set operations on each other.
|
||||
CmdSets have priorities that affect which of their ingoing commands
|
||||
gets used.
|
||||
|
||||
In the examples, cmdset A always have higher priority than cmdset B.
|
||||
|
||||
key - the name of the cmdset. This can be used on its own for game
|
||||
operations
|
||||
|
||||
mergetype (partly from Set theory):
|
||||
|
||||
Union - The two command sets are merged so that as many
|
||||
commands as possible of each cmdset ends up in the
|
||||
merged cmdset. Same-name commands are merged by
|
||||
priority. This is the most common default.
|
||||
Ex: A1,A3 + B1,B2,B4,B5 = A1,B2,A3,B4,B5
|
||||
Intersect - Only commands found in *both* cmdsets
|
||||
(i.e. which have same names) end up in the merged
|
||||
cmdset, with the higher-priority cmdset replacing the
|
||||
lower one. Ex: A1,A3 + B1,B2,B4,B5 = A1
|
||||
Replace - The commands of this cmdset completely replaces
|
||||
the lower-priority cmdset's commands, regardless
|
||||
of if same-name commands exist.
|
||||
Ex: A1,A3 + B1,B2,B4,B5 = A1,A3
|
||||
Remove - This removes the relevant commands from the
|
||||
lower-priority cmdset completely. They are not
|
||||
replaced with anything, so this in effects uses the
|
||||
high-priority cmdset as a filter to affect the
|
||||
low-priority cmdset.
|
||||
Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5
|
||||
|
||||
Note: Commands longer than 2 characters and starting
|
||||
with double underscrores, like '__noinput_command'
|
||||
are considered 'system commands' and are
|
||||
excempt from all merge operations - they are
|
||||
ALWAYS included across mergers and only affected
|
||||
if same-named system commands replace them.
|
||||
|
||||
priority- All cmdsets are always merged in pairs of two so that
|
||||
the higher set's mergetype is applied to the
|
||||
lower-priority cmdset. Default commands have priority 0,
|
||||
high-priority ones like Exits and Channels have 10 and 9.
|
||||
Priorities can be negative as well to give default
|
||||
commands preference.
|
||||
|
||||
duplicates - determines what happens when two sets of equal
|
||||
priority merge. Default has the first of them in the
|
||||
merger (i.e. A above) automatically taking
|
||||
precedence. But if allow_duplicates is true, the
|
||||
result will be a merger with more than one of each
|
||||
name match. This will usually lead to the player
|
||||
receiving a multiple-match error higher up the road,
|
||||
but can be good for things like cmdsets on non-player
|
||||
objects in a room, to allow the system to warn that
|
||||
more than one 'ball' in the room has the same 'kick'
|
||||
command defined on it, so it may offer a chance to
|
||||
select which ball to kick ... Allowing duplicates
|
||||
only makes sense for Union and Intersect, the setting
|
||||
is ignored for the other mergetypes.
|
||||
|
||||
key_mergetype (dict) - allows the cmdset to define a unique
|
||||
mergetype for particular cmdsets. Format is
|
||||
{CmdSetkeystring:mergetype}. Priorities still apply.
|
||||
Example: {'Myevilcmdset','Replace'} which would make
|
||||
sure for this set to always use 'Replace' on
|
||||
Myevilcmdset no matter what overall mergetype this set
|
||||
has.
|
||||
|
||||
no_objs - don't include any commands from nearby objects
|
||||
when searching for suitable commands
|
||||
no_exits - ignore the names of exits when matching against
|
||||
commands
|
||||
no_channels - ignore the name of channels when matching against
|
||||
commands (WARNING- this is dangerous since the
|
||||
player can then not even ask staff for help if
|
||||
something goes wrong)
|
||||
|
||||
|
||||
"""
|
||||
__metaclass__ = _CmdSetMeta
|
||||
|
||||
key = "Unnamed CmdSet"
|
||||
mergetype = "Union"
|
||||
priority = 0
|
||||
duplicates = False
|
||||
key_mergetypes = {}
|
||||
no_exits = False
|
||||
no_objs = False
|
||||
no_channels = False
|
||||
permanent = False
|
||||
errmessage = ""
|
||||
# pre-store properties to duplicate straight off
|
||||
to_duplicate = ("key", "cmdsetobj", "no_exits", "no_objs",
|
||||
"no_channels", "permanent", "mergetype",
|
||||
"priority", "duplicates", "errmessage")
|
||||
|
||||
def __init__(self, cmdsetobj=None, key=None):
|
||||
"""
|
||||
Creates a new CmdSet instance.
|
||||
|
||||
cmdsetobj - this is the database object to which this particular
|
||||
instance of cmdset is related. It is often a character but
|
||||
may also be a regular object.
|
||||
"""
|
||||
if key:
|
||||
self.key = key
|
||||
self.commands = []
|
||||
self.system_commands = []
|
||||
self.actual_mergetype = self.mergetype
|
||||
self.cmdsetobj = cmdsetobj
|
||||
# this is set only on merged sets, in cmdhandler.py, in order to
|
||||
# track, list and debug mergers correctly.
|
||||
self.merged_from = []
|
||||
|
||||
# initialize system
|
||||
self.at_cmdset_creation()
|
||||
self._contains_cache = WeakKeyDictionary()#{}
|
||||
|
||||
# Priority-sensitive merge operations for cmdsets
|
||||
|
||||
def _union(self, cmdset_a, cmdset_b):
|
||||
"C = A U B. CmdSet A is assumed to have higher priority"
|
||||
cmdset_c = cmdset_a._duplicate()
|
||||
# we make copies, not refs by use of [:]
|
||||
cmdset_c.commands = cmdset_a.commands[:]
|
||||
if cmdset_a.duplicates and cmdset_a.priority == cmdset_b.priority:
|
||||
cmdset_c.commands.extend(cmdset_b.commands)
|
||||
else:
|
||||
cmdset_c.commands.extend([cmd for cmd in cmdset_b
|
||||
if not cmd in cmdset_a])
|
||||
return cmdset_c
|
||||
|
||||
def _intersect(self, cmdset_a, cmdset_b):
|
||||
"C = A (intersect) B. A is assumed higher priority"
|
||||
cmdset_c = cmdset_a._duplicate()
|
||||
if cmdset_a.duplicates and cmdset_a.priority == cmdset_b.priority:
|
||||
for cmd in [cmd for cmd in cmdset_a if cmd in cmdset_b]:
|
||||
cmdset_c.add(cmd)
|
||||
cmdset_c.add(cmdset_b.get(cmd))
|
||||
else:
|
||||
cmdset_c.commands = [cmd for cmd in cmdset_a if cmd in cmdset_b]
|
||||
return cmdset_c
|
||||
|
||||
def _replace(self, cmdset_a, cmdset_b):
|
||||
"C = A + B where the result is A."
|
||||
cmdset_c = cmdset_a._duplicate()
|
||||
cmdset_c.commands = cmdset_a.commands[:]
|
||||
return cmdset_c
|
||||
|
||||
def _remove(self, cmdset_a, cmdset_b):
|
||||
"C = A + B, where B is filtered by A"
|
||||
cmdset_c = cmdset_a._duplicate()
|
||||
cmdset_c.commands = [cmd for cmd in cmdset_b if not cmd in cmdset_a]
|
||||
return cmdset_c
|
||||
|
||||
def _instantiate(self, cmd):
|
||||
"""
|
||||
checks so that object is an instantiated command
|
||||
and not, say a cmdclass. If it is, instantiate it.
|
||||
Other types, like strings, are passed through.
|
||||
"""
|
||||
try:
|
||||
return cmd()
|
||||
except TypeError:
|
||||
return cmd
|
||||
|
||||
def _duplicate(self):
|
||||
"""
|
||||
Returns a new cmdset with the same settings as this one
|
||||
(no actual commands are copied over)
|
||||
"""
|
||||
cmdset = CmdSet()
|
||||
for key, val in ((key, getattr(self, key)) for key in self.to_duplicate):
|
||||
if val != getattr(cmdset, key):
|
||||
# only copy if different from default; avoid turning
|
||||
# class-vars into instance vars
|
||||
setattr(cmdset, key, val)
|
||||
cmdset.key_mergetypes = self.key_mergetypes.copy()
|
||||
return cmdset
|
||||
#cmdset = self.__class__()
|
||||
#cmdset.__dict__.update(dict((key, val) for key, val in self.__dict__.items() if key in self.to_duplicate))
|
||||
#cmdset.key_mergetypes = self.key_mergetypes.copy() #copy.deepcopy(self.key_mergetypes)
|
||||
#return cmdset
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Show all commands in cmdset when printing it.
|
||||
"""
|
||||
return ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o:o.key)])
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Allows for things like 'for cmd in cmdset':
|
||||
"""
|
||||
return iter(self.commands)
|
||||
|
||||
def __contains__(self, othercmd):
|
||||
"""
|
||||
Returns True if this cmdset contains the given command (as defined
|
||||
by command name and aliases). This allows for things
|
||||
like 'if cmd in cmdset'
|
||||
"""
|
||||
ret = self._contains_cache.get(othercmd)
|
||||
if ret is None:
|
||||
ret = othercmd in self.commands
|
||||
self._contains_cache[othercmd] = ret
|
||||
return ret
|
||||
|
||||
def __add__(self, cmdset_b):
|
||||
"""
|
||||
Merge this cmdset (A) with another cmdset (B) using the + operator,
|
||||
|
||||
C = A + B
|
||||
|
||||
Here, we (by convention) say that 'A is merged onto B to form
|
||||
C'. The actual merge operation used in the 'addition' depends
|
||||
on which priorities A and B have. The one of the two with the
|
||||
highest priority will apply and give its properties to C. In
|
||||
the case of a tie, A takes priority and replaces the
|
||||
same-named commands in B unless A has the 'duplicate' variable
|
||||
set (which means both sets' commands are kept).
|
||||
"""
|
||||
|
||||
# It's okay to merge with None
|
||||
if not cmdset_b:
|
||||
return self
|
||||
|
||||
sys_commands_a = self.get_system_cmds()
|
||||
sys_commands_b = cmdset_b.get_system_cmds()
|
||||
|
||||
if self.priority >= cmdset_b.priority:
|
||||
# A higher or equal priority than B
|
||||
|
||||
# preserve system __commands
|
||||
sys_commands = sys_commands_a + [cmd for cmd in sys_commands_b
|
||||
if cmd not in sys_commands_a]
|
||||
|
||||
mergetype = self.key_mergetypes.get(cmdset_b.key, self.mergetype)
|
||||
if mergetype == "Intersect":
|
||||
cmdset_c = self._intersect(self, cmdset_b)
|
||||
elif mergetype == "Replace":
|
||||
cmdset_c = self._replace(self, cmdset_b)
|
||||
elif mergetype == "Remove":
|
||||
cmdset_c = self._remove(self, cmdset_b)
|
||||
else: # Union
|
||||
cmdset_c = self._union(self, cmdset_b)
|
||||
cmdset_c.no_channels = self.no_channels
|
||||
cmdset_c.no_exits = self.no_exits
|
||||
cmdset_c.no_objs = self.no_objs
|
||||
if self.key.startswith("_"):
|
||||
# don't rename new output if the merge set's name starts with _
|
||||
cmdset_c.key = cmdset_b.key
|
||||
|
||||
else:
|
||||
# B higher priority than A
|
||||
|
||||
# preserver system __commands
|
||||
sys_commands = sys_commands_b + [cmd for cmd in sys_commands_a
|
||||
if cmd not in sys_commands_b]
|
||||
|
||||
mergetype = cmdset_b.key_mergetypes.get(self.key, cmdset_b.mergetype)
|
||||
if mergetype == "Intersect":
|
||||
cmdset_c = self._intersect(cmdset_b, self)
|
||||
elif mergetype == "Replace":
|
||||
cmdset_c = self._replace(cmdset_b, self)
|
||||
elif mergetype == "Remove":
|
||||
cmdset_c = self._remove(self, cmdset_b)
|
||||
else: # Union
|
||||
cmdset_c = self._union(cmdset_b, self)
|
||||
cmdset_c.no_channels = cmdset_b.no_channels
|
||||
cmdset_c.no_exits = cmdset_b.no_exits
|
||||
cmdset_c.no_objs = cmdset_b.no_objs
|
||||
if cmdset_b.key.startswith("_"):
|
||||
# don't rename new output if the merge set's name starts with _
|
||||
cmdset_c.key = self.key
|
||||
|
||||
# we store actual_mergetype since key_mergetypes
|
||||
# might be different from the main mergetype.
|
||||
# This is used for diagnosis.
|
||||
cmdset_c.actual_mergetype = mergetype
|
||||
|
||||
# return the system commands to the cmdset
|
||||
cmdset_c.add(sys_commands)
|
||||
return cmdset_c
|
||||
|
||||
def add(self, cmd):
|
||||
"""
|
||||
Add a command, a list of commands or a cmdset to this cmdset.
|
||||
|
||||
Note that if cmd already exists in set,
|
||||
it will replace the old one (no priority checking etc
|
||||
at this point; this is often used to overload
|
||||
default commands).
|
||||
|
||||
If cmd is another cmdset class or -instance, the commands
|
||||
of that command set is added to this one, as if they were part
|
||||
of the original cmdset definition. No merging or priority checks
|
||||
are made, rather later added commands will simply replace
|
||||
existing ones to make a unique set.
|
||||
"""
|
||||
|
||||
if inherits_from(cmd, "src.commands.cmdset.CmdSet"):
|
||||
# cmd is a command set so merge all commands in that set
|
||||
# to this one. We raise a visible error if we created
|
||||
# an infinite loop (adding cmdset to itself somehow)
|
||||
try:
|
||||
cmd = self._instantiate(cmd)
|
||||
except RuntimeError:
|
||||
string = "Adding cmdset %(cmd)s to %(class)s lead to an "
|
||||
string += "infinite loop. When adding a cmdset to another, "
|
||||
string += "make sure they are not themself cyclically added to "
|
||||
string += "the new cmdset somewhere in the chain."
|
||||
raise RuntimeError(_(string) % {"cmd": cmd,
|
||||
"class": self.__class__})
|
||||
cmds = cmd.commands
|
||||
elif is_iter(cmd):
|
||||
cmds = [self._instantiate(c) for c in cmd]
|
||||
else:
|
||||
cmds = [self._instantiate(cmd)]
|
||||
commands = self.commands
|
||||
system_commands = self.system_commands
|
||||
for cmd in cmds:
|
||||
# add all commands
|
||||
if not hasattr(cmd, 'obj'):
|
||||
cmd.obj = self.cmdsetobj
|
||||
try:
|
||||
ic = commands.index(cmd)
|
||||
commands[ic] = cmd # replace
|
||||
except ValueError:
|
||||
commands.append(cmd)
|
||||
# extra run to make sure to avoid doublets
|
||||
self.commands = list(set(commands))
|
||||
#print "In cmdset.add(cmd):", self.key, cmd
|
||||
# add system_command to separate list as well,
|
||||
# for quick look-up
|
||||
if cmd.key.startswith("__"):
|
||||
try:
|
||||
ic = system_commands.index(cmd)
|
||||
system_commands[ic] = cmd # replace
|
||||
except ValueError:
|
||||
system_commands.append(cmd)
|
||||
|
||||
def remove(self, cmd):
|
||||
"""
|
||||
Remove a command instance from the cmdset.
|
||||
cmd can be either a cmd instance or a key string.
|
||||
"""
|
||||
cmd = self._instantiate(cmd)
|
||||
self.commands = [oldcmd for oldcmd in self.commands if oldcmd != cmd]
|
||||
|
||||
def get(self, cmd):
|
||||
"""
|
||||
Return the command in this cmdset that matches the
|
||||
given command. cmd may be either a command instance or
|
||||
a key string.
|
||||
"""
|
||||
cmd = self._instantiate(cmd)
|
||||
for thiscmd in self.commands:
|
||||
if thiscmd == cmd:
|
||||
return thiscmd
|
||||
|
||||
def count(self):
|
||||
"Return number of commands in set"
|
||||
return len(self.commands)
|
||||
|
||||
def get_system_cmds(self):
|
||||
"""
|
||||
Return system commands in the cmdset, defined as
|
||||
commands starting with double underscore __.
|
||||
These are excempt from merge operations.
|
||||
"""
|
||||
return self.system_commands
|
||||
#return [cmd for cmd in self.commands if cmd.key.startswith('__')]
|
||||
|
||||
def make_unique(self, caller):
|
||||
"""
|
||||
This is an unsafe command meant to clean out a cmdset of
|
||||
doublet commands after it has been created. It is useful
|
||||
for commands inheriting cmdsets from the cmdhandler where
|
||||
obj-based cmdsets always are added double. Doublets will
|
||||
be weeded out with preference to commands defined on caller,
|
||||
otherwise just by first-come-first-served.
|
||||
"""
|
||||
unique = {}
|
||||
for cmd in self.commands:
|
||||
if cmd.key in unique:
|
||||
ocmd = unique[cmd.key]
|
||||
if (hasattr(cmd, 'obj') and cmd.obj == caller) and not \
|
||||
(hasattr(ocmd, 'obj') and ocmd.obj == caller):
|
||||
unique[cmd.key] = cmd
|
||||
else:
|
||||
unique[cmd.key] = cmd
|
||||
self.commands = unique.values()
|
||||
|
||||
def get_all_cmd_keys_and_aliases(self, caller=None):
|
||||
"""
|
||||
Returns a list of all command keys and aliases
|
||||
available in this cmdset. If caller is given, the
|
||||
commands is checked for access on the "call" type
|
||||
before being returned.
|
||||
"""
|
||||
names = []
|
||||
if caller:
|
||||
[names.extend(cmd._keyaliases) for cmd in self.commands
|
||||
if cmd.access(caller)]
|
||||
else:
|
||||
[names.extend(cmd._keyaliases) for cmd in self.commands]
|
||||
return names
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
Hook method - this should be overloaded in the inheriting
|
||||
class, and should take care of populating the cmdset
|
||||
by use of self.add().
|
||||
"""
|
||||
pass
|
||||
478
lib/commands/cmdsethandler.py
Normal file
478
lib/commands/cmdsethandler.py
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
"""
|
||||
CmdSethandler
|
||||
|
||||
The Cmdsethandler tracks an object's 'Current CmdSet', which is the
|
||||
current merged sum of all CmdSets added to it.
|
||||
|
||||
A CmdSet constitues a set of commands. The CmdSet works as a special
|
||||
intelligent container that, when added to other CmdSet make sure that
|
||||
same-name commands are treated correctly (usually so there are no
|
||||
doublets). This temporary but up-to-date merger of CmdSet is jointly
|
||||
called the Current Cmset. It is this Current CmdSet that the
|
||||
commandhandler looks through whenever a player enters a command (it
|
||||
also adds CmdSets from objects in the room in real-time). All player
|
||||
objects have a 'default cmdset' containing all the normal in-game mud
|
||||
commands (look etc).
|
||||
|
||||
So what is all this cmdset complexity good for?
|
||||
|
||||
In its simplest form, a CmdSet has no commands, only a key name. In
|
||||
this case the cmdset's use is up to each individual game - it can be
|
||||
used by an AI module for example (mobs in cmdset 'roam' move from room
|
||||
to room, in cmdset 'attack' they enter combat with players).
|
||||
|
||||
Defining commands in cmdsets offer some further powerful game-design
|
||||
consequences however. Here are some examples:
|
||||
|
||||
As mentioned above, all players always have at least the Default
|
||||
CmdSet. This contains the set of all normal-use commands in-game,
|
||||
stuff like look and @desc etc. Now assume our players end up in a dark
|
||||
room. You don't want the player to be able to do much in that dark
|
||||
room unless they light a candle. You could handle this by changing all
|
||||
your normal commands to check if the player is in a dark room. This
|
||||
rapidly goes unwieldly and error prone. Instead you just define a
|
||||
cmdset with only those commands you want to be available in the 'dark'
|
||||
cmdset - maybe a modified look command and a 'light candle' command -
|
||||
and have this completely replace the default cmdset.
|
||||
|
||||
Another example: Say you want your players to be able to go
|
||||
fishing. You could implement this as a 'fish' command that fails
|
||||
whenever the player has no fishing rod. Easy enough. But what if you
|
||||
want to make fishing more complex - maybe you want four-five different
|
||||
commands for throwing your line, reeling in, etc? Most players won't
|
||||
(we assume) have fishing gear, and having all those detailed commands
|
||||
is cluttering up the command list. And what if you want to use the
|
||||
'throw' command also for throwing rocks etc instead of 'using it up'
|
||||
for a minor thing like fishing?
|
||||
|
||||
So instead you put all those detailed fishing commands into their own
|
||||
CommandSet called 'Fishing'. Whenever the player gives the command
|
||||
'fish' (presumably the code checks there is also water nearby), only
|
||||
THEN this CommandSet is added to the Cmdhandler of the player. The
|
||||
'throw' command (which normally throws rocks) is replaced by the
|
||||
custom 'fishing variant' of throw. What has happened is that the
|
||||
Fishing CommandSet was merged on top of the Default ones, and due to
|
||||
how we defined it, its command overrules the default ones.
|
||||
|
||||
When we are tired of fishing, we give the 'go home' command (or
|
||||
whatever) and the Cmdhandler simply removes the fishing CommandSet
|
||||
so that we are back at defaults (and can throw rocks again).
|
||||
|
||||
Since any number of CommandSets can be piled on top of each other, you
|
||||
can then implement separate sets for different situations. For
|
||||
example, you can have a 'On a boat' set, onto which you then tack on
|
||||
the 'Fishing' set. Fishing from a boat? No problem!
|
||||
"""
|
||||
from django.conf import settings
|
||||
from src.utils import logger, utils
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.server.models import ServerConfig
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
__all__ = ("import_cmdset", "CmdSetHandler")
|
||||
|
||||
_CACHED_CMDSETS = {}
|
||||
_CMDSET_PATHS = utils.make_iter(settings.CMDSET_PATHS)
|
||||
|
||||
class _ErrorCmdSet(CmdSet):
|
||||
"This is a special cmdset used to report errors"
|
||||
key = "_CMDSET_ERROR"
|
||||
errmessage = "Error when loading cmdset."
|
||||
|
||||
class _EmptyCmdSet(CmdSet):
|
||||
"This cmdset represents an empty cmdset"
|
||||
key = "_EMPTY_CMDSET"
|
||||
priority = -101
|
||||
mergetype = "Union"
|
||||
|
||||
def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
|
||||
"""
|
||||
This helper function is used by the cmdsethandler to load a cmdset
|
||||
instance from a python module, given a python_path. It's usually accessed
|
||||
through the cmdsethandler's add() and add_default() methods.
|
||||
path - This is the full path to the cmdset object on python dot-form
|
||||
cmdsetobj - the database object/typeclass on which this cmdset is to be
|
||||
assigned (this can be also channels and exits, as well as players
|
||||
but there will always be such an object)
|
||||
emit_to_obj - if given, error is emitted to this object (in addition
|
||||
to logging)
|
||||
no_logging - don't log/send error messages. This can be useful
|
||||
if import_cmdset is just used to check if this is a
|
||||
valid python path or not.
|
||||
function returns None if an error was encountered or path not found.
|
||||
"""
|
||||
|
||||
python_paths = [path] + ["%s.%s" % (prefix, path)
|
||||
for prefix in _CMDSET_PATHS if not path.startswith(prefix)]
|
||||
errstring = ""
|
||||
for python_path in python_paths:
|
||||
try:
|
||||
#print "importing %s: _CACHED_CMDSETS=%s" % (python_path, _CACHED_CMDSETS)
|
||||
wanted_cache_key = python_path
|
||||
cmdsetclass = _CACHED_CMDSETS.get(wanted_cache_key, None)
|
||||
errstring = ""
|
||||
if not cmdsetclass:
|
||||
#print "cmdset '%s' not in cache. Reloading %s on %s." % (wanted_cache_key, python_path, cmdsetobj)
|
||||
# Not in cache. Reload from disk.
|
||||
modulepath, classname = python_path.rsplit('.', 1)
|
||||
module = __import__(modulepath, fromlist=[True])
|
||||
cmdsetclass = module.__dict__[classname]
|
||||
_CACHED_CMDSETS[wanted_cache_key] = cmdsetclass
|
||||
#instantiate the cmdset (and catch its errors)
|
||||
if callable(cmdsetclass):
|
||||
cmdsetclass = cmdsetclass(cmdsetobj)
|
||||
return cmdsetclass
|
||||
except ImportError, e:
|
||||
logger.log_trace()
|
||||
errstring += _("Error loading cmdset '%s': %s.")
|
||||
errstring = errstring % (modulepath, e)
|
||||
except KeyError:
|
||||
logger.log_trace()
|
||||
errstring += _("Error in loading cmdset: No cmdset class '%(classname)s' in %(modulepath)s.")
|
||||
errstring = errstring % {"classname": classname,
|
||||
"modulepath": modulepath}
|
||||
except SyntaxError, e:
|
||||
logger.log_trace()
|
||||
errstring += _("SyntaxError encountered when loading cmdset '%s': %s.")
|
||||
errstring = errstring % (modulepath, e)
|
||||
except Exception, e:
|
||||
logger.log_trace()
|
||||
errstring += _("Compile/Run error when loading cmdset '%s': %s.")
|
||||
errstring = errstring % (python_path, e)
|
||||
|
||||
if errstring:
|
||||
# returning an empty error cmdset
|
||||
if not no_logging:
|
||||
logger.log_errmsg(errstring)
|
||||
if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"):
|
||||
emit_to_obj.msg(errstring)
|
||||
err_cmdset = _ErrorCmdSet()
|
||||
err_cmdset.errmessage = errstring + _("\n (See log for details.)")
|
||||
return err_cmdset
|
||||
|
||||
# classes
|
||||
|
||||
|
||||
class CmdSetHandler(object):
|
||||
"""
|
||||
The CmdSetHandler is always stored on an object, this object is supplied
|
||||
as an argument.
|
||||
|
||||
The 'current' cmdset is the merged set currently active for this object.
|
||||
This is the set the game engine will retrieve when determining which
|
||||
commands are available to the object. The cmdset_stack holds a history of
|
||||
all CmdSets to allow the handler to remove/add cmdsets at will. Doing so
|
||||
will re-calculate the 'current' cmdset.
|
||||
"""
|
||||
|
||||
def __init__(self, obj, init_true=True):
|
||||
"""
|
||||
This method is called whenever an object is recreated.
|
||||
|
||||
obj - this is a reference to the game object this handler
|
||||
belongs to.
|
||||
"""
|
||||
self.obj = obj
|
||||
|
||||
# the id of the "merged" current cmdset for easy access.
|
||||
self.key = None
|
||||
# this holds the "merged" current command set
|
||||
self.current = None
|
||||
# this holds a history of CommandSets
|
||||
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
|
||||
# this tracks which mergetypes are actually in play in the stack
|
||||
self.mergetype_stack = ["Union"]
|
||||
|
||||
# the subset of the cmdset_paths that are to be stored in the database
|
||||
self.permanent_paths = [""]
|
||||
|
||||
if init_true:
|
||||
self.update(init_mode=True) #is then called from the object __init__.
|
||||
|
||||
def __str__(self):
|
||||
"Display current commands"
|
||||
|
||||
string = ""
|
||||
mergelist = []
|
||||
if len(self.cmdset_stack) > 1:
|
||||
# We have more than one cmdset in stack; list them all
|
||||
#print self.cmdset_stack, self.mergetype_stack
|
||||
for snum, cmdset in enumerate(self.cmdset_stack):
|
||||
mergetype = self.mergetype_stack[snum]
|
||||
permstring = "non-perm"
|
||||
if cmdset.permanent:
|
||||
permstring = "perm"
|
||||
if mergetype != cmdset.mergetype:
|
||||
mergetype = "%s^" % (mergetype)
|
||||
string += "\n %i: <%s (%s, prio %i, %s)>: %s" % \
|
||||
(snum, cmdset.key, mergetype,
|
||||
cmdset.priority, permstring, cmdset)
|
||||
mergelist.append(str(snum))
|
||||
string += "\n"
|
||||
|
||||
# Display the currently active cmdset, limited by self.obj's permissions
|
||||
mergetype = self.mergetype_stack[-1]
|
||||
if mergetype != self.current.mergetype:
|
||||
merged_on = self.cmdset_stack[-2].key
|
||||
mergetype = _("custom %(mergetype)s on cmdset '%(merged_on)s'") % \
|
||||
{"mergetype": mergetype, "merged_on":merged_on}
|
||||
if mergelist:
|
||||
string += _(" <Merged %(mergelist)s (%(mergetype)s, prio %(prio)i)>: %(current)s") % \
|
||||
{"mergelist": "+".join(mergelist),
|
||||
"mergetype": mergetype, "prio": self.current.priority,
|
||||
"current":self.current}
|
||||
else:
|
||||
permstring = "non-perm"
|
||||
if self.current.permanent:
|
||||
permstring = "perm"
|
||||
string += _(" <%(key)s (%(mergetype)s, prio %(prio)i, %(permstring)s)>: %(keylist)s") % \
|
||||
{"key": self.current.key, "mergetype": mergetype,
|
||||
"prio": self.current.priority, "permstring": permstring,
|
||||
"keylist": ", ".join(cmd.key for cmd in sorted(self.current, key=lambda o: o.key))}
|
||||
return string.strip()
|
||||
|
||||
def _import_cmdset(self, cmdset_path, emit_to_obj=None):
|
||||
"""
|
||||
Method wrapper for import_cmdset.
|
||||
load a cmdset from a module.
|
||||
cmdset_path - the python path to an cmdset object.
|
||||
emit_to_obj - object to send error messages to
|
||||
"""
|
||||
if not emit_to_obj:
|
||||
emit_to_obj = self.obj
|
||||
return import_cmdset(cmdset_path, self.obj, emit_to_obj)
|
||||
|
||||
def update(self, init_mode=False):
|
||||
"""
|
||||
Re-adds all sets in the handler to have an updated
|
||||
current set.
|
||||
|
||||
init_mode is used right after this handler was
|
||||
created; it imports all permanent cmdsets from db.
|
||||
"""
|
||||
if init_mode:
|
||||
# reimport all permanent cmdsets
|
||||
storage = self.obj.cmdset_storage
|
||||
#print "cmdset_storage:", self.obj.cmdset_storage
|
||||
if storage:
|
||||
self.cmdset_stack = []
|
||||
for pos, path in enumerate(storage):
|
||||
if pos == 0 and not path:
|
||||
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
|
||||
elif path:
|
||||
cmdset = self._import_cmdset(path)
|
||||
if cmdset:
|
||||
cmdset.permanent = cmdset.key != '_CMDSET_ERROR'
|
||||
self.cmdset_stack.append(cmdset)
|
||||
|
||||
# merge the stack into a new merged cmdset
|
||||
new_current = None
|
||||
self.mergetype_stack = []
|
||||
for cmdset in self.cmdset_stack:
|
||||
try:
|
||||
# for cmdset's '+' operator, order matters.
|
||||
new_current = cmdset + new_current
|
||||
except TypeError:
|
||||
continue
|
||||
self.mergetype_stack.append(new_current.actual_mergetype)
|
||||
self.current = new_current
|
||||
|
||||
def add(self, cmdset, emit_to_obj=None, permanent=False):
|
||||
"""
|
||||
Add a cmdset to the handler, on top of the old ones.
|
||||
Default is to not make this permanent, i.e. the set
|
||||
will not survive a server reset.
|
||||
|
||||
cmdset - can be a cmdset object or the python path to
|
||||
such an object.
|
||||
emit_to_obj - an object to receive error messages.
|
||||
permanent - this cmdset will remain across a server reboot
|
||||
|
||||
Note: An interesting feature of this method is if you were to
|
||||
send it an *already instantiated cmdset* (i.e. not a class),
|
||||
the current cmdsethandler's obj attribute will then *not* be
|
||||
transferred over to this already instantiated set (this is
|
||||
because it might be used elsewhere and can cause strange effects).
|
||||
This means you could in principle have the handler
|
||||
launch command sets tied to a *different* object than the
|
||||
handler. Not sure when this would be useful, but it's a 'quirk'
|
||||
that has to be documented.
|
||||
"""
|
||||
if not (isinstance(cmdset, basestring) or utils.inherits_from(cmdset, CmdSet)):
|
||||
raise Exception(_("Only CmdSets can be added to the cmdsethandler!"))
|
||||
if callable(cmdset):
|
||||
cmdset = cmdset(self.obj)
|
||||
elif isinstance(cmdset, basestring):
|
||||
# this is (maybe) a python path. Try to import from cache.
|
||||
cmdset = self._import_cmdset(cmdset)
|
||||
if cmdset and cmdset.key != '_CMDSET_ERROR':
|
||||
if permanent and cmdset.key != '_CMDSET_ERROR':
|
||||
# store the path permanently
|
||||
cmdset.permanent = True
|
||||
storage = self.obj.cmdset_storage
|
||||
if not storage:
|
||||
storage = ["", cmdset.path]
|
||||
else:
|
||||
storage.append(cmdset.path)
|
||||
self.obj.cmdset_storage = storage
|
||||
else:
|
||||
cmdset.permanent = False
|
||||
self.cmdset_stack.append(cmdset)
|
||||
self.update()
|
||||
|
||||
def add_default(self, cmdset, emit_to_obj=None, permanent=True):
|
||||
"""
|
||||
Add a new default cmdset. If an old default existed,
|
||||
it is replaced. If permanent is set, the set will survive a reboot.
|
||||
cmdset - can be a cmdset object or the python path to
|
||||
an instance of such an object.
|
||||
emit_to_obj - an object to receive error messages.
|
||||
permanent - save cmdset across reboots
|
||||
See also the notes for self.add(), which applies here too.
|
||||
"""
|
||||
if callable(cmdset):
|
||||
if not utils.inherits_from(cmdset, CmdSet):
|
||||
raise Exception(_("Only CmdSets can be added to the cmdsethandler!"))
|
||||
cmdset = cmdset(self.obj)
|
||||
elif isinstance(cmdset, basestring):
|
||||
# this is (maybe) a python path. Try to import from cache.
|
||||
cmdset = self._import_cmdset(cmdset)
|
||||
if cmdset and cmdset.key != '_CMDSET_ERROR':
|
||||
if self.cmdset_stack:
|
||||
self.cmdset_stack[0] = cmdset
|
||||
self.mergetype_stack[0] = cmdset.mergetype
|
||||
else:
|
||||
self.cmdset_stack = [cmdset]
|
||||
self.mergetype_stack = [cmdset.mergetype]
|
||||
|
||||
if permanent and cmdset.key != '_CMDSET_ERROR':
|
||||
cmdset.permanent = True
|
||||
storage = self.obj.cmdset_storage
|
||||
if storage:
|
||||
storage[0] = cmdset.path
|
||||
else:
|
||||
storage = [cmdset.path]
|
||||
self.obj.cmdset_storage = storage
|
||||
else:
|
||||
cmdset.permanent = False
|
||||
self.update()
|
||||
|
||||
def delete(self, cmdset=None):
|
||||
"""
|
||||
Remove a cmdset from the handler.
|
||||
|
||||
cmdset can be supplied either as a cmdset-key,
|
||||
an instance of the CmdSet or a python path
|
||||
to the cmdset. If no key is given,
|
||||
the last cmdset in the stack is removed. Whenever
|
||||
the cmdset_stack changes, the cmdset is updated.
|
||||
The default cmdset (first entry in stack) is never
|
||||
removed - remove it explicitly with delete_default.
|
||||
|
||||
"""
|
||||
if len(self.cmdset_stack) < 2:
|
||||
# don't allow deleting default cmdsets here.
|
||||
return
|
||||
|
||||
if not cmdset:
|
||||
# remove the last one in the stack
|
||||
cmdset = self.cmdset_stack.pop()
|
||||
if cmdset.permanent:
|
||||
storage = self.obj.cmdset_storage
|
||||
storage.pop()
|
||||
self.obj.cmdset_storage = storage
|
||||
else:
|
||||
# try it as a callable
|
||||
if callable(cmdset) and hasattr(cmdset, 'path'):
|
||||
delcmdsets = [cset for cset in self.cmdset_stack[1:]
|
||||
if cset.path == cmdset.path]
|
||||
else:
|
||||
# try it as a path or key
|
||||
delcmdsets = [cset for cset in self.cmdset_stack[1:]
|
||||
if cset.path == cmdset or cset.key == cmdset]
|
||||
storage = []
|
||||
|
||||
if any(cset.permanent for cset in delcmdsets):
|
||||
# only hit database if there's need to
|
||||
storage = self.obj.cmdset_storage
|
||||
for cset in delcmdsets:
|
||||
if cset.permanent:
|
||||
try:
|
||||
storage.remove(cset.path)
|
||||
except ValueError:
|
||||
pass
|
||||
for cset in delcmdsets:
|
||||
# clean the in-memory stack
|
||||
try:
|
||||
self.cmdset_stack.remove(cset)
|
||||
except ValueError:
|
||||
pass
|
||||
# re-sync the cmdsethandler.
|
||||
self.update()
|
||||
|
||||
def delete_default(self):
|
||||
"""
|
||||
This explicitly deletes the default cmdset. It's the
|
||||
only command that can.
|
||||
"""
|
||||
if self.cmdset_stack:
|
||||
cmdset = self.cmdset_stack[0]
|
||||
if cmdset.permanent:
|
||||
storage = self.obj.cmdset_storage
|
||||
if storage:
|
||||
storage[0] = ""
|
||||
else:
|
||||
storage = [""]
|
||||
self.cmdset_storage = storage
|
||||
self.cmdset_stack[0] = _EmptyCmdSet(cmdsetobj=self.obj)
|
||||
else:
|
||||
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
|
||||
self.update()
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Returns the list of cmdsets. Mostly useful to check
|
||||
if stack if empty or not.
|
||||
"""
|
||||
return self.cmdset_stack
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Removes all extra Command sets from the handler, leaving only the
|
||||
default one.
|
||||
"""
|
||||
self.cmdset_stack = [self.cmdset_stack[0]]
|
||||
self.mergetype_stack = [self.cmdset_stack[0].mergetype]
|
||||
storage = self.obj.cmdset_storage
|
||||
if storage:
|
||||
storage = storage[0]
|
||||
self.obj.cmdset_storage = storage
|
||||
self.update()
|
||||
|
||||
def has_cmdset(self, cmdset_key, must_be_default=False):
|
||||
"""
|
||||
checks so the cmdsethandler contains a cmdset with the given key.
|
||||
must_be_default - only match against the default cmdset.
|
||||
"""
|
||||
if must_be_default:
|
||||
return self.cmdset_stack and self.cmdset_stack[0].key == cmdset_key
|
||||
else:
|
||||
return any([cmdset.key == cmdset_key for cmdset in self.cmdset_stack])
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Force reload of all cmdsets in handler. This should be called
|
||||
after _CACHED_CMDSETS have been cleared (normally by @reload).
|
||||
"""
|
||||
new_cmdset_stack = []
|
||||
new_mergetype_stack = []
|
||||
for cmdset in self.cmdset_stack:
|
||||
if cmdset.key == "_EMPTY_CMDSET":
|
||||
new_cmdset_stack.append(cmdset)
|
||||
new_mergetype_stack.append("Union")
|
||||
else:
|
||||
new_cmdset_stack.append(self._import_cmdset(cmdset.path))
|
||||
new_mergetype_stack.append(cmdset.mergetype)
|
||||
self.cmdset_stack = new_cmdset_stack
|
||||
self.mergetype_stack = new_mergetype_stack
|
||||
self.update()
|
||||
312
lib/commands/command.py
Normal file
312
lib/commands/command.py
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
"""
|
||||
The base Command class.
|
||||
|
||||
All commands in Evennia inherit from the 'Command' class in this module.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.utils.utils import is_iter, fill, lazy_property
|
||||
|
||||
|
||||
def _init_command(mcs, **kwargs):
|
||||
"""
|
||||
Helper command.
|
||||
Makes sure all data are stored as lowercase and
|
||||
do checking on all properties that should be in list form.
|
||||
Sets up locks to be more forgiving. This is used both by the metaclass
|
||||
and (optionally) at instantiation time.
|
||||
|
||||
If kwargs are given, these are set as instance-specific properties
|
||||
on the command.
|
||||
"""
|
||||
for i in range(len(kwargs)):
|
||||
# used for dynamic creation of commands
|
||||
key, value = kwargs.popitem()
|
||||
setattr(mcs, key, value)
|
||||
|
||||
mcs.key = mcs.key.lower()
|
||||
if mcs.aliases and not is_iter(mcs.aliases):
|
||||
try:
|
||||
mcs.aliases = [str(alias).strip().lower()
|
||||
for alias in mcs.aliases.split(',')]
|
||||
except Exception:
|
||||
mcs.aliases = []
|
||||
mcs.aliases = list(set(alias for alias in mcs.aliases
|
||||
if alias and alias != mcs.key))
|
||||
|
||||
# optimization - a set is much faster to match against than a list
|
||||
mcs._matchset = set([mcs.key] + mcs.aliases)
|
||||
# optimization for looping over keys+aliases
|
||||
mcs._keyaliases = tuple(mcs._matchset)
|
||||
|
||||
# by default we don't save the command between runs
|
||||
if not hasattr(mcs, "save_for_next"):
|
||||
mcs.save_for_next = False
|
||||
|
||||
# pre-process locks as defined in class definition
|
||||
temp = []
|
||||
if hasattr(mcs, 'permissions'):
|
||||
mcs.locks = mcs.permissions
|
||||
if not hasattr(mcs, 'locks'):
|
||||
# default if one forgets to define completely
|
||||
mcs.locks = "cmd:all()"
|
||||
if not "cmd:" in mcs.locks:
|
||||
mcs.locks = "cmd:all();" + mcs.locks
|
||||
for lockstring in mcs.locks.split(';'):
|
||||
if lockstring and not ':' in lockstring:
|
||||
lockstring = "cmd:%s" % lockstring
|
||||
temp.append(lockstring)
|
||||
mcs.lock_storage = ";".join(temp)
|
||||
|
||||
if hasattr(mcs, 'arg_regex') and isinstance(mcs.arg_regex, basestring):
|
||||
mcs.arg_regex = re.compile(r"%s" % mcs.arg_regex, re.I)
|
||||
if not hasattr(mcs, "auto_help"):
|
||||
mcs.auto_help = True
|
||||
if not hasattr(mcs, 'is_exit'):
|
||||
mcs.is_exit = False
|
||||
if not hasattr(mcs, "help_category"):
|
||||
mcs.help_category = "general"
|
||||
mcs.help_category = mcs.help_category.lower()
|
||||
|
||||
|
||||
class CommandMeta(type):
|
||||
"""
|
||||
The metaclass cleans up all properties on the class
|
||||
"""
|
||||
def __init__(mcs, *args, **kwargs):
|
||||
_init_command(mcs, **kwargs)
|
||||
super(CommandMeta, mcs).__init__(*args, **kwargs)
|
||||
|
||||
# The Command class is the basic unit of an Evennia command; when
|
||||
# defining new commands, the admin subclass this class and
|
||||
# define their own parser method to handle the input. The
|
||||
# advantage of this is inheritage; commands that have similar
|
||||
# structure can parse the input string the same way, minimizing
|
||||
# parsing errors.
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""
|
||||
Base command
|
||||
|
||||
Usage:
|
||||
command [args]
|
||||
|
||||
This is the base command class. Inherit from this
|
||||
to create new commands.
|
||||
|
||||
The cmdhandler makes the following variables available to the
|
||||
command methods (so you can always assume them to be there):
|
||||
self.caller - the game object calling the command
|
||||
self.cmdstring - the command name used to trigger this command (allows
|
||||
you to know which alias was used, for example)
|
||||
cmd.args - everything supplied to the command following the cmdstring
|
||||
(this is usually what is parsed in self.parse())
|
||||
cmd.cmdset - the cmdset from which this command was matched (useful only
|
||||
seldomly, notably for help-type commands, to create dynamic
|
||||
help entries and lists)
|
||||
cmd.obj - the object on which this command is defined. If a default command,
|
||||
this is usually the same as caller.
|
||||
|
||||
The following class properties can/should be defined on your child class:
|
||||
|
||||
key - identifier for command (e.g. "look")
|
||||
aliases - (optional) list of aliases (e.g. ["l", "loo"])
|
||||
locks - lock string (default is "cmd:all()")
|
||||
help_category - how to organize this help entry in help system
|
||||
(default is "General")
|
||||
auto_help - defaults to True. Allows for turning off auto-help generation
|
||||
arg_regex - (optional) raw string regex defining how the argument part of
|
||||
the command should look in order to match for this command
|
||||
(e.g. must it be a space between cmdname and arg?)
|
||||
|
||||
(Note that if auto_help is on, this initial string is also used by the
|
||||
system to create the help entry for the command, so it's a good idea to
|
||||
format it similar to this one)
|
||||
"""
|
||||
# Tie our metaclass, for some convenience cleanup
|
||||
__metaclass__ = CommandMeta
|
||||
|
||||
# the main way to call this command (e.g. 'look')
|
||||
key = "command"
|
||||
# alternative ways to call the command (e.g. 'l', 'glance', 'examine')
|
||||
aliases = []
|
||||
# a list of lock definitions on the form
|
||||
# cmd:[NOT] func(args) [ AND|OR][ NOT] func2(args)
|
||||
locks = ""
|
||||
# used by the help system to group commands in lists.
|
||||
help_category = "general"
|
||||
# This allows to turn off auto-help entry creation for individual commands.
|
||||
auto_help = True
|
||||
# optimization for quickly separating exit-commands from normal commands
|
||||
is_exit = False
|
||||
# define the command not only by key but by the regex form of its arguments
|
||||
arg_regex = None
|
||||
|
||||
# auto-set (by Evennia on command instantiation) are:
|
||||
# obj - which object this command is defined on
|
||||
# sessid - which session-id (if any) is responsible for triggering this command
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""the lockhandler works the same as for objects.
|
||||
optional kwargs will be set as properties on the Command at runtime,
|
||||
overloading evential same-named class properties."""
|
||||
if kwargs:
|
||||
_init_command(self, **kwargs)
|
||||
|
||||
@lazy_property
|
||||
def lockhandler(self):
|
||||
return LockHandler(self)
|
||||
|
||||
def __str__(self):
|
||||
"Print the command"
|
||||
return self.key
|
||||
|
||||
def __eq__(self, cmd):
|
||||
"""
|
||||
Compare two command instances to each other by matching their
|
||||
key and aliases.
|
||||
input can be either a cmd object or the name of a command.
|
||||
"""
|
||||
try:
|
||||
# first assume input is a command (the most common case)
|
||||
return cmd.key in self._matchset
|
||||
except AttributeError:
|
||||
# probably got a string
|
||||
return cmd in self._matchset
|
||||
|
||||
def __ne__(self, cmd):
|
||||
"""
|
||||
The logical negation of __eq__. Since this is one of the
|
||||
most called methods in Evennia (along with __eq__) we do some
|
||||
code-duplication here rather than issuing a method-lookup to __eq__.
|
||||
"""
|
||||
try:
|
||||
return not cmd.key in self._matcheset
|
||||
except AttributeError:
|
||||
return not cmd in self._matchset
|
||||
|
||||
def __contains__(self, query):
|
||||
"""
|
||||
This implements searches like 'if query in cmd'. It's a fuzzy matching
|
||||
used by the help system, returning True if query can be found
|
||||
as a substring of the commands key or its aliases.
|
||||
|
||||
query (str) - query to match against. Should be lower case.
|
||||
|
||||
"""
|
||||
return any(query in keyalias for keyalias in self._keyaliases)
|
||||
|
||||
def match(self, cmdname):
|
||||
"""
|
||||
This is called by the system when searching the available commands,
|
||||
in order to determine if this is the one we wanted. cmdname was
|
||||
previously extracted from the raw string by the system.
|
||||
|
||||
cmdname (str) is always lowercase when reaching this point.
|
||||
|
||||
"""
|
||||
return cmdname in self._matchset
|
||||
|
||||
def access(self, srcobj, access_type="cmd", default=False):
|
||||
"""
|
||||
This hook is called by the cmdhandler to determine if srcobj
|
||||
is allowed to execute this command. It should return a boolean
|
||||
value and is not normally something that need to be changed since
|
||||
it's using the Evennia permission system directly.
|
||||
"""
|
||||
return self.lockhandler.check(srcobj, access_type, default=default)
|
||||
|
||||
def msg(self, msg="", to_obj=None, from_obj=None,
|
||||
sessid=None, all_sessions=False, **kwargs):
|
||||
"""
|
||||
This is a shortcut instad of calling msg() directly on an object - it
|
||||
will detect if caller is an Object or a Player and also appends
|
||||
self.sessid automatically.
|
||||
|
||||
msg - text string of message to send
|
||||
to_obj - target object of message. Defaults to self.caller
|
||||
from_obj - source of message. Defaults to to_obj
|
||||
data - optional dictionary of data
|
||||
sessid - supply data only to a unique sessid (normally not used -
|
||||
this is only potentially useful if to_obj is a Player object
|
||||
different from self.caller or self.caller.player)
|
||||
all_sessions (bool) - default is to send only to the session
|
||||
connected to the target object
|
||||
"""
|
||||
from_obj = from_obj or self.caller
|
||||
to_obj = to_obj or from_obj
|
||||
if not sessid:
|
||||
if hasattr(to_obj, "sessid"):
|
||||
# this is the case when to_obj is e.g. a Character
|
||||
toobj_sessions = to_obj.sessid.get()
|
||||
|
||||
# If to_obj has more than one session MULTISESSION_MODE=3
|
||||
# we need to send to every session.
|
||||
#(setting it to None, does it)
|
||||
session_tosend = None
|
||||
if len(toobj_sessions) == 1:
|
||||
session_tosend=toobj_sessions[0]
|
||||
sessid = all_sessions and None or session_tosend
|
||||
elif to_obj == self.caller:
|
||||
# this is the case if to_obj is the calling Player
|
||||
sessid = all_sessions and None or self.sessid
|
||||
else:
|
||||
# if to_obj is a different Player, all their sessions
|
||||
# will be notified unless sessid was given specifically
|
||||
sessid = None
|
||||
to_obj.msg(msg, from_obj=from_obj, sessid=sessid, **kwargs)
|
||||
|
||||
# Common Command hooks
|
||||
|
||||
def at_pre_cmd(self):
|
||||
"""
|
||||
This hook is called before self.parse() on all commands.
|
||||
If this hook returns anything but False/None, the command
|
||||
sequence is aborted.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_post_cmd(self):
|
||||
"""
|
||||
This hook is called after the command has finished executing
|
||||
(after self.func()).
|
||||
"""
|
||||
pass
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Once the cmdhandler has identified this as the command we
|
||||
want, this function is run. If many of your commands have
|
||||
a similar syntax (for example 'cmd arg1 = arg2') you should simply
|
||||
define this once and just let other commands of the same form
|
||||
inherit from this. See the docstring of this module for
|
||||
which object properties are available to use
|
||||
(notably self.args).
|
||||
"""
|
||||
pass
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is the actual executing part of the command.
|
||||
It is called directly after self.parse(). See the docstring
|
||||
of this module for which object properties are available
|
||||
(beyond those set in self.parse())
|
||||
"""
|
||||
# a simple test command to show the available properties
|
||||
string = "-" * 50
|
||||
string += "\n{w%s{n - Command variables from evennia:\n" % self.key
|
||||
string += "-" * 50
|
||||
string += "\nname of cmd (self.key): {w%s{n\n" % self.key
|
||||
string += "cmd aliases (self.aliases): {w%s{n\n" % self.aliases
|
||||
string += "cmd locks (self.locks): {w%s{n\n" % self.locks
|
||||
string += "help category (self.help_category): {w%s{n\n" % self.help_category.capitalize()
|
||||
string += "object calling (self.caller): {w%s{n\n" % self.caller
|
||||
string += "object storing cmdset (self.obj): {w%s{n\n" % self.obj
|
||||
string += "command string given (self.cmdstring): {w%s{n\n" % self.cmdstring
|
||||
# show cmdset.key instead of cmdset to shorten output
|
||||
string += fill("current cmdset (self.cmdset): {w%s{n\n" % (self.cmdset.key if self.cmdset.key else self.cmdset.__class__))
|
||||
|
||||
self.caller.msg(string)
|
||||
21
lib/commands/connection_screen.py
Normal file
21
lib/commands/connection_screen.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#
|
||||
# This is Evennia's default connection screen. It is imported
|
||||
# and run from game/gamesrc/world/connection_screens.py.
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from src.utils import utils
|
||||
|
||||
DEFAULT_SCREEN = \
|
||||
"""{b=============================================================={n
|
||||
Welcome to {g%s{n, version %s!
|
||||
|
||||
If you have an existing account, connect to it by typing:
|
||||
{wconnect <username> <password>{n
|
||||
If you need to create an account, type (without the <>'s):
|
||||
{wcreate <username> <password>{n
|
||||
|
||||
If you have spaces in your username, enclose it in quotes.
|
||||
Enter {whelp{n for more info. {wlook{n will re-show this screen.
|
||||
{b=============================================================={n""" \
|
||||
% (settings.SERVERNAME, utils.get_evennia_version())
|
||||
3
lib/commands/default/__init__.py
Normal file
3
lib/commands/default/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
This package contains all default commands of Evennia, grouped after category.
|
||||
"""
|
||||
560
lib/commands/default/admin.py
Normal file
560
lib/commands/default/admin.py
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
"""
|
||||
|
||||
Admin commands
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
from django.conf import settings
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.server.models import ServerConfig
|
||||
from src.utils import prettytable, search
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
|
||||
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
|
||||
|
||||
# limit members for API inclusion
|
||||
__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelPlayer",
|
||||
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall")
|
||||
|
||||
|
||||
class CmdBoot(MuxCommand):
|
||||
"""
|
||||
kick a player from the server.
|
||||
|
||||
Usage
|
||||
@boot[/switches] <player obj> [: reason]
|
||||
|
||||
Switches:
|
||||
quiet - Silently boot without informing player
|
||||
port - boot by port number instead of name or dbref
|
||||
|
||||
Boot a player object from the server. If a reason is
|
||||
supplied it will be echoed to the user unless /quiet is set.
|
||||
"""
|
||||
|
||||
key = "@boot"
|
||||
locks = "cmd:perm(boot) or perm(Wizards)"
|
||||
help_category = "Admin"
|
||||
|
||||
def func(self):
|
||||
"Implementing the function"
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
|
||||
if not args:
|
||||
caller.msg("Usage: @boot[/switches] <player> [:reason]")
|
||||
return
|
||||
|
||||
if ':' in args:
|
||||
args, reason = [a.strip() for a in args.split(':', 1)]
|
||||
else:
|
||||
args, reason = args, ""
|
||||
|
||||
boot_list = []
|
||||
|
||||
if 'port' in self.switches:
|
||||
# Boot a particular port.
|
||||
sessions = SESSIONS.get_session_list(True)
|
||||
for sess in sessions:
|
||||
# Find the session with the matching port number.
|
||||
if sess.getClientAddress()[1] == int(args):
|
||||
boot_list.append(sess)
|
||||
break
|
||||
else:
|
||||
# Boot by player object
|
||||
pobj = search.player_search(args)
|
||||
if not pobj:
|
||||
self.caller("Player %s was not found." % pobj.key)
|
||||
return
|
||||
pobj = pobj[0]
|
||||
if not pobj.access(caller, 'boot'):
|
||||
string = "You don't have the permission to boot %s."
|
||||
pobj.msg(string)
|
||||
return
|
||||
# we have a bootable object with a connected user
|
||||
matches = SESSIONS.sessions_from_player(pobj)
|
||||
for match in matches:
|
||||
boot_list.append(match)
|
||||
|
||||
if not boot_list:
|
||||
caller.msg("No matching sessions found. The Player does not seem to be online.")
|
||||
return
|
||||
|
||||
# Carry out the booting of the sessions in the boot list.
|
||||
|
||||
feedback = None
|
||||
if not 'quiet' in self.switches:
|
||||
feedback = "You have been disconnected by %s.\n" % caller.name
|
||||
if reason:
|
||||
feedback += "\nReason given: %s" % reason
|
||||
|
||||
for session in boot_list:
|
||||
session.msg(feedback)
|
||||
pobj.disconnect_session_from_player(session.sessid)
|
||||
|
||||
|
||||
# regex matching IP addresses with wildcards, eg. 233.122.4.*
|
||||
IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")
|
||||
|
||||
|
||||
def list_bans(banlist):
|
||||
"""
|
||||
Helper function to display a list of active bans. Input argument
|
||||
is the banlist read into the two commands @ban and @unban below.
|
||||
"""
|
||||
if not banlist:
|
||||
return "No active bans were found."
|
||||
|
||||
table = prettytable.PrettyTable(["{wid", "{wname/ip", "{wdate", "{wreason"])
|
||||
for inum, ban in enumerate(banlist):
|
||||
table.add_row([str(inum + 1),
|
||||
ban[0] and ban[0] or ban[1],
|
||||
ban[3], ban[4]])
|
||||
string = "{wActive bans:{n\n%s" % table
|
||||
return string
|
||||
|
||||
|
||||
class CmdBan(MuxCommand):
|
||||
"""
|
||||
ban a player from the server
|
||||
|
||||
Usage:
|
||||
@ban [<name or ip> [: reason]]
|
||||
|
||||
Without any arguments, shows numbered list of active bans.
|
||||
|
||||
This command bans a user from accessing the game. Supply an
|
||||
optional reason to be able to later remember why the ban was put in
|
||||
place
|
||||
|
||||
It is often to
|
||||
prefer over deleting a player with @delplayer. If banned by name,
|
||||
that player account can no longer be logged into.
|
||||
|
||||
IP (Internet Protocol) address banning allows to block all access
|
||||
from a specific address or subnet. Use the asterisk (*) as a
|
||||
wildcard.
|
||||
|
||||
Examples:
|
||||
@ban thomas - ban account 'thomas'
|
||||
@ban/ip 134.233.2.111 - ban specific ip address
|
||||
@ban/ip 134.233.2.* - ban all in a subnet
|
||||
@ban/ip 134.233.*.* - even wider ban
|
||||
|
||||
A single IP filter is easy to circumvent by changing the computer
|
||||
(also, some ISPs assign only temporary IPs to their users in the
|
||||
first placer. Widening the IP block filter with wildcards might be
|
||||
tempting, but remember that blocking too much may accidentally
|
||||
also block innocent users connecting from the same country and
|
||||
region.
|
||||
|
||||
"""
|
||||
key = "@ban"
|
||||
aliases = ["@bans"]
|
||||
locks = "cmd:perm(ban) or perm(Immortals)"
|
||||
help_category = "Admin"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Bans are stored in a serverconf db object as a list of
|
||||
dictionaries:
|
||||
[ (name, ip, ipregex, date, reason),
|
||||
(name, ip, ipregex, date, reason),... ]
|
||||
where name and ip are set by the user and are shown in
|
||||
lists. ipregex is a converted form of ip where the * is
|
||||
replaced by an appropriate regex pattern for fast
|
||||
matching. date is the time stamp the ban was instigated and
|
||||
'reason' is any optional info given to the command. Unset
|
||||
values in each tuple is set to the empty string.
|
||||
"""
|
||||
banlist = ServerConfig.objects.conf('server_bans')
|
||||
if not banlist:
|
||||
banlist = []
|
||||
|
||||
if not self.args or (self.switches
|
||||
and not any(switch in ('ip', 'name')
|
||||
for switch in self.switches)):
|
||||
self.caller.msg(list_bans(banlist))
|
||||
return
|
||||
|
||||
now = time.ctime()
|
||||
reason = ""
|
||||
if ':' in self.args:
|
||||
ban, reason = self.args.rsplit(':', 1)
|
||||
else:
|
||||
ban = self.args
|
||||
ban = ban.lower()
|
||||
ipban = IPREGEX.findall(ban)
|
||||
if not ipban:
|
||||
# store as name
|
||||
typ = "Name"
|
||||
bantup = (ban, "", "", now, reason)
|
||||
else:
|
||||
# an ip address.
|
||||
typ = "IP"
|
||||
ban = ipban[0]
|
||||
# replace * with regex form and compile it
|
||||
ipregex = ban.replace('.', '\.')
|
||||
ipregex = ipregex.replace('*', '[0-9]{1,3}')
|
||||
#print "regex:",ipregex
|
||||
ipregex = re.compile(r"%s" % ipregex)
|
||||
bantup = ("", ban, ipregex, now, reason)
|
||||
# save updated banlist
|
||||
banlist.append(bantup)
|
||||
ServerConfig.objects.conf('server_bans', banlist)
|
||||
self.caller.msg("%s-Ban {w%s{n was added." % (typ, ban))
|
||||
|
||||
|
||||
class CmdUnban(MuxCommand):
|
||||
"""
|
||||
remove a ban from a player
|
||||
|
||||
Usage:
|
||||
@unban <banid>
|
||||
|
||||
This will clear a player name/ip ban previously set with the @ban
|
||||
command. Use this command without an argument to view a numbered
|
||||
list of bans. Use the numbers in this list to select which one to
|
||||
unban.
|
||||
|
||||
"""
|
||||
key = "@unban"
|
||||
locks = "cmd:perm(unban) or perm(Immortals)"
|
||||
help_category = "Admin"
|
||||
|
||||
def func(self):
|
||||
"Implement unbanning"
|
||||
|
||||
banlist = ServerConfig.objects.conf('server_bans')
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg(list_bans(banlist))
|
||||
return
|
||||
|
||||
try:
|
||||
num = int(self.args)
|
||||
except Exception:
|
||||
self.caller.msg("You must supply a valid ban id to clear.")
|
||||
return
|
||||
|
||||
if not banlist:
|
||||
self.caller.msg("There are no bans to clear.")
|
||||
elif not (0 < num < len(banlist) + 1):
|
||||
self.caller.msg("Ban id {w%s{x was not found." % self.args)
|
||||
else:
|
||||
# all is ok, clear ban
|
||||
ban = banlist[num - 1]
|
||||
del banlist[num - 1]
|
||||
ServerConfig.objects.conf('server_bans', banlist)
|
||||
self.caller.msg("Cleared ban %s: %s" %
|
||||
(num, " ".join([s for s in ban[:2]])))
|
||||
|
||||
|
||||
class CmdDelPlayer(MuxCommand):
|
||||
"""
|
||||
delete a player from the server
|
||||
|
||||
Usage:
|
||||
@delplayer[/switch] <name> [: reason]
|
||||
|
||||
Switch:
|
||||
delobj - also delete the player's currently
|
||||
assigned in-game object.
|
||||
|
||||
Completely deletes a user from the server database,
|
||||
making their nick and e-mail again available.
|
||||
"""
|
||||
|
||||
key = "@delplayer"
|
||||
locks = "cmd:perm(delplayer) or perm(Immortals)"
|
||||
help_category = "Admin"
|
||||
|
||||
def func(self):
|
||||
"Implements the command."
|
||||
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
|
||||
if hasattr(caller, 'player'):
|
||||
caller = caller.player
|
||||
|
||||
if not args:
|
||||
self.msg("Usage: @delplayer <player/user name or #id> [: reason]")
|
||||
return
|
||||
|
||||
reason = ""
|
||||
if ':' in args:
|
||||
args, reason = [arg.strip() for arg in args.split(':', 1)]
|
||||
|
||||
# We use player_search since we want to be sure to find also players
|
||||
# that lack characters.
|
||||
players = search.player_search(args)
|
||||
|
||||
if not players:
|
||||
self.msg('Could not find a player by that name.')
|
||||
return
|
||||
|
||||
if len(players) > 1:
|
||||
string = "There were multiple matches:"
|
||||
for player in players:
|
||||
string += "\n %s %s" % (player.id, player.key)
|
||||
return
|
||||
|
||||
# one single match
|
||||
|
||||
player = players.pop()
|
||||
|
||||
if not player.access(caller, 'delete'):
|
||||
string = "You don't have the permissions to delete that player."
|
||||
self.msg(string)
|
||||
return
|
||||
|
||||
uname = player.username
|
||||
# boot the player then delete
|
||||
self.msg("Informing and disconnecting player ...")
|
||||
string = "\nYour account '%s' is being *permanently* deleted.\n" % uname
|
||||
if reason:
|
||||
string += " Reason given:\n '%s'" % reason
|
||||
player.msg(string)
|
||||
player.delete()
|
||||
self.msg("Player %s was successfully deleted." % uname)
|
||||
|
||||
|
||||
class CmdEmit(MuxCommand):
|
||||
"""
|
||||
admin command for emitting message to multiple objects
|
||||
|
||||
Usage:
|
||||
@emit[/switches] [<obj>, <obj>, ... =] <message>
|
||||
@remit [<obj>, <obj>, ... =] <message>
|
||||
@pemit [<obj>, <obj>, ... =] <message>
|
||||
|
||||
Switches:
|
||||
room : limit emits to rooms only (default)
|
||||
players : limit emits to players only
|
||||
contents : send to the contents of matched objects too
|
||||
|
||||
Emits a message to the selected objects or to
|
||||
your immediate surroundings. If the object is a room,
|
||||
send to its contents. @remit and @pemit are just
|
||||
limited forms of @emit, for sending to rooms and
|
||||
to players respectively.
|
||||
"""
|
||||
key = "@emit"
|
||||
aliases = ["@pemit", "@remit"]
|
||||
locks = "cmd:perm(emit) or perm(Builders)"
|
||||
help_category = "Admin"
|
||||
|
||||
def func(self):
|
||||
"Implement the command"
|
||||
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
|
||||
if not args:
|
||||
string = "Usage: "
|
||||
string += "\n@emit[/switches] [<obj>, <obj>, ... =] <message>"
|
||||
string += "\n@remit [<obj>, <obj>, ... =] <message>"
|
||||
string += "\n@pemit [<obj>, <obj>, ... =] <message>"
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
rooms_only = 'rooms' in self.switches
|
||||
players_only = 'players' in self.switches
|
||||
send_to_contents = 'contents' in self.switches
|
||||
|
||||
# we check which command was used to force the switches
|
||||
if self.cmdstring == '@remit':
|
||||
rooms_only = True
|
||||
send_to_contents = True
|
||||
elif self.cmdstring == '@pemit':
|
||||
players_only = True
|
||||
|
||||
if not self.rhs:
|
||||
message = self.args
|
||||
objnames = [caller.location.key]
|
||||
else:
|
||||
message = self.rhs
|
||||
objnames = self.lhslist
|
||||
|
||||
# send to all objects
|
||||
for objname in objnames:
|
||||
obj = caller.search(objname, global_search=True)
|
||||
if not obj:
|
||||
return
|
||||
if rooms_only and not obj.location is None:
|
||||
caller.msg("%s is not a room. Ignored." % objname)
|
||||
continue
|
||||
if players_only and not obj.has_player:
|
||||
caller.msg("%s has no active player. Ignored." % objname)
|
||||
continue
|
||||
if obj.access(caller, 'tell'):
|
||||
obj.msg(message)
|
||||
if send_to_contents and hasattr(obj, "msg_contents"):
|
||||
obj.msg_contents(message)
|
||||
caller.msg("Emitted to %s and contents:\n%s" % (objname, message))
|
||||
else:
|
||||
caller.msg("Emitted to %s:\n%s" % (objname, message))
|
||||
else:
|
||||
caller.msg("You are not allowed to emit to %s." % objname)
|
||||
|
||||
|
||||
class CmdNewPassword(MuxCommand):
|
||||
"""
|
||||
change the password of a player
|
||||
|
||||
Usage:
|
||||
@userpassword <user obj> = <new password>
|
||||
|
||||
Set a player's password.
|
||||
"""
|
||||
|
||||
key = "@userpassword"
|
||||
locks = "cmd:perm(newpassword) or perm(Wizards)"
|
||||
help_category = "Admin"
|
||||
|
||||
def func(self):
|
||||
"Implement the function."
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if not self.rhs:
|
||||
self.msg("Usage: @userpassword <user obj> = <new password>")
|
||||
return
|
||||
|
||||
# the player search also matches 'me' etc.
|
||||
player = caller.search_player(self.lhs)
|
||||
if not player:
|
||||
return
|
||||
player.set_password(self.rhs)
|
||||
player.save()
|
||||
self.msg("%s - new password set to '%s'." % (player.name, self.rhs))
|
||||
if player.character != caller:
|
||||
player.msg("%s has changed your password to '%s'." % (caller.name,
|
||||
self.rhs))
|
||||
|
||||
|
||||
class CmdPerm(MuxCommand):
|
||||
"""
|
||||
set the permissions of a player/object
|
||||
|
||||
Usage:
|
||||
@perm[/switch] <object> [= <permission>[,<permission>,...]]
|
||||
@perm[/switch] *<player> [= <permission>[,<permission>,...]]
|
||||
|
||||
Switches:
|
||||
del : delete the given permission from <object> or <player>.
|
||||
player : set permission on a player (same as adding * to name)
|
||||
|
||||
This command sets/clears individual permission strings on an object
|
||||
or player. If no permission is given, list all permissions on <object>.
|
||||
"""
|
||||
key = "@perm"
|
||||
aliases = "@setperm"
|
||||
locks = "cmd:perm(perm) or perm(Immortals)"
|
||||
help_category = "Admin"
|
||||
|
||||
def func(self):
|
||||
"Implement function"
|
||||
|
||||
caller = self.caller
|
||||
switches = self.switches
|
||||
lhs, rhs = self.lhs, self.rhs
|
||||
|
||||
if not self.args:
|
||||
string = "Usage: @perm[/switch] object [ = permission, permission, ...]"
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
playermode = 'player' in self.switches or lhs.startswith('*')
|
||||
lhs = lhs.lstrip("*")
|
||||
|
||||
if playermode:
|
||||
obj = caller.search_player(lhs)
|
||||
else:
|
||||
obj = caller.search(lhs, global_search=True)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
if not rhs:
|
||||
if not obj.access(caller, 'examine'):
|
||||
caller.msg("You are not allowed to examine this object.")
|
||||
return
|
||||
|
||||
string = "Permissions on {w%s{n: " % obj.key
|
||||
if not obj.permissions.all():
|
||||
string += "<None>"
|
||||
else:
|
||||
string += ", ".join(obj.permissions.all())
|
||||
if (hasattr(obj, 'player') and
|
||||
hasattr(obj.player, 'is_superuser') and
|
||||
obj.player.is_superuser):
|
||||
string += "\n(... but this object is currently controlled by a SUPERUSER! "
|
||||
string += "All access checks are passed automatically.)"
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
# we supplied an argument on the form obj = perm
|
||||
|
||||
if not obj.access(caller, 'control'):
|
||||
caller.msg("You are not allowed to edit this object's permissions.")
|
||||
return
|
||||
|
||||
cstring = ""
|
||||
tstring = ""
|
||||
if 'del' in switches:
|
||||
# delete the given permission(s) from object.
|
||||
for perm in self.rhslist:
|
||||
obj.permissions.remove(perm)
|
||||
if obj.permissions.get(perm):
|
||||
cstring += "\nPermissions %s could not be removed from %s." % (perm, obj.name)
|
||||
else:
|
||||
cstring += "\nPermission %s removed from %s (if they existed)." % (perm, obj.name)
|
||||
tstring += "\n%s revokes the permission(s) %s from you." % (caller.name, perm)
|
||||
else:
|
||||
# add a new permission
|
||||
permissions = obj.permissions.all()
|
||||
|
||||
for perm in self.rhslist:
|
||||
|
||||
# don't allow to set a permission higher in the hierarchy than
|
||||
# the one the caller has (to prevent self-escalation)
|
||||
if (perm.lower() in PERMISSION_HIERARCHY and not
|
||||
obj.locks.check_lockstring(caller, "dummy:perm(%s)" % perm)):
|
||||
caller.msg("You cannot assign a permission higher than the one you have yourself.")
|
||||
return
|
||||
|
||||
if perm in permissions:
|
||||
cstring += "\nPermission '%s' is already defined on %s." % (rhs, obj.name)
|
||||
else:
|
||||
obj.permissions.add(perm)
|
||||
plystring = "the Player" if playermode else "the Object/Character"
|
||||
cstring += "\nPermission '%s' given to %s (%s)." % (rhs, obj.name, plystring)
|
||||
tstring += "\n%s gives you (%s, %s) the permission '%s'." % (caller.name, obj.name, plystring, rhs)
|
||||
caller.msg(cstring.strip())
|
||||
if tstring:
|
||||
obj.msg(tstring.strip())
|
||||
|
||||
class CmdWall(MuxCommand):
|
||||
"""
|
||||
make an announcement to all
|
||||
|
||||
Usage:
|
||||
@wall <message>
|
||||
|
||||
Announces a message to all connected players.
|
||||
"""
|
||||
key = "@wall"
|
||||
locks = "cmd:perm(wall) or perm(Wizards)"
|
||||
help_category = "Admin"
|
||||
|
||||
def func(self):
|
||||
"Implements command"
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: @wall <message>")
|
||||
return
|
||||
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
|
||||
self.msg("Announcing to all connected players ...")
|
||||
SESSIONS.announce_all(message)
|
||||
843
lib/commands/default/batchprocess.py
Normal file
843
lib/commands/default/batchprocess.py
Normal file
|
|
@ -0,0 +1,843 @@
|
|||
"""
|
||||
Batch processors
|
||||
|
||||
These commands implements the 'batch-command' and 'batch-code'
|
||||
processors, using the functionality in src.utils.batchprocessors.
|
||||
They allow for offline world-building.
|
||||
|
||||
Batch-command is the simpler system. This reads a file (*.ev)
|
||||
containing a list of in-game commands and executes them in sequence as
|
||||
if they had been entered in the game (including permission checks
|
||||
etc).
|
||||
|
||||
Example batch-command file: game/gamesrc/commands/examples/batch_cmds.ev
|
||||
|
||||
Batch-code is a full-fledged python code interpreter that reads blocks
|
||||
of python code (*.py) and executes them in sequence. This allows for
|
||||
much more power than Batch-command, but requires knowing Python and
|
||||
the Evennia API. It is also a severe security risk and should
|
||||
therefore always be limited to superusers only.
|
||||
|
||||
Example batch-code file: game/gamesrc/commands/examples/batch_code.py
|
||||
|
||||
"""
|
||||
from traceback import format_exc
|
||||
from django.conf import settings
|
||||
from src.utils.batchprocessors import BATCHCMD, BATCHCODE
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from src.utils import utils
|
||||
|
||||
# limit symbols for API inclusion
|
||||
__all__ = ("CmdBatchCommands", "CmdBatchCode")
|
||||
|
||||
_HEADER_WIDTH = 70
|
||||
_UTF8_ERROR = \
|
||||
"""
|
||||
{rDecode error in '%s'.{n
|
||||
|
||||
This file contains non-ascii character(s). This is common if you
|
||||
wrote some input in a language that has more letters and special
|
||||
symbols than English; such as accents or umlauts. This is usually
|
||||
fine and fully supported! But for Evennia to know how to decode such
|
||||
characters in a universal way, the batchfile must be saved with the
|
||||
international 'UTF-8' encoding. This file is not.
|
||||
|
||||
Please re-save the batchfile with the UTF-8 encoding (refer to the
|
||||
documentation of your text editor on how to do this, or switch to a
|
||||
better featured one) and try again.
|
||||
|
||||
Error reported was: '%s'
|
||||
"""
|
||||
|
||||
_PROCPOOL_BATCHCMD_SOURCE = """
|
||||
from src.commands.default.batchprocess import batch_cmd_exec, step_pointer, BatchSafeCmdSet
|
||||
caller.ndb.batch_stack = commands
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.ndb.batch_batchmode = "batch_commands"
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
for inum in range(len(commands)):
|
||||
print "command:", inum
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
if not batch_cmd_exec(caller):
|
||||
break
|
||||
step_pointer(caller, 1)
|
||||
print "leaving run ..."
|
||||
"""
|
||||
_PROCPOOL_BATCHCODE_SOURCE = """
|
||||
from src.commands.default.batchprocess import batch_code_exec, step_pointer, BatchSafeCmdSet
|
||||
caller.ndb.batch_stack = codes
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.ndb.batch_batchmode = "batch_code"
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
for inum in range(len(codes)):
|
||||
print "code:", inum
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
if not batch_code_exec(caller):
|
||||
break
|
||||
step_pointer(caller, 1)
|
||||
print "leaving run ..."
|
||||
"""
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Helper functions
|
||||
#------------------------------------------------------------
|
||||
|
||||
def format_header(caller, entry):
|
||||
"""
|
||||
Formats a header
|
||||
"""
|
||||
width = _HEADER_WIDTH - 10
|
||||
entry = entry.strip()
|
||||
header = utils.crop(entry, width=width)
|
||||
ptr = caller.ndb.batch_stackptr + 1
|
||||
stacklen = len(caller.ndb.batch_stack)
|
||||
header = "{w%02i/%02i{G: %s{n" % (ptr, stacklen, header)
|
||||
# add extra space to the side for padding.
|
||||
header = "%s%s" % (header, " " * (width - len(header)))
|
||||
header = header.replace('\n', '\\n')
|
||||
|
||||
return header
|
||||
|
||||
|
||||
def format_code(entry):
|
||||
"""
|
||||
Formats the viewing of code and errors
|
||||
"""
|
||||
code = ""
|
||||
for line in entry.split('\n'):
|
||||
code += "\n{G>>>{n %s" % line
|
||||
return code.strip()
|
||||
|
||||
|
||||
def batch_cmd_exec(caller):
|
||||
"""
|
||||
Helper function for executing a single batch-command entry
|
||||
"""
|
||||
ptr = caller.ndb.batch_stackptr
|
||||
stack = caller.ndb.batch_stack
|
||||
command = stack[ptr]
|
||||
caller.msg(format_header(caller, command))
|
||||
try:
|
||||
caller.execute_cmd(command)
|
||||
except Exception:
|
||||
caller.msg(format_code(format_exc()))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def batch_code_exec(caller):
|
||||
"""
|
||||
Helper function for executing a single batch-code entry
|
||||
"""
|
||||
ptr = caller.ndb.batch_stackptr
|
||||
stack = caller.ndb.batch_stack
|
||||
debug = caller.ndb.batch_debug
|
||||
code = stack[ptr]
|
||||
|
||||
caller.msg(format_header(caller, code))
|
||||
err = BATCHCODE.code_exec(code,
|
||||
extra_environ={"caller": caller}, debug=debug)
|
||||
if err:
|
||||
caller.msg(format_code(err))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def step_pointer(caller, step=1):
|
||||
"""
|
||||
Step in stack, returning the item located.
|
||||
|
||||
stackptr - current position in stack
|
||||
stack - the stack of units
|
||||
step - how many steps to move from stackptr
|
||||
"""
|
||||
ptr = caller.ndb.batch_stackptr
|
||||
stack = caller.ndb.batch_stack
|
||||
nstack = len(stack)
|
||||
if ptr + step <= 0:
|
||||
caller.msg("{RBeginning of batch file.")
|
||||
if ptr + step >= nstack:
|
||||
caller.msg("{REnd of batch file.")
|
||||
caller.ndb.batch_stackptr = max(0, min(nstack - 1, ptr + step))
|
||||
|
||||
|
||||
def show_curr(caller, showall=False):
|
||||
"""
|
||||
Show the current position in stack
|
||||
"""
|
||||
stackptr = caller.ndb.batch_stackptr
|
||||
stack = caller.ndb.batch_stack
|
||||
|
||||
if stackptr >= len(stack):
|
||||
caller.ndb.batch_stackptr = len(stack) - 1
|
||||
show_curr(caller, showall)
|
||||
return
|
||||
|
||||
entry = stack[stackptr]
|
||||
|
||||
string = format_header(caller, entry)
|
||||
codeall = entry.strip()
|
||||
string += "{G(hh for help)"
|
||||
if showall:
|
||||
for line in codeall.split('\n'):
|
||||
string += "\n{G|{n %s" % line
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
def purge_processor(caller):
|
||||
"""
|
||||
This purges all effects running
|
||||
on the caller.
|
||||
"""
|
||||
try:
|
||||
del caller.ndb.batch_stack
|
||||
del caller.ndb.batch_stackptr
|
||||
del caller.ndb.batch_pythonpath
|
||||
del caller.ndb.batch_batchmode
|
||||
except:
|
||||
pass
|
||||
# clear everything but the default cmdset.
|
||||
caller.cmdset.delete(BatchSafeCmdSet)
|
||||
caller.cmdset.clear()
|
||||
caller.scripts.validate() # this will purge interactive mode
|
||||
|
||||
#------------------------------------------------------------
|
||||
# main access commands
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
class CmdBatchCommands(MuxCommand):
|
||||
"""
|
||||
build from batch-command file
|
||||
|
||||
Usage:
|
||||
@batchcommands[/interactive] <python.path.to.file>
|
||||
|
||||
Switch:
|
||||
interactive - this mode will offer more control when
|
||||
executing the batch file, like stepping,
|
||||
skipping, reloading etc.
|
||||
|
||||
Runs batches of commands from a batch-cmd text file (*.ev).
|
||||
|
||||
"""
|
||||
key = "@batchcommands"
|
||||
aliases = ["@batchcommand", "@batchcmd"]
|
||||
locks = "cmd:perm(batchcommands) or superuser()"
|
||||
help_category = "Building"
|
||||
|
||||
def func(self):
|
||||
"Starts the processor."
|
||||
|
||||
caller = self.caller
|
||||
|
||||
args = self.args
|
||||
if not args:
|
||||
caller.msg("Usage: @batchcommands[/interactive] <path.to.file>")
|
||||
return
|
||||
python_path = self.args
|
||||
|
||||
#parse indata file
|
||||
|
||||
try:
|
||||
commands = BATCHCMD.parse_file(python_path)
|
||||
except UnicodeDecodeError, err:
|
||||
caller.msg(_UTF8_ERROR % (python_path, err))
|
||||
return
|
||||
except IOError:
|
||||
string = "'%s' not found.\nYou have to supply the python path "
|
||||
string += "of the file relative to \none of your batch-file directories (%s)."
|
||||
caller.msg(string % (python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
|
||||
return
|
||||
if not commands:
|
||||
caller.msg("File %s seems empty of valid commands." % python_path)
|
||||
return
|
||||
|
||||
switches = self.switches
|
||||
|
||||
# Store work data in cache
|
||||
caller.ndb.batch_stack = commands
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.ndb.batch_pythonpath = python_path
|
||||
caller.ndb.batch_batchmode = "batch_commands"
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
|
||||
if 'inter' in switches or 'interactive' in switches:
|
||||
# Allow more control over how batch file is executed
|
||||
|
||||
# Set interactive state directly
|
||||
caller.cmdset.add(BatchInteractiveCmdSet)
|
||||
|
||||
caller.msg("\nBatch-command processor - Interactive mode for %s ..." % python_path)
|
||||
show_curr(caller)
|
||||
else:
|
||||
caller.msg("Running Batch-command processor - Automatic mode for %s (this might take some time) ..." % python_path)
|
||||
|
||||
procpool = False
|
||||
if "PythonProcPool" in utils.server_services():
|
||||
if utils.uses_database("sqlite3"):
|
||||
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
|
||||
else:
|
||||
procpool = True
|
||||
|
||||
if procpool:
|
||||
# run in parallel process
|
||||
def callback(r):
|
||||
caller.msg(" {GBatchfile '%s' applied." % python_path)
|
||||
purge_processor(caller)
|
||||
|
||||
def errback(e):
|
||||
caller.msg(" {RError from processor: '%s'" % e)
|
||||
purge_processor(caller)
|
||||
|
||||
utils.run_async(_PROCPOOL_BATCHCMD_SOURCE,
|
||||
commands=commands,
|
||||
caller=caller,
|
||||
at_return=callback,
|
||||
at_err=errback)
|
||||
else:
|
||||
# run in-process (might block)
|
||||
for inum in range(len(commands)):
|
||||
# loop through the batch file
|
||||
if not batch_cmd_exec(caller):
|
||||
return
|
||||
step_pointer(caller, 1)
|
||||
# clean out the safety cmdset and clean out all other
|
||||
# temporary attrs.
|
||||
string = " Batchfile '%s' applied." % python_path
|
||||
caller.msg("{G%s" % string)
|
||||
purge_processor(caller)
|
||||
|
||||
|
||||
class CmdBatchCode(MuxCommand):
|
||||
"""
|
||||
build from batch-code file
|
||||
|
||||
Usage:
|
||||
@batchcode[/interactive] <python path to file>
|
||||
|
||||
Switch:
|
||||
interactive - this mode will offer more control when
|
||||
executing the batch file, like stepping,
|
||||
skipping, reloading etc.
|
||||
debug - auto-delete all objects that has been marked as
|
||||
deletable in the script file (see example files for
|
||||
syntax). This is useful so as to to not leave multiple
|
||||
object copies behind when testing out the script.
|
||||
|
||||
Runs batches of commands from a batch-code text file (*.py).
|
||||
|
||||
"""
|
||||
key = "@batchcode"
|
||||
aliases = ["@batchcodes"]
|
||||
locks = "cmd:superuser()"
|
||||
help_category = "Building"
|
||||
|
||||
def func(self):
|
||||
"Starts the processor."
|
||||
|
||||
caller = self.caller
|
||||
|
||||
args = self.args
|
||||
if not args:
|
||||
caller.msg("Usage: @batchcode[/interactive/debug] <path.to.file>")
|
||||
return
|
||||
python_path = self.args
|
||||
debug = 'debug' in self.switches
|
||||
|
||||
#parse indata file
|
||||
try:
|
||||
codes = BATCHCODE.parse_file(python_path, debug=debug)
|
||||
except UnicodeDecodeError, err:
|
||||
caller.msg(_UTF8_ERROR % (python_path, err))
|
||||
return
|
||||
except IOError:
|
||||
string = "'%s' not found.\nYou have to supply the python path "
|
||||
string += "of the file relative to \nyour batch-file directories (%s)."
|
||||
caller.msg(string % (python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
|
||||
return
|
||||
if not codes:
|
||||
caller.msg("File %s seems empty of functional code." % python_path)
|
||||
return
|
||||
|
||||
switches = self.switches
|
||||
|
||||
# Store work data in cache
|
||||
caller.ndb.batch_stack = codes
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.ndb.batch_pythonpath = python_path
|
||||
caller.ndb.batch_batchmode = "batch_code"
|
||||
caller.ndb.batch_debug = debug
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
|
||||
if 'inter' in switches or 'interactive'in switches:
|
||||
# Allow more control over how batch file is executed
|
||||
|
||||
# Set interactive state directly
|
||||
caller.cmdset.add(BatchInteractiveCmdSet)
|
||||
|
||||
caller.msg("\nBatch-code processor - Interactive mode for %s ..." % python_path)
|
||||
show_curr(caller)
|
||||
else:
|
||||
caller.msg("Running Batch-code processor - Automatic mode for %s ..." % python_path)
|
||||
|
||||
procpool = False
|
||||
if "PythonProcPool" in utils.server_services():
|
||||
if utils.uses_database("sqlite3"):
|
||||
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
|
||||
else:
|
||||
procpool = True
|
||||
if procpool:
|
||||
# run in parallel process
|
||||
def callback(r):
|
||||
caller.msg(" {GBatchfile '%s' applied." % python_path)
|
||||
purge_processor(caller)
|
||||
|
||||
def errback(e):
|
||||
caller.msg(" {RError from processor: '%s'" % e)
|
||||
purge_processor(caller)
|
||||
utils.run_async(_PROCPOOL_BATCHCODE_SOURCE,
|
||||
codes=codes,
|
||||
caller=caller,
|
||||
at_return=callback,
|
||||
at_err=errback)
|
||||
else:
|
||||
# un in-process (will block)
|
||||
for inum in range(len(codes)):
|
||||
# loop through the batch file
|
||||
if not batch_code_exec(caller):
|
||||
return
|
||||
step_pointer(caller, 1)
|
||||
# clean out the safety cmdset and clean out all other
|
||||
# temporary attrs.
|
||||
string = " Batchfile '%s' applied." % python_path
|
||||
caller.msg("{G%s" % string)
|
||||
purge_processor(caller)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# State-commands for the interactive batch processor modes
|
||||
# (these are the same for both processors)
|
||||
#------------------------------------------------------------
|
||||
|
||||
class CmdStateAbort(MuxCommand):
|
||||
"""
|
||||
@abort
|
||||
|
||||
This is a safety feature. It force-ejects us out of the processor and to
|
||||
the default cmdset, regardless of what current cmdset the processor might
|
||||
have put us in (e.g. when testing buggy scripts etc).
|
||||
"""
|
||||
key = "@abort"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
"Exit back to default."
|
||||
purge_processor(self.caller)
|
||||
self.caller.msg("Exited processor and reset out active cmdset back to the default one.")
|
||||
|
||||
|
||||
class CmdStateLL(MuxCommand):
|
||||
"""
|
||||
ll
|
||||
|
||||
Look at the full source for the current
|
||||
command definition.
|
||||
"""
|
||||
key = "ll"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
show_curr(self.caller, showall=True)
|
||||
|
||||
class CmdStatePP(MuxCommand):
|
||||
"""
|
||||
pp
|
||||
|
||||
Process the currently shown command definition.
|
||||
"""
|
||||
key = "pp"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This checks which type of processor we are running.
|
||||
"""
|
||||
caller = self.caller
|
||||
if caller.ndb.batch_batchmode == "batch_code":
|
||||
batch_code_exec(caller)
|
||||
else:
|
||||
batch_cmd_exec(caller)
|
||||
|
||||
|
||||
class CmdStateRR(MuxCommand):
|
||||
"""
|
||||
rr
|
||||
|
||||
Reload the batch file, keeping the current
|
||||
position in it.
|
||||
"""
|
||||
key = "rr"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
if caller.ndb.batch_batchmode == "batch_code":
|
||||
new_data = BATCHCODE.parse_file(caller.ndb.batch_pythonpath)
|
||||
else:
|
||||
new_data = BATCHCMD.parse_file(caller.ndb.batch_pythonpath)
|
||||
caller.ndb.batch_stack = new_data
|
||||
caller.msg(format_code("File reloaded. Staying on same command."))
|
||||
show_curr(caller)
|
||||
|
||||
|
||||
class CmdStateRRR(MuxCommand):
|
||||
"""
|
||||
rrr
|
||||
|
||||
Reload the batch file, starting over
|
||||
from the beginning.
|
||||
"""
|
||||
key = "rrr"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
if caller.ndb.batch_batchmode == "batch_code":
|
||||
BATCHCODE.parse_file(caller.ndb.batch_pythonpath)
|
||||
else:
|
||||
BATCHCMD.parse_file(caller.ndb.batch_pythonpath)
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.msg(format_code("File reloaded. Restarting from top."))
|
||||
show_curr(caller)
|
||||
|
||||
|
||||
class CmdStateNN(MuxCommand):
|
||||
"""
|
||||
nn
|
||||
|
||||
Go to next command. No commands are executed.
|
||||
"""
|
||||
key = "nn"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
arg = self.args
|
||||
if arg and arg.isdigit():
|
||||
step = int(self.args)
|
||||
else:
|
||||
step = 1
|
||||
step_pointer(caller, step)
|
||||
show_curr(caller)
|
||||
|
||||
|
||||
class CmdStateNL(MuxCommand):
|
||||
"""
|
||||
nl
|
||||
|
||||
Go to next command, viewing its full source.
|
||||
No commands are executed.
|
||||
"""
|
||||
key = "nl"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
arg = self.args
|
||||
if arg and arg.isdigit():
|
||||
step = int(self.args)
|
||||
else:
|
||||
step = 1
|
||||
step_pointer(caller, step)
|
||||
show_curr(caller, showall=True)
|
||||
|
||||
|
||||
class CmdStateBB(MuxCommand):
|
||||
"""
|
||||
bb
|
||||
|
||||
Backwards to previous command. No commands
|
||||
are executed.
|
||||
"""
|
||||
key = "bb"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
arg = self.args
|
||||
if arg and arg.isdigit():
|
||||
step = -int(self.args)
|
||||
else:
|
||||
step = -1
|
||||
step_pointer(caller, step)
|
||||
show_curr(caller)
|
||||
|
||||
|
||||
class CmdStateBL(MuxCommand):
|
||||
"""
|
||||
bl
|
||||
|
||||
Backwards to previous command, viewing its full
|
||||
source. No commands are executed.
|
||||
"""
|
||||
key = "bl"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
arg = self.args
|
||||
if arg and arg.isdigit():
|
||||
step = -int(self.args)
|
||||
else:
|
||||
step = -1
|
||||
step_pointer(caller, step)
|
||||
show_curr(caller, showall=True)
|
||||
|
||||
|
||||
class CmdStateSS(MuxCommand):
|
||||
"""
|
||||
ss [steps]
|
||||
|
||||
Process current command, then step to the next
|
||||
one. If steps is given,
|
||||
process this many commands.
|
||||
"""
|
||||
key = "ss"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
arg = self.args
|
||||
if arg and arg.isdigit():
|
||||
step = int(self.args)
|
||||
else:
|
||||
step = 1
|
||||
|
||||
for istep in range(step):
|
||||
if caller.ndb.batch_batchmode == "batch_code":
|
||||
batch_code_exec(caller)
|
||||
else:
|
||||
batch_cmd_exec(caller)
|
||||
step_pointer(caller, 1)
|
||||
show_curr(caller)
|
||||
|
||||
|
||||
class CmdStateSL(MuxCommand):
|
||||
"""
|
||||
sl [steps]
|
||||
|
||||
Process current command, then step to the next
|
||||
one, viewing its full source. If steps is given,
|
||||
process this many commands.
|
||||
"""
|
||||
key = "sl"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
arg = self.args
|
||||
if arg and arg.isdigit():
|
||||
step = int(self.args)
|
||||
else:
|
||||
step = 1
|
||||
|
||||
for istep in range(step):
|
||||
if caller.ndb.batch_batchmode == "batch_code":
|
||||
batch_code_exec(caller)
|
||||
else:
|
||||
batch_cmd_exec(caller)
|
||||
step_pointer(caller, 1)
|
||||
show_curr(caller)
|
||||
|
||||
|
||||
class CmdStateCC(MuxCommand):
|
||||
"""
|
||||
cc
|
||||
|
||||
Continue to process all remaining
|
||||
commands.
|
||||
"""
|
||||
key = "cc"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
nstack = len(caller.ndb.batch_stack)
|
||||
ptr = caller.ndb.batch_stackptr
|
||||
step = nstack - ptr
|
||||
|
||||
for istep in range(step):
|
||||
if caller.ndb.batch_batchmode == "batch_code":
|
||||
batch_code_exec(caller)
|
||||
else:
|
||||
batch_cmd_exec(caller)
|
||||
step_pointer(caller, 1)
|
||||
show_curr(caller)
|
||||
|
||||
del caller.ndb.batch_stack
|
||||
del caller.ndb.batch_stackptr
|
||||
del caller.ndb.batch_pythonpath
|
||||
del caller.ndb.batch_batchmode
|
||||
caller.msg(format_code("Finished processing batch file."))
|
||||
|
||||
|
||||
class CmdStateJJ(MuxCommand):
|
||||
"""
|
||||
j <command number>
|
||||
|
||||
Jump to specific command number
|
||||
"""
|
||||
key = "j"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
arg = self.args
|
||||
if arg and arg.isdigit():
|
||||
number = int(self.args) - 1
|
||||
else:
|
||||
caller.msg(format_code("You must give a number index."))
|
||||
return
|
||||
ptr = caller.ndb.batch_stackptr
|
||||
step = number - ptr
|
||||
step_pointer(caller, step)
|
||||
show_curr(caller)
|
||||
|
||||
|
||||
class CmdStateJL(MuxCommand):
|
||||
"""
|
||||
jl <command number>
|
||||
|
||||
Jump to specific command number and view its full source.
|
||||
"""
|
||||
key = "jl"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
arg = self.args
|
||||
if arg and arg.isdigit():
|
||||
number = int(self.args) - 1
|
||||
else:
|
||||
caller.msg(format_code("You must give a number index."))
|
||||
return
|
||||
ptr = caller.ndb.batch_stackptr
|
||||
step = number - ptr
|
||||
step_pointer(caller, step)
|
||||
show_curr(caller, showall=True)
|
||||
|
||||
|
||||
class CmdStateQQ(MuxCommand):
|
||||
"""
|
||||
qq
|
||||
|
||||
Quit the batchprocessor.
|
||||
"""
|
||||
key = "qq"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
purge_processor(self.caller)
|
||||
self.caller.msg("Aborted interactive batch mode.")
|
||||
|
||||
|
||||
class CmdStateHH(MuxCommand):
|
||||
"Help command"
|
||||
|
||||
key = "hh"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
def func(self):
|
||||
string = """
|
||||
Interactive batch processing commands:
|
||||
|
||||
nn [steps] - next command (no processing)
|
||||
nl [steps] - next & look
|
||||
bb [steps] - back to previous command (no processing)
|
||||
bl [steps] - back & look
|
||||
jj <N> - jump to command nr N (no processing)
|
||||
jl <N> - jump & look
|
||||
pp - process currently shown command (no step)
|
||||
ss [steps] - process & step
|
||||
sl [steps] - process & step & look
|
||||
ll - look at full definition of current command
|
||||
rr - reload batch file (stay on current)
|
||||
rrr - reload batch file (start from first)
|
||||
hh - this help list
|
||||
|
||||
cc - continue processing to end, then quit.
|
||||
qq - quit (abort all remaining commands)
|
||||
|
||||
@abort - this is a safety command that always is available
|
||||
regardless of what cmdsets gets added to us during
|
||||
batch-command processing. It immediately shuts down
|
||||
the processor and returns us to the default cmdset.
|
||||
"""
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Defining the cmdsets for the interactive batchprocessor
|
||||
# mode (same for both processors)
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class BatchSafeCmdSet(CmdSet):
|
||||
"""
|
||||
The base cmdset for the batch processor.
|
||||
This sets a 'safe' @abort command that will
|
||||
always be available to get out of everything.
|
||||
"""
|
||||
key = "Batch_default"
|
||||
priority = 150 # override other cmdsets.
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Init the cmdset"
|
||||
self.add(CmdStateAbort())
|
||||
|
||||
|
||||
class BatchInteractiveCmdSet(CmdSet):
|
||||
"""
|
||||
The cmdset for the interactive batch processor mode.
|
||||
"""
|
||||
key = "Batch_interactive"
|
||||
priority = 104
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"init the cmdset"
|
||||
self.add(CmdStateAbort())
|
||||
self.add(CmdStateLL())
|
||||
self.add(CmdStatePP())
|
||||
self.add(CmdStateRR())
|
||||
self.add(CmdStateRRR())
|
||||
self.add(CmdStateNN())
|
||||
self.add(CmdStateNL())
|
||||
self.add(CmdStateBB())
|
||||
self.add(CmdStateBL())
|
||||
self.add(CmdStateSS())
|
||||
self.add(CmdStateSL())
|
||||
self.add(CmdStateCC())
|
||||
self.add(CmdStateJJ())
|
||||
self.add(CmdStateJL())
|
||||
self.add(CmdStateQQ())
|
||||
self.add(CmdStateHH())
|
||||
2374
lib/commands/default/building.py
Normal file
2374
lib/commands/default/building.py
Normal file
File diff suppressed because it is too large
Load diff
87
lib/commands/default/cmdset_character.py
Normal file
87
lib/commands/default/cmdset_character.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""
|
||||
This module ties together all the commands default Character objects have
|
||||
available (i.e. IC commands). Note that some commands, such as
|
||||
communication-commands are instead put on the player level, in the
|
||||
Player cmdset. Player commands remain available also to Characters.
|
||||
"""
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default import general, help, admin, system
|
||||
from src.commands.default import building
|
||||
from src.commands.default import batchprocess
|
||||
|
||||
|
||||
class CharacterCmdSet(CmdSet):
|
||||
"""
|
||||
Implements the default command set.
|
||||
"""
|
||||
key = "DefaultCharacter"
|
||||
priority = 0
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Populates the cmdset"
|
||||
|
||||
# The general commands
|
||||
self.add(general.CmdLook())
|
||||
self.add(general.CmdHome())
|
||||
self.add(general.CmdInventory())
|
||||
self.add(general.CmdPose())
|
||||
self.add(general.CmdNick())
|
||||
self.add(general.CmdGet())
|
||||
self.add(general.CmdDrop())
|
||||
self.add(general.CmdGive())
|
||||
self.add(general.CmdSay())
|
||||
self.add(general.CmdAccess())
|
||||
|
||||
# The help system
|
||||
self.add(help.CmdHelp())
|
||||
self.add(help.CmdSetHelp())
|
||||
|
||||
# System commands
|
||||
self.add(system.CmdPy())
|
||||
self.add(system.CmdScripts())
|
||||
self.add(system.CmdObjects())
|
||||
self.add(system.CmdPlayers())
|
||||
self.add(system.CmdService())
|
||||
self.add(system.CmdAbout())
|
||||
self.add(system.CmdTime())
|
||||
self.add(system.CmdServerLoad())
|
||||
#self.add(system.CmdPs())
|
||||
|
||||
# Admin commands
|
||||
self.add(admin.CmdBoot())
|
||||
self.add(admin.CmdBan())
|
||||
self.add(admin.CmdUnban())
|
||||
self.add(admin.CmdEmit())
|
||||
self.add(admin.CmdPerm())
|
||||
self.add(admin.CmdWall())
|
||||
|
||||
# Building and world manipulation
|
||||
self.add(building.CmdTeleport())
|
||||
self.add(building.CmdSetObjAlias())
|
||||
self.add(building.CmdListCmdSets())
|
||||
self.add(building.CmdWipe())
|
||||
self.add(building.CmdSetAttribute())
|
||||
self.add(building.CmdName())
|
||||
self.add(building.CmdDesc())
|
||||
self.add(building.CmdCpAttr())
|
||||
self.add(building.CmdMvAttr())
|
||||
self.add(building.CmdCopy())
|
||||
self.add(building.CmdFind())
|
||||
self.add(building.CmdOpen())
|
||||
self.add(building.CmdLink())
|
||||
self.add(building.CmdUnLink())
|
||||
self.add(building.CmdCreate())
|
||||
self.add(building.CmdDig())
|
||||
self.add(building.CmdTunnel())
|
||||
self.add(building.CmdDestroy())
|
||||
self.add(building.CmdExamine())
|
||||
self.add(building.CmdTypeclass())
|
||||
self.add(building.CmdLock())
|
||||
self.add(building.CmdScript())
|
||||
self.add(building.CmdSetHome())
|
||||
self.add(building.CmdTag())
|
||||
self.add(building.CmdSpawn())
|
||||
|
||||
# Batchprocessor commands
|
||||
self.add(batchprocess.CmdBatchCommands())
|
||||
self.add(batchprocess.CmdBatchCode())
|
||||
74
lib/commands/default/cmdset_player.py
Normal file
74
lib/commands/default/cmdset_player.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"""
|
||||
|
||||
This is the cmdset for Player (OOC) commands. These are
|
||||
stored on the Player object and should thus be able to handle getting
|
||||
a Player object as caller rather than a Character.
|
||||
|
||||
Note - in order for session-rerouting (in MULTISESSION_MODE=2) to
|
||||
function, all commands in this cmdset should use the self.msg()
|
||||
command method rather than caller.msg().
|
||||
"""
|
||||
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default import help, comms, admin, system
|
||||
from src.commands.default import building, player
|
||||
|
||||
|
||||
class PlayerCmdSet(CmdSet):
|
||||
"""
|
||||
Implements the player command set.
|
||||
"""
|
||||
|
||||
key = "DefaultPlayer"
|
||||
priority = -10
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Populates the cmdset"
|
||||
|
||||
# Player-specific commands
|
||||
self.add(player.CmdOOCLook())
|
||||
self.add(player.CmdIC())
|
||||
self.add(player.CmdOOC())
|
||||
self.add(player.CmdCharCreate())
|
||||
#self.add(player.CmdSessions())
|
||||
self.add(player.CmdWho())
|
||||
self.add(player.CmdEncoding())
|
||||
self.add(player.CmdQuit())
|
||||
self.add(player.CmdPassword())
|
||||
self.add(player.CmdColorTest())
|
||||
self.add(player.CmdQuell())
|
||||
|
||||
# testing
|
||||
self.add(building.CmdExamine())
|
||||
|
||||
# Help command
|
||||
self.add(help.CmdHelp())
|
||||
|
||||
# system commands
|
||||
self.add(system.CmdReload())
|
||||
self.add(system.CmdReset())
|
||||
self.add(system.CmdShutdown())
|
||||
self.add(system.CmdPy())
|
||||
|
||||
# Admin commands
|
||||
self.add(admin.CmdDelPlayer())
|
||||
self.add(admin.CmdNewPassword())
|
||||
|
||||
# Comm commands
|
||||
self.add(comms.CmdAddCom())
|
||||
self.add(comms.CmdDelCom())
|
||||
self.add(comms.CmdAllCom())
|
||||
self.add(comms.CmdChannels())
|
||||
self.add(comms.CmdCdestroy())
|
||||
self.add(comms.CmdChannelCreate())
|
||||
self.add(comms.CmdClock())
|
||||
self.add(comms.CmdCBoot())
|
||||
self.add(comms.CmdCemit())
|
||||
self.add(comms.CmdCWho())
|
||||
self.add(comms.CmdCdesc())
|
||||
self.add(comms.CmdPage())
|
||||
self.add(comms.CmdIRC2Chan())
|
||||
self.add(comms.CmdRSS2Chan())
|
||||
#self.add(comms.CmdIMC2Chan())
|
||||
#self.add(comms.CmdIMCInfo())
|
||||
#self.add(comms.CmdIMCTell())
|
||||
16
lib/commands/default/cmdset_session.py
Normal file
16
lib/commands/default/cmdset_session.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
This module stores session-level commands.
|
||||
"""
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default import player
|
||||
|
||||
class SessionCmdSet(CmdSet):
|
||||
"""
|
||||
Sets up the unlogged cmdset.
|
||||
"""
|
||||
key = "DefaultSession"
|
||||
priority = -20
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Populate the cmdset"
|
||||
self.add(player.CmdSessions())
|
||||
24
lib/commands/default/cmdset_unloggedin.py
Normal file
24
lib/commands/default/cmdset_unloggedin.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""
|
||||
This module describes the unlogged state of the default game.
|
||||
The setting STATE_UNLOGGED should be set to the python path
|
||||
of the state instance in this module.
|
||||
"""
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default import unloggedin
|
||||
|
||||
|
||||
class UnloggedinCmdSet(CmdSet):
|
||||
"""
|
||||
Sets up the unlogged cmdset.
|
||||
"""
|
||||
key = "DefaultUnloggedin"
|
||||
priority = 0
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Populate the cmdset"
|
||||
self.add(unloggedin.CmdUnconnectedConnect())
|
||||
self.add(unloggedin.CmdUnconnectedCreate())
|
||||
self.add(unloggedin.CmdUnconnectedQuit())
|
||||
self.add(unloggedin.CmdUnconnectedLook())
|
||||
self.add(unloggedin.CmdUnconnectedHelp())
|
||||
self.add(unloggedin.CmdUnconnectedEncoding())
|
||||
1169
lib/commands/default/comms.py
Normal file
1169
lib/commands/default/comms.py
Normal file
File diff suppressed because it is too large
Load diff
448
lib/commands/default/general.py
Normal file
448
lib/commands/default/general.py
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
"""
|
||||
General Character commands usually availabe to all characters
|
||||
"""
|
||||
from django.conf import settings
|
||||
from src.utils import utils, prettytable
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdHome", "CmdLook", "CmdNick",
|
||||
"CmdInventory", "CmdGet", "CmdDrop", "CmdGive",
|
||||
"CmdSay", "CmdPose", "CmdAccess")
|
||||
|
||||
AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
|
||||
|
||||
|
||||
class CmdHome(MuxCommand):
|
||||
"""
|
||||
move to your character's home location
|
||||
|
||||
Usage:
|
||||
home
|
||||
|
||||
Teleports you to your home location.
|
||||
"""
|
||||
|
||||
key = "home"
|
||||
locks = "cmd:perm(home) or perm(Builders)"
|
||||
|
||||
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.move_to(home)
|
||||
caller.msg("There's no place like home ...")
|
||||
|
||||
class CmdLook(MuxCommand):
|
||||
"""
|
||||
look at location or object
|
||||
|
||||
Usage:
|
||||
look
|
||||
look <obj>
|
||||
look *<player>
|
||||
|
||||
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
|
||||
args = self.args
|
||||
if args:
|
||||
# Use search to handle duplicate/nonexistant results.
|
||||
looking_at_obj = caller.search(args, use_nicks=True)
|
||||
if not looking_at_obj:
|
||||
return
|
||||
else:
|
||||
looking_at_obj = caller.location
|
||||
if not looking_at_obj:
|
||||
caller.msg("You have no location to look at!")
|
||||
return
|
||||
|
||||
if not hasattr(looking_at_obj, 'return_appearance'):
|
||||
# this is likely due to us having a player instead
|
||||
looking_at_obj = looking_at_obj.character
|
||||
if not looking_at_obj.access(caller, "view"):
|
||||
caller.msg("Could not find '%s'." % args)
|
||||
return
|
||||
# get object's appearance
|
||||
caller.msg(looking_at_obj.return_appearance(caller))
|
||||
# the object's at_desc() method.
|
||||
looking_at_obj.at_desc(looker=caller)
|
||||
|
||||
|
||||
class CmdNick(MuxCommand):
|
||||
"""
|
||||
define a personal alias/nick
|
||||
|
||||
Usage:
|
||||
nick[/switches] <nickname> = [<string>]
|
||||
alias ''
|
||||
|
||||
Switches:
|
||||
object - alias an object
|
||||
player - alias a player
|
||||
clearall - clear all your aliases
|
||||
list - show all defined aliases (also "nicks" works)
|
||||
|
||||
Examples:
|
||||
nick hi = say Hello, I'm Sarah!
|
||||
nick/object tom = the tall man
|
||||
|
||||
A 'nick' is a personal shortcut you create for your own use. When
|
||||
you enter the nick, the alternative string will be sent instead.
|
||||
The switches control in which situations the substitution will
|
||||
happen. The default is that it will happen when you enter a
|
||||
command. The 'object' and 'player' nick-types kick in only when
|
||||
you use commands that requires an object or player as a target -
|
||||
you can then use the nick to refer to them.
|
||||
|
||||
Note that no objects are actually renamed or changed by this
|
||||
command - the nick is only available to you. If you want to
|
||||
permanently add keywords to an object for everyone to use, you
|
||||
need build privileges and to use the @alias command.
|
||||
"""
|
||||
key = "nick"
|
||||
aliases = ["nickname", "nicks", "@nick", "alias"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Create the nickname"
|
||||
|
||||
caller = self.caller
|
||||
switches = self.switches
|
||||
nicks = caller.nicks.get(return_obj=True)
|
||||
|
||||
if 'list' in switches:
|
||||
table = prettytable.PrettyTable(["{wNickType",
|
||||
"{wNickname",
|
||||
"{wTranslates-to"])
|
||||
for nick in utils.make_iter(nicks):
|
||||
table.add_row([nick.db_category, nick.db_key, nick.db_strvalue])
|
||||
string = "{wDefined Nicks:{n\n%s" % table
|
||||
caller.msg(string)
|
||||
return
|
||||
if 'clearall' in switches:
|
||||
caller.nicks.clear()
|
||||
caller.msg("Cleared all aliases.")
|
||||
return
|
||||
if not self.args or not self.lhs:
|
||||
caller.msg("Usage: nick[/switches] nickname = [realname]")
|
||||
return
|
||||
nick = self.lhs
|
||||
real = self.rhs
|
||||
|
||||
if real == nick:
|
||||
caller.msg("No point in setting nick same as the string to replace...")
|
||||
return
|
||||
|
||||
# check so we have a suitable nick type
|
||||
if not any(True for switch in switches if switch in ("object", "player", "inputline")):
|
||||
switches = ["inputline"]
|
||||
string = ""
|
||||
for switch in switches:
|
||||
oldnick = caller.nicks.get(key=nick, category=switch)
|
||||
if not real:
|
||||
# removal of nick
|
||||
if oldnick:
|
||||
# clear the alias
|
||||
string += "\nNick '%s' (= '%s') was cleared." % (nick, oldnick)
|
||||
caller.nicks.delete(nick, category=switch)
|
||||
else:
|
||||
string += "\nNo nick '%s' found, so it could not be removed." % nick
|
||||
else:
|
||||
# creating new nick
|
||||
if oldnick:
|
||||
string += "\nNick %s changed from '%s' to '%s'." % (nick, oldnick, real)
|
||||
else:
|
||||
string += "\nNick set: '%s' = '%s'." % (nick, real)
|
||||
caller.nicks.add(nick, real, category=switch)
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class CmdInventory(MuxCommand):
|
||||
"""
|
||||
view inventory
|
||||
|
||||
Usage:
|
||||
inventory
|
||||
inv
|
||||
|
||||
Shows your inventory.
|
||||
"""
|
||||
key = "inventory"
|
||||
aliases = ["inv", "i"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"check inventory"
|
||||
items = self.caller.contents
|
||||
if not items:
|
||||
string = "You are not carrying anything."
|
||||
else:
|
||||
table = prettytable.PrettyTable(["name", "desc"])
|
||||
table.header = False
|
||||
table.border = False
|
||||
for item in items:
|
||||
table.add_row(["{C%s{n" % item.name, item.db.desc and item.db.desc or ""])
|
||||
string = "{wYou are carrying:\n%s" % table
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class CmdGet(MuxCommand):
|
||||
"""
|
||||
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()"
|
||||
|
||||
def func(self):
|
||||
"implements the command."
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if not self.args:
|
||||
caller.msg("Get what?")
|
||||
return
|
||||
#print "general/get:", caller, caller.location, self.args, caller.location.contents
|
||||
obj = caller.search(self.args, location=caller.location)
|
||||
if not obj:
|
||||
return
|
||||
if caller == obj:
|
||||
caller.msg("You can't get yourself.")
|
||||
return
|
||||
#print obj, obj.location, caller, caller==obj.location
|
||||
if caller == obj.location:
|
||||
caller.msg("You already hold that.")
|
||||
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
|
||||
|
||||
obj.move_to(caller, quiet=True)
|
||||
caller.msg("You pick up %s." % obj.name)
|
||||
caller.location.msg_contents("%s picks up %s." %
|
||||
(caller.name,
|
||||
obj.name),
|
||||
exclude=caller)
|
||||
# calling hook method
|
||||
obj.at_get(caller)
|
||||
|
||||
|
||||
class CmdDrop(MuxCommand):
|
||||
"""
|
||||
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()"
|
||||
|
||||
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
|
||||
results = caller.search(self.args, location=caller, quiet=True)
|
||||
|
||||
# now we send it into the error handler (this will output consistent
|
||||
# error messages if there are problems).
|
||||
obj = AT_SEARCH_RESULT(caller, self.args, results, False,
|
||||
nofound_string="You aren't carrying %s." % self.args,
|
||||
multimatch_string="You carry more than one %s:" % self.args)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
obj.move_to(caller.location, quiet=True)
|
||||
caller.msg("You drop %s." % (obj.name,))
|
||||
caller.location.msg_contents("%s drops %s." %
|
||||
(caller.name, obj.name),
|
||||
exclude=caller)
|
||||
# Call the object script's at_drop() method.
|
||||
obj.at_drop(caller)
|
||||
|
||||
|
||||
class CmdGive(MuxCommand):
|
||||
"""
|
||||
give away something to someone
|
||||
|
||||
Usage:
|
||||
give <inventory obj> = <target>
|
||||
|
||||
Gives an items from your inventory to another character,
|
||||
placing it in their inventory.
|
||||
"""
|
||||
key = "give"
|
||||
locks = "cmd:all()"
|
||||
|
||||
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)
|
||||
target = caller.search(self.rhs)
|
||||
if not (to_give and target):
|
||||
return
|
||||
if target == caller:
|
||||
caller.msg("You keep %s to yourself." % to_give.key)
|
||||
return
|
||||
if not to_give.location == caller:
|
||||
caller.msg("You are not holding %s." % to_give.key)
|
||||
return
|
||||
# give object
|
||||
caller.msg("You give %s to %s." % (to_give.key, target.key))
|
||||
to_give.move_to(target, quiet=True)
|
||||
target.msg("%s gives you %s." % (caller.key, to_give.key))
|
||||
|
||||
|
||||
class CmdSay(MuxCommand):
|
||||
"""
|
||||
speak as your character
|
||||
|
||||
Usage:
|
||||
say <message>
|
||||
|
||||
Talk to those in your current location.
|
||||
"""
|
||||
|
||||
key = "say"
|
||||
aliases = ['"', "'"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Run the say command"
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if not self.args:
|
||||
caller.msg("Say what?")
|
||||
return
|
||||
|
||||
speech = self.args
|
||||
|
||||
# calling the speech hook on the location
|
||||
speech = caller.location.at_say(caller, speech)
|
||||
|
||||
# Feedback for the object doing the talking.
|
||||
caller.msg('You say, "%s{n"' % speech)
|
||||
|
||||
# Build the string to emit to neighbors.
|
||||
emit_string = '%s says, "%s{n"' % (caller.name,
|
||||
speech)
|
||||
caller.location.msg_contents(emit_string,
|
||||
exclude=caller)
|
||||
|
||||
|
||||
class CmdPose(MuxCommand):
|
||||
"""
|
||||
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()"
|
||||
|
||||
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 = "%s%s" % (self.caller.name, self.args)
|
||||
self.caller.location.msg_contents(msg)
|
||||
|
||||
|
||||
class CmdAccess(MuxCommand):
|
||||
"""
|
||||
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()"
|
||||
|
||||
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)
|
||||
#hierarchy = [p.lower() for p in hierarchy_full]
|
||||
|
||||
if self.caller.player.is_superuser:
|
||||
cperms = "<Superuser>"
|
||||
pperms = "<Superuser>"
|
||||
else:
|
||||
cperms = ", ".join(caller.permissions.all())
|
||||
pperms = ", ".join(caller.player.permissions.all())
|
||||
|
||||
string += "\n{wYour access{n:"
|
||||
string += "\nCharacter {c%s{n: %s" % (caller.key, cperms)
|
||||
if hasattr(caller, 'player'):
|
||||
string += "\nPlayer {c%s{n: %s" % (caller.player.key, pperms)
|
||||
caller.msg(string)
|
||||
266
lib/commands/default/help.py
Normal file
266
lib/commands/default/help.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
"""
|
||||
The help command. The basic idea is that help texts for commands
|
||||
are best written by those that write the commands - the admins. So
|
||||
command-help is all auto-loaded and searched from the current command
|
||||
set. The normal, database-tied help system is used for collaborative
|
||||
creation of other help topics such as RP help or game-world aides.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from src.utils.utils import fill, dedent
|
||||
from src.commands.command import Command
|
||||
from src.help.models import HelpEntry
|
||||
from src.utils import create
|
||||
from src.utils.utils import string_suggestions
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdHelp", "CmdSetHelp")
|
||||
|
||||
|
||||
SEP = "{C" + "-" * 78 + "{n"
|
||||
|
||||
|
||||
def format_help_entry(title, help_text, aliases=None, suggested=None):
|
||||
"""
|
||||
This visually formats the help entry.
|
||||
"""
|
||||
string = SEP + "\n"
|
||||
if title:
|
||||
string += "{CHelp topic for {w%s{n" % title
|
||||
if aliases:
|
||||
string += " {C(aliases: {w%s{n{C){n" % (", ".join(aliases))
|
||||
if help_text:
|
||||
string += "\n%s" % dedent(help_text.rstrip())
|
||||
if suggested:
|
||||
string += "\n\n{CSuggested:{n "
|
||||
string += "{w%s{n" % fill(", ".join(suggested))
|
||||
string.strip()
|
||||
string += "\n" + SEP
|
||||
return string
|
||||
|
||||
|
||||
def format_help_list(hdict_cmds, hdict_db):
|
||||
"""
|
||||
Output a category-ordered list. The input are the
|
||||
pre-loaded help files for commands and database-helpfiles
|
||||
resectively.
|
||||
"""
|
||||
string = ""
|
||||
if hdict_cmds and any(hdict_cmds.values()):
|
||||
string += "\n" + SEP + "\n {CCommand help entries{n\n" + SEP
|
||||
for category in sorted(hdict_cmds.keys()):
|
||||
string += "\n {w%s{n:\n" % (str(category).title())
|
||||
string += "{G" + fill(", ".join(sorted(hdict_cmds[category]))) + "{n"
|
||||
if hdict_db and any(hdict_db.values()):
|
||||
string += "\n\n" + SEP + "\n\r {COther help entries{n\n" + SEP
|
||||
for category in sorted(hdict_db.keys()):
|
||||
string += "\n\r {w%s{n:\n" % (str(category).title())
|
||||
string += "{G" + fill(", ".join(sorted([str(topic) for topic in hdict_db[category]]))) + "{n"
|
||||
return string
|
||||
|
||||
|
||||
class CmdHelp(Command):
|
||||
"""
|
||||
view help or a list of topics
|
||||
|
||||
Usage:
|
||||
help <topic or command>
|
||||
help list
|
||||
help all
|
||||
|
||||
This will search for help on commands and other
|
||||
topics related to the game.
|
||||
"""
|
||||
key = "help"
|
||||
locks = "cmd:all()"
|
||||
|
||||
# this is a special cmdhandler flag that makes the cmdhandler also pack
|
||||
# the current cmdset with the call to self.func().
|
||||
return_cmdset = True
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
input is a string containing the command or topic to match.
|
||||
"""
|
||||
self.original_args = self.args.strip()
|
||||
self.args = self.args.strip().lower()
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Run the dynamic help entry creator.
|
||||
"""
|
||||
query, cmdset = self.args, self.cmdset
|
||||
caller = self.caller
|
||||
|
||||
suggestion_cutoff = 0.6
|
||||
suggestion_maxnum = 5
|
||||
|
||||
if not query:
|
||||
query = "all"
|
||||
|
||||
# removing doublets in cmdset, caused by cmdhandler
|
||||
# having to allow doublet commands to manage exits etc.
|
||||
cmdset.make_unique(caller)
|
||||
|
||||
# retrieve all available commands and database topics
|
||||
all_cmds = [cmd for cmd in cmdset if cmd.auto_help and cmd.access(caller)]
|
||||
all_topics = [topic for topic in HelpEntry.objects.all() if topic.access(caller, 'view', default=True)]
|
||||
all_categories = list(set([cmd.help_category.lower() for cmd in all_cmds] + [topic.help_category.lower() for topic in all_topics]))
|
||||
|
||||
if query in ("list", "all"):
|
||||
# we want to list all available help entries, grouped by category
|
||||
hdict_cmd = defaultdict(list)
|
||||
hdict_topic = defaultdict(list)
|
||||
# create the dictionaries {category:[topic, topic ...]} required by format_help_list
|
||||
[hdict_cmd[cmd.help_category].append(cmd.key) for cmd in all_cmds]
|
||||
[hdict_topic[topic.help_category].append(topic.key) for topic in all_topics]
|
||||
# report back
|
||||
self.msg(format_help_list(hdict_cmd, hdict_topic))
|
||||
return
|
||||
|
||||
# Try to access a particular command
|
||||
|
||||
# build vocabulary of suggestions and rate them by string similarity.
|
||||
vocabulary = [cmd.key for cmd in all_cmds if cmd] + [topic.key for topic in all_topics] + all_categories
|
||||
[vocabulary.extend(cmd.aliases) for cmd in all_cmds]
|
||||
suggestions = [sugg for sugg in string_suggestions(query, set(vocabulary), cutoff=suggestion_cutoff, maxnum=suggestion_maxnum)
|
||||
if sugg != query]
|
||||
if not suggestions:
|
||||
suggestions = [sugg for sugg in vocabulary if sugg != query and sugg.startswith(query)]
|
||||
|
||||
# try an exact command auto-help match
|
||||
match = [cmd for cmd in all_cmds if cmd == query]
|
||||
if len(match) == 1:
|
||||
self.msg(format_help_entry(match[0].key,
|
||||
match[0].__doc__,
|
||||
aliases=match[0].aliases,
|
||||
suggested=suggestions))
|
||||
return
|
||||
|
||||
# try an exact database help entry match
|
||||
match = list(HelpEntry.objects.find_topicmatch(query, exact=True))
|
||||
if len(match) == 1:
|
||||
self.msg(format_help_entry(match[0].key,
|
||||
match[0].entrytext,
|
||||
suggested=suggestions))
|
||||
return
|
||||
|
||||
# try to see if a category name was entered
|
||||
if query in all_categories:
|
||||
self.msg(format_help_list({query:[cmd.key for cmd in all_cmds if cmd.help_category==query]},
|
||||
{query:[topic.key for topic in all_topics if topic.help_category==query]}))
|
||||
return
|
||||
|
||||
# no exact matches found. Just give suggestions.
|
||||
self.msg(format_help_entry("", "No help entry found for '%s'" % query, None, suggested=suggestions))
|
||||
|
||||
|
||||
class CmdSetHelp(MuxCommand):
|
||||
"""
|
||||
edit the help database
|
||||
|
||||
Usage:
|
||||
@help[/switches] <topic>[,category[,locks]] = <text>
|
||||
|
||||
Switches:
|
||||
add - add or replace a new topic with text.
|
||||
append - add text to the end of topic with a newline between.
|
||||
merge - As append, but don't add a newline between the old
|
||||
text and the appended text.
|
||||
delete - remove help topic.
|
||||
force - (used with add) create help topic also if the topic
|
||||
already exists.
|
||||
|
||||
Examples:
|
||||
@sethelp/add throw = This throws something at ...
|
||||
@sethelp/append pickpocketing,Thievery = This steals ...
|
||||
@sethelp/append pickpocketing, ,attr(is_thief) = This steals ...
|
||||
|
||||
This command manipulates the help database. A help entry can be created,
|
||||
appended/merged to and deleted. If you don't assign a category, the
|
||||
"General" category will be used. If no lockstring is specified, default
|
||||
is to let everyone read the help file.
|
||||
|
||||
"""
|
||||
key = "@help"
|
||||
aliases = "@sethelp"
|
||||
locks = "cmd:perm(PlayerHelpers)"
|
||||
help_category = "Building"
|
||||
|
||||
def func(self):
|
||||
"Implement the function"
|
||||
|
||||
switches = self.switches
|
||||
lhslist = self.lhslist
|
||||
|
||||
if not self.args:
|
||||
self.msg("Usage: @sethelp/[add|del|append|merge] <topic>[,category[,locks,..] = <text>")
|
||||
return
|
||||
|
||||
topicstr = ""
|
||||
category = "General"
|
||||
lockstring = "view:all()"
|
||||
try:
|
||||
topicstr = lhslist[0]
|
||||
category = lhslist[1]
|
||||
lockstring = ",".join(lhslist[2:])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not topicstr:
|
||||
self.msg("You have to define a topic!")
|
||||
return
|
||||
# check if we have an old entry with the same name
|
||||
try:
|
||||
old_entry = HelpEntry.objects.get(db_key__iexact=topicstr)
|
||||
except Exception:
|
||||
old_entry = None
|
||||
|
||||
if 'append' in switches or "merge" in switches:
|
||||
# merge/append operations
|
||||
if not old_entry:
|
||||
self.msg("Could not find topic '%s'. You must give an exact name." % topicstr)
|
||||
return
|
||||
if not self.rhs:
|
||||
self.msg("You must supply text to append/merge.")
|
||||
return
|
||||
if 'merge' in switches:
|
||||
old_entry.entrytext += " " + self.rhs
|
||||
else:
|
||||
old_entry.entrytext += "\n\n%s" % self.rhs
|
||||
self.msg("Entry updated:\n%s" % old_entry.entrytext)
|
||||
return
|
||||
if 'delete' in switches or 'del' in switches:
|
||||
# delete the help entry
|
||||
if not old_entry:
|
||||
self.msg("Could not find topic '%s'" % topicstr)
|
||||
return
|
||||
old_entry.delete()
|
||||
self.msg("Deleted help entry '%s'." % topicstr)
|
||||
return
|
||||
|
||||
# at this point it means we want to add a new help entry.
|
||||
if not self.rhs:
|
||||
self.msg("You must supply a help text to add.")
|
||||
return
|
||||
if old_entry:
|
||||
if 'for' in switches or 'force' in switches:
|
||||
# overwrite old entry
|
||||
old_entry.key = topicstr
|
||||
old_entry.entrytext = self.rhs
|
||||
old_entry.help_category = category
|
||||
old_entry.locks.clear()
|
||||
old_entry.locks.add(lockstring)
|
||||
old_entry.save()
|
||||
self.msg("Overwrote the old topic '%s' with a new one." % topicstr)
|
||||
else:
|
||||
self.msg("Topic '%s' already exists. Use /force to overwrite or /append or /merge to add text to it." % topicstr)
|
||||
else:
|
||||
# no old entry. Create a new one.
|
||||
new_entry = create.create_help_entry(topicstr,
|
||||
self.rhs, category, lockstring)
|
||||
if new_entry:
|
||||
self.msg("Topic '%s' was successfully created." % topicstr)
|
||||
else:
|
||||
self.msg("Error when creating topic '%s'! Contact an admin." % topicstr)
|
||||
194
lib/commands/default/muxcommand.py
Normal file
194
lib/commands/default/muxcommand.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"""
|
||||
The command template for the default MUX-style command set. There
|
||||
is also an Player/OOC version that makes sure caller is a Player object.
|
||||
"""
|
||||
|
||||
from src.utils import utils
|
||||
from src.commands.command import Command
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("MuxCommand", "MuxPlayerCommand")
|
||||
|
||||
class MuxCommand(Command):
|
||||
"""
|
||||
This sets up the basis for a MUX command. The idea
|
||||
is tkhat most other Mux-related commands should just
|
||||
inherit from this and don't have to implement much
|
||||
parsing of their own unless they do something particularly
|
||||
advanced.
|
||||
|
||||
Note that the class's __doc__ string (this text) is
|
||||
used by Evennia to create the automatic help entry for
|
||||
the command, so make sure to document consistently here.
|
||||
"""
|
||||
def has_perm(self, srcobj):
|
||||
"""
|
||||
This is called by the cmdhandler to determine
|
||||
if srcobj is allowed to execute this command.
|
||||
We just show it here for completeness - we
|
||||
are satisfied using the default check in Command.
|
||||
"""
|
||||
return super(MuxCommand, self).has_perm(srcobj)
|
||||
|
||||
def at_pre_cmd(self):
|
||||
"""
|
||||
This hook is called before self.parse() on all commands
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_post_cmd(self):
|
||||
"""
|
||||
This hook is called after the command has finished executing
|
||||
(after self.func()).
|
||||
"""
|
||||
pass
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
This method is called by the cmdhandler once the command name
|
||||
has been identified. It creates a new set of member variables
|
||||
that can be later accessed from self.func() (see below)
|
||||
|
||||
The following variables are available for our use when entering this
|
||||
method (from the command definition, and assigned on the fly by the
|
||||
cmdhandler):
|
||||
self.key - the name of this command ('look')
|
||||
self.aliases - the aliases of this cmd ('l')
|
||||
self.permissions - permission string for this command
|
||||
self.help_category - overall category of command
|
||||
|
||||
self.caller - the object calling this command
|
||||
self.cmdstring - the actual command name used to call this
|
||||
(this allows you to know which alias was used,
|
||||
for example)
|
||||
self.args - the raw input; everything following self.cmdstring.
|
||||
self.cmdset - the cmdset from which this command was picked. Not
|
||||
often used (useful for commands like 'help' or to
|
||||
list all available commands etc)
|
||||
self.obj - the object on which this command was defined. It is often
|
||||
the same as self.caller.
|
||||
|
||||
A MUX command has the following possible syntax:
|
||||
|
||||
name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]]
|
||||
|
||||
The 'name[ with several words]' part is already dealt with by the
|
||||
cmdhandler at this point, and stored in self.cmdname (we don't use
|
||||
it here). The rest of the command is stored in self.args, which can
|
||||
start with the switch indicator /.
|
||||
|
||||
This parser breaks self.args into its constituents and stores them in the
|
||||
following variables:
|
||||
self.switches = [list of /switches (without the /)]
|
||||
self.raw = This is the raw argument input, including switches
|
||||
self.args = This is re-defined to be everything *except* the switches
|
||||
self.lhs = Everything to the left of = (lhs:'left-hand side'). If
|
||||
no = is found, this is identical to self.args.
|
||||
self.rhs: Everything to the right of = (rhs:'right-hand side').
|
||||
If no '=' is found, this is None.
|
||||
self.lhslist - [self.lhs split into a list by comma]
|
||||
self.rhslist - [list of self.rhs split into a list by comma]
|
||||
self.arglist = [list of space-separated args (stripped, including '=' if it exists)]
|
||||
|
||||
All args and list members are stripped of excess whitespace around the
|
||||
strings, but case is preserved.
|
||||
"""
|
||||
raw = self.args
|
||||
args = raw.strip()
|
||||
|
||||
# split out switches
|
||||
switches = []
|
||||
if args and len(args) > 1 and args[0] == "/":
|
||||
# we have a switch, or a set of switches. These end with a space.
|
||||
#print "'%s'" % args
|
||||
switches = args[1:].split(None, 1)
|
||||
if len(switches) > 1:
|
||||
switches, args = switches
|
||||
switches = switches.split('/')
|
||||
else:
|
||||
args = ""
|
||||
switches = switches[0].split('/')
|
||||
arglist = [arg.strip() for arg in args.split()]
|
||||
|
||||
# check for arg1, arg2, ... = argA, argB, ... constructs
|
||||
lhs, rhs = args, None
|
||||
lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
|
||||
if args and '=' in args:
|
||||
lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
|
||||
lhslist = [arg.strip() for arg in lhs.split(',')]
|
||||
rhslist = [arg.strip() for arg in rhs.split(',')]
|
||||
|
||||
# save to object properties:
|
||||
self.raw = raw
|
||||
self.switches = switches
|
||||
self.args = args.strip()
|
||||
self.arglist = arglist
|
||||
self.lhs = lhs
|
||||
self.lhslist = lhslist
|
||||
self.rhs = rhs
|
||||
self.rhslist = rhslist
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is the hook function that actually does all the work. It is called
|
||||
by the cmdhandler right after self.parser() finishes, and so has access
|
||||
to all the variables defined therein.
|
||||
"""
|
||||
# a simple test command to show the available properties
|
||||
string = "-" * 50
|
||||
string += "\n{w%s{n - Command variables from evennia:\n" % self.key
|
||||
string += "-" * 50
|
||||
string += "\nname of cmd (self.key): {w%s{n\n" % self.key
|
||||
string += "cmd aliases (self.aliases): {w%s{n\n" % self.aliases
|
||||
string += "cmd locks (self.locks): {w%s{n\n" % self.locks
|
||||
string += "help category (self.help_category): {w%s{n\n" % self.help_category
|
||||
string += "object calling (self.caller): {w%s{n\n" % self.caller
|
||||
string += "object storing cmdset (self.obj): {w%s{n\n" % self.obj
|
||||
string += "command string given (self.cmdstring): {w%s{n\n" % self.cmdstring
|
||||
# show cmdset.key instead of cmdset to shorten output
|
||||
string += utils.fill("current cmdset (self.cmdset): {w%s{n\n" % self.cmdset)
|
||||
|
||||
|
||||
string += "\n" + "-" * 50
|
||||
string += "\nVariables from MuxCommand baseclass\n"
|
||||
string += "-" * 50
|
||||
string += "\nraw argument (self.raw): {w%s{n \n" % self.raw
|
||||
string += "cmd args (self.args): {w%s{n\n" % self.args
|
||||
string += "cmd switches (self.switches): {w%s{n\n" % self.switches
|
||||
string += "space-separated arg list (self.arglist): {w%s{n\n" % self.arglist
|
||||
string += "lhs, left-hand side of '=' (self.lhs): {w%s{n\n" % self.lhs
|
||||
string += "lhs, comma separated (self.lhslist): {w%s{n\n" % self.lhslist
|
||||
string += "rhs, right-hand side of '=' (self.rhs): {w%s{n\n" % self.rhs
|
||||
string += "rhs, comma separated (self.rhslist): {w%s{n\n" % self.rhslist
|
||||
string += "-" * 50
|
||||
self.caller.msg(string)
|
||||
|
||||
class MuxPlayerCommand(MuxCommand):
|
||||
"""
|
||||
This is an on-Player version of the MuxCommand. Since these commands sit
|
||||
on Players rather than on Characters/Objects, we need to check
|
||||
this in the parser.
|
||||
|
||||
Player commands are available also when puppeting a Character, it's
|
||||
just that they are applied with a lower priority and are always
|
||||
available, also when disconnected from a character (i.e. "ooc").
|
||||
|
||||
This class makes sure that caller is always a Player object, while
|
||||
creating a new property "character" that is set only if a
|
||||
character is actually attached to this Player and Session.
|
||||
"""
|
||||
def parse(self):
|
||||
"""
|
||||
We run the parent parser as usual, then fix the result
|
||||
"""
|
||||
super(MuxPlayerCommand, self).parse()
|
||||
|
||||
if utils.inherits_from(self.caller, "src.objects.objects.DefaultObject"):
|
||||
# caller is an Object/Character
|
||||
self.character = self.caller
|
||||
self.caller = self.caller.player
|
||||
elif utils.inherits_from(self.caller, "src.players.players.DefaultPlayer"):
|
||||
# caller was already a Player
|
||||
self.character = self.caller.get_puppet(self.sessid)
|
||||
else:
|
||||
self.character = None
|
||||
700
lib/commands/default/player.py
Normal file
700
lib/commands/default/player.py
Normal file
|
|
@ -0,0 +1,700 @@
|
|||
"""
|
||||
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.
|
||||
|
||||
"""
|
||||
import time
|
||||
from django.conf import settings
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.commands.default.muxcommand import MuxPlayerCommand
|
||||
from src.utils import utils, create, search, prettytable
|
||||
|
||||
from settings import MAX_NR_CHARACTERS, MULTISESSION_MODE
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit",
|
||||
"CmdCharCreate", "CmdEncoding", "CmdSessions", "CmdWho",
|
||||
"CmdColorTest", "CmdQuell")
|
||||
|
||||
# force max nr chars to 1 if mode is 0 or 1
|
||||
MAX_NR_CHARACTERS = MULTISESSION_MODE < 2 and 1 or MAX_NR_CHARACTERS
|
||||
BASE_PLAYER_TYPECLASS = settings.BASE_PLAYER_TYPECLASS
|
||||
|
||||
PERMISSION_HIERARCHY = settings.PERMISSION_HIERARCHY
|
||||
PERMISSION_HIERARCHY_LOWER = [perm.lower() for perm in PERMISSION_HIERARCHY]
|
||||
|
||||
# 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
|
||||
|
||||
class CmdOOCLook(MuxPlayerCommand):
|
||||
"""
|
||||
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 look_target(self):
|
||||
"Hook method for when an argument is given."
|
||||
player = self.player
|
||||
key = self.args.lower()
|
||||
chars = dict((utils.to_str(char.key.lower()), char)
|
||||
for char in player.db._playable_characters)
|
||||
looktarget = chars.get(key)
|
||||
if looktarget:
|
||||
self.msg(looktarget.return_appearance(player))
|
||||
else:
|
||||
self.msg("No such character.")
|
||||
return
|
||||
|
||||
def no_look_target(self):
|
||||
"Hook method for default look without a specified target"
|
||||
# caller is always a player at this point.
|
||||
player = self.player
|
||||
sessid = self.sessid
|
||||
# get all our characters and sessions
|
||||
characters = player.db._playable_characters
|
||||
if None in characters:
|
||||
# clean up list if character object was deleted in between
|
||||
characters = [character for character in characters if character]
|
||||
player.db._playable_characters = characters
|
||||
|
||||
sessions = player.get_all_sessions()
|
||||
is_su = player.is_superuser
|
||||
|
||||
# text shown when looking in the ooc area
|
||||
string = "Account {g%s{n (you are Out-of-Character)" % (player.key)
|
||||
|
||||
nsess = len(sessions)
|
||||
string += nsess == 1 and "\n\n{wConnected session:{n" or "\n\n{wConnected sessions (%i):{n" % nsess
|
||||
for isess, sess in enumerate(sessions):
|
||||
csessid = sess.sessid
|
||||
addr = "%s (%s)" % (sess.protocol_key, isinstance(sess.address, tuple) and str(sess.address[0]) or str(sess.address))
|
||||
string += "\n %s %s" % (sessid == csessid and "{w%s{n" % (isess + 1) or (isess + 1), addr)
|
||||
string += "\n\n {whelp{n - more commands"
|
||||
string += "\n {wooc <Text>{n - talk on public channel"
|
||||
|
||||
if is_su or len(characters) < MAX_NR_CHARACTERS:
|
||||
if not characters:
|
||||
string += "\n\n You don't have any characters yet. See {whelp @charcreate{n for creating one."
|
||||
else:
|
||||
string += "\n {w@charcreate <name> [=description]{n - create new character"
|
||||
|
||||
if characters:
|
||||
string_s_ending = len(characters) > 1 and "s" or ""
|
||||
string += "\n {w@ic <character>{n - enter the game ({w@ooc{n to get back here)"
|
||||
if is_su:
|
||||
string += "\n\nAvailable character%s (%i/unlimited):" % (string_s_ending, len(characters))
|
||||
else:
|
||||
string += "\n\nAvailable character%s%s:" % (string_s_ending,
|
||||
MAX_NR_CHARACTERS > 1 and " (%i/%i)" % (len(characters), MAX_NR_CHARACTERS) or "")
|
||||
|
||||
for char in characters:
|
||||
csessid = char.sessid.get()
|
||||
if csessid:
|
||||
# character is already puppeted
|
||||
sessi = player.get_session(csessid)
|
||||
for sess in utils.make_iter(sessi):
|
||||
sid = sess in sessions and sessions.index(sess) + 1
|
||||
if sess and sid:
|
||||
string += "\n - {G%s{n [%s] (played by you in session %i)" % (char.key, ", ".join(char.permissions.all()), sid)
|
||||
else:
|
||||
string += "\n - {R%s{n [%s] (played by someone else)" % (char.key, ", ".join(char.permissions.all()))
|
||||
else:
|
||||
# character is "free to puppet"
|
||||
string += "\n - %s [%s]" % (char.key, ", ".join(char.permissions.all()))
|
||||
string = ("-" * 68) + "\n" + string + "\n" + ("-" * 68)
|
||||
self.msg(string)
|
||||
|
||||
def func(self):
|
||||
"implement the ooc look command"
|
||||
if MULTISESSION_MODE < 2:
|
||||
# only one character allowed
|
||||
string = "You are out-of-character (OOC).\nUse {w@ic{n to get back into the game."
|
||||
self.msg(string)
|
||||
return
|
||||
if utils.inherits_from(self.caller, "src.objects.objects.Object"):
|
||||
# An object of some type is calling. Use default look instead.
|
||||
super(CmdOOCLook, self).func()
|
||||
elif self.args:
|
||||
self.look_target()
|
||||
else:
|
||||
self.no_look_target()
|
||||
|
||||
|
||||
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
|
||||
if not player.is_superuser and \
|
||||
(player.db._playable_characters and
|
||||
len(player.db._playable_characters) >= MAX_NR_CHARACTERS):
|
||||
self.msg("You may only create a maximum of %i characters." % MAX_NR_CHARACTERS)
|
||||
return
|
||||
# create the character
|
||||
from src.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"
|
||||
# lockmust 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
|
||||
sessid = self.sessid
|
||||
|
||||
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
|
||||
# permission checks
|
||||
if player.get_puppet(sessid) == new_character:
|
||||
self.msg("{RYou already act as {c%s{n." % new_character.name)
|
||||
return
|
||||
if new_character.player:
|
||||
# may not puppet an already puppeted character
|
||||
if new_character.sessid.count() and new_character.player == player:
|
||||
# as a safeguard we allow "taking over" chars from your own sessions.
|
||||
if MULTISESSION_MODE in (1, 3):
|
||||
txt = "{c%s{n{G is now shared from another of your sessions.{n"
|
||||
txt2 = "Sharing {c%s{n with another of your sessions."
|
||||
else:
|
||||
txt = "{c%s{n{R is now acted from another of your sessions.{n"
|
||||
txt2 = "Taking over {c%s{n from another of your sessions."
|
||||
player.unpuppet_object(new_character.sessid.get())
|
||||
player.msg(txt % (new_character.name), sessid=new_character.sessid.get())
|
||||
self.msg(txt2 % new_character.name)
|
||||
elif new_character.player != player and new_character.player.is_connected:
|
||||
self.msg("{c%s{r is already acted by another player.{n" % new_character.name)
|
||||
return
|
||||
if not new_character.access(player, "puppet"):
|
||||
# main acccess check
|
||||
self.msg("{rYou may not become %s.{n" % new_character.name)
|
||||
return
|
||||
if player.puppet_object(sessid, new_character):
|
||||
player.db._last_puppet = new_character
|
||||
else:
|
||||
self.msg("{rYou cannot become {C%s{n." % new_character.name)
|
||||
|
||||
|
||||
class CmdOOC(MuxPlayerCommand):
|
||||
"""
|
||||
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"
|
||||
# lock must be all(), for different puppeted objects to access it.
|
||||
locks = "cmd:pperm(Players)"
|
||||
aliases = "@unpuppet"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"Implement function"
|
||||
|
||||
player = self.player
|
||||
sessid = self.sessid
|
||||
|
||||
old_char = player.get_puppet(sessid)
|
||||
if not old_char:
|
||||
string = "You are already OOC."
|
||||
self.msg(string)
|
||||
return
|
||||
|
||||
player.db._last_puppet = old_char
|
||||
|
||||
# disconnect
|
||||
if player.unpuppet_object(sessid):
|
||||
self.msg("\n{GYou go OOC.{n\n")
|
||||
player.execute_cmd("look", sessid=sessid)
|
||||
else:
|
||||
raise RuntimeError("Could not unpuppet!")
|
||||
|
||||
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.get_all_sessions()
|
||||
|
||||
table = prettytable.PrettyTable(["{wsessid",
|
||||
"{wprotocol",
|
||||
"{whost",
|
||||
"{wpuppet/character",
|
||||
"{wlocation"])
|
||||
for sess in sorted(sessions, key=lambda x: x.sessid):
|
||||
sessid = sess.sessid
|
||||
char = player.get_puppet(sessid)
|
||||
table.add_row([str(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 CmdEncoding(MuxPlayerCommand):
|
||||
"""
|
||||
set which text encoding to use
|
||||
|
||||
Usage:
|
||||
@encoding/switches [<encoding>]
|
||||
|
||||
Switches:
|
||||
clear - clear your custom encoding
|
||||
|
||||
|
||||
This sets the text encoding for communicating with Evennia. This is mostly
|
||||
an issue only if you want to use non-ASCII characters (i.e. letters/symbols
|
||||
not found in English). If you see that your characters look strange (or you
|
||||
get encoding errors), you should use this command to set the server
|
||||
encoding to be the same used in your client program.
|
||||
|
||||
Common encodings are utf-8 (default), latin-1, ISO-8859-1 etc.
|
||||
|
||||
If you don't submit an encoding, the current encoding will be displayed
|
||||
instead.
|
||||
"""
|
||||
|
||||
key = "@encoding"
|
||||
aliases = "@encode"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Sets the encoding.
|
||||
"""
|
||||
|
||||
if self.session is None:
|
||||
return
|
||||
|
||||
if 'clear' in self.switches:
|
||||
# remove customization
|
||||
old_encoding = self.session.encoding
|
||||
if old_encoding:
|
||||
string = "Your custom text encoding ('%s') was cleared." % old_encoding
|
||||
else:
|
||||
string = "No custom encoding was set."
|
||||
self.session.encoding = "utf-8"
|
||||
elif not self.args:
|
||||
# just list the encodings supported
|
||||
pencoding = self.session.encoding
|
||||
string = ""
|
||||
if pencoding:
|
||||
string += "Default encoding: {g%s{n (change with {w@encoding <encoding>{n)" % pencoding
|
||||
encodings = settings.ENCODINGS
|
||||
if encodings:
|
||||
string += "\nServer's alternative encodings (tested in this order):\n {g%s{n" % ", ".join(encodings)
|
||||
if not string:
|
||||
string = "No encodings found."
|
||||
else:
|
||||
# change encoding
|
||||
old_encoding = self.session.encoding
|
||||
encoding = self.args
|
||||
self.session.encoding = encoding
|
||||
string = "Your custom text encoding was changed from '%s' to '%s'." % (old_encoding, encoding)
|
||||
self.msg(string.strip())
|
||||
|
||||
|
||||
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"
|
||||
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.", sessid=self.sessid)
|
||||
for session in player.get_all_sessions():
|
||||
player.disconnect_session_from_player(session.sessid)
|
||||
else:
|
||||
nsess = len(player.get_all_sessions())
|
||||
if nsess == 2:
|
||||
player.msg("{RQuitting{n. One session is still connected.", sessid=self.sessid)
|
||||
elif nsess > 2:
|
||||
player.msg("{RQuitting{n. %i session are still connected." % (nsess-1), sessid=self.sessid)
|
||||
else:
|
||||
# we are quitting the last available session
|
||||
player.msg("{RQuitting{n. Hope to see you soon again.", sessid=self.sessid)
|
||||
player.disconnect_session_from_player(self.sessid)
|
||||
|
||||
|
||||
|
||||
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 src.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[6:14]]
|
||||
col2 = ["%s%s{n" % (code, code.replace("{", "{{")) for code, _ in ap.ext_ansi_map[14:22]]
|
||||
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))])
|
||||
#hi = "%ch"
|
||||
#col2 = ["%s%s{n" % (code, code.replace("%", "%%")) for code, _ in ap.mux_ansi_map[6:]]
|
||||
#col3 = ["%s%s{n" % (hi + code, (hi + code).replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]]
|
||||
table = utils.format_table([col1, col2, col3])
|
||||
string = "ANSI colors:"
|
||||
for row in table:
|
||||
string += "\n " + " ".join(row)
|
||||
#print string
|
||||
self.msg(string)
|
||||
self.msg("{{X : black. {{/ : return, {{- : tab, {{_ : space, {{* : invert")
|
||||
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" % (ir, ig, ib,
|
||||
5 - ir, 5 - ig, 5 - 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.sessid:
|
||||
char = player.get_puppet(self.sessid)
|
||||
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 = player.get_puppet(self.sessid)
|
||||
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)
|
||||
|
||||
173
lib/commands/default/syscommands.py
Normal file
173
lib/commands/default/syscommands.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
"""
|
||||
System commands
|
||||
|
||||
These are the default commands called by the system commandhandler
|
||||
when various exceptions occur. If one of these commands are not
|
||||
implemented and part of the current cmdset, the engine falls back
|
||||
to a default solution instead.
|
||||
|
||||
Some system commands are shown in this module
|
||||
as a REFERENCE only (they are not all added to Evennia's
|
||||
default cmdset since they don't currently do anything differently from the
|
||||
default backup systems hard-wired in the engine).
|
||||
|
||||
Overloading these commands in a cmdset can be used to create
|
||||
interesting effects. An example is using the NoMatch system command
|
||||
to implement a line-editor where you don't have to start each
|
||||
line with a command (if there is no match to a known command,
|
||||
the line is just added to the editor buffer).
|
||||
"""
|
||||
|
||||
from src.comms.models import ChannelDB
|
||||
from src.utils import create
|
||||
|
||||
# The command keys the engine is calling
|
||||
# (the actual names all start with __)
|
||||
from src.commands.cmdhandler import CMD_NOINPUT
|
||||
from src.commands.cmdhandler import CMD_NOMATCH
|
||||
from src.commands.cmdhandler import CMD_MULTIMATCH
|
||||
from src.commands.cmdhandler import CMD_CHANNEL
|
||||
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
|
||||
# Command called when there is no input at line
|
||||
# (i.e. an lone return key)
|
||||
|
||||
|
||||
class SystemNoInput(MuxCommand):
|
||||
"""
|
||||
This is called when there is no input given
|
||||
"""
|
||||
key = CMD_NOINPUT
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Do nothing."
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Command called when there was no match to the
|
||||
# command name
|
||||
#
|
||||
class SystemNoMatch(MuxCommand):
|
||||
"""
|
||||
No command was found matching the given input.
|
||||
"""
|
||||
key = CMD_NOMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is given the failed raw string as input.
|
||||
"""
|
||||
self.caller.msg("Huh?")
|
||||
|
||||
|
||||
#
|
||||
# Command called when there were mulitple matches to the command.
|
||||
#
|
||||
class SystemMultimatch(MuxCommand):
|
||||
"""
|
||||
Multiple command matches.
|
||||
|
||||
The cmdhandler adds a special attribute 'matches' to this
|
||||
system command.
|
||||
|
||||
matches = [(candidate, cmd) , (candidate, cmd), ...],
|
||||
|
||||
where candidate is an instance of src.commands.cmdparser.CommandCandidate
|
||||
and cmd is an an instantiated Command object matching the candidate.
|
||||
"""
|
||||
key = CMD_MULTIMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def format_multimatches(self, caller, matches):
|
||||
"""
|
||||
Format multiple command matches to a useful error.
|
||||
|
||||
This is copied directly from the default method in
|
||||
src.commands.cmdhandler.
|
||||
|
||||
"""
|
||||
string = "There were multiple matches:"
|
||||
for num, match in enumerate(matches):
|
||||
# each match is a tuple (candidate, cmd)
|
||||
candidate, cmd = match
|
||||
|
||||
is_channel = hasattr(cmd, "is_channel") and cmd.is_channel
|
||||
if is_channel:
|
||||
is_channel = " (channel)"
|
||||
else:
|
||||
is_channel = ""
|
||||
is_exit = hasattr(cmd, "is_exit") and cmd.is_exit
|
||||
if is_exit and cmd.destination:
|
||||
is_exit = " (exit to %s)" % cmd.destination
|
||||
else:
|
||||
is_exit = ""
|
||||
|
||||
id1 = ""
|
||||
id2 = ""
|
||||
if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller):
|
||||
# the command is defined on some other object
|
||||
id1 = "%s-" % cmd.obj.name
|
||||
id2 = " (%s-%s)" % (num + 1, candidate.cmdname)
|
||||
else:
|
||||
id1 = "%s-" % (num + 1)
|
||||
id2 = ""
|
||||
string += "\n %s%s%s%s%s" % (id1, candidate.cmdname, id2, is_channel, is_exit)
|
||||
return string
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
argument to cmd is a comma-separated string of
|
||||
all the clashing matches.
|
||||
"""
|
||||
string = self.format_multimatches(self.caller, self.matches)
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
# Command called when the command given at the command line
|
||||
# was identified as a channel name, like there existing a
|
||||
# channel named 'ooc' and the user wrote
|
||||
# > ooc Hello!
|
||||
|
||||
class SystemSendToChannel(MuxCommand):
|
||||
"""
|
||||
This is a special command that the cmdhandler calls
|
||||
when it detects that the command given matches
|
||||
an existing Channel object key (or alias).
|
||||
"""
|
||||
|
||||
key = CMD_CHANNEL
|
||||
locks = "cmd:all()"
|
||||
|
||||
def parse(self):
|
||||
channelname, msg = self.args.split(':', 1)
|
||||
self.args = channelname.strip(), msg.strip()
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Create a new message and send it to channel, using
|
||||
the already formatted input.
|
||||
"""
|
||||
caller = self.caller
|
||||
channelkey, msg = self.args
|
||||
if not msg:
|
||||
caller.msg("Say what?")
|
||||
return
|
||||
channel = ChannelDB.objects.get_channel(channelkey)
|
||||
if not channel:
|
||||
caller.msg("Channel '%s' not found." % channelkey)
|
||||
return
|
||||
if not channel.has_connection(caller):
|
||||
string = "You are not connected to channel '%s'."
|
||||
caller.msg(string % channelkey)
|
||||
return
|
||||
if not channel.access(caller, 'send'):
|
||||
string = "You are not permitted to send to channel '%s'."
|
||||
caller.msg(string % channelkey)
|
||||
return
|
||||
msg = "[%s] %s: %s" % (channel.key, caller.name, msg)
|
||||
msgobj = create.create_message(caller, msg, channels=[channel])
|
||||
channel.msg(msgobj)
|
||||
725
lib/commands/default/system.py
Normal file
725
lib/commands/default/system.py
Normal file
|
|
@ -0,0 +1,725 @@
|
|||
"""
|
||||
|
||||
System commands
|
||||
|
||||
"""
|
||||
|
||||
import traceback
|
||||
import os
|
||||
import datetime
|
||||
import sys
|
||||
import django
|
||||
import twisted
|
||||
from time import time as timemeasure
|
||||
|
||||
from django.conf import settings
|
||||
#from src.server.caches import get_cache_sizes
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.objects.models import ObjectDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.utils import logger, utils, gametime, create, is_pypy, prettytable
|
||||
from src.utils.evtable import EvTable
|
||||
from src.utils.utils import crop
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
|
||||
# delayed imports
|
||||
_resource = None
|
||||
_idmapper = None
|
||||
_attribute_cache = None
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdReload", "CmdReset", "CmdShutdown", "CmdPy",
|
||||
"CmdScripts", "CmdObjects", "CmdService", "CmdAbout",
|
||||
"CmdTime", "CmdServerLoad")
|
||||
|
||||
|
||||
class CmdReload(MuxCommand):
|
||||
"""
|
||||
reload the server
|
||||
|
||||
Usage:
|
||||
@reload [reason]
|
||||
|
||||
This restarts the server. The Portal is not
|
||||
affected. Non-persistent scripts will survive a @reload (use
|
||||
@reset to purge) and at_reload() hooks will be called.
|
||||
"""
|
||||
key = "@reload"
|
||||
locks = "cmd:perm(reload) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Reload the system.
|
||||
"""
|
||||
reason = ""
|
||||
if self.args:
|
||||
reason = "(Reason: %s) " % self.args.rstrip(".")
|
||||
SESSIONS.announce_all(" Server restarting %s..." % reason)
|
||||
SESSIONS.server.shutdown(mode='reload')
|
||||
|
||||
|
||||
class CmdReset(MuxCommand):
|
||||
"""
|
||||
reset and reboot the server
|
||||
|
||||
Usage:
|
||||
@reset
|
||||
|
||||
A cold reboot. This works like a mixture of @reload and @shutdown,
|
||||
- all shutdown hooks will be called and non-persistent scrips will
|
||||
be purged. But the Portal will not be affected and the server will
|
||||
automatically restart again.
|
||||
"""
|
||||
key = "@reset"
|
||||
aliases = ['@reboot']
|
||||
locks = "cmd:perm(reload) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Reload the system.
|
||||
"""
|
||||
SESSIONS.announce_all(" Server resetting/restarting ...")
|
||||
SESSIONS.server.shutdown(mode='reset')
|
||||
|
||||
|
||||
class CmdShutdown(MuxCommand):
|
||||
|
||||
"""
|
||||
stop the server completely
|
||||
|
||||
Usage:
|
||||
@shutdown [announcement]
|
||||
|
||||
Gracefully shut down both Server and Portal.
|
||||
"""
|
||||
key = "@shutdown"
|
||||
locks = "cmd:perm(shutdown) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Define function"
|
||||
try:
|
||||
# Only allow shutdown if caller has session
|
||||
self.caller.sessions[0]
|
||||
except Exception:
|
||||
return
|
||||
self.msg('Shutting down server ...')
|
||||
announcement = "\nServer is being SHUT DOWN!\n"
|
||||
if self.args:
|
||||
announcement += "%s\n" % self.args
|
||||
logger.log_infomsg('Server shutdown by %s.' % self.caller.name)
|
||||
SESSIONS.announce_all(announcement)
|
||||
SESSIONS.portal_shutdown()
|
||||
SESSIONS.server.shutdown(mode='shutdown')
|
||||
|
||||
|
||||
class CmdPy(MuxCommand):
|
||||
"""
|
||||
execute a snippet of python code
|
||||
|
||||
Usage:
|
||||
@py <cmd>
|
||||
|
||||
Switch:
|
||||
time - output an approximate execution time for <cmd>
|
||||
|
||||
Separate multiple commands by ';'. A few variables are made
|
||||
available for convenience in order to offer access to the system
|
||||
(you can import more at execution time).
|
||||
|
||||
Available variables in @py environment:
|
||||
self, me : caller
|
||||
here : caller.location
|
||||
ev : the evennia API
|
||||
inherits_from(obj, parent) : check object inheritance
|
||||
|
||||
You can explore The evennia API from inside the game by calling
|
||||
ev.help(), ev.managers.help() etc.
|
||||
|
||||
{rNote: In the wrong hands this command is a severe security risk.
|
||||
It should only be accessible by trusted server admins/superusers.{n
|
||||
|
||||
"""
|
||||
key = "@py"
|
||||
aliases = ["!"]
|
||||
locks = "cmd:perm(py) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"hook function"
|
||||
|
||||
caller = self.caller
|
||||
pycode = self.args
|
||||
|
||||
if not pycode:
|
||||
string = "Usage: @py <code>"
|
||||
self.msg(string)
|
||||
return
|
||||
|
||||
# check if caller is a player
|
||||
|
||||
# import useful variables
|
||||
import ev
|
||||
available_vars = {'self': caller,
|
||||
'me': caller,
|
||||
'here': hasattr(caller, "location") and caller.location or None,
|
||||
'ev': ev,
|
||||
'inherits_from': utils.inherits_from}
|
||||
|
||||
try:
|
||||
self.msg(">>> %s" % pycode, raw=True, sessid=self.sessid)
|
||||
except TypeError:
|
||||
self.msg(">>> %s" % pycode, raw=True)
|
||||
|
||||
mode = "eval"
|
||||
try:
|
||||
try:
|
||||
pycode_compiled = compile(pycode, "", mode)
|
||||
except Exception:
|
||||
mode = "exec"
|
||||
pycode_compiled = compile(pycode, "", mode)
|
||||
|
||||
duration = ""
|
||||
if "time" in self.switches:
|
||||
t0 = timemeasure()
|
||||
ret = eval(pycode_compiled, {}, available_vars)
|
||||
t1 = timemeasure()
|
||||
duration = " (%.4f ms)" % ((t1 - t0) * 1000)
|
||||
else:
|
||||
ret = eval(pycode_compiled, {}, available_vars)
|
||||
if mode == "eval":
|
||||
ret = "{n<<< %s%s" % (str(ret), duration)
|
||||
else:
|
||||
ret = "{n<<< Done.%s" % duration
|
||||
except Exception:
|
||||
errlist = traceback.format_exc().split('\n')
|
||||
if len(errlist) > 4:
|
||||
errlist = errlist[4:]
|
||||
ret = "\n".join("{n<<< %s" % line for line in errlist if line)
|
||||
|
||||
try:
|
||||
self.msg(ret, sessid=self.sessid)
|
||||
except TypeError:
|
||||
self.msg(ret)
|
||||
|
||||
|
||||
# helper function. Kept outside so it can be imported and run
|
||||
# by other commands.
|
||||
|
||||
def format_script_list(scripts):
|
||||
"Takes a list of scripts and formats the output."
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
|
||||
table = EvTable("{wdbref{n", "{wobj{n", "{wkey{n", "{wintval{n", "{wnext{n",
|
||||
"{wrept{n", "{wdb", "{wtypeclass{n", "{wdesc{n",
|
||||
align='r', border="tablecols")
|
||||
for script in scripts:
|
||||
nextrep = script.time_until_next_repeat()
|
||||
if nextrep is None:
|
||||
nextrep = "PAUS" if script.db._paused_time else "--"
|
||||
else:
|
||||
nextrep = "%ss" % nextrep
|
||||
|
||||
maxrepeat = script.repeats
|
||||
if maxrepeat:
|
||||
rept = "%i/%i" % (maxrepeat - script.remaining_repeats(), maxrepeat)
|
||||
else:
|
||||
rept = "-/-"
|
||||
|
||||
table.add_row(script.id,
|
||||
script.obj.key if (hasattr(script, 'obj') and script.obj) else "<Global>",
|
||||
script.key,
|
||||
script.interval if script.interval > 0 else "--",
|
||||
nextrep,
|
||||
rept,
|
||||
"*" if script.persistent else "-",
|
||||
script.typeclass_path.rsplit('.', 1)[-1],
|
||||
crop(script.desc, width=20))
|
||||
return "%s" % table
|
||||
|
||||
|
||||
class CmdScripts(MuxCommand):
|
||||
"""
|
||||
list and manage all running scripts
|
||||
|
||||
Usage:
|
||||
@scripts[/switches] [#dbref, key, script.path or <obj>]
|
||||
|
||||
Switches:
|
||||
start - start a script (must supply a script path)
|
||||
stop - stops an existing script
|
||||
kill - kills a script - without running its cleanup hooks
|
||||
validate - run a validation on the script(s)
|
||||
|
||||
If no switches are given, this command just views all active
|
||||
scripts. The argument can be either an object, at which point it
|
||||
will be searched for all scripts defined on it, or an script name
|
||||
or #dbref. For using the /stop switch, a unique script #dbref is
|
||||
required since whole classes of scripts often have the same name.
|
||||
|
||||
Use @script for managing commands on objects.
|
||||
"""
|
||||
key = "@scripts"
|
||||
aliases = ["@globalscript", "@listscripts"]
|
||||
locks = "cmd:perm(listscripts) or perm(Wizards)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"implement method"
|
||||
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
|
||||
string = ""
|
||||
if args:
|
||||
if "start" in self.switches:
|
||||
# global script-start mode
|
||||
new_script = create.create_script(args)
|
||||
if new_script:
|
||||
caller.msg("Global script %s was started successfully." % args)
|
||||
else:
|
||||
caller.msg("Global script %s could not start correctly. See logs." % args)
|
||||
return
|
||||
|
||||
# test first if this is a script match
|
||||
scripts = ScriptDB.objects.get_all_scripts(key=args)
|
||||
if not scripts:
|
||||
# try to find an object instead.
|
||||
objects = ObjectDB.objects.object_search(args)
|
||||
if objects:
|
||||
scripts = []
|
||||
for obj in objects:
|
||||
# get all scripts on the object(s)
|
||||
scripts.extend(ScriptDB.objects.get_all_scripts_on_obj(obj))
|
||||
else:
|
||||
# we want all scripts.
|
||||
scripts = ScriptDB.objects.get_all_scripts()
|
||||
if not scripts:
|
||||
caller.msg("No scripts are running.")
|
||||
return
|
||||
|
||||
if not scripts:
|
||||
string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args)
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
if self.switches and self.switches[0] in ('stop', 'del', 'delete', 'kill'):
|
||||
# we want to delete something
|
||||
if not scripts:
|
||||
string = "No scripts/objects matching '%s'. " % args
|
||||
string += "Be more specific."
|
||||
elif len(scripts) == 1:
|
||||
# we have a unique match!
|
||||
if 'kill' in self.switches:
|
||||
string = "Killing script '%s'" % scripts[0].key
|
||||
scripts[0].stop(kill=True)
|
||||
else:
|
||||
string = "Stopping script '%s'." % scripts[0].key
|
||||
scripts[0].stop()
|
||||
#import pdb
|
||||
#pdb.set_trace()
|
||||
ScriptDB.objects.validate() #just to be sure all is synced
|
||||
else:
|
||||
# multiple matches.
|
||||
string = "Multiple script matches. Please refine your search:\n"
|
||||
string += format_script_list(scripts)
|
||||
elif self.switches and self.switches[0] in ("validate", "valid", "val"):
|
||||
# run validation on all found scripts
|
||||
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts)
|
||||
string = "Validated %s scripts. " % ScriptDB.objects.all().count()
|
||||
string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped)
|
||||
else:
|
||||
# No stopping or validation. We just want to view things.
|
||||
string = format_script_list(scripts)
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class CmdObjects(MuxCommand):
|
||||
"""
|
||||
statistics on objects in the database
|
||||
|
||||
Usage:
|
||||
@objects [<nr>]
|
||||
|
||||
Gives statictics on objects in database as well as
|
||||
a list of <nr> latest objects in database. If not
|
||||
given, <nr> defaults to 10.
|
||||
"""
|
||||
key = "@objects"
|
||||
aliases = ["@listobjects", "@listobjs", '@stats', '@db']
|
||||
locks = "cmd:perm(listobjects) or perm(Builders)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Implement the command"
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if self.args and self.args.isdigit():
|
||||
nlim = int(self.args)
|
||||
else:
|
||||
nlim = 10
|
||||
|
||||
nobjs = ObjectDB.objects.count()
|
||||
base_char_typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
nchars = ObjectDB.objects.filter(db_typeclass_path=base_char_typeclass).count()
|
||||
nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=base_char_typeclass).count()
|
||||
nexits = ObjectDB.objects.filter(db_location__isnull=False, db_destination__isnull=False).count()
|
||||
nother = nobjs - nchars - nrooms - nexits
|
||||
|
||||
nobjs = nobjs or 1 # fix zero-div error with empty database
|
||||
|
||||
# total object sum table
|
||||
totaltable = EvTable("{wtype{n", "{wcomment{n", "{wcount{n", "{w%%{n", border="table", align="l")
|
||||
totaltable.align = 'l'
|
||||
totaltable.add_row("Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars) / nobjs) * 100))
|
||||
totaltable.add_row("Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms) / nobjs) * 100))
|
||||
totaltable.add_row("Exits", "(destination!=None)", nexits, "%.2f" % ((float(nexits) / nobjs) * 100))
|
||||
totaltable.add_row("Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100))
|
||||
|
||||
# typeclass table
|
||||
typetable = EvTable("{wtypeclass{n", "{wcount{n", "{w%%{n", border="table", align="l")
|
||||
typetable.align = 'l'
|
||||
dbtotals = ObjectDB.objects.object_totals()
|
||||
for path, count in dbtotals.items():
|
||||
typetable.add_row(path, count, "%.2f" % ((float(count) / nobjs) * 100))
|
||||
|
||||
# last N table
|
||||
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):]
|
||||
latesttable = EvTable("{wcreated{n", "{wdbref{n", "{wname{n", "{wtypeclass{n", align="l", border="table")
|
||||
latesttable.align = 'l'
|
||||
for obj in objs:
|
||||
latesttable.add_row(utils.datetime_format(obj.date_created),
|
||||
obj.dbref, obj.key, obj.path)
|
||||
|
||||
string = "\n{wObject subtype totals (out of %i Objects):{n\n%s" % (nobjs, totaltable)
|
||||
string += "\n{wObject typeclass distribution:{n\n%s" % typetable
|
||||
string += "\n{wLast %s Objects created:{n\n%s" % (min(nobjs, nlim), latesttable)
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class CmdPlayers(MuxCommand):
|
||||
"""
|
||||
list all registered players
|
||||
|
||||
Usage:
|
||||
@players [nr]
|
||||
|
||||
Lists statistics about the Players registered with the game.
|
||||
It will list the <nr> amount of latest registered players
|
||||
If not given, <nr> defaults to 10.
|
||||
"""
|
||||
key = "@players"
|
||||
aliases = ["@listplayers"]
|
||||
locks = "cmd:perm(listplayers) or perm(Wizards)"
|
||||
|
||||
def func(self):
|
||||
"List the players"
|
||||
|
||||
caller = self.caller
|
||||
if self.args and self.args.is_digit():
|
||||
nlim = int(self.args)
|
||||
else:
|
||||
nlim = 10
|
||||
|
||||
nplayers = PlayerDB.objects.count()
|
||||
|
||||
# typeclass table
|
||||
dbtotals = PlayerDB.objects.object_totals()
|
||||
typetable = EvTable("{wtypeclass{n", "{wcount{n", "{w%%{n", border="cells", align="l")
|
||||
for path, count in dbtotals.items():
|
||||
typetable.add_row(path, count, "%.2f" % ((float(count) / nplayers) * 100))
|
||||
# last N table
|
||||
plyrs = PlayerDB.objects.all().order_by("db_date_created")[max(0, nplayers - nlim):]
|
||||
latesttable = EvTable("{wcreated{n", "{wdbref{n", "{wname{n", "{wtypeclass{n", border="cells", align="l")
|
||||
for ply in plyrs:
|
||||
latesttable.add_row(utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path)
|
||||
|
||||
string = "\n{wPlayer typeclass distribution:{n\n%s" % typetable
|
||||
string += "\n{wLast %s Players created:{n\n%s" % (min(nplayers, nlim), latesttable)
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class CmdService(MuxCommand):
|
||||
"""
|
||||
manage system services
|
||||
|
||||
Usage:
|
||||
@service[/switch] <service>
|
||||
|
||||
Switches:
|
||||
list - shows all available services (default)
|
||||
start - activates or reactivate a service
|
||||
stop - stops/inactivate a service (can often be restarted)
|
||||
delete - tries to permanently remove a service
|
||||
|
||||
Service management system. Allows for the listing,
|
||||
starting, and stopping of services. If no switches
|
||||
are given, services will be listed. Note that to operate on the
|
||||
service you have to supply the full (green or red) name as given
|
||||
in the list.
|
||||
"""
|
||||
|
||||
key = "@service"
|
||||
aliases = ["@services"]
|
||||
locks = "cmd:perm(service) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Implement command"
|
||||
|
||||
caller = self.caller
|
||||
switches = self.switches
|
||||
|
||||
if switches and switches[0] not in ("list", "start", "stop", "delete"):
|
||||
caller.msg("Usage: @service/<list|start|stop|delete> [servicename]")
|
||||
return
|
||||
|
||||
# get all services
|
||||
sessions = caller.sessions
|
||||
if not sessions:
|
||||
return
|
||||
service_collection = SESSIONS.server.services
|
||||
|
||||
if not switches or switches[0] == "list":
|
||||
# Just display the list of installed services and their
|
||||
# status, then exit.
|
||||
table = prettytable.PrettyTable(["{wService{n (use @services/start|stop|delete)", "{wstatus"])
|
||||
table.align = 'l'
|
||||
for service in service_collection.services:
|
||||
table.add_row([service.name, service.running and "{gRunning" or "{rNot Running"])
|
||||
caller.msg(str(table))
|
||||
return
|
||||
|
||||
# Get the service to start / stop
|
||||
|
||||
try:
|
||||
service = service_collection.getServiceNamed(self.args)
|
||||
except Exception:
|
||||
string = 'Invalid service name. This command is case-sensitive. '
|
||||
string += 'See @service/list for valid service name (enter the full name exactly).'
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
if switches[0] in ("stop", "delete"):
|
||||
# Stopping/killing a service gracefully closes it and disconnects
|
||||
# any connections (if applicable).
|
||||
|
||||
delmode = switches[0] == "delete"
|
||||
if not service.running:
|
||||
caller.msg('That service is not currently running.')
|
||||
return
|
||||
if service.name[:7] == 'Evennia':
|
||||
if delmode:
|
||||
caller.msg("You cannot remove a core Evennia service (named 'Evennia***').")
|
||||
return
|
||||
string = "You seem to be shutting down a core Evennia service (named 'Evennia***'). Note that"
|
||||
string += "stopping some TCP port services will *not* disconnect users *already*"
|
||||
string += "connected on those ports, but *may* instead cause spurious errors for them. To "
|
||||
string += "safely and permanently remove ports, change settings file and restart the server."
|
||||
caller.msg(string)
|
||||
|
||||
if delmode:
|
||||
service.stopService()
|
||||
service_collection.removeService(service)
|
||||
caller.msg("Stopped and removed service '%s'." % self.args)
|
||||
else:
|
||||
service.stopService()
|
||||
caller.msg("Stopped service '%s'." % self.args)
|
||||
return
|
||||
|
||||
if switches[0] == "start":
|
||||
#Starts a service.
|
||||
if service.running:
|
||||
caller.msg('That service is already running.')
|
||||
return
|
||||
caller.msg("Starting service '%s'." % self.args)
|
||||
service.startService()
|
||||
|
||||
|
||||
class CmdAbout(MuxCommand):
|
||||
"""
|
||||
show Evennia info
|
||||
|
||||
Usage:
|
||||
@about
|
||||
|
||||
Display info about the game engine.
|
||||
"""
|
||||
|
||||
key = "@about"
|
||||
aliases = "@version"
|
||||
locks = "cmd:all()"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Show the version"
|
||||
|
||||
string = """
|
||||
{cEvennia{n %s{n
|
||||
MUD/MUX/MU* development system
|
||||
|
||||
{wLicence{n BSD 3-Clause Licence
|
||||
{wWeb{n http://www.evennia.com
|
||||
{wIrc{n #evennia on FreeNode
|
||||
{wForum{n http://www.evennia.com/discussions
|
||||
{wMaintainer{n (2010-) Griatch (griatch AT gmail DOT com)
|
||||
{wMaintainer{n (2006-10) Greg Taylor
|
||||
|
||||
{wOS{n %s
|
||||
{wPython{n %s
|
||||
{wTwisted{n %s
|
||||
{wDjango{n %s
|
||||
""" % (utils.get_evennia_version(),
|
||||
os.name,
|
||||
sys.version.split()[0],
|
||||
twisted.version.short(),
|
||||
django.get_version())
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class CmdTime(MuxCommand):
|
||||
"""
|
||||
show server time statistics
|
||||
|
||||
Usage:
|
||||
@time
|
||||
|
||||
List Server time statistics such as uptime
|
||||
and the current time stamp.
|
||||
"""
|
||||
key = "@time"
|
||||
aliases = "@uptime"
|
||||
locks = "cmd:perm(time) or perm(Players)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Show server time data in a table."
|
||||
table = prettytable.PrettyTable(["{wserver time statistic","{wtime"])
|
||||
table.align = 'l'
|
||||
table.add_row(["Current server uptime", utils.time_format(gametime.uptime(), 3)])
|
||||
table.add_row(["Total server running time", utils.time_format(gametime.runtime(), 2)])
|
||||
table.add_row(["Total in-game time (realtime x %g" % (gametime.TIMEFACTOR), utils.time_format(gametime.gametime(), 2)])
|
||||
table.add_row(["Server time stamp", datetime.datetime.now()])
|
||||
self.caller.msg(str(table))
|
||||
|
||||
|
||||
class CmdServerLoad(MuxCommand):
|
||||
"""
|
||||
show server load and memory statistics
|
||||
|
||||
Usage:
|
||||
@server[/mem]
|
||||
|
||||
Switch:
|
||||
mem - return only a string of the current memory usage
|
||||
flushmem - flush the idmapper cache
|
||||
|
||||
This command shows server load statistics and dynamic memory
|
||||
usage. It also allows to flush the cache of accessed database
|
||||
objects.
|
||||
|
||||
Some Important statistics in the table:
|
||||
|
||||
{wServer load{n is an average of processor usage. It's usually
|
||||
between 0 (no usage) and 1 (100% usage), but may also be
|
||||
temporarily higher if your computer has multiple CPU cores.
|
||||
|
||||
The {wResident/Virtual memory{n displays the total memory used by
|
||||
the server process.
|
||||
|
||||
Evennia {wcaches{n all retrieved database entities when they are
|
||||
loaded by use of the idmapper functionality. This allows Evennia
|
||||
to maintain the same instances of an entity and allowing
|
||||
non-persistent storage schemes. The total amount of cached objects
|
||||
are displayed plus a breakdown of database object types.
|
||||
|
||||
The {wflushmem{n switch allows to flush the object cache. Please
|
||||
note that due to how Python's memory management works, releasing
|
||||
caches may not show you a lower Residual/Virtual memory footprint,
|
||||
the released memory will instead be re-used by the program.
|
||||
|
||||
"""
|
||||
key = "@server"
|
||||
aliases = ["@serverload", "@serverprocess"]
|
||||
locks = "cmd:perm(list) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Show list."
|
||||
|
||||
caller = self.caller
|
||||
|
||||
# display active processes
|
||||
|
||||
if not utils.host_os_is('posix'):
|
||||
string = "Process listings are only available under Linux/Unix."
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
global _resource, _idmapper
|
||||
if not _resource:
|
||||
import resource as _resource
|
||||
if not _idmapper:
|
||||
from src.utils.idmapper import base as _idmapper
|
||||
|
||||
import resource
|
||||
loadavg = os.getloadavg()
|
||||
psize = _resource.getpagesize()
|
||||
pid = os.getpid()
|
||||
rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1000.0 # resident memory
|
||||
vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1000.0 # virtual memory
|
||||
pmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "%mem")).read()) # percent of resident memory to total
|
||||
rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
|
||||
if "mem" in self.switches:
|
||||
caller.msg("Memory usage: RMEM: {w%g{n MB (%g%%), VMEM (res+swap+cache): {w%g{n MB." % (rmem, pmem, vmem))
|
||||
return
|
||||
|
||||
if "flushmem" in self.switches:
|
||||
caller.msg("Flushed object idmapper cache. Python garbage collector recovered memory from %i objects." % _idmapper.flush_cache())
|
||||
return
|
||||
|
||||
# load table
|
||||
loadtable = prettytable.PrettyTable(["property", "statistic"])
|
||||
loadtable.align = 'l'
|
||||
loadtable.add_row(["Server load (1 min)", "%g" % loadavg[0]])
|
||||
loadtable.add_row(["Process ID", "%g" % pid]),
|
||||
loadtable.add_row(["Bytes per page", "%g " % psize])
|
||||
loadtable.add_row(["CPU time used (total)", "%s (%gs)" % (utils.time_format(rusage.ru_utime), rusage.ru_utime)])
|
||||
loadtable.add_row(["CPU time used (user)", "%s (%gs)" % (utils.time_format(rusage.ru_stime), rusage.ru_stime)])
|
||||
loadtable.add_row(["Memory usage","%g MB (%g%%)" % (rmem, pmem)])
|
||||
loadtable.add_row(["Virtual address space\n {x(resident+swap+caching){n", "%g MB" % vmem])
|
||||
loadtable.add_row(["Page faults", "%g hard, %g soft, %g swapouts" % (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap)])
|
||||
loadtable.add_row(["Disk I/O", "%g reads, %g writes" % (rusage.ru_inblock, rusage.ru_oublock)])
|
||||
loadtable.add_row(["Network I/O", "%g in, %g out" % (rusage.ru_msgrcv, rusage.ru_msgsnd)])
|
||||
loadtable.add_row(["Context switching", "%g vol, %g forced, %g signals" % (rusage.ru_nvcsw, rusage.ru_nivcsw, rusage.ru_nsignals)])
|
||||
|
||||
string = "{wServer CPU and Memory load:{n\n%s" % loadtable
|
||||
|
||||
if not is_pypy:
|
||||
# Cache size measurements are not available on PyPy
|
||||
# because it lacks sys.getsizeof
|
||||
|
||||
# object cache size
|
||||
total_num, cachedict = _idmapper.cache_size()
|
||||
sorted_cache = sorted([(key, num) for key, num in cachedict.items() if num > 0],
|
||||
key=lambda tup: tup[1], reverse=True)
|
||||
memtable = prettytable.PrettyTable(["entity name",
|
||||
"number",
|
||||
"idmapper %%"])
|
||||
memtable.align = 'l'
|
||||
for tup in sorted_cache:
|
||||
memtable.add_row([tup[0],
|
||||
"%i" % tup[1],
|
||||
"%.2f" % (float(tup[1]) / total_num * 100)])
|
||||
|
||||
# get sizes of other caches
|
||||
string += "\n{w Entity idmapper cache:{n %i items\n%s" % (total_num, memtable)
|
||||
|
||||
caller.msg(string)
|
||||
|
||||
287
lib/commands/default/tests.py
Normal file
287
lib/commands/default/tests.py
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
** OBS - this is not a normal command module! **
|
||||
** You cannot import anything in this module as a command! **
|
||||
|
||||
This is part of the Evennia unittest framework, for testing the
|
||||
stability and integrity of the codebase during updates. This module
|
||||
test the default command set. It is instantiated by the
|
||||
src/objects/tests.py module, which in turn is run by as part of the
|
||||
main test suite started with
|
||||
> python game/manage.py test.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from django.conf import settings
|
||||
from django.utils.unittest import TestCase
|
||||
from src.server.serversession import ServerSession
|
||||
from src.objects.objects import DefaultObject, DefaultCharacter
|
||||
from src.players.player import DefaultPlayer
|
||||
from src.utils import create, ansi
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from src.server.caches import field_post_save
|
||||
post_save.connect(field_post_save, dispatch_uid="fieldcache")
|
||||
|
||||
# set up signal here since we are not starting the server
|
||||
|
||||
_RE = re.compile(r"^\+|-+\+|\+-+|--*|\|", re.MULTILINE)
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Command testing
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
def dummy(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
SESSIONS.data_out = dummy
|
||||
SESSIONS.disconnect = dummy
|
||||
|
||||
|
||||
class TestObjectClass(DefaultObject):
|
||||
def msg(self, text="", **kwargs):
|
||||
"test message"
|
||||
pass
|
||||
|
||||
|
||||
class TestCharacterClass(DefaultCharacter):
|
||||
def msg(self, text="", **kwargs):
|
||||
"test message"
|
||||
if self.player:
|
||||
self.player.msg(text=text, **kwargs)
|
||||
else:
|
||||
if not self.ndb.stored_msg:
|
||||
self.ndb.stored_msg = []
|
||||
self.ndb.stored_msg.append(text)
|
||||
|
||||
|
||||
class TestPlayerClass(DefaultPlayer):
|
||||
def msg(self, text="", **kwargs):
|
||||
"test message"
|
||||
if not self.ndb.stored_msg:
|
||||
self.ndb.stored_msg = []
|
||||
self.ndb.stored_msg.append(text)
|
||||
|
||||
# not supported to overload is_superuser field with property.
|
||||
#def _get_superuser(self):
|
||||
# "test with superuser flag"
|
||||
# return self.ndb.is_superuser
|
||||
#is_superuser = property(_get_superuser)
|
||||
|
||||
|
||||
class CommandTest(TestCase):
|
||||
"""
|
||||
Tests a command
|
||||
"""
|
||||
CID = 0 # we must set a different CID in every test to avoid unique-name collisions creating the objects
|
||||
def setUp(self):
|
||||
"sets up testing environment"
|
||||
#print "creating player %i: %s" % (self.CID, self.__class__.__name__)
|
||||
self.player = create.create_player("TestPlayer%i" % self.CID, "test@test.com", "testpassword", typeclass=TestPlayerClass)
|
||||
self.player2 = create.create_player("TestPlayer%ib" % self.CID, "test@test.com", "testpassword", typeclass=TestPlayerClass)
|
||||
self.room1 = create.create_object("src.objects.objects.DefaultRoom", key="Room%i"%self.CID, nohome=True)
|
||||
self.room1.db.desc = "room_desc"
|
||||
settings.DEFAULT_HOME = "#%i" % self.room1.id # we must have a default home
|
||||
self.room2 = create.create_object("src.objects.objects.DefaultRoom", key="Room%ib" % self.CID)
|
||||
self.obj1 = create.create_object(TestObjectClass, key="Obj%i" % self.CID, location=self.room1, home=self.room1)
|
||||
self.obj2 = create.create_object(TestObjectClass, key="Obj%ib" % self.CID, location=self.room1, home=self.room1)
|
||||
self.char1 = create.create_object(TestCharacterClass, key="Char%i" % self.CID, location=self.room1, home=self.room1)
|
||||
self.char1.permissions.add("Immortals")
|
||||
self.char2 = create.create_object(TestCharacterClass, key="Char%ib" % self.CID, location=self.room1, home=self.room1)
|
||||
self.char1.player = self.player
|
||||
self.char2.player = self.player2
|
||||
self.script = create.create_script("src.scripts.scripts.Script", key="Script%i" % self.CID)
|
||||
self.player.permissions.add("Immortals")
|
||||
|
||||
# set up a fake session
|
||||
|
||||
global SESSIONS
|
||||
session = ServerSession()
|
||||
session.init_session("telnet", ("localhost", "testmode"), SESSIONS)
|
||||
session.sessid = self.CID
|
||||
SESSIONS.portal_connect(session.get_sync_data())
|
||||
SESSIONS.login(SESSIONS.session_from_sessid(self.CID), self.player, testmode=True)
|
||||
|
||||
|
||||
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None):
|
||||
"""
|
||||
Test a command by assigning all the needed
|
||||
properties to cmdobj and running
|
||||
cmdobj.at_pre_cmd()
|
||||
cmdobj.parse()
|
||||
cmdobj.func()
|
||||
cmdobj.at_post_cmd()
|
||||
The msgreturn value is compared to eventual
|
||||
output sent to caller.msg in the game
|
||||
"""
|
||||
cmdobj.caller = caller if caller else self.char1
|
||||
#print "call:", cmdobj.key, cmdobj.caller, caller if caller else cmdobj.caller.player
|
||||
#print "perms:", cmdobj.caller.permissions.all()
|
||||
cmdobj.cmdstring = cmdobj.key
|
||||
cmdobj.args = args
|
||||
cmdobj.cmdset = cmdset
|
||||
cmdobj.sessid = self.CID
|
||||
cmdobj.session = SESSIONS.session_from_sessid(self.CID)
|
||||
cmdobj.player = self.player
|
||||
cmdobj.raw_string = cmdobj.key + " " + args
|
||||
cmdobj.obj = caller if caller else self.char1
|
||||
# test
|
||||
self.char1.player.ndb.stored_msg = []
|
||||
cmdobj.at_pre_cmd()
|
||||
cmdobj.parse()
|
||||
cmdobj.func()
|
||||
cmdobj.at_post_cmd()
|
||||
# clean out prettytable sugar
|
||||
stored_msg = self.char1.player.ndb.stored_msg if self.char1.player else self.char1.ndb.stored_msg
|
||||
returned_msg = "|".join(_RE.sub("", mess) for mess in stored_msg)
|
||||
#returned_msg = "|".join(self.char1.player.ndb.stored_msg)
|
||||
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
||||
if msg != None:
|
||||
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
||||
sep1 = "\n" + "="*30 + "Wanted message" + "="*34 + "\n"
|
||||
sep2 = "\n" + "="*30 + "Returned message" + "="*32 + "\n"
|
||||
sep3 = "\n" + "="*78
|
||||
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
||||
raise AssertionError(retval)
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Individual module Tests
|
||||
#------------------------------------------------------------
|
||||
|
||||
from src.commands.default import general
|
||||
class TestGeneral(CommandTest):
|
||||
CID = 1
|
||||
|
||||
def test_cmds(self):
|
||||
self.call(general.CmdLook(), "here", "Room1\n room_desc")
|
||||
self.call(general.CmdHome(), "", "You are already home")
|
||||
self.call(general.CmdInventory(), "", "You are not carrying anything.")
|
||||
self.call(general.CmdPose(), "looks around", "") # TODO-check this
|
||||
self.call(general.CmdHome(), "", "You are already home")
|
||||
self.call(general.CmdNick(), "testalias = testaliasedstring1", "Nick set:")
|
||||
self.call(general.CmdNick(), "/player testalias = testaliasedstring2", "Nick set:")
|
||||
self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Nick set:")
|
||||
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
|
||||
self.assertEqual(u"testaliasedstring2", self.char1.nicks.get("testalias", category="player"))
|
||||
self.assertEqual(u"testaliasedstring3", self.char1.nicks.get("testalias", category="object"))
|
||||
self.call(general.CmdGet(), "Obj1", "You pick up Obj1.")
|
||||
self.call(general.CmdDrop(), "Obj1", "You drop Obj1.")
|
||||
self.call(general.CmdSay(), "Testing", "You say, \"Testing\"")
|
||||
self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):")
|
||||
|
||||
|
||||
from src.commands.default import help
|
||||
from src.commands.default.cmdset_character import CharacterCmdSet
|
||||
class TestHelp(CommandTest):
|
||||
CID = 2
|
||||
def test_cmds(self):
|
||||
self.call(help.CmdHelp(), "", "Command help entries", cmdset=CharacterCmdSet())
|
||||
self.call(help.CmdSetHelp(), "testhelp, General = This is a test", "Topic 'testhelp' was successfully created.")
|
||||
self.call(help.CmdHelp(), "testhelp", "Help topic for testhelp", cmdset=CharacterCmdSet())
|
||||
|
||||
|
||||
from src.commands.default import system
|
||||
class TestSystem(CommandTest):
|
||||
CID = 3
|
||||
def test_cmds(self):
|
||||
# we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime
|
||||
# since the server is not running during these tests.
|
||||
self.call(system.CmdPy(), "1+2", ">>> 1+2|<<< 3")
|
||||
self.call(system.CmdScripts(), "", "dbref ")
|
||||
self.call(system.CmdObjects(), "", "Object subtype totals")
|
||||
self.call(system.CmdAbout(), "", None)
|
||||
self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:")
|
||||
|
||||
|
||||
from src.commands.default import admin
|
||||
class TestAdmin(CommandTest):
|
||||
CID = 4
|
||||
def test_cmds(self):
|
||||
# not testing CmdBoot, CmdDelPlayer, CmdNewPassword
|
||||
self.call(admin.CmdEmit(), "Char4b = Test", "Emitted to Char4b:\nTest")
|
||||
self.call(admin.CmdPerm(), "Obj4 = Builders", "Permission 'Builders' given to Obj4 (the Object/Character).")
|
||||
self.call(admin.CmdWall(), "Test", "Announcing to all connected players ...")
|
||||
self.call(admin.CmdPerm(), "Char4b = Builders","Permission 'Builders' given to Char4b (the Object/Character).")
|
||||
self.call(admin.CmdBan(), "Char4", "NameBan char4 was added.")
|
||||
|
||||
|
||||
from src.commands.default import player
|
||||
class TestPlayer(CommandTest):
|
||||
CID = 5
|
||||
def test_cmds(self):
|
||||
if settings.MULTISESSION_MODE < 2:
|
||||
self.call(player.CmdOOCLook(), "", "You are outofcharacter (OOC).", caller=self.player)
|
||||
if settings.MULTISESSION_MODE == 2:
|
||||
self.call(player.CmdOOCLook(), "", "Account TestPlayer5 (you are OutofCharacter)", caller=self.player)
|
||||
self.call(player.CmdOOC(), "", "You are already", caller=self.player)
|
||||
self.call(player.CmdIC(), "Char5","You become Char5.", caller=self.player)
|
||||
self.call(player.CmdPassword(), "testpassword = testpassword", "Password changed.", caller=self.player)
|
||||
self.call(player.CmdEncoding(), "", "Default encoding:", caller=self.player)
|
||||
self.call(player.CmdWho(), "", "Players:", caller=self.player)
|
||||
self.call(player.CmdQuit(), "", "Quitting. Hope to see you soon again.", caller=self.player)
|
||||
self.call(player.CmdSessions(), "", "Your current session(s):", caller=self.player)
|
||||
self.call(player.CmdColorTest(), "ansi", "ANSI colors:", caller=self.player)
|
||||
self.call(player.CmdCharCreate(), "Test1=Test char","Created new character Test1. Use @ic Test1 to enter the game", caller=self.player)
|
||||
self.call(player.CmdQuell(), "", "Quelling to current puppet's permissions (immortals).", caller=self.player)
|
||||
|
||||
|
||||
from src.commands.default import building
|
||||
class TestBuilding(CommandTest):
|
||||
CID = 6
|
||||
def test_cmds(self):
|
||||
self.call(building.CmdCreate(), "/drop TestObj1", "You create a new DefaultObject: TestObj1.")
|
||||
self.call(building.CmdExamine(), "TestObj1", "Name/key: TestObj1")
|
||||
self.call(building.CmdSetObjAlias(), "TestObj1 = TestObj1b","Alias(es) for 'TestObj1' set to testobj1b.")
|
||||
self.call(building.CmdCopy(), "TestObj1 = TestObj2;TestObj2b, TestObj3;TestObj3b", "Copied TestObj1 to 'TestObj3' (aliases: ['TestObj3b']")
|
||||
self.call(building.CmdSetAttribute(), "Obj6/test1=\"value1\"", "Created attribute Obj6/test1 = \"value1\"")
|
||||
self.call(building.CmdSetAttribute(), "Obj6b/test2=\"value2\"", "Created attribute Obj6b/test2 = \"value2\"")
|
||||
self.call(building.CmdMvAttr(), "Obj6b/test2 = Obj6/test3", "Moving Obj6b/test2 (with value value2) ...\nMoved Obj6b.test2")
|
||||
self.call(building.CmdCpAttr(), "Obj6/test1 = Obj6b/test3", "Copying Obj6/test1 (with value value1) ...\nCopied Obj6.test1")
|
||||
self.call(building.CmdName(), "Obj6b=Obj6c", "Object's name changed to 'Obj6c'.")
|
||||
self.call(building.CmdDesc(), "Obj6c=TestDesc", "The description was set on Obj6c.")
|
||||
self.call(building.CmdWipe(), "Obj6c/test2/test3", "Wiped attributes test2,test3 on Obj6c.")
|
||||
self.call(building.CmdDestroy(), "TestObj1","TestObj1 was destroyed.")
|
||||
self.call(building.CmdDig(), "TestRoom1=testroom;tr,back;b", "Created room TestRoom1")
|
||||
self.call(building.CmdTunnel(), "n = TestRoom2;test2", "Created room TestRoom2")
|
||||
self.call(building.CmdOpen(), "TestExit1=Room6b", "Created new Exit 'TestExit1' from Room6 to Room6b")
|
||||
self.call(building.CmdLink(),"TestExit1 = TestRoom1","Link created TestExit1 > TestRoom1 (one way).")
|
||||
self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
|
||||
self.call(building.CmdSetHome(), "Obj6 = Room6b", "Obj6's home location was changed from Room6")
|
||||
self.call(building.CmdListCmdSets(), "", "<DefaultCharacter (Union, prio 0, perm)>:")
|
||||
self.call(building.CmdTypeclass(), "Obj6 = src.objects.objects.DefaultExit",
|
||||
"Obj6 changed typeclass from src.commands.default.tests.TestObjectClass to src.objects.objects.DefaultExit")
|
||||
self.call(building.CmdLock(), "Obj6 = test:perm(Immortals)", "Added lock 'test:perm(Immortals)' to Obj6.")
|
||||
self.call(building.CmdFind(), "TestRoom1", "One Match")
|
||||
self.call(building.CmdScript(), "Obj6 = src.scripts.scripts.Script", "Script src.scripts.scripts.Script successfully added")
|
||||
self.call(building.CmdTeleport(), "TestRoom1", "TestRoom1\nExits: back|Teleported to TestRoom1.")
|
||||
|
||||
|
||||
from src.commands.default import comms
|
||||
class TestComms(CommandTest):
|
||||
CID = 7
|
||||
def test_cmds(self):
|
||||
# not testing the irc/imc2/rss commands here since testing happens offline
|
||||
self.call(comms.CmdChannelCreate(), "testchan;test=Test Channel", "Created channel testchan and connected to it.")
|
||||
self.call(comms.CmdAddCom(), "tc = testchan", "You are already connected to channel testchan. You can now")
|
||||
self.call(comms.CmdDelCom(), "tc", "Your alias 'tc' for channel testchan was cleared.")
|
||||
self.call(comms.CmdChannels(), "" ,"Available channels (use comlist,addcom and delcom to manage")
|
||||
self.call(comms.CmdAllCom(), "", "Available channels (use comlist,addcom and delcom to manage")
|
||||
self.call(comms.CmdClock(), "testchan=send:all()", "Lock(s) applied. Current locks on testchan:")
|
||||
self.call(comms.CmdCdesc(), "testchan = Test Channel", "Description of channel 'testchan' set to 'Test Channel'.")
|
||||
self.call(comms.CmdCemit(), "testchan = Test Message", "Sent to channel testchan: Test Message")
|
||||
self.call(comms.CmdCWho(), "testchan", "Channel subscriptions\ntestchan:\n TestPlayer7")
|
||||
self.call(comms.CmdPage(), "TestPlayer7b = Test", "TestPlayer7b is offline. They will see your message if they list their pages later.|You paged TestPlayer7b with: 'Test'.")
|
||||
self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] <channel> = <player> [:reason]") # noone else connected to boot
|
||||
self.call(comms.CmdCdestroy(), "testchan" ,"Channel 'testchan' was destroyed.")
|
||||
|
||||
|
||||
from src.commands.default import batchprocess
|
||||
class TestBatchProcess(CommandTest):
|
||||
CID = 8
|
||||
def test_cmds(self):
|
||||
# cannot test batchcode here, it must run inside the server process
|
||||
self.call(batchprocess.CmdBatchCommands(), "examples.batch_cmds", "Running Batchcommand processor Automatic mode for examples.batch_cmds")
|
||||
#self.call(batchprocess.CmdBatchCode(), "examples.batch_code", "")
|
||||
459
lib/commands/default/unloggedin.py
Normal file
459
lib/commands/default/unloggedin.py
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
"""
|
||||
Commands that are available from the connect screen.
|
||||
"""
|
||||
import re
|
||||
from random import getrandbits
|
||||
import traceback
|
||||
from django.conf import settings
|
||||
from src.players.models import PlayerDB
|
||||
from src.objects.models import ObjectDB
|
||||
from src.server.models import ServerConfig
|
||||
from src.comms.models import ChannelDB
|
||||
|
||||
from src.utils import create, logger, utils, ansi
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from src.commands.cmdhandler import CMD_LOGINSTART
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
|
||||
"CmdUnconnectedQuit", "CmdUnconnectedLook", "CmdUnconnectedHelp")
|
||||
|
||||
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
||||
CONNECTION_SCREEN = ""
|
||||
try:
|
||||
CONNECTION_SCREEN = ansi.parse_ansi(utils.random_string_from_module(CONNECTION_SCREEN_MODULE))
|
||||
except Exception:
|
||||
pass
|
||||
if not CONNECTION_SCREEN:
|
||||
CONNECTION_SCREEN = "\nEvennia: Error in CONNECTION_SCREEN MODULE (randomly picked connection screen variable is not a string). \nEnter 'help' for aid."
|
||||
|
||||
|
||||
class CmdUnconnectedConnect(MuxCommand):
|
||||
"""
|
||||
connect to the game
|
||||
|
||||
Usage (at login screen):
|
||||
connect playername password
|
||||
connect "player name" "pass word"
|
||||
|
||||
Use the create command to first create an account before logging in.
|
||||
|
||||
If you have spaces in your name, enclose it in quotes.
|
||||
"""
|
||||
key = "connect"
|
||||
aliases = ["conn", "con", "co"]
|
||||
locks = "cmd:all()" # not really needed
|
||||
arg_regex = r"\s.*?|$"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Uses the Django admin api. Note that unlogged-in commands
|
||||
have a unique position in that their func() receives
|
||||
a session object instead of a source_object like all
|
||||
other types of logged-in commands (this is because
|
||||
there is no object yet before the player has logged in)
|
||||
"""
|
||||
|
||||
session = self.caller
|
||||
args = self.args
|
||||
# extract quoted parts
|
||||
parts = [part.strip() for part in re.split(r"\"|\'", args) if part.strip()]
|
||||
if len(parts) == 1:
|
||||
# this was (hopefully) due to no quotes being found, or a guest login
|
||||
parts = parts[0].split(None, 1)
|
||||
# Guest login
|
||||
if len(parts) == 1 and parts[0].lower() == "guest" and settings.GUEST_ENABLED:
|
||||
try:
|
||||
# Find an available guest name.
|
||||
for playername in settings.GUEST_LIST:
|
||||
if not PlayerDB.objects.filter(username__iexact=playername):
|
||||
break
|
||||
playername = None
|
||||
if playername == None:
|
||||
session.msg("All guest accounts are in use. Please try again later.")
|
||||
return
|
||||
|
||||
password = "%016x" % getrandbits(64)
|
||||
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
|
||||
permissions = settings.PERMISSION_GUEST_DEFAULT
|
||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
ptypeclass = settings.BASE_GUEST_TYPECLASS
|
||||
start_location = ObjectDB.objects.get_id(settings.GUEST_START_LOCATION)
|
||||
|
||||
new_player = _create_player(session, playername, password,
|
||||
home, permissions, ptypeclass)
|
||||
if new_player:
|
||||
_create_character(session, new_player, typeclass, start_location,
|
||||
home, permissions)
|
||||
session.sessionhandler.login(session, new_player)
|
||||
|
||||
except Exception:
|
||||
# We are in the middle between logged in and -not, so we have
|
||||
# to handle tracebacks ourselves at this point. If we don't,
|
||||
# we won't see any errors at all.
|
||||
string = "%s\nThis is a bug. Please e-mail an admin if the problem persists."
|
||||
session.msg(string % (traceback.format_exc()))
|
||||
logger.log_errmsg(traceback.format_exc())
|
||||
finally:
|
||||
return
|
||||
|
||||
if len(parts) != 2:
|
||||
session.msg("\n\r Usage (without <>): connect <name> <password>")
|
||||
return
|
||||
playername, password = parts
|
||||
|
||||
# Match account name and check password
|
||||
player = PlayerDB.objects.get_player_from_name(playername)
|
||||
pswd = None
|
||||
if player:
|
||||
pswd = player.check_password(password)
|
||||
|
||||
if not (player and pswd):
|
||||
# No playername or password match
|
||||
string = "Wrong login information given.\nIf you have spaces in your name or "
|
||||
string += "password, don't forget to enclose it in quotes. Also capitalization matters."
|
||||
string += "\nIf you are new you should first create a new account "
|
||||
string += "using the 'create' command."
|
||||
session.msg(string)
|
||||
return
|
||||
|
||||
# Check IP and/or name bans
|
||||
bans = ServerConfig.objects.conf("server_bans")
|
||||
if bans and (any(tup[0]==player.name.lower() for tup in bans)
|
||||
or
|
||||
any(tup[2].match(session.address) for tup in bans if tup[2])):
|
||||
# this is a banned IP or name!
|
||||
string = "{rYou have been banned and cannot continue from here."
|
||||
string += "\nIf you feel this ban is in error, please email an admin.{x"
|
||||
session.msg(string)
|
||||
session.execute_cmd("quit")
|
||||
return
|
||||
|
||||
# actually do the login. This will call all other hooks:
|
||||
# session.at_login()
|
||||
# player.at_init() # always called when object is loaded from disk
|
||||
# player.at_pre_login()
|
||||
# player.at_first_login() # only once
|
||||
# player.at_post_login(sessid=sessid)
|
||||
session.sessionhandler.login(session, player)
|
||||
|
||||
|
||||
class CmdUnconnectedCreate(MuxCommand):
|
||||
"""
|
||||
create a new player account
|
||||
|
||||
Usage (at login screen):
|
||||
create <playername> <password>
|
||||
create "player name" "pass word"
|
||||
|
||||
This creates a new player account.
|
||||
|
||||
If you have spaces in your name, enclose it in quotes.
|
||||
"""
|
||||
key = "create"
|
||||
aliases = ["cre", "cr"]
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s.*?|$"
|
||||
|
||||
def func(self):
|
||||
"Do checks and create account"
|
||||
|
||||
session = self.caller
|
||||
args = self.args.strip()
|
||||
|
||||
# extract quoted parts
|
||||
parts = [part.strip() for part in re.split(r"\"|\'", args) if part.strip()]
|
||||
if len(parts) == 1:
|
||||
# this was (hopefully) due to no quotes being found
|
||||
parts = parts[0].split(None, 1)
|
||||
if len(parts) != 2:
|
||||
string = "\n Usage (without <>): create <name> <password>"
|
||||
string += "\nIf <name> or <password> contains spaces, enclose it in quotes."
|
||||
session.msg(string)
|
||||
return
|
||||
playername, password = parts
|
||||
|
||||
# sanity checks
|
||||
if not re.findall('^[\w. @+-]+$', playername) or not (0 < len(playername) <= 30):
|
||||
# this echoes the restrictions made by django's auth
|
||||
# module (except not allowing spaces, for convenience of
|
||||
# logging in).
|
||||
string = "\n\r Playername can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only."
|
||||
session.msg(string)
|
||||
return
|
||||
# strip excessive spaces in playername
|
||||
playername = re.sub(r"\s+", " ", playername).strip()
|
||||
if PlayerDB.objects.filter(username__iexact=playername):
|
||||
# player already exists (we also ignore capitalization here)
|
||||
session.msg("Sorry, there is already a player with the name '%s'." % playername)
|
||||
return
|
||||
# Reserve playernames found in GUEST_LIST
|
||||
if settings.GUEST_LIST and playername.lower() in map(str.lower, settings.GUEST_LIST):
|
||||
string = "\n\r That name is reserved. Please choose another Playername."
|
||||
session.msg(string)
|
||||
return
|
||||
if not re.findall('^[\w. @+-]+$', password) or not (3 < len(password)):
|
||||
string = "\n\r Password should be longer than 3 characers. Letters, spaces, digits and @\.\+\-\_ only."
|
||||
string += "\nFor best security, make it longer than 8 characters. You can also use a phrase of"
|
||||
string += "\nmany words if you enclose the password in quotes."
|
||||
session.msg(string)
|
||||
return
|
||||
|
||||
# Check IP and/or name bans
|
||||
bans = ServerConfig.objects.conf("server_bans")
|
||||
if bans and (any(tup[0]==playername.lower() for tup in bans)
|
||||
or
|
||||
any(tup[2].match(session.address) for tup in bans if tup[2])):
|
||||
# this is a banned IP or name!
|
||||
string = "{rYou have been banned and cannot continue from here."
|
||||
string += "\nIf you feel this ban is in error, please email an admin.{x"
|
||||
session.msg(string)
|
||||
session.execute_cmd("quit")
|
||||
return
|
||||
|
||||
# everything's ok. Create the new player account.
|
||||
try:
|
||||
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
|
||||
permissions = settings.PERMISSION_PLAYER_DEFAULT
|
||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
new_player = _create_player(session, playername, password, default_home, permissions)
|
||||
start_location = ObjectDB.objects.get_id(settings.START_LOCATION)
|
||||
if new_player:
|
||||
if MULTISESSION_MODE < 2:
|
||||
_create_character(session, new_player, typeclass, start_location,
|
||||
default_home, permissions)
|
||||
# tell the caller everything went well.
|
||||
string = "A new account '%s' was created. Welcome!"
|
||||
if " " in playername:
|
||||
string += "\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
|
||||
else:
|
||||
string += "\n\nYou can now log with the command 'connect %s <your password>'."
|
||||
session.msg(string % (playername, playername))
|
||||
|
||||
except Exception:
|
||||
# We are in the middle between logged in and -not, so we have
|
||||
# to handle tracebacks ourselves at this point. If we don't,
|
||||
# we won't see any errors at all.
|
||||
string = "%s\nThis is a bug. Please e-mail an admin if the problem persists."
|
||||
session.msg(string % (traceback.format_exc()))
|
||||
logger.log_errmsg(traceback.format_exc())
|
||||
|
||||
|
||||
class CmdUnconnectedQuit(MuxCommand):
|
||||
"""
|
||||
quit when in unlogged-in state
|
||||
|
||||
Usage:
|
||||
quit
|
||||
|
||||
We maintain a different version of the quit command
|
||||
here for unconnected players for the sake of simplicity. The logged in
|
||||
version is a bit more complicated.
|
||||
"""
|
||||
key = "quit"
|
||||
aliases = ["q", "qu"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Simply close the connection."
|
||||
session = self.caller
|
||||
#session.msg("Good bye! Disconnecting ...")
|
||||
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
|
||||
|
||||
|
||||
class CmdUnconnectedLook(MuxCommand):
|
||||
"""
|
||||
look when in unlogged-in state
|
||||
|
||||
Usage:
|
||||
look
|
||||
|
||||
This is an unconnected version of the look command for simplicity.
|
||||
|
||||
This is called by the server and kicks everything in gear.
|
||||
All it does is display the connect screen.
|
||||
"""
|
||||
key = CMD_LOGINSTART
|
||||
aliases = ["look", "l"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Show the connect screen."
|
||||
self.caller.msg(CONNECTION_SCREEN)
|
||||
|
||||
|
||||
class CmdUnconnectedHelp(MuxCommand):
|
||||
"""
|
||||
get help when in unconnected-in state
|
||||
|
||||
Usage:
|
||||
help
|
||||
|
||||
This is an unconnected version of the help command,
|
||||
for simplicity. It shows a pane of info.
|
||||
"""
|
||||
key = "help"
|
||||
aliases = ["h", "?"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Shows help"
|
||||
|
||||
string = \
|
||||
"""
|
||||
You are not yet logged into the game. Commands available at this point:
|
||||
|
||||
{wcreate{n - create a new account
|
||||
{wconnect{n - connect with an existing account
|
||||
{wlook{n - re-show the connection screen
|
||||
{whelp{n - show this help
|
||||
{wencoding{n - change the text encoding to match your client
|
||||
{wquit{n - abort the connection
|
||||
|
||||
To login, first create an account
|
||||
|
||||
{wcreate Anna c67jHL8p{n
|
||||
|
||||
Note that if you use spaces in your name, you have to enclose in quotes:
|
||||
|
||||
{wcreate "Anna the Barbarian" c67jHL8p{n
|
||||
|
||||
It's always a good idea (not only here, but everywhere on the net)
|
||||
to not use a regular word for your password. Make it longer than
|
||||
6 characters or write a full passphrase.
|
||||
|
||||
Once you have an account, connect using your password
|
||||
|
||||
{wconnect Anna c67jHL8p{n
|
||||
|
||||
(Again, if there are spaces in the name you have to enclose it in quotes).
|
||||
This should log you in. Run {whelp{n again once you're logged in
|
||||
to get more aid. Hope you enjoy your stay!
|
||||
|
||||
You can use the {wlook{n command if you want to see the connect screen again.
|
||||
|
||||
"""
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class CmdUnconnectedEncoding(MuxCommand):
|
||||
"""
|
||||
set which text encoding to use in unconnected-in state
|
||||
|
||||
Usage:
|
||||
encoding/switches [<encoding>]
|
||||
|
||||
Switches:
|
||||
clear - clear your custom encoding
|
||||
|
||||
|
||||
This sets the text encoding for communicating with Evennia. This is mostly
|
||||
an issue only if you want to use non-ASCII characters (i.e. letters/symbols
|
||||
not found in English). If you see that your characters look strange (or you
|
||||
get encoding errors), you should use this command to set the server
|
||||
encoding to be the same used in your client program.
|
||||
|
||||
Common encodings are utf-8 (default), latin-1, ISO-8859-1 etc.
|
||||
|
||||
If you don't submit an encoding, the current encoding will be displayed
|
||||
instead.
|
||||
"""
|
||||
|
||||
key = "encoding"
|
||||
aliases = "@encoding, @encode"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Sets the encoding.
|
||||
"""
|
||||
|
||||
if self.session is None:
|
||||
return
|
||||
|
||||
if 'clear' in self.switches:
|
||||
# remove customization
|
||||
old_encoding = self.session.encoding
|
||||
if old_encoding:
|
||||
string = "Your custom text encoding ('%s') was cleared." % old_encoding
|
||||
else:
|
||||
string = "No custom encoding was set."
|
||||
self.session.encoding = "utf-8"
|
||||
elif not self.args:
|
||||
# just list the encodings supported
|
||||
pencoding = self.session.encoding
|
||||
string = ""
|
||||
if pencoding:
|
||||
string += "Default encoding: {g%s{n (change with {w@encoding <encoding>{n)" % pencoding
|
||||
encodings = settings.ENCODINGS
|
||||
if encodings:
|
||||
string += "\nServer's alternative encodings (tested in this order):\n {g%s{n" % ", ".join(encodings)
|
||||
if not string:
|
||||
string = "No encodings found."
|
||||
else:
|
||||
# change encoding
|
||||
old_encoding = self.session.encoding
|
||||
encoding = self.args
|
||||
self.session.encoding = encoding
|
||||
string = "Your custom text encoding was changed from '%s' to '%s'." % (old_encoding, encoding)
|
||||
self.caller.msg(string.strip())
|
||||
|
||||
|
||||
def _create_player(session, playername, password,
|
||||
default_home, permissions, typeclass=None):
|
||||
"""
|
||||
Helper function, creates a player of the specified typeclass.
|
||||
"""
|
||||
try:
|
||||
new_player = create.create_player(playername, None, password,
|
||||
permissions=permissions, typeclass=typeclass)
|
||||
|
||||
except Exception, e:
|
||||
session.msg("There was an error creating the Player:\n%s\n If this problem persists, contact an admin." % e)
|
||||
logger.log_trace()
|
||||
return False
|
||||
|
||||
# This needs to be called so the engine knows this player is
|
||||
# logging in for the first time. (so it knows to call the right
|
||||
# hooks during login later)
|
||||
utils.init_new_player(new_player)
|
||||
|
||||
# join the new player to the public channel
|
||||
pchanneldef = settings.CHANNEL_PUBLIC
|
||||
if pchanneldef:
|
||||
pchannel = ChannelDB.objects.get_channel(pchanneldef[0])
|
||||
if not pchannel.connect(new_player):
|
||||
string = "New player '%s' could not connect to public channel!" % new_player.key
|
||||
logger.log_errmsg(string)
|
||||
return new_player
|
||||
|
||||
|
||||
def _create_character(session, new_player, typeclass, start_location, home, permissions):
|
||||
"""
|
||||
Helper function, creates a character based on a player's name.
|
||||
This is meant for Guest and MULTISESSION_MODE < 2 situations.
|
||||
"""
|
||||
try:
|
||||
if not start_location:
|
||||
start_location = home # fallback
|
||||
new_character = create.create_object(typeclass, key=new_player.key,
|
||||
location=start_location, home=home,
|
||||
permissions=permissions)
|
||||
# set playable character list
|
||||
new_player.db._playable_characters.append(new_character)
|
||||
|
||||
# allow only the character itself and the player to puppet this character (and Immortals).
|
||||
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" %
|
||||
(new_character.id, new_player.id))
|
||||
|
||||
# If no description is set, set a default description
|
||||
if not new_character.db.desc:
|
||||
new_character.db.desc = "This is a Player."
|
||||
# We need to set this to have @ic auto-connect to this character
|
||||
new_player.db._last_puppet = new_character
|
||||
except Exception, e:
|
||||
session.msg("There was an error creating the Character:\n%s\n If this problem persists, contact an admin." % e)
|
||||
logger.log_trace()
|
||||
return False
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue