Imported from archive.
authorJeremy Stanley <fungi@yuggoth.org>
Tue, 10 Jun 2008 22:42:56 +0000 (22:42 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Tue, 10 Jun 2008 22:42:56 +0000 (22:42 +0000)
* (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.

13 files changed:
LICENSE
archetype
banner.txt [new file with mode: 0644]
command
example/index
example/second_square/index
example/second_square/location
example/second_square/prop
login.txt [new file with mode: 0644]
menu
mudpy
mudpy.conf
mudpy.py

diff --git a/LICENSE b/LICENSE
index c1ce24f..902f8d5 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,23 +1,13 @@
-Copyright (c) 2005, 2006 Jeremy Stanley <fungi@yuggoth.org>. 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 <fungi@yuggoth.org>
+
+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.
index ad43eba..fa370b7 100644 (file)
--- a/archetype
+++ b/archetype
@@ -1,3 +1,7 @@
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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 (file)
index 0000000..8d12d6d
--- /dev/null
@@ -0,0 +1 @@
+Example lobby menu...
diff --git a/command b/command
index c5499c9..3d722a5 100644 (file)
--- a/command
+++ b/command
@@ -1,3 +1,7 @@
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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 (<parameter> is required, [parameter] is optional, (note) is a note):$(eol)$(eol)   show categories (list all element category names)$(eol)   show category <category> (list the elements in a category)$(eol)   show element <element> (list facet definitions for an element)$(eol)   show file <filename> (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 <expression> (evaluates a python expression)$(eol)   show time (returns several current timer values)
+help = Here are the possible incantations (<parameter> is required, [option] is optional, (note) is a note):$(eol)$(eol)   show categories (list all element category names)$(eol)   show category <category> (list the elements in a category)$(eol)   show element <element> (list facet definitions for an element)$(eol)   show file <filename> (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 <expression> (evaluates a python expression)$(eol)   show time (returns several current timer values)
 
index 10773c7..4dc7c5f 100644 (file)
@@ -1,3 +1,7 @@
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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
index 2836fe9..0997a26 100644 (file)
@@ -1,3 +1,7 @@
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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
index 1623f47..0a14869 100644 (file)
@@ -1,3 +1,7 @@
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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
index 64d1fd8..e7fb24a 100644 (file)
@@ -1,3 +1,7 @@
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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 (file)
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 (file)
--- a/menu
+++ b/menu
@@ -1,3 +1,7 @@
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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 (executable)
--- a/mudpy
+++ b/mudpy
@@ -1,9 +1,9 @@
 #!/usr/bin/python
 """Skeletal executable for the mudpy engine."""
 
-# Copyright (c) 2005, 2006 Jeremy Stanley <fungi@yuggoth.org>. All rights
-# reserved. Licensed per terms in the LICENSE file distributed with this
-# software.
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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
index 8fa0876..310c307 100644 (file)
@@ -1,3 +1,7 @@
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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]
index 01b9142..699eae1 100644 (file)
--- a/mudpy.py
+++ b/mudpy.py
@@ -1,8 +1,8 @@
 """Core objects for the mudpy engine."""
 
-# Copyright (c) 2005, 2006 Jeremy Stanley <fungi@yuggoth.org>. All rights
-# reserved. Licensed per terms in the LICENSE file distributed with this
-# software.
+# Copyright (c) 2004-2008 Jeremy Stanley <fungi@yuggoth.org>. 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."""