Initial import.

This commit is contained in:
Greg Taylor 2006-11-20 18:54:10 +00:00
parent 24ead8690c
commit 2421c23521
30 changed files with 773 additions and 0 deletions

74
evennia/ABOUT Executable file
View file

@ -0,0 +1,74 @@
Evennia Proof-of-Concept
------------------------
Evennia is a proof-of-concept MUD server written entirely in Python, backed
by SQL. The project rises from a general dissatisfaction with the limitations
of softcode in MUX and MUSH, and the generally inflexible Diku-derivatives and
relatives.
Evennia represents a combination of several technologies, and most importantly
of all, my first venture into codebase design. You may find things within
the source that look strange to you, perhaps not ideally designed. I'm open
to suggestions, but this really is largely an experiment and a learning
experience.
Design Objectives
-----------------
1) To create a MU* server that serves as a great foundation for capable admins
to craft into their respective games. It is not my intention to provide a
full-fledged, ready-to-run base, I'm releasing the means to make such games.
2) Development of games on Evennia must be easy for anyone with some degree
of Python experience. Building needs to be easy, and per-room, per-object,
and environmental customizations need to be simple to do.
3) The server must utilize SQL as a storage back-end to allow for web->game
integration. See the details on Django later on in the document for more
details.
4) Any and all game-specific configuration must reside in SQL, not
external configuration files. The only exception is the settings.py file
containing the SQL information.
How it all Works
----------------
Python (Including the SQL driver of your choice)
|-asynchat (included with Python2)
|-SQL (MySQL, SQLite, Postgresql)
|-Django (http://djangoproject.com)
Evennia is built on top of asynchat, an asynchronous TCP conversation/chat
library. This makes the actual socket/connection handling an absolute
no-brainer.
Serving as our storage medium, SQL is one of the more important and unique
features of the codebase. It allows for very simple code in many cases, and
can lead to a game being a lot more scalable due to the inherent speed of
most modern SQL servers. Another extremely important benefit is that by
storing everything in SQL, we make the entire game accessible from other
means, such as a website. Which leads us to the next component.
Django is perhaps one of the most interesting introductions to the codebase,
since I'm not aware of any other server using it to run MU*'s. Django is
technically a Python web framework, but it also includes a great data modeling
and database abstraction module. This means that things like Players or
Objects can be represented by a very short class, then related to one another.
This allows us to add, remove, delete, and manipulate things in our database
very easily. Another huge benefit is the admin interface that Django more
or less automatically generates for us. Instead of a bunch of clunky admin
commands, you can fire up your web browser and administer pretty much
everything from there, although equivalent in-game commands may be offered.
The possibilities for developing your game's website are nearly endless with
this tandem of MU* server, SQL, and Django.
Support
-------
At this time, I am offering no formal support for Evennia. It is not ready for
use and is subject to change in a major way from week to week. I can't hope
to support such a young product. However, if you have questions or ideas,
please direct them to squishywaffle@gmail.com.
Reporting Bugs
--------------
Feel free to contact me by email at squishywaffle@gmail.com with as much
details on the bug that you can find. Copy/pasting server logs is generally
a good idea.

16
evennia/README Executable file
View file

@ -0,0 +1,16 @@
Starting the Server
-------------------
Prior to starting up Evennia, you'll need the following environmental variable
set.
export DJANGO_SETTINGS_MODULE="settings"
You may wish to put this in your .bashrc file, or you can simple copy/paste
it before each startup. I'll fix this later so you don't have to, but it'll
do for now.
Once you've got the evar set, simply enter the following:
python server.py
The default port is 4000.

0
evennia/__init__.py Executable file
View file

BIN
evennia/__init__.pyc Executable file

Binary file not shown.

36
evennia/ansi.py Executable file
View file

@ -0,0 +1,36 @@
"""
ANSI related stuff.
"""
ansi = {}
ansi["beep"] = "\07"
ansi["escape"] = "\033"
ansi["normal"] = "\033[0m"
ansi["underline"] = "\033[4m"
ansi["hilite"] = "\033[1m"
ansi["blink"] = "\033[5m"
ansi["inverse"] = "\033[7m"
ansi["inv_hilite"] = "\033[1;7m"
ansi["inv_blink"] = "\033[7;5m"
ansi["blink_hilite"] = "\033[1;5m"
ansi["inv_blink_hilite"] = "\033[1;5;7m"
# Foreground colors
ansi["black"] = "\033[30m"
ansi["red"] = "\033[31m"
ansi["green"] = "\033[32m"
ansi["yellow"] = "\033[33m"
ansi["blue"] = "\033[34m"
ansi["magenta"] = "\033[35m"
ansi["cyan"] = "\033[36m"
ansi["white"] = "\033[37m"
# Background colors
ansi["back_black"] = "\033[40m"
ansi["back_red"] = "\033[41m"
ansi["back_green"] = "\033[42m"
ansi["back_yellow"] = "\033[43m"
ansi["back_blue"] = "\033[44m"
ansi["back_magenta"] = "\033[45m"
ansi["back_cyan"] = "\033[46m"
ansi["back_white"] = "\033[47m"

BIN
evennia/ansi.pyc Executable file

Binary file not shown.

0
evennia/apps/__init__.py Executable file
View file

BIN
evennia/apps/__init__.pyc Executable file

Binary file not shown.

View file

BIN
evennia/apps/config/__init__.pyc Executable file

Binary file not shown.

27
evennia/apps/config/models.py Executable file
View file

@ -0,0 +1,27 @@
from django.db import models
class CommandAlias(models.Model):
"""
Command aliases.
"""
user_input = models.CharField(maxlength=50)
equiv_command = models.CharField(maxlength=50)
class Admin:
list_display = ('user_input', 'equiv_command',)
class Config(models.Model):
"""
Although we technically have the ability to create more than one Config
object via the admin interface, we only really need one. This also leaves
the possibility for multiple games hosted on the same codebase or database
in the future, although this is not a priority. In any case, this model
contains most of the game-specific configuration.
"""
site_name = models.CharField(maxlength=100)
site_description = models.TextField(blank=True)
site_website = models.URLField(blank=True)
player_start_dbnum = models.IntegerField()
class Admin:
list_display = ('site_name', 'site_website',)

BIN
evennia/apps/config/models.pyc Executable file

Binary file not shown.

1
evennia/apps/config/views.py Executable file
View file

@ -0,0 +1 @@
# Create your views here.

View file

BIN
evennia/apps/objects/__init__.pyc Executable file

Binary file not shown.

92
evennia/apps/objects/models.py Executable file
View file

@ -0,0 +1,92 @@
from django.db import models
from django.contrib.auth.models import User
class ObjectClass(models.Model):
"""
Each object class can have different behaviors to apply to it.
"""
name = models.CharField(maxlength=255)
description = models.TextField()
def __str__(self):
return "%s(%d)" % (self.name, self.id,)
class Admin:
list_display = ('name', 'description',)
class Attribute(models.Model):
"""
Attributes are things that are specific to different types of objects. For
example, a drink container needs to store its fill level, whereas an exit
needs to store its open/closed/locked/unlocked state. These are done via
attributes, rather than making different classes for each object type and
storing them directly. The added benefit is that we can add/remove attributes
on the fly as we like.
"""
name = models.CharField(maxlength=255)
value = models.CharField(maxlength=255)
object = models.ForeignKey("Object")
def __str__(self):
return "%s(%d)" % (self.name, self.id,)
class Admin:
list_display = ('name', 'value',)
class Object(models.Model):
"""
The Object class is very generic. We put all of our common attributes
here and anything very particular into the attribute field. Notice the otype
field. The different otypes denote an object's behaviors.
"""
# Do not mess with the default types (0-4).
OBJECT_TYPES = (
(0, 'NOTHING'),
(1, 'PLAYER'),
(2, 'ROOM'),
(3, 'THING'),
(4, 'EXIT'),
)
name = models.CharField(maxlength=255)
type = models.SmallIntegerField(choices=OBJECT_TYPES)
description = models.TextField(blank=True)
location = models.ForeignKey('self', related_name="olocation", blank=True, null=True)
contents = models.ManyToManyField("Object", related_name="object", blank=True, null=True)
attributes = models.ManyToManyField(Attribute, related_name="attributes", blank=True, null=True)
def __str__(self):
return "%s(%d)" % (self.name, self.id,)
def is_type(self, typename):
"""
Do a string comparison of user's input and the object's type class object's
name.
"""
return self.type.name == typename
def set_type(self, typename):
"""
Sets a object's type.
"""
pass
class Admin:
list_display = ('name',)
"""
class Player(models.Model):
#
# Model representation of our players.
#
# Link back to our Django User class for password, username, email, etc.
account = models.ForeignKey(User)
location = models.ForeignKey(Object, related_name="plocation")
is_connected = models.BooleanField()
last_connected = models.DateTimeField()
contents = models.ManyToManyField(Object)
attributes = models.ManyToManyField(Attribute)
def __str__(self):
return "%s(%d)" % (self.name, self.id,)
"""

BIN
evennia/apps/objects/models.pyc Executable file

Binary file not shown.

1
evennia/apps/objects/views.py Executable file
View file

@ -0,0 +1 @@
# Create your views here.

207
evennia/cmdhandler.py Executable file
View file

@ -0,0 +1,207 @@
from django.contrib.auth.models import User
from apps.objects.models import Object
import settings
import string
from ansi import *
"""
This is the command processing module. It is instanced once in the main
server module and the handle() function is hit every time a player sends
something.
"""
class GenCommands:
"""
Generic command class. Pretty much every command should go here for
now.
"""
def __init__(self): pass
def do_look(self, cdat):
"""
Handle looking at objects.
"""
session = cdat['session']
server = session.server
player_loc = session.player_loc
player_loc_obj = Object.objects.filter(id=player_loc)[0]
retval = "%s%s%s%s\n\r%s\n\r" % (
ansi["normal"],
ansi["hilite"],
player_loc_obj.name,
ansi["normal"],
player_loc_obj.description,
)
session.push(retval)
def do_quit(self, cdat):
"""
Gracefully disconnect the user as per his own request.
"""
session = cdat['session']
session.push("Quitting!\n\r")
session.handle_close()
def do_who(self, cdat):
"""
Generic WHO command.
"""
session_list = cdat['server'].session_list
session = cdat['session']
retval = "Player Name\n\r"
for player in session_list:
retval += '%s\n\r' % (player,)
retval += '%d Players logged in.\n\r' % (len(session_list),)
session.push(retval)
def do_say(self, cdat):
"""
Room-based speech command.
"""
session_list = cdat['server'].session_list
session = cdat['session']
speech = cdat['uinput'][1:]
players_present = [player for player in session_list if player.player_loc == session.player_loc and player != session]
retval = "You say, '%s'\n\r" % (''.join(speech),)
for player in players_present:
player.push("%s says, '%s'\n\r" % (session.name, speech,))
session.push(retval)
def do_sa(self, cdat):
"""
Temporary alias until we come up with a command alias system.
"""
self.do_say(cdat)
def do_version(self, cdat):
"""
Version info command.
"""
session = cdat['session']
retval = "-"*50 +"\n\r"
retval += "Evennia %s\n\r" % (settings.EVENNIA_VERSION,)
retval += "-"*50 +"\n\r"
session.push(retval)
class StaffCommands:
"""
Restricted staff commands.
"""
def do_dig(self, cdat):
"""
Digs a new room out.
"""
session = cdat['session']
uinput= cdat['uinput']
roomname = ''.join(uinput[1:])
if roomname == '':
session.push("You must supply a room name!")
else:
newroom = Object()
newroom.name = roomname
newroom.type = "Room"
def do_nextfree(self, cdat):
"""
Returns the next free object number.
"""
session = cdat['session']
server = cdat['server']
nextfree = server.nextfree_objnum()
retval = "Next free object number: %d" % (nextfree,)
session.push(retval)
class UnLoggedInCommands:
"""
Commands that are available from the connect screen.
"""
def __init__(self): pass
def do_connect(self, cdat):
"""
This is the connect command at the connection screen. Fairly simple,
uses the Django database API and User model to make it extremely simple.
"""
session = cdat['session']
uname = cdat['uinput'][1]
password = cdat['uinput'][2]
account = User.objects.filter(username=uname)
user = account[0]
autherror = "Invalid username or password!\n\r"
if account.count() == 0:
session.push(autherror)
if not user.check_password(password):
session.push(autherror)
else:
uname = user.username
session.login(user)
def do_create(self, cdat):
"""
Handle the creation of new accounts.
"""
session = cdat['session']
uname = cdat['uinput'][1]
email = cdat['uinput'][2]
password = cdat['uinput'][3]
account = User.objects.filter(username=uname)
if not account.count() == 0:
session.push("There is already a player with that name!")
elif len(password) < 3:
session.push("Your password must be 3 characters or longer.")
else:
session.create_user(uname, email, password)
def do_quit(self, cdat):
"""
We're going to maintain a different version of the quit command
here for unconnected users for the sake of simplicity. The logged in
version will be a bit more complicated.
"""
session = cdat['session']
session.push("Disconnecting...\n\r")
session.handle_close()
# We'll use this for our getattr() in the Handler class.
gencommands = GenCommands()
staffcommands = StaffCommands()
unloggedincommands = UnLoggedInCommands()
class Handler:
def __init__(self): pass
def handle(self, cdat):
"""
Use the spliced (list) uinput variable to retrieve the correct
command, or return an invalid command error.
We're basically grabbing the player's command by tacking
their input on to 'do_' and looking it up in the GenCommands
class.
"""
session = cdat['session']
uinput = cdat['uinput']
if session.logged_in:
# If it's prefixed by an '@', it's a staff command.
if uinput[0].find('@') == -1:
cmdtable = gencommands
else:
cmdtable = staffcommands
else:
cmdtable = unloggedincommands
cmd = getattr(cmdtable, 'do_' + uinput[0].lower(), None)
if callable(cmd):
cmd(cdat)
else:
session.push("Unknown command.\n\r")

BIN
evennia/cmdhandler.pyc Executable file

Binary file not shown.

0
evennia/commonfuncs.py Executable file
View file

11
evennia/manage.py Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env python
from django.core.management import execute_manager
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)
if __name__ == "__main__":
execute_manager(settings)

3
evennia/prepenv.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
export DJANGO_SETTINGS_MODULE="settings"
python server.py

94
evennia/server.py Executable file
View file

@ -0,0 +1,94 @@
from asyncore import dispatcher
from asynchat import async_chat
import socket, asyncore, time, sys
from sessions import PlayerSession
from django.db import models
from apps.config.models import Config
from apps.objects.models import Object
#
## Begin: Time Functions
#
schedule = {'heal':100.0}
lastrun = {}
def heal():
pass
# The timer loop
def Timer(timer):
sched = schedule.iteritems()
for i in sched:
try: lastrun[i[0]]
except: lastrun[i[0]] = time.time()
diff = timer - lastrun[i[0]]
# Every 100 seconds, run heal(), defined above.
if diff >= schedule['heal']:
heal()
lastrun['heal'] = time.time()
#
## End: Time Functions
#
class Server(dispatcher):
"""
The main server class from which everything branches.
"""
def __init__(self, port):
dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind(('', port))
self.listen(100)
self.session_list = []
self.object_list = {}
self.game_running = True
print '-'*50
self.load_config()
self.load_objects()
print ' Server started on port %i.' % (port,)
print '-'*50
def load_config(self):
"""
Loads our site's configuration up for easy access.
"""
self.config = Config.objects.all()[0]
print ' Configuration Loaded.'
def load_objects(self):
"""
Load all of our objects into memory.
"""
object_list = Object.objects.all()
for object in object_list:
dbnum = object.id
self.object_list[dbnum] = object
print ' Objects Loaded: %i' % (len(self.object_list),)
def handle_accept(self):
"""
What to do when we get a connection.
"""
conn, addr = self.accept()
session = PlayerSession(self, conn, addr)
session.game_connect_screen(session)
print 'Connection:', str(session)
self.session_list.append(session)
print 'Sessions active:', len(self.session_list)
if __name__ == '__main__':
server = Server(4000)
try:
while server.game_running:
asyncore.loop(timeout=5, count=1) # Timer() called every 5 seconds.
Timer(time.time())
except KeyboardInterrupt:
print 'Interrupted'

BIN
evennia/server.pyc Executable file

Binary file not shown.

117
evennia/sessions.py Executable file
View file

@ -0,0 +1,117 @@
from asyncore import dispatcher
from asynchat import async_chat
import socket, asyncore, time, sys
from cmdhandler import *
chandler = Handler()
class PlayerSession(async_chat):
"""
This class represents a player's sesssion. From here we branch down into
other various classes, please try to keep this one tidy!
"""
def __init__(self, server, sock, addr):
async_chat.__init__(self, sock)
self.server = server
self.address = addr
self.set_terminator("\n")
self.name = None
self.data = []
self.sock = sock
self.logged_in = False
self.user = None
# The time the user last issued a command.
self.cmd_last = time.time()
# Total number of commands issued.
self.cmd_total = 0
# The time when the user connected.
self.conn_time = time.time()
# Player's room location. Move this to a player sub-class.
self.player_loc = 4
def collect_incoming_data(self, data):
"""
Stuff any incoming data into our buffer, self.data
"""
self.data.append(data)
def found_terminator(self):
"""
Any line return indicates a command for the purpose of a MUD. So we take
the user input, split it up by spaces into a list, and pass it to our
command handler.
"""
line = (''.join(self.data))
line = line.strip('\r')
uinput = line.split(' ')
self.data = []
# Increment our user's command counter.
self.cmd_total += 1
# Store the timestamp of the user's last command.
self.cmd_last = time.time()
# Stuff anything we need to pass in this dictionary.
cdat = {"server": self.server, "uinput": uinput, "session": self}
chandler.handle(cdat)
def handle_close(self):
"""
Break the connection and do some accounting.
"""
async_chat.handle_close(self)
self.logged_in = False
self.server.session_list.remove(self)
print 'Sessions active:', len(self.server.session_list)
def game_connect_screen(self, session):
"""
Show our banner screen.
"""
buffer = '-'*50
buffer += ' \n\rWelcome to Evennia!\n\r'
buffer += '-'*50 + '\n\r'
buffer += """Please type one of the following to begin:\n\r
connect <username> <password>\n\r
create <username> <email> <password>\n\r"""
buffer += '-'*50 + '\n\r'
session.push(buffer)
def login(self, user):
"""
After the user has authenticated, handle logging him in.
"""
self.user = user
self.name = user.username
self.logged_in = True
self.conn_time = time.time()
self.push("Logging in as %s.\n\r" % (self.name,))
print "Login: %s" % (self,)
def create_user(self, uname, email, password):
"""
Handles the creation of new users.
"""
# print uname, email, password
user = User.objects.create_user(uname, email, password)
self.login(user)
print 'Registration: %s' % (self,)
self.push("Welcome to the game, %s.\n\r" % (self.name,))
def nextfree_objnum(self):
"""
Returns the next free object number.
"""
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 handle_error(self):
# self.handle_close()

BIN
evennia/sessions.pyc Executable file

Binary file not shown.

85
evennia/settings.py Executable file
View file

@ -0,0 +1,85 @@
# Django settings for evennia project.
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
MANAGERS = ADMINS
DATABASE_ENGINE = 'mysql' # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
DATABASE_NAME = 'evennia' # Or path to database file if using sqlite3.
DATABASE_USER = 'evennia' # Not used with sqlite3.
DATABASE_PASSWORD = 'CvAPpy:FFRTmTMHf' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
# Local time zone for this installation. All choices can be found here:
# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
TIME_ZONE = 'America/New_York'
# Language code for this installation. All choices can be found here:
# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
# http://blogs.law.harvard.edu/tech/stories/storyReader$15
LANGUAGE_CODE = 'en-us'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = False
# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = '/home/gtaylor/dev/evennia/media'
# URL that handles the media served from MEDIA_ROOT.
# Example: "http://media.lawrence.com"
MEDIA_URL = '/media/'
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/amedia/'
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'fsd&lkj^LKJ8398200(@)(38919#23892(*$*#(981'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.load_template_source',
'django.template.loaders.app_directories.load_template_source',
# 'django.template.loaders.eggs.load_template_source',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.doc.XViewMiddleware',
)
ROOT_URLCONF = 'evennia.urls'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.admin',
'evennia.apps.config',
'evennia.apps.objects',
# 'django.contrib.sites',
)
#
## Evennia Config
#
EVENNIA_VERSION = 'Pre-Alpha 0.0'

BIN
evennia/settings.pyc Executable file

Binary file not shown.

9
evennia/urls.py Executable file
View file

@ -0,0 +1,9 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('',
# Example:
# (r'^evennia/', include('evennia.apps.foo.urls.foo')),
# Uncomment this for admin:
(r'^admin/', include('django.contrib.admin.urls')),
)