evennia/src/players/models.py

471 lines
17 KiB
Python

"""
Player
The player class is an extension of the default Django user class,
and is customized for the needs of Evennia.
We use the Player to store a more mud-friendly style of permission
system as well as to allow the admin more flexibility by storing
attributes on the Player. Within the game we should normally use the
Player manager's methods to create users so that permissions are set
correctly.
To make the Player model more flexible for your own game, it can also
persistently store attributes of its own. This is ideal for extra
account info and OOC account configuration variables etc.
"""
from django.conf import settings
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.encoding import smart_str
from src.players import manager
from src.scripts.models import ScriptDB
from src.typeclasses.models import (TypedObject, TagHandler, NickHandler,
AliasHandler, AttributeHandler)
from src.scripts.scripthandler import ScriptHandler
from src.commands.cmdsethandler import CmdSetHandler
from src.commands import cmdhandler
from src.utils import utils, logger
from src.utils.utils import to_str, make_iter
from django.utils.translation import ugettext as _
__all__ = ("PlayerDB",)
_ME = _("me")
_SELF = _("self")
_SESSIONS = None
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
_MULTISESSION_MODE = settings.MULTISESSION_MODE
_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
_TYPECLASS = None
#------------------------------------------------------------
#
# PlayerDB
#
#------------------------------------------------------------
class PlayerDB(TypedObject, AbstractUser):
"""
This is a special model using Django's 'profile' functionality
and extends the default Django User model. It is defined as such
by use of the variable AUTH_PROFILE_MODULE in the settings.
One accesses the fields/methods. We try use this model as much
as possible rather than User, since we can customize this to
our liking.
The TypedObject supplies the following (inherited) properties:
key - main name
typeclass_path - the path to the decorating typeclass
typeclass - auto-linked typeclass
date_created - time stamp of object creation
permissions - perm strings
dbref - #id of object
db - persistent attribute storage
ndb - non-persistent attribute storage
The PlayerDB adds the following properties:
user - Connected User object. django field, needs to be save():d.
name - alias for user.username
sessions - sessions connected to this player
is_superuser - bool if this player is a superuser
is_bot - bool if this player is a bot and not a real player
"""
#
# PlayerDB Database model setup
#
# inherited fields (from TypedObject):
# db_key, db_typeclass_path, db_date_created, db_permissions
# store a connected flag here too, not just in sessionhandler.
# This makes it easier to track from various out-of-process locations
db_is_connected = models.BooleanField(default=False,
verbose_name="is_connected",
help_text="If player is connected to game or not")
# database storage of persistant cmdsets.
db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True,
help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.")
# marks if this is a "virtual" bot player object
db_is_bot = models.BooleanField(default=False, verbose_name="is_bot", help_text="Used to identify irc/imc2/rss bots")
# Database manager
objects = manager.PlayerManager()
# caches for quick lookups
_typeclass_paths = settings.PLAYER_TYPECLASS_PATHS
_default_typeclass_path = settings.BASE_PLAYER_TYPECLASS or "src.players.player.Player"
class Meta:
app_label = 'players'
verbose_name = 'Player'
def __init__(self, *args, **kwargs):
"Parent must be initiated first"
TypedObject.__init__(self, *args, **kwargs)
# handlers
_SA(self, "cmdset", CmdSetHandler(self))
_GA(self, "cmdset").update(init_mode=True)
_SA(self, "scripts", ScriptHandler(self))
_SA(self, "attributes", AttributeHandler(self))
_SA(self, "nicks", NickHandler(self))
_SA(self, "tags", TagHandler(self))
_SA(self, "aliases", AliasHandler(self))
# alias to the objs property
def __characters_get(self):
return self.objs
def __characters_set(self, value):
self.objs = value
def __characters_del(self):
raise Exception("Cannot delete name")
characters = property(__characters_get, __characters_set, __characters_del)
# cmdset_storage property
# This seems very sensitive to caching, so leaving it be for now /Griatch
#@property
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
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
def cmdset_storage_del(self):
"Deleter. Allows for del self.name"
_SA(self, "db_cmdset_storage", None)
_GA(self, "save")()
cmdset_storage = property(cmdset_storage_get, cmdset_storage_set, cmdset_storage_del)
class Meta:
"Define Django meta options"
verbose_name = "Player"
verbose_name_plural = "Players"
#
# PlayerDB main class properties and methods
#
def __str__(self):
return smart_str("%s(player %s)" % (_GA(self, "name"), _GA(self, "dbid")))
def __unicode__(self):
return u"%s(player#%s)" % (_GA(self, "name"), _GA(self, "dbid"))
#@property
def __username_get(self):
return _GA(self, "username")
def __username_set(self, value):
_SA(self, "username", value)
def __username_del(self):
_DA(self, "username")
# aliases
name = property(__username_get, __username_set, __username_del)
key = property(__username_get, __username_set, __username_del)
#@property
def __uid_get(self):
"Getter. Retrieves the user id"
return self.id
def __uid_set(self, value):
raise Exception("User id cannot be set!")
def __uid_del(self):
raise Exception("User id cannot be deleted!")
uid = property(__uid_get, __uid_set, __uid_del)
#@property
#def __is_superuser_get(self):
# "Superusers have all permissions."
# return self.db_is_superuser
# #is_suser = get_prop_cache(self, "_is_superuser")
# #if is_suser == None:
# # is_suser = _GA(self, "user").is_superuser
# # set_prop_cache(self, "_is_superuser", is_suser)
# #return is_suser
#is_superuser = property(__is_superuser_get)
#
# PlayerDB class access methods
#
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).
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:
_GA(from_obj, "at_msg_send")(text=text, to_obj=self, **kwargs)
except Exception:
pass
session = _MULTISESSION_MODE == 2 and sessid and _GA(self, "get_session")(sessid) or None
if session:
obj = session.puppet
if obj and not obj.at_msg_receive(text=text, **kwargs):
# if hook returns false, cancel send
return
session.msg(text=text, **kwargs)
else:
# if no session was specified, send to them all
for sess in _GA(self, 'get_all_sessions')():
sess.msg(text=text, **kwargs)
# session-related methods
def get_session(self, sessid):
"""
Return session with given sessid connected to this player.
"""
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:
_GA(obj.typeclass, "at_pre_puppet")(self.typeclass, sessid=sessid)
# do the connection
obj.sessid = 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:
_GA(obj.typeclass, "at_post_puppet")()
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
_GA(obj.typeclass, "at_pre_unpuppet")()
del obj.dbobj.sessid
del obj.dbobj.player
session.puppet = None
session.puid = None
_GA(obj.typeclass, "at_post_unpuppet")(self.typeclass, sessid=sessid)
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,
character - return character if connected to this player, else None.
"""
session = self.get_session(sessid)
if not session:
return None
if return_dbobj:
return session.puppet
return session.puppet and session.puppet.typeclass 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.typeclass 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)
# 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."))
super(PlayerDB, self).delete(*args, **kwargs)
def execute_cmd(self, raw_string, sessid=None):
"""
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.
"""
raw_string = utils.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)
sessid = self.get_all_sessions()[0].sessid
return cmdhandler.cmdhandler(self.typeclass, raw_string,
callertype="player", sessid=sessid)
def search(self, ostring, return_puppet=False,
return_character=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.
return_character - will try to return the character the player controls
instead of the Player object itself. If no
Character 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.
"""
if return_character:
logger.log_depmsg("Player.search's 'return_character' keyword is deprecated. Use the return_puppet keyword instead.")
#return_puppet = return_character
# handle me, self
if ostring in (_ME, _SELF, '*' + _ME, '*' + _SELF):
return self
matches = _GA(self, "__class__").objects.player_search(ostring)
matches = _AT_SEARCH_RESULT(self, ostring, matches, global_search=True)
if matches and return_character:
try:
return _GA(matches, "character")
except:
pass
return matches