From: Jeremy Stanley Date: Thu, 21 Jul 2005 20:19:02 +0000 (+0000) Subject: Imported from archive. X-Git-Tag: 0.0.1~344 X-Git-Url: https://mudpy.org/gitweb?a=commitdiff_plain;h=0f39af78818acbbee0b99145ff5ff303553027c6;p=mudpy.git Imported from archive. * bin/muff, lib/muff: Replaced the embedded license copies with pointers to the LICENSE file. Added lots of in-line code commentary and docstrings. Refactored some blocks of code for improved readability. * bin/muff, etc/mudpy.conf: Moved to the top-level directory. * doc/license: Renamed to LICENSE and moved it to the top-level directory. * lib/muff/muffcmds.py (command_null): Removed. (command_say): Additional output cleanup. * lib/muff/muffmain.py (main): More robust handling for variable files. * lib/muff/muffmisc.py (wrap_ansi_text): Minor word-wrapping fix. (User.__init__): Added tracking of last login address. --- diff --git a/doc/license b/LICENSE similarity index 93% rename from doc/license rename to LICENSE index 09499ea..da974d5 100644 --- a/doc/license +++ b/LICENSE @@ -1,5 +1,4 @@ -Copyright (c) 2005 mudpy, Jeremy Stanley -All rights reserved. +Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/bin/muff b/bin/muff deleted file mode 100755 index 64aab9d..0000000 --- a/bin/muff +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/python -"""Skeletal executable for the MUFF Engine""" - -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import ConfigParser -import sys - -def get_main_loop(): - """Find and return the main loop function""" - - # figure out where to find our python modules - config_data = ConfigParser.SafeConfigParser() - config_dirs = [".", "./etc", "/usr/local/muff", "/usr/local/muff/etc", "/etc/muff", "/etc" ] - config_name = "muff.conf" - config_files = [] - for each_dir in config_dirs: - config_files.append(each_dir + "/" + config_name) - config_data.read(config_files) - modules = config_data.get("general", "modules") - - # add it to the load path - sys.path.append(modules) - - # import the main loop function - from muff.muffmain import main - return main - -# load the main loop -main = get_main_loop() - -# run the main loop -main() - diff --git a/lib/muff/__init__.py b/lib/muff/__init__.py index 27df656..318e7a8 100644 --- a/lib/muff/__init__.py +++ b/lib/muff/__init__.py @@ -1,31 +1,9 @@ """Initialization for MUFF Modules""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. +# Licensed per terms in the LICENSE file distributed with this software. +# 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", "muffuser", "muffvars" ] diff --git a/lib/muff/muffcmds.py b/lib/muff/muffcmds.py index a9df032..555526a 100644 --- a/lib/muff/muffcmds.py +++ b/lib/muff/muffcmds.py @@ -1,130 +1,207 @@ """Command objects for the MUFF Engine""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. +# Licensed per terms in the LICENSE file distributed with this software. +# command data like descriptions, help text, limits, et cetera, are stored in +# ini-style configuration files supported by the ConfigParser module import ConfigParser + +# md5 hashing is used for verification of account passwords import md5 + +# the os module is used to we can get a directory listing and build lists of +# multiple config files in one directory import os + +# the random module is useful for creating random conditional output import random + +# string.split is used extensively to tokenize user input (break up command +# names and parameters) import string +# bit of a hack to load all modules in the muff package import muff for module in muff.__all__: exec("import " + module) +# does the files:commands setting exist yet? try: - if muffconf.config_data.get("general", "command_path"): - pass + if muffconf.config_data.get("files", "commands"): pass + +# if not, reload the muffconf module except AttributeError: reload(muffconf) -command_path = muffconf.config_data.get("general", "command_path") + +# now we can safely nab the command path setting and build a list of data files +command_path = muffconf.config_data.get("files", "commands") command_files = [] for each_file in os.listdir(command_path): command_files.append(command_path + "/" + each_file) + +# read the command data files command_data = ConfigParser.SafeConfigParser() command_data.read(command_files) + +# this creates a list of commands mentioned in the data files command_list = command_data.sections() def handle_user_input(user, input): + """The main handler, branches to a state-specific handler.""" + + # TODO: change this to use a dict if user.state == "active": handler_active(user, input) elif user.state == "entering account name": handler_entering_account_name(user, input) elif user.state == "checking password": handler_checking_password(user, input) elif user.state == "checking new account name": handler_checking_new_account_name(user, input) elif user.state == "entering new password": handler_entering_new_password(user, input) elif user.state == "verifying new password": handler_verifying_new_password(user, input) + + # if there's input with an unknown user state, something is wrong else: handler_fallthrough(user, input) + + # since we got input, flag that the menu/prompt needs to be redisplayed user.menu_seen = False def handler_entering_account_name(user, input): + """Handle the login account name.""" + + # did the user enter anything? if input: + + # keep only the first word and convert to lower-case user.proposed_name = string.split(input)[0].lower() + + # try to get a password hash for the proposed name user.get_passhash() + + # if we have a password hash, time to request a password + # TODO: make get_passhash() return pass/fail and test that if user.passhash: user.state = "checking password" + + # otherwise, this could be a brand new user else: user.name = user.proposed_name user.proposed_name = None user.load() user.state = "checking new account name" + + # if the user entered nothing for a name, then buhbye + # TODO: make a disconnect state instead of calling command_quit() else: command_quit(user) def handler_checking_password(user, input): + """Handle the login account password.""" + + # does the hashed input equal the stored hash? if md5.new(user.proposed_name + input).hexdigest() == user.passhash: + + # if so, set the username and load from cold storage user.name = user.proposed_name - user.proposed_name = None + del(user.proposed_name) user.load() + + # now go active + # TODO: branch to character creation and selection menus user.state = "active" + + # if at first your hashes don't match, try, try again elif user.password_tries < muffconf.config_data.getint("general", "password_tries"): user.password_tries += 1 user.error = "incorrect" + + # we've exceeded the maximum number of password failures, so disconnect + # TODO: make a disconnect state instead of calling command_quit() else: user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)") command_quit(user) def handler_checking_new_account_name(user, input): + """Handle input for the new user menu.""" + + # if there's input, take the first character and lowercase it if input: choice = input.lower()[0] + + # if there's no input, use the default else: choice = muffmenu.get_default(user) + + # user selected to disconnect + # TODO: make a disconnect state instead of calling command_quit() if choice == "d": command_quit(user) + + # go back to the login screen elif choice == "g": user.state = "entering account name" + + # new user, so ask for a password elif choice == "n": user.state = "entering new password" + + # user entered a non-existent option else: user.error = "default" def handler_entering_new_password(user, input): + """Handle a new password entry.""" + + # make sure the password is strong--at least one upper, one lower and + # one digit, seven or more characters in length if len(input) > 6 and len(filter(lambda x: x>="0" and x<="9", input)) and len(filter(lambda x: x>="A" and x<="Z", input)) and len(filter(lambda x: x>="a" and x<="z", input)): + + # hash and store it, then move on to verification user.passhash = md5.new(user.name + input).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.config_data.getint("general", "password_tries"): user.password_tries += 1 user.error = "weak" + + # too many tries, so adios + # TODO: make a disconnect state instead of calling command_quit() else: user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)") command_quit(user) def handler_verifying_new_password(user, input): + """Handle the re-entered new password for verification.""" + + # hash the input and match it to storage if md5.new(user.name + input).hexdigest() == user.passhash: + + # the hashes matched, so go active + # TODO: branch to character creation and selection menus user.state = "active" + + # go back to entering the new password as long as you haven't tried + # too many times elif user.password_tries < muffconf.config_data.getint("general", "password_tries"): user.password_tries += 1 user.error = "differs" user.state = "entering new password" + + # otherwise, sayonara + # TODO: make a disconnect state instead of calling command_quit() else: user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)") command_quit(user) +# TODO: um, input is a reserved word. better replace it in all sources with +# something else, like input_data def handler_active(user, input): + """Handle input for active users.""" + + # users reaching this stage should be considered authenticated + # TODO: this should actually happen before or in load() instead if not user.authenticated: user.authenticated = True + + # split out the command (first word) and parameters (everything else) try: inputlist = string.split(input, None, 1) command = inputlist[0] @@ -134,50 +211,90 @@ def handler_active(user, input): parameters = inputlist[1] except: parameters = "" + del(inputlist) + + # lowercase the command command = command.lower() - if not command: command_null(user, command, parameters) - elif command in command_list: exec("command_" + command + "(user, command, parameters)") - else: command_error(user, command, parameters) + # the command matches a command word for which we have data + if command in command_list: exec("command_" + command + "(user, command, parameters)") + + # no data matching the entered command word + elif command: command_error(user, command, parameters) + +# TODO: need a log function to handle conditions like this instead of print() def handler_fallthrough(user, input): + """Input received in an unknown user state.""" if input: print("User \"" + user + "\" entered \"" + input + "\" while in unknown state \"" + user.state + "\".") def command_halt(user, command="", parameters=""): + """Halt the world.""" + + # let everyone know + # TODO: optionally take input for the message muffmisc.broadcast(user.name + " halts the world.") + + # save everyone + # TODO: probably want a misc function for this for each_user in muffvars.userlist: each_user.save() + + # set a flag to terminate the world muffvars.terminate_world = True def command_reload(user, command="", parameters=""): - user.send("Reloading all code modules.") + """Reload all code modules, configs and data.""" + + # let the user know + user.send("Reloading all code modules, configs and data.") + + # set a flag to reload muffvars.reload_modules = True def command_quit(user, command="", parameters=""): + """Quit the world.""" + + # save to cold storage user.save() + + # close the connection user.connection.close() + + # remove from the list user.remove() def command_help(user, command="", parameters=""): + """List available commands and provide help for commands.""" + + # did the user ask for help on a specific command word? if parameters: + + # is the command word one for which we have data? if parameters in command_list: - if command_data.has_section(parameters): - try: - description = command_data.get(parameters, "description") - except: - description = "(no short description provided)" - output = "$(grn)" + parameters + "$(nrm) - " + command_data.get(parameters, "description") + "$(eol)$(eol)" - try: - help_text = command_data.get(parameters, "help") - except: - help_text = "No help is provided for this command." - output += help_text - else: - output = "There is no information on that command." + + # add a description if provided + try: + description = command_data.get(parameters, "description") + except: + description = "(no short description provided)" + output = "$(grn)" + parameters + "$(nrm) - " + command_data.get(parameters, "description") + "$(eol)$(eol)" + + # add the help text if provided + try: + help_text = command_data.get(parameters, "help") + except: + help_text = "No help is provided for this command." + output += help_text + + # no data for the requested command word else: output = "That is not an available command." - + + # no specific command word was indicated else: + + # give a sorted list of commands with descriptions if provided output = "These are the commands available to you:$(eol)$(eol)" sorted_commands = command_list sorted_commands.sort() @@ -188,39 +305,71 @@ def command_help(user, command="", parameters=""): description = "(no short description provided)" output += " $(grn)" + item + "$(nrm) - " + command_data.get(item, "description") + "$(eol)" output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"." + + # send the accumulated output to the user user.send(output) def command_say(user, command="", parameters=""): - message = parameters.strip("\"'`").capitalize() + """Speak to others in the same room.""" + + # the user entered a message if parameters: + + # get rid of quote marks on the ends of the message and + # capitalize the first letter + message = parameters.strip("\"'`").capitalize() + + # exclaim because the message ended in an exclamation mark + # TODO: use the ends() function instead of an index throughout if message[-1] == "!": action = "exclaim" + + # begin because the message ended in miscellaneous punctuation elif message[-1] in [ ",", "-", ":", ";" ]: action = "begin" + + # muse because the message ended in an ellipsis elif message[-3:] == "...": action = "muse" + + # ask because the message ended in a question mark elif message[-1] == "?": action = "ask" + + # say because the message ended in a singular period + # TODO: entering one period results in a double-period--oops! else: action = "say" message += "." + + # capitalize a list of words within the message + # TODO: move this list to the config capitalization = [ "i", "i'd", "i'll" ] for word in capitalization: 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 + "\"") + + # there was no message else: user.send("What do you want to say?") -def command_null(user, command="", parameters=""): - pass - def command_error(user, command="", parameters=""): + """Generic error for an unrecognized command word.""" + + # 90% of the time use a generic error if random.random() > 0.1: message = "I'm not sure what \"" + command if parameters: message += " " + parameters message += "\" means..." + + # 10% of the time use the classic diku error else: message = "Arglebargle, glop-glyf!?!" + + # send the error message user.send(message) diff --git a/lib/muff/muffconf.py b/lib/muff/muffconf.py index 44c344c..67bdf1b 100644 --- a/lib/muff/muffconf.py +++ b/lib/muff/muffconf.py @@ -1,45 +1,28 @@ """Configuration objects for the MUFF Engine""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - +# 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 -import string +# hack to load all modules in teh muff package import muff for module in muff.__all__: exec("import " + module) -config_data = ConfigParser.SafeConfigParser() +# 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 = "muff.conf" + +# build a list of possible config files config_files = [] for each_dir in config_dirs: config_files.append(each_dir + "/" + config_name) + +# read the config +config_data = ConfigParser.SafeConfigParser() config_data.read(config_files) diff --git a/lib/muff/muffmain.py b/lib/muff/muffmain.py index 0b6707e..9e6c0b4 100644 --- a/lib/muff/muffmain.py +++ b/lib/muff/muffmain.py @@ -1,69 +1,102 @@ """Main loop for the MUFF Engine""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. +# Licensed per terms in the LICENSE file distributed with this software. +# string.strip is used to clean up leading/trailing whitespace in user input import string + +# time.sleep is used in the loop to save cpu and provide crude pulse timing import time +# hack to load all modules in the muff package import muff for module in muff.__all__: exec("import " + module) def main(): + """The main loop.""" + + # loop indefinitely while the world is not flagged for termination while not muffvars.terminate_world: - if not muffvars.newsocket: - muffsock.initialize_server_socket() + + # open the listening socket if it hasn't been already + if not muffvars.newsocket: muffsock.initialize_server_socket() + + # pause for a configurable amount of time (decimal seconds) time.sleep(muffconf.config_data.getfloat("general", "increment")) + + # the world was flagged for a reload of all code/data if muffvars.reload_modules: + + # reload the muff package reload(muff) + + # reload all modules listed in the muff package for module in muff.__all__: exec("reload(muff." + module + ")") - muffvars.reload_modules = 0 + + # reset the reload flag + muffvars.reload_modules = False + + # assign a user if a new connection is waiting user = muffsock.check_for_connection(muffvars.newsocket) + + # there was a new connection if user: + + # welcome to the user list muffvars.userlist.append(user) + + # make a note of it + # TODO: need to log this crap print len(muffvars.userlist),"connection(s)" + + # iterate over the connected users for each_user in muffvars.userlist: + + # show the user a menu as needed each_user.show_menu() - input_data = "" + + # check for some input + # TODO: make a separate function for this try: input_data = each_user.connection.recv(1024) except: - pass + input_data = "" + # we got something if input_data: + + # tack this on to any previous partial input each_user.partial_input += input_data - if each_user.partial_input and each_user.partial_input[-1] == "\n": + + # the held input ends in a newline + if each_user.partial_input[-1] == "\n": + + # filter out non-printable characters each_user.partial_input = filter(lambda x: x>=' ' and x<='~', each_user.partial_input) + + # strip off leading/trailing whitespace each_user.partial_input = string.strip(each_user.partial_input) + + # move it to the end of the input queue each_user.input_queue.append(each_user.partial_input) + + # reset the held partial input each_user.partial_input = "" + + # pass the first item in the input + # queue to the main handler muffcmds.handle_user_input(each_user, each_user.input_queue[0]) + + # remove the first item from the queue each_user.input_queue.remove(each_user.input_queue[0]) + + # the loop has terminated, so tear down all sockets + # TODO: move the save from command_halt() to here muffsock.destroy_all_sockets() + + # log a final message + # TODO: need a logging function for this kind of stuff print "Shutting down now." diff --git a/lib/muff/muffmenu.py b/lib/muff/muffmenu.py index 0a9989b..b84f783 100644 --- a/lib/muff/muffmenu.py +++ b/lib/muff/muffmenu.py @@ -1,112 +1,156 @@ """Menu objects for the MUFF Engine""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. +# Licensed per terms in the LICENSE file distributed with this software. +# muff uses menu data stored in ini-style files supported by ConfigParser import ConfigParser + +# os.listdir is needed to get a file listing from a config directory import os + +# re.match is used to find menu options based on the choice_ prefix import re -import string +# hack to load all modules in the muff package import muff for module in muff.__all__: exec("import " + module) +# see if the menupath can be retrieved from muffconf try: - if muffconf.config_data.get("general", "menu_path"): - pass + if muffconf.config_data.get("files", "menus"): pass + +# otherwise, reload muffconf except AttributeError: reload(muffconf) +# build a list of files in the menus directory menu_files = [] -menu_path = muffconf.config_data.get("general", "menu_path") +menu_path = muffconf.config_data.get("files", "menus") for each_file in os.listdir(menu_path): menu_files.append(menu_path + "/" + each_file) + +# read the menu files menu_data = ConfigParser.SafeConfigParser() menu_data.read(menu_files) def get_default(user): + """Return the default choice for a menu.""" return menu_data.get(user.state, "default") def get_menu(user): + """Show the correct menu text to a user.""" + + # the menu hasn't been shown yet since the user's last input if not user.menu_seen: + + # echo is currently on for the user, so don't add an extra eol if user.echoing: spacer = "" + + # echo is currently off for the user, so add an extra eol else: spacer = "$(eol)" + + # if the user has echo on and the menu specifies it should be + # turned off, send: iac + will + echo + null try: if menu_data.get(user.state, "echo") == "off" and user.echoing: echo = chr(255) + chr(251) + chr(1) + chr(0) user.echoing = False else: echo = "" + + # if echo is not set to off in the menu and the user curently + # has echo off, send: iac + wont + echo + null except: if not user.echoing: echo = chr(255) + chr(252) + chr(1) + chr(0) user.echoing = True else: echo = "" + + # and error condition was raised by the handler if user.error: + + # try to get an error message matching the condition + # and current state try: description = "$(red)" + menu_data.get(user.state, "error_" + user.error) + "$(nrm)$(eol)$(eol)" + + # otherwise, use a generic one except: description = "$(red)That is not a valid choice...$(nrm)$(eol)$(eol)" + + # now clear the error condition user.error = "" + + # there was no error condition raised by the handler else: + + # try to get a menu description for the current state try: description = menu_data.get(user.state, "description") + "$(eol)$(eol)" + + # otherwise, leave it blank except: description = "" + + # try to get menu choices for the current state try: + + # build a dict of choice:meaning choices = {} for option in menu_data.options(user.state): if re.match("choice_", option): choices[option.split("_")[1]] = menu_data.get(user.state, option) + + # make a sorted list of choices choice_keys = choices.keys() choice_keys.sort() + + # concatenate them all into a list for display choicestring = "" for choice in choice_keys: choicestring += " [$(red)" + choice + "$(nrm)] " + choices[choice] + "$(eol)" + + # throw in an additional blank line after the choices, + # if there were any if choicestring: choicestring += "$(eol)" + + # there were no choices, so leave them blank except: choicestring = "" + + # try to get a prompt, if it was defined try: prompt = menu_data.get(user.state, "prompt") + " " + + # otherwise, leave it blank except: prompt = "" + + # throw in the default choice, if it exists try: default = "[$(red)" + menu_data.get(user.state, "default") + "$(nrm)] " + + # otherwise, leave it blank except: default = "" + + # echo is on, so don't display a message about it if user.echoing: echoing = "" + + # echo is off, so let the user know else: echoing = "(won't echo) " + + # assemble and send the various strings defined above user.send(echo + spacer + description + choicestring + prompt + default + echoing, "") + + # flag that the menu has now been displayed user.menu_seen = True diff --git a/lib/muff/muffmisc.py b/lib/muff/muffmisc.py index 6bfbe62..ba90981 100644 --- a/lib/muff/muffmisc.py +++ b/lib/muff/muffmisc.py @@ -1,63 +1,84 @@ """Miscellaneous objects for the MUFF Engine""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import string +# Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. +# Licensed per terms in the LICENSE file distributed with this software. +# hack to load all modules in the muff package import muff for module in muff.__all__: exec("import " + module) def broadcast(output): + """Send a message to all connected users.""" for each_user in muffvars.userlist: each_user.send(output) def wrap_ansi_text(text, width): - relative_position = 0 + """Wrap text with arbitrary width while ignoring ANSI colors.""" + + # the current position in the entire text string, including all + # characters, printable or otherwise absolute_position = 0 - escape = 0 + + # the current text position relative to the begining of the line, + # ignoring color escape sequences + relative_position = 0 + + # whether the current character is part of a color escape sequence + escape = False + + # iterate over each character from the begining of the text for each_character in text: + + # the current character is the escape character if each_character == chr(27): - escape = 1 + escape = True + + # the current character is within an escape sequence elif escape: + + # the current character is m, which terminates the + # current escape sequence if each_character == "m": - escape = 0 + escape = False + + # the current character is a newline, so reset the relative + # position (start a new line) elif each_character == "\n": relative_position = 0 + + # the current character meets the requested maximum line width, + # so we need to backtrack and find a space at which to wrap elif relative_position == width: + + # distance of the current character examined from the + # relative position wrap_offset = 0 + + # count backwards until we find a space while text[absolute_position - wrap_offset] != " ": wrap_offset += 1 + + # insert an eol in place of the space text = text[:absolute_position - wrap_offset] + "\r\n" + text[absolute_position - wrap_offset + 1:] + + # increase the absolute position because an eol is two + # characters but the space it replaced was only one absolute_position += 1 + + # now we're at the begining of a new line, plus the + # number of characters wrapped from the previous line relative_position = wrap_offset + + # as long as the character is not a carriage return and the + # other above conditions haven't been met, count it as a + # printable character elif each_character != "\r": relative_position += 1 + + # increase the absolute position for every character absolute_position += 1 + + # return the newly-wrapped text return text diff --git a/lib/muff/muffsock.py b/lib/muff/muffsock.py index 1ca604c..67010b1 100644 --- a/lib/muff/muffsock.py +++ b/lib/muff/muffsock.py @@ -1,61 +1,79 @@ """Socket objects for the MUFF Engine""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. +# Licensed per terms in the LICENSE file distributed with this software. +# need socket.socket for new connection objects and the server's listener import socket +# hack to load all modules in the muff package import muff for module in muff.__all__: exec("import " + module) def check_for_connection(newsocket): + """Check for a waiting connection and return a new user object.""" + + # try to accept a new connection try: connection, address = newsocket.accept() except: return None + + # note that we got one + # TODO: we should log this crap somewhere print "Connection from", address + + # disable blocking so we can proceed whether or not we can send/receive connection.setblocking(0) + + # create a new user object user = muffuser.User(); + + # associate this connection with it user.connection = connection + + # set the user's ipa from the connection's ipa user.address = address[0] + + # return the new user object return user def initialize_server_socket(): + """Create and open the listening socket.""" + + # create a new ipv4 stream-type socket object newsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # set the socket options to allow existing open ones to be reused + # (fixes a bug where the server can't bind for a minute when restarting + # on linux systems) newsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + # bind the socket to to our desired server ipa and port newsocket.bind((muffconf.config_data.get("network", "host"), muffconf.config_data.getint("network", "port"))) + + # disable blocking so we can proceed whether or not we can send/receive newsocket.setblocking(0) + + # start listening on the socket newsocket.listen(1) + + # note that we're now ready for user connections + # TODO: we should log this crap somewhere print "Waiting for connection(s)..." + + # store this in a globally-accessible place muffvars.newsocket = newsocket def destroy_all_sockets(): + """Go through all connected users and close their sockets.""" + + # note that we're closing all sockets + # TODO: we should log this crap somewhere print "Closing remaining connections..." + + # iterate over each connected user and close their associated sockets for user in muffvars.userlist: user.connection.close() diff --git a/lib/muff/muffuser.py b/lib/muff/muffuser.py index 4ecdebf..6881869 100644 --- a/lib/muff/muffuser.py +++ b/lib/muff/muffuser.py @@ -1,103 +1,191 @@ """User objects for the MUFF Engine""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. +# Licensed per terms in the LICENSE file distributed with this software. +# user accounts are stored in ini-style files supported by ConfigParser import ConfigParser + +# test for existence of the account dir with os.listdir and os.mkdir to make it +import os + +# string.replace is used to perform substitutions for color codes and the like import string +# hack to load all modules in the muff package import muff for module in muff.__all__: exec("import " + module) class User: + """This is a connected 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 + + # an object containing persistent account data self.record = ConfigParser.SafeConfigParser() + def load(self): - filename = muffconf.config_data.get("general", "account_path") + "/" + self.name + """Retrieve account data from cold storage.""" + + # what the filename for the user account should be + filename = muffconf.config_data.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): - filename = muffconf.config_data.get("general", "account_path") + "/" + self.proposed_name + """Retrieve the user's account password hash from storage.""" + + # what the filename for the user account could be + filename = muffconf.config_data.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") + + # otherwise, the password hash is empty except: self.passhash = "" + def save(self): + """Record account data to cold storage.""" + + # the user account must be authenticated to save if self.authenticated: - filename = muffconf.config_data.get("general", "account_path") + "/" + self.name.lower() + + # 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.config_data.get("files", "accounts") + # the filename to which we'll write + filename = account_path + "/" + self.name.lower() + + # if the directory doesn't exist, create it + # TODO: create account_path with 0700 perms + try: + if os.listdir(account_path): pass + except: + os.mkdir(account_path, ) + + # open the user account file for writing + # TODO: create filename with 0600 perms record_file = file(filename, "w") + + # dump the account data to it self.record.write(record_file) + + # close the user account file record_file.close() + def show_menu(self): + """Send the user their current menu.""" self.send(muffmenu.get_menu(self)) + def remove(self): + """Remove a user from the list of connected users.""" muffvars.userlist.remove(self) + def send(self, output, eol="$(eol)"): + """Send arbitrary text to a connected user.""" + + # only when there is actual output if output: + + # start with a newline, append the message, then end + # with the optional eol string passed to this function output = "$(eol)" + output + eol + + # replace eol markers with a crlf + # TODO: search for markers and replace from a dict output = string.replace(output, "$(eol)", "\r\n") - output = string.replace(output, "$(div)", "\r\n\r\n") + + # replace display markers with ansi escapse sequences output = string.replace(output, "$(bld)", chr(27)+"[1m") output = string.replace(output, "$(nrm)", chr(27)+"[0m") output = string.replace(output, "$(blk)", chr(27)+"[30m") output = string.replace(output, "$(grn)", chr(27)+"[32m") output = string.replace(output, "$(red)", chr(27)+"[31m") + + # the user's account name output = string.replace(output, "$(account)", self.name) + + # wrap the text at 80 characters + # TODO: prompt user for preferred wrap width output = muffmisc.wrap_ansi_text(output, 80) + + # drop the formatted output into the output queue self.output_queue.append(output) + + # try to send the last item in the queue, remove it and + # flag that menu display is not needed try: self.connection.send(self.output_queue[0]) self.output_queue.remove(self.output_queue[0]) self.menu_seen = False + + # but if we can't, that's okay too except: pass diff --git a/lib/muff/muffvars.py b/lib/muff/muffvars.py index b7879cc..cededce 100644 --- a/lib/muff/muffvars.py +++ b/lib/muff/muffvars.py @@ -1,46 +1,28 @@ """Global variable objects for the MUFF Engine""" -# Copyright (c) 2005 mudpy, Jeremy Stanley -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005 mudpy, Jeremy Stanley , all rights reserved. +# Licensed per terms in the LICENSE file distributed with this software. +# hack to load all modules in the muff package import muff for module in muff.__all__: exec("import " + module) +# if there is no userlist, create an empty one try: if userlist: pass except NameError: userlist = [] +# if there is no listening socket, create an empty one try: if newsocket: pass except NameError: newsocket = None +# flag to raise when the world should be shut down terminate_world = False + +# flag to raise when all code modules, config and data should be reloaded reload_modules = False diff --git a/muff b/muff new file mode 100755 index 0000000..57874d4 --- /dev/null +++ b/muff @@ -0,0 +1,36 @@ +#!/usr/bin/python +"""Skeletal executable 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 uses the ini-style configs supported by the ConfigParser module +import ConfigParser + +# need the sys module to alter the import path appropriately +import sys + +def get_main_loop(): + """Find and return the main loop function""" + + # figure out where to find our main configuration file + config_data = ConfigParser.SafeConfigParser() + config_dirs = [".", "./etc", "/usr/local/muff", "/usr/local/muff/etc", "/etc/muff", "/etc" ] + config_name = "muff.conf" + config_files = [] + for each_dir in config_dirs: + config_files.append(each_dir + "/" + config_name) + + # load the config file, get the module path and add it to sys.path + config_data.read(config_files) + module_path = config_data.get("files", "modules") + sys.path.append(module_path) + + # import the main loop function + from muff.muffmain import main + return main + +# load the main loop and run it +main = get_main_loop() +main() + diff --git a/etc/muff.conf b/muff.conf similarity index 52% rename from etc/muff.conf rename to muff.conf index ca879e7..da57ea8 100644 --- a/etc/muff.conf +++ b/muff.conf @@ -1,9 +1,11 @@ +[files] +accounts = ./lib/accounts +commands = ./lib/commands +menus = ./lib/menus +modules = ./lib + [general] -account_path = ./lib/accounts -command_path = ./lib/commands increment = 0.1 -menu_path = ./lib/menus -modules = ./lib password_tries = 3 [network]