Imported from archive.
authorJeremy Stanley <fungi@yuggoth.org>
Sun, 28 Aug 2005 18:36:46 +0000 (18:36 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Sun, 28 Aug 2005 18:36:46 +0000 (18:36 +0000)
* data/commands: Renamed to command and moved into the top-level
directory.

* data/universe, lib/index: Removed this sample data.

* lib/menus: Collapsed the contents of the directory into a menu
file within the top-level directory.

* lib/muff: Moved into the top-level directory.

* lib/muff, mudpy.py: Assorted minor code readability fixes and
additional comments. Replaced several more vague try/except
constructs with more specific if/then/else checks.

* lib/muff/muffcmds.py (command_time, command_show): Changed the
admin command time to be a parameter to the admin command show
instead.
(handler_checking_new_account_name, handler_entering_new_password)
(handler_verifying_new_password): Adjusted the fix for stray empty
accounts to create them upon entry but delete them again if no
password is successfully added, avoiding a potential race and
exception where two sockets start to create a user with the same
name.
(handler_entering_account_name): Disallow non-alphanumeric
characters in account names.

* lib/muff/muffconf.py: Replaced with object methods in class
definitions within muffuniv.py.

* mud.py: Renamed to mudpy matching, the executable to the project
name.

21 files changed:
command [moved from data/commands with 82% similarity]
data/universe/index [deleted file]
data/universe/rooms [deleted file]
lib/index [deleted file]
lib/menus/account_creation [deleted file]
lib/menus/index [deleted file]
lib/menus/login [deleted file]
lib/muff/muffconf.py [deleted file]
lib/muff/muffuniv.py [deleted file]
menu [moved from lib/menus/miscellaneous with 52% similarity]
mudpy [moved from mud.py with 100% similarity]
mudpy.conf
muff/__init__.py [moved from lib/muff/__init__.py with 72% similarity]
muff/muffcmds.py [moved from lib/muff/muffcmds.py with 79% similarity]
muff/muffmain.py [moved from lib/muff/muffmain.py with 100% similarity]
muff/muffmenu.py [moved from lib/muff/muffmenu.py with 68% similarity]
muff/muffmisc.py [moved from lib/muff/muffmisc.py with 92% similarity]
muff/muffsock.py [moved from lib/muff/muffsock.py with 91% similarity]
muff/muffuniv.py [new file with mode: 0644]
muff/muffuser.py [moved from lib/muff/muffuser.py with 64% similarity]
muff/muffvars.py [moved from lib/muff/muffvars.py with 100% similarity]

similarity index 82%
rename from data/commands
rename to command
index 7ce62d0..c1447bd 100644 (file)
+++ b/command
@@ -13,15 +13,10 @@ action = command_help(user, command, parameters)
 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)
diff --git a/data/universe/index b/data/universe/index
deleted file mode 100644 (file)
index 1abf7f5..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-[include]
-actors = actors
-avatars = avatars
-rooms = rooms
-
diff --git a/data/universe/rooms b/data/universe/rooms
deleted file mode 100644 (file)
index b71299c..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-[room:1,0,0,0]
-
-[room:0,0,0,0]
-
-[room:-1,0,0,0]
-
diff --git a/lib/index b/lib/index
deleted file mode 100644 (file)
index e26c2d4..0000000
--- a/lib/index
+++ /dev/null
@@ -1,3 +0,0 @@
-[include]
-universe = universe/index
-
diff --git a/lib/menus/account_creation b/lib/menus/account_creation
deleted file mode 100644 (file)
index 6402fab..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-[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
-
diff --git a/lib/menus/index b/lib/menus/index
deleted file mode 100644 (file)
index d5fb932..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-[index]
-files = account_creation login miscellaneous
-
diff --git a/lib/menus/login b/lib/menus/login
deleted file mode 100644 (file)
index 831328b..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-[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)
-
diff --git a/lib/muff/muffconf.py b/lib/muff/muffconf.py
deleted file mode 100644 (file)
index 8d36615..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-"""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))
-
diff --git a/lib/muff/muffuniv.py b/lib/muff/muffuniv.py
deleted file mode 100644 (file)
index 9fd9850..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-# 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()
-
similarity index 52%
rename from lib/menus/miscellaneous
rename to menu
index ef18de5..2d4b3aa 100644 (file)
+++ b/menu
@@ -1,44 +1,74 @@
-[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
 
diff --git a/mud.py b/mudpy
similarity index 100%
rename from mud.py
rename to mudpy
index 81e6c9b..3dfcdeb 100644 (file)
@@ -1,16 +1,25 @@
+[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 = ?
@@ -19,11 +28,11 @@ punctuation_exclaim = !
 punctuation_muse = ...
 punctuation_say = .
 
-[network]
+[internal:network]
 host = 
 port = 6669
 
-[time]
+[internal:time]
 definition_d = 24h
 definition_h = 60mi
 definition_mi = 10r
similarity index 72%
rename from lib/muff/__init__.py
rename to muff/__init__.py
index c4c88f2..f638cbd 100644 (file)
@@ -5,5 +5,5 @@
 
 # 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" ]
 
similarity index 79%
rename from lib/muff/muffcmds.py
rename to muff/muffcmds.py
index 8f1562c..da061fa 100644 (file)
@@ -68,18 +68,22 @@ def handler_entering_account_name(user):
        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
@@ -93,18 +97,15 @@ def handler_checking_password(user):
        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"
 
@@ -129,10 +130,12 @@ def handler_checking_new_account_name(user):
 
        # 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
@@ -154,17 +157,18 @@ def handler_entering_new_password(user):
        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):
@@ -174,16 +178,15 @@ 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"
@@ -191,6 +194,7 @@ def handler_verifying_new_password(user):
        # otherwise, sayonara
        else:
                user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
+               user.account.delete()
                user.state = "disconnecting"
 
 def handler_active(user):
@@ -200,23 +204,18 @@ 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)
@@ -226,7 +225,7 @@ def command_halt(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)
@@ -240,7 +239,7 @@ def command_reload(user, command="", parameters=""):
 
        # 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
@@ -249,10 +248,6 @@ def command_quit(user, command="", parameters=""):
        """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."""
 
@@ -260,16 +255,16 @@ def command_help(user, command="", parameters=""):
        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
@@ -283,10 +278,10 @@ def command_help(user, command="", parameters=""):
 
                # 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)"
@@ -311,17 +306,15 @@ def command_say(user, command="", parameters=""):
 
                # 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]
@@ -332,13 +325,13 @@ def command_say(user, command="", parameters=""):
                        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:
@@ -361,6 +354,8 @@ def command_show(user, command="", parameters=""):
                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)
similarity index 100%
rename from lib/muff/muffmain.py
rename to muff/muffmain.py
similarity index 68%
rename from lib/muff/muffmenu.py
rename to muff/muffmenu.py
index daf6512..c9023a2 100644 (file)
@@ -11,26 +11,6 @@ import muff
 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."""
 
@@ -57,10 +37,7 @@ def get_menu(state, error=None, echoing=True, choices={}):
 
 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."""
@@ -83,10 +60,8 @@ def get_echo_message(state):
 
 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)
@@ -101,39 +76,34 @@ def get_menu_description(state, error):
 
                # 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):
@@ -149,20 +119,14 @@ 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."""
@@ -175,20 +139,14 @@ def get_choice_branch(user, 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."""
similarity index 92%
rename from lib/muff/muffmisc.py
rename to muff/muffmisc.py
index f7218e6..cca2218 100644 (file)
@@ -152,7 +152,7 @@ def replace_macros(user, text, is_input=False):
 
                # 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)":
@@ -195,8 +195,10 @@ def replace_macros(user, text, is_input=False):
 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."""
@@ -217,14 +219,13 @@ def on_pulse():
 
        # 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."""
similarity index 91%
rename from lib/muff/muffsock.py
rename to muff/muffsock.py
index fe47061..a3b18ab 100644 (file)
@@ -8,6 +8,7 @@ import socket
 
 # hack to load all modules in the muff package
 import muff
+import muffuniv
 for module in muff.__all__:
        exec("import " + module)
 
@@ -50,7 +51,7 @@ def initialize_server_socket():
        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)
diff --git a/muff/muffuniv.py b/muff/muffuniv.py
new file mode 100644 (file)
index 0000000..bd06099
--- /dev/null
@@ -0,0 +1,136 @@
+# 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()
+
similarity index 64%
rename from lib/muff/muffuser.py
rename to muff/muffuser.py
index 75e6459..1ac0166 100644 (file)
@@ -22,62 +22,28 @@ class User:
 
        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()
 
@@ -100,10 +66,7 @@ class User:
                        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
@@ -135,10 +98,10 @@ class User:
                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)")
 
@@ -163,84 +126,9 @@ class User:
        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:
@@ -358,25 +246,18 @@ class User:
 
        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 = []
similarity index 100%
rename from lib/muff/muffvars.py
rename to muff/muffvars.py