""" Typeclass for Player objects Note that this object is primarily intended to store OOC information, not game info! This object represents the actual user (not their character) and has NO actual precence in the game world (this is handled by the associated character object, so you should customize that instead for most things). """ import datetime from django.conf import settings from src.typeclasses.models import TypeclassBase from src.players.manager import PlayerManager from src.players.models import PlayerDB from src.comms.models import ChannelDB from src.commands import cmdhandler from src.scripts.models import ScriptDB from src.utils import logger from src.utils.utils import (lazy_property, to_str, make_iter, to_unicode, variable_from_module) from src.typeclasses.attributes import NickHandler from src.scripts.scripthandler import ScriptHandler from src.commands.cmdsethandler import CmdSetHandler from django.utils.translation import ugettext as _ __all__ = ("DefaultPlayer",) _SESSIONS = None _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) _MULTISESSION_MODE = settings.MULTISESSION_MODE _CMDSET_PLAYER = settings.CMDSET_PLAYER _CONNECT_CHANNEL = None class DefaultPlayer(PlayerDB): """ This is the base Typeclass for all Players. Players represent the person playing the game and tracks account info, password etc. They are OOC entities without presence in-game. A Player can connect to a Character Object in order to "enter" the game. Player Typeclass API: * Available properties (only available on initiated typeclass objects) key (string) - name of player name (string)- wrapper for user.username 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. date_created (string) - time stamp of object creation permissions (list of strings) - list of permission strings user (User, read-only) - django User authorization object obj (Object) - game object controlled by player. 'character' can also be used. sessions (list of Sessions) - sessions connected to this player is_superuser (bool, read-only) - if the connected user is a superuser * Handlers 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 msg(outgoing_string, from_obj=None, **kwargs) #swap_character(new_character, delete_old_character=False) execute_cmd(raw_string) search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, player=False) 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() at_player_creation() - note that the following hooks are also found on Objects and are usually handled on the character level: at_init() at_access() at_cmdset_get(**kwargs) at_first_login() at_post_login(sessid=None) at_disconnect() at_message_receive() at_message_send() at_server_reload() at_server_shutdown() """ __metaclass__ = TypeclassBase objects = PlayerManager() # 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) # session-related methods def get_session(self, sessid): """ Return session with given sessid connected to this player. note that the sessionhandler also accepts sessid as an iterable. """ global _SESSIONS if not _SESSIONS: from src.server.sessionhandler import SESSIONS as _SESSIONS return _SESSIONS.session_from_player(self, sessid) def get_all_sessions(self): "Return all sessions connected to this player" global _SESSIONS if not _SESSIONS: from src.server.sessionhandler import SESSIONS as _SESSIONS return _SESSIONS.sessions_from_player(self) sessions = property(get_all_sessions) # alias shortcut def disconnect_session_from_player(self, sessid): """ Access method for disconnecting a given session from the player (connection happens automatically in the sessionhandler) """ # this should only be one value, loop just to make sure to # clean everything sessions = (session for session in self.get_all_sessions() if session.sessid == sessid) for session in sessions: # this will also trigger unpuppeting session.sessionhandler.disconnect(session) # puppeting operations def puppet_object(self, sessid, obj, normal_mode=True): """ Use the given session to control (puppet) the given object (usually a Character type). Note that we make no puppet checks here, that must have been done before calling this method. sessid - session id of session to connect obj - the object to connect to normal_mode - trigger hooks and extra checks - this is turned off when the server reloads, to quickly re-connect puppets. returns True if successful, False otherwise """ session = self.get_session(sessid) if not session: return False if normal_mode and session.puppet: # cleanly unpuppet eventual previous object puppeted by this session self.unpuppet_object(sessid) if obj.player and obj.player.is_connected and obj.player != self: # we don't allow to puppet an object already controlled by an active # player. To kick a player, call unpuppet_object on them explicitly. return # if we get to this point the character is ready to puppet or it # was left with a lingering player/sessid reference from an unclean # server kill or similar if normal_mode: obj.at_pre_puppet(self, sessid=sessid) # do the connection obj.sessid.add(sessid) obj.player = self session.puid = obj.id session.puppet = obj # validate/start persistent scripts on object ScriptDB.objects.validate(obj=obj) if normal_mode: obj.at_post_puppet() # re-cache locks to make sure superuser bypass is updated obj.locks.cache_lock_bypass(obj) return True def unpuppet_object(self, sessid): """ Disengage control over an object sessid - the session id to disengage returns True if successful """ session = self.get_session(sessid) if not session: return False obj = hasattr(session, "puppet") and session.puppet or None if not obj: return False # do the disconnect, but only if we are the last session to puppet obj.at_pre_unpuppet() obj.sessid.remove(sessid) if not obj.sessid.count(): del obj.player obj.at_post_unpuppet(self, sessid=sessid) session.puppet = None session.puid = None return True def unpuppet_all(self): """ Disconnect all puppets. This is called by server before a reset/shutdown. """ for session in self.get_all_sessions(): self.unpuppet_object(session.sessid) def get_puppet(self, sessid, return_dbobj=False): """ Get an object puppeted by this session through this player. This is the main method for retrieving the puppeted object from the player's end. sessid - return character connected to this sessid, """ session = self.get_session(sessid) if not session: return None if return_dbobj: return session.puppet return session.puppet and session.puppet or None def get_all_puppets(self, return_dbobj=False): """ Get all currently puppeted objects as a list """ puppets = [session.puppet for session in self.get_all_sessions() if session.puppet] if return_dbobj: return puppets return [puppet for puppet in puppets] def __get_single_puppet(self): """ This is a legacy convenience link for users of MULTISESSION_MODE 0 or 1. It will return only the first puppet. For mode 2, this returns a list of all characters. """ puppets = self.get_all_puppets() if _MULTISESSION_MODE in (0, 1): return puppets and puppets[0] or None return puppets character = property(__get_single_puppet) puppet = property(__get_single_puppet) # utility methods def delete(self, *args, **kwargs): """ Deletes the player permanently. """ for session in self.get_all_sessions(): # unpuppeting all objects and disconnecting the user, if any # sessions remain (should usually be handled from the # deleting command) self.unpuppet_object(session.sessid) session.sessionhandler.disconnect(session, reason=_("Player being deleted.")) self.scripts.stop() self.attributes.clear() self.nicks.clear() self.aliases.clear() super(PlayerDB, self).delete(*args, **kwargs) ## methods inherited from database model def msg(self, text=None, from_obj=None, sessid=None, **kwargs): """ Evennia -> User This is the main route for sending data back to the user from the server. outgoing_string (string) - text data to send from_obj (Object/Player) - source object of message to send. Its at_msg_send() hook will be called. sessid - the session id of the session to send to. If not given, return to all sessions connected to this player. This is usually only relevant when using msg() directly from a player-command (from a command on a Character, the character automatically stores and handles the sessid). Can also be a list of sessids. kwargs (dict) - All other keywords are parsed as extra data. """ if "data" in kwargs: # deprecation warning logger.log_depmsg("PlayerDB:msg() 'data'-dict keyword is deprecated. Use **kwargs instead.") data = kwargs.pop("data") if isinstance(data, dict): kwargs.update(data) text = to_str(text, force_string=True) if text else "" if from_obj: # call hook try: from_obj.at_msg_send(text=text, to_obj=self, **kwargs) except Exception: pass sessions = _MULTISESSION_MODE > 1 and sessid and self.get_session(sessid) or None if sessions: for session in make_iter(sessions): obj = session.puppet if obj and not obj.at_msg_receive(text=text, **kwargs): # if hook returns false, cancel send continue session.msg(text=text, **kwargs) else: # if no session was specified, send to them all for sess in self.get_all_sessions(): sess.msg(text=text, **kwargs) def execute_cmd(self, raw_string, sessid=None, **kwargs): """ Do something as this player. This method is never called normally, but only when the player object itself is supposed to execute the command. It takes player nicks into account, but not nicks of eventual puppets. raw_string - raw command input coming from the command line. sessid - the optional session id to be responsible for the command-send **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. """ raw_string = to_unicode(raw_string) raw_string = self.nicks.nickreplace(raw_string, categories=("inputline", "channel"), include_player=False) if not sessid and _MULTISESSION_MODE in (0, 1): # in this case, we should either have only one sessid, or the sessid # should not matter (since the return goes to all of them we can # just use the first one as the source) try: sessid = self.get_all_sessions()[0].sessid except IndexError: # this can happen for bots sessid = None return cmdhandler.cmdhandler(self, raw_string, callertype="player", sessid=sessid, **kwargs) def search(self, searchdata, return_puppet=False, **kwargs): """ This is similar to the ObjectDB search method but will search for Players only. Errors will be echoed, and None returned if no Player is found. searchdata - search criterion, the Player's key or dbref to search for return_puppet - will try to return the object the player controls instead of the Player object itself. If no puppeted object exists (since Player is OOC), None will be returned. Extra keywords are ignored, but are allowed in call in order to make API more consistent with objects.models.TypedObject.search. """ # handle me, self and *me, *self if isinstance(searchdata, basestring): # handle wrapping of common terms if searchdata.lower() in ("me", "*me", "self", "*self",): return self matches = self.__class__.objects.player_search(searchdata) matches = _AT_SEARCH_RESULT(self, searchdata, matches, global_search=True) if matches and return_puppet: try: return matches.puppet except AttributeError: return None return matches 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(DefaultPlayer, 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. """ super(DefaultPlayer, 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 the at_access hook along with the result. """ result = super(DefaultPlayer, self).access(accessing_obj, access_type=access_type, default=default) self.at_access(result, accessing_obj, access_type, **kwargs) return result def check_permstring(self, permstring): """ This explicitly checks the given string against this object's 'permissions' property without involving any locks. permstring (string) - permission string that need to match a permission on the object. (example: 'Builders') Note that this method does -not- call the at_access hook. """ return super(DefaultPlayer, self).check_permstring(permstring) ## player hooks def basetype_setup(self): """ This sets up the basic properties for a player. Overload this with at_player_creation rather than changing this method. """ # A basic security setup lockstring = "examine:perm(Wizards);edit:perm(Wizards);delete:perm(Wizards);boot:perm(Wizards);msg:all()" self.locks.add(lockstring) # The ooc player cmdset self.cmdset.add_default(_CMDSET_PLAYER, permanent=True) def at_player_creation(self): """ This is called once, the very first time the player is created (i.e. first time they register with the game). It's a good place to store attributes all players should have, like configuration values etc. """ # set an (empty) attribute holding the characters this player has lockstring = "attrread:perm(Admins);attredit:perm(Admins);attrcreate:perm(Admins)" self.attributes.add("_playable_characters", [], lockstring=lockstring) # TODO - handle this in __init__ instead. def at_init(self): """ 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 in some way after being created but also after each server restart or reload. In the case of player objects, this usually happens the moment the player logs in or reconnects after a reload. """ pass # Note that the hooks below also exist in the character object's # typeclass. You can often ignore these and rely on the character # ones instead, unless you are implementing a multi-character game # and have some things that should be done regardless of which # character is currently connected to this player. def at_first_save(self): """ This is a generic hook called by Evennia when this object is saved to the database the very first time. You generally don't override this method but the hooks called by it. """ self.basetype_setup() self.at_player_creation() permissions = settings.PERMISSION_PLAYER_DEFAULT if hasattr(self, "_createdict"): # this will only be set if the utils.create_player # function was used to create the object. cdict = self._createdict if cdict.get("locks"): self.locks.add(cdict["locks"]) if cdict.get("permissions"): permissions = cdict["permissions"] del self._createdict self.permissions.add(permissions) def at_access(self, result, accessing_obj, access_type, **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. It can be used e.g. to customize error messages in a central location or other effects based on the access result. """ pass def at_cmdset_get(self, **kwargs): """ Called just before cmdsets on this player 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 player currently have no cmdsets. kwargs are usually not used unless the cmdset is generated dynamically. """ pass def at_first_login(self): """ Called the very first time this player logs into the game. """ pass def at_pre_login(self): """ Called every time the user logs in, just before the actual login-state is set. """ pass def _send_to_connect_channel(self, message): "Helper method for loading the default comm channel" global _CONNECT_CHANNEL if not _CONNECT_CHANNEL: try: _CONNECT_CHANNEL = ChannelDB.objects.filter(db_key=settings.CHANNEL_CONNECTINFO[0])[0] except Exception: logger.log_trace() now = datetime.datetime.now() now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute) if _CONNECT_CHANNEL: _CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message)) else: logger.log_infomsg("[%s]: %s" % (now, message)) def at_post_login(self, sessid=None): """ Called at the end of the login process, just before letting them loose. This is called before an eventual Character's at_post_login hook. """ self._send_to_connect_channel("{G%s connected{n" % self.key) if _MULTISESSION_MODE == 0: # in this mode we should have only one character available. We # try to auto-connect to it by calling the @ic command # (this relies on player.db._last_puppet being set) self.execute_cmd("@ic", sessid=sessid) elif _MULTISESSION_MODE == 1: # in this mode the first session to connect acts like mode 0, # the following sessions "share" the same view and should # not perform any actions if not self.get_all_puppets(): self.execute_cmd("@ic", sessid=sessid) elif _MULTISESSION_MODE in (2, 3): # In this mode we by default end up at a character selection # screen. We execute look on the player. self.execute_cmd("look", sessid=sessid) def at_disconnect(self, reason=None): """ Called just before user is disconnected. """ reason = reason and "(%s)" % reason or "" self._send_to_connect_channel("{R%s disconnected %s{n" % (self.key, reason)) def at_post_disconnect(self): """ This is called after disconnection is complete. No messages can be relayed to the player from here. After this call, the player should not be accessed any more, making this a good spot for deleting it (in the case of a guest player account, for example). """ pass def at_message_receive(self, message, from_obj=None): """ Called when any text is emitted to this object. If it returns False, no text will be sent automatically. """ return True def at_message_send(self, message, to_object): """ Called whenever this object tries to send text to another object. Only called if the object supplied itself as a sender in the msg() call. """ pass def at_server_reload(self): """ This hook is called whenever the server is shutting down for restart/reboot. If you want to, for example, save non-persistent properties across a restart, this is the place to do it. """ pass def at_server_shutdown(self): """ This hook is called whenever the server is shutting down fully (i.e. not for a restart). """ pass class Guest(DefaultPlayer): """ This class is used for guest logins. Unlike Players, Guests and their characters are deleted after disconnection. """ def at_post_login(self, sessid=None): """ In theory, guests only have one character regardless of which MULTISESSION_MODE we're in. They don't get a choice. """ self._send_to_connect_channel("{G%s connected{n" % self.key) self.execute_cmd("@ic", sessid=sessid) def at_disconnect(self): """ A Guest's characters aren't meant to linger on the server. When a Guest disconnects, we remove its character. """ super(Guest, self).at_disconnect() characters = self.db._playable_characters for character in filter(None, characters): character.delete() def at_server_shutdown(self): """ We repeat at_disconnect() here just to be on the safe side. """ super(Guest, self).at_server_shutdown() characters = self.db._playable_characters for character in filter(None, characters): character.delete() def at_post_disconnect(self): """ Guests aren't meant to linger on the server, either. We need to wait until after the Guest disconnects to delete it, though. """ super(Guest, self).at_post_disconnect() self.delete()