Trunk: Merged griatch-branch. This implements a new reload mechanism - splitting Evennia into two processes: Server and Portal with different tasks. Also cleans and fixes several bugs in script systems as well as introduces i18n (courtesy of raydeejay).

This commit is contained in:
Griatch 2011-09-03 10:22:19 +00:00
parent 14dae44a46
commit f13e8cdf7c
50 changed files with 3175 additions and 2565 deletions

View file

@ -1,31 +1,72 @@
"""
This module handles sessions of users connecting
to the server.
This module defines handlers for storing sessions when handles
sessions of users connecting to the server.
Since Evennia supports several different connection
protocols, it is important to have a joint place
to store session info. It also makes it easier
to dispatch data.
Whereas server.py handles all setup of the server
and database itself, this file handles all that
comes after initial startup.
All new sessions (of whatever protocol) are responsible for
registering themselves with this module.
There are two similar but separate stores of sessions:
ServerSessionHandler - this stores generic game sessions
for the game. These sessions has no knowledge about
how they are connected to the world.
PortalSessionHandler - this stores sessions created by
twisted protocols. These are dumb connectors that
handle network communication but holds no game info.
"""
import time
from django.conf import settings
from django.contrib.auth.models import User
from src.server.models import ServerConfig
from src.utils import utils
# i18n
from django.utils.translation import ugettext as _
ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
#------------------------------------------------------------
# SessionHandler class
#------------------------------------------------------------
class SessionHandler(object):
"""
This handler holds a stack of sessions.
"""
def __init__(self):
"""
Init the handler.
"""
self.sessions = {}
def get_sessions(self, include_unloggedin=False):
"""
Returns the connected session objects.
"""
if include_unloggedin:
return self.sessions.values()
else:
return [session for session in self.sessions.values() if session.logged_in]
def get_session(self, sessid):
"""
Get session by sessid
"""
return self.sessions.get(sessid, None)
def get_all_sync_data(self):
"""
Create a dictionary of sessdata dicts representing all
sessions in store.
"""
sessdict = {}
for sess in self.sessions.values():
# copy all relevant data from all sessions
sessdict[sess.sessid] = sess.get_sync_data()
return sessdict
#------------------------------------------------------------
# Server-SessionHandler class
#------------------------------------------------------------
class ServerSessionHandler(SessionHandler):
"""
This object holds the stack of sessions active in the game at
any time.
@ -38,102 +79,144 @@ class SessionHandler(object):
"""
# AMP communication methods
def __init__(self):
"""
Init the handler. We track two types of sessions, those
who have just connected (unloggedin) and those who have
logged in (authenticated).
Init the handler.
"""
self.unloggedin = []
self.loggedin = []
# we keep a link to the server here, for the rest of the game to access.
self.sessions = {}
self.server = None
def add_unloggedin_session(self, session):
def portal_connect(self, sessid, session):
"""
Call at first connect. This adds a not-yet authenticated session.
"""
self.unloggedin.insert(0, session)
Called by Portal when a new session has connected.
Creates a new, unlogged-in game session.
"""
self.sessions[sessid] = session
session.execute_cmd('look')
def portal_disconnect(self, sessid):
"""
Called by Portal when portal reports a closing of a session
from the portal side.
"""
session = self.sessions.get(sessid, None)
if session:
del self.sessions[session.sessid]
self.session_count(-1)
def portal_session_sync(self, sesslist):
"""
Syncing all session ids of the portal with the ones of the server. This is instantiated
by the portal when reconnecting.
def add_loggedin_session(self, session):
sesslist is a complete list of (sessid, session) pairs, matching the list on the portal.
if session was logged in, the amp handler will have logged them in before this point.
"""
for sess in self.sessions.values():
# we delete the old session to make sure to catch eventual lingering references.
del sess
for sess in sesslist:
self.sessions[sess.sessid] = sess
sess.at_sync()
def portal_shutdown(self):
"""
Called by server when shutting down the portal.
"""
self.server.amp_protocol.call_remote_PortalAdmin(0,
operation='SSHUTD',
data="")
# server-side access methods
def disconnect(self, session, reason=""):
"""
Called from server side to remove session and inform portal
of this fact.
"""
session = self.sessions.get(session.sessid, None)
if session:
sessid = session.sessid
del self.sessions[sessid]
# inform portal that session should be closed.
self.server.amp_protocol.call_remote_PortalAdmin(sessid,
operation='SDISCONN',
data=reason)
self.session_count(-1)
def login(self, session):
"""
Log in the previously unloggedin session and the player we by
now should know is connected to it. After this point we
assume the session to be logged in one way or another.
"""
# prep the session with player/user info
if not ALLOW_MULTISESSION:
# disconnect previous sessions.
self.disconnect_duplicate_sessions(session)
# store/move the session to the right list
try:
self.unloggedin.remove(session)
except ValueError:
pass
self.loggedin.insert(0, session)
session.logged_in = True
self.session_count(1)
# sync the portal to this session
sessdata = session.get_sync_data()
self.server.amp_protocol.call_remote_PortalAdmin(session.sessid,
operation='SLOGIN',
data=sessdata)
def session_sync(self):
"""
This is called by the server when it reboots. It syncs all session data
to the portal.
"""
sessdata = self.get_all_sync_data()
self.server.amp_protocol.call_remote_PortalAdmin(0,
'SSYNC',
data=sessdata)
def remove_session(self, session):
"""
Remove session from the handler
"""
removed = False
try:
self.unloggedin.remove(session)
except Exception:
try:
self.loggedin.remove(session)
except Exception:
return
self.session_count(-1)
def get_sessions(self, include_unloggedin=False):
"""
Returns the connected session objects.
"""
if include_unloggedin:
return self.loggedin + self.unloggedin
else:
return self.loggedin
def disconnect_all_sessions(self, reason="You have been disconnected."):
"""
Cleanly disconnect all of the connected sessions.
"""
sessions = self.get_sessions(include_unloggedin=True)
for session in sessions:
session.at_data_out(reason)
session.session_disconnect()
for session in self.sessions:
del session
self.session_count(0)
# tell portal to disconnect all sessions
self.server.amp_protocol.call_remote_PortalAdmin(0,
operation='SDISCONNALL',
data=reason)
def disconnect_duplicate_sessions(self, curr_session):
"""
Disconnects any existing sessions with the same game object.
"""
reason = "Your account has been logged in from elsewhere. Disconnecting."
curr_char = curr_session.get_character()
doublet_sessions = [sess for sess in self.get_sessions()
if sess.get_character() == curr_char and sess != curr_session]
logged_out = 0
for session in doublet_sessions:
session.msg(reason)
self.remove_session(session)
logged_out += 1
self.session_count(-logged_out)
return logged_out
doublet_sessions = [sess for sess in self.sessions
if sess.logged_in
and sess.get_character() == curr_char
and sess != curr_session]
reason = _("Logged in from elsewhere. Disconnecting.")
for sessid in doublet_sessions:
self.disconnect(session, reason)
self.session_count(-1)
def validate_sessions(self):
"""
Check all currently connected sessions (logged in and not)
and see if any are dead.
"""
for session in self.get_sessions(include_unloggedin=True):
session.session_validate()
tcurr = time.time()
invalid_sessions = [session for session in self.sessions.values()
if session.logged_in and IDLE_TIMEOUT > 0
and (tcurr - session.cmd_last) > IDLE_TIMEOUT]
for session in invalid_sessions:
self.disconnect(session, reason=_("Idle timeout exceeded, disconnecting."))
self.session_count(-1)
def session_count(self, num=None):
"""
Count up/down the number of connected, authenticated users.
@ -160,7 +243,7 @@ class SessionHandler(object):
may have more than one session connected if ALLOW_MULTISESSION is True)
Only logged-in players are counted here.
"""
return len(set(sess.uid for sess in self.get_sessions()))
return len(set(session.uid for session in self.sessions.values() if session.logged_in))
def sessions_from_player(self, player):
"""
@ -172,7 +255,7 @@ class SessionHandler(object):
except User.DoesNotExist:
return None
uid = uobj.id
return [session for session in self.loggedin if session.uid == uid]
return [session for session in self.sessions.values() if session.logged_in and session.uid == uid]
def sessions_from_character(self, character):
"""
@ -183,20 +266,129 @@ class SessionHandler(object):
return self.sessions_from_player(player)
return None
def session_from_suid(self, suid):
"""
Given a session id, retrieve the session (this is primarily
intended to be called by web clients)
"""
return [sess for sess in self.get_sessions(include_unloggedin=True) if sess.suid and sess.suid == suid]
def announce_all(self, message):
"""
Send message to all connected sessions
"""
for sess in self.get_sessions(include_unloggedin=True):
sess.msg(message)
for sess in self.sessions.values():
self.data_out(sess, message)
SESSIONS = SessionHandler()
def data_out(self, session, string="", data=""):
"""
Sending data Server -> Portal
"""
self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid,
msg=string,
data=data)
def data_in(self, sessid, string="", data=""):
"""
Data Portal -> Server
"""
session = self.sessions.get(sessid, None)
if session:
session.execute_cmd(string)
# ignore 'data' argument for now; this is otherwise the place
# to put custom effects on the server due to data input, e.g.
# from a custom client.
#------------------------------------------------------------
# Portal-SessionHandler class
#------------------------------------------------------------
class PortalSessionHandler(SessionHandler):
"""
This object holds the sessions connected to the portal at any time.
It is synced with the server's equivalent SessionHandler over the AMP
connection.
Sessions register with the handler using the connect() method. This
will assign a new unique sessionid to the session and send that sessid
to the server using the AMP connection.
"""
def __init__(self):
"""
Init the handler
"""
self.portal = None
self.sessions = {}
self.latest_sessid = 0
def connect(self, session):
"""
Called by protocol at first connect. This adds a not-yet authenticated session
using an ever-increasing counter for sessid.
"""
self.latest_sessid += 1
sessid = self.latest_sessid
session.sessid = sessid
sessdata = session.get_sync_data()
self.sessions[sessid] = session
# sync with server-side
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation="PCONN",
data=sessdata)
def disconnect(self, session):
"""
Called from portal side when the connection is closed from the portal side.
"""
sessid = session.sessid
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation="PDISCONN")
def server_disconnect(self, sessid, reason=""):
"""
Called by server to force a disconnect by sessid
"""
session = self.sessions.get(sessid, None)
if session:
session.disconnect(reason)
del session
def server_disconnect_all(self, reason=""):
"""
Called by server when forcing a clean disconnect for everyone.
"""
for session in self.sessions.values():
session.disconnect(reason)
del session
def session_from_suid(self, suid):
"""
Given a session id, retrieve the session (this is primarily
intended to be called by web clients)
"""
return [sess for sess in self.get_sessions(include_unloggedin=True)
if hasattr(sess, 'suid') and sess.suid == suid]
def data_in(self, session, string="", data=""):
"""
Called by portal sessions for relaying data coming
in from the protocol to the server. data is
serialized before passed on.
"""
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
msg=string,
data=data)
def announce_all(self, message):
"""
Send message to all connection sessions
"""
for session in self.sessions.values():
session.data_out(message)
def data_out(self, sessid, string="", data=""):
"""
Called by server for having the portal relay messages and data
to the correct session protocol.
"""
session = self.sessions.get(sessid, None)
if session:
session.data_out(string, data=data)
SESSIONS = ServerSessionHandler()
PORTAL_SESSIONS = PortalSessionHandler()