From: Jeremy Stanley Date: Sun, 28 Aug 2005 18:36:46 +0000 (+0000) Subject: Imported from archive. X-Git-Tag: 0.0.1~339 X-Git-Url: https://mudpy.org/gitweb?a=commitdiff_plain;h=fecd4c0fc49593052697b8cf199603cf1fac2b61;p=mudpy.git Imported from archive. * 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. --- diff --git a/data/commands b/command similarity index 82% rename from data/commands rename to command index 7ce62d0..c1447bd 100644 --- a/data/commands +++ 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 index 1abf7f5..0000000 --- a/data/universe/index +++ /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 index b71299c..0000000 --- a/data/universe/rooms +++ /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 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 index 6402fab..0000000 --- a/lib/menus/account_creation +++ /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 index d5fb932..0000000 --- a/lib/menus/index +++ /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 index 831328b..0000000 --- a/lib/menus/login +++ /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 index 8d36615..0000000 --- a/lib/muff/muffconf.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Configuration objects for the MUFF Engine""" - -# Copyright (c) 2005 mudpy, Jeremy Stanley , 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 index 9fd9850..0000000 --- a/lib/muff/muffuniv.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) 2005 mudpy, Jeremy Stanley , 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() - diff --git a/lib/menus/miscellaneous b/menu similarity index 52% rename from lib/menus/miscellaneous rename to menu index ef18de5..2d4b3aa 100644 --- a/lib/menus/miscellaneous +++ 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 diff --git a/mudpy.conf b/mudpy.conf index 81e6c9b..3dfcdeb 100644 --- a/mudpy.conf +++ b/mudpy.conf @@ -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 diff --git a/lib/muff/__init__.py b/muff/__init__.py similarity index 72% rename from lib/muff/__init__.py rename to muff/__init__.py index c4c88f2..f638cbd 100644 --- a/lib/muff/__init__.py +++ b/muff/__init__.py @@ -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" ] diff --git a/lib/muff/muffcmds.py b/muff/muffcmds.py similarity index 79% rename from lib/muff/muffcmds.py rename to muff/muffcmds.py index 8f1562c..da061fa 100644 --- a/lib/muff/muffcmds.py +++ b/muff/muffcmds.py @@ -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) diff --git a/lib/muff/muffmain.py b/muff/muffmain.py similarity index 100% rename from lib/muff/muffmain.py rename to muff/muffmain.py diff --git a/lib/muff/muffmenu.py b/muff/muffmenu.py similarity index 68% rename from lib/muff/muffmenu.py rename to muff/muffmenu.py index daf6512..c9023a2 100644 --- a/lib/muff/muffmenu.py +++ b/muff/muffmenu.py @@ -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.""" diff --git a/lib/muff/muffmisc.py b/muff/muffmisc.py similarity index 92% rename from lib/muff/muffmisc.py rename to muff/muffmisc.py index f7218e6..cca2218 100644 --- a/lib/muff/muffmisc.py +++ b/muff/muffmisc.py @@ -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.""" diff --git a/lib/muff/muffsock.py b/muff/muffsock.py similarity index 91% rename from lib/muff/muffsock.py rename to muff/muffsock.py index fe47061..a3b18ab 100644 --- a/lib/muff/muffsock.py +++ b/muff/muffsock.py @@ -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 index 0000000..bd06099 --- /dev/null +++ b/muff/muffuniv.py @@ -0,0 +1,136 @@ +# Copyright (c) 2005 mudpy, Jeremy Stanley , 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() + diff --git a/lib/muff/muffuser.py b/muff/muffuser.py similarity index 64% rename from lib/muff/muffuser.py rename to muff/muffuser.py index 75e6459..1ac0166 100644 --- a/lib/muff/muffuser.py +++ b/muff/muffuser.py @@ -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 = [] diff --git a/lib/muff/muffvars.py b/muff/muffvars.py similarity index 100% rename from lib/muff/muffvars.py rename to muff/muffvars.py