"""Core objects for the mudpy engine."""
-# Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
-# Licensed per terms in the LICENSE file distributed with this software.
+# Copyright (c) 2005, 2006 Jeremy Stanley <fungi@yuggoth.org>. All rights
+# reserved. Licensed per terms in the LICENSE file distributed with this
+# software.
# import some things we need
from ConfigParser import RawConfigParser
from md5 import new as new_md5
-from os import R_OK, access, chmod, makedirs, stat
-from os.path import abspath, dirname, exists, isabs, join as path_join
+from os import _exit, R_OK, W_OK, access, chdir, chmod, close, fork, getcwd, getpid, listdir, makedirs, remove, rename, setsid, stat, umask
+from os.path import abspath, basename, dirname, exists, isabs, join as path_join
from random import choice, randrange
from re import match
+from signal import SIGHUP, SIGTERM, signal
from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
from stat import S_IMODE, ST_MODE
-from sys import stderr
+from string import digits, letters, punctuation, uppercase
+from sys import argv, stderr
from syslog import LOG_PID, LOG_INFO, LOG_DAEMON, closelog, openlog, syslog
from telnetlib import DO, DONT, ECHO, EOR, GA, IAC, LINEMODE, SB, SE, SGA, WILL, WONT
from time import asctime, sleep
from traceback import format_exception
-def excepthook(excepttype, value, traceback):
- """Handle uncaught exceptions."""
-
- # assemble the list of errors into a single string
- message = "".join(format_exception(excepttype, value, traceback))
-
- # try to log it, if possible
- try: log(message, 9)
- except: pass
-
- # try to write it to stderr, if possible
- try: stderr.write(message)
- except: pass
-
-# redefine sys.excepthook with ours
-import sys
-sys.excepthook = excepthook
-
class Element:
"""An element of the universe."""
def __init__(self, key, universe, filename=None):
"""Set up a new element."""
- # not owned by a user by default (used for avatars)
- self.owner = None
-
- # no contents in here by default
- self.contents = {}
-
- # an event queue for the element
- self.events = {}
-
# keep track of our key name
self.key = key
- # parse out appropriate category and subkey names, add to list
- if self.key.find(":") > 0:
- self.category, self.subkey = self.key.split(":", 1)
+ # keep track of what universe it's loading` into
+ self.universe = universe
+
+ # clone attributes if this is replacing another element
+ if self.key in self.universe.contents:
+ old_element = self.universe.contents[self.key]
+ for attribute in vars(old_element).keys():
+ exec("self." + attribute + " = old_element." + attribute)
+ if self.owner: self.owner.avatar = self
+
+ # i guess this is a new element then
else:
- self.category = "other"
- self.subkey = self.key
- if not self.category in universe.categories: self.category = "other"
- universe.categories[self.category][self.subkey] = self
- # get an appropriate filename for the origin
- if not filename: filename = universe.default_origins[self.category]
- if not isabs(filename): filename = abspath(filename)
+ # not owned by a user by default (used for avatars)
+ self.owner = None
- # add the file if it doesn't exist yet
- if not filename in universe.files: DataFile(filename, universe)
+ # no contents in here by default
+ self.contents = {}
- # record a pointer to the origin file
- self.origin = universe.files[filename]
+ # parse out appropriate category and subkey names, add to list
+ if self.key.find(":") > 0:
+ self.category, self.subkey = self.key.split(":", 1)
+ else:
+ self.category = "other"
+ self.subkey = self.key
+ if not self.category in self.universe.categories:
+ self.category = "other"
+ self.subkey = self.key
+
+ # get an appropriate filename for the origin
+ if not filename: filename = self.universe.default_origins[self.category]
+ if not isabs(filename): filename = abspath(filename)
+
+ # add the file if it doesn't exist yet
+ if not filename in self.universe.files: DataFile(filename, self.universe)
+
+ # record or reset a pointer to the origin file
+ self.origin = self.universe.files[filename]
# add a data section to the origin if necessary
if not self.origin.data.has_section(self.key):
self.origin.data.add_section(self.key)
- # add this element to the universe contents
- universe.contents[self.key] = self
+ # add or replace this element in the universe
+ self.universe.contents[self.key] = self
+ self.universe.categories[self.category][self.subkey] = self
+ def reload(self):
+ """Create a new element and replace this one."""
+ new_element = Element(self.key, self.universe, self.origin.filename)
+ del(self)
def destroy(self):
"""Remove an element from the universe and destroy it."""
self.origin.data.remove_section(self.key)
- del universe.categories[self.category][self.subkey]
- del universe.contents[self.key]
+ del self.universe.categories[self.category][self.subkey]
+ del self.universe.contents[self.key]
del self
def facets(self):
"""Return a list of non-inherited facets for this element."""
if self.has_facet("inherit"):
ancestry = self.getlist("inherit")
for parent in ancestry[:]:
- ancestors = universe.contents[parent].ancestry()
+ ancestors = self.universe.contents[parent].ancestry()
for ancestor in ancestors:
if ancestor not in ancestry: ancestry.append(ancestor)
return ancestry
return self.origin.data.get(self.key, facet)
elif self.has_facet("inherit"):
for ancestor in self.ancestry():
- if universe.contents[ancestor].has_facet(facet):
- return universe.contents[ancestor].get(facet)
+ if self.universe.contents[ancestor].has_facet(facet):
+ return self.universe.contents[ancestor].get(facet)
else: return default
def getboolean(self, facet, default=None):
"""Retrieve values as boolean type."""
return self.origin.data.getboolean(self.key, facet)
elif self.has_facet("inherit"):
for ancestor in self.ancestry():
- if universe.contents[ancestor].has_facet(facet):
- return universe.contents[ancestor].getboolean(facet)
+ if self.universe.contents[ancestor].has_facet(facet):
+ return self.universe.contents[ancestor].getboolean(facet)
else: return default
def getint(self, facet, default=None):
"""Return values as int/long type."""
return self.origin.data.getint(self.key, facet)
elif self.has_facet("inherit"):
for ancestor in self.ancestry():
- if universe.contents[ancestor].has_facet(facet):
- return universe.contents[ancestor].getint(facet)
+ if self.universe.contents[ancestor].has_facet(facet):
+ return self.universe.contents[ancestor].getint(facet)
else: return default
def getfloat(self, facet, default=None):
"""Return values as float type."""
return self.origin.data.getfloat(self.key, facet)
elif self.has_facet("inherit"):
for ancestor in self.ancestry():
- if universe.contents[ancestor].has_facet(facet):
- return universe.contents[ancestor].getfloat(facet)
+ if self.universe.contents[ancestor].has_facet(facet):
+ return self.universe.contents[ancestor].getfloat(facet)
else: return default
def getlist(self, facet, default=None):
"""Return values as list type."""
"""Create, attach and enqueue an event element."""
# if when isn't specified, that means now
- if not when: when = universe.get_time()
+ if not when: when = self.universe.get_time()
# events are elements themselves
event = Element("event:" + self.key + ":" + counter)
- def send(self, message, eol="$(eol)"):
+ def send(self, message, eol="$(eol)", raw=False, flush=False, add_prompt=True, just_prompt=False):
"""Convenience method to pass messages to an owner."""
- if self.owner: self.owner.send(message, eol)
+ if self.owner: self.owner.send(message, eol, raw, flush, add_prompt, just_prompt)
def can_run(self, command):
"""Check if the user can run this command object."""
# has to be in the commands category
- if command not in universe.categories["command"].values(): result = False
+ if command not in self.universe.categories["command"].values(): result = False
# avatars of administrators can run any command
elif self.owner and self.owner.account.getboolean("administrator"): result = True
# pass back the result
return result
+ def update_location(self):
+ """Make sure the location's contents contain this element."""
+ location = self.get("location")
+ if location in self.universe.contents:
+ self.universe.contents[location].contents[self.key] = self
+ def clean_contents(self):
+ """Make sure the element's contents aren't bogus."""
+ for element in self.contents.values():
+ if element.get("location") != self.key:
+ del self.contents[element.key]
def go_to(self, location):
"""Relocate the element to a specific location."""
current = self.get("location")
- if current and self.key in universe.contents[current].contents:
+ if current and self.key in self.universe.contents[current].contents:
del universe.contents[current].contents[self.key]
- if location in universe.contents: self.set("location", location)
- universe.contents[location].contents[self.key] = self
+ if location in self.universe.contents: self.set("location", location)
+ self.universe.contents[location].contents[self.key] = self
self.look_at(location)
def go_home(self):
"""Relocate the element to its default location."""
self.echo_to_location("You suddenly realize that " + self.get("name") + " is here.")
def move_direction(self, direction):
"""Relocate the element in a specified direction."""
- self.echo_to_location(self.get("name") + " exits " + universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
- self.send("You exit " + universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
- self.go_to(universe.contents[self.get("location")].link_neighbor(direction))
- self.echo_to_location(self.get("name") + " arrives from " + universe.categories["internal"]["directions"].getdict(direction)["enter"] + ".")
+ self.echo_to_location(self.get("name") + " exits " + self.universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
+ self.send("You exit " + self.universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".", add_prompt=False)
+ self.go_to(self.universe.contents[self.get("location")].link_neighbor(direction))
+ self.echo_to_location(self.get("name") + " arrives from " + self.universe.categories["internal"]["directions"].getdict(direction)["enter"] + ".")
def look_at(self, key):
"""Show an element to another element."""
if self.owner:
- element = universe.contents[key]
+ element = self.universe.contents[key]
message = ""
name = element.get("name")
if name: message += "$(cyn)" + name + "$(nrm)$(eol)"
if portal_list:
portal_list.sort()
message += "$(cyn)[ Exits: " + ", ".join(portal_list) + " ]$(nrm)$(eol)"
- for element in universe.contents[self.get("location")].contents.values():
+ for element in self.universe.contents[self.get("location")].contents.values():
if element.getboolean("is_actor") and element is not self:
message += "$(yel)" + element.get("name") + " is here.$(nrm)$(eol)"
+ elif element is not self:
+ message += "$(grn)" + element.get("impression") + "$(nrm)$(eol)"
self.send(message)
def portals(self):
"""Map the portal directions for a room to neighbors."""
portals = {}
if match("""^location:-?\d+,-?\d+,-?\d+$""", self.key):
coordinates = [(int(x)) for x in self.key.split(":")[1].split(",")]
- directions = universe.categories["internal"]["directions"]
+ directions = self.universe.categories["internal"]["directions"]
offsets = dict([(x, directions.getdict(x)["vector"]) for x in directions.facets()])
for portal in self.getlist("gridlinks"):
adjacent = map(lambda c,o: c+o, coordinates, offsets[portal])
neighbor = "location:" + ",".join([(str(x)) for x in adjacent])
- if neighbor in universe.contents: portals[portal] = neighbor
+ if neighbor in self.universe.contents: portals[portal] = neighbor
for facet in self.facets():
if facet.startswith("link_"):
neighbor = self.get(facet)
- if neighbor in universe.contents:
+ if neighbor in self.universe.contents:
portal = facet.split("_")[1]
portals[portal] = neighbor
return portals
if direction in portals: return portals[direction]
def echo_to_location(self, message):
"""Show a message to other elements in the current location."""
- for element in universe.contents[self.get("location")].contents.values():
+ for element in self.universe.contents[self.get("location")].contents.values():
if element is not self: element.send(message)
class DataFile:
"""A file containing universe elements."""
def __init__(self, filename, universe):
+ self.filename = filename
+ self.universe = universe
+ self.load()
+ def load(self):
+ """Read a file and create elements accordingly."""
self.modified = False
self.data = RawConfigParser()
- if access(filename, R_OK): self.data.read(filename)
- self.filename = filename
- universe.files[filename] = self
+ if access(self.filename, R_OK): self.data.read(self.filename)
+ if not hasattr(self.universe, "files"): self.universe.files = {}
+ self.universe.files[self.filename] = self
if self.data.has_option("__control__", "include_files"):
includes = makelist(self.data.get("__control__", "include_files"))
else: includes = []
if self.data.has_option("__control__", "default_files"):
origins = makedict(self.data.get("__control__", "default_files"))
for key in origins.keys():
- if not key in includes: includes.append(key)
- universe.default_origins[key] = origins[key]
- if not key in universe.categories:
- universe.categories[key] = {}
+ if not isabs(origins[key]):
+ origins[key] = path_join(dirname(self.filename), origins[key])
+ if not origins[key] in includes: includes.append(origins[key])
+ self.universe.default_origins[key] = origins[key]
+ if not key in self.universe.categories:
+ self.universe.categories[key] = {}
if self.data.has_option("__control__", "private_files"):
for item in makelist(self.data.get("__control__", "private_files")):
if not item in includes: includes.append(item)
- if not item in universe.private_files:
+ if not item in self.universe.private_files:
if not isabs(item):
- item = path_join(dirname(filename), item)
- universe.private_files.append(item)
+ item = path_join(dirname(self.filename), item)
+ self.universe.private_files.append(item)
for section in self.data.sections():
if section != "__control__":
- Element(section, universe, filename)
+ Element(section, self.universe, self.filename)
for include_file in includes:
if not isabs(include_file):
- include_file = path_join(dirname(filename), include_file)
- DataFile(include_file, universe)
- def is_writeable(self):
- """Returns True if the __control__ read_only is False."""
- return not self.data.has_option("__control__", "read_only") or not self.data.getboolean("__control__", "read_only")
+ include_file = path_join(dirname(self.filename), include_file)
+ if include_file not in self.universe.files or not self.universe.files[include_file].is_writeable():
+ DataFile(include_file, self.universe)
def save(self):
"""Write the data, if necessary."""
if not exists(dirname(self.filename)):
makedirs(dirname(self.filename))
+ # backup the file
+ if self.data.has_option("__control__", "backup_count"):
+ max_count = self.data.has_option("__control__", "backup_count")
+ else: max_count = universe.categories["internal"]["limits"].getint("default_backup_count")
+ if exists(self.filename) and max_count:
+ backups = []
+ for candidate in listdir(dirname(self.filename)):
+ if match(basename(self.filename) + """\.\d+$""", candidate):
+ backups.append(int(candidate.split(".")[-1]))
+ backups.sort()
+ backups.reverse()
+ for old_backup in backups:
+ if old_backup >= max_count-1:
+ remove(self.filename+"."+str(old_backup))
+ elif not exists(self.filename+"."+str(old_backup+1)):
+ rename(self.filename+"."+str(old_backup), self.filename+"."+str(old_backup+1))
+ if not exists(self.filename+".0"):
+ rename(self.filename, self.filename+".0")
+
# our data file
file_descriptor = file(self.filename, "w")
# if it's marked private, chmod it appropriately
- if self.filename in universe.private_files and oct(S_IMODE(stat(self.filename)[ST_MODE])) != 0600:
+ if self.filename in self.universe.private_files and oct(S_IMODE(stat(self.filename)[ST_MODE])) != 0600:
chmod(self.filename, 0600)
# write it back sorted, instead of using ConfigParser
# unset the modified flag
self.modified = False
+ def is_writeable(self):
+ """Returns True if the __control__ read_only is False."""
+ return not self.data.has_option("__control__", "read_only") or not self.data.getboolean("__control__", "read_only")
class Universe:
"""The universe."""
- def __init__(self, filename=""):
+
+ def __init__(self, filename="", load=False):
"""Initialize the universe."""
self.categories = {}
self.contents = {}
self.default_origins = {}
- self.files = {}
- self.private_files = []
- self.loglines = {}
+ self.loglines = []
self.pending_events_long = {}
self.pending_events_short = {}
+ self.private_files = []
+ self.reload_flag = False
+ self.startdir = getcwd()
+ self.terminate_flag = False
self.userlist = []
- self.terminate_world = False
- self.reload_modules = False
if not filename:
possible_filenames = [
".mudpyrc",
for filename in possible_filenames:
if access(filename, R_OK): break
if not isabs(filename):
- filename = abspath(filename)
- DataFile(filename, self)
+ filename = path_join(self.startdir, filename)
+ self.filename = filename
+ if load: self.load()
+
+ def load(self):
+ """Load universe data from persistent storage."""
+
+ # the files dict must exist and filename needs to be read-only
+ if not hasattr(self, "files") or not ( self.filename in self.files and self.files[self.filename].is_writeable() ):
+
+ # clear out all read-only files
+ if hasattr(self, "files"):
+ for data_filename in self.files.keys():
+ if not self.files[data_filename].is_writeable():
+ del self.files[data_filename]
+
+ # start loading from the initial file
+ DataFile(self.filename, self)
+
+ # make a list of inactive avatars
+ inactive_avatars = []
+ for account in self.categories["account"].values():
+ inactive_avatars += [ (self.contents[x]) for x in account.getlist("avatars") ]
+ for user in self.userlist:
+ if user.avatar in inactive_avatars:
+ inactive_avatars.remove(user.avatar)
+
+ # go through all elements to clear out inactive avatar locations
+ for element in self.contents.values():
+ location = element.get("location")
+ if element in inactive_avatars and location:
+ if location in self.contents and element.key in self.contents[location].contents:
+ del self.contents[location].contents[element.key]
+ element.set("default_location", location)
+ element.remove_facet("location")
+
+ # another pass to straighten out all the element contents
+ for element in self.contents.values():
+ element.update_location()
+ element.clean_contents()
+
+ def new(self):
+ """Create a new, empty Universe (the Big Bang)."""
+ new_universe = Universe()
+ for attribute in vars(self).keys():
+ exec("new_universe." + attribute + " = self." + attribute)
+ new_universe.reload_flag = False
+ del self
+ return new_universe
+
def save(self):
"""Save the universe to persistent storage."""
for key in self.files: self.files[key].save()
self.listening_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# bind the socket to to our desired server ipa and port
- self.listening_socket.bind((self.categories["internal"]["network"].get("host"), self.categories["internal"]["network"].getint("port")))
+ host = self.categories["internal"]["network"].get("host")
+ port = self.categories["internal"]["network"].getint("port")
+ self.listening_socket.bind((host, port))
# disable blocking so we can proceed whether or not we can
# send/receive
self.listening_socket.listen(1)
# note that we're now ready for user connections
- log("Waiting for connection(s)...")
+ if not host: host = "0.0.0.0"
+ log("Listening for Telnet connections on: " + host + ":" + str(port))
def get_time(self):
"""Convenience method to get the elapsed time counter."""
def __init__(self):
"""Default values for the in-memory user variables."""
+ self.account = None
self.address = ""
- self.last_address = ""
- self.connection = None
self.authenticated = False
- self.password_tries = 0
- self.state = "initial"
- self.menu_seen = False
+ self.avatar = None
+ self.connection = None
+ self.echoing = True
self.error = ""
self.input_queue = []
+ self.last_address = ""
+ self.menu_choices = {}
+ self.menu_seen = False
+ self.negotiation_pause = 0
self.output_queue = []
self.partial_input = ""
- self.echoing = True
+ self.password_tries = 0
self.received_newline = True
+ self.state = "initial"
self.terminator = IAC+GA
- self.negotiation_pause = 0
- self.avatar = None
- self.account = None
def quit(self):
"""Log, close the connection and remove."""
# create a new user object
new_user = User()
- # set everything else equivalent
- for attribute in [
- "address",
- "last_address",
- "connection",
- "authenticated",
- "password_tries",
- "state",
- "menu_seen",
- "error",
- "input_queue",
- "output_queue",
- "partial_input",
- "echoing",
- "received_newline",
- "terminator",
- "negotiation_pause",
- "avatar",
- "account"
- ]:
+ # set everything equivalent
+ for attribute in vars(self).keys():
exec("new_user." + attribute + " = self." + attribute)
+ # the avatar needs a new owner
+ if new_user.avatar: new_user.avatar.owner = new_user
+
# add it to the list
universe.userlist.append(new_user)
for old_user in universe.userlist:
# the name is the same but it's not us
- if old_user.account.get("name") == self.account.get("name") and old_user is not self:
+ if hasattr(old_user, "account") and old_user.account.get("name") == self.account.get("name") and old_user is not self:
# make a note of it
log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".", 2)
old_user.connection = self.connection
old_user.last_address = old_user.address
old_user.address = self.address
- old_user.echoing = self.echoing
+
+ # may need to tell the new connection to echo
+ if old_user.echoing:
+ old_user.send(get_echo_sequence(old_user.state, self.echoing), raw=True)
# take this one out of the list and delete
self.remove()
# strip extra $(eol) off if present
while output.startswith("$(eol)"): output = output[6:]
while output.endswith("$(eol)"): output = output[:-6]
+ extra_lines = output.find("$(eol)$(eol)$(eol)")
+ while extra_lines > -1:
+ output = output[:extra_lines] + output[extra_lines+6:]
+ extra_lines = output.find("$(eol)$(eol)$(eol)")
# we'll take out GA or EOR and add them back on the end
if output.endswith(IAC+GA) or output.endswith(IAC+EOR):
# start with a newline, append the message, then end
# with the optional eol string passed to this function
# and the ansi escape to return to normal text
- if not just_prompt: output = "$(eol)$(eol)" + output
+ if not just_prompt:
+ if not self.output_queue or not self.output_queue[-1].endswith("\r\n"):
+ output = "$(eol)$(eol)" + output
+ elif not self.output_queue[-1].endswith("\r\n"+chr(27)+"[0m"+"\r\n") and not self.output_queue[-1].endswith("\r\n\r\n"):
+ output = "$(eol)" + output
output += eol + chr(27) + "[0m"
# tack on a prompt if active
if self.state == "active":
if not just_prompt: output += "$(eol)"
- if add_prompt: output += "> "
+ if add_prompt:
+ output += "> "
+ mode = self.avatar.get("mode")
+ if mode: output += "(" + mode + ") "
# find and replace macros in the output
output = replace_macros(self, output)
- # wrap the text at 80 characters
- output = wrap_ansi_text(output, 80)
+ # wrap the text at 79 characters
+ output = wrap_ansi_text(output, 79)
# tack the terminator back on
if terminate: output += self.terminator
"""All the things to do to the user per increment."""
# if the world is terminating, disconnect
- if universe.terminate_world:
+ if universe.terminate_flag:
self.state = "disconnecting"
self.menu_seen = False
self.enqueue_input()
# there is input waiting in the queue
- if self.input_queue: handle_user_input(self)
+ if self.input_queue:
+ handle_user_input(self)
def flush(self):
"""Try to send the last item in the queue and remove it."""
"""Have the active avatar leave the world."""
if self.avatar:
current = self.avatar.get("location")
- self.avatar.set("default_location", current)
- self.avatar.echo_to_location("You suddenly wonder where " + self.avatar.get("name") + " went.")
- del universe.contents[current].contents[self.avatar.key]
- self.avatar.remove_facet("location")
+ if current:
+ self.avatar.set("default_location", current)
+ self.avatar.echo_to_location("You suddenly wonder where " + self.avatar.get("name") + " went.")
+ del universe.contents[current].contents[self.avatar.key]
+ self.avatar.remove_facet("location")
self.avatar.owner = None
self.avatar = None
# send the timestamp and line to a file
if file_name:
+ if not isabs(file_name):
+ file_name = path_join(universe.startdir, file_name)
file_descriptor = file(file_name, "a")
for line in lines: file_descriptor.write(timestamp + " " + line + "\n")
file_descriptor.flush()
while 0 < len(universe.loglines) >= max_log_lines: del universe.loglines[0]
universe.loglines.append((level, timestamp + " " + line))
-def get_loglines(level, start, stop=0):
+def get_loglines(level, start, stop):
"""Return a specific range of loglines filtered by level."""
- # begin with a blank message
- message = ""
-
# filter the log lines
- loglines = filter(lambda x,y: x>=level, universe.loglines)
+ loglines = filter(lambda x: x[0]>=level, universe.loglines)
- # we need this in several places
- count = len(loglines)
+ # we need these in several places
+ total_count = str(len(universe.loglines))
+ filtered_count = len(loglines)
# don't proceed if there are no lines
- if count:
+ if filtered_count:
# can't start before the begining or at the end
- if start > count: start = count
+ if start > filtered_count: start = filtered_count
if start < 1: start = 1
# can't stop before we start
- if stop >= start: stop = start - 1
+ if stop > start: stop = start
+ elif stop < 1: stop = 1
# some preamble
- message += "There are " + str(len(universe.loglist))
- message += " log lines in memory and " + str(count)
+ message = "There are " + str(total_count)
+ message += " log lines in memory and " + str(filtered_count)
message += " at or above level " + str(level) + "."
- message += " The lines from " + str(stop)
- message += " to " + str(start) + " are:$(eol)$(eol)"
+ message += " The matching lines from " + str(stop) + " to "
+ message += str(start) + " are:$(eol)$(eol)"
# add the text from the selected lines
- for line in loglines[-start:-stop]:
- message += " " + line[1] + "$(eol)"
+ if stop > 1: range_lines = loglines[-start:-(stop-1)]
+ else: range_lines = loglines[-start:]
+ for line in range_lines:
+ message += " (" + str(line[0]) + ") " + line[1] + "$(eol)"
+
+ # there were no lines
+ else:
+ message = "None of the " + str(total_count)
+ message += " lines in memory matches your request."
# pass it back
return message
"""Escapes replacement macros in text."""
return text.replace("$(", "$_(")
-def check_time(frequency):
- """Check for a factor of the current increment count."""
- if type(frequency) is str:
- frequency = universe.categories["internal"]["time"].getint(frequency)
- if not "counters" in universe.categories["internal"]:
- Element("internal:counters", universe)
- return not universe.categories["internal"]["counters"].getint("elapsed") % frequency
+def first_word(text, separator=" "):
+ """Returns a tuple of the first word and the rest."""
+ if text:
+ if text.find(separator) > 0: return text.split(separator, 1)
+ else: return text, ""
+ else: return "", ""
def on_pulse():
"""The things which should happen on each pulse, aside from reloads."""
# iterate over the connected users
for user in universe.userlist: user.pulse()
+ # add an element for counters if it doesn't exist
+ if not "counters" in universe.categories["internal"]:
+ universe.categories["internal"]["counters"] = Element("internal:counters", universe)
+
# update the log every now and then
- if check_time("frequency_log"):
+ if not universe.categories["internal"]["counters"].getint("mark"):
log(str(len(universe.userlist)) + " connection(s)")
+ universe.categories["internal"]["counters"].set("mark", universe.categories["internal"]["time"].getint("frequency_log"))
+ else: universe.categories["internal"]["counters"].set("mark", universe.categories["internal"]["counters"].getint("mark") - 1)
# periodically save everything
- if check_time("frequency_save"):
+ if not universe.categories["internal"]["counters"].getint("save"):
universe.save()
+ universe.categories["internal"]["counters"].set("save", universe.categories["internal"]["time"].getint("frequency_save"))
+ else: universe.categories["internal"]["counters"].set("save", universe.categories["internal"]["counters"].getint("save") - 1)
# pause for a configurable amount of time (decimal seconds)
sleep(universe.categories["internal"]["time"].getfloat("increment"))
- # increment the elapsed increment counter
+ # increase the elapsed increment counter
universe.categories["internal"]["counters"].set("elapsed", universe.categories["internal"]["counters"].getint("elapsed") + 1)
def reload_data():
- """Reload data into new persistent objects."""
+ """Reload all relevant objects."""
for user in universe.userlist[:]: user.reload()
+ for element in universe.contents.values():
+ if element.origin.is_writeable(): element.reload()
+ universe.load()
def check_for_connection(listening_socket):
"""Check for a waiting connection and return a new user object."""
def handle_user_input(user):
"""The main handler, branches to a state-specific handler."""
+ # if the user's client echo is off, send a blank line for aesthetics
+ if user.echoing: user.received_newline = True
+
# check to make sure the state is expected, then call that handler
if "handler_" + user.state in globals():
exec("handler_" + user.state + "(user)")
# since we got input, flag that the menu/prompt needs to be redisplayed
user.menu_seen = False
- # if the user's client echo is off, send a blank line for aesthetics
- if user.echoing: user.received_newline = True
-
def generic_menu_handler(user):
"""A generic menu choice handler."""
# is there input?
if input_data:
- # split out the command (first word) and parameters (everything else)
- if input_data.find(" ") > 0:
- command_name, parameters = input_data.split(" ", 1)
+ # split out the command and parameters
+ actor = user.avatar
+ mode = actor.get("mode")
+ if mode and input_data.startswith("!"):
+ command_name, parameters = first_word(input_data[1:])
+ elif mode == "chat":
+ command_name = "say"
+ parameters = input_data
else:
- command_name = input_data
- parameters = ""
+ command_name, parameters = first_word(input_data)
# lowercase the command
command_name = command_name.lower()
else: command = None
# if it's allowed, do it
- if user.avatar.can_run(command): exec(command.get("action"))
+ if actor.can_run(command): exec(command.get("action"))
# otherwise, give an error
- elif command_name: command_error(user.avatar, input_data)
+ elif command_name: command_error(actor, input_data)
# if no input, just idle back with a prompt
else: user.send("", just_prompt=True)
log(message, 8)
# set a flag to terminate the world
- universe.terminate_world = True
+ universe.terminate_flag = True
def command_reload(actor):
"""Reload all code modules, configs and data."""
log("User " + actor.owner.account.get("name") + " reloaded the world.", 8)
# set a flag to reload
- universe.reload_modules = True
+ universe.reload_flag = True
def command_quit(actor):
"""Leave the world and go back to the main menu."""
if actor.owner:
- actor.owner.deactivate_avatar()
actor.owner.state = "main_utility"
+ actor.owner.deactivate_avatar()
def command_help(actor, parameters):
"""List available commands and provide help for commands."""
help_text = "No help is provided for this command."
output += help_text
+ # list related commands
+ see_also = command.getlist("see_also")
+ if see_also:
+ really_see_also = ""
+ for item in see_also:
+ if item in universe.categories["command"]:
+ command = universe.categories["command"][item]
+ if actor.can_run(command):
+ if really_see_also:
+ really_see_also += ", "
+ if command.getboolean("administrative"):
+ really_see_also += "$(red)"
+ else:
+ really_see_also += "$(grn)"
+ really_see_also += item + "$(nrm)"
+ if really_see_also:
+ output += "$(eol)$(eol)See also: " + really_see_also
+
# no data for the requested command word
else:
output = "That is not an available command."
# the user entered a message
elif parameters:
- # get rid of quote marks on the ends of the message and
- # capitalize the first letter
- message = parameters.strip("\"'`").capitalize()
-
- # a dictionary of punctuation:action pairs
- actions = {}
- for facet in universe.categories["internal"]["language"].facets():
- if facet.startswith("punctuation_"):
- action = facet.split("_")[1]
- for mark in universe.categories["internal"]["language"].getlist(facet):
- actions[mark] = action
+ # get rid of quote marks on the ends of the message
+ message = parameters.strip("\"'`")
# match the punctuation used, if any, to an action
+ actions = universe.categories["internal"]["language"].getdict("actions")
default_punctuation = universe.categories["internal"]["language"].get("default_punctuation")
- action = actions[default_punctuation]
+ action = ""
for mark in actions.keys():
- if message.endswith(mark) and mark != default_punctuation:
+ if message.endswith(mark):
action = actions[mark]
break
- # if the action is default and there is no mark, add one
- if action == actions[default_punctuation] and not message.endswith(default_punctuation):
- message += default_punctuation
+ # add punctuation if needed
+ if not action:
+ action = actions[default_punctuation]
+ if message and not message[-1] in punctuation:
+ message += default_punctuation
+
+ # decapitalize the first letter to improve matching
+ if message and message[0] in uppercase:
+ message = message[0].lower() + message[1:]
+
+ # iterate over all words in message, replacing typos
+ typos = universe.categories["internal"]["language"].getdict("typos")
+ words = message.split()
+ for index in range(len(words)):
+ word = words[index]
+ bare_word = word.strip(punctuation)
+ if bare_word in typos.keys():
+ words[index] = word.replace(bare_word, typos[bare_word])
+ message = " ".join(words)
- # capitalize a list of words within the message
- capitalize_words = universe.categories["internal"]["language"].getlist("capitalize_words")
- for word in capitalize_words:
- message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
+ # capitalize the first letter
+ message = message[0].upper() + message[1:]
# tell the room
actor.echo_to_location(actor.get("name") + " " + action + "s, \"" + message + "\"")
else:
actor.send("What do you want to say?")
+def command_chat(actor):
+ """Toggle chat mode."""
+ mode = actor.get("mode")
+ if not mode:
+ actor.set("mode", "chat")
+ actor.send("Entering chat mode (use $(grn)!chat$(nrm) to exit).")
+ elif mode == "chat":
+ actor.remove_facet("mode")
+ actor.send("Exiting chat mode.")
+ else: actor.send("Sorry, but you're already busy with something else!")
+
def command_show(actor, parameters):
"""Show program data."""
message = ""
- if parameters.find(" ") < 1:
- if parameters == "time":
- message = universe.categories["internal"]["counters"].get("elapsed") + " increments elapsed since the world was created."
- elif parameters == "categories":
- message = "These are the element categories:$(eol)"
- categories = universe.categories.keys()
- categories.sort()
- for category in categories: message += "$(eol) $(grn)" + category + "$(nrm)"
- elif parameters == "files":
- message = "These are the current files containing the universe:$(eol)"
- filenames = universe.files.keys()
- filenames.sort()
- for filename in filenames: message += "$(eol) $(grn)" + filename + "$(nrm)"
- else: message = ""
- else:
- arguments = parameters.split()
- if arguments[0] == "category":
- if arguments[1] in universe.categories:
- message = "These are the elements in the \"" + arguments[1] + "\" category:$(eol)"
- elements = universe.categories[arguments[1]].keys()
- elements.sort()
- for element in elements:
- message += "$(eol) $(grn)" + universe.categories[arguments[1]][element].key + "$(nrm)"
- elif arguments[0] == "element":
- if arguments[1] in universe.contents:
- message = "These are the properties of the \"" + arguments[1] + "\" element:$(eol)"
- element = universe.contents[arguments[1]]
- facets = element.facets()
- facets.sort()
- for facet in facets:
- message += "$(eol) $(grn)" + facet + ": $(red)" + escape_macros(element.get(facet)) + "$(nrm)"
- elif arguments[0] == "result":
- if len(arguments) > 1:
- try:
- message = repr(eval(" ".join(arguments[1:])))
- except:
- message = "Your expression raised an exception!"
- elif arguments[0] == "log":
- if match("^\d+$", arguments[1]) and int(arguments[1]) > 0:
- linecount = int(arguments[1])
- if linecount > len(universe.loglines): linecount = len(universe.loglist)
- message = "There are " + str(len(universe.loglist)) + " log lines in memory."
- message += " The most recent " + str(linecount) + " lines are:$(eol)$(eol)"
- for line in universe.loglist[-linecount:]:
- message += " " + line + "$(eol)"
- else: message = "\"" + arguments[1] + "\" is not a positive integer greater than 0."
- if not message:
- if parameters: message = "I don't know what \"" + parameters + "\" is."
- else: message = "What do you want to show?"
+ arguments = parameters.split()
+ if not parameters: message = "What do you want to show?"
+ elif arguments[0] == "time":
+ message = universe.categories["internal"]["counters"].get("elapsed") + " increments elapsed since the world was created."
+ elif arguments[0] == "categories":
+ message = "These are the element categories:$(eol)"
+ categories = universe.categories.keys()
+ categories.sort()
+ for category in categories: message += "$(eol) $(grn)" + category + "$(nrm)"
+ elif arguments[0] == "files":
+ message = "These are the current files containing the universe:$(eol)"
+ filenames = universe.files.keys()
+ filenames.sort()
+ for filename in filenames:
+ if universe.files[filename].is_writeable(): status = "rw"
+ else: status = "ro"
+ message += "$(eol) $(red)(" + status + ") $(grn)" + filename + "$(nrm)"
+ elif arguments[0] == "category":
+ if len(arguments) != 2: message = "You must specify one category."
+ elif arguments[1] in universe.categories:
+ message = "These are the elements in the \"" + arguments[1] + "\" category:$(eol)"
+ elements = [(universe.categories[arguments[1]][x].key) for x in universe.categories[arguments[1]].keys()]
+ elements.sort()
+ for element in elements:
+ message += "$(eol) $(grn)" + element + "$(nrm)"
+ else: message = "Category \"" + arguments[1] + "\" does not exist."
+ elif arguments[0] == "file":
+ if len(arguments) != 2: message = "You must specify one file."
+ elif arguments[1] in universe.files:
+ message = "These are the elements in the \"" + arguments[1] + "\" file:$(eol)"
+ elements = universe.files[arguments[1]].data.sections()
+ elements.sort()
+ for element in elements:
+ message += "$(eol) $(grn)" + element + "$(nrm)"
+ else: message = "Category \"" + arguments[1] + "\" does not exist."
+ elif arguments[0] == "element":
+ if len(arguments) != 2: message = "You must specify one element."
+ elif arguments[1] in universe.contents:
+ element = universe.contents[arguments[1]]
+ message = "These are the properties of the \"" + arguments[1] + "\" element (in \"" + element.origin.filename + "\"):$(eol)"
+ facets = element.facets()
+ facets.sort()
+ for facet in facets:
+ message += "$(eol) $(grn)" + facet + ": $(red)" + escape_macros(element.get(facet)) + "$(nrm)"
+ else: message = "Element \"" + arguments[1] + "\" does not exist."
+ elif arguments[0] == "result":
+ if len(arguments) < 2: message = "You need to specify an expression."
+ else:
+ try:
+ message = repr(eval(" ".join(arguments[1:])))
+ except:
+ message = "Your expression raised an exception!"
+ elif arguments[0] == "log":
+ if len(arguments) == 4:
+ if match("^\d+$", arguments[3]) and int(arguments[3]) >= 0:
+ stop = int(arguments[3])
+ else: stop = -1
+ else: stop = 0
+ if len(arguments) >= 3:
+ if match("^\d+$", arguments[2]) and int(arguments[2]) > 0:
+ start = int(arguments[2])
+ else: start = -1
+ else: start = 10
+ if len(arguments) >= 2:
+ if match("^\d+$", arguments[1]) and 0 <= int(arguments[1]) <= 9:
+ level = int(arguments[1])
+ else: level = -1
+ elif 0 <= actor.owner.account.getint("loglevel") <= 9:
+ level = actor.owner.account.getint("loglevel")
+ else: level = 1
+ if level > -1 and start > -1 and stop > -1:
+ message = get_loglines(level, start, stop)
+ else: message = "When specified, level must be 0-9 (default 1), start and stop must be >=1 (default 10 and 1)."
+ else: message = "I don't know what \"" + parameters + "\" is."
actor.send(message)
def command_create(actor, parameters):
# send the error message
actor.send(message)
+def daemonize():
+ """Fork and disassociate from everything."""
+ if universe.contents["internal:process"].getboolean("daemon"):
+ import sys
+ from resource import getrlimit, RLIMIT_NOFILE
+ log("Disassociating from the controlling terminal.")
+ if fork(): _exit(0)
+ setsid()
+ if fork(): _exit(0)
+ chdir("/")
+ umask(0)
+ for stdpipe in range(3): close(stdpipe)
+ sys.stdin = sys.__stdin__ = file("/dev/null", "r")
+ sys.stdout = sys.stderr = sys.__stdout__ = sys.__stderr__ = file("/dev/null", "w")
+
+def create_pidfile(universe):
+ """Write a file containing the current process ID."""
+ pid = str(getpid())
+ log("Process ID: " + pid)
+ file_name = universe.contents["internal:process"].get("pidfile")
+ if file_name:
+ if not isabs(file_name):
+ file_name = path_join(universe.startdir, file_name)
+ file_descriptor = file(file_name, 'w')
+ file_descriptor.write(pid + "\n")
+ file_descriptor.flush()
+ file_descriptor.close()
+
+def remove_pidfile(universe):
+ """Remove the file containing the current process ID."""
+ file_name = universe.contents["internal:process"].get("pidfile")
+ if file_name:
+ if not isabs(file_name):
+ file_name = path_join(universe.startdir, file_name)
+ if access(file_name, W_OK): remove(file_name)
+
+def excepthook(excepttype, value, traceback):
+ """Handle uncaught exceptions."""
+
+ # assemble the list of errors into a single string
+ message = "".join(format_exception(excepttype, value, traceback))
+
+ # try to log it, if possible
+ try: log(message, 9)
+ except: pass
+
+ # try to write it to stderr, if possible
+ try: stderr.write(message)
+ except: pass
+
+def sighook(what, where):
+ """Handle external signals."""
+
+ # a generic message
+ message = "Caught signal: "
+
+ # for a hangup signal
+ if what == SIGHUP:
+ message += "hangup (reloading)"
+ universe.reload_flag = True
+
+ # for a terminate signal
+ elif what == SIGTERM:
+ message += "terminate (halting)"
+ universe.terminate_flag = True
+
+ # catchall for unexpected signals
+ else: message += str(what) + " (unhandled)"
+
+ # log what happened
+ log(message, 8)
+
+# redefine sys.excepthook with ours
+import sys
+sys.excepthook = excepthook
+
+# assign the sgnal handlers
+signal(SIGHUP, sighook)
+signal(SIGTERM, sighook)
+
# if there is no universe, create an empty one
-if not "universe" in locals(): universe = Universe()
+if not "universe" in locals():
+ if len(argv) > 1: conffile = argv[1]
+ else: conffile = ""
+ universe = Universe(conffile, True)
+elif universe.reload_flag:
+ universe = universe.new()
+ reload_data()