Reshuffling the Evennia package into the new template paradigm.
This commit is contained in:
parent
2846e64833
commit
2b3a32e447
371 changed files with 17250 additions and 304 deletions
13
lib/players/__init__.py
Normal file
13
lib/players/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""
|
||||
Makes it easier to import by grouping all relevant things already at this
|
||||
level.
|
||||
|
||||
You can henceforth import most things directly from src.player
|
||||
Also, the initiated object manager is available as src.players.manager.
|
||||
|
||||
"""
|
||||
|
||||
#from src.players.player import *
|
||||
#from src.players.models import PlayerDB
|
||||
#
|
||||
#manager = PlayerDB.objects
|
||||
230
lib/players/admin.py
Normal file
230
lib/players/admin.py
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
#
|
||||
# This sets up how models are displayed
|
||||
# in the web admin interface.
|
||||
#
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
|
||||
from src.players.models import PlayerDB
|
||||
from src.typeclasses.admin import AttributeInline, TagInline
|
||||
from src.utils import create
|
||||
|
||||
|
||||
# handle the custom User editor
|
||||
class PlayerDBChangeForm(UserChangeForm):
|
||||
|
||||
class Meta:
|
||||
model = PlayerDB
|
||||
fields = '__all__'
|
||||
|
||||
username = forms.RegexField(
|
||||
label="Username",
|
||||
max_length=30,
|
||||
regex=r'^[\w. @+-]+$',
|
||||
widget=forms.TextInput(
|
||||
attrs={'size': '30'}),
|
||||
error_messages={
|
||||
'invalid': "This value may contain only letters, spaces, numbers "
|
||||
"and @/./+/-/_ characters."},
|
||||
help_text="30 characters or fewer. Letters, spaces, digits and "
|
||||
"@/./+/-/_ only.")
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data['username']
|
||||
if username.upper() == self.instance.username.upper():
|
||||
return username
|
||||
elif PlayerDB.objects.filter(username__iexact=username):
|
||||
raise forms.ValidationError('A player with that name '
|
||||
'already exists.')
|
||||
return self.cleaned_data['username']
|
||||
|
||||
|
||||
class PlayerDBCreationForm(UserCreationForm):
|
||||
|
||||
class Meta:
|
||||
model = PlayerDB
|
||||
fields = '__all__'
|
||||
|
||||
username = forms.RegexField(
|
||||
label="Username",
|
||||
max_length=30,
|
||||
regex=r'^[\w. @+-]+$',
|
||||
widget=forms.TextInput(
|
||||
attrs={'size': '30'}),
|
||||
error_messages={
|
||||
'invalid': "This value may contain only letters, spaces, numbers "
|
||||
"and @/./+/-/_ characters."},
|
||||
help_text="30 characters or fewer. Letters, spaces, digits and "
|
||||
"@/./+/-/_ only.")
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data['username']
|
||||
if PlayerDB.objects.filter(username__iexact=username):
|
||||
raise forms.ValidationError('A player with that name already '
|
||||
'exists.')
|
||||
return username
|
||||
|
||||
|
||||
class PlayerForm(forms.ModelForm):
|
||||
"""
|
||||
Defines how to display Players
|
||||
"""
|
||||
class Meta:
|
||||
model = PlayerDB
|
||||
fields = '__all__'
|
||||
|
||||
db_key = forms.RegexField(
|
||||
label="Username",
|
||||
initial="PlayerDummy",
|
||||
max_length=30,
|
||||
regex=r'^[\w. @+-]+$',
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'size': '30'}),
|
||||
error_messages={
|
||||
'invalid': "This value may contain only letters, spaces, numbers"
|
||||
" and @/./+/-/_ characters."},
|
||||
help_text="This should be the same as the connected Player's key "
|
||||
"name. 30 characters or fewer. Letters, spaces, digits and "
|
||||
"@/./+/-/_ only.")
|
||||
|
||||
db_typeclass_path = forms.CharField(
|
||||
label="Typeclass",
|
||||
initial=settings.BASE_PLAYER_TYPECLASS,
|
||||
widget=forms.TextInput(
|
||||
attrs={'size': '78'}),
|
||||
help_text="Required. Defines what 'type' of entity this is. This "
|
||||
"variable holds a Python path to a module with a valid "
|
||||
"Evennia Typeclass. Defaults to "
|
||||
"settings.BASE_PLAYER_TYPECLASS.")
|
||||
|
||||
db_permissions = forms.CharField(
|
||||
label="Permissions",
|
||||
initial=settings.PERMISSION_PLAYER_DEFAULT,
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'size': '78'}),
|
||||
help_text="In-game permissions. A comma-separated list of text "
|
||||
"strings checked by certain locks. They are often used for "
|
||||
"hierarchies, such as letting a Player have permission "
|
||||
"'Wizards', 'Builders' etc. A Player permission can be "
|
||||
"overloaded by the permissions of a controlled Character. "
|
||||
"Normal players use 'Players' by default.")
|
||||
|
||||
db_lock_storage = forms.CharField(
|
||||
label="Locks",
|
||||
widget=forms.Textarea(attrs={'cols': '100', 'rows': '2'}),
|
||||
required=False,
|
||||
help_text="In-game lock definition string. If not given, defaults "
|
||||
"will be used. This string should be on the form "
|
||||
"<i>type:lockfunction(args);type2:lockfunction2(args);...")
|
||||
db_cmdset_storage = forms.CharField(
|
||||
label="cmdset",
|
||||
initial=settings.CMDSET_PLAYER,
|
||||
widget=forms.TextInput(attrs={'size': '78'}),
|
||||
required=False,
|
||||
help_text="python path to player cmdset class (set in "
|
||||
"settings.CMDSET_PLAYER by default)")
|
||||
|
||||
|
||||
class PlayerInline(admin.StackedInline):
|
||||
"""
|
||||
Inline creation of Player
|
||||
"""
|
||||
model = PlayerDB
|
||||
template = "admin/players/stacked.html"
|
||||
form = PlayerForm
|
||||
fieldsets = (
|
||||
("In-game Permissions and Locks",
|
||||
{'fields': ('db_lock_storage',),
|
||||
#{'fields': ('db_permissions', 'db_lock_storage'),
|
||||
'description': "<i>These are permissions/locks for in-game use. "
|
||||
"They are unrelated to website access rights.</i>"}),
|
||||
("In-game Player data",
|
||||
{'fields': ('db_typeclass_path', 'db_cmdset_storage'),
|
||||
'description': "<i>These fields define in-game-specific properties "
|
||||
"for the Player object in-game.</i>"}))
|
||||
|
||||
extra = 1
|
||||
max_num = 1
|
||||
|
||||
|
||||
class PlayerTagInline(TagInline):
|
||||
model = PlayerDB.db_tags.through
|
||||
|
||||
|
||||
class PlayerAttributeInline(AttributeInline):
|
||||
model = PlayerDB.db_attributes.through
|
||||
|
||||
|
||||
class PlayerDBAdmin(BaseUserAdmin):
|
||||
"""
|
||||
This is the main creation screen for Users/players
|
||||
"""
|
||||
|
||||
list_display = ('username', 'email', 'is_staff', 'is_superuser')
|
||||
form = PlayerDBChangeForm
|
||||
add_form = PlayerDBCreationForm
|
||||
inlines = [PlayerTagInline, PlayerAttributeInline]
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password', 'email')}),
|
||||
('Website profile', {
|
||||
'fields': ('first_name', 'last_name'),
|
||||
'description': "<i>These are not used "
|
||||
"in the default system.</i>"}),
|
||||
('Website dates', {
|
||||
'fields': ('last_login', 'date_joined'),
|
||||
'description': '<i>Relevant only to the website.</i>'}),
|
||||
('Website Permissions', {
|
||||
'fields': ('is_active', 'is_staff', 'is_superuser',
|
||||
'user_permissions', 'groups'),
|
||||
'description': "<i>These are permissions/permission groups for "
|
||||
"accessing the admin site. They are unrelated to "
|
||||
"in-game access rights.</i>"}),
|
||||
('Game Options', {
|
||||
'fields': ('db_typeclass_path', 'db_cmdset_storage',
|
||||
'db_lock_storage'),
|
||||
'description': '<i>These are attributes that are more relevant '
|
||||
'to gameplay.</i>'}))
|
||||
# ('Game Options', {'fields': (
|
||||
# 'db_typeclass_path', 'db_cmdset_storage',
|
||||
# 'db_permissions', 'db_lock_storage'),
|
||||
# 'description': '<i>These are attributes that are '
|
||||
# 'more relevant to gameplay.</i>'}))
|
||||
|
||||
add_fieldsets = (
|
||||
(None,
|
||||
{'fields': ('username', 'password1', 'password2', 'email'),
|
||||
'description': "<i>These account details are shared by the admin "
|
||||
"system and the game.</i>"},),)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.save()
|
||||
if not change:
|
||||
#calling hooks for new player
|
||||
ply = obj
|
||||
ply.basetype_setup()
|
||||
ply.at_player_creation()
|
||||
|
||||
## TODO! Remove User reference!
|
||||
#def save_formset(self, request, form, formset, change):
|
||||
# """
|
||||
# Run all hooks on the player object
|
||||
# """
|
||||
# super(PlayerDBAdmin, self).save_formset(request, form, formset, change)
|
||||
# userobj = form.instance
|
||||
# userobj.name = userobj.username
|
||||
# if not change:
|
||||
# # uname, passwd, email = str(request.POST.get(u"username")), \
|
||||
# # str(request.POST.get(u"password1")), \
|
||||
# # str(request.POST.get(u"email"))
|
||||
# typeclass = str(request.POST.get(
|
||||
# u"playerdb_set-0-db_typeclass_path"))
|
||||
# create.create_player("", "", "",
|
||||
# user=userobj,
|
||||
# typeclass=typeclass,
|
||||
# player_dbobj=userobj)
|
||||
|
||||
admin.site.register(PlayerDB, PlayerDBAdmin)
|
||||
334
lib/players/bots.py
Normal file
334
lib/players/bots.py
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
"""
|
||||
Bots are a special child typeclasses of
|
||||
Player that are controlled by the server.
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from src.players.player import DefaultPlayer
|
||||
from src.scripts.scripts import Script
|
||||
from src.commands.command import Command
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.utils import search
|
||||
|
||||
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||
|
||||
_SESSIONS = None
|
||||
|
||||
|
||||
# Bot helper utilities
|
||||
|
||||
class BotStarter(Script):
|
||||
"""
|
||||
This non-repeating script has the
|
||||
sole purpose of kicking its bot
|
||||
into gear when it is initialized.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = "botstarter"
|
||||
self.desc = "bot start/keepalive"
|
||||
self.persistent = True
|
||||
self.db.started = False
|
||||
if _IDLE_TIMEOUT > 0:
|
||||
# call before idle_timeout triggers
|
||||
self.interval = int(max(60, _IDLE_TIMEOUT * 0.90))
|
||||
self.start_delay = True
|
||||
|
||||
def at_start(self):
|
||||
"Kick bot into gear"
|
||||
if not self.db.started:
|
||||
self.player.start()
|
||||
self.db.started = True
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
Called self.interval seconds to keep connection. We cannot use
|
||||
the IDLE command from inside the game since the system will
|
||||
not catch it (commands executed from the server side usually
|
||||
has no sessions). So we update the idle counter manually here
|
||||
instead. This keeps the bot getting hit by IDLE_TIMEOUT.
|
||||
"""
|
||||
global _SESSIONS
|
||||
if not _SESSIONS:
|
||||
from src.server.sessionhandler import SESSIONS as _SESSIONS
|
||||
for session in _SESSIONS.sessions_from_player(self.player):
|
||||
session.update_session_counters(idle=True)
|
||||
|
||||
def at_server_reload(self):
|
||||
"""
|
||||
If server reloads we don't need to reconnect the protocol
|
||||
again, this is handled by the portal reconnect mechanism.
|
||||
"""
|
||||
self.db.started = True
|
||||
|
||||
def at_server_shutdown(self):
|
||||
"Make sure we are shutdown"
|
||||
self.db.started = False
|
||||
|
||||
|
||||
class CmdBotListen(Command):
|
||||
"""
|
||||
This is a command that absorbs input
|
||||
aimed specifically at the bot. The session
|
||||
must prepend its data with bot_data_in for
|
||||
this to trigger.
|
||||
"""
|
||||
key = "bot_data_in"
|
||||
def func(self):
|
||||
"Relay to typeclass"
|
||||
self.obj.execute_cmd(self.args.strip(), sessid=self.sessid)
|
||||
|
||||
class BotCmdSet(CmdSet):
|
||||
"Holds the BotListen command"
|
||||
key = "botcmdset"
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdBotListen())
|
||||
|
||||
|
||||
# Bot base class
|
||||
|
||||
class Bot(DefaultPlayer):
|
||||
"""
|
||||
A Bot will start itself when the server
|
||||
starts (it will generally not do so
|
||||
on a reload - that will be handled by the
|
||||
normal Portal session resync)
|
||||
"""
|
||||
|
||||
def basetype_setup(self):
|
||||
"""
|
||||
This sets up the basic properties for the bot.
|
||||
"""
|
||||
# the text encoding to use.
|
||||
self.db.encoding = "utf-8"
|
||||
# A basic security setup
|
||||
lockstring = "examine:perm(Wizards);edit:perm(Wizards);delete:perm(Wizards);boot:perm(Wizards);msg:false()"
|
||||
self.locks.add(lockstring)
|
||||
# set the basics of being a bot
|
||||
self.cmdset.add_default(BotCmdSet)
|
||||
script_key = "%s" % self.key
|
||||
self.scripts.add(BotStarter, key=script_key)
|
||||
self.is_bot = True
|
||||
|
||||
def start(self, **kwargs):
|
||||
"""
|
||||
This starts the bot, whatever that may mean.
|
||||
"""
|
||||
pass
|
||||
|
||||
def msg(self, text=None, from_obj=None, sessid=None, **kwargs):
|
||||
"""
|
||||
Evennia -> outgoing protocol
|
||||
"""
|
||||
pass
|
||||
|
||||
def execute_cmd(self, raw_string, sessid=None):
|
||||
"""
|
||||
Incoming protocol -> Evennia
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_shutdown(self):
|
||||
"We need to handle this case manually since the shutdown may be a reset"
|
||||
print "bots at_server_shutdown called"
|
||||
for session in self.get_all_sessions():
|
||||
session.sessionhandler.disconnect(session)
|
||||
|
||||
|
||||
# Bot implementations
|
||||
|
||||
# IRC
|
||||
|
||||
class IRCBot(Bot):
|
||||
"""
|
||||
Bot for handling IRC connections.
|
||||
"""
|
||||
def start(self, ev_channel=None, irc_botname=None, irc_channel=None, irc_network=None, irc_port=None):
|
||||
"""
|
||||
Start by telling the portal to start a new session.
|
||||
|
||||
ev_channel - key of the Evennia channel to connect to
|
||||
irc_botname - name of bot to connect to irc channel. If not set, use self.key
|
||||
irc_channel - name of channel on the form #channelname
|
||||
irc_network - url of network, like irc.freenode.net
|
||||
irc_port - port number of irc network, like 6667
|
||||
"""
|
||||
global _SESSIONS
|
||||
if not _SESSIONS:
|
||||
from src.server.sessionhandler import SESSIONS as _SESSIONS
|
||||
|
||||
# if keywords are given, store (the BotStarter script
|
||||
# will not give any keywords, so this should normally only
|
||||
# happen at initialization)
|
||||
if irc_botname:
|
||||
self.db.irc_botname = irc_botname
|
||||
elif not self.db.irc_botname:
|
||||
self.db.irc_botname = self.key
|
||||
if ev_channel:
|
||||
# connect to Evennia channel
|
||||
channel = search.channel_search(ev_channel)
|
||||
if not channel:
|
||||
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
|
||||
channel = channel[0]
|
||||
channel.connect(self)
|
||||
self.db.ev_channel = channel
|
||||
if irc_channel:
|
||||
self.db.irc_channel = irc_channel
|
||||
if irc_network:
|
||||
self.db.irc_network = irc_network
|
||||
if irc_port:
|
||||
self.db.irc_port = irc_port
|
||||
|
||||
# instruct the server and portal to create a new session with
|
||||
# the stored configuration
|
||||
configdict = {"uid":self.dbid,
|
||||
"botname": self.db.irc_botname,
|
||||
"channel": self.db.irc_channel ,
|
||||
"network": self.db.irc_network,
|
||||
"port": self.db.irc_port}
|
||||
_SESSIONS.start_bot_session("src.server.portal.irc.IRCBotFactory", configdict)
|
||||
|
||||
def msg(self, text=None, **kwargs):
|
||||
"""
|
||||
Takes text from connected channel (only)
|
||||
"""
|
||||
if not self.ndb.ev_channel and self.db.ev_channel:
|
||||
# cache channel lookup
|
||||
self.ndb.ev_channel = self.db.ev_channel
|
||||
if "from_channel" in kwargs and text and self.ndb.ev_channel.dbid == kwargs["from_channel"]:
|
||||
if "from_obj" not in kwargs or kwargs["from_obj"] != [self.id]:
|
||||
text = "bot_data_out %s" % text
|
||||
self.msg(text=text)
|
||||
|
||||
def execute_cmd(self, text=None, sessid=None):
|
||||
"""
|
||||
Take incoming data and send it to connected channel. This is triggered
|
||||
by the CmdListen command in the BotCmdSet.
|
||||
"""
|
||||
if not self.ndb.ev_channel and self.db.ev_channel:
|
||||
# cache channel lookup
|
||||
self.ndb.ev_channel = self.db.ev_channel
|
||||
if self.ndb.ev_channel:
|
||||
self.ndb.ev_channel.msg(text, senders=self.id)
|
||||
|
||||
|
||||
# RSS
|
||||
|
||||
class RSSBot(Bot):
|
||||
"""
|
||||
An RSS relayer. The RSS protocol itself runs a ticker to update its feed at regular
|
||||
intervals.
|
||||
"""
|
||||
def start(self, ev_channel=None, rss_url=None, rss_rate=None):
|
||||
"""
|
||||
Start by telling the portal to start a new RSS session
|
||||
|
||||
ev_channel - key of the Evennia channel to connect to
|
||||
rss_url - full URL to the RSS feed to subscribe to
|
||||
rss_update_rate - how often for the feedreader to update
|
||||
"""
|
||||
global _SESSIONS
|
||||
if not _SESSIONS:
|
||||
from src.server.sessionhandler import SESSIONS as _SESSIONS
|
||||
|
||||
if ev_channel:
|
||||
# connect to Evennia channel
|
||||
channel = search.channel_search(ev_channel)
|
||||
if not channel:
|
||||
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
|
||||
channel = channel[0]
|
||||
self.db.ev_channel = channel
|
||||
if rss_url:
|
||||
self.db.rss_url = rss_url
|
||||
if rss_rate:
|
||||
self.db.rss_rate = rss_rate
|
||||
# instruct the server and portal to create a new session with
|
||||
# the stored configuration
|
||||
configdict = {"uid": self.dbid,
|
||||
"url": self.db.rss_url,
|
||||
"rate": self.db.rss_rate}
|
||||
_SESSIONS.start_bot_session("src.server.portal.rss.RSSBotFactory", configdict)
|
||||
|
||||
def execute_cmd(self, text=None, sessid=None):
|
||||
"""
|
||||
Echo RSS input to connected channel
|
||||
"""
|
||||
print "execute_cmd rss:", text
|
||||
if not self.ndb.ev_channel and self.db.ev_channel:
|
||||
# cache channel lookup
|
||||
self.ndb.ev_channel = self.db.ev_channel
|
||||
if self.ndb.ev_channel:
|
||||
self.ndb.ev_channel.msg(text, senders=self.id)
|
||||
|
||||
class IMC2Bot(Bot):
|
||||
"""
|
||||
IMC2 Bot
|
||||
"""
|
||||
def start(self, ev_channel=None, imc2_network=None, imc2_mudname=None,
|
||||
imc2_port=None, imc2_client_pwd=None, imc2_server_pwd=None):
|
||||
"""
|
||||
Start by telling the portal to start a new session
|
||||
ev_channel - key of the Evennia channel to connect to
|
||||
imc2_network - IMC2 network name
|
||||
imc2_mudname - registered mudname (if not given, use settings.SERVERNAME)
|
||||
imc2_port - port number of IMC2 network
|
||||
imc2_client_pwd - client password registered with IMC2 network
|
||||
imc2_server_pwd - server password registered with IMC2 network
|
||||
"""
|
||||
global _SESSIONS
|
||||
if not _SESSIONS:
|
||||
from src.server.sessionhandler import SESSIONS as _SESSIONS
|
||||
if ev_channel:
|
||||
# connect to Evennia channel
|
||||
channel = search.channel_search(ev_channel)
|
||||
if not channel:
|
||||
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
|
||||
channel = channel[0]
|
||||
channel.connect(self)
|
||||
self.db.ev_channel = channel
|
||||
if imc2_network:
|
||||
self.db.imc2_network = imc2_network
|
||||
if imc2_port:
|
||||
self.db.imc2_port = imc2_port
|
||||
if imc2_mudname:
|
||||
self.db.imc2_mudname = imc2_mudname
|
||||
elif not self.db.imc2_mudname:
|
||||
self.db.imc2_mudname = settings.SERVERNAME
|
||||
# storing imc2 passwords in attributes - a possible
|
||||
# security issue?
|
||||
if imc2_server_pwd:
|
||||
self.db.imc2_server_pwd = imc2_server_pwd
|
||||
if imc2_client_pwd:
|
||||
self.db.imc2_client_pwd = imc2_client_pwd
|
||||
|
||||
configdict = {"uid": self.dbid,
|
||||
"mudname": self.db.imc2_mudname,
|
||||
"network": self.db.imc2_network,
|
||||
"port": self.db.imc2_port,
|
||||
"client_pwd": self.db.client_pwd,
|
||||
"server_pwd": self.db.server_pwd}
|
||||
|
||||
_SESSIONS.start_bot_session("src.server.portal.imc2.IMC2BotFactory", configdict)
|
||||
|
||||
def msg(self, text=None, **kwargs):
|
||||
"""
|
||||
Takes text from connected channel (only)
|
||||
"""
|
||||
if not self.ndb.ev_channel and self.db.ev_channel:
|
||||
# cache channel lookup
|
||||
self.ndb.ev_channel = self.db.ev_channel
|
||||
if "from_channel" in kwargs and text and self.ndb.ev_channel.dbid == kwargs["from_channel"]:
|
||||
if "from_obj" not in kwargs or kwargs["from_obj"] != [self.id]:
|
||||
text = "bot_data_out %s" % text
|
||||
self.msg(text=text)
|
||||
|
||||
def execute_cmd(self, text=None, sessid=None):
|
||||
"""
|
||||
Relay incoming data to connected channel.
|
||||
"""
|
||||
if not self.ndb.ev_channel and self.db.ev_channel:
|
||||
# cache channel lookup
|
||||
self.ndb.ev_channel = self.db.ev_channel
|
||||
if self.ndb.ev_channel:
|
||||
self.ndb.ev_channel.msg(text, senders=self.id)
|
||||
|
||||
155
lib/players/manager.py
Normal file
155
lib/players/manager.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
"""
|
||||
The managers for the custom Player object and permissions.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from django.contrib.auth.models import UserManager
|
||||
#from functools import update_wrapper
|
||||
from src.typeclasses.managers import (returns_typeclass_list, returns_typeclass,
|
||||
TypedObjectManager, TypeclassManager)
|
||||
#from src.utils import logger
|
||||
__all__ = ("PlayerManager",)
|
||||
|
||||
|
||||
#
|
||||
# Player Manager
|
||||
#
|
||||
|
||||
class PlayerDBManager(TypedObjectManager, UserManager):
|
||||
"""
|
||||
This PlayerManager implements methods for searching
|
||||
and manipulating Players directly from the database.
|
||||
|
||||
Evennia-specific search methods (will return Characters if
|
||||
possible or a Typeclass/list of Typeclassed objects, whereas
|
||||
Django-general methods will return Querysets or database objects):
|
||||
|
||||
dbref (converter)
|
||||
dbref_search
|
||||
get_dbref_range
|
||||
object_totals
|
||||
typeclass_search
|
||||
num_total_players
|
||||
get_connected_players
|
||||
get_recently_created_players
|
||||
get_recently_connected_players
|
||||
get_player_from_email
|
||||
get_player_from_uid
|
||||
get_player_from_name
|
||||
player_search (equivalent to ev.search_player)
|
||||
#swap_character
|
||||
|
||||
"""
|
||||
def num_total_players(self):
|
||||
"""
|
||||
Returns the total number of registered players.
|
||||
"""
|
||||
return self.count()
|
||||
|
||||
@returns_typeclass_list
|
||||
def get_connected_players(self):
|
||||
"""
|
||||
Returns a list of player objects with currently connected users/players.
|
||||
"""
|
||||
return self.filter(db_is_connected=True)
|
||||
|
||||
@returns_typeclass_list
|
||||
def get_recently_created_players(self, days=7):
|
||||
"""
|
||||
Returns a QuerySet containing the player User accounts that have been
|
||||
connected within the last <days> days.
|
||||
"""
|
||||
end_date = datetime.datetime.now()
|
||||
tdelta = datetime.timedelta(days)
|
||||
start_date = end_date - tdelta
|
||||
return self.filter(date_joined__range=(start_date, end_date))
|
||||
|
||||
@returns_typeclass_list
|
||||
def get_recently_connected_players(self, days=7):
|
||||
"""
|
||||
Returns a QuerySet containing the player accounts that have been
|
||||
connected within the last <days> days.
|
||||
|
||||
days - number of days backwards to check
|
||||
"""
|
||||
end_date = datetime.datetime.now()
|
||||
tdelta = datetime.timedelta(days)
|
||||
start_date = end_date - tdelta
|
||||
return self.filter(last_login__range=(
|
||||
start_date, end_date)).order_by('-last_login')
|
||||
|
||||
@returns_typeclass
|
||||
def get_player_from_email(self, uemail):
|
||||
"""
|
||||
Returns a player object when given an email address.
|
||||
"""
|
||||
return self.filter(email__iexact=uemail)
|
||||
|
||||
@returns_typeclass
|
||||
def get_player_from_uid(self, uid):
|
||||
"""
|
||||
Returns a player object based on User id.
|
||||
"""
|
||||
try:
|
||||
return self.get(id=uid)
|
||||
except self.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
@returns_typeclass
|
||||
def get_player_from_name(self, uname):
|
||||
"Get player object based on name"
|
||||
try:
|
||||
return self.get(username__iexact=uname)
|
||||
except self.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
@returns_typeclass_list
|
||||
def player_search(self, ostring, exact=True):
|
||||
"""
|
||||
Searches for a particular player by name or
|
||||
database id.
|
||||
|
||||
ostring - a string or database id.
|
||||
exact - allow for a partial match
|
||||
"""
|
||||
dbref = self.dbref(ostring)
|
||||
if dbref or dbref == 0:
|
||||
# bref search is always exact
|
||||
matches = self.filter(id=dbref)
|
||||
if matches:
|
||||
return matches
|
||||
if exact:
|
||||
return self.filter(username__iexact=ostring)
|
||||
else:
|
||||
return self.filter(username__icontains=ostring)
|
||||
|
||||
# def swap_character(self, player, new_character, delete_old_character=False):
|
||||
# """
|
||||
# This disconnects a player from the current character (if any) and
|
||||
# connects to a new character object.
|
||||
#
|
||||
# """
|
||||
#
|
||||
# if new_character.player:
|
||||
# # the new character is already linked to a player!
|
||||
# return False
|
||||
#
|
||||
# # do the swap
|
||||
# old_character = player.character
|
||||
# if old_character:
|
||||
# old_character.player = None
|
||||
# try:
|
||||
# player.character = new_character
|
||||
# new_character.player = player
|
||||
# except Exception:
|
||||
# # recover old setup
|
||||
# if old_character:
|
||||
# old_character.player = player
|
||||
# player.character = old_character
|
||||
# return False
|
||||
# if old_character and delete_old_character:
|
||||
# old_character.delete()
|
||||
# return True
|
||||
|
||||
class PlayerManager(PlayerDBManager, TypeclassManager):
|
||||
pass
|
||||
49
lib/players/migrations/0001_initial.py
Normal file
49
lib/players/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.utils.timezone
|
||||
import django.core.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
('typeclasses', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PlayerDB',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, max_length=30, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')])),
|
||||
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
|
||||
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
|
||||
('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('db_key', models.CharField(max_length=255, verbose_name=b'key', db_index=True)),
|
||||
('db_typeclass_path', models.CharField(help_text=b"this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name=b'typeclass')),
|
||||
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
|
||||
('db_lock_storage', models.TextField(help_text=b"locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name=b'locks', blank=True)),
|
||||
('db_is_connected', models.BooleanField(default=False, help_text=b'If player is connected to game or not', verbose_name=b'is_connected')),
|
||||
('db_cmdset_storage', models.CharField(help_text=b'optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.', max_length=255, null=True, verbose_name=b'cmdset')),
|
||||
('db_is_bot', models.BooleanField(default=False, help_text=b'Used to identify irc/imc2/rss bots', verbose_name=b'is_bot')),
|
||||
('db_attributes', models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute', null=True)),
|
||||
('db_tags', models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag', null=True)),
|
||||
('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Player',
|
||||
'verbose_name_plural': 'Players',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
1
lib/players/migrations/__init__.py
Normal file
1
lib/players/migrations/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
173
lib/players/models.py
Normal file
173
lib/players/models.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
"""
|
||||
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.manager import PlayerDBManager
|
||||
from src.typeclasses.models import TypedObject
|
||||
from src.utils.utils import make_iter
|
||||
|
||||
__all__ = ("PlayerDB",)
|
||||
|
||||
#_ME = _("me")
|
||||
#_SELF = _("self")
|
||||
|
||||
_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 = PlayerDBManager()
|
||||
|
||||
class Meta:
|
||||
app_label = 'players'
|
||||
verbose_name = 'Player'
|
||||
|
||||
# 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 = 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)
|
||||
|
||||
#
|
||||
# property/field access
|
||||
#
|
||||
|
||||
def __str__(self):
|
||||
return smart_str("%s(player %s)" % (self.name, self.dbid))
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s(player#%s)" % (self.name, self.dbid)
|
||||
|
||||
#@property
|
||||
def __username_get(self):
|
||||
return self.username
|
||||
|
||||
def __username_set(self, value):
|
||||
self.username = value
|
||||
self.save(update_fields=["username"])
|
||||
|
||||
def __username_del(self):
|
||||
del 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)
|
||||
704
lib/players/player.py
Normal file
704
lib/players/player.py
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
"""
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue