mirror of
https://github.com/evennia/evennia.git
synced 2026-03-29 03:57:17 +02:00
Evennia now runs on its own Twisted webserver (no need for testserver or Apache if you don't want to). Evennia now also has an ajax long-polling web client running from Twisted. The web client requires no extra dependencies beyond jQuery which is included. The src/server structure has been r
cleaned up and rewritten to make it easier to add new protocols in the future - all new protocols need to inherit from server.session.Session, whi ch implements a set of hooks that Evennia uses to communicate. The current web client protocol is functional but does not implement any of rcaskey 's suggestions as of yet - it uses a separate data object passed through msg() to communicate between the server and the various protocols. Also the client itself could probably need cleanup and 'prettification'. The fact that the system runs a hybrid of Django and Twisted, getting the best of both worlds should allow for many possibilities in the future. /Griatch
This commit is contained in:
parent
ecefbfac01
commit
251f94aa7a
118 changed files with 9049 additions and 593 deletions
|
|
@ -1,214 +1,183 @@
|
|||
"""
|
||||
This module contains classes related to Sessions. sessionhandler has the things
|
||||
needed to manage them.
|
||||
This defines a generic session class.
|
||||
|
||||
All protocols should implement this class and its hook methods.
|
||||
"""
|
||||
import time
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from twisted.conch.telnet import StatefulTelnetProtocol
|
||||
#from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from src.server import sessionhandler
|
||||
from src.objects.models import ObjectDB
|
||||
#from src.objects.models import ObjectDB
|
||||
from src.comms.models import Channel
|
||||
from src.config.models import ConnectScreen
|
||||
from src.utils import logger, reloads
|
||||
from src.commands import cmdhandler
|
||||
from src.utils import ansi
|
||||
from src.utils import reloads
|
||||
from src.utils import logger
|
||||
from src.utils import utils
|
||||
from src.server import sessionhandler
|
||||
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||
IDLE_COMMAND = settings.IDLE_COMMAND
|
||||
|
||||
class SessionProtocol(StatefulTelnetProtocol):
|
||||
|
||||
|
||||
class IOdata(object):
|
||||
"""
|
||||
This class represents a player's session. Each player
|
||||
gets a session assigned to them whenever
|
||||
they connect to the game server. All communication
|
||||
between game and player goes through here.
|
||||
A simple storage object that allows for storing
|
||||
new attributes on it at creation.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
"Give keyword arguments to store as new arguments on the object."
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# SessionBase class
|
||||
#------------------------------------------------------------
|
||||
|
||||
class SessionBase(object):
|
||||
"""
|
||||
This class represents a player's session and is a template for
|
||||
individual protocols to communicate with Evennia.
|
||||
|
||||
Each player gets a session assigned to them whenever they connect
|
||||
to the game server. All communication between game and player goes
|
||||
through their session.
|
||||
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
String representation of the user session class. We use
|
||||
this a lot in the server logs and stuff.
|
||||
"""
|
||||
if self.logged_in:
|
||||
symbol = '#'
|
||||
else:
|
||||
symbol = '?'
|
||||
return "<%s> %s@%s" % (symbol, self.name, self.address,)
|
||||
# use this to uniquely identify the protocol name, e.g. "telnet" or "comet"
|
||||
protocol_key = "BaseProtocol"
|
||||
|
||||
def connectionMade(self):
|
||||
def session_connect(self, address, suid=None):
|
||||
"""
|
||||
What to do when we get a connection.
|
||||
"""
|
||||
# setup the parameters
|
||||
self.prep_session()
|
||||
# send info
|
||||
logger.log_infomsg('New connection: %s' % self)
|
||||
# add this new session to handler
|
||||
sessionhandler.add_session(self)
|
||||
# show a connect screen
|
||||
self.game_connect_screen()
|
||||
The setup of the session. An address (usually an IP address) on any form is required.
|
||||
|
||||
def getClientAddress(self):
|
||||
"""
|
||||
Returns the client's address and port in a tuple. For example
|
||||
('127.0.0.1', 41917)
|
||||
"""
|
||||
return self.transport.client
|
||||
This should be called by the protocol at connection time.
|
||||
|
||||
def prep_session(self):
|
||||
suid = this is a session id. Needed by some transport protocols.
|
||||
"""
|
||||
This sets up the main parameters of
|
||||
the session. The game will poll these
|
||||
properties to check the status of the
|
||||
connection and to be able to contact
|
||||
the connected player.
|
||||
"""
|
||||
# main server properties
|
||||
self.server = self.factory.server
|
||||
self.address = self.getClientAddress()
|
||||
self.address = address
|
||||
|
||||
# player setup
|
||||
# user setup
|
||||
self.name = None
|
||||
self.uid = None
|
||||
self.suid = suid
|
||||
self.logged_in = False
|
||||
self.encoding = "utf-8"
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
# The time the user last issued a command.
|
||||
self.cmd_last = time.time()
|
||||
self.cmd_last = current_time
|
||||
# Player-visible idle time, excluding the IDLE command.
|
||||
self.cmd_last_visible = time.time()
|
||||
self.cmd_last_visible = current_time
|
||||
# The time when the user connected.
|
||||
self.conn_time = current_time
|
||||
# Total number of commands issued.
|
||||
self.cmd_total = 0
|
||||
# The time when the user connected.
|
||||
self.conn_time = time.time()
|
||||
#self.channels_subscribed = {}
|
||||
sessionhandler.SESSIONS.add_unloggedin_session(self)
|
||||
# call hook method
|
||||
self.at_connect()
|
||||
|
||||
def disconnectClient(self):
|
||||
def session_login(self, player):
|
||||
"""
|
||||
Manually disconnect the client.
|
||||
"""
|
||||
self.transport.loseConnection()
|
||||
Private startup mechanisms that need to run at login
|
||||
|
||||
def connectionLost(self, reason):
|
||||
player - the connected player
|
||||
"""
|
||||
Execute this when a client abruplty loses their connection.
|
||||
"""
|
||||
logger.log_infomsg('Disconnected: %s' % self)
|
||||
self.cemit_info('Disconnected: %s.' % self)
|
||||
self.handle_close()
|
||||
self.player = player
|
||||
self.user = player.user
|
||||
self.uid = self.user.id
|
||||
self.name = self.user.username
|
||||
self.logged_in = True
|
||||
self.conn_time = time.time()
|
||||
|
||||
def lineReceived(self, raw_string):
|
||||
"""
|
||||
Communication Player -> Evennia
|
||||
Any line return indicates a command for the purpose of the MUD.
|
||||
So we take the user input and pass it to the Player and their currently
|
||||
connected character.
|
||||
"""
|
||||
# Update account's last login time.
|
||||
self.user.last_login = datetime.now()
|
||||
self.user.save()
|
||||
self.log('Logged in: %s' % self)
|
||||
|
||||
if self.encoding:
|
||||
try:
|
||||
raw_string = utils.to_unicode(raw_string, encoding=self.encoding)
|
||||
self.execute_cmd(raw_string)
|
||||
return
|
||||
except Exception, e:
|
||||
err = str(e)
|
||||
print err
|
||||
pass
|
||||
# start (persistent) scripts on this object
|
||||
reloads.reload_scripts(obj=self.player.character, init_mode=True)
|
||||
|
||||
#add session to connected list
|
||||
sessionhandler.SESSIONS.add_loggedin_session(self)
|
||||
|
||||
# malformed/wrong encoding defined on player-try some defaults
|
||||
for encoding in ENCODINGS:
|
||||
try:
|
||||
raw_string = utils.to_unicode(raw_string, encoding=encoding)
|
||||
err = None
|
||||
break
|
||||
except Exception, e:
|
||||
err = str(e)
|
||||
continue
|
||||
if err:
|
||||
self.sendLine(err)
|
||||
#call hook
|
||||
self.at_login()
|
||||
|
||||
def session_disconnect(self, reason=None):
|
||||
"""
|
||||
Clean up the session, removing it from the game and doing some
|
||||
accounting. This method is used also for non-loggedin
|
||||
accounts.
|
||||
|
||||
Note that this methods does not close the connection - this is protocol-dependent
|
||||
and have to be done right after this function!
|
||||
"""
|
||||
if self.logged_in:
|
||||
character = self.get_character()
|
||||
if character:
|
||||
character.player.at_disconnect(reason)
|
||||
uaccount = character.player.user
|
||||
uaccount.last_login = datetime.now()
|
||||
uaccount.save()
|
||||
self.logged_in = False
|
||||
sessionhandler.SESSIONS.remove_session(self)
|
||||
|
||||
def session_validate(self):
|
||||
"""
|
||||
Validate the session to make sure they have not been idle for too long
|
||||
"""
|
||||
if IDLE_TIMEOUT > 0 and (time.time() - self.cmd_last) > IDLE_TIMEOUT:
|
||||
self.msg("Idle timeout exceeded, disconnecting.")
|
||||
self.session_disconnect()
|
||||
|
||||
def get_player(self):
|
||||
"""
|
||||
Get the player associated with this session
|
||||
"""
|
||||
if self.logged_in:
|
||||
return self.player
|
||||
else:
|
||||
self.execute_cmd(raw_string)
|
||||
|
||||
def msg(self, message, markup=True):
|
||||
"""
|
||||
Communication Evennia -> Player
|
||||
Sends a message to the session.
|
||||
|
||||
markup - determines if formatting markup should be
|
||||
parsed or not. Currently this means ANSI
|
||||
colors, but could also be html tags for
|
||||
web connections etc.
|
||||
"""
|
||||
if self.encoding:
|
||||
try:
|
||||
message = utils.to_str(message, encoding=self.encoding)
|
||||
self.sendLine(ansi.parse_ansi(message, strip_ansi=not markup))
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# malformed/wrong encoding defined on player - try some defaults
|
||||
for encoding in ENCODINGS:
|
||||
try:
|
||||
message = utils.to_str(message, encoding=encoding)
|
||||
err = None
|
||||
break
|
||||
except Exception, e:
|
||||
err = str(e)
|
||||
continue
|
||||
if err:
|
||||
self.sendLine(err)
|
||||
else:
|
||||
self.sendLine(ansi.parse_ansi(message, strip_ansi=not markup))
|
||||
return None
|
||||
|
||||
# if self.logged_in:
|
||||
# character = ObjectDB.objects.get_object_with_user(self.uid)
|
||||
# if not character:
|
||||
# string = "No player match for session uid: %s" % self.uid
|
||||
# logger.log_errmsg(string)
|
||||
# return None
|
||||
# return character.player
|
||||
# return None
|
||||
|
||||
def get_character(self):
|
||||
"""
|
||||
Returns the in-game character associated with a session.
|
||||
This returns the typeclass of the object.
|
||||
"""
|
||||
if self.logged_in:
|
||||
character = ObjectDB.objects.get_object_with_user(self.uid)
|
||||
if not character:
|
||||
string = "No character match for session uid: %s" % self.uid
|
||||
logger.log_errmsg(string)
|
||||
else:
|
||||
return character
|
||||
player = self.get_player()
|
||||
if player:
|
||||
return player.character
|
||||
return None
|
||||
|
||||
def execute_cmd(self, raw_string):
|
||||
def log(self, message, channel=True):
|
||||
"""
|
||||
Sends a command to this session's
|
||||
character for processing.
|
||||
Emits session info to the appropriate outputs and info channels.
|
||||
"""
|
||||
if channel:
|
||||
try:
|
||||
cchan = settings.CHANNEL_CONNECTINFO
|
||||
cchan = Channel.objects.get_channel(cchan[0])
|
||||
cchan.msg("[%s]: %s" % (cchan.key, message))
|
||||
except Exception:
|
||||
pass
|
||||
logger.log_infomsg(message)
|
||||
|
||||
'idle' is a special command that is
|
||||
interrupted already here. It doesn't do
|
||||
anything except silently updates the
|
||||
last-active timer to avoid getting kicked
|
||||
off for idleness.
|
||||
"""
|
||||
# handle the 'idle' command
|
||||
if str(raw_string).strip() == 'idle':
|
||||
self.update_counters(idle=True)
|
||||
return
|
||||
|
||||
# all other inputs, including empty inputs
|
||||
character = self.get_character()
|
||||
if character:
|
||||
# normal operation.
|
||||
character.execute_cmd(raw_string)
|
||||
else:
|
||||
# we are not logged in yet
|
||||
cmdhandler.cmdhandler(self, raw_string, unloggedin=True)
|
||||
# update our command counters and idle times.
|
||||
self.update_counters()
|
||||
|
||||
def update_counters(self, idle=False):
|
||||
def update_session_counters(self, idle=False):
|
||||
"""
|
||||
Hit this when the user enters a command in order to update idle timers
|
||||
and command counters. If silently is True, the public-facing idle time
|
||||
is not updated.
|
||||
and command counters.
|
||||
"""
|
||||
# Store the timestamp of the user's last command.
|
||||
self.cmd_last = time.time()
|
||||
|
|
@ -217,80 +186,125 @@ class SessionProtocol(StatefulTelnetProtocol):
|
|||
self.cmd_total += 1
|
||||
# Player-visible idle time, not used in idle timeout calcs.
|
||||
self.cmd_last_visible = time.time()
|
||||
|
||||
def handle_close(self):
|
||||
|
||||
def execute_cmd(self, command_string):
|
||||
"""
|
||||
Break the connection and do some accounting.
|
||||
Execute a command string.
|
||||
"""
|
||||
|
||||
# handle the 'idle' command
|
||||
if str(command_string).strip() == IDLE_COMMAND:
|
||||
self.update_session_counters(idle=True)
|
||||
return
|
||||
|
||||
# all other inputs, including empty inputs
|
||||
character = self.get_character()
|
||||
|
||||
if character:
|
||||
#call hook functions
|
||||
character.at_disconnect()
|
||||
character.player.at_disconnect()
|
||||
uaccount = character.player.user
|
||||
uaccount.last_login = datetime.now()
|
||||
uaccount.save()
|
||||
self.disconnectClient()
|
||||
self.logged_in = False
|
||||
sessionhandler.remove_session(self)
|
||||
|
||||
def game_connect_screen(self):
|
||||
#print "loggedin _execute_cmd: '%s' __ %s" % (command_string, character)
|
||||
# normal operation.
|
||||
character.execute_cmd(command_string)
|
||||
else:
|
||||
#print "unloggedin _execute_cmd: '%s' __ %s" % (command_string, character)
|
||||
# we are not logged in yet; call cmdhandler directly
|
||||
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
|
||||
|
||||
def get_data_obj(self, **kwargs):
|
||||
"""
|
||||
Show the banner screen. Grab from the 'connect_screen'
|
||||
config directive. If more than one connect screen is
|
||||
defined in the ConnectScreen attribute, it will be
|
||||
random which screen is used.
|
||||
Create a data object, storing keyword arguments on itself as arguments.
|
||||
"""
|
||||
screen = ConnectScreen.objects.get_random_connect_screen()
|
||||
string = ansi.parse_ansi(screen.text)
|
||||
self.msg(string)
|
||||
|
||||
return IOdata(**kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.address == other.address
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
String representation of the user session class. We use
|
||||
this a lot in the server logs.
|
||||
"""
|
||||
if self.logged_in:
|
||||
symbol = '#'
|
||||
else:
|
||||
symbol = '?'
|
||||
return "<%s> %s@%s" % (symbol, self.name, self.address,)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Unicode representation
|
||||
"""
|
||||
return u"%s" % str(self)
|
||||
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Session class - inherit from this
|
||||
#------------------------------------------------------------
|
||||
|
||||
class Session(SessionBase):
|
||||
"""
|
||||
The main class to inherit from. Overload the methods here.
|
||||
"""
|
||||
|
||||
# exchange this for a unique name you can use to identify the
|
||||
# protocol type this session uses
|
||||
protocol_key = "TemplateProtocol"
|
||||
|
||||
#
|
||||
# Hook methods
|
||||
#
|
||||
|
||||
def at_connect(self):
|
||||
"""
|
||||
This method is called by the connection mechanic after
|
||||
connection has been made. The session is added to the
|
||||
sessionhandler and basic accounting has been made at this
|
||||
point.
|
||||
|
||||
This is the place to put e.g. welcome screens specific to the
|
||||
protocol.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
This method is called by the login mechanic whenever the user
|
||||
has finished authenticating. The user has been moved to the
|
||||
right sessionhandler list and basic book keeping has been
|
||||
done at this point (so logged_in=True).
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_disconnect(self, reason=None):
|
||||
"""
|
||||
This method is called just before cleaning up the session
|
||||
(so still logged_in=True at this point).
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_data_in(self, string="", data=None):
|
||||
"""
|
||||
Player -> Evennia
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_data_out(self, string="", data=None):
|
||||
"""
|
||||
Evennia -> Player
|
||||
|
||||
string - an string of any form to send to the player
|
||||
data - a data structure of any form
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
# easy-access functions
|
||||
def login(self, player):
|
||||
"""
|
||||
After the user has authenticated, this actually
|
||||
logs them in. At this point the session has
|
||||
a User account tied to it. User is an django
|
||||
object that handles stuff like permissions and
|
||||
access, it has no visible precense in the game.
|
||||
This User object is in turn tied to a game
|
||||
Object, which represents whatever existence
|
||||
the player has in the game world. This is the
|
||||
'character' referred to in this module.
|
||||
"""
|
||||
# set the session properties
|
||||
|
||||
user = player.user
|
||||
self.uid = user.id
|
||||
self.name = user.username
|
||||
self.logged_in = True
|
||||
self.conn_time = time.time()
|
||||
if player.db.encoding:
|
||||
self.encoding = player.db.encoding
|
||||
|
||||
if not settings.ALLOW_MULTISESSION:
|
||||
# disconnect previous sessions.
|
||||
sessionhandler.disconnect_duplicate_session(self)
|
||||
|
||||
# start (persistent) scripts on this object
|
||||
reloads.reload_scripts(obj=self.get_character(), init_mode=True)
|
||||
|
||||
logger.log_infomsg("Logged in: %s" % self)
|
||||
self.cemit_info('Logged in: %s' % self)
|
||||
|
||||
# Update their account's last login time.
|
||||
user.last_login = datetime.now()
|
||||
user.save()
|
||||
|
||||
def cemit_info(self, message):
|
||||
"""
|
||||
Channel emits info to the appropriate info channel. By default, this
|
||||
is MUDConnections.
|
||||
"""
|
||||
try:
|
||||
cchan = settings.CHANNEL_CONNECTINFO
|
||||
cchan = Channel.objects.get_channel(cchan[0])
|
||||
cchan.msg("[%s]: %s" % (cchan.key, message))
|
||||
except Exception:
|
||||
logger.log_infomsg(message)
|
||||
|
||||
|
||||
"alias for at_login"
|
||||
self.at_login(player)
|
||||
def logout(self):
|
||||
"alias for at_logout"
|
||||
self.at_disconnect()
|
||||
def msg(self, string='', data=None):
|
||||
"alias for at_data_out"
|
||||
self.at_data_out(string, data)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue