Permalink
Browse files

Initial import, barebones MUD server.

  • Loading branch information...
0 parents commit d7159d8327c14cbf9de8a3be075f658fef9780aa @gtaylor committed Oct 28, 2010
6 .gitignore
@@ -0,0 +1,6 @@
+*.pyc
+*~
+*.swp
+*.tmp
+.project
+.pydevproject
0 __init__.py
No changes.
125 mg_manage.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+"""
+SERVER CONTROL SCRIPT
+
+Sets the appropriate environmental variables and launches the server
+process. Run the script with the -h flag to see usage information.
+"""
+import os
+import sys
+import signal
+from optparse import OptionParser
+from subprocess import Popen, call
+
+import settings
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+# Name of the twistd binary to run
+TWISTED_BINARY = 'twistd'
+
+# Setup access of the evennia server itself
+SERVER_PY_FILE = os.path.join(settings.SRC_DIR, 'server/server.py')
+
+# Add this to the environmental variable for the 'twistd' command.
+thispath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+if 'PYTHONPATH' in os.environ:
+ os.environ['PYTHONPATH'] += (":%s" % thispath)
+else:
+ os.environ['PYTHONPATH'] = thispath
+
+def cycle_logfile():
+ """
+ Move the old log file to evennia.log.old (by default).
+
+ """
+ logfile = settings.DEFAULT_LOG_FILE.strip()
+ logfile_old = logfile + '.old'
+ if os.path.exists(logfile):
+ # Cycle the old logfiles to *.old
+ if os.path.exists(logfile_old):
+ # E.g. Windows don't support rename-replace
+ os.remove(logfile_old)
+ os.rename(logfile, logfile_old)
+
+def start_daemon(parser, options, args):
+ """
+ Start the server in daemon mode. This means that all logging output will
+ be directed to logs/evennia.log by default, and the process will be
+ backgrounded.
+ """
+ if os.path.exists('twistd.pid'):
+ print "A twistd.pid file exists in the current directory, which suggests that the server is already running."
+ sys.exit()
+
+ print '\nStarting Evennia server in daemon mode ...'
+ print 'Logging to: %s.' % settings.DEFAULT_LOG_FILE
+
+ # Move the old evennia.log file out of the way.
+ #cycle_logfile()
+
+ # Start it up
+ Popen([TWISTED_BINARY,
+ '--logfile=%s' % settings.DEFAULT_LOG_FILE,
+ '--python=%s' % SERVER_PY_FILE])
+
+def start_interactive(parser, options, args):
+ """
+ Start in interactive mode, which means the process is foregrounded and
+ all logging output is directed to stdout.
+ """
+ print '\nStarting Evennia server in interactive mode (stop with keyboard interrupt) ...'
+ print 'Logging to: Standard output.'
+
+ try:
+ call([TWISTED_BINARY,
+ '-n',
+ '--python=%s' % SERVER_PY_FILE])
+ except KeyboardInterrupt:
+ pass
+
+def stop_server(parser, options, args):
+ """
+ Gracefully stop the server process.
+ """
+ if os.name == 'posix':
+ if os.path.exists('twistd.pid'):
+ print 'Stopping the Evennia server...'
+ f = open('twistd.pid', 'r')
+ pid = f.read()
+ os.kill(int(pid), signal.SIGINT)
+ print 'Server stopped.'
+ else:
+ print "No twistd.pid file exists, the server doesn't appear to be running."
+ elif os.name == 'nt':
+ print '\n\rStopping cannot be done safely under this operating system.'
+ print 'Kill server using the task manager or shut it down from inside the game.'
+ else:
+ print '\n\rUnknown OS detected, can not stop. '
+
+
+def main():
+ """
+ Beginning of the program logic.
+ """
+ parser = OptionParser(usage="%prog [options] <start|stop>",
+ description="This command starts or stops the Evennia game server. Note that you have to setup the database by running 'manage.py syncdb' before starting the server for the first time.")
+ parser.add_option('-i', '--interactive', action='store_true',
+ dest='interactive', default=False,
+ help='Start in interactive mode')
+ parser.add_option('-d', '--daemon', action='store_false',
+ dest='interactive',
+ help='Start in daemon mode (default)')
+ (options, args) = parser.parse_args()
+
+ if "start" in args:
+ if options.interactive:
+ start_interactive(parser, options, args)
+ else:
+ start_daemon(parser, options, args)
+ elif "stop" in args:
+ stop_server(parser, options, args)
+ else:
+ parser.print_help()
+if __name__ == '__main__':
+ main()
6 settings.py
@@ -0,0 +1,6 @@
+import os
+
+GAME_NAME = "Evennia"
+LISTEN_PORTS = [4000]
+BASE_PATH = os.path.dirname(os.path.abspath(__file__))
+SRC_DIR = os.path.join(BASE_PATH, 'src')
0 src/__init__.py
No changes.
0 src/server/__init__.py
No changes.
0 src/server/protocols/__init__.py
No changes.
130 src/server/protocols/telnet.py
@@ -0,0 +1,130 @@
+"""
+This module contains classes related to Sessions. sessionhandler has the things
+needed to manage them.
+"""
+import time
+from datetime import datetime
+
+from twisted.conch.telnet import StatefulTelnetProtocol
+
+from mongomud.src.server.session import Session
+from mongomud.src.utils import logger
+from mongomud.src.utils.general import to_unicode, to_str
+
+class MudTelnetProtocol(StatefulTelnetProtocol):
+ """
+ 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.
+ """
+ def __str__(self):
+ return "MudTelnetProtocol conn from %s" % self.getClientAddress()[0]
+
+ def connectionMade(self):
+ """
+ What to do when we get a connection.
+ """
+ # setup the parameters
+ self.session = Session(self)
+ # send info
+ logger.info('New connection from: %s' % self.getClientAddress()[0])
+ # add this new session to handler
+ #sessionhandler.add_session(self)
+ # show a connect screen
+ self.session.game_connect_screen()
+
+ def getClientAddress(self):
+ """
+ Returns the client's address and port in a tuple. For example
+ ('127.0.0.1', 41917)
+ """
+ return self.transport.client
+
+ def prep_session(self):
+ """
+ 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()
+
+ # player setup
+ self.name = None
+ self.uid = None
+ self.logged_in = False
+
+ # The time the user last issued a command.
+ self.cmd_last = time.time()
+ # Player-visible idle time, excluding the IDLE command.
+ self.cmd_last_visible = time.time()
+ # Total number of commands issued.
+ self.cmd_total = 0
+ # The time when the user connected.
+ self.conn_time = time.time()
+ #self.channels_subscribed = {}
+
+ def disconnectClient(self):
+ """
+ Manually disconnect the client.
+ """
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ """
+ Execute this when a client abruplty loses their connection.
+ """
+ logger.info('Disconnected: %s' % self)
+ self.handle_close()
+
+ 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.
+ """
+ try:
+ raw_string = to_unicode(raw_string)
+ except Exception, e:
+ self.sendLine(str(e))
+ return
+ #self.execute_cmd(raw_string)
+ self.session.msg("ECHO: %s" % raw_string)
+
+ def msg(self, message):
+ """
+ Communication Evennia -> Player
+ """
+ try:
+ message = to_str(message)
+ except Exception, e:
+ self.sendLine(str(e))
+ return
+ self.sendLine(message)
+
+ def update_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.
+ """
+ # Store the timestamp of the user's last command.
+ self.cmd_last = time.time()
+ if not idle:
+ # Increment the user's command counter.
+ self.cmd_total += 1
+ # Player-visible idle time, not used in idle timeout calcs.
+ self.cmd_last_visible = time.time()
+
+ def handle_close(self):
+ """
+ Break the connection and do some accounting.
+ """
+ self.disconnectClient()
+ self.logged_in = False
+
69 src/server/server.py
@@ -0,0 +1,69 @@
+"""
+This module implements the main Evennia server process, the core of the
+game engine.
+"""
+import time
+import sys
+import os
+
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor
+
+from mongomud import settings
+from mongomud.src.server.protocols.telnet import MudTelnetProtocol
+from mongomud.src.utils import logger
+
+class EvenniaService(service.Service):
+ """
+ The main server service task.
+ """
+ def __init__(self):
+ # Holds the TCP services.
+ self.service_collection = None
+ self.game_running = True
+
+ # Begin startup debug output.
+ print('\n' + '-'*50)
+
+ self.start_time = time.time()
+
+ # Make output to the terminal.
+ print(' %s started on port(s):' % settings.GAME_NAME)
+ for port in settings.LISTEN_PORTS:
+ print(' * %s' % port)
+ print('-'*50)
+
+ def shutdown(self, message=None):
+ """
+ Gracefully disconnect everyone and kill the reactor.
+ """
+ if not message:
+ message = 'The server has been shutdown. Please check back soon.'
+
+ reactor.callLater(0, reactor.stop)
+
+ def getEvenniaServiceFactory(self):
+ """
+ Retrieve instances of the server
+ """
+ factory = protocol.ServerFactory()
+ factory.protocol = MudTelnetProtocol
+ factory.server = self
+ return factory
+
+ def start_services(self, application):
+ """
+ Starts all of the TCP services.
+ """
+ self.service_collection = service.IServiceCollection(application)
+ for port in settings.LISTEN_PORTS:
+ evennia_server = \
+ internet.TCPServer(port, self.getEvenniaServiceFactory())
+ evennia_server.setName('Evennia%s' %port)
+ evennia_server.setServiceParent(self.service_collection)
+
+# Twisted requires us to define an 'application' attribute.
+application = service.Application('mongomud')
+# The main mud service. Import this for access to the server methods.
+mud_service = EvenniaService()
+mud_service.start_services(application)
111 src/server/session.py
@@ -0,0 +1,111 @@
+"""
+This module contains classes related to Sessions. sessionhandler has the things
+needed to manage them.
+"""
+import time
+from datetime import datetime
+
+from mongomud.src.utils import logger
+from mongomud.src.utils.general import to_unicode, to_str
+
+class Session(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.
+ """
+ def __init__(self, protocol):
+ """
+ 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.protocol = protocol
+ self.server = self.protocol.factory.server
+ self.address = self.protocol.getClientAddress()
+
+ # player setup
+ self.name = None
+ self.uid = None
+ self.logged_in = False
+
+ # The time the user last issued a command.
+ self.cmd_last = time.time()
+ # Player-visible idle time, excluding the IDLE command.
+ self.cmd_last_visible = time.time()
+ # Total number of commands issued.
+ self.cmd_total = 0
+ # The time when the user connected.
+ self.conn_time = time.time()
+
+ 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)
+
+ 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.
+ """
+ self.protocol.msg(message)
+
+ def update_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.
+ """
+ # Store the timestamp of the user's last command.
+ self.cmd_last = time.time()
+ if not idle:
+ # Increment the user's command counter.
+ self.cmd_total += 1
+ # Player-visible idle time, not used in idle timeout calcs.
+ self.cmd_last_visible = time.time()
+
+ def game_connect_screen(self):
+ """
+ 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.
+ """
+ self.msg("THIS IS A CONNECT SCREEN")
+
+ 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()
+
+ logger.info("Logged in: %s" % self)
137 src/server/sessionhandler.py
@@ -0,0 +1,137 @@
+"""
+Sessionhandler, stores and handles
+a list of all player connections (sessions).
+"""
+import time
+from django.contrib.auth.models import User
+from src.config.models import ConfigValue
+from src.utils import logger
+
+# Our list of connected sessions.
+SESSIONS = []
+
+def add_session(session):
+ """
+ Adds a session to the session list.
+ """
+ SESSIONS.insert(0, session)
+ change_session_count(1)
+ logger.log_infomsg('Sessions active: %d' % (len(get_sessions(return_unlogged=True),)))
+
+def get_sessions(return_unlogged=False):
+ """
+ Lists the connected session objects.
+ """
+ if return_unlogged:
+ return SESSIONS
+ else:
+ return [sess for sess in SESSIONS if sess.logged_in]
+
+def get_session_id_list(return_unlogged=False):
+ """
+ Lists the connected session object ids.
+ """
+ if return_unlogged:
+ return SESSIONS
+ else:
+ return [sess.uid for sess in SESSIONS if sess.logged_in]
+
+def disconnect_all_sessions():
+ """
+ Cleanly disconnect all of the connected sessions.
+ """
+ for sess in get_sessions():
+ sess.handle_close()
+
+def disconnect_duplicate_session(session):
+ """
+ Disconnects any existing session under the same object. This is used in
+ connection recovery to help with record-keeping.
+ """
+ SESSIONS = get_sessions()
+ session_pobj = session.get_character()
+ for other_session in SESSIONS:
+ other_pobject = other_session.get_character()
+ if session_pobj == other_pobject and other_session != session:
+ other_session.msg("Your account has been logged in from elsewhere, disconnecting.")
+ other_session.disconnectClient()
+ return True
+ return False
+
+def check_all_sessions():
+ """
+ Check all currently connected sessions and see if any are dead.
+ """
+ idle_timeout = int(ConfigValue.objects.conf('idle_timeout'))
+
+ if len(SESSIONS) <= 0:
+ return
+
+ if idle_timeout <= 0:
+ return
+
+ for sess in get_sessions(return_unlogged=True):
+ if (time.time() - sess.cmd_last) > idle_timeout:
+ sess.msg("Idle timeout exceeded, disconnecting.")
+ sess.handle_close()
+
+def change_session_count(num):
+ """
+ Count number of connected users by use of a config value
+
+ num can be a positive or negative value. If 0, the counter
+ will be reset to 0.
+ """
+
+ if num == 0:
+ # reset
+ ConfigValue.objects.conf('nr_sessions', 0)
+
+ nr = ConfigValue.objects.conf('nr_sessions')
+ if nr == None:
+ nr = 0
+ else:
+ nr = int(nr)
+ nr += num
+ ConfigValue.objects.conf('nr_sessions', str(nr))
+
+
+def remove_session(session):
+ """
+ Removes a session from the session list.
+ """
+ try:
+ SESSIONS.remove(session)
+ change_session_count(-1)
+ logger.log_infomsg('Sessions active: %d' % (len(get_sessions()),))
+ except ValueError:
+ # the session was already removed.
+ logger.log_errmsg("Unable to remove session: %s" % (session,))
+ return
+
+def find_sessions_from_username(username):
+ """
+ Given a username, return any matching sessions.
+ """
+ try:
+ uobj = User.objects.get(username=username)
+ uid = uobj.id
+ return [session for session in SESSIONS if session.uid == uid]
+ except User.DoesNotExist:
+ return None
+
+def sessions_from_object(targ_object):
+ """
+ Returns a list of matching session objects, or None if there are no matches.
+
+ targobject: (Object) The object to match.
+ """
+ return [session for session in SESSIONS
+ if session.get_character() == targ_object]
+
+def announce_all(message):
+ """
+ Announces something to all connected players.
+ """
+ for session in get_sessions():
+ session.msg('%s' % message)
0 src/utils/__init__.py
No changes.
35 src/utils/general.py
@@ -0,0 +1,35 @@
+"""
+General helper functions that don't fit neatly under any given category.
+
+They provide some useful string and conversion methods that might
+be of use when designing your own game.
+"""
+
+def to_unicode(obj, encoding='utf-8'):
+ """
+ This decodes a suitable object to
+ the unicode format. Note that one
+ needs to encode it back to utf-8
+ before writing to disk or printing.
+ """
+ if isinstance(obj, basestring) \
+ and not isinstance(obj, unicode):
+ try:
+ obj = unicode(obj, encoding)
+ except UnicodeDecodeError:
+ raise Exception("Error: '%s' contains invalid character(s) not in %s." % (obj, encoding))
+ return obj
+
+def to_str(obj, encoding='utf-8'):
+ """
+ This encodes a unicode string
+ back to byte-representation,
+ for printing, writing to disk etc.
+ """
+ if isinstance(obj, basestring) \
+ and isinstance(obj, unicode):
+ try:
+ obj = obj.encode(encoding)
+ except UnicodeEncodeError:
+ raise Exception("Error: Unicode could not encode unicode string '%s'(%s) to a bytestring. " % (obj, encoding))
+ return obj
72 src/utils/logger.py
@@ -0,0 +1,72 @@
+"""
+Logging facilities
+
+This file should have an absolute minimum in imports. If you'd like to layer
+additional functionality on top of some of the methods below, wrap them in
+a higher layer module.
+"""
+import sys
+from traceback import format_exc
+
+from twisted.python import log
+
+from mongomud.src.utils.general import to_str
+
+def trace(errmsg=None):
+ """
+ Log a traceback to the log. This should be called
+ from within an exception. errmsg is optional and
+ adds an extra line with added info.
+ """
+ tracestring = format_exc()
+ if tracestring:
+ for line in tracestring.splitlines():
+ log.msg('[::] %s' % line)
+ if errmsg:
+ try:
+ errmsg = to_str(errmsg)
+ except Exception, e:
+ errmsg = str(e)
+ for line in errmsg.splitlines():
+ log.msg('[EE] %s' % line)
+
+def error(errmsg):
+ """
+ Prints/logs an error message to the server log.
+
+ errormsg: (string) The message to be logged.
+ """
+ try:
+ errmsg = to_str(errmsg)
+ except Exception, e:
+ errmsg = str(e)
+ for line in errmsg.splitlines():
+ log.msg('[EE] %s' % line)
+ #log.err('ERROR: %s' % (errormsg,))
+
+def warning(warnmsg):
+ """
+ Prints/logs any warnings that aren't critical but should be noted.
+
+ warnmsg: (string) The message to be logged.
+ """
+ try:
+ warnmsg = to_str(warnmsg)
+ except Exception, e:
+ warnmsg = str(e)
+ for line in warnmsg.splitlines():
+ log.msg('[WW] %s' % line)
+ #log.msg('WARNING: %s' % (warnmsg,))
+
+def info(infomsg):
+ """
+ Prints any generic debugging/informative info that should appear in the log.
+
+ infomsg: (string) The message to be logged.
+ """
+ try:
+ infomsg = to_str(infomsg)
+ except Exception, e:
+ infomsg = str(e)
+ for line in infomsg.splitlines():
+ log.msg('[..] %s' % line)

0 comments on commit d7159d8

Please sign in to comment.