From: Jeremy Stanley Date: Fri, 25 Dec 2009 20:15:24 +0000 (+0000) Subject: Imported from archive. X-Git-Tag: 0.0.1~315 X-Git-Url: https://mudpy.org/gitweb?p=mudpy.git;a=commitdiff_plain;h=8bf6b7a787510321b75e477079ebf70ac150d853 Imported from archive. * LICENSE: Moved to doc subdirectory, indicating it's a documentation file. * archetype, command, menu: Moved to share subdirectory, indicating it's generally read-only data. Also appended .mpy extention to file names, to indicate they're mudpy data. * banner.txt, login.txt, mudpy.conf: Moved to etc subdirectory, indicating they're generally hand-edited configuration data. * lib/mudpy/__init__.py: Created a new Python module package, to begin the process of splitting up the old monolithic module. * mudpy: Moved to bin subdirectory, indicating it's directly executable. * mudpy.conf (__control__, internal:storage), mudpy.py (DataFile.load, find_file, replace_macros): New functionality allows arbitrary location of data files from relative or absolute paths out of a prioritized series of potential directory trees. * mudpy.conf (internal:network): Changed host to the IPv6 localhost address of ::1 instead of the old IPv4 127.0.0.1 equivalent. * mudpy.py: Renamed to misc.py and added to the new lib/mudpy Python module package. (Universe.initialize_server_socket): If local IPv6 support is present default to listening on ::, otherwise fall back to 0.0.0.0 like before. (command_say): Instead of denying parameters which look like replacement macros, they are simply escaped before being processed. Messages enclosed in quotation marks no longer get language fix-ups applied. Fixed a bug where a type exception could be triggered if an actor provided parameters to the say command which evaluated to an empty string; reported by Mark Kolloros, a.k.a. Colourful. * sample/index: Renamed to __init__.mpy in an effort to emulate Python module package structure for groups of mudpy data files. * sample/location, sample/prop: Appended .mpy extention to file names, to indicate they're mudpy data. --- diff --git a/mudpy b/bin/mudpy similarity index 50% rename from mudpy rename to bin/mudpy index 230ab0e..0dfefd2 100755 --- a/mudpy +++ b/bin/mudpy @@ -7,34 +7,25 @@ u"""Skeletal executable for the mudpy engine.""" # terms provided in the LICENSE file distributed with this software. # core objects for the mudpy engine +import os.path, sys +sys.path.append( os.path.realpath(u"lib") ) import mudpy -# log an initial message -import sys -mudpy.log(u"Started mudpy with command line: " + u" ".join(sys.argv)) - -# fork and disassociate -mudpy.daemonize(mudpy.universe) - -# make the pidfile -mudpy.create_pidfile(mudpy.universe) +# start it up +mudpy.misc.setup() # loop indefinitely while the world is not flagged for termination or # there are still connected users -while not mudpy.universe.terminate_flag or mudpy.universe.userlist: +while not mudpy.misc.universe.terminate_flag or mudpy.misc.universe.userlist: # the world was flagged for a reload of all code/data - if mudpy.universe.reload_flag: reload(mudpy) + if mudpy.misc.universe.reload_flag: + reload(mudpy) + mudpy.misc.reload_data() + mudpy.misc.universe.reload_flag = False # do what needs to be done on each pulse - mudpy.on_pulse() - -# the loop has terminated, so save persistent data -mudpy.universe.save() - -# log a final message -mudpy.log(u"Shutting down now.") - -# get rid of the pidfile -mudpy.remove_pidfile(mudpy.universe) + mudpy.misc.on_pulse() +# shut it all down +mudpy.misc.finish() diff --git a/LICENSE b/doc/LICENSE similarity index 100% rename from LICENSE rename to doc/LICENSE diff --git a/banner.txt b/etc/banner.txt similarity index 100% rename from banner.txt rename to etc/banner.txt diff --git a/login.txt b/etc/login.txt similarity index 100% rename from login.txt rename to etc/login.txt diff --git a/mudpy.conf b/etc/mudpy.conf similarity index 79% rename from mudpy.conf rename to etc/mudpy.conf index 5368c15..584da6a 100644 --- a/mudpy.conf +++ b/etc/mudpy.conf @@ -3,9 +3,10 @@ # terms provided in the LICENSE file distributed with this software. [__control__] -default_files = { "account": "account", "actor": "actor", "command": "command", "internal": "internal", "location": "location", "menu": "menu", "other": "other", "prop": "prop" } -include_files = [ "archetype", "sample/index" ] -private_files = account +default_files = { "account": "account.mpy", "actor": "actor.mpy", "command": "command.mpy", "internal": "internal.mpy", "location": "location.mpy", "menu": "menu.mpy", "other": "other.mpy", "prop": "prop.mpy" } +include_dirs = "sample" +include_files = "archetype.mpy" +private_files = "account.mpy" read_only = yes [internal:language] @@ -26,13 +27,19 @@ stdout = yes #syslog = mudpy [internal:network] -host = 127.0.0.1 +host = ::1 +#host = 127.0.0.1 port = 6669 [internal:process] #daemon = yes #pidfile = mudpy.pid +[internal:storage] +default_dir = "data" +#root_path = "." +search_path = [ "", "etc", "share", "data" ] + [internal:time] definition_d = 24h definition_h = 60mi diff --git a/lib/mudpy/__init__.py b/lib/mudpy/__init__.py new file mode 100644 index 0000000..06b1e23 --- /dev/null +++ b/lib/mudpy/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +u"""Core modules package for the mudpy engine.""" + +# Copyright (c) 2004-2009 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. + +def load(): + u"""Import/reload some modules (be careful, as this can result in loops).""" + + # pick up the modules list from this package + global modules + + # iterate over the list of modules provided + for module in modules: + + # attempt to reload the module, assuming it was probably imported earlier + try: exec(u"reload(%s)" % module) + + # must not have been, so import it now + except NameError: exec(u"import %s" % module) + +# load the modules contained in this package +modules = [ u"misc" ] +load() diff --git a/mudpy.py b/lib/mudpy/misc.py similarity index 90% rename from mudpy.py rename to lib/mudpy/misc.py index 6ec7c5b..033699c 100644 --- a/mudpy.py +++ b/lib/mudpy/misc.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -u"""Core objects for the mudpy engine.""" +u"""Miscellaneous functions for the mudpy engine.""" # Copyright (c) 2004-2009 Jeremy Stanley . Permission # to use, copy, modify, and distribute this software is granted under @@ -102,6 +102,9 @@ class Element: if default is None: default = u"" if self.origin.data.has_option(self.key, facet): raw_data = self.origin.data.get(self.key, facet) + if raw_data.startswith(u"u\"") or raw_data.startswith(u"u'"): + raw_data = raw_data[1:] + raw_data.strip(u"\"'") if type(raw_data) == str: return unicode(raw_data, "utf-8") else: return raw_data elif self.has_facet(u"inherit"): @@ -356,34 +359,58 @@ class DataFile: if os.access(self.filename, os.R_OK): self.data.read(self.filename) if not hasattr(self.universe, u"files"): self.universe.files = {} self.universe.files[self.filename] = self + includes = [] if self.data.has_option(u"__control__", u"include_files"): - includes = makelist(self.data.get(u"__control__", u"include_files")) - else: includes = [] + for included in makelist( + self.data.get(u"__control__", u"include_files") + ): + included = find_file( + included, + relative=self.filename, + universe=self.universe + ) + if included not in includes: includes.append(included) + if self.data.has_option(u"__control__", u"include_dirs"): + for included in [ os.path.join(x, u"__init__.mpy") for x in makelist( + self.data.get(u"__control__", u"include_dirs") + ) ]: + included = find_file( + included, + relative=self.filename, + universe=self.universe + ) + if included not in includes: includes.append(included) if self.data.has_option(u"__control__", u"default_files"): origins = makedict(self.data.get(u"__control__", u"default_files")) for key in origins.keys(): - if not os.path.isabs(origins[key]): - origins[key] = os.path.join( - os.path.dirname(self.filename), origins[key] - ) - if not origins[key] in includes: includes.append(origins[key]) + origins[key] = find_file( + origins[key], + relative=self.filename, + universe=self.universe + ) + if origins[key] not in includes: includes.append(origins[key]) self.universe.default_origins[key] = origins[key] - if not key in self.universe.categories: + if key not in self.universe.categories: self.universe.categories[key] = {} if self.data.has_option(u"__control__", u"private_files"): for item in makelist(self.data.get(u"__control__", u"private_files")): - if not item in includes: includes.append(item) - if not item in self.universe.private_files: - if not os.path.isabs(item): - item = os.path.join(os.path.dirname(self.filename), item) + item = find_file( + item, + relative=self.filename, + universe=self.universe + ) + if item not in includes: includes.append(item) + if item not in self.universe.private_files: self.universe.private_files.append(item) for section in self.data.sections(): if section != u"__control__": Element(section, self.universe, self.filename) for include_file in includes: if not os.path.isabs(include_file): - include_file = os.path.join( - os.path.dirname(self.filename), include_file + include_file = find_file( + include_file, + relative=self.filename, + universe=self.universe ) if include_file not in self.universe.files or not self.universe.files[ include_file @@ -567,8 +594,22 @@ class Universe: u"""Create and open the listening socket.""" import socket - # create a new ipv4 stream-type socket object - self.listening_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # need to know the local address and port number for the listener + host = self.categories[u"internal"][u"network"].get(u"host") + port = self.categories[u"internal"][u"network"].getint(u"port") + + # if no host was specified, bind to all local addresses (preferring ipv6) + if not host: + if socket.has_ipv6: host = u"::" + else: host = u"0.0.0.0" + + # figure out if this is ipv4 or v6 + family = socket.getaddrinfo(host, port)[0][0] + if family is socket.AF_INET6 and not socket.has_ipv6: + log(u"No support for IPv6 address %s (use IPv4 instead)." % host) + + # create a new stream-type socket object + self.listening_socket = socket.socket(family, 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 @@ -578,8 +619,6 @@ class Universe: ) # bind the socket to to our desired server ipa and port - host = self.categories[u"internal"][u"network"].get(u"host") - port = self.categories[u"internal"][u"network"].getint(u"port") self.listening_socket.bind((host, port)) # disable blocking so we can proceed whether or not we can @@ -590,7 +629,6 @@ class Universe: self.listening_socket.listen(1) # note that we're now ready for user connections - if not host: host = u"0.0.0.0" log( u"Listening for Telnet connections on: " + host + u":" + unicode(port) ) @@ -659,7 +697,7 @@ class User: logline += self.account.get(u"name") else: logline += u"an unknown user" - logline += u" after idling too long in a " + self.state + u" state." + logline += u" after idling too long in the " + self.state + u" state." log(logline, 2) self.state = u"disconnecting" self.menu_seen = False @@ -920,8 +958,8 @@ class User: account = self.account.get(u"name") else: account = u"an unknown user" log( - u"Sending to " + account \ - + u" raised an exception (broken pipe?)." + u"Sending to %s raised an exception (broken pipe?)." % account, + 7 ) pass @@ -1200,6 +1238,7 @@ class User: def makelist(value): u"""Turn string into list type.""" if value[0] + value[-1] == u"[]": return eval(value) + elif value[0] + value[-1] == u"\"\"": return [ value[1:-1] ] else: return [ value ] def makedict(value): @@ -1551,7 +1590,7 @@ def replace_macros(user, text, is_input=False): # this is how we handle local file inclusion (dangerous!) elif macro.startswith(u"inc:"): - incfile = os.path.join(universe.startdir, macro[4:]) + incfile = find_file(macro[4:], universe=universe) if os.path.exists(incfile): incfd = codecs.open(incfile, u"r", u"utf-8") replacement = u"" @@ -2210,15 +2249,21 @@ def command_say(actor, parameters): u"""Speak to others in the same room.""" import unicodedata - # check for replacement macros - if replace_macros(actor.owner, parameters, True) != parameters: - actor.send(u"You cannot speak $_(replacement macros).") + # check for replacement macros and escape them + parameters = escape_macros(parameters) - # the user entered a message - elif parameters: + # if the message is wrapped in quotes, remove them and leave contents intact + if parameters.startswith(u"\"") and parameters.endswith(u"\""): + message = parameters[1:-1] + literal = True - # get rid of quote marks on the ends of the message + # otherwise, get rid of stray quote marks on the ends of the message + else: message = parameters.strip(u"\"'`") + literal = False + + # the user entered a message + if message: # match the punctuation used, if any, to an action actions = universe.categories[u"internal"][u"language"].getdict( @@ -2229,36 +2274,44 @@ def command_say(actor, parameters): ) action = u"" for mark in actions.keys(): - if message.endswith(mark): + if not literal and message.endswith(mark): action = actions[mark] break # add punctuation if needed if not action: action = actions[default_punctuation] - if message and not unicodedata.category(message[-1]) == u"Po": + if message and not ( + literal or unicodedata.category(message[-1]) == u"Po" + ): message += default_punctuation - # decapitalize the first letter to improve matching - message = message[0].lower() + message[1:] - - # iterate over all words in message, replacing typos - typos = universe.categories[u"internal"][u"language"].getdict(u"typos") - words = message.split() - for index in range(len(words)): - word = words[index] - while unicodedata.category(word[0]) == u"Po": - word = word[1:] - while unicodedata.category(word[-1]) == u"Po": - word = word[:-1] - if word in typos.keys(): - words[index] = words[index].replace(word, typos[word]) - message = u" ".join(words) - - # capitalize the first letter - message = message[0].upper() + message[1:] - - # tell the room + # failsafe checks to avoid unwanted reformatting and null strings + if message and not literal: + + # decapitalize the first letter to improve matching + message = message[0].lower() + message[1:] + + # iterate over all words in message, replacing typos + typos = universe.categories[u"internal"][u"language"].getdict( + u"typos" + ) + words = message.split() + for index in range(len(words)): + word = words[index] + while unicodedata.category(word[0]) == u"Po": + word = word[1:] + while unicodedata.category(word[-1]) == u"Po": + word = word[:-1] + if word in typos.keys(): + words[index] = words[index].replace(word, typos[word]) + message = u" ".join(words) + + # capitalize the first letter + message = message[0].upper() + message[1:] + + # tell the room + if message: actor.echo_to_location( actor.get(u"name") + u" " + action + u"s, \"" + message + u"\"" ) @@ -2484,6 +2537,128 @@ def command_error(actor, input_data): # send the error message actor.send(message) +def find_file( + file_name=None, + root_path=None, + search_path=None, + default_dir=None, + relative=None, + universe=None +): + u"""Return an absolute file path based on configuration.""" + import os, os.path, sys + + # make sure to get rid of any surrounding quotes first thing + if file_name: file_name = file_name.strip(u"\"'") + + # this is all unnecessary if it's already absolute + if file_name and os.path.isabs(file_name): + return os.path.realpath(file_name) + + # when no file name is specified, look for .conf + elif not file_name: file_name = os.path.basename( sys.argv[0] ) + u".conf" + + # if a universe was provided, try to get some defaults from there + if universe: + + if hasattr( + universe, + u"contents" + ) and u"internal:storage" in universe.contents: + storage = universe.categories[u"internal"][u"storage"] + if not root_path: root_path = storage.get(u"root_path").strip("\"'") + if not search_path: search_path = storage.getlist(u"search_path") + if not default_dir: default_dir = storage.get(u"default_dir").strip("\"'") + + # if there's only one file loaded, try to work around a chicken 1: conffile = sys.argv[1] else: conffile = u"" + + # the big bang + global universe universe = Universe(conffile, True) -elif universe.reload_flag: - universe = universe.new() - reload_data() + # log an initial message + log(u"Started mudpy with command line: " + u" ".join(sys.argv)) + + # fork and disassociate + daemonize(universe) + + # override the default exception handler so we get logging first thing + override_excepthook() + + # set up custom signal handlers + assign_sighook() + + # make the pidfile + create_pidfile(universe) + + # pass the initialized universe back + return universe + +def finish(): + """This contains functions to be performed when shutting down the engine.""" + + # the loop has terminated, so save persistent data + universe.save() + + # log a final message + log(u"Shutting down now.") + + # get rid of the pidfile + remove_pidfile(universe) diff --git a/sample/index b/sample/__init__.mpy similarity index 83% rename from sample/index rename to sample/__init__.mpy index e543503..93925a0 100644 --- a/sample/index +++ b/sample/__init__.mpy @@ -3,6 +3,6 @@ # terms provided in the LICENSE file distributed with this software. [__control__] -include_files = [ "location", "prop" ] +include_files = [ "location.mpy", "prop.mpy" ] read_only = yes diff --git a/sample/location b/sample/location.mpy similarity index 100% rename from sample/location rename to sample/location.mpy diff --git a/sample/prop b/sample/prop.mpy similarity index 100% rename from sample/prop rename to sample/prop.mpy diff --git a/archetype b/share/archetype.mpy similarity index 100% rename from archetype rename to share/archetype.mpy diff --git a/command b/share/command.mpy similarity index 95% rename from command rename to share/command.mpy index 17ac3e3..cd907ff 100644 --- a/command +++ b/share/command.mpy @@ -64,7 +64,7 @@ help = This will reload all python modules and read-only data files. [command:say] action = command_say(actor, parameters) description = State something out loud. -help = This allows you to speak to other characters within the same room. If you end your sentence with punctuation, the message displayed will incorporate an appropriate action (ask, exclaim, et cetera). It will also correct common typographical errors, add punctuation and capitalize your sentence as needed (assuming you speak one sentence per line). For example:$(eol)$(eol) > say youre sure i went teh wrong way?$(eol) You ask, "You're sure I went the wrong way?" +help = This allows you to speak to other characters within the same room. If you end your sentence with punctuation, the message displayed will incorporate an appropriate action (ask, exclaim, et cetera). It will also correct common typographical errors, add punctuation and capitalize your sentence as needed (assuming you speak one sentence per line). For example:$(eol)$(eol) > say youre sure i went teh wrong way?$(eol) You ask, "You're sure I went the wrong way?"$(eol)$(eol)If necessary, enclose literal statements in quotation marks:$(eol)$(eol) > say "youre sure i went teh wrong way?"$(eol) You say, "youre sure i went teh wrong way?" see_also = chat [command:set] diff --git a/menu b/share/menu.mpy similarity index 100% rename from menu rename to share/menu.mpy