description = List commands or get help on one.
help = This will list all comand words available to you along with a brief description or, alternatively, give you detailed information on one command.
-[command:time]
-action = command_time(user, command, parameters)
-description = Show the current world time in elapsed increments.
-help = This will show the current world time in elapsed increments.
-
[command:show]
action = command_show(user, command, parameters)
description = Show program data.
-help = For now, this is used to show things like "universe" and "avatars".
+help = For now, this is used to show things like "avatars", "time" and "universe".
[command:reload]
action = command_reload(user, command, parameters)
+++ /dev/null
-[include]
-actors = actors
-avatars = avatars
-rooms = rooms
-
+++ /dev/null
-[room:1,0,0,0]
-
-[room:0,0,0,0]
-
-[room:-1,0,0,0]
-
+++ /dev/null
-[include]
-universe = universe/index
-
+++ /dev/null
-[checking_new_account_name]
-description = There is no existing account for "$(account)" (note that an account name is not the same as a character name). Would you like to create a new account by this name, go back and enter a different name or disconnect now?
-choice_d = disconnect now
-choice_g = go back
-choice_n = new account
-prompt = Enter your choice:
-default = d
-
-[entering_new_password]
-prompt = Enter a new password for "$(account)":
-echo = off
-error_weak = That is a weak password... Try something at least 7 characters long with a combination of mixed-case letters, numbers and punctuation/spaces.
-error_differs = The two passwords did not match. Try again...
-
-[verifying_new_password]
-prompt = Enter the same new password again:
-echo = off
-
+++ /dev/null
-[index]
-files = account_creation login miscellaneous
-
+++ /dev/null
-[entering_account_name]
-description = Welcome to the mudpy example...
-prompt = Identify yourself:
-
-[checking_password]
-prompt = Password:
-echo = off
-error_incorrect = Incorrect password, please try again...
-
-[disconnecting_duplicates]
-prompt = $(red)Closing your previous connection...$(nrm)$(eol)
-
+++ /dev/null
-"""Configuration objects for the MUFF Engine"""
-
-# Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
-# Licensed per terms in the LICENSE file distributed with this software.
-
-# muff configuration files use the ini format supported by ConfigParser
-import ConfigParser
-
-# need os for testing whether the config file exists and is readable
-import os
-
-# hack to load all modules in teh muff package
-import muff
-for module in muff.__all__:
- exec("import " + module)
-
-# list of places to look for the config
-config_dirs = [".", "./etc", "/usr/local/muff", "/usr/local/muff/etc", "/etc/muff", "/etc" ]
-
-# name of the config file
-config_name = "mudpy.conf"
-
-# find the config file
-for each_dir in config_dirs:
- config_file = each_dir + "/" + config_name
- if os.access(config_file, os.R_OK): break
-
-# read the config
-config_data = ConfigParser.SafeConfigParser()
-config_data.read(config_file)
-
-def get(section, option):
- """Convenience function to get configuration data."""
- return config_data.get(section, option)
-
-def getfloat(section, option):
- "Convenience function to get floating-point configuration data."""
- return config_data.getfloat(section, option)
-
-def getint(section, option):
- """Convenience function to get integer configuration data."""
- return config_data.getint(section, option)
-
-def set(section, option, value):
- """Convenienve function to set miscellaneous configuration data."""
- return config_data.get(section, option, repr(value))
-
+++ /dev/null
-# Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
-# Licensed per terms in the LICENSE file distributed with this software.
-
-# persistent variables are stored in ini-style files supported by ConfigParser
-import ConfigParser
-
-# need to know the directory separator
-import os
-
-# hack to load all modules in the muff package
-import muff
-for module in muff.__all__:
- exec("import " + module)
-
-class Element:
- """An element of the universe."""
- def __init__(self, key, origin, universe):
- """Default values for the in-memory element variables."""
- self.key = key
- if not origin.startswith(os.sep):
- origin = os.getcwd() + os.sep + origin
- self.origin = origin
- universe.contents[key] = self
- if key.find(":"):
- category = key.split(":", 1)
- if category[0] in universe.categories:
- exec("universe." + category[0] + "s[category[1]] = self")
- if not origin in universe.files.keys():
- DataFile(origin, universe)
- if not universe.files[origin].data.has_section(key):
- universe.files[origin].data.add_section(key)
- def facets(self):
- """Return a list of facets for this element."""
- return universe.files[self.origin].data.options(self.key)
- def get(self, facet):
- """Retrieve values."""
- if facet in dir(self): return self.facet
- else:
- try:
- return universe.files[self.origin].data.get(self.key, facet)
- except:
- return None
- def getint(self, facet):
- """Convenience method to coerce return values as type int."""
- value = self.get(facet)
- if not value: value = 0
- elif type(value) is str: value = value.rstrip("L")
- return int(value)
- def set(self, facet, value):
- """Set values."""
- if facet in universe.files[self.origin].data.options(self.key):
- universe.files[self.origin].data.set(self.key, facet, repr(value))
- else: self.facet = value
- def rec(self, facet, value):
- """Add initial values to the record."""
- universe.files[self.origin].data.set(self.key, facet, value)
-
-class DataFile:
- """A file containing universe elements."""
- def __init__(self, filename, universe):
- filedir = os.sep.join(filename.split(os.sep)[:-1])
- data = ConfigParser.SafeConfigParser()
- data.read(filename)
- self.filename = filename
- self.data = data
- universe.files[filename] = self
- for section in data.sections():
- if section == "include":
- for option in data.options(section):
- includefile = data.get(section, option)
- if not includefile.startswith(os.sep):
- includefile = filedir + os.sep + includefile
- DataFile(includefile, universe)
- else:
- Element(section, filename, universe)
- def save(self):
- basedir = os.sep.join(self.filename.split(os.sep)[:-1])
- if not os.access(basedir, os.F_OK): os.makedirs(basedir)
- file_descriptor = file(self.filename, "w")
- self.data.write(file_descriptor)
- file_descriptor.flush()
- file_descriptor.close()
-
-class Universe:
- """The universe."""
- def __init__(self):
- self.contents = {}
- self.files = {}
- self.categories = [ "account", "actor", "command", "internal", "item", "menu", "room" ]
- for item in self.categories: exec("self." + item + "s = {}")
- for item in [ "avatars", "commands", "internals", "universe" ]:
- filename = muffconf.get("files", item)
- if not filename.startswith(os.sep): filename = os.getcwd() + os.sep + filename
- DataFile(filename, self)
- def save(self):
- for key in self.files.keys(): self.files[key].save()
-
-def element_exists(key): return key in universe.contents.keys()
-
-# reload the muffconf module if the files:data setting does not exist
-try:
- if muffconf.has_section("files"): pass
-except AttributeError:
- reload(muffconf)
-
-# if there is no universe, create an empty one
-if not "universe" in dir(): universe = Universe()
-
-[active]
-prompt = >
-
-[disconnecting]
-description = $(red)Disconnecting...$(nrm)
-
-[main_utility]
-description = From here you can activate, create and delete avatars. An avatar is your persona in the world of Example.
-choice_a = activate an existing avatar
-#branch_a = activate_avatar
-branch_a = active
-choice_c = create a new avatar
-action_c = user.new_avatar()
-branch_c = choose_gender
-choice_d = delete an unwanted avatar
-branch_d = delete_avatar
-choice_l = leave example for now
-branch_l = disconnecting
-choice_p = permanently remove your account
-branch_p = delete_account
-prompt = What would you like to do?
-error_no_avatars = You don't have any avatars yet. An avatar is your persona in the world of Example. It is recommended that you create one now.
-
-[choose_gender]
-description = First, your new avatar needs a gender. In the world of Example, all avatars are either male or female.
+[menu:choose_gender]
choice_f = female
+prompt = Pick a gender for your new avatar:
+description = First, your new avatar needs a gender. In the world of Example, all avatars are either male or female.
choice_m = male
-action = user.avatar.rec("gender", user.menu_choices[choice])
branch = choose_name
-prompt = Pick a gender for your new avatar:
+action = user.avatar.set("gender", user.menu_choices[choice])
+
+[menu:entering_new_password]
+error_differs = The two passwords did not match. Try again...
+error_weak = That is a weak password... Try something at least 7 characters long with a combination of mixed-case letters, numbers and punctuation/spaces.
+prompt = Enter a new password for "$(account)":
+echo = off
+
+[menu:verifying_new_password]
+prompt = Enter the same new password again:
+echo = off
-[choose_name]
+[menu:checking_new_account_name]
+choice_d = disconnect now
+choice_g = go back
+prompt = Enter your choice:
+description = There is no existing account for "$(account)" (note that an account name is not the same as a character name). Would you like to create a new account by this name, go back and enter a different name or disconnect now?
+default = d
+choice_n = new account
+
+[menu:disconnecting_duplicates]
+prompt = $(red)Closing your previous connection...$(nrm)$(eol)
+
+[menu:choose_name]
+prompt = Choose a name for $(tpop):
description = Your new avatar needs a name. This will be the name with which $(tpsp) grew up, and will initially be the name by which $(tpsp) is known in the world of Example. There are ways for your new avatar to make a name for $(tpop)self over time, so $(tpsp) won't be stuck going by such an unremarkable name forever.
create_1 = muffmisc.random_name()
-create_2 = muffmisc.random_name()
create_3 = muffmisc.random_name()
-create_4 = muffmisc.random_name()
+create_2 = muffmisc.random_name()
create_5 = muffmisc.random_name()
-create_6 = muffmisc.random_name()
+create_4 = muffmisc.random_name()
create_7 = muffmisc.random_name()
-action = user.avatar.rec("name", user.menu_choices[choice])
+create_6 = muffmisc.random_name()
branch = active
-prompt = Choose a name for $(tpop):
+action = user.avatar.set("name", user.menu_choices[choice])
+
+[menu:entering_account_name]
+prompt = Identify yourself:
+description = Welcome to the mudpy example...
+error_bad_name = Your account name needs to contain only digits (0-9) and letters (a-z).
+
+[menu:disconnecting]
+description = $(red)Disconnecting...$(nrm)
+
+[menu:active]
+prompt = >
+
+[menu:main_utility]
+choice_d = delete an unwanted avatar
+branch_d = delete_avatar
+action_c = user.new_avatar()
+description = From here you can activate, create and delete avatars. An avatar is your persona in the world of Example.
+branch_c = choose_gender
+choice_c = create a new avatar
+choice_l = leave example for now
+branch_l = disconnecting
+prompt = What would you like to do?
+error_no_avatars = You don't have any avatars yet. An avatar is your persona in the world of Example. It is recommended that you create one now.
+choice_p = permanently remove your account
+branch_p = delete_account
+branch_a = active
+choice_a = activate an existing avatar
+
+[menu:checking_password]
+prompt = Password:
+error_incorrect = Incorrect password, please try again...
+echo = off
+[control]
+read_only = yes
+
+[categories]
+account = account
+actor = actor
+command = command
+internal = internal
+location = location
+menu = menu
+other = other
+
+[include]
+testdata = testdata
+
[files]
-accounts = ./lib/accounts
-avatars = data/avatars
-commands = data/commands
-internals = data/internals
-menus = ./lib/menus
-modules = ./lib
-universe = data/universe/index
-
-[general]
+modules =
+
+[internal:general]
password_tries = 3
-[language]
+[internal:language]
capitalize = i i'd i'll i'm
default_punctuation = .
punctuation_ask = ?
punctuation_muse = ...
punctuation_say = .
-[network]
+[internal:network]
host =
port = 6669
-[time]
+[internal:time]
definition_d = 24h
definition_h = 60mi
definition_mi = 10r
# these are all the modules included in the muff package; if you create
# another, be sure to add it to this list
-__all__ = [ "muffcmds", "muffconf", "muffmain", "muffmenu", "muffmisc", "muffsock", "muffuniv", "muffuser", "muffvars" ]
+__all__ = [ "muffcmds", "muffmain", "muffmenu", "muffmisc", "muffsock", "muffuniv", "muffuser", "muffvars" ]
if input_data:
# keep only the first word and convert to lower-case
- user.proposed_name = string.split(input_data)[0].lower()
+ name = input_data.lower()
- # if we have a password hash, time to request a password
- if user.get_passhash():
+ # fail if there are non-alphanumeric characters
+ if name != filter(lambda x: x>="0" and x<="9" or x>="a" and x<="z", name):
+ user.error = "bad_name"
+
+ # if that account exists, time to request a password
+ elif name in muffuniv.universe.categories["account"]:
+ user.account = muffuniv.universe.categories["account"][name]
user.state = "checking_password"
# otherwise, this could be a brand new user
else:
- user.name = user.proposed_name
- user.proposed_name = None
- user.load()
- muffmisc.log("New user: " + user.name)
+ user.account = muffuniv.Element("account:" + name, muffuniv.universe)
+ user.account.set("name", name)
+ muffmisc.log("New user: " + name)
user.state = "checking_new_account_name"
# if the user entered nothing for a name, then buhbye
input_data = user.input_queue.pop(0)
# does the hashed input equal the stored hash?
- if md5.new(user.proposed_name + input_data).hexdigest() == user.passhash:
+ if md5.new(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
# if so, set the username and load from cold storage
- user.name = user.proposed_name
- del(user.proposed_name)
if not user.replace_old_connections():
- user.load()
user.authenticate()
user.state = "main_utility"
# if at first your hashes don't match, try, try again
- elif user.password_tries < muffconf.getint("general", "password_tries"):
+ elif user.password_tries < muffuniv.universe.categories["internal"]["general"].getint("password_tries"):
user.password_tries += 1
user.error = "incorrect"
# user selected to disconnect
if choice == "d":
+ user.account.delete()
user.state = "disconnecting"
# go back to the login screen
elif choice == "g":
+ user.account.delete()
user.state = "entering_account_name"
# new user, so ask for a password
if len(input_data) > 6 and len(filter(lambda x: x>="0" and x<="9", input_data)) and len(filter(lambda x: x>="A" and x<="Z", input_data)) and len(filter(lambda x: x>="a" and x<="z", input_data)):
# hash and store it, then move on to verification
- user.passhash = md5.new(user.name + input_data).hexdigest()
+ user.account.set("passhash", md5.new(user.account.get("name") + input_data).hexdigest())
user.state = "verifying_new_password"
# the password was weak, try again if you haven't tried too many times
- elif user.password_tries < muffconf.getint("general", "password_tries"):
+ elif user.password_tries < muffuniv.universe.categories["internal"]["general"].getint("password_tries"):
user.password_tries += 1
user.error = "weak"
# too many tries, so adios
else:
user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
+ user.account.delete()
user.state = "disconnecting"
def handler_verifying_new_password(user):
input_data = user.input_queue.pop(0)
# hash the input and match it to storage
- if md5.new(user.name + input_data).hexdigest() == user.passhash:
+ if md5.new(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
user.authenticate()
- user.save()
# the hashes matched, so go active
if not user.replace_old_connections(): user.state = "main_utility"
# go back to entering the new password as long as you haven't tried
# too many times
- elif user.password_tries < muffconf.getint("general", "password_tries"):
+ elif user.password_tries < muffuniv.universe.categories["internal"]["general"].getint("password_tries"):
user.password_tries += 1
user.error = "differs"
user.state = "entering_new_password"
# otherwise, sayonara
else:
user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
+ user.account.delete()
user.state = "disconnecting"
def handler_active(user):
input_data = user.input_queue.pop(0)
# split out the command (first word) and parameters (everything else)
- try:
- inputlist = string.split(input_data, None, 1)
- command = inputlist[0]
- except:
+ if input_data.find(" ") > 0:
+ command, parameters = input_data.split(" ", 1)
+ else:
command = input_data
- try:
- parameters = inputlist[1]
- except:
parameters = ""
- del(inputlist)
# lowercase the command
command = command.lower()
# the command matches a command word for which we have data
- if command in muffuniv.universe.commands.keys():
- exec(muffuniv.universe.commands[command].get("action"))
+ if command in muffuniv.universe.categories["command"]:
+ exec(muffuniv.universe.categories["command"][command].get("action"))
# no data matching the entered command word
elif command: command_error(user, command, parameters)
# see if there's a message or use a generic one
if parameters: message = "Halting: " + parameters
- else: message = "User " + user.name + " halted the world."
+ else: message = "User " + user.account.get("name") + " halted the world."
# let everyone know
muffmisc.broadcast(message)
# let the user know and log
user.send("Reloading all code modules, configs and data.")
- muffmisc.log("User " + user.name + " reloaded the world.")
+ muffmisc.log("User " + user.account.get("name") + " reloaded the world.")
# set a flag to reload
muffvars.reload_modules = True
"""Quit the world."""
user.state = "disconnecting"
-def command_time(user, command="", parameters=""):
- """Show the current world time in elapsed increments."""
- user.send(muffuniv.universe.internals["counters"].get("elapsed") + " increments elapsed since the world was created.")
-
def command_help(user, command="", parameters=""):
"""List available commands and provide help for commands."""
if parameters:
# is the command word one for which we have data?
- if parameters in muffuniv.universe.commands.keys():
+ if parameters in muffuniv.universe.categories["command"]:
# add a description if provided
- description = muffuniv.universe.commands[parameters].get("description")
+ description = muffuniv.universe.categories["command"][parameters].get("description")
if not description:
description = "(no short description provided)"
output = "$(grn)" + parameters + "$(nrm) - " + description + "$(eol)$(eol)"
# add the help text if provided
- help_text = muffuniv.universe.commands[parameters].get("help")
+ help_text = muffuniv.universe.categories["command"][parameters].get("help")
if not help_text:
help_text = "No help is provided for this command."
output += help_text
# give a sorted list of commands with descriptions if provided
output = "These are the commands available to you:$(eol)$(eol)"
- sorted_commands = muffuniv.universe.commands.keys()
+ sorted_commands = muffuniv.universe.categories["command"].keys()
sorted_commands.sort()
for item in sorted_commands:
- description = muffuniv.universe.commands[item].get("description")
+ description = muffuniv.universe.categories["command"][item].get("description")
if not description:
description = "(no short description provided)"
output += " $(grn)" + item + "$(nrm) - " + description + "$(eol)"
# a dictionary of punctuation:action pairs
actions = {}
- for option in muffconf.config_data.options("language"):
- if option.startswith("punctuation_"):
- action = option.split("_")[1]
- for mark in muffconf.config_data.get("language", option).split():
+ for facet in muffuniv.universe.categories["internal"]["language"].facets():
+ if facet.startswith("punctuation_"):
+ action = facet.split("_")[1]
+ for mark in muffuniv.universe.categories["internal"]["language"].get(facet).split():
actions[mark] = action
- # set the default action
- action = actions[muffconf.config_data.get("language", "default_punctuation")]
-
# match the punctuation used, if any, to an action
- default_punctuation = muffconf.config_data.get("language", "default_punctuation")
+ default_punctuation = muffuniv.universe.categories["internal"]["language"].get("default_punctuation")
+ action = actions[default_punctuation]
for mark in actions.keys():
if message.endswith(mark) and mark != default_punctuation:
action = actions[mark]
message += default_punctuation
# capitalize a list of words within the message
- capitalize = muffconf.get("language", "capitalize").split()
+ capitalize = muffuniv.universe.categories["internal"]["language"].get("capitalize").split()
for word in capitalize:
message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
# tell the room
# TODO: we won't be using broadcast once there are actual rooms
- muffmisc.broadcast(user.name + " " + action + "s, \"" + message + "\"")
+ muffmisc.broadcast(user.account.get("name") + " " + action + "s, \"" + message + "\"")
# there was no message
else:
keys = muffuniv.universe.contents.keys()
keys.sort()
for key in keys: message += "$(eol) $(grn)" + key + "$(nrm)"
+ elif parameters == "time":
+ message = muffuniv.universe.categories["internal"]["counters"].get("elapsed") + " increments elapsed since the world was created."
elif parameters: message = "I don't know what \"" + parameters + "\" is."
else: message = "What do you want to show?"
user.send(message)
for module in muff.__all__:
exec("import " + module)
-# see if the menupath can be retrieved from muffconf
-try:
- if muffconf.get("files", "menus"): pass
-
-# otherwise, reload muffconf
-except AttributeError:
- reload(muffconf)
-
-# now we can safely nab the menu path setting and build a list of data files
-menu_path = muffconf.get("files", "menus")
-menu_files_index = ConfigParser.SafeConfigParser()
-menu_files_index.read(menu_path + "/index")
-menu_files = []
-for each_file in menu_files_index.get("index", "files").split():
- menu_files.append(menu_path + "/" + each_file)
-
-# read the menu files
-menu_data = ConfigParser.SafeConfigParser()
-menu_data.read(menu_files)
-
def get_menu(state, error=None, echoing=True, choices={}):
"""Show the correct menu text to a user."""
def menu_echo_on(state):
"""True if echo is on, false if it is off."""
- try:
- return menu_data.getboolean(state, "echo")
- except:
- return True
+ return muffuniv.universe.categories["menu"][state].getboolean("echo", True)
def get_echo_sequence(state, echoing):
"""Build the appropriate IAC ECHO sequence as needed."""
def get_default_menu_choice(state):
"""Return the default choice for a menu."""
- try:
- return menu_data.get(state, "default")
- except:
- return ""
+ return muffuniv.universe.categories["menu"][state].get("default")
+
def get_formatted_default_menu_choice(state):
"""Default menu choice foratted for inclusion in a prompt string."""
default = get_default_menu_choice(state)
# try to get an error message matching the condition
# and current state
- try:
- return "$(red)" + menu_data.get(state, "error_" + error) + "$(nrm)$(eol)$(eol)"
-
- # otherwise, use a generic one
- except:
- return "$(red)That is not a valid choice...$(nrm)$(eol)$(eol)"
+ description = muffuniv.universe.categories["menu"][state].get("error_" + error)
+ if not description: description = "That is not a valid choice..."
+ description = "$(red)" + description + "$(nrm)"
# there was no error condition
else:
# try to get a menu description for the current state
- try:
- return menu_data.get(state, "description") + "$(eol)$(eol)"
+ description = muffuniv.universe.categories["menu"][state].get("description")
- # otherwise, leave it blank
- except:
- return ""
+ # return the description or error message
+ if description: description += "$(eol)$(eol)"
+ return description
def get_menu_prompt(state):
"""Try to get a prompt, if it was defined."""
- try:
- return menu_data.get(state, "prompt") + " "
- except:
- return ""
+ prompt = muffuniv.universe.categories["menu"][state].get("prompt")
+ if prompt: prompt += " "
+ return prompt
def get_menu_choices(user):
"""Return a dict of choice:meaning."""
choices = {}
- for option in menu_data.options(user.state):
- if option.startswith("choice_"):
- choices[option.split("_", 2)[1]] = menu_data.get(user.state, option)
- elif option.startswith("create_"):
- choices[option.split("_", 2)[1]] = eval(menu_data.get(user.state, option))
+ for facet in muffuniv.universe.categories["menu"][user.state].facets():
+ if facet.startswith("choice_"):
+ choices[facet.split("_", 2)[1]] = muffuniv.universe.categories["menu"][user.state].get(facet)
+ elif facet.startswith("create_"):
+ choices[facet.split("_", 2)[1]] = eval(muffuniv.universe.categories["menu"][user.state].get(facet))
return choices
def get_formatted_menu_choices(state, choices):
def get_menu_branches(state):
"""Return a dict of choice:branch."""
branches = {}
- try:
- for option in menu_data.options(state):
- if option.startswith("branch_"):
- branches[option.split("_", 2)[1]] = menu_data.get(state, option)
- except:
- pass
+ for facet in muffuniv.universe.categories["menu"][state].facets():
+ if facet.startswith("branch_"):
+ branches[facet.split("_", 2)[1]] = muffuniv.universe.categories["menu"][state].get(facet)
return branches
def get_default_branch(state):
"""Return the default branch."""
- try:
- return menu_data.get(state, "branch")
- except:
- return ""
+ return muffuniv.universe.categories["menu"][state].get("branch")
def get_choice_branch(user, choice):
"""Returns the new state matching the given choice."""
def get_menu_actions(state):
"""Return a dict of choice:branch."""
actions = {}
- try:
- for option in menu_data.options(state):
- if option.startswith("action_"):
- actions[option.split("_", 2)[1]] = menu_data.get(state, option)
- except:
- pass
+ for facet in muffuniv.universe.categories["menu"][state].facets():
+ if facet.startswith("action_"):
+ actions[facet.split("_", 2)[1]] = muffuniv.universe.categories["menu"][state].get(facet)
return actions
def get_default_action(state):
"""Return the default action."""
- try:
- return menu_data.get(state, "action")
- except:
- return ""
+ return muffuniv.universe.categories["menu"][state].get("action")
def get_choice_action(user, choice):
"""Run any indicated script for the given choice."""
# the user's account name
elif macro == "$(account)":
- text = string.replace(text, macro, user.name)
+ text = string.replace(text, macro, user.account.get("name"))
# third person subjective pronoun
elif macro == "$(tpsp)":
def check_time(frequency):
"""Check for a factor of the current increment count."""
if type(frequency) is str:
- frequency = muffconf.getint("time", frequency)
- return not muffuniv.universe.internals["counters"].getint("elapsed") % frequency
+ frequency = muffuniv.universe.categories["internal"]["time"].getint(frequency)
+ if not "counters" in muffuniv.universe.categories["internal"]:
+ muffuniv.Element("internal:counters", muffuniv.universe)
+ return not muffuniv.universe.categories["internal"]["counters"].getint("elapsed") % frequency
def on_pulse():
"""The things which should happen on each pulse, aside from reloads."""
# periodically save everything
if check_time("frequency_save"):
- for user in muffvars.userlist: user.save()
muffuniv.universe.save()
# pause for a configurable amount of time (decimal seconds)
- time.sleep(muffconf.getfloat("time", "increment"))
+ time.sleep(muffuniv.universe.categories["internal"]["time"].getfloat("increment"))
# increment the elapsed increment counter
- muffuniv.universe.internals["counters"].set("elapsed", muffuniv.universe.internals["counters"].getint("elapsed") + 1)
+ muffuniv.universe.categories["internal"]["counters"].set("elapsed", muffuniv.universe.categories["internal"]["counters"].getint("elapsed") + 1)
def reload_data():
"""Reload data into new persistent objects."""
# hack to load all modules in the muff package
import muff
+import muffuniv
for module in muff.__all__:
exec("import " + module)
newsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# bind the socket to to our desired server ipa and port
- newsocket.bind((muffconf.get("network", "host"), muffconf.getint("network", "port")))
+ newsocket.bind((muffuniv.universe.categories["internal"]["network"].get("host"), muffuniv.universe.categories["internal"]["network"].getint("port")))
# disable blocking so we can proceed whether or not we can send/receive
newsocket.setblocking(0)
--- /dev/null
+# Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
+# Licensed per terms in the LICENSE file distributed with this software.
+
+# persistent variables are stored in ini-style files supported by ConfigParser
+import ConfigParser
+
+# need to know the directory separator
+import os
+
+# hack to load all modules in the muff package
+import muff
+for module in muff.__all__:
+ exec("import " + module)
+
+class Element:
+ """An element of the universe."""
+ def __init__(self, key, universe, origin=""):
+ """Default values for the in-memory element variables."""
+ self.key = key
+ 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 universe.categories: self.category = "other"
+ universe.categories[self.category][self.subkey] = self
+ self.origin = origin
+ if not self.origin: self.origin = universe.default_origins[self.category]
+ if not self.origin.startswith(os.sep):
+ self.origin = os.getcwd() + os.sep + self.origin
+ universe.contents[self.key] = self
+ if not self.origin in universe.files:
+ DataFile(self.origin, universe)
+ if not universe.files[self.origin].data.has_section(self.key):
+ universe.files[self.origin].data.add_section(self.key)
+ def delete(self):
+ muffmisc.log("Deleting: " + self.key + ".")
+ universe.files[self.origin].data.remove_section(self.key)
+ del universe.categories[self.category][self.subkey]
+ del universe.contents[self.key]
+ del self
+ def facets(self):
+ """Return a list of facets for this element."""
+ return universe.files[self.origin].data.options(self.key)
+ def get(self, facet):
+ """Retrieve values."""
+ if universe.files[self.origin].data.has_option(self.key, facet):
+ return universe.files[self.origin].data.get(self.key, facet)
+ else:
+ return ""
+ def getboolean(self, facet, default=False):
+ """Retrieve values as boolean type."""
+ if universe.files[self.origin].data.has_option(self.key, facet):
+ return universe.files[self.origin].data.getboolean(self.key, facet)
+ else:
+ return default
+ def getint(self, facet):
+ """Convenience method to coerce return values as type int."""
+ value = self.get(facet)
+ if not value: value = 0
+ elif type(value) is str: value = value.rstrip("L")
+ return int(value)
+ def getfloat(self, facet):
+ """Convenience method to coerce return values as type float."""
+ value = self.get(facet)
+ if not value: value = 0
+ elif type(value) is str: value = value.rstrip("L")
+ return float(value)
+ def set(self, facet, value):
+ """Set values."""
+ if not type(value) is str: value = repr(value)
+ universe.files[self.origin].data.set(self.key, facet, value)
+
+class DataFile:
+ """A file containing universe elements."""
+ def __init__(self, filename, universe):
+ filedir = os.sep.join(filename.split(os.sep)[:-1])
+ self.data = ConfigParser.SafeConfigParser()
+ if os.access(filename, os.R_OK): self.data.read(filename)
+ self.filename = filename
+ universe.files[filename] = self
+ if "categories" in self.data.sections():
+ for option in self.data.options("categories"):
+ universe.default_origins[option] = self.data.get("categories", option)
+ if not option in universe.categories:
+ universe.categories[option] = {}
+ for section in self.data.sections():
+ if section == "categories" or section == "include":
+ for option in self.data.options(section):
+ includefile = self.data.get(section, option)
+ if not includefile.startswith(os.sep):
+ includefile = filedir + os.sep + includefile
+ DataFile(includefile, universe)
+ elif section != "control":
+ Element(section, universe, filename)
+ def save(self):
+ if self.data.sections() and not ( "control" in self.data.sections() and self.data.getboolean("control", "read_only") ):
+ basedir = os.sep.join(self.filename.split(os.sep)[:-1])
+ if not os.access(basedir, os.F_OK): os.makedirs(basedir)
+ file_descriptor = file(self.filename, "w")
+ self.data.write(file_descriptor)
+ file_descriptor.flush()
+ file_descriptor.close()
+
+class Universe:
+ """The universe."""
+ def __init__(self, filename=""):
+ """Initialize the universe."""
+ self.categories = {}
+ self.contents = {}
+ self.default_origins = {}
+ self.files = {}
+ if not filename:
+ possible_filenames = [
+ ".mudpyrc",
+ ".mudpy/mudpyrc",
+ ".mudpy/mudpy.conf",
+ "mudpy.conf",
+ "etc/mudpy.conf",
+ "/usr/local/mudpy/mudpy.conf",
+ "/usr/local/mudpy/etc/mudpy.conf",
+ "/etc/mudpy/mudpy.conf",
+ "/etc/mudpy.conf"
+ ]
+ for filename in possible_filenames:
+ if os.access(filename, os.R_OK): break
+ if not filename.startswith(os.sep):
+ filename = os.getcwd() + os.sep + filename
+ DataFile(filename, self)
+ def save(self):
+ """Save the universe to persistent storage."""
+ for key in self.files: self.files[key].save()
+
+# if there is no universe, create an empty one
+if not "universe" in locals(): universe = Universe()
+
def __init__(self):
"""Default values for the in-memory user variables."""
-
- # the account name
- self.name = ""
-
- # the password hash
- self.passhash = ""
-
- # the current client ip address
self.address = ""
-
- # the previous client ip address
self.last_address = ""
-
- # the current socket connection object
self.connection = None
-
- # a flag to denote whether the user is authenticated
self.authenticated = False
-
- # number of times password entry has failed during this session
self.password_tries = 1
-
- # the current state of the user
self.state = "entering_account_name"
-
- # flag to indicate whether a menu has been displayed
self.menu_seen = False
-
- # current error condition, if any
self.error = ""
-
- # fifo-style queue for lines of user input
self.input_queue = []
-
- # fifo-style queue for blocks of user output
self.output_queue = []
-
- # holding pen for unterminated user input
self.partial_input = ""
-
- # flag to indicate the current echo status of the client
self.echoing = True
-
- # the active avatar
self.avatar = None
-
- # an object containing persistent account data
- self.record = ConfigParser.SafeConfigParser()
+ self.account = None
def quit(self):
"""Log, save, close the connection and remove."""
- if self.name: message = "User " + self.name
+ name = self.account.get("name")
+ if name: message = "User " + name
else: message = "An unnamed user"
message += " logged out."
muffmisc.log(message)
- self.save()
self.connection.close()
self.remove()
new_user = muffuser.User()
# give it the same name
- new_user.name = self.name
-
- # load from file
- new_user.load()
+ new_user.account.set("name", self.account.get("name"))
# set everything else equivalent
new_user.address = self.address
for old_user in muffvars.userlist:
# the name is the same but it's not us
- if old_user.name == self.name and old_user is not self:
+ if old_user.account.get("name") == self.account.get("name") and old_user is not self:
# make a note of it
- muffmisc.log("User " + self.name + " reconnected--closing old connection to " + old_user.address + ".")
+ muffmisc.log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".")
old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
def authenticate(self):
"""Flag the user as authenticated and disconnect duplicates."""
if not self.state is "authenticated":
- muffmisc.log("User " + self.name + " logged in.")
+ muffmisc.log("User " + self.account.get("name") + " logged in.")
self.authenticated = True
- def load(self):
- """Retrieve account data from cold storage."""
-
- # what the filename for the user account should be
- filename = muffconf.get("files", "accounts") + "/" + self.name
-
- # try to load the password hash and last connection ipa
- try:
- self.record.read(filename)
- self.passhash = self.record.get("account", "passhash")
- self.last_address = self.record.get("account", "last_address", self.address)
-
- # if we can't, that's okay too
- except:
- pass
-
- def get_passhash(self):
- """Retrieve the user's account password hash from storage."""
-
- # what the filename for the user account could be
- filename = muffconf.get("files", "accounts") + "/" + self.proposed_name
-
- # create a temporary account record object
- temporary_record = ConfigParser.SafeConfigParser()
-
- # try to load the indicated account and get a password hash
- try:
- temporary_record.read(filename)
- self.passhash = temporary_record.get("account", "passhash")
- return True
-
- # otherwise, the password hash is empty
- except:
- self.passhash = ""
- return False
-
- def save(self):
- """Record account data to cold storage."""
-
- # the user account must be authenticated to save
- if self.authenticated:
-
- # create an account section if it doesn't exist
- if not self.record.has_section("account"):
- self.record.add_section("account")
-
- # write some in-memory data to the record
- self.record.set("account", "name", self.name)
- self.record.set("account", "passhash", self.passhash)
- self.record.set("account", "last_address", self.address)
-
- # the account files live here
- account_path = muffconf.get("files", "accounts")
- # the filename to which we'll write
- filename = account_path + "/" + self.name.lower()
-
- # open the user account file for writing
- try:
- record_file = file(filename, "w")
-
- # if the directory doesn't exist, create it first
- except IOError:
- os.makedirs(account_path)
- record_file = file(filename, "w")
-
- # dump the account data to it
- self.record.write(record_file)
-
- # close the user account file
- record_file.flush()
- record_file.close()
-
- # set the permissions to 0600
- os.chmod(filename, 0600)
-
def show_menu(self):
"""Send the user their current menu."""
if not self.menu_seen:
def new_avatar(self):
"""Instantiate a new, unconfigured avatar for this user."""
- try:
- counter = muffuniv.universe.internals["counters"].getint("next_actor")
- except:
- muffmisc.log("get next_actor failed")
- counter = 1
- while muffuniv.element_exists("actor:" + repr(counter)): counter += 1
- muffuniv.universe.internals["counters"].set("next_actor", counter + 1)
- self.avatar = muffuniv.Element("actor:" + repr(counter), muffconf.get("files", "avatars"), muffuniv.universe)
- try:
- avatars = self.record.get("account", "avatars").split()
- except:
- avatars = []
+ counter = muffuniv.universe.categories["internal"]["counters"].getint("next_avatar")
+ while "avatar:" + repr(counter + 1) in muffuniv.universe.categories["actor"].keys(): counter += 1
+ muffuniv.universe.categories["internal"]["counters"].set("next_avatar", counter + 1)
+ self.avatar = muffuniv.Element("actor:avatar:" + repr(counter), muffuniv.universe)
+ avatars = self.account.get("avatars").split()
avatars.append(self.avatar.key)
- self.record.set("account", "avatars", " ".join(avatars))
+ self.account.set("avatars", " ".join(avatars))
def list_avatar_names(self):
"""A test function to list names of assigned avatars."""
try:
- avatars = self.record.get("account", "avatars").split()
+ avatars = self.account.get("avatars").split()
except:
avatars = []
avatar_names = []