From a89c4b2060bf3c93207a659d88f51a4bf0f891fa Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Tue, 10 Jun 2008 22:42:56 +0000 Subject: [PATCH] Imported from archive. * (all): Updated copyright notices for 2008, and added references to the included LICENSE file. Added similar copyright notices to all data/document files. * LICENSE: Switched the project from modified 2-clause BSD license to the simpler and equivalent ISC license. * banner.txt, login.txt, menu (menu:entering_account_name) (menu:main_utility): Implemented text file inclusion using a file replacement macro, and relocated the ASCII/ANSI art from the login/lobby menu descriptions into separate login.txt and banner.txt files. * command (command:show): Renamed parameter to option in the help. * menu (menu:delete_avatar): Corrected a misleading typo in the desription. * mudpy.conf (internal:time), mudpy.py (User.__init__) (User.check_idle, User.pulse, handle_user_input): Added idle and linkdead dict facets, indicating how long users can idle in various states before they're warned and ultimately disconnected. * mudpy.py (replace_macros): Performance enhancement, moving unnecessary declarations outside the processing loop. (wrap_ansi_text): Refactored the word-wrapping routines to solve a bug where lines explicitly terminated at the wrap column got wrapped early. --- LICENSE | 36 ++++------ archetype | 4 ++ banner.txt | 1 + command | 6 +- example/index | 4 ++ example/second_square/index | 4 ++ example/second_square/location | 6 +- example/second_square/prop | 4 ++ login.txt | 1 + menu | 10 ++- mudpy | 6 +- mudpy.conf | 6 ++ mudpy.py | 150 +++++++++++++++++++++++++++++------------ 13 files changed, 165 insertions(+), 73 deletions(-) create mode 100644 banner.txt create mode 100644 login.txt diff --git a/LICENSE b/LICENSE index c1ce24f..902f8d5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,23 +1,13 @@ -Copyright (c) 2005, 2006 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) 2004-2008 Jeremy Stanley + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/archetype b/archetype index ad43eba..fa370b7 100644 --- a/archetype +++ b/archetype @@ -1,3 +1,7 @@ +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. + [__control__] read_only = yes diff --git a/banner.txt b/banner.txt new file mode 100644 index 0000000..8d12d6d --- /dev/null +++ b/banner.txt @@ -0,0 +1 @@ +Example lobby menu... diff --git a/command b/command index c5499c9..3d722a5 100644 --- a/command +++ b/command @@ -1,3 +1,7 @@ +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. + [__control__] read_only = yes @@ -73,5 +77,5 @@ help = Invoke it like this:$(eol)$(eol) set actor:dominique description You se action = command_show(actor, parameters) administrative = yes description = Show various data. -help = Here are the possible incantations ( is required, [parameter] is optional, (note) is a note):$(eol)$(eol) show categories (list all element category names)$(eol) show category (list the elements in a category)$(eol) show element (list facet definitions for an element)$(eol) show file (list elements in a file)$(eol) show files (list all element data files)$(eol) show log [level [start [stop]]] (list logs above level from start to stop)$(eol) show result (evaluates a python expression)$(eol) show time (returns several current timer values) +help = Here are the possible incantations ( is required, [option] is optional, (note) is a note):$(eol)$(eol) show categories (list all element category names)$(eol) show category (list the elements in a category)$(eol) show element (list facet definitions for an element)$(eol) show file (list elements in a file)$(eol) show files (list all element data files)$(eol) show log [level [start [stop]]] (list logs above level from start to stop)$(eol) show result (evaluates a python expression)$(eol) show time (returns several current timer values) diff --git a/example/index b/example/index index 10773c7..4dc7c5f 100644 --- a/example/index +++ b/example/index @@ -1,3 +1,7 @@ +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. + [__control__] include_files = second_square/index read_only = yes diff --git a/example/second_square/index b/example/second_square/index index 2836fe9..0997a26 100644 --- a/example/second_square/index +++ b/example/second_square/index @@ -1,3 +1,7 @@ +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. + [__control__] include_files = [ "location", "prop" ] read_only = yes diff --git a/example/second_square/location b/example/second_square/location index 1623f47..0a14869 100644 --- a/example/second_square/location +++ b/example/second_square/location @@ -1,3 +1,7 @@ +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. + [location:-1,-1,0] description = This booth sells cloth garments of every description. Fine silks and linens line the walls, draped from every protrusion and stacked on every surface. gridlinks = ['north'] @@ -196,7 +200,7 @@ name = Second Square terrain = city [location:0,0,1] -description = The stand is currently not in use for a performance, but shoppers gather here to rest and chat with one another. A pleasantly cool breeze coupled with a nice view of the market make this a good place to relax. +description = The stand is currently not in use for a performance, but shoppers gather here to rest and chat with one another. A pleasantly cool breeze coupled with a nice view of the market makes this a good place to relax. gridlinks = ['down'] name = The Bandstand terrain = inside diff --git a/example/second_square/prop b/example/second_square/prop index 64d1fd8..e7fb24a 100644 --- a/example/second_square/prop +++ b/example/second_square/prop @@ -1,3 +1,7 @@ +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. + [prop:fountain] impression = An inviting public fountain bubbles here, tempting you with thirst. keywords = fountain water diff --git a/login.txt b/login.txt new file mode 100644 index 0000000..7192f9d --- /dev/null +++ b/login.txt @@ -0,0 +1 @@ +Welcome to the mudpy example... diff --git a/menu b/menu index 495fce9..040de65 100644 --- a/menu +++ b/menu @@ -1,3 +1,7 @@ +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. + [__control__] read_only = yes @@ -73,7 +77,7 @@ branch_a = main_utility choice_a = abort selection create = dict([(str(x+1),y) for x,y in enumerate(user.list_avatar_names())]) default = a -description = This is the list of avatars available for you to awaken. +description = This is the list of avatars available for you to delete. prompt = Whom would you like to delete? [menu:disconnecting] @@ -83,7 +87,7 @@ description = $(red)Disconnecting...$(nrm) prompt = $(red)Closing your previous connection...$(nrm)$(eol) [menu:entering_account_name] -description = Welcome to the mudpy example... +description = $(inc:login.txt) error_bad_name = Your account name needs to contain only digits (0-9) and letters (a-z). prompt = Identify yourself: @@ -110,7 +114,7 @@ choice_p = permanently remove your account demand_a = user.account.getlist("avatars") demand_c = len(user.account.getlist("avatars")) < universe.categories["internal"]["limits"].getint("max_avatars") demand_d = user.account.getlist("avatars") -description = From here you can awaken, create and delete avatars. An avatar is your persona in the world of Example. You can also leave or permanently delete your account. +description = $(red)$(inc:banner.txt)$(nrm)$(eol)$(eol)From here you can awaken, create and delete avatars. An avatar is your persona in the world of Example. You can also leave or permanently delete your account. prompt = What would you like to do? [menu:verifying_new_password] diff --git a/mudpy b/mudpy index 519f07b..8e115bd 100755 --- a/mudpy +++ b/mudpy @@ -1,9 +1,9 @@ #!/usr/bin/python """Skeletal executable for the mudpy engine.""" -# Copyright (c) 2005, 2006 Jeremy Stanley . All rights -# reserved. Licensed per terms in the LICENSE file distributed with this -# software. +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. # core objects for the mudpy engine import mudpy diff --git a/mudpy.conf b/mudpy.conf index 8fa0876..310c307 100644 --- a/mudpy.conf +++ b/mudpy.conf @@ -1,3 +1,7 @@ +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# 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", "example/index" ] @@ -39,6 +43,8 @@ definition_w = 7d definition_y = 12mo frequency_log = 6000 frequency_save = 600 +linkdead = { "default": 6000, "entering_account_name": 600, "active": 6048000 } +idle = { "default": 5000, "entering_account_name": 500, "active": 5040000 } increment = 0.1 [internal:directions] diff --git a/mudpy.py b/mudpy.py index 01b9142..699eae1 100644 --- a/mudpy.py +++ b/mudpy.py @@ -1,8 +1,8 @@ """Core objects for the mudpy engine.""" -# Copyright (c) 2005, 2006 Jeremy Stanley . All rights -# reserved. Licensed per terms in the LICENSE file distributed with this -# software. +# Copyright (c) 2004-2008 Jeremy Stanley . Permission +# to use, copy, modify, and distribute this software is granted under +# terms provided in the LICENSE file distributed with this software. # import some things we need from ConfigParser import RawConfigParser @@ -508,6 +508,7 @@ class User: self.error = "" self.input_queue = [] self.last_address = "" + self.last_input = universe.get_time() self.menu_choices = {} self.menu_seen = False self.negotiation_pause = 0 @@ -530,6 +531,27 @@ class User: self.connection.close() self.remove() + def check_idle(self): + """Warn or disconnect idle users as appropriate.""" + idletime = universe.get_time() - self.last_input + linkdead_dict = universe.categories["internal"]["time"].getdict("linkdead") + if self.state in linkdead_dict: linkdead_state = self.state + else: linkdead_state = "default" + if idletime > linkdead_dict[linkdead_state]: + self.send("$(eol)$(red)You've done nothing for far too long... goodbye!$(nrm)$(eol)", flush=True, add_prompt=False) + logline = "Disconnecting " + if self.account and self.account.get("name"): logline += self.account.get("name") + else: logline += "an unknown user" + logline += " after idling too long in a " + self.state + " state." + log(logline, 2) + self.state = "disconnecting" + self.menu_seen = False + idle_dict = universe.categories["internal"]["time"].getdict("idle") + if self.state in idle_dict: idle_state = self.state + else: idle_state = "default" + if idletime == idle_dict[idle_state]: + self.send("$(eol)$(red)If you continue to be unproductive, you'll be shown the door...$(nrm)$(eol)") + def reload(self): """Save, load a new user and relocate the connection.""" @@ -677,6 +699,9 @@ class User: self.state = "disconnecting" self.menu_seen = False + # check for an idle connection and act appropriately + else: self.check_idle() + # if output is paused, decrement the counter if self.state == "initial": if self.negotiation_pause: self.negotiation_pause -= 1 @@ -992,21 +1017,40 @@ def wrap_ansi_text(text, width): # ignoring color escape sequences relative_position = 0 + # whether the current character is part of a telnet IAC sequence + iac_counter = 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 telnet IAC character + if each_character == IAC and not iac_counter: + iac_counter = 2 + + # the current character is within an IAC sequence + elif iac_counter: + + # the current character is another IAC, + # terminating the sequence + if each_character == IAC: + iac_counter = 0 + + # otherwise, decrement the IAC counter + else: + iac_counter -= 1 + # the current character is the escape character - if each_character == chr(27): + elif each_character == chr(27) and not escape: escape = True # the current character is within an escape sequence elif escape: # the current character is m, which terminates the - # current escape sequence + # escape sequence if each_character == "m": escape = False @@ -1017,7 +1061,7 @@ def wrap_ansi_text(text, width): # 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: + elif relative_position == width and not each_character == "\r": # distance of the current character examined from the # relative position @@ -1092,56 +1136,75 @@ def random_name(): def replace_macros(user, text, is_input=False): """Replaces macros in text output.""" + # third person pronouns + pronouns = { + "female": { "obj": "her", "pos": "hers", "sub": "she" }, + "male": { "obj": "him", "pos": "his", "sub": "he" }, + "neuter": { "obj": "it", "pos": "its", "sub": "it" } + } + + # a dict of replacement macros + macros = { + "eol": "\r\n", + "bld": chr(27) + "[1m", + "nrm": chr(27) + "[0m", + "blk": chr(27) + "[30m", + "blu": chr(27) + "[34m", + "cyn": chr(27) + "[36m", + "grn": chr(27) + "[32m", + "mgt": chr(27) + "[35m", + "red": chr(27) + "[31m", + "yel": chr(27) + "[33m", + } + + # add dynamic macros where possible + if user.account: + account_name = user.account.get("name") + if account_name: + macros["account"] = account_name + if user.avatar: + avatar_gender = user.avatar.get("gender") + if avatar_gender: + macros["tpop"] = pronouns[avatar_gender]["obj"] + macros["tppp"] = pronouns[avatar_gender]["pos"] + macros["tpsp"] = pronouns[avatar_gender]["sub"] + # loop until broken while True: - # third person pronouns - pronouns = { - "female": { "obj": "her", "pos": "hers", "sub": "she" }, - "male": { "obj": "him", "pos": "his", "sub": "he" }, - "neuter": { "obj": "it", "pos": "its", "sub": "it" } - } - - # a dict of replacement macros - macros = { - "$(eol)": "\r\n", - "$(bld)": chr(27) + "[1m", - "$(nrm)": chr(27) + "[0m", - "$(blk)": chr(27) + "[30m", - "$(blu)": chr(27) + "[34m", - "$(cyn)": chr(27) + "[36m", - "$(grn)": chr(27) + "[32m", - "$(mgt)": chr(27) + "[35m", - "$(red)": chr(27) + "[31m", - "$(yel)": chr(27) + "[33m", - } - - # add dynamic macros where possible - if user.account: - account_name = user.account.get("name") - if account_name: - macros["$(account)"] = account_name - if user.avatar: - avatar_gender = user.avatar.get("gender") - if avatar_gender: - macros["$(tpop)"] = pronouns[avatar_gender]["obj"] - macros["$(tppp)"] = pronouns[avatar_gender]["pos"] - macros["$(tpsp)"] = pronouns[avatar_gender]["sub"] - # find and replace per the macros dict macro_start = text.find("$(") if macro_start == -1: break macro_end = text.find(")", macro_start) + 1 - macro = text[macro_start:macro_end] + macro = text[macro_start+2:macro_end-1] if macro in macros.keys(): - text = text.replace(macro, macros[macro]) + replacement = macros[macro] + + # this is how we handle local file inclusion (dangerous!) + elif macro.startswith("inc:"): + incfile = path_join(universe.startdir, macro[4:]) + if exists(incfile): + incfd = file(incfile) + replacement = "" + for line in incfd: + if line.endswith("\n") and not line.endswith("\r\n"): + line = line.replace("\n", "\r\n") + replacement += line + # lose the trailing eol + replacement = replacement[:-2] + else: + replacement = "" + log("Couldn't read included " + incfile + " file.", 6) # if we get here, log and replace it with null else: - text = text.replace(macro, "") + replacement = "" if not is_input: log("Unexpected replacement macro " + macro + " encountered.", 6) + # and now we act on the replacement + text = text.replace("$(" + macro + ")", replacement) + # replace the look-like-a-macro sequence text = text.replace("$_(", "$(") @@ -1410,6 +1473,9 @@ def handle_user_input(user): # since we got input, flag that the menu/prompt needs to be redisplayed user.menu_seen = False + # update the last_input timestamp while we're at it + user.last_input = universe.get_time() + def generic_menu_handler(user): """A generic menu choice handler.""" -- 2.11.0