mirror of
https://github.com/evennia/evennia.git
synced 2026-03-31 04:57:16 +02:00
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:
parent
14dae44a46
commit
f13e8cdf7c
50 changed files with 3175 additions and 2565 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue