diff --git a/src/objects/__init__.py b/src/objects/__init__.py index 2cdc469ceb..0f255657c2 100644 --- a/src/objects/__init__.py +++ b/src/objects/__init__.py @@ -7,6 +7,5 @@ Also, the initiated object manager is available as src.objects.manager. """ #from src.objects.objects import * -from src.objects.models import ObjectDB - -manager = ObjectDB.objects +#from src.objects.models import ObjectDB +#manager = ObjectDB.objects diff --git a/src/objects/models.py b/src/objects/models.py index 9bab7715c9..5752c126e8 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -14,81 +14,14 @@ the database object. Like everything else, they can be accessed transparently through the decorating TypeClass. """ -import traceback from django.db import models from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from src.typeclasses.models import TypedObject, NickHandler +from src.typeclasses.models import TypedObject from src.objects.manager import ObjectDBManager -from src.players.models import PlayerDB -from src.commands.cmdsethandler import CmdSetHandler -from src.commands import cmdhandler -from src.scripts.scripthandler import ScriptHandler from src.utils import logger -from src.utils.utils import (make_iter, to_str, to_unicode, lazy_property, - variable_from_module, dbref) - -MULTISESSION_MODE = settings.MULTISESSION_MODE -from django.utils.translation import ugettext as _ - -#__all__ = ("ObjectDB", ) - -_ScriptDB = None -_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) -_SESSIONS = None - -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ - -# the sessid_max is based on the length of the db_sessid csv field (excluding commas) -_SESSID_MAX = 16 if MULTISESSION_MODE in (1, 3) else 1 - -class SessidHandler(object): - """ - Handles the get/setting of the sessid - comma-separated integer field - """ - def __init__(self, obj): - self.obj = obj - self._cache = set() - self._recache() - - def _recache(self): - self._cache = list(set(int(val) for val in (_GA(self.obj, "db_sessid") or "").split(",") if val)) - - def get(self): - "Returns a list of one or more session ids" - return self._cache - - def add(self, sessid): - "Add sessid to handler" - _cache = self._cache - if sessid not in _cache: - if len(_cache) >= _SESSID_MAX: - return - _cache.append(sessid) - _SA(self.obj, "db_sessid", ",".join(str(val) for val in _cache)) - _GA(self.obj, "save")(update_fields=["db_sessid"]) - - def remove(self, sessid): - "Remove sessid from handler" - _cache = self._cache - if sessid in _cache: - _cache.remove(sessid) - _SA(self.obj, "db_sessid", ",".join(str(val) for val in _cache)) - _GA(self.obj, "save")(update_fields=["db_sessid"]) - - def clear(self): - "Clear sessids" - self._cache = [] - _SA(self.obj, "db_sessid", None) - _GA(self.obj, "save")(update_fields=["db_sessid"]) - - def count(self): - "Return amount of sessions connected" - return len(self._cache) +from src.utils.utils import (make_iter, dbref) #------------------------------------------------------------ @@ -173,27 +106,7 @@ class ObjectDB(TypedObject): # Database manager objects = ObjectDBManager() - # caches for quick lookups of typeclass loading. - _typeclass_paths = settings.OBJECT_TYPECLASS_PATHS - _default_typeclass_path = settings.BASE_OBJECT_TYPECLASS or "src.objects.objects.Object" - - # lazy-load handlers - @lazy_property - def cmdset(self): - return CmdSetHandler(self, True) - - @lazy_property - def scripts(self): - return ScriptHandler(self) - - @lazy_property - def nicks(self): - return NickHandler(self) - - @lazy_property - def sessid(self): - return SessidHandler(self) - + # field-related field-related properties def _at_db_player_postsave(self): """ This hook is called automatically after the player field is saved. @@ -201,30 +114,21 @@ class ObjectDB(TypedObject): # we need to re-cache this for superusers to bypass. self.locks.cache_lock_bypass(self) - # cmdset_storage property. We use a custom wrapper to manage this. This also - # seems very sensitive to caching, so leaving it be for now. /Griatch - #@property + # cmdset_storage property handling def __cmdset_storage_get(self): - """ - Getter. Allows for value = self.name. - Returns a list of cmdset_storage. - """ - storage = _GA(self, "db_cmdset_storage") - # we need to check so storage is not None + "getter" + storage = self.db_cmdset_storage return [path.strip() for path in storage.split(',')] if storage else [] - #@cmdset_storage.setter + def __cmdset_storage_set(self, value): - """ - Setter. Allows for self.name = value. - Stores as a comma-separated string. - """ - _SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value))) - _GA(self, "save")() - #@cmdset_storage.deleter + "setter" + self.db_cmdset_storage = ",".join(str(val).strip() for val in make_iter(value)) + self.save(update_fields=["db_cmdset_storage"]) + def __cmdset_storage_del(self): - "Deleter. Allows for del self.name" - _SA(self, "db_cmdset_storage", None) - _GA(self, "save")() + "deleter" + self.db_cmdset_storage = None + self.save(update_fields=["db_cmdset_storage"]) cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set, __cmdset_storage_del) # location getsetter @@ -280,567 +184,3 @@ class ObjectDB(TypedObject): verbose_name = "Object" verbose_name_plural = "Objects" - # - # ObjectDB class access methods/properties - # - - #@property - def __sessions_get(self): - """ - Retrieve sessions connected to this object. - """ - # if the player is not connected, this will simply be an empty list. - if _GA(self, "db_player"): - return _GA(_GA(self, "db_player"), "get_all_sessions")() - return [] - sessions = property(__sessions_get) - - #@property - def __has_player_get(self): - """ - Convenience function for checking if an active player is - currently connected to this object - """ - return any(_GA(self, "sessions")) - has_player = property(__has_player_get) - is_player = property(__has_player_get) - - #@property - def __is_superuser_get(self): - "Check if user has a player, and if so, if it is a superuser." - return (_GA(self, "db_player") and _GA(_GA(self, "db_player"), "is_superuser") - and not _GA(_GA(self, "db_player"), "attributes").get("_quell")) - is_superuser = property(__is_superuser_get) - - # contents - - def contents_get(self, exclude=None): - """ - Returns the contents of this object, i.e. all - objects that has this object set as its location. - This should be publically available. - - exclude is one or more objects to not return - """ - if exclude: - return ObjectDB.objects.get_contents(self, excludeobj=make_iter(exclude)) - return ObjectDB.objects.get_contents(self) - contents = property(contents_get) - - #@property - def __exits_get(self): - """ - Returns all exits from this object, i.e. all objects - at this location having the property destination != None. - """ - return [exi for exi in _GA(self, "contents") - if exi.destination] - exits = property(__exits_get) - - # - # Main Search method - # - - def search(self, searchdata, - global_search=False, - use_nicks=True, # should this default to off? - typeclass=None, - location=None, - attribute_name=None, - quiet=False, - exact=False): - """ - Returns the typeclass of an Object matching a search string/condition - - Perform a standard object search in the database, handling - multiple results and lack thereof gracefully. By default, only - objects in self's current location or inventory is searched. - Note: to find Players, use eg. ev.player_search. - - Inputs: - - searchdata (str or obj): Primary search criterion. Will be matched - against object.key (with object.aliases second) unless - the keyword attribute_name specifies otherwise. - Special strings: - # - search by unique dbref. This is always - a global search. - me,self - self-reference to this object - - - can be used to differentiate - between multiple same-named matches - global_search (bool): Search all objects globally. This is overruled - by "location" keyword. - use_nicks (bool): Use nickname-replace (nicktype "object") on the - search string - typeclass (str or Typeclass, or list of either): Limit search only - to Objects with this typeclass. May be a list of typeclasses - for a broader search. - location (Object): Specify a location to search, if different from the - self's given location plus its contents. This can also - be a list of locations. - attribute_name (str): Define which property to search. If set, no - key+alias search will be performed. This can be used to - search database fields (db_ will be automatically - appended), and if that fails, it will try to return - objects having Attributes with this name and value - equal to searchdata. A special use is to search for - "key" here if you want to do a key-search without - including aliases. - quiet (bool) - don't display default error messages - this tells the - search method that the user wants to handle all errors - themselves. It also changes the return value type, see - below. - exact (bool) - if unset (default) - prefers to match to beginning of - string rather than not matching at all. If set, requires - exact mathing of entire string. - - Returns: - quiet=False (default): - no match or multimatch: - auto-echoes errors to self.msg, then returns None - (results are handled by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) - match: - a unique object match - quiet=True: - returns a list of 0, 1 or more matches - - """ - is_string = isinstance(searchdata, basestring) - - if use_nicks: - # do nick-replacement on search - searchdata = self.nicks.nickreplace(searchdata, categories=("object", "player"), include_player=True) - - candidates=None - if(global_search or (is_string and searchdata.startswith("#") and - len(searchdata) > 1 and searchdata[1:].isdigit())): - # only allow exact matching if searching the entire database - # or unique #dbrefs - exact = True - elif location: - # location(s) were given - candidates = [] - for obj in make_iter(location): - candidates.extend(obj.contents) - else: - # local search. Candidates are self.contents, self.location - # and self.location.contents - location = self.location - candidates = self.contents - if location: - candidates = candidates + [location] + location.contents - else: - # normally we are included in location.contents - candidates.append(self) - - results = ObjectDB.objects.object_search(searchdata, - attribute_name=attribute_name, - typeclass=typeclass, - candidates=candidates, - exact=exact) - if quiet: - return results - return _AT_SEARCH_RESULT(self, searchdata, results, global_search) - - def search_player(self, searchdata, quiet=False): - """ - Simple shortcut wrapper to search for players, not characters. - - searchdata - search criterion - the key or dbref of the player - to search for. If this is "here" or "me", search - for the player connected to this object. - quiet - return the results as a list rather than echo eventual - standard error messages. - - Returns: - quiet=False (default): - no match or multimatch: - auto-echoes errors to self.msg, then returns None - (results are handled by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) - match: - a unique player match - quiet=True: - no match or multimatch: - returns None or list of multi-matches - match: - a unique object match - """ - results = PlayerDB.objects.player_search(searchdata) - if quiet: - return results - return _AT_SEARCH_RESULT(self, searchdata, results, global_search=True) - - # - # Execution/action methods - # - - def execute_cmd(self, raw_string, sessid=None, **kwargs): - """ - Do something as this object. This method is a copy of the execute_ - cmd method on the session. This is never called normally, it's only - used when wanting specifically to let an object be the caller of a - command. It makes use of nicks of eventual connected players as well. - - Argument: - raw_string (string) - raw command input - sessid (int) - optional session id to return results to - **kwargs - other keyword arguments will be added to the found command - object instace as variables before it executes. This is - unused by default Evennia but may be used to set flags and - change operating paramaters for commands at run-time. - - Returns Deferred - this is an asynchronous Twisted object that will - not fire until the command has actually finished executing. To - overload this one needs to attach callback functions to it, with - addCallback(function). This function will be called with an - eventual return value from the command execution. - - This return is not used at all by Evennia by default, but might - be useful for coders intending to implement some sort of nested - command structure. - """ - # nick replacement - we require full-word matching. - - # do text encoding conversion - raw_string = to_unicode(raw_string) - raw_string = self.nicks.nickreplace(raw_string, - categories=("inputline", "channel"), include_player=True) - return cmdhandler.cmdhandler(self, raw_string, callertype="object", sessid=sessid, **kwargs) - - def msg(self, text=None, from_obj=None, sessid=0, **kwargs): - """ - Emits something to a session attached to the object. - - message (str): The message to send - from_obj (obj): object that is sending. - data (object): an optional data object that may or may not - be used by the protocol. - sessid (int): sessid to relay to, if any. - If set to 0 (default), use either from_obj.sessid (if set) or self.sessid automatically - If None, echo to all connected sessions - - When this message is called, from_obj.at_msg_send and self.at_msg_receive are called. - - """ - global _SESSIONS - if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS - - text = to_str(text, force_string=True) if text else "" - - if "data" in kwargs: - # deprecation warning - logger.log_depmsg("ObjectDB.msg(): 'data'-dict keyword is deprecated. Use **kwargs instead.") - data = kwargs.pop("data") - if isinstance(data, dict): - kwargs.update(data) - - if from_obj: - # call hook - try: - from_obj.at_msg_send(text=text, to_obj=self, **kwargs) - except Exception: - logger.log_trace() - try: - if not self.at_msg_receive(text=text, **kwargs): - # if at_msg_receive returns false, we abort message to this object - return - except Exception: - logger.log_trace() - - sessions = _SESSIONS.session_from_sessid([sessid] if sessid else make_iter(_GA(self, "sessid").get())) - for session in sessions: - session.msg(text=text, **kwargs) - - def msg_contents(self, message, exclude=None, from_obj=None, **kwargs): - """ - Emits something to all objects inside an object. - - exclude is a list of objects not to send to. See self.msg() for - more info. - """ - contents = _GA(self, "contents") - if exclude: - exclude = make_iter(exclude) - contents = [obj for obj in contents if obj not in exclude] - for obj in contents: - obj.msg(message, from_obj=from_obj, **kwargs) - - def move_to(self, destination, quiet=False, - emit_to_obj=None, use_destination=True, to_none=False): - """ - Moves this object to a new location. - - Moves this object to a new location. Note that if is an - exit object (i.e. it has "destination"!=None), the move_to will - happen to this destination and -not- into the exit object itself, unless - use_destination=False. Note that no lock checks are done by this - function, such things are assumed to have been handled before calling - move_to. - - destination: (Object) Reference to the object to move to. This - can also be an exit object, in which case the destination - property is used as destination. - quiet: (bool) If true, don't emit left/arrived messages. - emit_to_obj: (Object) object to receive error messages - use_destination (bool): Default is for objects to use the "destination" - property of destinations as the target to move to. - Turning off this keyword allows objects to move - "inside" exit objects. - to_none - allow destination to be None. Note that no hooks are run when - moving to a None location. If you want to run hooks, - run them manually (and make sure they can manage None - locations). - - Returns True/False depending on if there were problems with the move. - This method may also return various error messages to the - emit_to_obj. - """ - def logerr(string=""): - trc = traceback.format_exc() - errstring = "%s%s" % (trc, string) - logger.log_trace() - _GA(self, "msg")(errstring) - - errtxt = _("Couldn't perform move ('%s'). Contact an admin.") - if not emit_to_obj: - emit_to_obj = self - - if not destination: - if to_none: - # immediately move to None. There can be no hooks called since - # there is no destination to call them with. - self.location = None - return True - emit_to_obj.msg(_("The destination doesn't exist.")) - return - if destination.destination and use_destination: - # traverse exits - destination = destination.destination - - # Before the move, call eventual pre-commands. - try: - if not self.at_before_move(destination): - return - except Exception: - logerr(errtxt % "at_before_move()") - #emit_to_obj.msg(errtxt % "at_before_move()") - #logger.log_trace() - return False - - # Save the old location - source_location = _GA(self, "location") - if not source_location: - # there was some error in placing this room. - # we have to set one or we won't be able to continue - if _GA(self, "home"): - source_location = _GA(self, "home") - else: - default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) - source_location = default_home - - # Call hook on source location - try: - source_location.at_object_leave(self, destination) - except Exception: - logerr(errtxt % "at_object_leave()") - #emit_to_obj.msg(errtxt % "at_object_leave()") - #logger.log_trace() - return False - - if not quiet: - #tell the old room we are leaving - try: - self.announce_move_from(destination) - except Exception: - logerr(errtxt % "at_announce_move()") - #emit_to_obj.msg(errtxt % "at_announce_move()" ) - #logger.log_trace() - return False - - # Perform move - try: - #print "move_to location:", destination - _SA(self, "location", destination) - except Exception: - emit_to_obj.msg(errtxt % "location change") - logger.log_trace() - return False - - if not quiet: - # Tell the new room we are there. - try: - self.announce_move_to(source_location) - except Exception: - logerr(errtxt % "announce_move_to()") - #emit_to_obj.msg(errtxt % "announce_move_to()") - #logger.log_trace() - return False - - # Perform eventual extra commands on the receiving location - # (the object has already arrived at this point) - try: - destination.at_object_receive(self, source_location) - except Exception: - logerr(errtxt % "at_object_receive()") - #emit_to_obj.msg(errtxt % "at_object_receive()") - #logger.log_trace() - return False - - # Execute eventual extra commands on this object after moving it - # (usually calling 'look') - try: - self.at_after_move(source_location) - except Exception: - logerr(errtxt % "at_after_move") - #emit_to_obj.msg(errtxt % "at_after_move()") - #logger.log_trace() - return False - return True - - # - # Object Swap, Delete and Cleanup methods - # - - def clear_exits(self): - """ - Destroys all of the exits and any exits pointing to this - object as a destination. - """ - for out_exit in [exi for exi in ObjectDB.objects.get_contents(self) if exi.db_destination]: - out_exit.delete() - for in_exit in ObjectDB.objects.filter(db_destination=self): - in_exit.delete() - - def clear_contents(self): - """ - Moves all objects (players/things) to their home - location or to default home. - """ - # Gather up everything that thinks this is its location. - objs = ObjectDB.objects.filter(db_location=self) - default_home_id = int(settings.DEFAULT_HOME.lstrip("#")) - try: - default_home = ObjectDB.objects.get(id=default_home_id) - if default_home.dbid == _GA(self, "dbid"): - # we are deleting default home! - default_home = None - except Exception: - string = _("Could not find default home '(#%d)'.") - logger.log_errmsg(string % default_home_id) - default_home = None - - for obj in objs: - home = obj.home - # Obviously, we can't send it back to here. - if not home or (home and home.dbid == _GA(self, "dbid")): - obj.home = default_home - home = default_home - - # If for some reason it's still None... - if not home: - string = "Missing default home, '%s(#%d)' " - string += "now has a null location." - obj.location = None - obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin.")) - logger.log_errmsg(string % (obj.name, obj.dbid)) - return - - if obj.has_player: - if home: - string = "Your current location has ceased to exist," - string += " moving you to %s(#%d)." - obj.msg(_(string) % (home.name, home.dbid)) - else: - # Famous last words: The player should never see this. - string = "This place should not exist ... contact an admin." - obj.msg(_(string)) - obj.move_to(home) - - def copy(self, new_key=None): - """ - Makes an identical copy of this object. If you want to customize the - copy by changing some settings, use ObjectDB.object.copy_object() - directly. - - new_key (string) - new key/name of copied object. If new_key is not - specified, the copy will be named _copy - by default. - Returns: Object (copy of this one) - """ - def find_clone_key(): - """ - Append 01, 02 etc to obj.key. Checks next higher number in the - same location, then adds the next number available - - returns the new clone name on the form keyXX - """ - key = _GA(self, "key") - num = 1 - for obj in (obj for obj in self.location.contents - if obj.key.startswith(key) and - obj.key.lstrip(key).isdigit()): - num += 1 - return "%s%03i" % (key, num) - new_key = new_key or find_clone_key() - return ObjectDB.objects.copy_object(self, new_key=new_key) - - delete_iter = 0 - def delete(self): - """ - Deletes this object. - Before deletion, this method makes sure to move all contained - objects to their respective home locations, as well as clean - up all exits to/from the object. - """ - global _ScriptDB - if not _ScriptDB: - from src.scripts.models import ScriptDB as _ScriptDB - - if _GA(self, "delete_iter") > 0: - # make sure to only call delete once on this object - # (avoid recursive loops) - return False - - if not self.at_object_delete(): - # this is an extra pre-check - # run before deletion mechanism - # is kicked into gear. - _SA(self, "delete_iter", 0) - return False - - self.delete_iter += 1 - - # See if we need to kick the player off. - - for session in _GA(self, "sessions"): - session.msg(_("Your character %s has been destroyed.") % _GA(self, "key")) - # no need to disconnect, Player just jumps to OOC mode. - # sever the connection (important!) - if _GA(self, 'player'): - _SA(_GA(self, "player"), "character", None) - _SA(self, "player", None) - - for script in _ScriptDB.objects.get_all_scripts_on_obj(self): - script.stop() - #for script in _GA(self, "scripts").all(): - # script.stop() - - # if self.player: - # self.player.user.is_active = False - # self.player.user.save( - - # Destroy any exits to and from this room, if any - _GA(self, "clear_exits")() - # Clear out any non-exit objects located within the object - _GA(self, "clear_contents")() - _GA(self, "attributes").clear() - _GA(self, "nicks").clear() - _GA(self, "aliases").clear() - - # Perform the deletion of the object - super(ObjectDB, self).delete() - return True - diff --git a/src/objects/objects.py b/src/objects/objects.py index 0a949eb93a..4bacf0336a 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -15,18 +15,76 @@ That an object is controlled by a player/user is just defined by its they control by simply linking to a new object's user property. """ +import traceback from django.conf import settings -from src.typeclasses.models import TypeclassBase + +from src.typeclasses.models import TypeclassBase, NickHandler from src.objects.manager import ObjectManager from src.objects.models import ObjectDB +from src.scripts.scripthandler import ScriptHandler from src.commands import cmdset, command -from src.utils.logger import log_depmsg +from src.commands.cmdsethandler import CmdSetHandler +from src.commands import cmdhandler +from src.utils.logger import log_depmsg, log_trace, log_errmsg +from src.utils.utils import (variable_from_module, lazy_property, + make_iter, to_str, to_unicode) -__all__ = ("Object", "Character", "Room", "Exit") +MULTISESSION_MODE = settings.MULTISESSION_MODE + +_ScriptDB = None +_SESSIONS = None + +_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) +# the sessid_max is based on the length of the db_sessid csv field (excluding commas) +_SESSID_MAX = 16 if MULTISESSION_MODE in (1, 3) else 1 + +from django.utils.translation import ugettext as _ + +class SessidHandler(object): + """ + Handles the get/setting of the sessid + comma-separated integer field + """ + def __init__(self, obj): + self.obj = obj + self._cache = set() + self._recache() + + def _recache(self): + self._cache = list(set(int(val) for val in (self.obj.db_sessid or "").split(",") if val)) + + def get(self): + "Returns a list of one or more session ids" + return self._cache + + def add(self, sessid): + "Add sessid to handler" + _cache = self._cache + if sessid not in _cache: + if len(_cache) >= _SESSID_MAX: + return + _cache.append(sessid) + self.obj.db_sessid = ",".join(str(val) for val in _cache) + self.obj.save(update_fields=["db_sessid"]) + + def remove(self, sessid): + "Remove sessid from handler" + _cache = self._cache + if sessid in _cache: + _cache.remove(sessid) + self.obj.db_sessid = ",".join(str(val) for val in _cache) + self.obj.save(update_fields=["db_sessid"]) + + def clear(self): + "Clear sessids" + self._cache = [] + self.obj.db_sessid = None + self.obj.save(update_fields=["db_sessid"]) + + def count(self): + "Return amount of sessions connected" + return len(self._cache) -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ # @@ -35,172 +93,227 @@ _DA = object.__delattr__ class DefaultObject(ObjectDB): """ - This is the base class for all in-game objects. Inherit from this - to create different types of objects in the game. + This is the root typeclass object, representing all entities + that have an actual presence in-game. Objects generally have a + location. They can also be manipulated and looked at. Most + game entities you define should inherit from Object at some distance. + Evennia defines some important subclasses of Object by default, namely + Characters, Exits and Rooms (see the bottom of this module). - """ + Note that all new Objects and their subclasses *must* always be + created using the ev.create_object() function. This is so the + typeclass system can be correctly initiated behind the scenes. + + + Object Typeclass API: + + * Available properties (only available on *initiated* typeclass objects) + + key (string) - name of object + name (string) - same as key + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + dbobj (Object, read-only) - link to database model. dbobj.typeclass + points back to this class + typeclass (Object, read-only) - this links back to this class as an + identified only. Use self.swap_typeclass() to switch. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + player (Player) - controlling player (if any, only set together with + sessid below) + sessid (int, read-only) - session id (if any, only set together with + player above) + location (Object) - current location. Is None if this is a room + home (Object) - safety start-location + sessions (list of Sessions, read-only) - returns all sessions + connected to this object + has_player (bool, read-only)- will only return *connected* players + contents (list of Objects, read-only) - returns all objects inside + this object (including exits) + exits (list of Objects, read-only) - returns all exits from this + object, if any + destination (Object) - only set if this object is an exit. + is_superuser (bool, read-only) - True/False if this user is a superuser + + * Handlers available + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + + * Helper methods (see src.objects.objects.py for full headers) + + search(ostring, global_search=False, use_nicks=True, + typeclass=None, + attribute_name=None, use_nicks=True, location=None, + quiet=False, exact=False) + execute_cmd(raw_string) + msg(text=None, from_obj=None, sessid=0, **kwargs) + msg_contents(message, exclude=None, from_obj=None, **kwargs) + move_to(destination, quiet=False, emit_to_obj=None, + use_destination=True, to_none=False) + copy(new_key=None) + delete() + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hook methods + + basetype_setup() - only called once, used for behind-the-scenes + setup. Normally not modified. + basetype_posthook_setup() - customization in basetype, after the + object has been created; Normally not modified. + + at_object_creation() - only called once, when object is first created. + Object customizations go here. + at_object_delete() - called just before deleting an object. If + returning False, deletion is aborted. Note that + all objects inside a deleted object are + automatically moved to their , they don't + need to be removed here. + + at_init() called whenever typeclass is cached from + memory, at least once every server restart/reload + at_cmdset_get(**kwargs) - this is called just before the command + handler requests a cmdset from this object, usually + without any kwargs + at_pre_puppet(player)- (player-controlled objects only) called just + before puppeting + at_post_puppet() - (player-controlled objects only) called just + after completing connection player<->object + at_pre_unpuppet() - (player-controlled objects only) called just + before un-puppeting + at_post_unpuppet(player) (player-controlled objects only) called + just after disconnecting player<->object link + at_server_reload() - called before server is reloaded + at_server_shutdown() - called just before server is fully shut down + + at_before_move(destination) called just before moving + object to the destination. If returns + False, move is cancelled. + announce_move_from(destination) - called in old location, just before + move, if obj.move_to() has + quiet=False + announce_move_to(source_location) - called in new location, + just after move, if obj.move_to() + has quiet=False + at_after_move(source_location) - always called after a move + has been successfully performed. + at_object_leave(obj, target_location) - called when an object leaves + this object in any fashion + at_object_receive(obj, source_location) - called when this object + receives another object + at_access(result, **kwargs) - this is called with the result of an + access call, along with any kwargs used + for that call. The return of this + method does not affect the result of the + lock check. + at_before_traverse(traversing_object) - (exit-objects only) called + just before an object + traverses this object + at_after_traverse(traversing_object, source_location) - (exit-objects + only) called just after a traversal has happened. + at_failed_traverse(traversing_object) - (exit-objects only) called + if traversal fails and property err_traverse is not defined. + + at_msg_receive(self, msg, from_obj=None, data=None) - called when a + message (via self.msg()) is sent to this obj. + If returns false, aborts send. + at_msg_send(self, msg, to_obj=None, data=None) - called when this + objects sends a message to someone via self.msg(). + + return_appearance(looker) - describes this object. Used by "look" + command by default + at_desc(looker=None) - called by 'look' whenever the appearance + is requested. + at_get(getter) - called after object has been picked up. + Does not stop pickup. + at_drop(dropper) - called when this object has been dropped. + at_say(speaker, message) - by default, called if an object inside + this object speaks + + """ + # typeclass setup __metaclass__ = TypeclassBase objects = ObjectManager() - # __init__ is only defined here in order to present docstring to API. - def __init__(self, *args, **kwargs): + # on-object properties + + @lazy_property + def cmdset(self): + return CmdSetHandler(self, True) + + @lazy_property + def scripts(self): + return ScriptHandler(self) + + @lazy_property + def nicks(self): + return NickHandler(self) + + @lazy_property + def sessid(self): + return SessidHandler(self) + + @property + def sessions(self): """ - This is the root typeclass object, representing all entities - that have an actual presence in-game. Objects generally have a - location. They can also be manipulated and looked at. Most - game entities you define should inherit from Object at some distance. - Evennia defines some important subclasses of Object by default, namely - Characters, Exits and Rooms (see the bottom of this module). + Retrieve sessions connected to this object. + """ + # if the player is not connected, this will simply be an empty list. + if self.db_player: + return self.db_player.get_all_sessions() + return [] - Note that all new Objects and their subclasses *must* always be - created using the ev.create_object() function. This is so the - typeclass system can be correctly initiated behind the scenes. + @property + def has_player(self): + """ + Convenience function for checking if an active player is + currently connected to this object + """ + return any(self.sessions) + @property + def is_superuser(self): + "Check if user has a player, and if so, if it is a superuser." + return self.db_player and self.db_player.is_superuser \ + and not self.db_player.attributes.get("_quell") - Object Typeclass API: + @property + def contents(self): + """ + Returns the contents of this object, i.e. all + objects that has this object set as its location. + This should be publically available. - * Available properties (only available on *initiated* typeclass objects) + exclude is one or more objects to not return + """ + return ObjectDB.objects.get_contents(self) - key (string) - name of object - name (string) - same as key - aliases (list of strings) - aliases to the object. Will be saved to - database as AliasDB entries but returned as strings. - dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass - points back to this class - typeclass (Object, read-only) - this links back to this class as an - identified only. Use self.swap_typeclass() to switch. - date_created (string) - time stamp of object creation - permissions (list of strings) - list of permission strings + @property + def exits(self): + """ + Returns all exits from this object, i.e. all objects + at this location having the property destination != None. + """ + return [exi for exi in self.contents if exi.destination] - player (Player) - controlling player (if any, only set together with - sessid below) - sessid (int, read-only) - session id (if any, only set together with - player above) - location (Object) - current location. Is None if this is a room - home (Object) - safety start-location - sessions (list of Sessions, read-only) - returns all sessions - connected to this object - has_player (bool, read-only)- will only return *connected* players - contents (list of Objects, read-only) - returns all objects inside - this object (including exits) - exits (list of Objects, read-only) - returns all exits from this - object, if any - destination (Object) - only set if this object is an exit. - is_superuser (bool, read-only) - True/False if this user is a superuser - - * Handlers available - - locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this - self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not - create a database entry when storing data - scripts - script-handler. Add new scripts to object with scripts.add() - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object - nicks - nick-handler. New nicks with nicks.add(). - - * Helper methods (see src.objects.objects.py for full headers) - - search(ostring, global_search=False, use_nicks=True, - typeclass=None, - attribute_name=None, use_nicks=True, location=None, - quiet=False, exact=False) - execute_cmd(raw_string) - msg(text=None, from_obj=None, sessid=0, **kwargs) - msg_contents(message, exclude=None, from_obj=None, **kwargs) - move_to(destination, quiet=False, emit_to_obj=None, - use_destination=True, to_none=False) - copy(new_key=None) - delete() - is_typeclass(typeclass, exact=False) - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - access(accessing_obj, access_type='read', default=False) - check_permstring(permstring) - - * Hook methods - - basetype_setup() - only called once, used for behind-the-scenes - setup. Normally not modified. - basetype_posthook_setup() - customization in basetype, after the - object has been created; Normally not modified. - - at_object_creation() - only called once, when object is first created. - Object customizations go here. - at_object_delete() - called just before deleting an object. If - returning False, deletion is aborted. Note that - all objects inside a deleted object are - automatically moved to their , they don't - need to be removed here. - - at_init() called whenever typeclass is cached from - memory, at least once every server restart/reload - at_cmdset_get(**kwargs) - this is called just before the command - handler requests a cmdset from this object, usually - without any kwargs - at_pre_puppet(player)- (player-controlled objects only) called just - before puppeting - at_post_puppet() - (player-controlled objects only) called just - after completing connection player<->object - at_pre_unpuppet() - (player-controlled objects only) called just - before un-puppeting - at_post_unpuppet(player) (player-controlled objects only) called - just after disconnecting player<->object link - at_server_reload() - called before server is reloaded - at_server_shutdown() - called just before server is fully shut down - - at_before_move(destination) called just before moving - object to the destination. If returns - False, move is cancelled. - announce_move_from(destination) - called in old location, just before - move, if obj.move_to() has - quiet=False - announce_move_to(source_location) - called in new location, - just after move, if obj.move_to() - has quiet=False - at_after_move(source_location) - always called after a move - has been successfully performed. - at_object_leave(obj, target_location) - called when an object leaves - this object in any fashion - at_object_receive(obj, source_location) - called when this object - receives another object - at_access(result, **kwargs) - this is called with the result of an - access call, along with any kwargs used - for that call. The return of this - method does not affect the result of the - lock check. - at_before_traverse(traversing_object) - (exit-objects only) called - just before an object - traverses this object - at_after_traverse(traversing_object, source_location) - (exit-objects - only) called just after a traversal has happened. - at_failed_traverse(traversing_object) - (exit-objects only) called - if traversal fails and property err_traverse is not defined. - - at_msg_receive(self, msg, from_obj=None, data=None) - called when a - message (via self.msg()) is sent to this obj. - If returns false, aborts send. - at_msg_send(self, msg, to_obj=None, data=None) - called when this - objects sends a message to someone via self.msg(). - - return_appearance(looker) - describes this object. Used by "look" - command by default - at_desc(looker=None) - called by 'look' whenever the appearance - is requested. - at_get(getter) - called after object has been picked up. - Does not stop pickup. - at_drop(dropper) - called when this object has been dropped. - at_say(speaker, message) - by default, called if an object inside - this object speaks - - """ - super(DefaultObject, self).__init__(*args, **kwargs) + # main methods ## methods inherited from the database object (overload them here) def search(self, searchdata, global_search=False, - use_nicks=True, + use_nicks=True, # should this default to off? typeclass=None, location=None, attribute_name=None, @@ -216,66 +329,96 @@ class DefaultObject(ObjectDB): Inputs: - searchdata (str): Primary search criterion. Will be matched against - object.key (with object.aliases second) - unless the keyword attribute_name specifies otherwise. - Special strings: - # - search by unique dbref. This is always a - global search. + searchdata (str or obj): Primary search criterion. Will be matched + against object.key (with object.aliases second) unless + the keyword attribute_name specifies otherwise. + Special strings: + # - search by unique dbref. This is always + a global search. me,self - self-reference to this object - here - current location - - - can be used to differentiate between - multiple same-named matches + - - can be used to differentiate + between multiple same-named matches global_search (bool): Search all objects globally. This is overruled by "location" keyword. use_nicks (bool): Use nickname-replace (nicktype "object") on the search string - typeclass (str or Typeclass): Limit search only to Objects with this - typeclass. May be a list of typeclasses for a - broader search. + typeclass (str or Typeclass, or list of either): Limit search only + to Objects with this typeclass. May be a list of typeclasses + for a broader search. location (Object): Specify a location to search, if different from the - self's given location - plus its contents. This can also be a list of locations. - attribute_name (str): Use this named Attribute to match ostring against, - instead of object.key. - quiet (bool) - don't display default error messages - return multiple - matches as a list and no matches as None. If not - set (default), will echo error messages and return None. + self's given location plus its contents. This can also + be a list of locations. + attribute_name (str): Define which property to search. If set, no + key+alias search will be performed. This can be used to + search database fields (db_ will be automatically + appended), and if that fails, it will try to return + objects having Attributes with this name and value + equal to searchdata. A special use is to search for + "key" here if you want to do a key-search without + including aliases. + quiet (bool) - don't display default error messages - this tells the + search method that the user wants to handle all errors + themselves. It also changes the return value type, see + below. exact (bool) - if unset (default) - prefers to match to beginning of - string rather than not matching at all. If set, - requires exact mathing of entire string. + string rather than not matching at all. If set, requires + exact mathing of entire string. Returns: - quiet=False (default): no match or multimatch: - auto-echoes errors to self.msg, then returns None + auto-echoes errors to self.msg, then returns None (results are handled by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) + and settings.SEARCH_AT_MULTIMATCH_INPUT) match: a unique object match quiet=True: - no match or multimatch: - returns None or list of multi-matches - match: - a unique object match + returns a list of 0, 1 or more matches """ - if isinstance(searchdata, basestring): + is_string = isinstance(searchdata, basestring) + + if is_string: # searchdata is a string; wrap some common self-references if searchdata.lower() in ("here", ): return self.location if searchdata.lower() in ("me", "self",): return self - return super(DefaultObject, self).search(searchdata, - global_search=global_search, - use_nicks=use_nicks, - typeclass=typeclass, - location=location, - attribute_name=attribute_name, - quiet=quiet, - exact=exact) + if use_nicks: + # do nick-replacement on search + searchdata = self.nicks.nickreplace(searchdata, categories=("object", "player"), include_player=True) + + candidates=None + if(global_search or (is_string and searchdata.startswith("#") and + len(searchdata) > 1 and searchdata[1:].isdigit())): + # only allow exact matching if searching the entire database + # or unique #dbrefs + exact = True + elif location: + # location(s) were given + candidates = [] + for obj in make_iter(location): + candidates.extend(obj.contents) + else: + # local search. Candidates are self.contents, self.location + # and self.location.contents + location = self.location + candidates = self.contents + if location: + candidates = candidates + [location] + location.contents + else: + # normally we are included in location.contents + candidates.append(self) + + results = ObjectDB.objects.object_search(searchdata, + attribute_name=attribute_name, + typeclass=typeclass, + candidates=candidates, + exact=exact) + if quiet: + return results + return _AT_SEARCH_RESULT(self, searchdata, results, global_search) def search_player(self, searchdata, quiet=False): """ @@ -305,19 +448,23 @@ class DefaultObject(ObjectDB): # searchdata is a string; wrap some common self-references if searchdata.lower() in ("me", "self",): return self.player - return super(DefaultObject, self).search_player(searchdata, quiet=quiet) + + results = self.player.__class__.objects.player_search(searchdata) + + if quiet: + return results + return _AT_SEARCH_RESULT(self, searchdata, results, global_search=True) def execute_cmd(self, raw_string, sessid=None, **kwargs): """ - Do something as this object. This command transparently - lets its typeclass execute the command. This method is - never called normally, it is only called explicitly in - code. + Do something as this object. This method is a copy of the execute_ + cmd method on the session. This is never called normally, it's only + used when wanting specifically to let an object be the caller of a + command. It makes use of nicks of eventual connected players as well. Argument: raw_string (string) - raw command input - sessid (int) - id of session executing the command. This sets the - sessid property on the command. + sessid (int) - optional session id to return results to **kwargs - other keyword arguments will be added to the found command object instace as variables before it executes. This is unused by default Evennia but may be used to set flags and @@ -329,45 +476,89 @@ class DefaultObject(ObjectDB): addCallback(function). This function will be called with an eventual return value from the command execution. - This return is not used at all by Evennia by default, but might be - useful for coders intending to implement some sort of nested + This return is not used at all by Evennia by default, but might + be useful for coders intending to implement some sort of nested command structure. """ - return super(DefaultObject, self).execute_cmd(raw_string, sessid=sessid, **kwargs) + # nick replacement - we require full-word matching. - def msg(self, text=None, from_obj=None, sessid=None, **kwargs): + # do text encoding conversion + raw_string = to_unicode(raw_string) + raw_string = self.nicks.nickreplace(raw_string, + categories=("inputline", "channel"), include_player=True) + return cmdhandler.cmdhandler(self, raw_string, callertype="object", sessid=sessid, **kwargs) + + + def msg(self, text=None, from_obj=None, sessid=0, **kwargs): """ - Emits something to any sessions attached to the object. + Emits something to a session attached to the object. message (str): The message to send from_obj (obj): object that is sending. data (object): an optional data object that may or may not be used by the protocol. - sessid: optional session target. If sessid=0, the session will - default to self.sessid or from_obj.sessid. + sessid (int): sessid to relay to, if any. + If set to 0 (default), use either from_obj.sessid (if set) or self.sessid automatically + If None, echo to all connected sessions + + When this message is called, from_obj.at_msg_send and self.at_msg_receive are called. + """ + global _SESSIONS + if not _SESSIONS: + from src.server.sessionhandler import SESSIONS as _SESSIONS - super(DefaultObject, self).msg(text=text, from_obj=from_obj, sessid=sessid, **kwargs) + text = to_str(text, force_string=True) if text else "" - def msg_contents(self, text=None, exclude=None, from_obj=None, **kwargs): + if "data" in kwargs: + # deprecation warning + log_depmsg("ObjectDB.msg(): 'data'-dict keyword is deprecated. Use **kwargs instead.") + data = kwargs.pop("data") + if isinstance(data, dict): + kwargs.update(data) + + if from_obj: + # call hook + try: + from_obj.at_msg_send(text=text, to_obj=self, **kwargs) + except Exception: + log_trace() + try: + if not self.at_msg_receive(text=text, **kwargs): + # if at_msg_receive returns false, we abort message to this object + return + except Exception: + log_trace() + + sessions = _SESSIONS.session_from_sessid([sessid] if sessid else make_iter(self.sessid.get())) + for session in sessions: + session.msg(text=text, **kwargs) + + def msg_contents(self, message, exclude=None, from_obj=None, **kwargs): """ Emits something to all objects inside an object. exclude is a list of objects not to send to. See self.msg() for more info. """ - super(DefaultObject, self).msg_contents(text, exclude=exclude, - from_obj=from_obj, **kwargs) + contents = self.contents + if exclude: + exclude = make_iter(exclude) + contents = [obj for obj in contents if obj not in exclude] + for obj in contents: + obj.msg(message, from_obj=from_obj, **kwargs) def move_to(self, destination, quiet=False, emit_to_obj=None, use_destination=True, to_none=False): """ + Moves this object to a new location. + Moves this object to a new location. Note that if is an exit object (i.e. it has "destination"!=None), the move_to will - happen to this destination and -not- into the exit object itself, - unless use_destination=False. Note that no lock checks are done by - this function, such things are assumed to have been handled before - calling move_to. + happen to this destination and -not- into the exit object itself, unless + use_destination=False. Note that no lock checks are done by this + function, such things are assumed to have been handled before calling + move_to. destination: (Object) Reference to the object to move to. This can also be an exit object, in which case the destination @@ -375,21 +566,175 @@ class DefaultObject(ObjectDB): quiet: (bool) If true, don't emit left/arrived messages. emit_to_obj: (Object) object to receive error messages use_destination (bool): Default is for objects to use the "destination" - property of destinations as the target to move to. - Turning off this keyword allows objects to move - "inside" exit objects. - to_none - allow destination to be None. Note that no hooks are run - when moving to a None location. If you want to run hooks, run - them manually (and make sure the hooks can handle a None - location). - Returns True/False depending on if there were problems with the move. - This method may also return various error messages to the - emit_to_obj. + property of destinations as the target to move to. + Turning off this keyword allows objects to move + "inside" exit objects. + to_none - allow destination to be None. Note that no hooks are run when + moving to a None location. If you want to run hooks, + run them manually (and make sure they can manage None + locations). + Returns True/False depending on if there were problems with the move. + This method may also return various error messages to the + emit_to_obj. """ - return super(DefaultObject, self).move_to(destination, quiet=quiet, - emit_to_obj=emit_to_obj, - use_destination=use_destination) + def logerr(string=""): + trc = traceback.format_exc() + errstring = "%s%s" % (trc, string) + log_trace() + self.msg(errstring) + + errtxt = _("Couldn't perform move ('%s'). Contact an admin.") + if not emit_to_obj: + emit_to_obj = self + + if not destination: + if to_none: + # immediately move to None. There can be no hooks called since + # there is no destination to call them with. + self.location = None + return True + emit_to_obj.msg(_("The destination doesn't exist.")) + return + if destination.destination and use_destination: + # traverse exits + destination = destination.destination + + # Before the move, call eventual pre-commands. + try: + if not self.at_before_move(destination): + return + except Exception: + logerr(errtxt % "at_before_move()") + #emit_to_obj.msg(errtxt % "at_before_move()") + #logger.log_trace() + return False + + # Save the old location + source_location = self.location + if not source_location: + # there was some error in placing this room. + # we have to set one or we won't be able to continue + if self.home: + source_location = self.home + else: + default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) + source_location = default_home + + # Call hook on source location + try: + source_location.at_object_leave(self, destination) + except Exception: + logerr(errtxt % "at_object_leave()") + #emit_to_obj.msg(errtxt % "at_object_leave()") + #logger.log_trace() + return False + + if not quiet: + #tell the old room we are leaving + try: + self.announce_move_from(destination) + except Exception: + logerr(errtxt % "at_announce_move()") + #emit_to_obj.msg(errtxt % "at_announce_move()" ) + #logger.log_trace() + return False + + # Perform move + try: + #print "move_to location:", destination + self.location = destination + except Exception: + emit_to_obj.msg(errtxt % "location change") + log_trace() + return False + + if not quiet: + # Tell the new room we are there. + try: + self.announce_move_to(source_location) + except Exception: + logerr(errtxt % "announce_move_to()") + #emit_to_obj.msg(errtxt % "announce_move_to()") + #logger.log_trace() + return False + + # Perform eventual extra commands on the receiving location + # (the object has already arrived at this point) + try: + destination.at_object_receive(self, source_location) + except Exception: + logerr(errtxt % "at_object_receive()") + #emit_to_obj.msg(errtxt % "at_object_receive()") + #logger.log_trace() + return False + + # Execute eventual extra commands on this object after moving it + # (usually calling 'look') + try: + self.at_after_move(source_location) + except Exception: + logerr(errtxt % "at_after_move") + #emit_to_obj.msg(errtxt % "at_after_move()") + #logger.log_trace() + return False + return True + + def clear_exits(self): + """ + Destroys all of the exits and any exits pointing to this + object as a destination. + """ + for out_exit in [exi for exi in ObjectDB.objects.get_contents(self) if exi.db_destination]: + out_exit.delete() + for in_exit in ObjectDB.objects.filter(db_destination=self): + in_exit.delete() + + def clear_contents(self): + """ + Moves all objects (players/things) to their home + location or to default home. + """ + # Gather up everything that thinks this is its location. + objs = ObjectDB.objects.filter(db_location=self) + default_home_id = int(settings.DEFAULT_HOME.lstrip("#")) + try: + default_home = ObjectDB.objects.get(id=default_home_id) + if default_home.dbid == self.dbid: + # we are deleting default home! + default_home = None + except Exception: + string = _("Could not find default home '(#%d)'.") + log_errmsg(string % default_home_id) + default_home = None + + for obj in objs: + home = obj.home + # Obviously, we can't send it back to here. + if not home or (home and home.dbid == self.dbid): + obj.home = default_home + home = default_home + + # If for some reason it's still None... + if not home: + string = "Missing default home, '%s(#%d)' " + string += "now has a null location." + obj.location = None + obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin.")) + log_errmsg(string % (obj.name, obj.dbid)) + return + + if obj.has_player: + if home: + string = "Your current location has ceased to exist," + string += " moving you to %s(#%d)." + obj.msg(_(string) % (home.name, home.dbid)) + else: + # Famous last words: The player should never see this. + string = "This place should not exist ... contact an admin." + obj.msg(_(string)) + obj.move_to(home) + def copy(self, new_key=None): """ @@ -398,113 +743,84 @@ class DefaultObject(ObjectDB): directly. new_key (string) - new key/name of copied object. If new_key is not - specified, the copy will be named - _copy by default. - Returns: DefaultObject (copy of this one) + specified, the copy will be named _copy + by default. + Returns: Object (copy of this one) """ - return super(DefaultObject, self).copy(new_key=new_key) + def find_clone_key(): + """ + Append 01, 02 etc to obj.key. Checks next higher number in the + same location, then adds the next number available + returns the new clone name on the form keyXX + """ + key = self.key + num = 1 + for obj in (obj for obj in self.location.contents + if obj.key.startswith(key) and + obj.key.lstrip(key).isdigit()): + num += 1 + return "%s%03i" % (key, num) + new_key = new_key or find_clone_key() + return ObjectDB.objects.copy_object(self, new_key=new_key) + + delete_iter = 0 def delete(self): """ Deletes this object. Before deletion, this method makes sure to move all contained objects to their respective home locations, as well as clean up all exits to/from the object. - - Returns: boolean True if deletion succeded, False if there - were errors during deletion or deletion otherwise - failed. """ - return super(DefaultObject, self).delete() + global _ScriptDB + if not _ScriptDB: + from src.scripts.models import ScriptDB as _ScriptDB - # methods inherited from the typeclass system - - def is_typeclass(self, typeclass, exact=False): - """ - Returns true if this object has this type - OR has a typeclass which is an subclass of - the given typeclass. - - typeclass - can be a class object or the - python path to such an object to match against. - - exact - returns true only if the object's - type is exactly this typeclass, ignoring - parents. - - Returns: Boolean - """ - return super(DefaultObject, self).is_typeclass(typeclass, exact=exact) - - def swap_typeclass(self, new_typeclass, clean_attributes=False, no_default=True): - """ - This performs an in-situ swap of the typeclass. This means - that in-game, this object will suddenly be something else. - Player will not be affected. To 'move' a player to a different - object entirely (while retaining this object's type), use - self.player.swap_object(). - - Note that this might be an error prone operation if the - old/new typeclass was heavily customized - your code - might expect one and not the other, so be careful to - bug test your code if using this feature! Often its easiest - to create a new object and just swap the player over to - that one instead. - - Arguments: - new_typeclass (path/classobj) - type to switch to - clean_attributes (bool/list) - will delete all attributes - stored on this object (but not any - of the database fields such as name or - location). You can't get attributes back, - but this is often the safest bet to make - sure nothing in the new typeclass clashes - with the old one. If you supply a list, - only those named attributes will be cleared. - no_default - if this is active, the swapper will not allow for - swapping to a default typeclass in case the given - one fails for some reason. Instead the old one - will be preserved. - Returns: - boolean True/False depending on if the swap worked or not. - - - """ - return super(DefaultObject, self).swap_typeclass(new_typeclass, - clean_attributes=clean_attributes, no_default=no_default) - - def access(self, accessing_obj, access_type='read', default=False, **kwargs): - """ - Determines if another object has permission to access this object in - whatever way. - - accessing_obj (Object)- object trying to access this one - access_type (string) - type of access sought - default (bool) - what to return if no lock of access_type was found - **kwargs - passed to at_access hook along with result,accessing_obj and access_type - """ - result = super(DefaultObject, self).access(accessing_obj, access_type=access_type, default=default) - self.at_access(result, accessing_obj, access_type, **kwargs) - return result - - # OBS: DEPRECATED! - if result: - self.at_access_success(accessing_obj, access_type) - return True - else: - self.at_access_failure(accessing_obj, access_type) + if self.delete_iter > 0: + # make sure to only call delete once on this object + # (avoid recursive loops) return False - def check_permstring(self, permstring): - """ - This explicitly checks the given string against this object's - 'permissions' property without involving any locks. + if not self.at_object_delete(): + # this is an extra pre-check + # run before deletio field-related properties + # is kicked into gear. + self.delete_iter = 0 + return False - permstring (string) - permission string that need to match a - permission on the object. - (example: 'Builders') - """ - return super(DefaultObject, self).check_permstring(permstring) + self.delete_iter += 1 + + # See if we need to kick the player off. + + for session in self.sessions: + session.msg(_("Your character %s has been destroyed.") % self.key) + # no need to disconnect, Player just jumps to OOC mode. + # sever the connection (important!) + if self.player: + self.player.character = None + self.player = None + + for script in _ScriptDB.objects.get_all_scripts_on_obj(self): + script.stop() + #for script in _GA(self, "scripts").all(): + # script.stop() + + # if self.player: + # self.player.user.is_active = False + # self.player.user.save( + + # Destroy any exits to and from this room, if any + self.clear_exits() + # Clear out any non-exit objects located within the object + self.clear_contents() + self.attributes.clear() + self.nicks.clear() + self.aliases.clear() + + # Perform the deletion of the object + super(ObjectDB, self).delete() + return True + # methods inherited from the typeclass system def __eq__(self, other): """ @@ -514,14 +830,61 @@ class DefaultObject(ObjectDB): parent doesn't work. """ try: - return _GA(_GA(self, "dbobj"), "dbid") == _GA(_GA(other, "dbobj"), "dbid") + return self.dbid == other.dbid except AttributeError: # compare players instead try: - return _GA(_GA(_GA(self, "dbobj"), "player"), "uid") == _GA(_GA(other, "player"), "uid") + return self.player.uid == other.player.uid except AttributeError: return False + def at_instance_creation(self): + """ + This is called by the typeclass system whenever an instance of + this class is saved for the first time. It is a generic hook + for calling the startup hooks for the various game entities. + When overloading you generally don't overload this but + overload the hooks called by this method. + """ + self.basetype_setup() + self.at_object_creation() + + if hasattr(self, "_createdict"): + # this will only be set if the utils.create function + # was used to create the object. We want the create + # call's kwargs to override the values set by hooks. + cdict = self._createdict + updates = [] + if not cdict["key"]: + self.db_key = "#%i" % self.dbid + updates.append("db_key") + elif self.key != cdict["key"]: + updates.append("db_key") + self.db_key = cdict["key"] + if cdict["location"] and self.location != cdict["location"]: + self.db_location = cdict["location"] + updates.append("db_location") + if cdict["home"] and self.home != cdict["home"]: + self.home = cdict["home"] + updates.append("db_home") + if cdict["destination"] and self.destination != cdict["destination"]: + self.destination = cdict["destination"] + updates.append("db_destination") + if updates: + self.save(update_fields=updates) + if cdict["permissions"]: + self.permissions.add(cdict["permissions"]) + if cdict["locks"]: + self.locks.add(cdict["locks"]) + if cdict["aliases"]: + self.aliases.add(cdict["aliases"]) + if cdict["location"]: + cdict["location"].at_object_receive(self, None) + self.at_after_move(None) + + self.basetype_posthook_setup() + + ## hooks called by the game engine def basetype_setup(self): @@ -550,17 +913,19 @@ class DefaultObject(ObjectDB): def basetype_posthook_setup(self): """ - Called once, after basetype_setup and at_object_creation. This should - generally not be overloaded unless you are redefining how a - room/exit/object works. It allows for basetype-like setup after the - object is created. An example of this is EXITs, who need to know keys, - aliases, locks etc to set up their exit-cmdsets. + Called once, after basetype_setup and at_object_creation. This + should generally not be overloaded unless you are redefining + how a room/exit/object works. It allows for basetype-like + setup after the object is created. An example of this is + EXITs, who need to know keys, aliases, locks etc to set up + their exit-cmdsets. """ pass def at_object_creation(self): """ - Called once, when this object is first created. + Called once, when this object is first created. This is + the normal hook to overload for most object types. """ pass @@ -574,6 +939,8 @@ class DefaultObject(ObjectDB): def at_init(self): """ + DEPRECATED: Use __init__ instead. + This is always called whenever this object is initiated -- that is, whenever it its typeclass is cached from memory. This happens on-demand first time the object is used or activated @@ -582,14 +949,13 @@ class DefaultObject(ObjectDB): """ pass - def at_cmdset_get(self, **kwargs): """ Called just before cmdsets on this object are requested by the command handler. If changes need to be done on the fly to the cmdset before passing them on to the cmdhandler, this is the - place to do it. This is called also if the object currently - have no cmdsets. **kwargs are usually not set but could be + place to do it. This is called also if the object currently + have no cmdsets. **kwargs are usually not set but could be used e.g. to force rebuilding of a dynamically created cmdset or similar. """ @@ -655,29 +1021,6 @@ class DefaultObject(ObjectDB): """ pass - def at_access_success(self, accessing_obj, access_type): - """ - OBS: DEPRECATED. Use at_access instead - - This hook is called whenever accessing_obj succeed a lock check of - type access_type on this object, for whatever reason. The return value - of this hook is not used, the lock will still pass regardless of what - this hook does (use lockstring/funcs to tweak the lock result). - """ - log_depmsg("at_access_success is deprecated. Use at_access(result,**kwargs) instead.") - pass - - def at_access_failure(self, accessing_obj, access_type): - """ - OBS: DEPRECATED. Use at_access instead - - This hook is called whenever accessing_obj fails a lock check of type - access_type on this object, for whatever reason. The return value of - this hook is not used, the lock will still fail regardless of what - this hook does (use lockstring/funcs to tweak the lock result). - """ - log_depmsg("at_access_failure is deprecated. Use at_access(result,**kwargs) instead.") - pass # hooks called when moving the object diff --git a/src/players/bots.py b/src/players/bots.py index 6a32b19261..2298758154 100644 --- a/src/players/bots.py +++ b/src/players/bots.py @@ -5,7 +5,7 @@ Player that are controlled by the server. """ from django.conf import settings -from src.players.player import Player +from src.players.player import DefaultPlayer from src.scripts.scripts import Script from src.commands.command import Command from src.commands.cmdset import CmdSet @@ -87,7 +87,7 @@ class BotCmdSet(CmdSet): # Bot base class -class Bot(Player): +class Bot(DefaultPlayer): """ A Bot will start itself when the server starts (it will generally not do so diff --git a/src/server/initial_setup.py b/src/server/initial_setup.py index 10b5e90490..524752babc 100644 --- a/src/server/initial_setup.py +++ b/src/server/initial_setup.py @@ -9,10 +9,10 @@ Everything starts at handle_setup() import django from django.conf import settings from django.contrib.auth import get_user_model +from django.utils.translation import ugettext as _ from src.server.models import ServerConfig from src.utils import create -from django.utils.translation import ugettext as _ - +from src.utils.utils import class_from_module def create_config_values(): """ @@ -26,10 +26,10 @@ def get_god_player(): """ Creates the god user. """ - PlayerDB = get_user_model() + Player = class_from_module(settings.BASE_PLAYER_TYPECLASS) try: - god_player = PlayerDB.objects.get(id=1) - except PlayerDB.DoesNotExist: + god_player = Player.objects.get(id=1) + except Player.DoesNotExist: txt = "\n\nNo superuser exists yet. The superuser is the 'owner'" txt += "\account on the Evennia server. Create a new superuser using" txt += "\nthe command" @@ -78,6 +78,7 @@ def create_objects(): god_character.save() god_player.attributes.add("_first_login", True) + print god_character god_player.attributes.add("_last_puppet", god_character) god_player.db._playable_characters.append(god_character) diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index d7433f3e39..fcecdbe31d 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -30,7 +30,9 @@ import sys import re import traceback import weakref -from importlib import import_module + +from django.db.models import signals +from django.dispatch import dispatcher from django.db import models from django.core.exceptions import ObjectDoesNotExist @@ -38,6 +40,7 @@ from django.conf import settings from django.utils.encoding import smart_str from src.utils.idmapper.models import SharedMemoryModel +from src.utils.idmapper.base import SharedMemoryModelBase from src.server.caches import get_prop_cache, set_prop_cache #from src.server.caches import set_attr_cache @@ -48,7 +51,8 @@ from src.locks.lockhandler import LockHandler #from src.utils import logger from django.db.models.base import ModelBase from src.utils.utils import ( - make_iter, is_iter, to_str, inherits_from, lazy_property) + make_iter, is_iter, to_str, inherits_from, lazy_property, + class_from_module) from src.utils.dbserialize import to_pickle, from_pickle from src.utils.picklefield import PickledObjectField @@ -56,6 +60,8 @@ __all__ = ("Attribute", "TypeNick", "TypedObject") TICKER_HANDLER = None +_DB_REGEX = re.compile(r"db$") + _PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] _TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE @@ -757,7 +763,16 @@ from django.apps.config import MODELS_MODULE_NAME from django.db.models.fields.related import OneToOneField #/ django patch imports -from src.utils.idmapper.base import SharedMemoryModelBase + +# signal receivers. Assigned in __new__ +def post_save(sender, instance, created, **kwargs): + """ + Is called Receive a signal just after the object is saved. + """ + if created: + instance.at_instance_creation() + #TODO - put OOB handler here? + class TypeclassBase(SharedMemoryModelBase): """ @@ -1013,10 +1028,14 @@ class TypeclassBase(SharedMemoryModelBase): new_class._prepare() new_class._meta.apps.register_model(new_class._meta.app_label, new_class) - return new_class + + #return new_class # /patch end + # attach signal + signals.post_save.connect(post_save, sender=new_class) + return new_class # @@ -1082,22 +1101,14 @@ class TypedObject(SharedMemoryModel): # typeclass mechanism - def _import_class(self, path): - path, clsname = path.rsplit(".", 1) - mod = import_module(path) - try: - return getattr(mod, clsname) - except AttributeError: - raise AttributeError("module '%s' has no attribute '%s'" % (path, clsname)) - def __init__(self, *args, **kwargs): typeclass_path = kwargs.pop("typeclass", None) super(TypedObject, self).__init__(*args, **kwargs) if typeclass_path: - self.__class__ = self._import_class(typeclass_path) + self.__class__ = class_from_module(typeclass_path) self.db_typclass_path = typeclass_path elif self.db_typeclass_path: - self.__class__ = self._import_class(self.db_typeclass_path) + self.__class__ = class_from_module(self.db_typeclass_path) else: self.db_typeclass_path = "%s.%s" % (self.__module__, self.__class__.__name__) # important to put this at the end since _meta is based on the set __class__ @@ -1372,12 +1383,12 @@ class TypedObject(SharedMemoryModel): self._is_deleted = True super(TypedObject, self).delete() - def save(self, *args, **kwargs): - "Block saving non-proxy typeclassed objects" - if not self._meta.proxy: - raise RuntimeError("Don't create instances of %s, " - "use its child typeclasses instead." % self.__class__.__name__) - super(TypedObject, self).save(*args, **kwargs) + #def save(self, *args, **kwargs): + # "Block saving non-proxy typeclassed objects" + # if not self._meta.proxy: + # raise RuntimeError("Don't create instances of %s, " + # "use its child typeclasses instead." % self.__class__.__name__) + # super(TypedObject, self).save(*args, **kwargs) # # Memory management diff --git a/src/utils/create.py b/src/utils/create.py index 43e2f04cd4..4ab9492eae 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -25,12 +25,12 @@ from django.conf import settings from django.db import IntegrityError from src.utils.idmapper.models import SharedMemoryModel from src.utils import utils, logger -from src.utils.utils import make_iter +from src.utils.utils import make_iter, class_from_module, dbid_to_obj # delayed imports _User = None -_Object = None _ObjectDB = None +_Object = None _Script = None _ScriptDB = None _HelpEntry = None @@ -58,11 +58,7 @@ def handle_dbref(inp, objclass, raise_errors=True): objects. """ if not (isinstance(inp, basestring) and inp.startswith("#")): - try: - return inp.dbobj - except AttributeError: - return inp - + return inp # a string, analyze it inp = inp.lstrip('#') try: @@ -87,119 +83,54 @@ def create_object(typeclass=None, key=None, location=None, home=None, permissions=None, locks=None, aliases=None, destination=None, report_to=None, nohome=False): """ - Create a new in-game object. Any game object is a combination - of a database object that stores data persistently to - the database, and a typeclass, which on-the-fly 'decorates' - the database object into whataver different type of object - it is supposed to be in the game. - See src.objects.managers for methods to manipulate existing objects - in the database. src.objects.objects holds the base typeclasses - and src.objects.models hold the database model. + Create a new in-game object. - report_to is an optional object for reporting errors to in string form. - If report_to is not set, errors will be raised as en Exception - containing the error message. If set, this method will return - None upon errors. - nohome - this allows the creation of objects without a default home location; - this only used when creating the default location itself or during unittests + keywords: + typeclass - class or python path to a typeclass + key - name of the new object. If not set, a name of #dbref will be set. + home - obj or #dbref to use as the object's home location + permissions - a comma-separated string of permissions + locks - one or more lockstrings, separated by semicolons + aliases - a list of alternative keys + destination - obj or #dbref to use as an Exit's target + + nohome - this allows the creation of objects without a default home location; + only used when creating the default location itself or during unittests """ - global _Object, _ObjectDB - if not _Object: - from src.objects.objects import Object as _Object - if not _ObjectDB: - from src.objects.models import ObjectDB as _ObjectDB + typeclass = typeclass if typeclass else settings.BASE_OBJECT_TYPECLASS - # input validation + if isinstance(typeclass, basestring): + # a path is given. Load the actual typeclass + typeclass = class_from_module(typeclass, settings.OBJECT_TYPECLASS_PATHS) - if not typeclass: - typeclass = settings.BASE_OBJECT_TYPECLASS - elif isinstance(typeclass, _ObjectDB): - # this is already an objectdb instance, extract its typeclass - typeclass = typeclass.typeclass.path - elif isinstance(typeclass, _Object) or utils.inherits_from(typeclass, _Object): - # this is already an object typeclass, extract its path - typeclass = typeclass.path - typeclass = utils.to_unicode(typeclass) + # Setup input for the create command. We use ObjectDB as baseclass here + # to give us maximum freedom (the typeclasses will load + # correctly when each object is recovered). - # Setup input for the create command - - location = handle_dbref(location, _ObjectDB) - destination = handle_dbref(destination, _ObjectDB) - home = handle_dbref(home, _ObjectDB) + location = dbid_to_obj(location, _ObjectDB) + destination = dbid_to_obj(destination, _ObjectDB) + home = dbid_to_obj(home, _ObjectDB) if not home: try: - home = handle_dbref(settings.DEFAULT_HOME, _ObjectDB) if not nohome else None + home = dbid_to_obj(settings.DEFAULT_HOME, _ObjectDB) if not nohome else None except _ObjectDB.DoesNotExist: raise _ObjectDB.DoesNotExist("settings.DEFAULT_HOME (= '%s') does not exist, or the setting is malformed." % settings.DEFAULT_HOME) - # create new database object all in one go - new_db_object = _ObjectDB(db_key=key, db_location=location, + # create new instance + new_object = typeclass(db_key=key, db_location=location, db_destination=destination, db_home=home, - db_typeclass_path=typeclass) - - if not key: - # the object should always have a key, so if not set we give a default - new_db_object.key = "#%i" % new_db_object.dbid - - # this will either load the typeclass or the default one (will also save object) - new_object = new_db_object.typeclass - - if not _GA(new_object, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input and it still - # gave us a default - try: - SharedMemoryModel.delete(new_db_object) - except AssertionError: - # this happens if object was never created - pass - if report_to: - report_to = handle_dbref(report_to, _ObjectDB) - _GA(report_to, "msg")("Error creating %s (%s).\n%s" % (new_db_object.key, typeclass, - _GA(new_db_object, "typeclass_last_errmsg"))) - return None - else: - raise Exception(_GA(new_db_object, "typeclass_last_errmsg")) - - # from now on we can use the typeclass object - # as if it was the database object. - - # call the hook methods. This is where all at_creation - # customization happens as the typeclass stores custom - # things on its database object. - - # note - this may override input keys, locations etc! - new_object.basetype_setup() # setup the basics of Exits, Characters etc. - new_object.at_object_creation() - - # we want the input to override that set in the hooks, so - # we re-apply those if needed - if new_object.key != key: - new_object.key = key - if new_object.location != location: - new_object.location = location - if new_object.home != home: - new_object.home = home - if new_object.destination != destination: - new_object.destination = destination - - # custom-given perms/locks do overwrite hooks - if permissions: - new_object.permissions.add(permissions) - if locks: - new_object.locks.add(locks) - if aliases: - new_object.aliases.add(aliases) - - # trigger relevant move_to hooks in order to display messages. - if location: - location.at_object_receive(new_object, None) - new_object.at_after_move(None) - - # post-hook setup (mainly used by Exits) - new_object.basetype_posthook_setup() - + db_typeclass_path=typeclass.path) + # store the call signature for the signal + new_object._createdict = {"key":key, "location":location, "destination":destination, + "home":home, "typeclass":typeclass.path, "permissions":permissions, + "locks":locks, "aliases":aliases, "destination":destination, + "report_to":report_to, "nohome":nohome} + # this will trigger the save signal which in turn calls the + # at_instance_creation hook on the typeclass, where the _createdict can be + # used. + new_object.save() return new_object #alias for create_object diff --git a/src/utils/utils.py b/src/utils/utils.py index 6305097e97..fa17d4e0e7 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -12,11 +12,11 @@ import imp import types import math import re -import importlib import textwrap import datetime import random import traceback +from importlib import import_module from inspect import ismodule from collections import defaultdict from twisted.internet import threads, defer, reactor @@ -347,16 +347,43 @@ def dbref(dbref, reqhash=True): Output is the integer part. """ if reqhash: - return (int(dbref.lstrip('#')) if (isinstance(dbref, basestring) and + num = (int(dbref.lstrip('#')) if (isinstance(dbref, basestring) and dbref.startswith("#") and dbref.lstrip('#').isdigit()) else None) + return num if num > 0 else None elif isinstance(dbref, basestring): dbref = dbref.lstrip('#') - return int(dbref) if dbref.isdigit() else None - return dbref if isinstance(dbref, int) else None + return int(dbref) if dbref.isdigit() and int(dbref) > 0 else None + else: + return dbref if isinstance(dbref, int) else None +def dbid_to_obj(inp, objclass, raise_errors=True): + """ + Convert a #dbid to a valid object of objclass. objclass + should be a valid object class to filter against (objclass.filter ...) + If not raise_errors is set, this will swallow errors of non-existing + objects. + """ + dbid = dbref(inp) + if not dbid: + # we only convert #dbrefs + return inp + try: + if int(inp) < 0: + return None + except ValueError: + return None + + # if we get to this point, inp is an integer dbref; get the matching object + try: + return objclass.objects.get(id=inp) + except Exception: + if raise_errors: + raise + return inp + def to_unicode(obj, encoding='utf-8', force_string=False): """ This decodes a suitable object to the unicode format. Note that @@ -887,15 +914,48 @@ def fuzzy_import_from_module(path, variable, default=None, defaultpaths=None): paths = [path] + make_iter(defaultpaths) for modpath in paths: try: - mod = importlib.import_module(path) + mod = import_module(path) except ImportError, ex: - if not str(ex) == "No module named %s" % path: + if not str(ex).startswith ("No module named %s" % path): # this means the module was found but it # triggers an ImportError on import. raise ex return getattr(mod, variable, default) return default +def class_from_module(path, defaultpaths=None): + """ + Return a class from a module, given the module's path. This is + primarily used to convert db_typeclass_path:s to classes. + + if a list of defaultpaths is given, try subsequent runs by + prepending those paths to the given path. + """ + cls = None + if defaultpaths: + paths = [path] + make_iter(defaultpaths) if defaultpaths else [] + else: + paths = [path] + + for path in paths: + path, clsname = path.rsplit(".", 1) + try: + mod = import_module(path) + except ImportError, ex: + # normally this is due to a not-found property + if not str(ex).startswith ("No module named %s" % path): + raise ex + try: + cls = getattr(mod, clsname) + break + except AttributeError, ex: + if not str(ex).startswith("Object 'module' has no attribute '%s'" % clsname): + raise ex + if not cls: + raise ImportError("Could not load typeclass '%s'." % path) + return cls + + def init_new_player(player): """ Helper method to call all hooks, set flags etc on a newly created