Trunk: Merged the Devel-branch (branches/griatch) into /trunk. This constitutes a major refactoring of Evennia. Development will now continue in trunk. See the wiki and the past posts to the mailing list for info. /Griatch

This commit is contained in:
Griatch 2010-08-29 18:46:58 +00:00
parent df29defbcd
commit f83c2bddf8
222 changed files with 22304 additions and 14371 deletions

0
src/server/__init__.py Normal file
View file

250
src/server/initial_setup.py Normal file
View file

@ -0,0 +1,250 @@
"""
This module handles initial database propagation, which is only run the first
time the game starts. It will create some default channels, objects, and
other things.
Everything starts at handle_setup()
"""
from django.contrib.auth.models import User
from django.core import management
from django.conf import settings
from src.config.models import ConfigValue, ConnectScreen
from src.objects.models import ObjectDB
from src.comms.models import Channel, ChannelConnection
from src.players.models import PlayerDB
from src.help.models import HelpEntry
from src.scripts import scripts
from src.utils import create
from src.utils import gametime
def create_config_values():
"""
Creates the initial config values.
"""
ConfigValue.objects.conf("default_home", "2")
ConfigValue.objects.conf("idle_timeout", "3600")
#ConfigValue.objects.conf("money_name_singular", "Credit")
#ConfigValue.objects.conf("money_name_plural", "Credits")
ConfigValue.objects.conf("site_name", settings.SERVERNAME)
def create_connect_screens():
"""
Creates the default connect screen(s).
"""
print " Creating startup screen(s) ..."
text = "%ch%cb==================================================================%cn"
text += "\r\n Welcome to %chEvennia%cn! Please type one of the following to begin:\r\n"
text += "\r\n If you have an existing account, connect to it by typing:\r\n "
text += "%chconnect <email> <password>%cn\r\n If you need to create an account, "
text += "type (without the <>'s):\r\n "
text += "%chcreate \"<username>\" <email> <password>%cn\r\n"
text += "\r\n Enter %chhelp%cn for more info. %chlook%cn will re-show this screen.\r\n"
text += "%ch%cb==================================================================%cn\r\n"
ConnectScreen(db_key="Default", db_text=text, db_is_active=True).save()
def get_god_user():
"""
Returns the initially created 'god' User object.
"""
return User.objects.get(id=1)
def create_objects():
"""
Creates the #1 player and Limbo room.
"""
print " Creating objects (Player #1 and Limbo room) ..."
# Set the initial User's account object's username on the #1 object.
# This object is pure django and only holds name, email and password.
god_user = get_god_user()
# Create a Player 'user profile' object to hold eventual
# mud-specific settings for the bog standard User object. This is
# accessed by user.get_profile() and can also store attributes.
# It also holds mud permissions, but for a superuser these
# have no effect anyhow.
character_typeclass = settings.BASE_CHARACTER_TYPECLASS
# Create the Player object as well as the in-game god-character
# for user #1. We can't set location and home yet since nothing
# exists. Also, all properties (name, email, password, is_superuser)
# is inherited from the user so we don't specify it again here.
god_character = create.create_player(god_user.username, None, None,
create_character=True,
typeclass=character_typeclass,
user=god_user)
god_character.id = 1
god_character.attr('desc', 'You are Player #1.')
god_character.save()
# Limbo is the initial starting room.
object_typeclass = settings.BASE_OBJECT_TYPECLASS
limbo_obj = create.create_object(object_typeclass, 'Limbo')
limbo_obj.id = 2
string = "Welcome to your new %chEvennia%cn-based game."
string += " From here you are ready to begin development."
string += " If you should need help or would like to participate"
string += " in community discussions, visit http://evennia.com."
limbo_obj.attr('desc', string)
limbo_obj.save()
# Now that Limbo exists, set the user up in Limbo.
god_character.location = limbo_obj
god_character.home = limbo_obj
def create_channels():
"""
Creates some sensible default channels.
"""
print " Creating default channels ..."
# public channel
key, aliases, desc, perms = settings.CHANNEL_PUBLIC
pchan = create.create_channel(key, aliases, desc, perms)
# mudinfo channel
key, aliases, desc, perms = settings.CHANNEL_MUDINFO
ichan = create.create_channel(key, aliases, desc, perms)
# connectinfo channel
key, aliases, desc, perms = settings.CHANNEL_CONNECTINFO
cchan = create.create_channel(key, aliases, desc, perms)
# connect the god user to all these channels by default.
goduser = get_god_user()
ChannelConnection.objects.create_connection(goduser, pchan)
ChannelConnection.objects.create_connection(goduser, ichan)
ChannelConnection.objects.create_connection(goduser, cchan)
def import_MUX_help_files():
"""
Imports the MUX help files.
"""
print " Importing MUX help database (devel reference only) ..."
management.call_command('loaddata', '../src/help/mux_help_db.json', verbosity=0)
# categorize the MUX help files into its own category.
default_category = "MUX"
print " Moving imported help db to help category '%s'." \
% default_category
HelpEntry.objects.all_to_category(default_category)
def create_permission_groups():
"""
This sets up the default permissions groups
by parsing a permission config file.
Note that we don't catch any exceptions here,
this must be debugged until it works.
"""
print " Creating and setting up permissions/groups ..."
# We try to get the data from config first.
setup_path = settings.PERMISSION_SETUP_MODULE
if not setup_path:
# go with the default
setup_path = "src.permissions.default_permissions"
module = __import__(setup_path, fromlist=[True])
# We have a successful import. Get the dicts.
groupdict = module.GROUPS
# Create groups and populate them
for group in groupdict:
group = create.create_permission_group(group, desc=group,
group_perms=groupdict[group])
if not group:
print " Creation of Group '%s' failed." % group
def create_system_scripts():
"""
Setup the system repeat scripts. They are automatically started
by the create_script function.
"""
print " Creating and starting global scripts ..."
# check so that all sessions are alive.
script1 = create.create_script(scripts.CheckSessions)
# validate all scripts in script table.
script2 = create.create_script(scripts.ValidateScripts)
# update the channel handler to make sure it's in sync
script3 = create.create_script(scripts.ValidateChannelHandler)
if not script1 or not script2 or not script3:
print " Error creating system scripts."
def start_game_time():
"""
This starts a persistent script that keeps track of the
in-game time (in whatever accelerated reference frame), but also
the total run time of the server as well as its current uptime
(the uptime can also be found directly from the server though).
"""
print " Starting in-game time ..."
gametime.init_gametime()
def handle_setup(last_step):
"""
Main logic for the module. It allows to restart the initialization
if one of the modules should crash.
"""
if last_step < 0:
# this means we don't need to handle setup since
# it already ran sucessfully once.
return
elif last_step == None:
# config doesn't exist yet. First start of server
last_step = 0
# setting up the list of functions to run
setup_queue = [
create_config_values,
create_connect_screens,
create_objects,
create_channels,
create_permission_groups,
create_system_scripts,
import_MUX_help_files,
start_game_time]
if not settings.IMPORT_MUX_HELP:
# skip importing of the MUX helpfiles, they are
# not interesting except for developers.
del setup_queue[6]
#print " Initial setup: %s steps." % (len(setup_queue))
# step through queue, from last completed function
for num, setup_func in enumerate(setup_queue[last_step:]):
# run the setup function. Note that if there is a
# traceback we let it stop the system so the config
# step is not saved.
#print "%s..." % num
try:
setup_func()
except Exception:
if last_step + num == 2:
for obj in ObjectDB.objects.all():
obj.delete()
for profile in PlayerDB.objects.all():
profile.delete()
elif last_step + num == 3:
for chan in Channel.objects.all():
chan.delete()
for conn in ChannelConnection.objects.all():
conn.delete()
raise
ConfigValue.objects.conf("last_initial_setup_step", last_step + num + 1)
# We got through the entire list. Set last_step to -1 so we don't
# have to run this again.
ConfigValue.objects.conf("last_initial_setup_step", -1)

155
src/server/server.py Normal file
View file

@ -0,0 +1,155 @@
"""
This module implements the main Evennia
server process, the core of the game engine.
"""
import time
import sys
import os
if os.name == 'nt':
# For Windows batchfile we need an extra path insertion here.
sys.path.insert(0, os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__)))))
from twisted.application import internet, service
from twisted.internet import protocol, reactor
from django.db import connection
from django.conf import settings
from src.config.models import ConfigValue
from src.server.session import SessionProtocol
from src.server import sessionhandler
from src.server import initial_setup
from src.utils import reloads
from src.utils.utils import get_evennia_version
from src.comms import channelhandler
class EvenniaService(service.Service):
"""
The main server service task.
"""
def __init__(self):
# Holds the TCP services.
self.service_collection = None
self.game_running = True
sys.path.append('.')
# Database-specific startup optimizations.
if (settings.DATABASE_ENGINE == "sqlite3"
or hasattr(settings, 'DATABASE')
and settings.DATABASE.get('ENGINE', None) == 'django.db.backends.sqlite3'):
# run sqlite3 preps
self.sqlite3_prep()
# Begin startup debug output.
print '\n' + '-'*50
last_initial_setup_step = \
ConfigValue.objects.conf('last_initial_setup_step')
if not last_initial_setup_step:
# None is only returned if the config does not exist,
# i.e. this is an empty DB that needs populating.
print ' Server started for the first time. Setting defaults.'
initial_setup.handle_setup(0)
print '-'*50
elif int(last_initial_setup_step) >= 0:
# last_setup_step >= 0 means the setup crashed
# on one of its modules and setup will resume, retrying
# the last failed module. When all are finished, the step
# is set to -1 to show it does not need to be run again.
print ' Resuming initial setup from step %s.' % \
last_initial_setup_step
initial_setup.handle_setup(int(last_initial_setup_step))
print '-'*50
# we have to null this here.
sessionhandler.change_session_count(0)
self.start_time = time.time()
# initialize channelhandler
channelhandler.CHANNELHANDLER.update()
# init all global scripts
reloads.reload_scripts(init_mode=True)
# Make output to the terminal.
print ' %s (%s) started on port(s):' % \
(settings.SERVERNAME, get_evennia_version())
for port in settings.GAMEPORTS:
print ' * %s' % (port)
print '-'*50
# Server startup methods
def sqlite3_prep(self):
"""
Optimize some SQLite stuff at startup since we
can't save it to the database.
"""
cursor = connection.cursor()
cursor.execute("PRAGMA cache_size=10000")
cursor.execute("PRAGMA synchronous=OFF")
cursor.execute("PRAGMA count_changes=OFF")
cursor.execute("PRAGMA temp_store=2")
# General methods
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.'
sessionhandler.announce_all(message)
sessionhandler.disconnect_all_sessions()
reactor.callLater(0, reactor.stop)
def getEvenniaServiceFactory(self):
"Retrieve instances of the server"
factory = protocol.ServerFactory()
factory.protocol = SessionProtocol
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.GAMEPORTS:
evennia_server = \
internet.TCPServer(port, self.getEvenniaServiceFactory())
evennia_server.setName('Evennia%s' %port)
evennia_server.setServiceParent(self.service_collection)
if settings.IMC2_ENABLED:
from src.imc2.connection import IMC2ClientFactory
from src.imc2 import events as imc2_events
imc2_factory = IMC2ClientFactory()
svc = internet.TCPClient(settings.IMC2_SERVER_ADDRESS,
settings.IMC2_SERVER_PORT,
imc2_factory)
svc.setName('IMC2')
svc.setServiceParent(self.service_collection)
imc2_events.add_events()
if settings.IRC_ENABLED:
from src.irc.connection import IRC_BotFactory
irc = internet.TCPClient(settings.IRC_NETWORK,
settings.IRC_PORT,
IRC_BotFactory(settings.IRC_CHANNEL,
settings.IRC_NETWORK,
settings.IRC_NICKNAME))
irc.setName("%s:%s" % ("IRC", settings.IRC_CHANNEL))
irc.setServiceParent(self.service_collection)
# Twisted requires us to define an 'application' attribute.
application = service.Application('Evennia')
# The main mud service. Import this for access to the server methods.
mud_service = EvenniaService()
mud_service.start_services(application)

262
src/server/session.py Normal file
View file

@ -0,0 +1,262 @@
"""
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 django.conf import settings
from src.server import sessionhandler
from src.objects.models import ObjectDB
from src.comms.models import Channel
from src.config.models import ConnectScreen
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
class SessionProtocol(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):
"""
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 connectionMade(self):
"""
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()
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.log_infomsg('Disconnected: %s' % self)
self.cemit_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 = utils.to_unicode(raw_string)
except Exception, e:
self.sendLine(str(e))
return
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.
"""
try:
message = utils.to_str(message)
except Exception, e:
self.sendLine(str(e))
return
if markup:
message = ansi.parse_ansi(message)
else:
message = ansi.clean_ansi(message)
self.sendLine(message)
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)
return None
return character[0]
return None
def execute_cmd(self, raw_string):
"""
Sends a command to this session's
character for processing.
'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):
"""
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.
"""
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):
"""
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.
"""
screen = ConnectScreen.objects.get_random_connect_screen()
string = ansi.parse_ansi(screen.text)
self.msg(string)
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 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)

View file

@ -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)