mirror of
https://github.com/evennia/evennia.git
synced 2026-03-19 22:36:31 +01:00
414 lines
17 KiB
Python
414 lines
17 KiB
Python
"""
|
|
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.
|
|
"""
|
|
|
|
import copy
|
|
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
|
|
# pre-store properties to duplicate straight off
|
|
to_duplicate = ("key", "cmdsetobj", "no_exits", "no_objs", "no_channels", "permanent",
|
|
"mergetype", "priority", "duplicates")
|
|
|
|
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 player but may also be a
|
|
regular object.
|
|
"""
|
|
if key:
|
|
self.key = key
|
|
self.commands = []
|
|
self.actual_mergetype = self.mergetype
|
|
self.cmdsetobj = cmdsetobj
|
|
# initialize system
|
|
self.at_cmdset_creation()
|
|
|
|
# Priority-sensitive merge operations for cmdsets
|
|
|
|
def _union(self, cmdset_a, cmdset_b, duplicates=False):
|
|
"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 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, duplicates=False):
|
|
"C = A (intersect) B. A is assumed higher priority"
|
|
cmdset_c = cmdset_a._duplicate()
|
|
if 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, cmdset_c):
|
|
"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, cmdset_c):
|
|
"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()
|
|
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'
|
|
"""
|
|
return othercmd in self.commands
|
|
|
|
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, cmdset_b.duplicates)
|
|
elif mergetype == "Replace":
|
|
cmdset_c = self._replace(self, cmdset_b, cmdset_b.duplicates)
|
|
elif mergetype == "Remove":
|
|
cmdset_c = self._remove(self, cmdset_b, cmdset_b.duplicates)
|
|
else: # Union
|
|
cmdset_c = self._union(self, cmdset_b, cmdset_b.duplicates)
|
|
cmdset_c.no_channels = self.no_channels
|
|
cmdset_c.no_exits = self.no_exits
|
|
cmdset_c.no_objs = self.no_objs
|
|
|
|
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, self.duplicates)
|
|
elif mergetype == "Replace":
|
|
cmdset_c = self._replace(cmdset_b, self, self.duplicates)
|
|
elif mergetype == "Remove":
|
|
cmdset_c = self._remove(self, cmdset_b, self.duplicates)
|
|
else: # Union
|
|
cmdset_c = self._union(cmdset_b, self, self.duplicates)
|
|
cmdset_c.no_channels = cmdset_b.no_channels
|
|
cmdset_c.no_exits = cmdset_b.no_exits
|
|
cmdset_c.no_objs = cmdset_b.no_objs
|
|
|
|
# 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, e:
|
|
string = "Adding cmdset %(cmd)s to %(class)s lead to an infinite loop. When adding a cmdset to another, "
|
|
string += "make sure they are not themself cyclically added to 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)]
|
|
for cmd in cmds:
|
|
# add all commands
|
|
if not hasattr(cmd, 'obj'):
|
|
cmd.obj = self.cmdsetobj
|
|
try:
|
|
ic = self.commands.index(cmd)
|
|
self.commands[ic] = cmd # replace
|
|
except ValueError:
|
|
self.commands.append(cmd)
|
|
# extra run to make sure to avoid doublets
|
|
self.commands = list(set(self.commands))
|
|
#print "In cmdset.add(cmd):", self.key, 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 [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
|