From 1e96b13920d9753483ff9dae208f727f859ccfea Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 8 Sep 2013 00:14:06 +0200 Subject: [PATCH] Changed cmdhandler to include Session-level cmdset. --- src/commands/cmdhandler.py | 202 +++++++++++++++++-------- src/commands/cmdparser.py | 2 +- src/commands/default/building.py | 4 +- src/commands/default/cmdset_player.py | 2 +- src/commands/default/cmdset_session.py | 16 ++ src/objects/models.py | 2 +- src/players/models.py | 2 +- src/server/serversession.py | 39 +++-- src/server/session.py | 2 +- src/server/sessionhandler.py | 5 +- src/settings_default.py | 9 +- 11 files changed, 205 insertions(+), 80 deletions(-) create mode 100644 src/commands/default/cmdset_session.py diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index 44e6860a3d..91291088da 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -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'): diff --git a/src/commands/cmdparser.py b/src/commands/cmdparser.py index 1b4650eaa2..514eb4340b 100644 --- a/src/commands/cmdparser.py +++ b/src/commands/cmdparser.py @@ -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) diff --git a/src/commands/default/building.py b/src/commands/default/building.py index e8e27ccc8c..c45ed0ff32 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -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): diff --git a/src/commands/default/cmdset_player.py b/src/commands/default/cmdset_player.py index e448c2fca7..cb7641c924 100644 --- a/src/commands/default/cmdset_player.py +++ b/src/commands/default/cmdset_player.py @@ -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()) diff --git a/src/commands/default/cmdset_session.py b/src/commands/default/cmdset_session.py new file mode 100644 index 0000000000..46c038da15 --- /dev/null +++ b/src/commands/default/cmdset_session.py @@ -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()) diff --git a/src/objects/models.py b/src/objects/models.py index 8ed9f0279a..7fcb0d7684 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -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): """ diff --git a/src/players/models.py b/src/players/models.py index 04616b008f..79eecd043b 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -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): """ diff --git a/src/server/serversession.py b/src/server/serversession.py index 24ba73cae1..2626eb008e 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -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 diff --git a/src/server/session.py b/src/server/session.py index d2866a2abb..5cfed71b9c 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -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): """ diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 6554279d31..eaa76c7d9a 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -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: diff --git a/src/settings_default.py b/src/settings_default.py index f8a6bb0479..e55987ad07 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -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.