Source code for evennia.commands.default.building

"""
Building and world design commands
"""
import re
from django.conf import settings
from django.db.models import Q, Min, Max
from evennia.objects.models import ObjectDB
from evennia.locks.lockhandler import LockException
from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.utils import create, utils, search, logger
from evennia.utils.utils import (
    inherits_from,
    class_from_module,
    get_all_typeclasses,
    variable_from_module,
    dbref,
    interactive,
    list_to_string,
    display_len,
)
from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
from evennia.utils.ansi import raw as ansi_raw
from evennia.utils.inlinefuncs import raw as inlinefunc_raw

COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)

# limit symbol import for API
__all__ = (
    "ObjManipCommand",
    "CmdSetObjAlias",
    "CmdCopy",
    "CmdCpAttr",
    "CmdMvAttr",
    "CmdCreate",
    "CmdDesc",
    "CmdDestroy",
    "CmdDig",
    "CmdTunnel",
    "CmdLink",
    "CmdUnLink",
    "CmdSetHome",
    "CmdListCmdSets",
    "CmdName",
    "CmdOpen",
    "CmdSetAttribute",
    "CmdTypeclass",
    "CmdWipe",
    "CmdLock",
    "CmdExamine",
    "CmdFind",
    "CmdTeleport",
    "CmdScript",
    "CmdTag",
    "CmdSpawn",
)

# used by set
from ast import literal_eval as _LITERAL_EVAL

LIST_APPEND_CHAR = "+"

# used by find
CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
ROOM_TYPECLASS = settings.BASE_ROOM_TYPECLASS
EXIT_TYPECLASS = settings.BASE_EXIT_TYPECLASS
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH

_PROTOTYPE_PARENTS = None


[docs]class ObjManipCommand(COMMAND_DEFAULT_CLASS): """ This is a parent class for some of the defining objmanip commands since they tend to have some more variables to define new objects. Each object definition can have several components. First is always a name, followed by an optional alias list and finally an some optional data, such as a typeclass or a location. A comma ',' separates different objects. Like this: name1;alias;alias;alias:option, name2;alias;alias ... Spaces between all components are stripped. A second situation is attribute manipulation. Such commands are simpler and offer combinations objname/attr/attr/attr, objname/attr, ... """ # OBS - this is just a parent - it's not intended to actually be # included in a commandset on its own!
[docs] def parse(self): """ We need to expand the default parsing to get all the cases, see the module doc. """ # get all the normal parsing done (switches etc) super().parse() obj_defs = ([], []) # stores left- and right-hand side of '=' obj_attrs = ([], []) # " for iside, arglist in enumerate((self.lhslist, self.rhslist)): # lhslist/rhslist is already split by ',' at this point for objdef in arglist: aliases, option, attrs = [], None, [] if ":" in objdef: objdef, option = [part.strip() for part in objdef.rsplit(":", 1)] if ";" in objdef: objdef, aliases = [part.strip() for part in objdef.split(";", 1)] aliases = [alias.strip() for alias in aliases.split(";") if alias.strip()] if "/" in objdef: objdef, attrs = [part.strip() for part in objdef.split("/", 1)] attrs = [part.strip().lower() for part in attrs.split("/") if part.strip()] # store data obj_defs[iside].append({"name": objdef, "option": option, "aliases": aliases}) obj_attrs[iside].append({"name": objdef, "attrs": attrs}) # store for future access self.lhs_objs = obj_defs[0] self.rhs_objs = obj_defs[1] self.lhs_objattr = obj_attrs[0] self.rhs_objattr = obj_attrs[1]
[docs]class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): """ adding permanent aliases for object Usage: alias <obj> [= [alias[,alias,alias,...]]] alias <obj> = alias/category <obj> = [alias[,alias,...]:<category> Switches: category - requires ending input with :category, to store the given aliases with the given category. Assigns aliases to an object so it can be referenced by more than one name. Assign empty to remove all aliases from object. If assigning a category, all aliases given will be using this category. Observe that this is not the same thing as personal aliases created with the 'nick' command! Aliases set with alias are changing the object in question, making those aliases usable by everyone. """ key = "alias" aliases = "setobjalias" switch_options = ("category",) locks = "cmd:perm(setobjalias) or perm(Builder)" help_category = "Building"
[docs] def func(self): """Set the aliases.""" caller = self.caller if not self.lhs: string = "Usage: alias <obj> [= [alias[,alias ...]]]" self.caller.msg(string) return objname = self.lhs # Find the object to receive aliases obj = caller.search(objname) if not obj: return if self.rhs is None: # no =, so we just list aliases on object. aliases = obj.aliases.all(return_key_and_category=True) if aliases: caller.msg( "Aliases for %s: %s" % ( obj.get_display_name(caller), ", ".join( "'%s'%s" % (alias, "" if category is None else "[category:'%s']" % category) for (alias, category) in aliases ), ) ) else: caller.msg("No aliases exist for '%s'." % obj.get_display_name(caller)) return if not (obj.access(caller, "control") or obj.access(caller, "edit")): caller.msg("You don't have permission to do that.") return if not self.rhs: # we have given an empty =, so delete aliases old_aliases = obj.aliases.all() if old_aliases: caller.msg( "Cleared aliases from %s: %s" % (obj.get_display_name(caller), ", ".join(old_aliases)) ) obj.aliases.clear() else: caller.msg("No aliases to clear.") return category = None if "category" in self.switches: if ":" in self.rhs: rhs, category = self.rhs.rsplit(":", 1) category = category.strip() else: caller.msg( "If specifying the /category switch, the category must be given " "as :category at the end." ) else: rhs = self.rhs # merge the old and new aliases (if any) old_aliases = obj.aliases.get(category=category, return_list=True) new_aliases = [alias.strip().lower() for alias in rhs.split(",") if alias.strip()] # make the aliases only appear once old_aliases.extend(new_aliases) aliases = list(set(old_aliases)) # save back to object. obj.aliases.add(aliases, category=category) # we need to trigger this here, since this will force # (default) Exits to rebuild their Exit commands with the new # aliases obj.at_cmdset_get(force_init=True) # report all aliases on the object caller.msg( "Alias(es) for '%s' set to '%s'%s." % ( obj.get_display_name(caller), str(obj.aliases), " (category: '%s')" % category if category else "", ) )
[docs]class CmdCopy(ObjManipCommand): """ copy an object and its properties Usage: copy <original obj> [= <new_name>][;alias;alias..] [:<new_location>] [,<new_name2> ...] Create one or more copies of an object. If you don't supply any targets, one exact copy of the original object will be created with the name *_copy. """ key = "copy" locks = "cmd:perm(copy) or perm(Builder)" help_category = "Building"
[docs] def func(self): """Uses ObjManipCommand.parse()""" caller = self.caller args = self.args if not args: caller.msg( "Usage: copy <obj> [=<new_name>[;alias;alias..]]" "[:<new_location>] [, <new_name2>...]" ) return if not self.rhs: # this has no target =, so an identical new object is created. from_obj_name = self.args from_obj = caller.search(from_obj_name) if not from_obj: return to_obj_name = "%s_copy" % from_obj_name to_obj_aliases = ["%s_copy" % alias for alias in from_obj.aliases.all()] copiedobj = ObjectDB.objects.copy_object( from_obj, new_key=to_obj_name, new_aliases=to_obj_aliases ) if copiedobj: string = "Identical copy of %s, named '%s' was created." % ( from_obj_name, to_obj_name, ) else: string = "There was an error copying %s." else: # we have specified =. This might mean many object targets from_obj_name = self.lhs_objs[0]["name"] from_obj = caller.search(from_obj_name) if not from_obj: return for objdef in self.rhs_objs: # loop through all possible copy-to targets to_obj_name = objdef["name"] to_obj_aliases = objdef["aliases"] to_obj_location = objdef["option"] if to_obj_location: to_obj_location = caller.search(to_obj_location, global_search=True) if not to_obj_location: return copiedobj = ObjectDB.objects.copy_object( from_obj, new_key=to_obj_name, new_location=to_obj_location, new_aliases=to_obj_aliases, ) if copiedobj: string = "Copied %s to '%s' (aliases: %s)." % ( from_obj_name, to_obj_name, to_obj_aliases, ) else: string = "There was an error copying %s to '%s'." % (from_obj_name, to_obj_name) # we are done, echo to user caller.msg(string)
[docs]class CmdCpAttr(ObjManipCommand): """ copy attributes between objects Usage: cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...] cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...] cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...] cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...] Switches: move - delete the attribute from the source object after copying. Example: cpattr coolness = Anna/chillout, Anna/nicety, Tom/nicety -> copies the coolness attribute (defined on yourself), to attributes on Anna and Tom. Copy the attribute one object to one or more attributes on another object. If you don't supply a source object, yourself is used. """ key = "cpattr" switch_options = ("move",) locks = "cmd:perm(cpattr) or perm(Builder)" help_category = "Building"
[docs] def check_from_attr(self, obj, attr, clear=False): """ Hook for overriding on subclassed commands. Checks to make sure a caller can copy the attr from the object in question. If not, return a false value and the command will abort. An error message should be provided by this function. If clear is True, user is attempting to move the attribute. """ return True
[docs] def check_to_attr(self, obj, attr): """ Hook for overriding on subclassed commands. Checks to make sure a caller can write to the specified attribute on the specified object. If not, return a false value and the attribute will be skipped. An error message should be provided by this function. """ return True
[docs] def check_has_attr(self, obj, attr): """ Hook for overriding on subclassed commands. Do any preprocessing required and verify an object has an attribute. """ if not obj.attributes.has(attr): self.caller.msg("%s doesn't have an attribute %s." % (obj.name, attr)) return False return True
[docs] def get_attr(self, obj, attr): """ Hook for overriding on subclassed commands. Do any preprocessing required and get the attribute from the object. """ return obj.attributes.get(attr)
[docs] def func(self): """ Do the copying. """ caller = self.caller if not self.rhs: string = """Usage: cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...] cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...] cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...] cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]""" caller.msg(string) return lhs_objattr = self.lhs_objattr to_objs = self.rhs_objattr from_obj_name = lhs_objattr[0]["name"] from_obj_attrs = lhs_objattr[0]["attrs"] if not from_obj_attrs: # this means the from_obj_name is actually an attribute # name on self. from_obj_attrs = [from_obj_name] from_obj = self.caller else: from_obj = caller.search(from_obj_name) if not from_obj or not to_objs: caller.msg("You have to supply both source object and target(s).") return # copy to all to_obj:ects if "move" in self.switches: clear = True else: clear = False if not self.check_from_attr(from_obj, from_obj_attrs[0], clear=clear): return for attr in from_obj_attrs: if not self.check_has_attr(from_obj, attr): return if (len(from_obj_attrs) != len(set(from_obj_attrs))) and clear: self.caller.msg("|RCannot have duplicate source names when moving!") return result = [] for to_obj in to_objs: to_obj_name = to_obj["name"] to_obj_attrs = to_obj["attrs"] to_obj = caller.search(to_obj_name) if not to_obj: result.append("\nCould not find object '%s'" % to_obj_name) continue for inum, from_attr in enumerate(from_obj_attrs): try: to_attr = to_obj_attrs[inum] except IndexError: # if there are too few attributes given # on the to_obj, we copy the original name instead. to_attr = from_attr if not self.check_to_attr(to_obj, to_attr): continue value = self.get_attr(from_obj, from_attr) to_obj.attributes.add(to_attr, value) if clear and not (from_obj == to_obj and from_attr == to_attr): from_obj.attributes.remove(from_attr) result.append( "\nMoved %s.%s -> %s.%s. (value: %s)" % (from_obj.name, from_attr, to_obj_name, to_attr, repr(value)) ) else: result.append( "\nCopied %s.%s -> %s.%s. (value: %s)" % (from_obj.name, from_attr, to_obj_name, to_attr, repr(value)) ) caller.msg("".join(result))
[docs]class CmdMvAttr(ObjManipCommand): """ move attributes between objects Usage: mvattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...] mvattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...] mvattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...] mvattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...] Switches: copy - Don't delete the original after moving. Move an attribute from one object to one or more attributes on another object. If you don't supply a source object, yourself is used. """ key = "mvattr" switch_options = ("copy",) locks = "cmd:perm(mvattr) or perm(Builder)" help_category = "Building"
[docs] def func(self): """ Do the moving """ if not self.rhs: string = """Usage: mvattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...] mvattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...] mvattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...] mvattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]""" self.caller.msg(string) return # simply use cpattr for all the functionality if "copy" in self.switches: self.execute_cmd("cpattr %s" % self.args) else: self.execute_cmd("cpattr/move %s" % self.args)
[docs]class CmdCreate(ObjManipCommand): """ create new objects Usage: create[/drop] <objname>[;alias;alias...][:typeclass], <objname>... switch: drop - automatically drop the new object into your current location (this is not echoed). This also sets the new object's home to the current location rather than to you. Creates one or more new objects. If typeclass is given, the object is created as a child of this typeclass. The typeclass script is assumed to be located under types/ and any further directory structure is given in Python notation. So if you have a correct typeclass 'RedButton' defined in types/examples/red_button.py, you could create a new object of this type like this: create/drop button;red : examples.red_button.RedButton """ key = "create" switch_options = ("drop",) locks = "cmd:perm(create) or perm(Builder)" help_category = "Building" # lockstring of newly created objects, for easy overloading. # Will be formatted with the {id} of the creating object. new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)"
[docs] def func(self): """ Creates the object. """ caller = self.caller if not self.args: string = "Usage: create[/drop] <newname>[;alias;alias...] [:typeclass.path]" caller.msg(string) return # create the objects for objdef in self.lhs_objs: string = "" name = objdef["name"] aliases = objdef["aliases"] typeclass = objdef["option"] # create object (if not a valid typeclass, the default # object typeclass will automatically be used) lockstring = self.new_obj_lockstring.format(id=caller.id) obj = create.create_object( typeclass, name, caller, home=caller, aliases=aliases, locks=lockstring, report_to=caller, ) if not obj: continue if aliases: string = "You create a new %s: %s (aliases: %s)." string = string % (obj.typename, obj.name, ", ".join(aliases)) else: string = "You create a new %s: %s." string = string % (obj.typename, obj.name) # set a default desc if not obj.db.desc: obj.db.desc = "You see nothing special." if "drop" in self.switches: if caller.location: obj.home = caller.location obj.move_to(caller.location, quiet=True) if string: caller.msg(string)
def _desc_load(caller): return caller.db.evmenu_target.db.desc or "" def _desc_save(caller, buf): """ Save line buffer to the desc prop. This should return True if successful and also report its status to the user. """ caller.db.evmenu_target.db.desc = buf caller.msg("Saved.") return True def _desc_quit(caller): caller.attributes.remove("evmenu_target") caller.msg("Exited editor.")
[docs]class CmdDesc(COMMAND_DEFAULT_CLASS): """ describe an object or the current room. Usage: desc [<obj> =] <description> Switches: edit - Open up a line editor for more advanced editing. Sets the "desc" attribute on an object. If an object is not given, describe the current room. """ key = "desc" aliases = "describe" switch_options = ("edit",) locks = "cmd:perm(desc) or perm(Builder)" help_category = "Building"
[docs] def edit_handler(self): if self.rhs: self.msg("|rYou may specify a value, or use the edit switch, " "but not both.|n") return if self.args: obj = self.caller.search(self.args) else: obj = self.caller.location or self.msg("|rYou can't describe oblivion.|n") if not obj: return if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")): self.caller.msg("You don't have permission to edit the description of %s." % obj.key) self.caller.db.evmenu_target = obj # launch the editor EvEditor( self.caller, loadfunc=_desc_load, savefunc=_desc_save, quitfunc=_desc_quit, key="desc", persistent=True, ) return
[docs] def func(self): """Define command""" caller = self.caller if not self.args and "edit" not in self.switches: caller.msg("Usage: desc [<obj> =] <description>") return if "edit" in self.switches: self.edit_handler() return if "=" in self.args: # We have an = obj = caller.search(self.lhs) if not obj: return desc = self.rhs or "" else: obj = caller.location or self.msg("|rYou can't describe oblivion.|n") if not obj: return desc = self.args if obj.access(self.caller, "control") or obj.access(self.caller, "edit"): obj.db.desc = desc caller.msg("The description was set on %s." % obj.get_display_name(caller)) else: caller.msg("You don't have permission to edit the description of %s." % obj.key)
[docs]class CmdDestroy(COMMAND_DEFAULT_CLASS): """ permanently delete objects Usage: destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...] Switches: override - The destroy command will usually avoid accidentally destroying account objects. This switch overrides this safety. force - destroy without confirmation. Examples: destroy house, roof, door, 44-78 destroy 5-10, flower, 45 destroy/force north Destroys one or many objects. If dbrefs are used, a range to delete can be given, e.g. 4-10. Also the end points will be deleted. This command displays a confirmation before destroying, to make sure of your choice. You can specify the /force switch to bypass this confirmation. """ key = "destroy" aliases = ["delete", "del"] switch_options = ("override", "force") locks = "cmd:perm(destroy) or perm(Builder)" help_category = "Building" confirm = True # set to False to always bypass confirmation default_confirm = "yes" # what to assume if just pressing enter (yes/no)
[docs] def func(self): """Implements the command.""" caller = self.caller delete = True if not self.args or not self.lhslist: caller.msg("Usage: destroy[/switches] [obj, obj2, obj3, [dbref-dbref],...]") delete = False def delobj(obj): # helper function for deleting a single object string = "" if not obj.pk: string = "\nObject %s was already deleted." % obj.db_key else: objname = obj.name if not (obj.access(caller, "control") or obj.access(caller, "delete")): return "\nYou don't have permission to delete %s." % objname if obj.account and "override" not in self.switches: return ( "\nObject %s is controlled by an active account. Use /override to delete anyway." % objname ) if obj.dbid == int(settings.DEFAULT_HOME.lstrip("#")): return ( "\nYou are trying to delete |c%s|n, which is set as DEFAULT_HOME. " "Re-point settings.DEFAULT_HOME to another " "object before continuing." % objname ) had_exits = hasattr(obj, "exits") and obj.exits had_objs = hasattr(obj, "contents") and any( obj for obj in obj.contents if not (hasattr(obj, "exits") and obj not in obj.exits) ) # do the deletion okay = obj.delete() if not okay: string += ( "\nERROR: %s not deleted, probably because delete() returned False." % objname ) else: string += "\n%s was destroyed." % objname if had_exits: string += " Exits to and from %s were destroyed as well." % objname if had_objs: string += " Objects inside %s were moved to their homes." % objname return string objs = [] for objname in self.lhslist: if not delete: continue if "-" in objname: # might be a range of dbrefs dmin, dmax = [utils.dbref(part, reqhash=False) for part in objname.split("-", 1)] if dmin and dmax: for dbref in range(int(dmin), int(dmax + 1)): obj = caller.search("#" + str(dbref)) if obj: objs.append(obj) continue else: obj = caller.search(objname) else: obj = caller.search(objname) if obj is None: self.caller.msg( " (Objects to destroy must either be local or specified with a unique #dbref.)" ) elif obj not in objs: objs.append(obj) if objs and ("force" not in self.switches and type(self).confirm): confirm = "Are you sure you want to destroy " if len(objs) == 1: confirm += objs[0].get_display_name(caller) elif len(objs) < 5: confirm += ", ".join([obj.get_display_name(caller) for obj in objs]) else: confirm += ", ".join(["#{}".format(obj.id) for obj in objs]) confirm += " [yes]/no?" if self.default_confirm == "yes" else " yes/[no]" answer = "" answer = yield (confirm) answer = self.default_confirm if answer == "" else answer if answer and answer not in ("yes", "y", "no", "n"): caller.msg( "Canceled: Either accept the default by pressing return or specify yes/no." ) delete = False elif answer.strip().lower() in ("n", "no"): caller.msg("Canceled: No object was destroyed.") delete = False if delete: results = [] for obj in objs: results.append(delobj(obj)) if results: caller.msg("".join(results).strip())
[docs]class CmdDig(ObjManipCommand): """ build new rooms and connect them to the current location Usage: dig[/switches] <roomname>[;alias;alias...][:typeclass] [= <exit_to_there>[;alias][:typeclass]] [, <exit_to_here>[;alias][:typeclass]] Switches: tel or teleport - move yourself to the new room Examples: dig kitchen = north;n, south;s dig house:myrooms.MyHouseTypeclass dig sheer cliff;cliff;sheer = climb up, climb down This command is a convenient way to build rooms quickly; it creates the new room and you can optionally set up exits back and forth between your current room and the new one. You can add as many aliases as you like to the name of the room and the exits in question; an example would be 'north;no;n'. """ key = "dig" switch_options = ("teleport",) locks = "cmd:perm(dig) or perm(Builder)" help_category = "Building" # lockstring of newly created rooms, for easy overloading. # Will be formatted with the {id} of the creating object. new_room_lockstring = ( "control:id({id}) or perm(Admin); " "delete:id({id}) or perm(Admin); " "edit:id({id}) or perm(Admin)" )
[docs] def func(self): """Do the digging. Inherits variables from ObjManipCommand.parse()""" caller = self.caller if not self.lhs: string = "Usage: dig[/teleport] <roomname>[;alias;alias...]" "[:parent] [= <exit_there>" string += "[;alias;alias..][:parent]] " string += "[, <exit_back_here>[;alias;alias..][:parent]]" caller.msg(string) return room = self.lhs_objs[0] if not room["name"]: caller.msg("You must supply a new room name.") return location = caller.location # Create the new room typeclass = room["option"] if not typeclass: typeclass = settings.BASE_ROOM_TYPECLASS # create room new_room = create.create_object( typeclass, room["name"], aliases=room["aliases"], report_to=caller ) lockstring = self.new_room_lockstring.format(id=caller.id) new_room.locks.add(lockstring) alias_string = "" if new_room.aliases.all(): alias_string = " (%s)" % ", ".join(new_room.aliases.all()) room_string = "Created room %s(%s)%s of type %s." % ( new_room, new_room.dbref, alias_string, typeclass, ) # create exit to room exit_to_string = "" exit_back_string = "" if self.rhs_objs: to_exit = self.rhs_objs[0] if not to_exit["name"]: exit_to_string = "\nNo exit created to new room." elif not location: exit_to_string = "\nYou cannot create an exit from a None-location." else: # Build the exit to the new room from the current one typeclass = to_exit["option"] if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS new_to_exit = create.create_object( typeclass, to_exit["name"], location, aliases=to_exit["aliases"], locks=lockstring, destination=new_room, report_to=caller, ) alias_string = "" if new_to_exit.aliases.all(): alias_string = " (%s)" % ", ".join(new_to_exit.aliases.all()) exit_to_string = "\nCreated Exit from %s to %s: %s(%s)%s." exit_to_string = exit_to_string % ( location.name, new_room.name, new_to_exit, new_to_exit.dbref, alias_string, ) # Create exit back from new room if len(self.rhs_objs) > 1: # Building the exit back to the current room back_exit = self.rhs_objs[1] if not back_exit["name"]: exit_back_string = "\nNo back exit created." elif not location: exit_back_string = "\nYou cannot create an exit back to a None-location." else: typeclass = back_exit["option"] if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS new_back_exit = create.create_object( typeclass, back_exit["name"], new_room, aliases=back_exit["aliases"], locks=lockstring, destination=location, report_to=caller, ) alias_string = "" if new_back_exit.aliases.all(): alias_string = " (%s)" % ", ".join(new_back_exit.aliases.all()) exit_back_string = "\nCreated Exit back from %s to %s: %s(%s)%s." exit_back_string = exit_back_string % ( new_room.name, location.name, new_back_exit, new_back_exit.dbref, alias_string, ) caller.msg("%s%s%s" % (room_string, exit_to_string, exit_back_string)) if new_room and "teleport" in self.switches: caller.move_to(new_room)
[docs]class CmdTunnel(COMMAND_DEFAULT_CLASS): """ create new rooms in cardinal directions only Usage: tunnel[/switch] <direction>[:typeclass] [= <roomname>[;alias;alias;...][:typeclass]] Switches: oneway - do not create an exit back to the current location tel - teleport to the newly created room Example: tunnel n tunnel n = house;mike's place;green building This is a simple way to build using pre-defined directions: |wn,ne,e,se,s,sw,w,nw|n (north, northeast etc) |wu,d|n (up and down) |wi,o|n (in and out) The full names (north, in, southwest, etc) will always be put as main name for the exit, using the abbreviation as an alias (so an exit will always be able to be used with both "north" as well as "n" for example). Opposite directions will automatically be created back from the new room unless the /oneway switch is given. For more flexibility and power in creating rooms, use dig. """ key = "tunnel" aliases = ["tun"] switch_options = ("oneway", "tel") locks = "cmd: perm(tunnel) or perm(Builder)" help_category = "Building" # store the direction, full name and its opposite directions = { "n": ("north", "s"), "ne": ("northeast", "sw"), "e": ("east", "w"), "se": ("southeast", "nw"), "s": ("south", "n"), "sw": ("southwest", "ne"), "w": ("west", "e"), "nw": ("northwest", "se"), "u": ("up", "d"), "d": ("down", "u"), "i": ("in", "o"), "o": ("out", "i"), }
[docs] def func(self): """Implements the tunnel command""" if not self.args or not self.lhs: string = ( "Usage: tunnel[/switch] <direction>[:typeclass] [= <roomname>" "[;alias;alias;...][:typeclass]]" ) self.caller.msg(string) return # If we get a typeclass, we need to get just the exitname exitshort = self.lhs.split(":")[0] if exitshort not in self.directions: string = "tunnel can only understand the following directions: %s." % ",".join( sorted(self.directions.keys()) ) string += "\n(use dig for more freedom)" self.caller.msg(string) return # retrieve all input and parse it exitname, backshort = self.directions[exitshort] backname = self.directions[backshort][0] # if we received a typeclass for the exit, add it to the alias(short name) if ":" in self.lhs: # limit to only the first : character exit_typeclass = ":" + self.lhs.split(":", 1)[-1] # exitshort and backshort are the last part of the exit strings, # so we add our typeclass argument after exitshort += exit_typeclass backshort += exit_typeclass roomname = "Some place" if self.rhs: roomname = self.rhs # this may include aliases; that's fine. telswitch = "" if "tel" in self.switches: telswitch = "/teleport" backstring = "" if "oneway" not in self.switches: backstring = ", %s;%s" % (backname, backshort) # build the string we will use to call dig digstring = "dig%s %s = %s;%s%s" % (telswitch, roomname, exitname, exitshort, backstring) self.execute_cmd(digstring)
[docs]class CmdSetHome(CmdLink): """ set an object's home location Usage: sethome <obj> [= <home_location>] sethom <obj> The "home" location is a "safety" location for objects; they will be moved there if their current location ceases to exist. All objects should always have a home location for this reason. It is also a convenient target of the "home" command. If no location is given, just view the object's home location. """ key = "sethome" locks = "cmd:perm(sethome) or perm(Builder)" help_category = "Building"
[docs] def func(self): """implement the command""" if not self.args: string = "Usage: sethome <obj> [= <home_location>]" self.caller.msg(string) return obj = self.caller.search(self.lhs, global_search=True) if not obj: return if not self.rhs: # just view home = obj.home if not home: string = "This object has no home location set!" else: string = "%s's current home is %s(%s)." % (obj, home, home.dbref) else: # set a home location new_home = self.caller.search(self.rhs, global_search=True) if not new_home: return old_home = obj.home obj.home = new_home if old_home: string = "Home location of %s was changed from %s(%s) to %s(%s)." % ( obj, old_home, old_home.dbref, new_home, new_home.dbref, ) else: string = "Home location of %s was set to %s(%s)." % (obj, new_home, new_home.dbref) self.caller.msg(string)
[docs]class CmdListCmdSets(COMMAND_DEFAULT_CLASS): """ list command sets defined on an object Usage: cmdsets <obj> This displays all cmdsets assigned to a user. Defaults to yourself. """ key = "cmdsets" aliases = "listcmsets" locks = "cmd:perm(listcmdsets) or perm(Builder)" help_category = "Building"
[docs] def func(self): """list the cmdsets""" caller = self.caller if self.arglist: obj = caller.search(self.arglist[0]) if not obj: return else: obj = caller string = "%s" % obj.cmdset caller.msg(string)
[docs]class CmdName(ObjManipCommand): """ change the name and/or aliases of an object Usage: name <obj> = <newname>;alias1;alias2 Rename an object to something new. Use *obj to rename an account. """ key = "name" aliases = ["rename"] locks = "cmd:perm(rename) or perm(Builder)" help_category = "Building"
[docs] def func(self): """change the name""" caller = self.caller if not self.args: caller.msg("Usage: name <obj> = <newname>[;alias;alias;...]") return obj = None if self.lhs_objs: objname = self.lhs_objs[0]["name"] if objname.startswith("*"): # account mode obj = caller.account.search(objname.lstrip("*")) if obj: if self.rhs_objs[0]["aliases"]: caller.msg("Accounts can't have aliases.") return newname = self.rhs if not newname: caller.msg("No name defined!") return if not (obj.access(caller, "control") or obj.access(caller, "edit")): caller.msg("You don't have right to edit this account %s." % obj) return obj.username = newname obj.save() caller.msg("Account's name changed to '%s'." % newname) return # object search, also with * obj = caller.search(objname) if not obj: return if self.rhs_objs: newname = self.rhs_objs[0]["name"] aliases = self.rhs_objs[0]["aliases"] else: newname = self.rhs aliases = None if not newname and not aliases: caller.msg("No names or aliases defined!") return if not (obj.access(caller, "control") or obj.access(caller, "edit")): caller.msg("You don't have the right to edit %s." % obj) return # change the name and set aliases: if newname: obj.name = newname astring = "" if aliases: [obj.aliases.add(alias) for alias in aliases] astring = " (%s)" % (", ".join(aliases)) # fix for exits - we need their exit-command to change name too if obj.destination: obj.flush_from_cache(force=True) caller.msg("Object's name changed to '%s'%s." % (newname, astring))
[docs]class CmdOpen(ObjManipCommand): """ open a new exit from the current room Usage: open <new exit>[;alias;alias..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = <destination> Handles the creation of exits. If a destination is given, the exit will point there. The <return exit> argument sets up an exit at the destination leading back to the current room. Destination name can be given both as a #dbref and a name, if that name is globally unique. """ key = "open" locks = "cmd:perm(open) or perm(Builder)" help_category = "Building" new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)" # a custom member method to chug out exits and do checks
[docs] def create_exit(self, exit_name, location, destination, exit_aliases=None, typeclass=None): """ Helper function to avoid code duplication. At this point we know destination is a valid location """ caller = self.caller string = "" # check if this exit object already exists at the location. # we need to ignore errors (so no automatic feedback)since we # have to know the result of the search to decide what to do. exit_obj = caller.search(exit_name, location=location, quiet=True, exact=True) if len(exit_obj) > 1: # give error message and return caller.search(exit_name, location=location, exact=True) return None if exit_obj: exit_obj = exit_obj[0] if not exit_obj.destination: # we are trying to link a non-exit string = "'%s' already exists and is not an exit!\nIf you want to convert it " string += ( "to an exit, you must assign an object to the 'destination' property first." ) caller.msg(string % exit_name) return None # we are re-linking an old exit. old_destination = exit_obj.destination if old_destination: string = "Exit %s already exists." % exit_name if old_destination.id != destination.id: # reroute the old exit. exit_obj.destination = destination if exit_aliases: [exit_obj.aliases.add(alias) for alias in exit_aliases] string += " Rerouted its old destination '%s' to '%s' and changed aliases." % ( old_destination.name, destination.name, ) else: string += " It already points to the correct place." else: # exit does not exist before. Create a new one. lockstring = self.new_obj_lockstring.format(id=caller.id) if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS exit_obj = create.create_object( typeclass, key=exit_name, location=location, aliases=exit_aliases, locks=lockstring, report_to=caller, ) if exit_obj: # storing a destination is what makes it an exit! exit_obj.destination = destination string = ( "" if not exit_aliases else " (aliases: %s)" % (", ".join([str(e) for e in exit_aliases])) ) string = "Created new Exit '%s' from %s to %s%s." % ( exit_name, location.name, destination.name, string, ) else: string = "Error: Exit '%s' not created." % exit_name # emit results caller.msg(string) return exit_obj
[docs] def func(self): """ This is where the processing starts. Uses the ObjManipCommand.parser() for pre-processing as well as the self.create_exit() method. """ caller = self.caller if not self.args or not self.rhs: string = "Usage: open <new exit>[;alias...][:typeclass][,<return exit>[;alias..][:typeclass]]] " string += "= <destination>" caller.msg(string) return # We must have a location to open an exit location = caller.location if not location: caller.msg("You cannot create an exit from a None-location.") return # obtain needed info from cmdline exit_name = self.lhs_objs[0]["name"] exit_aliases = self.lhs_objs[0]["aliases"] exit_typeclass = self.lhs_objs[0]["option"] dest_name = self.rhs # first, check so the destination exists. destination = caller.search(dest_name, global_search=True) if not destination: return # Create exit ok = self.create_exit(exit_name, location, destination, exit_aliases, exit_typeclass) if not ok: # an error; the exit was not created, so we quit. return # Create back exit, if any if len(self.lhs_objs) > 1: back_exit_name = self.lhs_objs[1]["name"] back_exit_aliases = self.lhs_objs[1]["aliases"] back_exit_typeclass = self.lhs_objs[1]["option"] self.create_exit( back_exit_name, destination, location, back_exit_aliases, back_exit_typeclass )
def _convert_from_string(cmd, strobj): """ Converts a single object in *string form* to its equivalent python type. Python earlier than 2.6: Handles floats, ints, and limited nested lists and dicts (can't handle lists in a dict, for example, this is mainly due to the complexity of parsing this rather than any technical difficulty - if there is a need for set-ing such complex structures on the command line we might consider adding it). Python 2.6 and later: Supports all Python structures through literal_eval as long as they are valid Python syntax. If they are not (such as [test, test2], ie without the quotes around the strings), the entire structure will be converted to a string and a warning will be given. We need to convert like this since all data being sent over the telnet connection by the Account is text - but we will want to store it as the "real" python type so we can do convenient comparisons later (e.g. obj.db.value = 2, if value is stored as a string this will always fail). """ # Use literal_eval to parse python structure exactly. try: return _LITERAL_EVAL(strobj) except (SyntaxError, ValueError): # treat as string strobj = utils.to_str(strobj) string = ( '|RNote: name "|r%s|R" was converted to a string. ' "Make sure this is acceptable." % strobj ) cmd.caller.msg(string) return strobj except Exception as err: string = "|RUnknown error in evaluating Attribute: {}".format(err) return string
[docs]class CmdSetAttribute(ObjManipCommand): """ set attribute on an object or account Usage: set <obj>/<attr> = <value> set <obj>/<attr> = set <obj>/<attr> set *<account>/<attr> = <value> Switch: edit: Open the line editor (string values only) script: If we're trying to set an attribute on a script channel: If we're trying to set an attribute on a channel account: If we're trying to set an attribute on an account room: Setting an attribute on a room (global search) exit: Setting an attribute on an exit (global search) char: Setting an attribute on a character (global search) character: Alias for char, as above. Sets attributes on objects. The second example form above clears a previously set attribute while the third form inspects the current value of the attribute (if any). The last one (with the star) is a shortcut for operating on a player Account rather than an Object. The most common data to save with this command are strings and numbers. You can however also set Python primitives such as lists, dictionaries and tuples on objects (this might be important for the functionality of certain custom objects). This is indicated by you starting your value with one of |c'|n, |c"|n, |c(|n, |c[|n or |c{ |n. Once you have stored a Python primitive as noted above, you can include |c[<key>]|n in <attr> to reference nested values in e.g. a list or dict. Remember that if you use Python primitives like this, you must write proper Python syntax too - notably you must include quotes around your strings or you will get an error. """ key = "set" locks = "cmd:perm(set) or perm(Builder)" help_category = "Building" nested_re = re.compile(r"\[.*?\]") not_found = object()
[docs] def check_obj(self, obj): """ This may be overridden by subclasses in case restrictions need to be placed on whether certain objects can have attributes set by certain accounts. This function is expected to display its own error message. Returning False will abort the command. """ return True
[docs] def check_attr(self, obj, attr_name): """ This may be overridden by subclasses in case restrictions need to be placed on what attributes can be set by who beyond the normal lock. This functions is expected to display its own error message. It is run once for every attribute that is checked, blocking only those attributes which are not permitted and letting the others through. """ return attr_name
[docs] def split_nested_attr(self, attr): """ Yields tuples of (possible attr name, nested keys on that attr). For performance, this is biased to the deepest match, but allows compatibility with older attrs that might have been named with `[]`'s. > list(split_nested_attr("nested['asdf'][0]")) [ ('nested', ['asdf', 0]), ("nested['asdf']", [0]), ("nested['asdf'][0]", []), ] """ quotes = "\"'" def clean_key(val): val = val.strip("[]") if val[0] in quotes: return val.strip(quotes) if val[0] == LIST_APPEND_CHAR: # List insert/append syntax return val try: return int(val) except ValueError: return val parts = self.nested_re.findall(attr) base_attr = "" if parts: base_attr = attr[: attr.find(parts[0])] for index, part in enumerate(parts): yield (base_attr, [clean_key(p) for p in parts[index:]]) base_attr += part yield (attr, [])
[docs] def do_nested_lookup(self, value, *keys): result = value for key in keys: try: result = result.__getitem__(key) except (IndexError, KeyError, TypeError): return self.not_found return result
[docs] def view_attr(self, obj, attr): """ Look up the value of an attribute and return a string displaying it. """ nested = False for key, nested_keys in self.split_nested_attr(attr): nested = True if obj.attributes.has(key): val = obj.attributes.get(key) val = self.do_nested_lookup(val, *nested_keys) if val is not self.not_found: return "\nAttribute %s/%s = %s" % (obj.name, attr, val) error = "\n%s has no attribute '%s'." % (obj.name, attr) if nested: error += " (Nested lookups attempted)" return error
[docs] def rm_attr(self, obj, attr): """ Remove an attribute from the object, or a nested data structure, and report back. """ nested = False for key, nested_keys in self.split_nested_attr(attr): nested = True if obj.attributes.has(key): if nested_keys: del_key = nested_keys[-1] val = obj.attributes.get(key) deep = self.do_nested_lookup(val, *nested_keys[:-1]) if deep is not self.not_found: try: del deep[del_key] except (IndexError, KeyError, TypeError): continue return "\nDeleted attribute '%s' (= nested) from %s." % (attr, obj.name) else: exists = obj.attributes.has(key) obj.attributes.remove(attr) return "\nDeleted attribute '%s' (= %s) from %s." % (attr, exists, obj.name) error = "\n%s has no attribute '%s'." % (obj.name, attr) if nested: error += " (Nested lookups attempted)" return error
[docs] def set_attr(self, obj, attr, value): done = False for key, nested_keys in self.split_nested_attr(attr): if obj.attributes.has(key) and nested_keys: acc_key = nested_keys[-1] lookup_value = obj.attributes.get(key) deep = self.do_nested_lookup(lookup_value, *nested_keys[:-1]) if deep is not self.not_found: # To support appending and inserting to lists # a key that starts with LIST_APPEND_CHAR will insert a new item at that # location, and move the other elements down. # Using LIST_APPEND_CHAR alone will append to the list if isinstance(acc_key, str) and acc_key[0] == LIST_APPEND_CHAR: try: if len(acc_key) > 1: where = int(acc_key[1:]) deep.insert(where, value) else: deep.append(value) except (ValueError, AttributeError): pass else: value = lookup_value attr = key done = True break # List magic failed, just use like a key/index try: deep[acc_key] = value except TypeError as err: # Tuples can't be modified return "\n%s - %s" % (err, deep) value = lookup_value attr = key done = True break verb = "Modified" if obj.attributes.has(attr) else "Created" try: if not done: obj.attributes.add(attr, value) return "\n%s attribute %s/%s = %s" % (verb, obj.name, attr, repr(value)) except SyntaxError: # this means literal_eval tried to parse a faulty string return ( "\n|RCritical Python syntax error in your value. Only " "primitive Python structures are allowed.\nYou also " "need to use correct Python syntax. Remember especially " "to put quotes around all strings inside lists and " "dicts.|n" )
[docs] def edit_handler(self, obj, attr): """Activate the line editor""" def load(caller): """Called for the editor to load the buffer""" old_value = obj.attributes.get(attr) if old_value is not None and not isinstance(old_value, str): typ = type(old_value).__name__ self.caller.msg( "|RWARNING! Saving this buffer will overwrite the " "current attribute (of type %s) with a string!|n" % typ ) return str(old_value) return old_value def save(caller, buf): """Called when editor saves its buffer.""" obj.attributes.add(attr, buf) caller.msg("Saved Attribute %s." % attr) # start the editor EvEditor(self.caller, load, save, key="%s/%s" % (obj, attr))
[docs] def search_for_obj(self, objname): """ Searches for an object matching objname. The object may be of different typeclasses. Args: objname: Name of the object we're looking for Returns: A typeclassed object, or None if nothing is found. """ from evennia.utils.utils import variable_from_module _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1)) caller = self.caller if objname.startswith("*") or "account" in self.switches: found_obj = caller.search_account(objname.lstrip("*")) elif "script" in self.switches: found_obj = _AT_SEARCH_RESULT(search.search_script(objname), caller) elif "channel" in self.switches: found_obj = _AT_SEARCH_RESULT(search.search_channel(objname), caller) else: global_search = True if "char" in self.switches or "character" in self.switches: typeclass = settings.BASE_CHARACTER_TYPECLASS elif "room" in self.switches: typeclass = settings.BASE_ROOM_TYPECLASS elif "exit" in self.switches: typeclass = settings.BASE_EXIT_TYPECLASS else: global_search = False typeclass = None found_obj = caller.search(objname, global_search=global_search, typeclass=typeclass) return found_obj
[docs] def func(self): """Implement the set attribute - a limited form of py.""" caller = self.caller if not self.args: caller.msg("Usage: set obj/attr = value. Use empty value to clear.") return # get values prepared by the parser value = self.rhs objname = self.lhs_objattr[0]["name"] attrs = self.lhs_objattr[0]["attrs"] obj = self.search_for_obj(objname) if not obj: return if not self.check_obj(obj): return result = [] if "edit" in self.switches: # edit in the line editor if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")): caller.msg("You don't have permission to edit %s." % obj.key) return if len(attrs) > 1: caller.msg("The Line editor can only be applied " "to one attribute at a time.") return if not attrs: caller.msg("Use `set/edit <objname>/<attr>` to define the Attribute to edit.\nTo " "edit the current room description, use `set/edit here/desc` (or " "use the `desc` command).") return self.edit_handler(obj, attrs[0]) return if not value: if self.rhs is None: # no = means we inspect the attribute(s) if not attrs: attrs = [attr.key for attr in obj.attributes.all()] for attr in attrs: if not self.check_attr(obj, attr): continue result.append(self.view_attr(obj, attr)) # we view it without parsing markup. self.caller.msg("".join(result).strip(), options={"raw": True}) return else: # deleting the attribute(s) if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")): caller.msg("You don't have permission to edit %s." % obj.key) return for attr in attrs: if not self.check_attr(obj, attr): continue result.append(self.rm_attr(obj, attr)) else: # setting attribute(s). Make sure to convert to real Python type before saving. if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")): caller.msg("You don't have permission to edit %s." % obj.key) return for attr in attrs: if not self.check_attr(obj, attr): continue value = _convert_from_string(self, value) result.append(self.set_attr(obj, attr, value)) # send feedback caller.msg("".join(result).strip("\n"))
[docs]class CmdTypeclass(COMMAND_DEFAULT_CLASS): """ set or change an object's typeclass Usage: typeclass[/switch] <object> [= typeclass.path] typeclass/prototype <object> = prototype_key typeclass/list/show [typeclass.path] swap - this is a shorthand for using /force/reset flags. update - this is a shorthand for using the /force/reload flag. Switch: show, examine - display the current typeclass of object (default) or, if given a typeclass path, show the docstring of that typeclass. update - *only* re-run at_object_creation on this object meaning locks or other properties set later may remain. reset - clean out *all* the attributes and properties on the object - basically making this a new clean object. This will also reset cmdsets. force - change to the typeclass also if the object already has a typeclass of the same name. list - show available typeclasses. Only typeclasses in modules actually imported or used from somewhere in the code will show up here (those typeclasses are still available if you know the path) prototype - clean and overwrite the object with the specified prototype key - effectively making a whole new object. Example: type button = examples.red_button.RedButton type/prototype button=a red button If the typeclass_path is not given, the current object's typeclass is assumed. View or set an object's typeclass. If setting, the creation hooks of the new typeclass will be run on the object. If you have clashing properties on the old class, use /reset. By default you are protected from changing to a typeclass of the same name as the one you already have - use /force to override this protection. The given typeclass must be identified by its location using python dot-notation pointing to the correct module and class. If no typeclass is given (or a wrong typeclass is given). Errors in the path or new typeclass will lead to the old typeclass being kept. The location of the typeclass module is searched from the default typeclass directory, as defined in the server settings. """ key = "typeclass" aliases = ["type", "parent", "swap", "update"] switch_options = ("show", "examine", "update", "reset", "force", "list", "prototype") locks = "cmd:perm(typeclass) or perm(Builder)" help_category = "Building"
[docs] def func(self): """Implements command""" caller = self.caller if "list" in self.switches: tclasses = get_all_typeclasses() contribs = [key for key in sorted(tclasses) if key.startswith("evennia.contrib")] or [ "<None loaded>" ] core = [ key for key in sorted(tclasses) if key.startswith("evennia") and key not in contribs ] or ["<None loaded>"] game = [key for key in sorted(tclasses) if not key.startswith("evennia")] or [ "<None loaded>" ] string = ( "|wCore typeclasses|n\n" " {core}\n" "|wLoaded Contrib typeclasses|n\n" " {contrib}\n" "|wGame-dir typeclasses|n\n" " {game}" ).format( core="\n ".join(core), contrib="\n ".join(contribs), game="\n ".join(game) ) EvMore(caller, string, exit_on_lastpage=True) return if not self.args: caller.msg("Usage: %s <object> [= typeclass]" % self.cmdstring) return if "show" in self.switches or "examine" in self.switches: oquery = self.lhs obj = caller.search(oquery, quiet=True) if not obj: # no object found to examine, see if it's a typeclass-path instead tclasses = get_all_typeclasses() matches = [ (key, tclass) for key, tclass in tclasses.items() if key.endswith(oquery) ] nmatches = len(matches) if nmatches > 1: caller.msg( "Multiple typeclasses found matching {}:\n {}".format( oquery, "\n ".join(tup[0] for tup in matches) ) ) elif not matches: caller.msg("No object or typeclass path found to match '{}'".format(oquery)) else: # one match found caller.msg( "Docstring for typeclass '{}':\n{}".format(oquery, matches[0][1].__doc__) ) else: # do the search again to get the error handling in case of multi-match obj = caller.search(oquery) if not obj: return caller.msg( "{}'s current typeclass is '{}.{}'".format( obj.name, obj.__class__.__module__, obj.__class__.__name__ ) ) return # get object to swap on obj = caller.search(self.lhs) if not obj: return if not hasattr(obj, "__dbclass__"): string = "%s is not a typed object." % obj.name caller.msg(string) return new_typeclass = self.rhs or obj.path prototype = None if "prototype" in self.switches: key = self.rhs prototype = protlib.search_prototype(key=key) if len(prototype) > 1: caller.msg( "More than one match for {}:\n{}".format( key, "\n".join(proto.get("prototype_key", "") for proto in prototype) ) ) return elif prototype: # one match prototype = prototype[0] else: # no match caller.msg("No prototype '{}' was found.".format(key)) return new_typeclass = prototype["typeclass"] self.switches.append("force") if "show" in self.switches or "examine" in self.switches: string = "%s's current typeclass is %s." % (obj.name, obj.__class__) caller.msg(string) return if self.cmdstring == "swap": self.switches.append("force") self.switches.append("reset") elif self.cmdstring == "update": self.switches.append("force") self.switches.append("update") if not (obj.access(caller, "control") or obj.access(caller, "edit")): caller.msg("You are not allowed to do that.") return if not hasattr(obj, "swap_typeclass"): caller.msg("This object cannot have a type at all!") return is_same = obj.is_typeclass(new_typeclass, exact=True) if is_same and "force" not in self.switches: string = "%s already has the typeclass '%s'. Use /force to override." % ( obj.name, new_typeclass, ) else: update = "update" in self.switches reset = "reset" in self.switches hooks = "at_object_creation" if update and not reset else "all" old_typeclass_path = obj.typeclass_path # special prompt for the user in cases where we want # to confirm changes. if "prototype" in self.switches: diff, _ = spawner.prototype_diff_from_object(prototype, obj) txt = spawner.format_diff(diff) prompt = ( "Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n" % (prototype["key"], obj.name, txt) ) if not reset: prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state." prompt += "\nAre you sure you want to apply these changes [yes]/no?" answer = yield (prompt) if answer and answer in ("no", "n"): caller.msg("Canceled: No changes were applied.") return # we let this raise exception if needed obj.swap_typeclass( new_typeclass, clean_attributes=reset, clean_cmdsets=reset, run_start_hooks=hooks ) if "prototype" in self.switches: modified = spawner.batch_update_objects_with_prototype(prototype, objects=[obj]) prototype_success = modified > 0 if not prototype_success: caller.msg("Prototype %s failed to apply." % prototype["key"]) if is_same: string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.path) else: string = "%s changed typeclass from %s to %s.\n" % ( obj.name, old_typeclass_path, obj.typeclass_path, ) if update: string += "Only the at_object_creation hook was run (update mode)." else: string += "All object creation hooks were run." if reset: string += " All old attributes where deleted before the swap." else: string += " Attributes set before swap were not removed." if "prototype" in self.switches and prototype_success: string += ( " Prototype '%s' was successfully applied over the object type." % prototype["key"] ) caller.msg(string)
[docs]class CmdWipe(ObjManipCommand): """ clear all attributes from an object Usage: wipe <object>[/<attr>[/<attr>...]] Example: wipe box wipe box/colour Wipes all of an object's attributes, or optionally only those matching the given attribute-wildcard search string. """ key = "wipe" locks = "cmd:perm(wipe) or perm(Builder)" help_category = "Building"
[docs] def func(self): """ inp is the dict produced in ObjManipCommand.parse() """ caller = self.caller if not self.args: caller.msg("Usage: wipe <object>[/<attr>/<attr>...]") return # get the attributes set by our custom parser objname = self.lhs_objattr[0]["name"] attrs = self.lhs_objattr[0]["attrs"] obj = caller.search(objname) if not obj: return if not (obj.access(caller, "control") or obj.access(caller, "edit")): caller.msg("You are not allowed to do that.") return if not attrs: # wipe everything obj.attributes.clear() string = "Wiped all attributes on %s." % obj.name else: for attrname in attrs: obj.attributes.remove(attrname) string = "Wiped attributes %s on %s." string = string % (",".join(attrs), obj.name) caller.msg(string)
[docs]class CmdLock(ObjManipCommand): """ assign a lock definition to an object Usage: lock <object or *account>[ = <lockstring>] or lock[/switch] <object or *account>/<access_type> Switch: del - delete given access type view - view lock associated with given access type (default) If no lockstring is given, shows all locks on object. Lockstring is of the form access_type:[NOT] func1(args)[ AND|OR][ NOT] func2(args) ...] Where func1, func2 ... valid lockfuncs with or without arguments. Separator expressions need not be capitalized. For example: 'get: id(25) or perm(Admin)' The 'get' lock access_type is checked e.g. by the 'get' command. An object locked with this example lock will only be possible to pick up by Admins or by an object with id=25. You can add several access_types after one another by separating them by ';', i.e: 'get:id(25); delete:perm(Builder)' """ key = "lock" aliases = ["locks"] locks = "cmd: perm(locks) or perm(Builder)" help_category = "Building"
[docs] def func(self): """Sets up the command""" caller = self.caller if not self.args: string = ( "Usage: lock <object>[ = <lockstring>] or lock[/switch] " "<object>/<access_type>" ) caller.msg(string) return if "/" in self.lhs: # call of the form lock obj/access_type objname, access_type = [p.strip() for p in self.lhs.split("/", 1)] obj = None if objname.startswith("*"): obj = caller.search_account(objname.lstrip("*")) if not obj: obj = caller.search(objname) if not obj: return has_control_access = obj.access(caller, "control") if access_type == "control" and not has_control_access: # only allow to change 'control' access if you have 'control' access already caller.msg("You need 'control' access to change this type of lock.") return if not (has_control_access or obj.access(caller, "edit")): caller.msg("You are not allowed to do that.") return lockdef = obj.locks.get(access_type) if lockdef: if "del" in self.switches: obj.locks.delete(access_type) string = "deleted lock %s" % lockdef else: string = lockdef else: string = "%s has no lock of access type '%s'." % (obj, access_type) caller.msg(string) return if self.rhs: # we have a = separator, so we are assigning a new lock if self.switches: swi = ", ".join(self.switches) caller.msg( "Switch(es) |w%s|n can not be used with a " "lock assignment. Use e.g. " "|wlock/del objname/locktype|n instead." % swi ) return objname, lockdef = self.lhs, self.rhs obj = None if objname.startswith("*"): obj = caller.search_account(objname.lstrip("*")) if not obj: obj = caller.search(objname) if not obj: return if not (obj.access(caller, "control") or obj.access(caller, "edit")): caller.msg("You are not allowed to do that.") return ok = False lockdef = re.sub(r"\'|\"", "", lockdef) try: ok = obj.locks.add(lockdef) except LockException as e: caller.msg(str(e)) if "cmd" in lockdef.lower() and inherits_from( obj, "evennia.objects.objects.DefaultExit" ): # special fix to update Exits since "cmd"-type locks won't # update on them unless their cmdsets are rebuilt. obj.at_init() if ok: caller.msg("Added lock '%s' to %s." % (lockdef, obj)) return # if we get here, we are just viewing all locks on obj obj = None if self.lhs.startswith("*"): obj = caller.search_account(self.lhs.lstrip("*")) if not obj: obj = caller.search(self.lhs) if not obj: return if not (obj.access(caller, "control") or obj.access(caller, "edit")): caller.msg("You are not allowed to do that.") return caller.msg("\n".join(obj.locks.all()))
[docs]class CmdExamine(ObjManipCommand): """ get detailed information about an object Usage: examine [<object>[/attrname]] examine [*<account>[/attrname]] Switch: account - examine an Account (same as adding *) object - examine an Object (useful when OOC) The examine command shows detailed game info about an object and optionally a specific attribute on it. If object is not specified, the current location is examined. Append a * before the search string to examine an account. """ key = "examine" aliases = ["ex", "exam"] locks = "cmd:perm(examine) or perm(Builder)" help_category = "Building" arg_regex = r"(/\w+?(\s|$))|\s|$" account_mode = False detail_color = "|c" header_color = "|w" quell_color = "|r" separator = "-"
[docs] def list_attribute(self, crop, attr, category, value): """ Formats a single attribute line. Args: crop (bool): If output should be cropped if too long. attr (str): Attribute key. category (str): Attribute category. value (any): Attribute value. Returns: """ if attr is None: return "No such attribute was found." value = utils.to_str(value) if crop: value = utils.crop(value) value = inlinefunc_raw(ansi_raw(value)) if category: return f"{attr}[{category}] = {value}" else: return f"{attr} = {value}"
[docs] def format_attributes(self, obj, attrname=None, crop=True): """ Helper function that returns info about attributes and/or non-persistent data stored on object """ if attrname: if obj.attributes.has(attrname): db_attr = [(attrname, obj.attributes.get(attrname), None)] else: db_attr = None try: ndb_attr = [(attrname, object.__getattribute__(obj.ndb, attrname))] except Exception: ndb_attr = None if not (db_attr or ndb_attr): return {"Attribue(s)": f"\n No Attribute '{attrname}' found on {obj.name}"} else: db_attr = [(attr.key, attr.value, attr.category) for attr in obj.db_attributes.all()] try: ndb_attr = obj.nattributes.all(return_tuples=True) except Exception: ndb_attr = (None, None, None) output = {} if db_attr and db_attr[0]: output["Persistent attribute(s)"] = "\n " + "\n ".join( sorted( self.list_attribute(crop, attr, category, value) for attr, value, category in db_attr ) ) if ndb_attr and ndb_attr[0]: output["Non-Persistent attribute(s)"] = " \n" + " \n".join( sorted(self.list_attribute(crop, attr, None, value) for attr, value in ndb_attr) ) return output
[docs] def format_output(self, obj, current_cmdset): """ Helper function that creates a nice report about an object. Args: obj (any): Object to analyze. current_cmdset (CmdSet): Current cmdset for object. Returns: str: The formatted string. """ hclr = self.header_color dclr = self.detail_color qclr = self.quell_color output = {} # main key output["Name/key"] = f"{dclr}{obj.name}|n ({obj.dbref})" # aliases if hasattr(obj, "aliases") and obj.aliases.all(): output["Aliases"] = ", ".join(utils.make_iter(str(obj.aliases))) # typeclass output["Typeclass"] = f"{obj.typename} ({obj.typeclass_path})" # sessions if hasattr(obj, "sessions") and obj.sessions.all(): output["Session id(s)"] = ", ".join(f"#{sess.sessid}" for sess in obj.sessions.all()) # email, if any if hasattr(obj, "email") and obj.email: output["Email"] = f"{dclr}{obj.email}|n" # account, for puppeted objects if hasattr(obj, "has_account") and obj.has_account: output["Account"] = f"{dclr}{obj.account.name}|n ({obj.account.dbref})" # account typeclass output[" Account Typeclass"] = f"{obj.account.typename} ({obj.account.typeclass_path})" # account permissions perms = obj.account.permissions.all() if obj.account.is_superuser: perms = ["<Superuser>"] elif not perms: perms = ["<None>"] perms = ", ".join(perms) if obj.account.attributes.has("_quell"): perms += f" {qclr}(quelled)|n" output[" Account Permissions"] = perms # location if hasattr(obj, "location"): loc = str(obj.location) if obj.location: loc += f" (#{obj.location.id})" output["Location"] = loc # home if hasattr(obj, "home"): home = str(obj.home) if obj.home: home += f" (#{obj.home.id})" output["Home"] = home # destination, for exits if hasattr(obj, "destination") and obj.destination: dest = str(obj.destination) if obj.destination: dest += f" (#{obj.destination.id})" output["Destination"] = dest # main permissions perms = obj.permissions.all() perms_string = "" if perms: perms_string = ", ".join(perms) if obj.is_superuser: perms_string += " <Superuser>" if perms_string: output["Permissions"] = perms_string # locks locks = str(obj.locks) if locks: locks_string = "\n" + utils.fill( "; ".join([lock for lock in locks.split(";")]), indent=2 ) else: locks_string = " Default" output["Locks"] = locks_string # cmdsets if current_cmdset and not ( len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"): # all() returns a 'stack', so make a copy to sort. def _format_options(cmdset): """helper for cmdset-option display""" def _truefalse(string, value): if value is None: return "" if value: return f"{string}: T" return f"{string}: F" options = ", ".join( _truefalse(opt, getattr(cmdset, opt)) for opt in ("no_exits", "no_objs", "no_channels", "duplicates") if getattr(cmdset, opt) is not None ) options = ", " + options if options else "" return options # cmdset stored on us stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True) stored = [] for cmdset in stored_cmdsets: if cmdset.key == "_EMPTY_CMDSET": continue options = _format_options(cmdset) stored.append( f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority}{options})" ) output["Stored Cmdset(s)"] = "\n " + "\n ".join(stored) # this gets all components of the currently merged set all_cmdsets = [(cmdset.key, cmdset) for cmdset in current_cmdset.merged_from] # we always at least try to add account- and session sets since these are ignored # if we merge on the object level. if hasattr(obj, "account") and obj.account: all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.account.cmdset.all()]) if obj.sessions.count(): # if there are more sessions than one on objects it's because of multisession mode 3. # we only show the first session's cmdset here (it is -in principle- possible that # different sessions have different cmdsets but for admins who want such madness # it is better that they overload with their own CmdExamine to handle it). all_cmdsets.extend( [ (cmdset.key, cmdset) for cmdset in obj.account.sessions.all()[0].cmdset.all() ] ) else: try: # we have to protect this since many objects don't have sessions. all_cmdsets.extend( [ (cmdset.key, cmdset) for cmdset in obj.get_session(obj.sessions.get()).cmdset.all() ] ) except (TypeError, AttributeError): # an error means we are merging an object without a session pass all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()] all_cmdsets.sort(key=lambda x: x.priority, reverse=True) # the resulting merged cmdset options = _format_options(current_cmdset) merged = [ f"<Current merged cmdset> ({current_cmdset.mergetype} prio {current_cmdset.priority}{options})" ] # the merge stack for cmdset in all_cmdsets: options = _format_options(cmdset) merged.append( f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority}{options})" ) output["Merged Cmdset(s)"] = "\n " + "\n ".join(merged) # list the commands available to this object current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")]) cmdsetstr = "\n" + utils.fill(", ".join(current_commands), indent=2) output[f"Commands available to {obj.key} (result of Merged CmdSets)"] = str(cmdsetstr) # scripts if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all(): output["Scripts"] = "\n " + f"{obj.scripts}" # add the attributes output.update(self.format_attributes(obj)) # Tags tags = obj.tags.all(return_key_and_category=True) tags_string = "\n" + utils.fill( ", ".join(sorted(f"{tag}[{category}]" for tag, category in tags)), indent=2, ) if tags: output["Tags[category]"] = tags_string # Contents of object exits = [] pobjs = [] things = [] if hasattr(obj, "contents"): for content in obj.contents: if content.destination: exits.append(content) elif content.account: pobjs.append(content) else: things.append(content) if exits: output["Exits (has .destination)"] = ", ".join( f"{exit.name}({exit.dbref})" for exit in exits ) if pobjs: output["Characters"] = ", ".join( f"{dclr}{pobj.name}|n({pobj.dbref})" for pobj in pobjs ) if things: output["Contents"] = ", ".join( f"{cont.name}({cont.dbref})" for cont in obj.contents if cont not in exits and cont not in pobjs ) # format output max_width = -1 for block in output.values(): max_width = max(max_width, max(display_len(line) for line in block.split("\n"))) max_width = max(0, min(self.client_width(), max_width)) sep = self.separator * max_width mainstr = "\n".join(f"{hclr}{header}|n: {block}" for (header, block) in output.items()) return f"{sep}\n{mainstr}\n{sep}"
[docs] def func(self): """Process command""" caller = self.caller def get_cmdset_callback(cmdset): """ We make use of the cmdhandeler.get_and_merge_cmdsets below. This is an asynchronous function, returning a Twisted deferred. So in order to properly use this we need use this callback; it is called with the result of get_and_merge_cmdsets, whenever that function finishes. Taking the resulting cmdset, we continue to format and output the result. """ self.msg(self.format_output(obj, cmdset).strip()) if not self.args: # If no arguments are provided, examine the invoker's location. if hasattr(caller, "location"): obj = caller.location if not obj.access(caller, "examine"): # If we don't have special info access, just look at the object instead. self.msg(caller.at_look(obj)) return obj_session = obj.sessions.get()[0] if obj.sessions.count() else None # using callback for printing result whenever function returns. get_and_merge_cmdsets( obj, obj_session, self.account, obj, "object", self.raw_string ).addCallback(get_cmdset_callback) else: self.msg("You need to supply a target to examine.") return # we have given a specific target object for objdef in self.lhs_objattr: obj = None obj_name = objdef["name"] obj_attrs = objdef["attrs"] self.account_mode = ( utils.inherits_from(caller, "evennia.accounts.accounts.DefaultAccount") or "account" in self.switches or obj_name.startswith("*") ) if self.account_mode: try: obj = caller.search_account(obj_name.lstrip("*")) except AttributeError: # this means we are calling examine from an account object obj = caller.search( obj_name.lstrip("*"), search_object="object" in self.switches ) else: obj = caller.search(obj_name) if not obj: continue if not obj.access(caller, "examine"): # If we don't have special info access, just look # at the object instead. self.msg(caller.at_look(obj)) continue if obj_attrs: for attrname in obj_attrs: # we are only interested in specific attributes ret = "\n".join( f"{self.header_color}{header}|n:{value}" for header, value in self.format_attributes( obj, attrname, crop=False ).items() ) self.caller.msg(ret) else: session = None if obj.sessions.count(): mergemode = "session" session = obj.sessions.get()[0] elif self.account_mode: mergemode = "account" else: mergemode = "object" account = None objct = None if self.account_mode: account = obj else: account = obj.account objct = obj # this is usually handled when a command runs, but when we examine # we may have leftover inherited cmdsets directly after a move etc. obj.cmdset.update() # using callback to print results whenever function returns. get_and_merge_cmdsets( obj, session, account, objct, mergemode, self.raw_string ).addCallback(get_cmdset_callback)
[docs]class CmdFind(COMMAND_DEFAULT_CLASS): """ search the database for objects Usage: find[/switches] <name or dbref or *account> [= dbrefmin[-dbrefmax]] locate - this is a shorthand for using the /loc switch. Switches: room - only look for rooms (location=None) exit - only look for exits (destination!=None) char - only look for characters (BASE_CHARACTER_TYPECLASS) exact - only exact matches are returned. loc - display object location if exists and match has one result startswith - search for names starting with the string, rather than containing Searches the database for an object of a particular name or exact #dbref. Use *accountname to search for an account. The switches allows for limiting object matches to certain game entities. Dbrefmin and dbrefmax limits matches to within the given dbrefs range, or above/below if only one is given. """ key = "find" aliases = "search, locate" switch_options = ("room", "exit", "char", "exact", "loc", "startswith") locks = "cmd:perm(find) or perm(Builder)" help_category = "Building"
[docs] def func(self): """Search functionality""" caller = self.caller switches = self.switches if not self.args or (not self.lhs and not self.rhs): caller.msg("Usage: find <string> [= low [-high]]") return if "locate" in self.cmdstring: # Use option /loc as a default for locate command alias switches.append("loc") searchstring = self.lhs try: # Try grabbing the actual min/max id values by database aggregation qs = ObjectDB.objects.values("id").aggregate(low=Min("id"), high=Max("id")) low, high = sorted(qs.values()) if not (low and high): raise ValueError( f"{self.__class__.__name__}: Min and max ID not returned by aggregation; falling back to queryset slicing." ) except Exception as e: logger.log_trace(e) # If that doesn't work for some reason (empty DB?), guess the lower # bound and do a less-efficient query to find the upper. low, high = 1, ObjectDB.objects.all().order_by("-id").first().id if self.rhs: try: # Check that rhs is either a valid dbref or dbref range bounds = tuple( sorted(dbref(x, False) for x in re.split("[-\s]+", self.rhs.strip())) ) # dbref() will return either a valid int or None assert bounds # None should not exist in the bounds list assert None not in bounds low = bounds[0] if len(bounds) > 1: high = bounds[-1] except AssertionError: caller.msg("Invalid dbref range provided (not a number).") return except IndexError as e: logger.log_err( f"{self.__class__.__name__}: Error parsing upper and lower bounds of query." ) logger.log_trace(e) low = min(low, high) high = max(low, high) is_dbref = utils.dbref(searchstring) is_account = searchstring.startswith("*") restrictions = "" if self.switches: restrictions = ", %s" % (", ".join(self.switches)) if is_dbref or is_account: if is_dbref: # a dbref search result = caller.search(searchstring, global_search=True, quiet=True) string = "|wExact dbref match|n(#%i-#%i%s):" % (low, high, restrictions) else: # an account search searchstring = searchstring.lstrip("*") result = caller.search_account(searchstring, quiet=True) string = "|wMatch|n(#%i-#%i%s):" % (low, high, restrictions) if "room" in switches: result = result if inherits_from(result, ROOM_TYPECLASS) else None if "exit" in switches: result = result if inherits_from(result, EXIT_TYPECLASS) else None if "char" in switches: result = result if inherits_from(result, CHAR_TYPECLASS) else None if not result: string += "\n |RNo match found.|n" elif not low <= int(result[0].id) <= high: string += "\n |RNo match found for '%s' in #dbref interval.|n" % searchstring else: result = result[0] string += "\n|g %s - %s|n" % (result.get_display_name(caller), result.path) if "loc" in self.switches and not is_account and result.location: string += " (|wlocation|n: |g{}|n)".format( result.location.get_display_name(caller) ) else: # Not an account/dbref search but a wider search; build a queryset. # Searches for key and aliases if "exact" in switches: keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high) aliasquery = Q( db_tags__db_key__iexact=searchstring, db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high, ) elif "startswith" in switches: keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high) aliasquery = Q( db_tags__db_key__istartswith=searchstring, db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high, ) else: keyquery = Q(db_key__icontains=searchstring, id__gte=low, id__lte=high) aliasquery = Q( db_tags__db_key__icontains=searchstring, db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high, ) # Keep the initial queryset handy for later reuse result_qs = ObjectDB.objects.filter(keyquery | aliasquery).distinct() nresults = result_qs.count() # Use iterator to minimize memory ballooning on large result sets results = result_qs.iterator() # Check and see if type filtering was requested; skip it if not if any(x in switches for x in ("room", "exit", "char")): obj_ids = set() for obj in results: if ( ("room" in switches and inherits_from(obj, ROOM_TYPECLASS)) or ("exit" in switches and inherits_from(obj, EXIT_TYPECLASS)) or ("char" in switches and inherits_from(obj, CHAR_TYPECLASS)) ): obj_ids.add(obj.id) # Filter previous queryset instead of requesting another filtered_qs = result_qs.filter(id__in=obj_ids).distinct() nresults = filtered_qs.count() # Use iterator again to minimize memory ballooning results = filtered_qs.iterator() # still results after type filtering? if nresults: if nresults > 1: header = f"{nresults} Matches" else: header = "One Match" string = f"|w{header}|n(#{low}-#{high}{restrictions}):" res = None for res in results: string += f"\n |g{res.get_display_name(caller)} - {res.path}|n" if ( "loc" in self.switches and nresults == 1 and res and getattr(res, "location", None) ): string += f" (|wlocation|n: |g{res.location.get_display_name(caller)}|n)" else: string = f"|wNo Matches|n(#{low}-#{high}{restrictions}):" string += f"\n |RNo matches found for '{searchstring}'|n" # send result caller.msg(string.strip())
[docs]class CmdTeleport(COMMAND_DEFAULT_CLASS): """ teleport object to another location Usage: tel/switch [<object> to||=] <target location> Examples: tel Limbo tel/quiet box = Limbo tel/tonone box Switches: quiet - don't echo leave/arrive messages to the source/target locations for the move. intoexit - if target is an exit, teleport INTO the exit object instead of to its destination tonone - if set, teleport the object to a None-location. If this switch is set, <target location> is ignored. Note that the only way to retrieve an object from a None location is by direct #dbref reference. A puppeted object cannot be moved to None. loc - teleport object to the target's location instead of its contents Teleports an object somewhere. If no object is given, you yourself are teleported to the target location. """ key = "tel" aliases = "teleport" switch_options = ("quiet", "intoexit", "tonone", "loc") rhs_split = ("=", " to ") # Prefer = delimiter, but allow " to " usage. locks = "cmd:perm(teleport) or perm(Builder)" help_category = "Building"
[docs] def func(self): """Performs the teleport""" caller = self.caller args = self.args lhs, rhs = self.lhs, self.rhs switches = self.switches # setting switches tel_quietly = "quiet" in switches to_none = "tonone" in switches to_loc = "loc" in switches if to_none: # teleporting to None if not args: obj_to_teleport = caller else: obj_to_teleport = caller.search(lhs, global_search=True) if not obj_to_teleport: caller.msg("Did not find object to teleport.") return if obj_to_teleport.has_account: caller.msg( "Cannot teleport a puppeted object " "(%s, puppeted by %s) to a None-location." % (obj_to_teleport.key, obj_to_teleport.account) ) return caller.msg("Teleported %s -> None-location." % obj_to_teleport) if obj_to_teleport.location and not tel_quietly: obj_to_teleport.location.msg_contents( "%s teleported %s into nothingness." % (caller, obj_to_teleport), exclude=caller ) obj_to_teleport.location = None return # not teleporting to None location if not args and not to_none: caller.msg("Usage: teleport[/switches] [<obj> =] <target_loc>||home") return if rhs: obj_to_teleport = caller.search(lhs, global_search=True) destination = caller.search(rhs, global_search=True) else: obj_to_teleport = caller destination = caller.search(lhs, global_search=True) if not obj_to_teleport: caller.msg("Did not find object to teleport.") return if not destination: caller.msg("Destination not found.") return if to_loc: destination = destination.location if not destination: caller.msg("Destination has no location.") return if obj_to_teleport == destination: caller.msg("You can't teleport an object inside of itself!") return if obj_to_teleport == destination.location: caller.msg("You can't teleport an object inside something it holds!") return if obj_to_teleport.location and obj_to_teleport.location == destination: caller.msg("%s is already at %s." % (obj_to_teleport, destination)) return use_destination = True if "intoexit" in self.switches: use_destination = False # try the teleport if obj_to_teleport.move_to( destination, quiet=tel_quietly, emit_to_obj=caller, use_destination=use_destination ): if obj_to_teleport == caller: caller.msg("Teleported to %s." % destination) else: caller.msg("Teleported %s -> %s." % (obj_to_teleport, destination))
[docs]class CmdScript(COMMAND_DEFAULT_CLASS): """ attach a script to an object Usage: script[/switch] <obj> [= script_path or <scriptkey>] Switches: start - start all non-running scripts on object, or a given script only stop - stop all scripts on objects, or a given script only If no script path/key is given, lists all scripts active on the given object. Script path can be given from the base location for scripts as given in settings. If adding a new script, it will be started automatically (no /start switch is needed). Using the /start or /stop switches on an object without specifying a script key/path will start/stop ALL scripts on the object. """ key = "script" aliases = "addscript" switch_options = ("start", "stop") locks = "cmd:perm(script) or perm(Builder)" help_category = "Building"
[docs] def func(self): """Do stuff""" caller = self.caller if not self.args: string = "Usage: script[/switch] <obj> [= script_path or <script key>]" caller.msg(string) return if not self.lhs: caller.msg("To create a global script you need |wscripts/add <typeclass>|n.") return obj = caller.search(self.lhs) if not obj: return result = [] if not self.rhs: # no rhs means we want to operate on all scripts scripts = obj.scripts.all() if not scripts: result.append("No scripts defined on %s." % obj.get_display_name(caller)) elif not self.switches: # view all scripts from evennia.commands.default.system import ScriptEvMore ScriptEvMore(self.caller, scripts.order_by("id"), session=self.session) return elif "start" in self.switches: num = sum([obj.scripts.start(script.key) for script in scripts]) result.append("%s scripts started on %s." % (num, obj.get_display_name(caller))) elif "stop" in self.switches: for script in scripts: result.append( "Stopping script %s on %s." % (script.get_display_name(caller), obj.get_display_name(caller)) ) script.stop() obj.scripts.validate() else: # rhs exists if not self.switches: # adding a new script, and starting it ok = obj.scripts.add(self.rhs, autostart=True) if not ok: result.append( "\nScript %s could not be added and/or started on %s " "(or it started and immediately shut down)." % (self.rhs, obj.get_display_name(caller)) ) else: result.append( "Script |w%s|n successfully added and started on %s." % (self.rhs, obj.get_display_name(caller)) ) else: paths = [self.rhs] + [ "%s.%s" % (prefix, self.rhs) for prefix in settings.TYPECLASS_PATHS ] if "stop" in self.switches: # we are stopping an already existing script for path in paths: ok = obj.scripts.stop(path) if not ok: result.append("\nScript %s could not be stopped. Does it exist?" % path) else: result = ["Script stopped and removed from object."] break if "start" in self.switches: # we are starting an already existing script for path in paths: ok = obj.scripts.start(path) if not ok: result.append("\nScript %s could not be (re)started." % path) else: result = ["Script started successfully."] break EvMore(caller, "".join(result).strip())
[docs]class CmdTag(COMMAND_DEFAULT_CLASS): """ handles the tags of an object Usage: tag[/del] <obj> [= <tag>[:<category>]] tag/search <tag>[:<category] Switches: search - return all objects with a given Tag del - remove the given tag. If no tag is specified, clear all tags on object. Manipulates and lists tags on objects. Tags allow for quick grouping of and searching for objects. If only <obj> is given, list all tags on the object. If /search is used, list objects with the given tag. The category can be used for grouping tags themselves, but it should be used with restrain - tags on their own are usually enough to for most grouping schemes. """ key = "tag" aliases = ["tags"] options = ("search", "del") locks = "cmd:perm(tag) or perm(Builder)" help_category = "Building" arg_regex = r"(/\w+?(\s|$))|\s|$"
[docs] def func(self): """Implement the tag functionality""" if not self.args: self.caller.msg("Usage: tag[/switches] <obj> [= <tag>[:<category>]]") return if "search" in self.switches: # search by tag tag = self.args category = None if ":" in tag: tag, category = [part.strip() for part in tag.split(":", 1)] objs = search.search_tag(tag, category=category) nobjs = len(objs) if nobjs > 0: catstr = ( " (category: '|w%s|n')" % category if category else ("" if nobjs == 1 else " (may have different tag categories)") ) matchstr = ", ".join(o.get_display_name(self.caller) for o in objs) string = "Found |w%i|n object%s with tag '|w%s|n'%s:\n %s" % ( nobjs, "s" if nobjs > 1 else "", tag, catstr, matchstr, ) else: string = "No objects found with tag '%s%s'." % ( tag, " (category: %s)" % category if category else "", ) self.caller.msg(string) return if "del" in self.switches: # remove one or all tags obj = self.caller.search(self.lhs, global_search=True) if not obj: return if self.rhs: # remove individual tag tag = self.rhs category = None if ":" in tag: tag, category = [part.strip() for part in tag.split(":", 1)] if obj.tags.get(tag, category=category): obj.tags.remove(tag, category=category) string = "Removed tag '%s'%s from %s." % ( tag, " (category: %s)" % category if category else "", obj, ) else: string = "No tag '%s'%s to delete on %s." % ( tag, " (category: %s)" % category if category else "", obj, ) else: # no tag specified, clear all tags old_tags = [ "%s%s" % (tag, " (category: %s)" % category if category else "") for tag, category in obj.tags.all(return_key_and_category=True) ] if old_tags: obj.tags.clear() string = "Cleared all tags from %s: %s" % (obj, ", ".join(sorted(old_tags))) else: string = "No Tags to clear on %s." % obj self.caller.msg(string) return # no search/deletion if self.rhs: # = is found; command args are of the form obj = tag obj = self.caller.search(self.lhs, global_search=True) if not obj: return tag = self.rhs category = None if ":" in tag: tag, category = [part.strip() for part in tag.split(":", 1)] # create the tag obj.tags.add(tag, category=category) string = "Added tag '%s'%s to %s." % ( tag, " (category: %s)" % category if category else "", obj, ) self.caller.msg(string) else: # no = found - list tags on object obj = self.caller.search(self.args, global_search=True) if not obj: return tagtuples = obj.tags.all(return_key_and_category=True) ntags = len(tagtuples) tags = [tup[0] for tup in tagtuples] categories = [" (category: %s)" % tup[1] if tup[1] else "" for tup in tagtuples] if ntags: string = "Tag%s on %s: %s" % ( "s" if ntags > 1 else "", obj, ", ".join(sorted("'%s'%s" % (tags[i], categories[i]) for i in range(ntags))), ) else: string = "No tags attached to %s." % obj self.caller.msg(string)
# helper functions for spawn
[docs]class CmdSpawn(COMMAND_DEFAULT_CLASS): """ spawn objects from prototype Usage: spawn[/noloc] <prototype_key> spawn[/noloc] <prototype_dict> spawn/search [prototype_keykey][;tag[,tag]] spawn/list [tag, tag, ...] spawn/list modules - list only module-based prototypes spawn/show [<prototype_key>] spawn/update <prototype_key> spawn/save <prototype_dict> spawn/edit [<prototype_key>] olc - equivalent to spawn/edit Switches: noloc - allow location to be None if not specified explicitly. Otherwise, location will default to caller's current location. search - search prototype by name or tags. list - list available prototypes, optionally limit by tags. show, examine - inspect prototype by key. If not given, acts like list. raw - show the raw dict of the prototype as a one-line string for manual editing. save - save a prototype to the database. It will be listable by /list. delete - remove a prototype from database, if allowed to. update - find existing objects with the same prototype_key and update them with latest version of given prototype. If given with /save, will auto-update all objects with the old version of the prototype without asking first. edit, menu, olc - create/manipulate prototype in a menu interface. Example: spawn GOBLIN spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"} spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all() \f Dictionary keys: |wprototype_parent |n - name of parent prototype to use. Required if typeclass is not set. Can be a path or a list for multiple inheritance (inherits left to right). If set one of the parents must have a typeclass. |wtypeclass |n - string. Required if prototype_parent is not set. |wkey |n - string, the main object identifier |wlocation |n - this should be a valid object or #dbref |whome |n - valid object or #dbref |wdestination|n - only valid for exits (object or dbref) |wpermissions|n - string or list of permission strings |wlocks |n - a lock-string |waliases |n - string or list of strings. |wndb_|n<name> - value of a nattribute (ndb_ is stripped) |wprototype_key|n - name of this prototype. Unique. Used to store/retrieve from db and update existing prototyped objects if desired. |wprototype_desc|n - desc of this prototype. Used in listings |wprototype_locks|n - locks of this prototype. Limits who may use prototype |wprototype_tags|n - tags of this prototype. Used to find prototype any other keywords are interpreted as Attributes and their values. The available prototypes are defined globally in modules set in settings.PROTOTYPE_MODULES. If spawn is used without arguments it displays a list of available prototypes. """ key = "spawn" aliases = ["olc"] switch_options = ( "noloc", "search", "list", "show", "raw", "examine", "save", "delete", "menu", "olc", "update", "edit", ) locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" def _search_prototype(self, prototype_key, quiet=False): """ Search for prototype and handle no/multi-match and access. Returns a single found prototype or None - in the case, the caller has already been informed of the search error we need not do any further action. """ prototypes = protlib.search_prototype(prototype_key) nprots = len(prototypes) # handle the search result err = None if not prototypes: err = f"No prototype named '{prototype_key}' was found." elif nprots > 1: err = "Found {} prototypes matching '{}':\n {}".format( nprots, prototype_key, ", ".join(proto.get("prototype_key", "") for proto in prototypes), ) else: # we have a single prototype, check access prototype = prototypes[0] if not self.caller.locks.check_lockstring( self.caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True ): err = "You don't have access to use this prototype." if err: # return None on any error if not quiet: self.caller.msg(err) return return prototype def _parse_prototype(self, inp, expect=dict): """ Parse a prototype dict or key from the input and convert it safely into a dict if appropriate. Args: inp (str): The input from user. expect (type, optional): Returns: prototype (dict, str or None): The parsed prototype. If None, the error was already reported. """ eval_err = None try: prototype = _LITERAL_EVAL(inp) except (SyntaxError, ValueError) as err: # treat as string eval_err = err prototype = utils.to_str(inp) finally: # it's possible that the input was a prototype-key, in which case # it's okay for the LITERAL_EVAL to fail. Only if the result does not # match the expected type do we have a problem. if not isinstance(prototype, expect): if eval_err: string = ( f"{inp}\n{eval_err}\n|RCritical Python syntax error in argument. Only primitive " "Python structures are allowed. \nMake sure to use correct " "Python syntax. Remember especially to put quotes around all " "strings inside lists and dicts.|n For more advanced uses, embed " "inlinefuncs in the strings." ) else: string = "Expected {}, got {}.".format(expect, type(prototype)) self.caller.msg(string) return if expect == dict: # an actual prototype. We need to make sure it's safe, # so don't allow exec. # TODO: Exec support is deprecated. Remove completely for 1.0. if "exec" in prototype and not self.caller.check_permstring("Developer"): self.caller.msg( "Spawn aborted: You are not allowed to " "use the 'exec' prototype key." ) return try: # we homogenize the prototype first, to be more lenient with free-form protlib.validate_prototype(protlib.homogenize_prototype(prototype)) except RuntimeError as err: self.caller.msg(str(err)) return return prototype def _get_prototype_detail(self, query=None, prototypes=None): """ Display the detailed specs of one or more prototypes. Args: query (str, optional): If this is given and `prototypes` is not, search for the prototype(s) by this query. This may be a partial query which may lead to multiple matches, all being displayed. prototypes (list, optional): If given, ignore `query` and only show these prototype-details. Returns: display (str, None): A formatted string of one or more prototype details. If None, the caller was already informed of the error. """ if not prototypes: # we need to query. Note that if query is None, all prototypes will # be returned. prototypes = protlib.search_prototype(key=query) if prototypes: return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes) elif query: self.caller.msg(f"No prototype named '{query}' was found.") else: self.caller.msg("No prototypes found.") def _list_prototypes(self, key=None, tags=None): """Display prototypes as a list, optionally limited by key/tags. """ protlib.list_prototypes(self.caller, key=key, tags=tags, session=self.session) @interactive def _update_existing_objects(self, caller, prototype_key, quiet=False): """ Update existing objects (if any) with this prototype-key to the latest prototype version. Args: caller (Object): This is necessary for @interactive to work. prototype_key (str): The prototype to update. quiet (bool, optional): If set, don't report to user if no old objects were found to update. Returns: n_updated (int): Number of updated objects. """ prototype = self._search_prototype(prototype_key) if not prototype: return existing_objects = protlib.search_objects_with_prototype(prototype_key) if not existing_objects: if not quiet: caller.msg("No existing objects found with an older version of this prototype.") return if existing_objects: n_existing = len(existing_objects) slow = " (note that this may be slow)" if n_existing > 10 else "" string = ( f"There are {n_existing} existing object(s) with an older version " f"of prototype '{prototype_key}'. Should it be re-applied to them{slow}? [Y]/N" ) answer = yield (string) if answer.lower() in ["n", "no"]: caller.msg( "|rNo update was done of existing objects. " "Use spawn/update <key> to apply later as needed.|n" ) return try: n_updated = spawner.batch_update_objects_with_prototype( prototype, objects=existing_objects ) except Exception: logger.log_trace() caller.msg(f"{n_updated} objects were updated.") return def _parse_key_desc_tags(self, argstring, desc=True): """ Parse ;-separated input list. """ key, desc, tags = "", "", [] if ";" in argstring: parts = [part.strip().lower() for part in argstring.split(";")] if len(parts) > 1 and desc: key = parts[0] desc = parts[1] tags = parts[2:] else: key = parts[0] tags = parts[1:] else: key = argstring.strip().lower() return key, desc, tags
[docs] def func(self): """Implements the spawner""" caller = self.caller noloc = "noloc" in self.switches # run the menu/olc if ( self.cmdstring == "olc" or "menu" in self.switches or "olc" in self.switches or "edit" in self.switches ): # OLC menu mode prototype = None if self.lhs: prototype_key = self.lhs prototype = self._search_prototype(prototype_key) if not prototype: return olc_menus.start_olc(caller, session=self.session, prototype=prototype) return if "search" in self.switches: # query for a key match. The arg is a search query or nothing. if not self.args: # an empty search returns the full list self._list_prototypes() return # search for key;tag combinations key, _, tags = self._parse_key_desc_tags(self.args, desc=False) self._list_prototypes(key, tags) return if "raw" in self.switches: # query for key match and return the prototype as a safe one-liner string. if not self.args: caller.msg("You need to specify a prototype-key to get the raw data for.") prototype = self._search_prototype(self.args) if not prototype: return caller.msg(str(prototype)) return if "show" in self.switches or "examine" in self.switches: # show a specific prot detail. The argument is a search query or empty. if not self.args: # we don't show the list of all details, that's too spammy. caller.msg("You need to specify a prototype-key to show.") return detail_string = self._get_prototype_detail(self.args) if not detail_string: return caller.msg(detail_string) return if "list" in self.switches: # for list, all optional arguments are tags. tags = self.lhslist err = self._list_prototypes(tags=tags) if err: caller.msg( "No prototypes found with prototype-tag(s): {}".format( list_to_string(tags, "or") ) ) return if "save" in self.switches: # store a prototype to the database store if not self.args: caller.msg( "Usage: spawn/save [<key>[;desc[;tag,tag[,...][;lockstring]]]] = <prototype_dict>" ) return if self.rhs: # input on the form key = prototype prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs) prototype_key = None if not prototype_key else prototype_key prototype_desc = None if not prototype_desc else prototype_desc prototype_tags = None if not prototype_tags else prototype_tags prototype_input = self.rhs.strip() else: prototype_key = prototype_desc = None prototype_tags = None prototype_input = self.lhs.strip() # handle parsing prototype = self._parse_prototype(prototype_input) if not prototype: return prot_prototype_key = prototype.get("prototype_key") if not (prototype_key or prot_prototype_key): caller.msg( "A prototype_key must be given, either as `prototype_key = <prototype>` " "or as a key 'prototype_key' inside the prototype structure." ) return if prototype_key is None: prototype_key = prot_prototype_key if prot_prototype_key != prototype_key: caller.msg("(Replacing `prototype_key` in prototype with given key.)") prototype["prototype_key"] = prototype_key if prototype_desc is not None and prot_prototype_key != prototype_desc: caller.msg("(Replacing `prototype_desc` in prototype with given desc.)") prototype["prototype_desc"] = prototype_desc if prototype_tags is not None and prototype.get("prototype_tags") != prototype_tags: caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))") prototype["prototype_tags"] = prototype_tags string = "" # check for existing prototype (exact match) old_prototype = self._search_prototype(prototype_key, quiet=True) diff = spawner.prototype_diff(old_prototype, prototype, homogenize=True) diffstr = spawner.format_diff(diff) new_prototype_detail = self._get_prototype_detail(prototypes=[prototype]) if old_prototype: if not diffstr: string = f"|yAlready existing Prototype:|n\n{new_prototype_detail}\n" question = ( "\nThere seems to be no changes. Do you still want to (re)save? [Y]/N" ) else: string = ( f'|yExisting prototype "{prototype_key}" found. Change:|n\n{diffstr}\n' f"|yNew changed prototype:|n\n{new_prototype_detail}" ) question = ( "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N" ) else: string = f"|yCreating new prototype:|n\n{new_prototype_detail}" question = "\nDo you want to continue saving? [Y]/N" answer = yield (string + question) if answer.lower() in ["n", "no"]: caller.msg("|rSave cancelled.|n") return # all seems ok. Try to save. try: prot = protlib.save_prototype(prototype) if not prot: caller.msg("|rError saving:|R {}.|n".format(prototype_key)) return except protlib.PermissionError as err: caller.msg("|rError saving:|R {}|n".format(err)) return caller.msg("|gSaved prototype:|n {}".format(prototype_key)) # check if we want to update existing objects self._update_existing_objects(self.caller, prototype_key, quiet=True) return if not self.args: # all switches beyond this point gets a common non-arg return ncount = len(protlib.search_prototype()) caller.msg( "Usage: spawn <prototype-key> or {{key: value, ...}}" f"\n ({ncount} existing prototypes. Use /list to inspect)" ) return if "delete" in self.switches: # remove db-based prototype prototype_detail = self._get_prototype_detail(self.args) if not prototype_detail: return string = f"|rDeleting prototype:|n\n{prototype_detail}" question = "\nDo you want to continue deleting? [Y]/N" answer = yield (string + question) if answer.lower() in ["n", "no"]: caller.msg("|rDeletion cancelled.|n") return try: success = protlib.delete_prototype(self.args) except protlib.PermissionError as err: retmsg = f"|rError deleting:|R {err}|n" else: retmsg = ( "Deletion successful" if success else "Deletion failed (does the prototype exist?)" ) caller.msg(retmsg) return if "update" in self.switches: # update existing prototypes prototype_key = self.args.strip().lower() self._update_existing_objects(self.caller, prototype_key) return # If we get to this point, we use not switches but are trying a # direct creation of an object from a given prototype or -key prototype = self._parse_prototype( self.args, expect=dict if self.args.strip().startswith("{") else str ) if not prototype: # this will only let through dicts or strings return key = "<unnamed>" if isinstance(prototype, str): # A prototype key we are looking to apply prototype_key = prototype prototype = self._search_prototype(prototype_key) if not prototype: return # proceed to spawning try: for obj in spawner.spawn(prototype): self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) if not prototype.get("location") and not noloc: # we don't hardcode the location in the prototype (unless the user # did so manually) - that would lead to it having to be 'removed' every # time we try to update objects with this prototype in the future. obj.location = caller.location except RuntimeError as err: caller.msg(err)