mirror of
https://github.com/evennia/evennia.git
synced 2026-03-17 13:26:30 +01:00
464 lines
19 KiB
Python
464 lines
19 KiB
Python
"""
|
|
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 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 = {}
|
|
|
|
|
|
class _ErrorCmdSet(CmdSet):
|
|
"This is a special cmdset used to report errors"
|
|
key = "_CMDSET_ERROR"
|
|
errmessage = "Error when loading cmdset."
|
|
|
|
|
|
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.
|
|
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.
|
|
"""
|
|
|
|
try:
|
|
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:
|
|
errstring = _("Error loading cmdset: Couldn't import module '%s'.")
|
|
errstring = errstring % modulepath
|
|
raise
|
|
except KeyError:
|
|
errstring = _("Error in loading cmdset: No cmdset class '%(classname)s' in %(modulepath)s.")
|
|
errstring = errstring % {"classname": classname,
|
|
"modulepath": modulepath}
|
|
raise
|
|
except Exception:
|
|
errstring = _("Compile/Run error when loading cmdset '%s'. Error was logged.")
|
|
errstring = errstring % (python_path)
|
|
raise
|
|
except Exception:
|
|
# returning an empty error cmdset
|
|
if not no_logging:
|
|
logger.log_trace(errstring)
|
|
if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"):
|
|
object.__getattribute__(emit_to_obj, "msg")(errstring)
|
|
err_cmdset = _ErrorCmdSet()
|
|
err_cmdset.errmessage = errstring
|
|
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):
|
|
"""
|
|
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 = [CmdSet(cmdsetobj=self.obj, key="Empty")]
|
|
# 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 = [""]
|
|
|
|
#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 = [CmdSet(cmdsetobj=self.obj, key="Empty")]
|
|
elif path:
|
|
cmdset = self._import_cmdset(path)
|
|
if cmdset:
|
|
cmdset.permanent = cmdset.key != '_ERROR_CMDSET'
|
|
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 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 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] = CmdSet(cmdsetobj=self.obj, key="Empty")
|
|
else:
|
|
self.cmdset_stack = [CmdSet(cmdsetobj=self.obj, key="Empty")]
|
|
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":
|
|
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()
|