Changed cmdhandler to include Session-level cmdset.

This commit is contained in:
Griatch 2013-09-08 00:14:06 +02:00
parent 07b1e40299
commit 1e96b13920
11 changed files with 205 additions and 80 deletions

View file

@ -48,6 +48,8 @@ from django.utils.translation import ugettext as _
__all__ = ("cmdhandler",)
_GA = object.__getattribute__
# 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))
@ -83,65 +85,112 @@ class ExecSystemCommand(Exception):
# Helper function
@inlineCallbacks
def get_and_merge_cmdsets(caller):
def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None):
"""
Gather all relevant cmdsets and merge them. Note
that this is only relevant for logged-in callers.
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!
"""
# The calling object's cmdset
try:
yield caller.at_cmdset_get()
except Exception:
logger.log_trace()
try:
caller_cmdset = caller.cmdset.current
except AttributeError:
caller_cmdset = None
local_obj_cmdsets = [None]
# Create cmdset for all player's available channels
channel_cmdset = None
if not caller_cmdset.no_channels:
channel_cmdset = yield CHANNELHANDLER.get_cmdset(caller)
@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)
# Gather cmdsets from location, objects in location or carried
local_objects_cmdsets = [None]
try:
location = caller.location
except Exception:
location = None
if location and not caller_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=caller.dbobj) + caller.contents + [location]
for obj in local_objlist:
try:
# call hook in case we need to do dynamic changing to cmdset
yield obj.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_objects_cmdsets = yield [obj.cmdset.current for obj in local_objlist
if (obj.cmdset.current and obj.locks.check(caller, 'call', no_superuser_bypass=True))]
for cset in local_objects_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
@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.dbobj) + 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)
# Player object's commandsets
try:
player_cmdset = caller.player.cmdset.current
except AttributeError:
player_cmdset = None
@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
cmdsets = yield [caller_cmdset] + [player_cmdset] + [channel_cmdset] + local_objects_cmdsets
# weed out all non-found sets
cmdsets = yield [cmdset for cmdset in cmdsets if cmdset and cmdset.key!="Empty"]
# report cmdset errors to user (these should already have been logged)
yield [caller.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR"]
yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR"]
if cmdsets:
# we group and merge all same-prio cmdsets separately (this avoids order-dependent
@ -167,7 +216,7 @@ def get_and_merge_cmdsets(caller):
else:
cmdset = None
for cset in (cset for cset in local_objects_cmdsets if cset):
for cset in (cset for cset in local_obj_cmdsets if cset):
cset.duplicates = cset.old_duplicates
returnValue(cmdset)
@ -175,21 +224,49 @@ def get_and_merge_cmdsets(caller):
# Main command-handler function
@inlineCallbacks
def cmdhandler(caller, raw_string, testing=False, sessid=None):
def cmdhandler(called_on, raw_string, testing=False, callertype="session", sessid=None):
"""
This is the main function to handle any string sent to the engine.
caller - calling object
called_on - 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.
sessid - the session id calling this handler, if any
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.
Note that this function returns a deferred!
"""
session, player, obj = None, None, None
if callertype == "session":
session = called_on
player = session.player
if player:
obj = yield _GA(player.dbobj, "get_puppet")(session.sessid)
elif callertype == "player":
player = called_on
if sessid:
obj = yield _GA(player.dbobj, "get_puppet")(sessid)
elif callertype == "object":
obj = called_on
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)
cmdset = yield get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid)
if not cmdset:
# this is bad and shouldn't happen.
raise NoCmdSets
@ -212,13 +289,15 @@ def cmdhandler(caller, raw_string, testing=False, sessid=None):
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.
# We have a unique command match. But it may still be invalid.
match = matches[0]
cmdname, args, cmd = match[0], match[1], match[2]
@ -232,8 +311,10 @@ def cmdhandler(caller, raw_string, testing=False, sessid=None):
# 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:
@ -243,7 +324,7 @@ def cmdhandler(caller, raw_string, testing=False, sessid=None):
raise ExecSystemCommand(syscmd, sysarg)
# Check if this is a Channel match.
# 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.
@ -251,7 +332,7 @@ def cmdhandler(caller, raw_string, testing=False, sessid=None):
if syscmd:
# replace system command with custom version
cmd = syscmd
cmd.sessid = sessid
cmd.sessid = caller.sessid if callertype=="session" else None
sysarg = "%s:%s" % (cmdname, args)
raise ExecSystemCommand(cmd, sysarg)
@ -262,11 +343,14 @@ def cmdhandler(caller, raw_string, testing=False, sessid=None):
cmd.cmdstring = cmdname
cmd.args = args
cmd.cmdset = cmdset
cmd.sessid = sessid
cmd.session = session
cmd.player = player
#cmd.obj # set via command handler
cmd.sessid = session.sessid if session else sessid
cmd.raw_string = unformatted_raw_string
if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'):
# cmd.obj is automatically made available.
# cmd.obj is automatically made available by the cmdhandler.
# we make sure to validate its scripts.
yield cmd.obj.scripts.validate()
@ -312,7 +396,7 @@ def cmdhandler(caller, raw_string, testing=False, sessid=None):
syscmd.cmdstring = syscmd.key
syscmd.args = sysarg
syscmd.cmdset = cmdset
syscmd.sessid = sessid
syscmd.sessid = caller.sessid if callertype=="session" else None
syscmd.raw_string = unformatted_raw_string
if hasattr(syscmd, 'obj') and hasattr(syscmd.obj, 'scripts'):

View file

@ -262,7 +262,7 @@ def at_multimatch_cmd(caller, matches):
id1 = ""
id2 = ""
if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller):
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)

View file

@ -1673,7 +1673,7 @@ class CmdExamine(ObjManipCommand):
caller.execute_cmd('look %s' % obj.name)
return
# using callback for printing result whenever function returns.
get_and_merge_cmdsets(obj).addCallback(get_cmdset_callback)
get_and_merge_cmdsets(obj, self.session, self.player, obj, "session").addCallback(get_cmdset_callback)
else:
self.msg("You need to supply a target to examine.")
return
@ -1708,7 +1708,7 @@ class CmdExamine(ObjManipCommand):
caller.msg(self.format_attributes(obj, attrname, crop=False))
else:
# using callback to print results whenever function returns.
get_and_merge_cmdsets(obj).addCallback(get_cmdset_callback)
get_and_merge_cmdsets(obj, self.session, self.player, obj, "session").addCallback(get_cmdset_callback)
class CmdFind(MuxCommand):

View file

@ -29,7 +29,7 @@ class PlayerCmdSet(CmdSet):
self.add(player.CmdIC())
self.add(player.CmdOOC())
self.add(player.CmdCharCreate())
self.add(player.CmdSessions())
#self.add(player.CmdSessions())
self.add(player.CmdWho())
self.add(player.CmdEncoding())
self.add(player.CmdQuit())

View 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 = 0
def at_cmdset_creation(self):
"Populate the cmdset"
self.add(player.CmdSessions())

View file

@ -656,7 +656,7 @@ class ObjectDB(TypedObject):
if nick.db_key in raw_list:
raw_string = raw_string.replace(nick.db_key, nick.db_strvalue, 1)
break
return cmdhandler.cmdhandler(_GA(self, "typeclass"), raw_string, sessid=sessid)
return cmdhandler.cmdhandler(_GA(self, "typeclass"), raw_string, callertype="object", sessid=sessid)
def msg(self, msg=None, from_obj=None, data=None, sessid=0):
"""

View file

@ -494,7 +494,7 @@ class PlayerDB(TypedObject, AbstractUser):
# should not matter (since the return goes to all of them we can just
# use the first one as the source)
sessid = self.get_all_sessions()[0].sessid
return cmdhandler.cmdhandler(self.typeclass, raw_string, sessid=sessid)
return cmdhandler.cmdhandler(self.typeclass, raw_string, callertype="player", sessid=sessid)
def search(self, ostring, return_character=False, **kwargs):
"""

View file

@ -13,6 +13,7 @@ from django.conf import settings
from src.scripts.models import ScriptDB
from src.comms.models import Channel
from src.utils import logger, utils
from src.utils.utils import make_iter
from src.commands import cmdhandler, cmdsethandler
from src.server.session import Session
@ -46,6 +47,15 @@ class ServerSession(Session):
def __init__(self):
"Initiate to avoid AttributeErrors down the line"
self.puppet = None
self.player = None
self.cmdset_storage_string = ""
self.cmdset = cmdsethandler.CmdSetHandler(self)
def __cmdset_storage_get(self):
return [path.strip() for path in self.cmdset_storage_string.split(',')]
def __cmdset_storage_set(self, value):
self.cmdset_storage_string = ",".join(str(val).strip() for val in make_iter(value))
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set)
def at_sync(self):
"""
@ -62,10 +72,11 @@ class ServerSession(Session):
if not self.logged_in:
# assign the unloggedin-command set.
self.cmdset = cmdsethandler.CmdSetHandler(self)
self.cmdset_storage = [settings.CMDSET_UNLOGGEDIN]
self.cmdset.update(init_mode=True)
elif self.puid:
self.cmdset_storage = settings.CMDSET_UNLOGGEDIN
self.cmdset.update(init_mode=True)
if self.puid:
# reconnect puppet (puid is only set if we are coming back from a server reload)
obj = _ObjectDB.objects.get(id=self.puid)
self.player.puppet_object(self.sessid, obj, normal_mode=False)
@ -83,11 +94,16 @@ class ServerSession(Session):
self.conn_time = time.time()
self.puid = None
self.puppet = None
self.cmdset_storage = settings.CMDSET_SESSION
# Update account's last login time.
self.player.last_login = datetime.now()
self.player.save()
# add the session-level cmdset
self.cmdset = cmdsethandler.CmdSetHandler(self)
self.cmdset.update(init_mode=True)
def at_disconnect(self):
"""
Hook called by sessionhandler when disconnecting this session.
@ -156,13 +172,14 @@ class ServerSession(Session):
if str(command_string).strip() == IDLE_COMMAND:
self.update_session_counters(idle=True)
return
if self.logged_in:
# the inmsg handler will relay to the right place
self.player.inmsg(command_string, self)
else:
# we are not logged in. Execute cmd with the the session directly
# (it uses the settings.UNLOGGEDIN cmdset)
cmdhandler.cmdhandler(self, command_string, sessid=self.sessid)
cmdhandler.cmdhandler(self, command_string, callertype="session", sessid=self.sessid)
#if self.logged_in:
# # the inmsg handler will relay to the right place
# self.player.inmsg(command_string, self)
#else:
# # we are not logged in. Execute cmd with the the session directly
# # (it uses the settings.UNLOGGEDIN cmdset)
# cmdhandler.cmdhandler(self, command_string, sessid=self.sessid)
self.update_session_counters()
execute_cmd = data_in # alias

View file

@ -36,7 +36,7 @@ class Session(object):
_attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname',
'logged_in', 'puid', 'encoding',
'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total',
'protocol_flags', 'server_data')
'protocol_flags', 'server_data', "cmdset_storage_string")
def init_session(self, protocol_key, address, sessionhandler):
"""

View file

@ -15,6 +15,7 @@ There are two similar but separate stores of sessions:
import time
from django.conf import settings
from src.commands.cmdhandler import CMD_LOGINSTART
from src.utils.utils import variable_from_module
# delayed imports
_PlayerDB = None
@ -43,7 +44,9 @@ def delayed_import():
"Helper method for delayed import of all needed entities"
global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
if not _ServerSession:
from src.server.serversession import ServerSession as _ServerSession
# we allow optional arbitrary serversession class for overloading
modulename, classname = settings.SERVER_SESSION_CLASS.rsplit(".", 1)
_ServerSession = variable_from_module(modulename, classname)
if not _PlayerDB:
from src.players.models import PlayerDB as _PlayerDB
if not _ServerConfig:

View file

@ -216,17 +216,22 @@ LOCK_FUNC_MODULES = ("src.locks.lockfuncs",)
# to point to these copies instead - these you can then change as you please
# (or copy/paste from the default modules in src/ if you prefer).
# Command set used before player has logged in
# Command set used on session before player has logged in
CMDSET_UNLOGGEDIN = "src.commands.default.cmdset_unloggedin.UnloggedinCmdSet"
# Command set used on the logged-in session
CMDSET_SESSION = "src.commands.default.cmdset_session.SessionCmdSet"
# Default set for logged in player with characters (fallback)
CMDSET_CHARACTER = "src.commands.default.cmdset_character.CharacterCmdSet"
# Command set for players without a character (ooc)
CMDSET_PLAYER = "src.commands.default.cmdset_player.PlayerCmdSet"
######################################################################
# Typeclasses
# Typeclasses and other paths
######################################################################
# Server-side session class used.
SERVER_SESSION_CLASS = "src.server.serversession.ServerSession"
# Base paths for typeclassed object classes. These paths must be
# defined relative evennia's root directory. They will be searched in
# order to find relative typeclass paths.