2010-08-29 18:46:58 +00:00
|
|
|
"""
|
|
|
|
|
CmdSethandler
|
|
|
|
|
|
|
|
|
|
The Cmdhandler 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).
|
|
|
|
|
|
2010-09-04 16:57:01 +00:00
|
|
|
Since any number of CommandSets can be piled on top of each other, you
|
2010-08-29 18:46:58 +00:00
|
|
|
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!
|
|
|
|
|
"""
|
|
|
|
|
import traceback
|
|
|
|
|
from src.utils import logger
|
|
|
|
|
from src.utils import create
|
|
|
|
|
from src.commands.cmdset import CmdSet
|
|
|
|
|
from src.scripts.scripts import AddCmdSet
|
|
|
|
|
|
|
|
|
|
CACHED_CMDSETS = {}
|
|
|
|
|
|
|
|
|
|
def import_cmdset(python_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.
|
|
|
|
|
python_path - This is the full path to the cmdset object.
|
2010-09-19 06:57:08 +00:00
|
|
|
cmdsetobj - the database object/typeclass on which this cmdset is to be assigned
|
2010-08-29 18:46:58 +00:00
|
|
|
(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.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
try:
|
2010-10-11 17:22:39 +00:00
|
|
|
#print "importing %s: CACHED_CMDSETS=%s" % (python_path, CACHED_CMDSETS)
|
2010-09-05 15:05:40 +00:00
|
|
|
wanted_cache_key = python_path
|
2010-08-29 18:46:58 +00:00
|
|
|
cmdsetclass = CACHED_CMDSETS.get(wanted_cache_key, None)
|
|
|
|
|
errstring = ""
|
|
|
|
|
if not cmdsetclass:
|
2010-10-21 18:58:47 +00:00
|
|
|
#print "cmdset %s not in cache. Reloading." % wanted_cache_key
|
2010-08-29 18:46:58 +00:00
|
|
|
# Not in cache. Reload from disk.
|
|
|
|
|
modulepath, classname = python_path.rsplit('.', 1)
|
|
|
|
|
module = __import__(modulepath, fromlist=[True])
|
2010-09-05 16:35:39 +00:00
|
|
|
cmdsetclass = module.__dict__[classname]
|
2010-08-29 18:46:58 +00:00
|
|
|
CACHED_CMDSETS[wanted_cache_key] = cmdsetclass
|
2010-10-11 17:22:39 +00:00
|
|
|
#print "cmdset %s found." % wanted_cache_key
|
2010-08-29 18:46:58 +00:00
|
|
|
#instantiate the cmdset (and catch its errors)
|
|
|
|
|
if callable(cmdsetclass):
|
|
|
|
|
cmdsetclass = cmdsetclass(cmdsetobj)
|
|
|
|
|
return cmdsetclass
|
|
|
|
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
errstring = "Error loading cmdset: Couldn't import module '%s'."
|
|
|
|
|
errstring = errstring % modulepath
|
|
|
|
|
raise
|
|
|
|
|
except KeyError:
|
|
|
|
|
errstring = "Error in loading cmdset: No cmdset class '%s' in %s."
|
|
|
|
|
errstring = errstring % (modulepath, classname)
|
|
|
|
|
raise
|
|
|
|
|
except Exception:
|
|
|
|
|
errstring = "\n%s\nCompile/Run error when loading cmdset '%s'."
|
|
|
|
|
errstring = errstring % (traceback.format_exc(), python_path)
|
|
|
|
|
raise
|
|
|
|
|
except Exception:
|
|
|
|
|
if errstring and not no_logging:
|
2010-09-19 06:57:08 +00:00
|
|
|
print errstring
|
2010-08-29 18:46:58 +00:00
|
|
|
logger.log_trace()
|
|
|
|
|
if emit_to_obj:
|
|
|
|
|
emit_to_obj.msg(errstring)
|
2010-09-05 16:35:39 +00:00
|
|
|
raise # have to raise, or we will not see any errors in some situations!
|
2010-08-29 18:46:58 +00:00
|
|
|
|
|
|
|
|
# classes
|
|
|
|
|
|
|
|
|
|
class CmdSetHandler(object):
|
|
|
|
|
"""
|
|
|
|
|
The CmdSetHandler is always stored on an object, supplied as the 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, outside_access=True):
|
|
|
|
|
"""
|
|
|
|
|
This method is called whenever an object is recreated.
|
|
|
|
|
|
|
|
|
|
obj - this is a reference to the game object this handler
|
|
|
|
|
belongs to.
|
|
|
|
|
outside_access - if false, the cmdparser will only retrieve
|
|
|
|
|
this cmdset when it is its obj itself that is calling for it.
|
|
|
|
|
(this is is good to use for player objects, since they
|
|
|
|
|
should not have access to the cmdsets of other player
|
|
|
|
|
objects).
|
|
|
|
|
"""
|
|
|
|
|
self.obj = obj
|
|
|
|
|
# if false, only the object itself may use this handler
|
|
|
|
|
# (this should be set especially by character objects)
|
|
|
|
|
self.outside_access = outside_access
|
|
|
|
|
|
|
|
|
|
# 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 = [CmdSet(cmdsetobj=self.obj, key="Empty")]
|
|
|
|
|
# this tracks which mergetypes are actually in play in the stack
|
|
|
|
|
self.mergetype_stack = ["Union"]
|
|
|
|
|
self.update()
|
2010-10-11 17:22:39 +00:00
|
|
|
|
|
|
|
|
#print "cmdsethandler init. id:%s, obj:%s, cmdsetstack:%s " % (id(self), self.obj.key, [cmdset.key for cmdset in self.cmdset_stack])
|
2010-08-29 18:46:58 +00:00
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
"Display current commands"
|
|
|
|
|
|
|
|
|
|
string = ""
|
|
|
|
|
if len(self.cmdset_stack) > 1:
|
|
|
|
|
# We have more than one cmdset in stack; list them all
|
|
|
|
|
num = 0
|
|
|
|
|
#print self.cmdset_stack, self.mergetype_stack
|
|
|
|
|
for snum, cmdset in enumerate(self.cmdset_stack):
|
|
|
|
|
num = snum
|
|
|
|
|
mergetype = self.mergetype_stack[snum]
|
|
|
|
|
if mergetype != cmdset.mergetype:
|
|
|
|
|
mergetype = "%s^" % (mergetype)
|
|
|
|
|
string += "\n %i: <%s (%s, prio %i)>: %s" % \
|
|
|
|
|
(snum, cmdset.key, mergetype,
|
|
|
|
|
cmdset.priority, cmdset)
|
|
|
|
|
string += "\n (combining %i cmdsets):" % (num+1)
|
|
|
|
|
else:
|
|
|
|
|
string += "\n "
|
|
|
|
|
|
|
|
|
|
# Display the currently active cmdset
|
|
|
|
|
mergetype = self.mergetype_stack[-1]
|
|
|
|
|
if mergetype != self.current.mergetype:
|
|
|
|
|
merged_on = self.cmdset_stack[-2].key
|
|
|
|
|
mergetype = "custom %s on %s" % (mergetype, merged_on)
|
|
|
|
|
string += " <%s (%s)> %s" % (self.current.key,
|
|
|
|
|
mergetype, self.current)
|
|
|
|
|
return string.strip()
|
|
|
|
|
|
|
|
|
|
def update(self):
|
|
|
|
|
"""
|
|
|
|
|
Re-adds all sets in the handler to have an updated
|
|
|
|
|
current set.
|
|
|
|
|
"""
|
|
|
|
|
updated = None
|
|
|
|
|
self.mergetype_stack = []
|
|
|
|
|
for cmdset in self.cmdset_stack:
|
|
|
|
|
try:
|
|
|
|
|
# for cmdset's '+' operator, order matters.
|
|
|
|
|
updated = cmdset + updated
|
|
|
|
|
except TypeError:
|
|
|
|
|
continue
|
|
|
|
|
self.mergetype_stack.append(updated.actual_mergetype)
|
|
|
|
|
self.current = updated
|
|
|
|
|
|
|
|
|
|
def import_cmdset(self, cmdset_path, emit_to_obj=None):
|
|
|
|
|
"""
|
|
|
|
|
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 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. no script
|
|
|
|
|
will be added to add the cmdset every server start/login).
|
|
|
|
|
|
|
|
|
|
cmdset - can be a cmdset object or the python path to
|
|
|
|
|
such an object.
|
|
|
|
|
emit_to_obj - an object to receive error messages.
|
|
|
|
|
permanent - create a script to automatically add the cmdset
|
|
|
|
|
every time the server starts/the object logins.
|
|
|
|
|
|
|
|
|
|
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 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, emit_to_obj)
|
|
|
|
|
if cmdset:
|
|
|
|
|
self.cmdset_stack.append(cmdset)
|
|
|
|
|
self.update()
|
|
|
|
|
if permanent:
|
|
|
|
|
# create a script to automatically add this cmdset at
|
|
|
|
|
# startup. We don't start it here since the cmdset was
|
|
|
|
|
# already added above.
|
|
|
|
|
try:
|
|
|
|
|
cmdset = "%s.%s" % (cmdset.__module__, cmdset.__name__)
|
|
|
|
|
except Exception:
|
|
|
|
|
logger.log_trace()
|
|
|
|
|
return
|
|
|
|
|
script = create.create_script(AddCmdSet)
|
|
|
|
|
script.db.cmdset = cmdset
|
|
|
|
|
script.db.add_default = False
|
|
|
|
|
self.obj.scripts.add(script, autostart=False)
|
|
|
|
|
|
|
|
|
|
def add_default(self, cmdset, emit_to_obj=None, permanent=False):
|
|
|
|
|
"""
|
|
|
|
|
Add a new default cmdset. If an old default existed,
|
|
|
|
|
it is replaced. If permanent is set, a script will be created to
|
|
|
|
|
add the cmdset to the object.
|
|
|
|
|
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 - create a script that assigns this script every
|
|
|
|
|
startup/login.
|
|
|
|
|
See also the notes for self.add(), which applies here too.
|
|
|
|
|
"""
|
|
|
|
|
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, emit_to_obj)
|
|
|
|
|
if cmdset:
|
|
|
|
|
self.cmdset_stack[0] = cmdset
|
|
|
|
|
self.mergetype_stack[0] = cmdset.mergetype
|
|
|
|
|
self.update()
|
|
|
|
|
#print "add_default:", permanent
|
|
|
|
|
if permanent:
|
|
|
|
|
# create a script to automatically add this cmdset at
|
|
|
|
|
# startup. We don't start it here since the cmdset was
|
|
|
|
|
# already added above.
|
|
|
|
|
try:
|
|
|
|
|
cmdset = "%s.%s" % (cmdset.__module__, cmdset.__class__.__name__)
|
|
|
|
|
except Exception:
|
|
|
|
|
#print traceback.format_exc()
|
|
|
|
|
logger.log_trace()
|
|
|
|
|
return
|
|
|
|
|
#print "cmdset to add:", cmdset
|
|
|
|
|
script = create.create_script(AddCmdSet)
|
|
|
|
|
script.db.cmdset = cmdset
|
|
|
|
|
script.db.add_default = True
|
|
|
|
|
self.obj.scripts.add(script, key="add_default_cmdset", autostart=False)
|
|
|
|
|
|
|
|
|
|
def delete(self, key_or_class=None):
|
|
|
|
|
"""
|
|
|
|
|
Remove a cmdset from the handler. If a key is supplied,
|
|
|
|
|
it attempts to remove this. 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
|
2010-09-05 18:20:39 +00:00
|
|
|
removed - remove it explicitly with delete_default.
|
2010-08-29 18:46:58 +00:00
|
|
|
|
|
|
|
|
key_or_class - a specific cmdset key or a cmdset class (in
|
|
|
|
|
the latter case, *all* cmdsets of this class
|
|
|
|
|
will be removed from handler!)
|
|
|
|
|
"""
|
|
|
|
|
if len(self.cmdset_stack) < 2:
|
|
|
|
|
# don't allow deleting default cmdsets here.
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not key_or_class:
|
|
|
|
|
# remove the last one in the stack (except the default position)
|
|
|
|
|
self.cmdset_stack.pop()
|
|
|
|
|
else:
|
|
|
|
|
# argument key is given, is it a key or a class?
|
|
|
|
|
|
|
|
|
|
default_cmdset = self.cmdset_stack[0]
|
|
|
|
|
|
|
|
|
|
if callable(key_or_class) and hasattr(key_or_class, '__name__'):
|
|
|
|
|
# this is a callable with __name__ - we assume it's a class
|
|
|
|
|
self.cmdset_stack = [cmdset for cmdset in self.cmdset_stack[1:]
|
|
|
|
|
if cmdset.__class__.__name__ != key_or_class.__name__]
|
|
|
|
|
else:
|
|
|
|
|
# try it as a string
|
|
|
|
|
self.cmdset_stack = [cmdset for cmdset in self.cmdset_stack[1:]
|
|
|
|
|
if cmdset.key != key_or_class]
|
|
|
|
|
|
|
|
|
|
self.cmdset_stack.insert(0, default_cmdset)
|
|
|
|
|
|
|
|
|
|
# re-sync the cmdsethandler.
|
|
|
|
|
self.update()
|
|
|
|
|
|
2010-09-05 18:20:39 +00:00
|
|
|
def delete_default(self):
|
|
|
|
|
"This explicitly deletes the default cmdset. It's the only command that can."
|
|
|
|
|
self.cmdset_stack[0] = CmdSet(cmdsetobj=self.obj, key="Empty")
|
|
|
|
|
self.update()
|
|
|
|
|
|
2010-08-29 18:46:58 +00:00
|
|
|
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]
|
|
|
|
|
self.update()
|