changeset 533:38eaf9f14f3e

merge
author digitalxero
date Mon, 22 Mar 2010 18:38:22 -0600
parents 22942728c43c (current diff) 54d234728df4 (diff)
children e55283b9b2de
files orpg/dieroller/rollers/gurps.py orpg/dieroller/rollers/hero.py orpg/dieroller/rollers/runequest.py orpg/dieroller/rollers/savage.py orpg/dieroller/rollers/sr4.py orpg/networking/mplay_client.py
diffstat 75 files changed, 10326 insertions(+), 4928 deletions(-) [+]
line wrap: on
line diff
--- a/orpg/chat/chatwnd.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/chat/chatwnd.py	Mon Mar 22 18:38:22 2010 -0600
@@ -60,24 +60,24 @@
 
 from orpg.orpg_version import VERSION
 
-from orpg.gametree.nodehandlers.chatmacro import macro_handler
-from orpg.gametree.nodehandlers.forms import textctrl_handler
-from orpg.gametree.nodehandlers.rpg_grid import grid_row_handler
-import orpg.external.etree.ElementTree as ET
-
 # needed to only send typing/not_typing messages while connected
 from orpg.networking.mplay_client import MPLAY_CONNECTED
 
 from orpg.chat import commands, chat_msg, chat_util
+from orpg.gametree.nodehandlers.core import is_integer, node_handler
 
+from orpg.dieroller import roller_manager
 
 NEWCHAT = False
 try:
     import wx.webview
-    NEWCHAT = True
+    #NEWCHAT = True
 except:
     pass
 
+compiled_check_for_reference_regex = re.compile("(!@(.*?)@!)")
+compiled_simple_arithmetic_regex = re.compile("^([0-9,\+\-\(\) \*\/><=!]|max|min|if|else)+$")
+
 def log(text):
     filename = settings.get('GameLogPrefix')
     if filename and filename[0] != commands.ANTI_LOG_CHAR:
@@ -93,7 +93,6 @@
         with open(orpg.dirpath.dir_struct["user"] + filename, 'a') as f:
             f.write('<div class="post">%s%s</div>\n' % (header, text))
 
-
 class chat_html_window(wx.html.HtmlWindow):
     """
     This class displayes the chat information in html?
@@ -441,19 +440,20 @@
         return private_tab
 
     @debugging
+    def close_tab(self, tabid):
+        self.DeletePage(tabid)
+
+    @debugging
     def onCloseTab(self, evt):
         try:
             tabid = evt.GetSelection()
         except:
             tabid = self.GetSelection()
 
-        logger.debug(tabid, True)
-
         if self.GetPageText(tabid) == 'Main Room':
             #send no close error to chat
             evt.Veto()
             return
-
         if self.GetPageText(tabid) == 'GM':
             try:
                 msg = "Are You Sure You Want To Close This Page?"
@@ -480,6 +480,7 @@
         elif panel in self.null_tabs:
             self.null_tabs.remove(panel)
 
+
     @debugging
     def newMsg(self, tabid):
         if tabid != self.GetSelection():
@@ -525,9 +526,9 @@
        scroll_down(self)
        InfoPost(self,s)
        Post(self,s="",send=False,myself=False)
-       ParsePost(self,s,send=False,myself=False)
+       ParsePost(self,s,send=False,myself=False,context=ParserContext())
        ParseDice(self,s)
-       ParseNodes(self,s)
+       ParseNode(self,s,context)
        get_sha_checksum(self)
        get_color(self)
     """
@@ -546,8 +547,6 @@
         wx.Panel.__init__(self, parent, id)
         self.session = open_rpg.get_component('session')
         self.activeplugins = open_rpg.get_component('plugins')
-        self.gametree = None
-
         self.parent = parent
 
         # who receives outbound messages, either "all" or "playerid" string
@@ -555,9 +554,6 @@
         self.type = tab_type
         self.sound_player = open_rpg.get_component('sound')
 
-        # create die roller manager
-        self.DiceManager = open_rpg.get_component('DiceManager')
-
         # create rpghex tool
         self.r_h = orpg.tools.rgbhex.RGBHex()
         self.h = 0
@@ -613,6 +609,7 @@
         self.font = self.chatwnd.GetFont().GetFaceName()
         self.fontsize = self.chatwnd.GetFont().GetPointSize()
         self.scroll_down()
+        self.get_completion_word_set = self.get_player_set
 
     @debugging
     def set_default_font(self, fontname=None, fontsize=None):
@@ -845,6 +842,10 @@
             self.parent.create_gm_tab()
         else:
             settings.set("GMWhisperTab", '0')
+            for idx in xrange(self.parent.GetPageCount()):
+                if self.parent.GetPageText(idx) == 'GM':
+                    self.parent.close_tab(idx)
+                    return
 
     @debugging
     def OnMB_GroupWhisperTabs(self, event):
@@ -982,7 +983,6 @@
     def back_tabs(self, evt):
         self.parent.AdvanceSelection(False)
 
-
     @debugging
     def build_ctrls(self):
         self.chatwnd = chat_html_window(self,-1)
@@ -998,10 +998,10 @@
                                                   ' '.join(msg)))
 
         self.chattxt = orpg.tools.predTextCtrl.predTextCtrl(self, -1, "",
-                                            style=wx.TE_PROCESS_ENTER|
-                                            wx.TE_PROCESS_TAB,
+                                            style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB,
                                             keyHook=self.myKeyHook,
                                             validator=None)
+
         self.build_bar()
         self.basesizer = wx.BoxSizer(wx.VERTICAL)
         self.basesizer.Add(self.chatwnd,1,wx.EXPAND)
@@ -1040,6 +1040,7 @@
         self.chattxt.Bind(wx.EVT_MOUSEWHEEL, self.chatwnd.mouse_wheel)
         self.chattxt.Bind(wx.EVT_CHAR, self.chattxt.OnChar)
         self.chattxt.Bind(wx.EVT_TEXT_COPY, self.textCopy)
+        self.chattxt.Bind(wx.EVT_KEY_DOWN, self.on_chat_key_down)
 
     @debugging
     def textCopy(self, event):
@@ -1060,7 +1061,7 @@
             self.build_dice()
             self.build_scroll()
             self.build_text()
-            self.toolbar_sizer.Add( self.textpop_lock, 0, wx.EXPAND )
+            self.toolbar_sizer.Add(self.textpop_lock, 0, wx.EXPAND)
             self.toolbar_sizer.Add(self.scroll_lock,0,wx.EXPAND)
             self.build_formating()
             self.build_colorbutton()
@@ -1363,47 +1364,79 @@
         wx.CallAfter(self.chattxt.SetFocus)
 
     @debugging
+    def on_chat_key_down(self, event):
+        # pre-empt the ENTER key
+        if event.GetKeyCode() == wx.WXK_RETURN and not event.ShiftDown():
+            s = self.chattxt.GetValue()
+            self.set_colors()
+            if self.session.get_status() == MPLAY_CONNECTED:
+                self.sendTyping(0)
+            if len(s):
+                self.chattxt.SetValue("")
+                self.submit_chat_text(s)
+            return
+        event.Skip()
+
+    @debugging
+    def submit_chat_text(self, s):
+        self.histidx = -1
+        self.temptext = ""
+        self.history = [s] + self.history
+
+        # play sound
+        sound_file = settings.get("SendSound")
+        if sound_file != '':
+            self.sound_player.play(sound_file)
+
+        # get the alias as the context
+        alias = self.aliasList.GetStringSelection()
+        if alias == self.defaultAliasName:
+            alias = self.session.get_my_info()[0]
+        # check for the entire string being a reference without the !@...@! format
+        # this is a special case not checked for except for cases of one line direct user input
+        # through the chat view and also through the /all command
+        # doesn't work with nodes, whisper, emote, aliasLib entry, multi-line etc
+        handler = open_rpg.get_component('tree').get_handler_by_path(s, ParserContext(alias))
+        if handler is not None and handler.get_value() is not None:
+            s = "!@"+s+"@!"
+        self.ParsePost(s, True, True, ParserContext(alias))
+
+    def get_player_set(self):
+        players = set()
+        for key in self.session.players.keys():
+            striphtmltag = re.compile ('<[^>]+>*')
+            # the ": " was part of the original code
+            players.add(striphtmltag.sub("", self.session.players[key][0])+": ")
+        return players
+
+    def get_longest_common_prefix(self, string1, string2):
+        for i in range(len(string1)):
+            if string1[i].lower() != string2[i].lower():
+                return string1[:i]
+        return string1
+
+    @debugging
     def OnChar(self, event):
         s = self.chattxt.GetValue()
 
-        """
-        RETURN KEY (no matter if there is text in chattxt)
-        This section is run even if there is nothing in the chattxt
-        (as opposed to the next wx.WXK_RETURN handler
-        """
-        if event.GetKeyCode() == wx.WXK_RETURN:
-            self.set_colors()
-            if self.session.get_status() == MPLAY_CONNECTED:
-                self.sendTyping(0)
-
-        macroText = ""
         s_key = self.f_key_macros.get(event.GetKeyCode(), None)
         if s_key:
             macroText = settings.get_setting(s_key)
-
-        # Append to the existing typed text as needed and make sure the
-        # status doesn't change back.
-        if len(macroText):
-            self.sendTyping(0)
-            s = macroText
-
-        if (event.GetKeyCode() == wx.WXK_RETURN and len(s)) or len(macroText):
-            self.histidx = -1
-            self.temptext = ""
-            self.history = [s] + self.history
+            if len(macroText):
+                self.sendTyping(0)
+                # submit the text directly without disturbing any text in text ctrl
+                self.submit_chat_text(macroText)
 
-            if not len(macroText):
-                self.chattxt.SetValue("")
-
-            # play sound
-            sound_file = settings.get("SendSound")
-            if sound_file != '':
-                self.sound_player.play(sound_file)
-
-            if s[0] != "/": ## it's not a slash command
-                s = self.ParsePost(s, True, True)
+        elif event.GetKeyCode() == wx.WXK_RETURN and event.ShiftDown():
+            p = self.chattxt.GetInsertionPoint()
+            if os.linesep == "\r\n":
+                # with shift down, replace the special character with a simple newline
+                self.chattxt.SetValue(s[:p-1]+"\n"+s[p:])
+                self.chattxt.SetInsertionPoint(p)
             else:
-                self.chat_cmds.docmd(s)
+                # just insert a newline
+                self.chattxt.SetValue(s[:p]+"\n"+s[p:])
+                self.chattxt.SetInsertionPoint(p+1)
 
         elif event.GetKeyCode() == wx.WXK_UP:
             if self.histidx < len(self.history)-1:
@@ -1417,7 +1450,7 @@
                     self.temptext = self.chattxt.GetValue()
 
                 self.histidx += 1
-                self.chattxt.SetValue(self.history[self.histidx].strip('\n'))
+                self.chattxt.SetValue(self.history[self.histidx])
                 self.chattxt.SetInsertionPointEnd()
             else:
                 self.histidx = len(self.history) -1
@@ -1432,63 +1465,30 @@
                 if self.histidx is -1:
                     self.chattxt.SetValue(self.temptext)
                 else:
-                    self.chattxt.SetValue(self.history[self.histidx].strip('\n'))
+                    self.chattxt.SetValue(self.history[self.histidx])
                 self.chattxt.SetInsertionPointEnd()
             else:
                 self.histidx = -1
 
         elif  event.GetKeyCode() == wx.WXK_TAB:
             if s !="":
-                found = 0
-                nicks = []
-                testnick = ""
-                inlength = len(s)
-
-                for getnames in self.session.players.keys():
-                    striphtmltag = re.compile ('<[^>]+>*')
-                    testnick = striphtmltag.sub ("",
-                                            self.session.players[getnames][0])
-                    if string.lower(s) == string.lower(testnick[:inlength]):
-                        found = found + 1
-                        nicks[len(nicks):]=[testnick]
-
-                if found == 0: ## no nick match
-                    self.Post(self.colorize(self.syscolor,
-                                            " ** No match found"))
-
-                elif found > 1:
-                    nickstring = ""
-                    nicklist = []
-
-                    for foundnicks in nicks:
-                        nickstring = nickstring + foundnicks + ", "
-                        nicklist.append(foundnicks)
-                    nickstring = nickstring[:-2]
-                    self.Post(self.colorize(self.syscolor,
-                                " ** Multiple matches found: " + nickstring))
-
-                    # set text to the prefix match between first two matches
-                    settext = re.match(''.join(map(lambda x: '(%s?)' % x,
-                                                   string.lower(nicklist[0]))),
-                                       string.lower(nicklist[1])).group()
-
-                    # run through the rest of the nicks
-                    for i in nicklist:
-                        settext = re.match(''.join(map(lambda x: '(%s?)' % x,
-                                                       string.lower(i))),
-                                           string.lower(settext)).group()
-
-                    if settext:
-                        self.chattxt.SetValue(settext)
-                        self.chattxt.SetInsertionPointEnd()
-
+                candidate_words = self.get_completion_word_set()
+                words = []
+                s_lower = s.lower()
+                for word in candidate_words:
+                    if word.lower().startswith(s_lower):
+                        words.append(word)
+                if len(words) == 0: ## no nick match
+                    self.InfoPost(" ** No match found")
+                elif len(words) > 1:
+                    self.InfoPost(" ** Multiple matches found: " + ", ".join(words))
+                    self.chattxt.SetValue(reduce(self.get_longest_common_prefix, words))
+                    self.chattxt.SetInsertionPointEnd()
                 else:
-                    settext = nicks[0] + ": "
-                    self.chattxt.SetValue(settext)
+                    self.chattxt.SetValue(words[0])
                     self.chattxt.SetInsertionPointEnd()
             else:
-                self.Post(self.colorize(self.syscolor,
-                                        " ** That's the Tab key, Dave"))
+                self.InfoPost(" ** That's the Tab key, Dave")
 
         elif event.GetKeyCode() in (wx.WXK_PRIOR, wx.WXK_PAGEUP):
             self.chatwnd.ScrollPages(-1)
@@ -1704,12 +1704,25 @@
         return the_gms
 
     @debugging
-    def GetName(self):
+    def GetName(self, context=None):
         self.AliasLib = open_rpg.get_component('alias')
         player = self.session.get_my_info()
 
         if self.AliasLib != None:
-            self.AliasLib.alias = self.aliasList.GetStringSelection();
+            self.AliasLib.alias = self.aliasList.GetStringSelection()
+            if context:
+                if isinstance(context.namespace_hint, node_handler):
+                    namespace_name = open_rpg.get_component('tree').get_namespace_name(context.namespace_hint.mytree_node)
+                elif isinstance(context.namespace_hint, (str, unicode)):
+                    namespace_name = context.namespace_hint
+                else:
+                    namespace_name = None
+                if namespace_name:
+                    for n in xrange(self.AliasLib.selectAliasWnd.GetItemCount()):
+                        alias = self.AliasLib.selectAliasWnd.GetItem(n, 0).GetText()
+                        if alias.strip().lower() == namespace_name:
+                            self.AliasLib.alias = alias
+                            break
 
             if self.AliasLib.alias[0] != self.defaultAliasName:
                 return [self.chat_display_name([self.AliasLib.alias[0],
@@ -1720,7 +1733,7 @@
 
     @debugging
     def GetFilteredText(self, text):
-        advregex = re.compile('\"(.*?)\"', re.I)
+        advregex = re.compile('\"(.*?)\"', re.I|re.M)
         self.AliasLib = open_rpg.get_component('alias')
 
         if self.AliasLib != None:
@@ -1737,8 +1750,8 @@
         return text
 
     @debugging
-    def emote_message(self, text):
-        text = self.NormalizeParse(text)
+    def emote_message(self, text, context=ParserContext()):
+        text = self.NormalizeParse(text, context)
         text = self.colorize(self.emotecolor, text)
 
         if self.type == MAIN_TAB and self.sendtarget == 'all':
@@ -1765,11 +1778,11 @@
         self.EmotePost(text)
 
     @debugging
-    def whisper_to_players(self, text, player_ids):
+    def whisper_to_players(self, text, player_ids, context=ParserContext()):
         tabbed_whispers_p = settings.get("tabbedwhispers")
 
         # Heroman - apply any filtering selected
-        text = self.NormalizeParse(text)
+        text = self.NormalizeParse(text, context)
         player_names = ""
 
         # post to our chat before we colorize
@@ -2014,7 +2027,7 @@
         self.Post(self.colorize(self.emotecolor, s))
 
     @debugging
-    def Post(self, s="", send=False, myself=False):
+    def Post(self, s="", send=False, myself=False, context=None):
         strip_p = settings.get("striphtml")
         strip_img = settings.get("Show_Images_In_Chat")
 
@@ -2029,7 +2042,7 @@
         s = chat_util.strip_li_tags(s)
         s = chat_util.strip_body_tags(s)
         s = chat_util.strip_misalignment_tags(s)
-        aliasInfo = self.GetName()
+        aliasInfo = self.GetName(context)
         display_name = aliasInfo[0]
 
         if aliasInfo[1] != 'Default':
@@ -2147,24 +2160,28 @@
             return "[ERROR]"
 
     @debugging
-    def ParsePost(self, s, send=False, myself=False):
-        s = self.NormalizeParse(s)
+    def ParsePost(self, s, send=False, myself=False, context=ParserContext()):
+        s = self.NormalizeParse(s, context=context)
         self.set_colors()
-        self.Post(s,send,myself)
+        self.Post(s, send, myself, context)
 
     @debugging
-    def NormalizeParse(self, s):
+    def NormalizeParse(self, s, context):
         """
         plugin pre_parse hook
+        you must now supply a ParserContext
         """
         for plugin_fname in self.activeplugins.keys():
             plugin = self.activeplugins[plugin_fname]
             s = plugin.pre_parse(s)
 
         if self.parsed == 0:
-            s = self.ParseNode(s)
+            #replace common unicode with ascii
+            s = chat_util.strip_unicode(s)
+            s = self.ParseNode(s, context)
             s = self.ParseDice(s)
             s = self.ParseFilter(s)
+            s = s.replace("\n", "<br />")
             self.parsed = 1
 
         return s
@@ -2175,33 +2192,8 @@
         return s
 
     @debugging
-    def ParseNode(self, s):
-        if self.gametree is None:
-            self.gametree = open_rpg.get_component('tree')
-        """
-        Parses player input for embedded nodes rolls
-        """
-        cur_loc = 0
-        reg = re.compile("(!@([a-zA-Z0-9 _\-\./]+(::[a-zA-Z0-9 _\-\./]+)*)@!)")
-        matches = reg.findall(s)
-
-        for i in xrange(0,len(matches)):
-            path = matches[i][1]
-            handler = self.gametree.GetPyDataByPath(path)
-            if handler:
-                newstr = handler.get_value()
-                if newstr is not None:
-                    newstr = self.ParseNode(newstr)
-                    if newstr.isdigit() and path in self.gametree.bonuses:
-                        newint = int(newstr)
-                        for bonus in self.gametree.bonuses[path]:# a list of bonus_handler objects
-                            value = bonus.get_value()
-                            value = self.ParseNode(value)
-                            if value.isdigit():
-                                newint += int(value)
-                        newstr = str(newint)
-                    s = s.replace(matches[i][0], newstr, 1)
-        return s
+    def ParseNode(self, s, context):
+        return self.parse_multiline(s, context)
 
     @debugging
     def ParseDice(self, s):
@@ -2212,29 +2204,8 @@
         matches = reg.findall(s)
         for i in xrange(0,len(matches)):
             newstr = self.PraseUnknowns(matches[i])
-            qmode = 0
-            newstr1 = newstr
-            if newstr[0].lower() == 'q':
-                newstr = newstr[1:]
-                qmode = 1
-            try:
-                newstr = self.DiceManager.proccessRoll(newstr)
-            except:
-                pass
-            if qmode == 1:
-                off_roll = ["<!-- Official Roll [",
-                            newstr1,
-                            "] => ",
-                            newstr,
-                            "-->",
-                            newstr]
-                s = s.replace("[" + matches[i] + "]", ''.join(off_roll), 1)
-            else:
-                off_roll = ["[",
-                            newstr1,
-                            "<!-- Official Roll -->] => ",
-                            newstr]
-                s = s.replace("[" + matches[i] + "]", ''.join(off_roll), 1)
+            roll_result = roller_manager.process_roll(matches[i])
+            s = s.replace("[" + matches[i] + "]", ''.join(roll_result), 1)
         return s
 
     @debugging
@@ -2311,12 +2282,48 @@
 
         return rs
 
-##    @debugging
-##    def resolve_nodes(self, s):
-##        gametree = open_rpg.get_component('tree')
-##        handler = gametree.GetPyDataByPath(s)
-##        if handler:
-##            value = handler.get_value()
-##            if value is not None:
-##                return value
-##        return s
+    @debugging
+    def parse_multiline(self, multiline_string, context, perform_commands=True):
+        # /command is an alternative to a reference so never try to combine within one line
+        non_command_lines = []
+        context.lines = multiline_string.split("\n")
+        while len(context.lines):
+            line = context.lines.pop(0)
+            if line != '' and line[0] == "/":
+                if perform_commands:
+                    self.chat_cmds.docmd(line, context)
+            else:
+                non_command_lines.append(self.parse_line(line, context, perform_commands))
+        return "\n".join(non_command_lines)
+
+    @debugging
+    def parse_line(self, string, context, perform_commands=True):
+        """ don't call this; call parse_multiline """
+        matches = compiled_check_for_reference_regex.findall(string)
+        for reference in matches:
+            path = reference[1] #the bit between the !@ and @!
+            handler = open_rpg.get_component('tree').get_handler_by_path(path, context)
+            if handler:
+                value = handler.get_value()
+                if value is not None:
+                    value = self.parse_multiline(value, ParserContext(handler), perform_commands)
+                    value = self.handle_adding_bonuses(value, handler)
+                    string = string.replace(reference[0], value, 1)
+        # evaluate simple integer arithmetic 0-9 plus ( + - * / ) only if there was at least one reference
+        if matches and compiled_simple_arithmetic_regex.match(string):
+            try:
+                string = str(eval(string))
+            except:
+                pass
+        return string
+
+    @debugging
+    def contains_reference(self, string):
+        if settings.get('Use_Readonly_On') == '0':
+            return False
+        return compiled_check_for_reference_regex.search(string) is not None
+
+    @debugging
+    def handle_adding_bonuses(self, value, handler):
+        """ A no-op function.  I will override this function in a plugin while it's being thrashed out """
+        return value
--- a/orpg/chat/commands.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/chat/commands.py	Mon Mar 22 18:38:22 2010 -0600
@@ -13,12 +13,17 @@
 import orpg.orpg_version
 import orpg.orpg_windows
 
+from orpg.orpgCore import *
+
+from orpg.dieroller import roller_manager
 from orpg.external.etree.ElementTree import ElementTree, Element
 from orpg.external.etree.ElementTree import fromstring, tostring
 
 from orpg.tools.settings import settings
 from orpg.tools.decorators import debugging
 
+from wx import TextEntryDialog, ID_OK
+
 ##--------------------------------------------------------------
 ## dynamically loading module for extended developer commands
 ## allows developers to work on new chat commands without
@@ -35,6 +40,42 @@
 
 ANTI_LOG_CHAR = '!'
 
+keyword_value_pair_regex = re.compile("((\w+)\s*=\s*)?(((?P<quote>'|\")(.*?)(?P=quote))|([^\s='\"]*))(?=(\s|$))")
+
+# may be used in plugins
+def command_args_parser(cmdargs):
+    var_list = []
+    key_var_map = {}
+    while True:
+        cmdargs = cmdargs.strip()
+        match = keyword_value_pair_regex.match(cmdargs)
+        if match is None:
+            if cmdargs != '':
+                var_list.append(cmdargs)
+            break
+        keyword = match.group(2)
+        value = match.group(6)
+        if value == None:
+            value = match.group(7)
+
+        if keyword is None and value == '':
+            if cmdargs != '':
+                var_list.append(cmdargs)
+            break
+        elif keyword is None:
+            var_list.append(value)
+        elif keyword in key_var_map:
+            old_value = key_var_map[keyword]
+            if isinstance(old_value, str):
+                key_var_map[keyword] = [old_value, value]
+            else:
+                old_value.append(value)
+        else:
+            key_var_map[keyword] = value
+        cmdargs = cmdargs[len(match.group(0)):]
+
+    return var_list, key_var_map
+
 class chat_commands:
     @debugging
     def __init__(self, chat):
@@ -47,6 +88,7 @@
         self.defaultcmds()
         self.defaultcmdalias()
         self.previous_whisper = []
+        self.context = ParserContext() # set upon receiving each command
 
     @debugging
     def addcommand(self, cmd, function, helpmsg):
@@ -135,6 +177,15 @@
                         'This will clear the entire chat window')
         self.addcommand('/advfilter', self.on_filter,
                         'This will toggle the Advanced Filter')
+        self.addcommand('/addtonode', self.on_add,
+                        '[number pathname] Add an integer to the value of a node')
+        self.addcommand('/setnode', self.on_setnode,
+                        '[number pathname] Set the value of a node')
+        self.addcommand('/allaliases', self.on_all,
+                        'The following line of text is spoken by all aliases in the alias lib in turn.')
+        self.addcommand('/input', self.on_input,
+                        '[variable_name caption] '
+                        'Ask user for a string that is substituted for the variable name in subsequent lines.')
 
     @debugging
     def defaultcmdalias(self):
@@ -147,12 +198,13 @@
         self.addshortcmd('/date', '/time')
         self.addshortcmd('/desc', '/description')
         self.addshortcmd('/d', '/description')
+        self.addshortcmd('/all', '/allaliases')
 
         #This is just an example or a differant way the shorcmd can be used
         self.addshortcmd('/sleep', '/me falls asleep')
 
     @debugging
-    def docmd(self,text):
+    def docmd(self, text, context=ParserContext()):
         cmdsearch = text.split(None, 1)
         cmd = cmdsearch[0].lower()
         start = len(cmd)
@@ -160,9 +212,11 @@
         cmdargs = text[start+1:end]
 
         if self.cmdlist.has_key(cmd):
+            self.context = context # instead of passing as a param; keeps old command fns backwards compatible
             self.cmdlist[cmd]['function'](cmdargs)
+            self.context = None
         elif self.shortcmdlist.has_key(cmd):
-            self.docmd(self.shortcmdlist[cmd] + " " + cmdargs)
+            self.docmd(self.shortcmdlist[cmd] + " " + cmdargs, context)
         else:
             msg = "Sorry I don't know what %s is!" % (cmd)
             self.chat.InfoPost(msg)
@@ -284,17 +338,16 @@
     @debugging
     def on_dieroller(self, cmdargs):
         args = cmdargs.split(None, 1)
-        rm = self.chat.DiceManager
         try:
-            rm.setRoller(args[0])
+            roller_manager.roller = args[0]
             self.chat.SystemPost("You have changed your die roller to the "
                                  "<b>\"" + args[0] + "\"</b> roller.")
             settings.set('dieroller',args[0])
         except Exception, e:
             self.chat.InfoPost("Available die rollers: " +\
-                               str(rm.listRollers()))
+                               str(roller_manager.list()))
             self.chat.InfoPost("You are using the <b>\"" +\
-                               rm.getRoller() + "\"</b> die roller.")
+                               roller_manager.roller + "\"</b> die roller.")
 
     @debugging
     def on_ping(self, cmdargs):
@@ -401,7 +454,7 @@
                 for m in keys:
                     if m == name:
                         setting = cmdargs[split_name_from_data+1:].strip()
-                        settings.set(name,setting)
+                        settings.set(name, setting)
                         return_string = name + " changed to " + setting
                         self.chat.InfoPost(return_string)
                         self.session.set_name(settings.get("player"))
@@ -490,12 +543,16 @@
         role = role.lower()
         if role.lower() == "player" or role.lower() == "gm" or\
            role.lower() == "lurker":
-            if role.lower() == "player": role = "Player"
-            elif role.lower() == "gm":   role = "GM"
-            else: role = "Lurker"
+            if role.lower() == "player":
+                role = "Player"
+            elif role.lower() == "gm":
+                role = "GM"
+            else:
+                role = "Lurker"
+
             try:
                 role_pwd = self.session.orpgFrame_callback.password_manager\
-                         .GetPassword("admin",int(self.session.group_id))
+                         .GetPassword("admin", int(self.session.group_id))
                 if role_pwd != None:
                     for m in player_ids:
                         self.session.set_role(m.strip(),role,role_pwd)
@@ -518,7 +575,7 @@
 
         self.previous_whisper = player_ids
         mesg = cmdargs[delim+1:].strip()
-        self.chat.whisper_to_players(mesg,player_ids)
+        self.chat.whisper_to_players(mesg, player_ids, self.context)
 
     @debugging
     def on_groupwhisper(self, cmdargs):
@@ -651,9 +708,29 @@
 
     @debugging
     def on_description(self, cmdargs):
-        if len(cmdargs) <= 0:
+        if len(cmdargs) == 0:
             self.chat.InfoPost("**No description text to display." + str(delim))
             return
+        if self.context is not None and len(self.context.lines) > 0:
+            quoted = False
+            reachedEnd = False
+            if cmdargs[0] == '"':
+                quoted = True
+                if len(cmdargs)>1 and cmdargs[-1] == '"':
+                    reachedEnd = True
+                    cmdargs = cmdargs[1:-1] #remove quotes
+            remainingLines = []
+            for line in self.context.lines:
+                if not quoted and len(line) and line[0] == '/': #slash command
+                    reachedEnd = True
+                if reachedEnd:
+                    remainingLines.append(line)
+                else:
+                    cmdargs = cmdargs + '<br />' + line
+                if quoted and len(line) and line[-1] == '"':
+                    cmdargs = cmdargs[1:-1] #remove quotes
+                    reachedEnd = True
+            self.context.lines = remainingLines
         mesg = ['<table bgcolor="#c0c0c0" border="3" cellpadding="5" ',
                 'cellspacing="0" width="100%"><tr><td>',
                 '<font color="#000000">',
@@ -663,6 +740,93 @@
         self.chat.send_chat_message(''.join(mesg))
 
     @debugging
+    def on_add(self, cmdargs):
+        cmdargs.strip()
+        pos = cmdargs.find(" ")
+        try:
+            number = int(cmdargs[:pos])
+        except:
+            self.chat.InfoPost("First parameter must be an integer.")
+            return
+        path = cmdargs[pos:].strip()
+        tree = open_rpg.get_component('tree')
+        handler = tree.get_handler_by_path(path, self.context)
+        if handler is None:
+            self.chat.InfoPost("Second parameter must be an indexed node.")
+            return
+        value = handler.get_value()
+        try:
+            value = int(value)
+        except:
+            self.chat.InfoPost("Value of node must be an integer.")
+            return
+        handler.set_value(str(value+number))
+
+    @debugging
+    def on_setnode(self, cmdargs):
+        cmdargs.strip()
+        pos = cmdargs.find(" ")
+        try:
+            number = int(cmdargs[:pos])
+        except:
+            self.chat.InfoPost("First parameter must be an integer.")
+            return
+        path = cmdargs[pos:].strip()
+        tree = open_rpg.get_component('tree')
+        handler = tree.get_handler_by_path(path, self.context)
+        if handler is None:
+            self.chat.InfoPost("Second parameter must be an indexed node.")
+            return
+        value = handler.get_value()
+        try:
+            value = int(value)
+        except:
+            self.chat.InfoPost("Value of node must be an integer.")
+            return
+        handler.set_value(str(number))
+
+    @debugging
+    def on_all(self, cmdargs):
+        names = self.chat.aliasList.GetItems()
+        if len(names) == 1:
+            name = self.chat.session.get_my_info()[0]
+            handler = open_rpg.get_component('tree').get_handler_by_path(cmdargs, ParserContext(name))
+            if handler is not None and handler.get_value() is not None:
+                cmdargs = "!@"+cmdargs+"@!"
+            self.chat.ParsePost(cmdargs, True, True, ParserContext(name))
+        else:
+            old = self.chat.aliasList.GetStringSelection()
+            for name in names:
+                if name != self.chat.defaultAliasName:
+                    self.chat.aliasList.SetStringSelection(name) # for the colour
+                    handler = open_rpg.get_component('tree').get_handler_by_path(cmdargs, ParserContext(name))
+                    if handler is not None and handler.get_value() is not None:
+                        cmdargs = "!@"+cmdargs+"@!"
+                    self.chat.ParsePost(cmdargs, True, True, ParserContext(name))
+            self.chat.aliasList.SetStringSelection(old)
+
+    @debugging
+    def on_input(self, cmdargs):
+        if self.context is None or len(self.context.lines) == 0:
+            self.chat.InfoPost("/input only works in the context of multi-line text.")
+            return
+        parts = cmdargs.split(None, 1)
+        if len(parts) == 0:
+            self.chat.InfoPost("Must specify a variable name.")
+            return
+        variable = parts[0]
+        prompt = ""
+        if len(parts) > 1:
+            prompt = parts[1]
+        dlg = TextEntryDialog(self.chat, "", prompt)
+        if dlg.ShowModal() != ID_OK:
+            return
+        for i in range(len(self.context.lines)):
+            # must assign to context because this is all a reference
+            # 'for line in self.context:' would not work here!
+            self.context.lines[i] = self.context.lines[i].replace(variable, dlg.GetValue())
+
+    @debugging
     def invoke_tab(self, cmdargs):
         try:
             int(cmdargs)
--- a/orpg/dieroller/__init__.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/dieroller/__init__.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,4 @@
+__all__ = ['BaseRoller', 'die_rollers', 'roller_manager']
+
+from orpg.dieroller._base import BaseRoller, roller_manager, die_rollers
+import orpg.dieroller.rollers
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/_base.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,538 @@
+import string
+import random
+import traceback
+import re
+import time
+from collections import deque
+
+from orpg.external.pyparsing import (Word, alphas, Literal, CaselessLiteral,
+                       Combine, Optional, nums, Or, Forward, ZeroOrMore,
+                       StringEnd, alphanums, OneOrMore, Group, ParseException)
+
+from orpg.tools.orpg_log import logger
+from orpg.tools.decorators import deprecated
+
+__all__ = ['BaseRoller', 'die_rollers', 'roller_manager']
+
+class BaseRoller(object):
+    """
+    This is the base roller class that all rollers should derive from
+    In your custom roller you can
+    1) set your own `format_string`
+    2) change the fudge value by overriding the `fudge_roll` method
+    3) Add additional info to the roll result by overriding `build_extra`
+
+    It is possible to, but not reccomended
+    1) override the `roll` method, that gets the #d# strig and turns it into
+       a list and adds it to the parse tree.
+    2) override the `evaluate_roll` method. This turns the parse tree into
+       a numeric result. You really should not touch this. If you dont want
+       your numeric result just change your `format_string` and dont include
+       the {result} part. Then you will definitively need to override the
+       `build_extra` method to change your result string to your liking.
+    """
+    format_string = "{roll_string}{rolls} = ({result}) {extra}"
+    extra_string = ""
+
+    def fudge_roll(self, *args):
+        """
+        This changes then f in #df to the return result, which then gets
+        processed by the roll handler. By default it changes it to 15
+        You can feel free to change this in your custom roller
+        """
+        return 15
+
+    def build_extra(self):
+        """
+        You should override this method if you want to add extra info to a
+        roll result string
+        """
+        self.extra_string = ""
+
+    def roll(self, *args):
+        """
+        This processes the actual roll, you should not need to override this
+        """
+        roll_str, loc, tokens = args
+        roll_string = tokens[0]
+        num, sides = roll_string.split("d")
+        roll = Roll(roll_string, num, sides, [])
+        for i in range(roll.die):
+            roll.result.append(random.randint(1, roll.sides))
+
+        self.history.append(roll)
+
+    def evaluate_roll(self):
+        """
+        This processes the parse tree, you should not override this
+        unless you are SURE you know what you are doing
+        """
+        op = self.history.pop()
+        if isinstance(op, Roll):
+            self._rolls.append(op)
+            return int(op)
+        elif op in "+-*/^":
+            op1 = self.evaluate_roll()
+            op2 = self.evaluate_roll()
+            return self._opn[op](op2, op1)
+        else:
+            a = int(op)
+            b = float(op)
+            if a == b:
+                return a
+
+            return b
+
+    def __call__(self, roll_string):
+        """
+        This method takes a `roll_string` like [1d4] and does all the parsing
+        it will then call `evaluate_roll` to get the results and finally it
+        calls `format_result` to allow custom rollers to format their output
+        how they see fit.
+
+        This should be the last method you look at overriding, but if you need
+        very non standard rollers
+        """
+        self._rolls = []
+        self._quiet = False
+        self.extra_string = ""
+
+        self._bnf.die_pattern.parseString(roll_string)
+        self.build_extra()
+        self._result = self.evaluate_roll()
+        total_rolls = Roll("", 0, 0)
+        total_rolls.result.extend(
+            [[r for r in roll.result] for roll in self._rolls])
+        total_rolls.result.reverse()
+        if len(total_rolls.result) == 1:
+            total_rolls.result = total_rolls.result[0]
+        official = self._official.format(roll_string=roll_string,
+                                         rolls=total_rolls,
+                                         result=self.result)
+        if self.quiet:
+            roll_string = ""
+        else:
+            roll_string = "[{roll_string}] => ".format(
+                roll_string=roll_string)
+
+
+        total_rolls = Roll("", 0, 0)
+        total_rolls.result.extend(
+            [[r for r in roll] for roll in self._rolls])
+        total_rolls.result.reverse()
+        if len(total_rolls.result) == 1:
+            total_rolls.result = total_rolls.result[0]
+
+        return official + self.format_string.format(roll_string=roll_string,
+                                                    rolls=total_rolls,
+                                                    result=self.result,
+                                                    extra=self.extra_string)
+
+    def extraroll(self, roll):
+        """
+        Utility function to return another roll
+        """
+        return random.randint(1, roll.sides)
+
+    def extend_roll(self, roll, idx, value=None):
+        roll.modified = roll.modified or roll.result
+        c = roll.modified[idx]
+        new = value or self.extraroll(roll)
+        if isinstance(c, int):
+            roll.modified[idx] = Roll(roll.string, roll.die, roll.sides,
+                                      [c, new])
+        else:
+            roll.modified[idx].result.append(new)
+
+    #private methods DO NOT OVERRIDE these
+    _history = deque([])
+    _rolls = None
+    _result = None
+    _official = "<!-- Official Roll {roll_string} => {rolls!s} = ({result}) -->"
+    _opn = {"+": (lambda a,b: a + b),
+           "-": (lambda a,b: a - b),
+           "*": (lambda a,b: a * b),
+           "/": (lambda a,b: float(a) / float(b)),
+           "^": (lambda a,b: a ** b)}
+
+    def __new__(cls):
+        it = cls.__dict__.get("__it__")
+        if it is not None:
+            return it
+        cls.__it__ = it = object.__new__(cls)
+        it._bnf = BNF(it)
+        return it
+
+    def _do_function(self, *args):
+        """
+        This takes a method from the roller string and calls the
+        corisponding method on the roller. It will throw an exception
+        if the method does not exist.
+        """
+        roll_str, loc, tokens = args
+        func = getattr(self, tokens.name)
+        if callable(func):
+            if tokens.args[0]:
+                func(*tokens.args)
+            else:
+                func()
+
+    def _push(self, *args):
+        """
+        Used to build the parse tree
+        """
+        roll_str, loc, tokens = args
+        self.history.append(tokens[0])
+
+    def _get_quiet(self):
+        return self._quiet
+    def _set_quiet(self, *args):
+        self._quiet = True
+    quiet = property(_get_quiet)
+
+    def _get_rolls(self):
+        return self._rolls
+    rolls = property(_get_rolls)
+
+    def _get_history(self):
+        return self._history
+    history = property(_get_history)
+
+    def _get_result(self):
+        return self._result
+    result = property(_get_result)
+
+
+class RollerManager(object):
+    _rollers = {}
+    _roller = None
+
+    def __new__(cls):
+        it = cls.__dict__.get("__it__")
+        if it is not None:
+            return it
+        cls.__it__ = it = object.__new__(cls)
+        return it
+
+    def register(self, roller):
+        if not self._rollers.has_key(roller.name):
+            if not isinstance(roller, BaseRoller) and\
+               isinstance(roller(), BaseRoller):
+                roller = roller()
+            self._rollers[roller.name] = roller
+
+    def list(self):
+        return self._rollers.keys()
+
+    def process_roll(self, roll_string):
+        st = time.time()
+        try:
+            if isinstance(self._roller, BaseRoller):
+                return self._roller(roll_string)
+            else:
+                #Ok we are using an OLD roller, so have fun with eval and shit
+                return self.proccessRoll(roll_string)
+        except Exception:
+            logger.exception(traceback.format_exc())
+            return roll_string
+        finally:
+            print "Roll Time:", time.time()-st
+
+    def _get_roller(self):
+        return self._roller.name
+    def _set_roller(self, value):
+        if not isinstance(value, basestring):
+            raise TypeError("roller must be a string")
+
+        if value not in self._rollers:
+            logger.exception("Unknown Roller: {roller}.\n"\
+                           "    Setting the roller to std".format(roller=value))
+            value = 'std'
+
+        self._roller = self._rollers[value]
+    roller = property(_get_roller, _set_roller)
+
+    #All these Methods are depreciated and will go away soon
+    def __getitem__(self, name):
+        return self._rollers[name]
+
+    @deprecated("setRoller has been depreciated, use the roller property")
+    def setRoller(self, name):
+        self.roller = name
+
+    @deprecated("getRoller has been depreciated, use the roller property")
+    def getRoller(self):
+        return self.roller
+
+    @deprecated("listRollers has been depreciated, use the list method")
+    def listRollers(self):
+        return self.list()
+
+    @deprecated("stdDieToDClass has been depreciated, convert your roller to the new style")
+    def stdDieToDClass(self,match):
+        s = match.group(0)
+        (num,sides) = s.split('d')
+
+        if sides.strip().upper() == 'F':
+            sides = "'f'"
+
+        try:
+            if int(num) > 100 or int(sides) > 10000:
+                return None
+        except:
+            pass
+
+        ret = ['(', num.strip(), "**roller_manager['", self.roller, "'](",
+                sides.strip(), '))']
+        return ''.join(ret)
+
+    #  Use this to convert ndm-style (3d6) dice to d_base format
+    @deprecated("convertTheDieString has been depreciated, convert your roller to the new style")
+    def convertTheDieString(self,s):
+        reg = re.compile("(?:\d+|\([0-9\*/\-\+]+\))\s*[a-zA-Z]+\s*[\dFf]+")
+        (result, num_matches) = reg.subn(self.stdDieToDClass, s)
+        if num_matches == 0 or result is None:
+            try:
+                s2 = self.roller_class + "(0)." + s
+                test = eval(s2)
+                return s2
+            except:
+                pass
+        return result
+
+    @deprecated("proccessRoll has been depreciated, convert your roller to the new style")
+    def proccessRoll(self, s):
+        qmode = 0
+        newstr1 = s
+        if s[0].lower() == 'q':
+            s = s[1:]
+            qmode = 1
+        try:
+            s = str(eval(self.convertTheDieString(s)))
+        except Exception:
+            logger.exception(traceback.format_exc())
+
+        if qmode == 1:
+            off_roll = ["<!-- Official Roll [ ",
+                        newstr1,
+                        " ] => ",
+                        s,
+                        "-->",
+                        s]
+        else:
+            off_roll = ["[",
+                        newstr1,
+                        "<!-- Official Roll -->] => ",
+                        s]
+        return off_roll
+
+roller_manager = RollerManager()
+
+class DieRollers(object):
+    def __new__(cls):
+        it = cls.__dict__.get("__it__")
+        if it is not None:
+            return it
+        cls.__it__ = it = object.__new__(cls)
+        return it
+
+    @deprecated("Use roller_manager.list() instead")
+    def keys(self):
+        return roller_manager.list()
+
+    @deprecated("Use roller_manager.register instead")
+    def register(self, roller):
+        roller_manager.register(roller)
+
+    def __getitem__(self, roller_name):
+        raise NotImplemented
+
+    def __setitem__(self, *args):
+        raise AttributeError
+
+die_rollers = DieRollers()
+
+class Roll(object):
+    _string = ""
+    _die = 0
+    _sides = 0
+    _result = None
+    _modified = None
+
+    def __init__(self, roll_string, num_die, sides, result=None):
+        self.string = roll_string
+        self.die = num_die
+        self.sides = sides
+        self.result = result or []
+
+    def __int__(self):
+        if self._modified:
+            return sum(self._modified)
+
+        return sum(self._result)
+
+    def __float__(self):
+        return float(int(self))
+
+    def __str__(self):
+        out = ['[']
+        use = self._modified or self._result
+        out.extend([str(r) + ', ' for r in use])
+        out[-1] = out[-1].strip(', ')
+        out.append(']')
+
+        return ''.join(out)
+
+    def __add__(self, other):
+        return int(self) + other
+    __radd__ = __add__
+
+    def __mul__(self, by):
+        return int(self) * by
+    __rmul__ = __mul__
+
+    def __div__(self, by):
+        return int(self) / float(by)
+    def __rdiv__(self, by):
+        return float(by) / float(self)
+
+    def __sub__(self, other):
+        return int(self) - other
+    def __rsub__(self, other):
+        return other - int(self)
+
+    def __iter__(self):
+        loop = self._modified or self._result
+        for i in loop:
+            yield i
+
+    def __contains__(self, item):
+        check = self._modified or self._result
+        return item in check
+
+    def __len__(self):
+        check = self._modified or self._result
+        return len(check)
+
+    def sort(self, cmp=None, key=None, reverse=False):
+        self._modified = self._modified or self._result
+        self._modified.sort(cmp, key, reverse)
+
+    def _get_string(self):
+        return self._string
+    def _set_string(self, value):
+        if not isinstance(value, basestring):
+            raise TypeError("string must be a string")
+        self._string = value
+    string = property(_get_string, _set_string)
+
+    def _get_die(self):
+        return self._die
+    def _set_die(self, value):
+        if not isinstance(value, int):
+            try:
+                value = int(eval(value))
+            except ValueError:
+                raise TypeError("die must be an int")
+        self._die = value
+    die = property(_get_die, _set_die)
+
+    def _get_sides(self):
+        return self._sides
+    def _set_sides(self, value):
+        if not isinstance(value, int):
+            try:
+                value = int(eval(value))
+            except ValueError:
+                raise TypeError("die must be an int")
+        self._sides = value
+    sides = property(_get_sides, _set_sides)
+
+    def _get_result(self):
+        return self._result
+    def _set_result(self, value):
+        if not isinstance(value, (tuple, list)):
+            raise TypeError("result must be a list")
+        self._result = value
+    result = property(_get_result, _set_result)
+
+    def _get_modified(self):
+        return self._modified
+    def _set_modified(self, value):
+        if not isinstance(value, (list, type(None))):
+            raise TypeError("modified must be an instance or None")
+        self._modified = value
+    modified = property(_get_modified, _set_modified)
+
+class BNF(object):
+    def __init__(self, roller):
+        point = Literal('.')
+        plusorminus = Literal('+') | Literal('-')
+        number = Word(nums)
+        integer = Combine(Optional(
+            plusorminus) + number)
+        floatnumber = Combine(integer +\
+                                       Optional(point +\
+                                                Optional(number)))
+        plus = Literal( "+" )
+        minus = Literal( "-" )
+        mult = Literal( "*" )
+        div = Literal( "/" )
+        lpar = Literal( "(" )
+        rpar = Literal( ")" )
+        addop = plus | minus
+        multop = mult | div
+        expop = Literal( "^" )
+
+        die_expr = Forward()
+
+        func_name = Word(alphas, alphanums + '_').setResultsName("name")
+        func_arg = ZeroOrMore(Word(alphanums, nums))
+        func_args = Combine(func_arg +\
+                                     ZeroOrMore(ZeroOrMore(" ").suppress() +\
+                                                Literal(",") +\
+                                                ZeroOrMore(" ").suppress() +\
+                                                  func_arg)).\
+            setResultsName("args").setParseAction(self.build_args)
+        func_expr = Combine(point + func_name +\
+                                     lpar + func_args +\
+                                     rpar).setParseAction(roller._do_function)
+
+        roll_expr = Forward()
+
+        die_expr << Combine(ZeroOrMore(CaselessLiteral("q").\
+                                      setParseAction(roller._set_quiet).\
+                                      suppress()) +\
+                            ((lpar + roll_expr + rpar) | number)+\
+                           CaselessLiteral("d") +\
+                           (CaselessLiteral("f").\
+                            setParseAction(roller.fudge_roll) |\
+                            ((lpar + roll_expr + rpar) | number))).\
+            setParseAction(roller.roll)
+
+        atom = (
+            OneOrMore(die_expr + ZeroOrMore(func_expr)) |
+            (floatnumber | integer).setParseAction(roller._push) |
+            (lpar + roll_expr.suppress() + rpar)
+        )
+        factor = Forward()
+        factor << atom + ZeroOrMore((expop + factor).\
+                            setParseAction(roller._push))
+        term = factor + ZeroOrMore((multop + factor).\
+                            setParseAction(roller._push))
+        roll_expr << term + ZeroOrMore((addop + term).\
+                                setParseAction(roller._push))
+
+        self.die_pattern = roll_expr + StringEnd()
+
+    def build_args(self, *args):
+        roll_str, loc, tokens = args
+        ret = []
+        for arg in tokens[0].split(","):
+            try:
+                a = int(arg)
+                b = float(arg)
+                ret.append(a if a == b else b)
+            except ValueError:
+                ret.append(arg)
+        return [ret]
\ No newline at end of file
--- a/orpg/dieroller/base.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/dieroller/base.py	Mon Mar 22 18:38:22 2010 -0600
@@ -35,9 +35,13 @@
 import copy
 #import string
 
+from orpg.tools.decorators import deprecated
+from _base import die_rollers
+
 class die_base(UserList.UserList):
     name = None
 
+    @deprecated("Convert your die roller to the new style")
     def __init__(self,source = []):
         if isinstance(source, (int, float, basestring)):
             s = []
@@ -301,6 +305,7 @@
 ### di class to handle actual dice
 
 class di:
+    @deprecated("Convert your die roller to the new style")
     def __init__(self,sides,min=1):
         self.sides = sides
         self.history = None
@@ -436,30 +441,7 @@
         return self.history[:]
 
 class static_di(di):
+    @deprecated("Convert your die roller to the new style")
     def __init__(self,value):
         di.__init__(self,value,value)
-        self.set_value(value)
-
-class DieRollers(object):
-    _rollers = {}
-    def __new__(cls):
-        it = cls.__dict__.get("__it__")
-        if it is not None:
-            return it
-        cls.__it__ = it = object.__new__(cls)
-        return it
-
-    def keys(self):
-        return self._rollers.keys()
-
-    def register(self, roller):
-        if not self._rollers.has_key(roller.name):
-            self._rollers[roller.name] = roller
-
-    def __getitem__(self, roller_name):
-        return self._rollers.get(roller_name, None)
-
-    def __setitem__(self, *args):
-        raise AttributeError
-
-die_rollers = DieRollers()
\ No newline at end of file
+        self.set_value(value)
\ No newline at end of file
--- a/orpg/dieroller/rollers/__init__.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/dieroller/rollers/__init__.py	Mon Mar 22 18:38:22 2010 -0600
@@ -4,5 +4,5 @@
 rollers_path = __import__(rollers, {}, {}, ['orpg.dieroller.rollers'.split('.')[-1]]).__path__
 
 for roller in os.listdir(os.path.abspath(os.path.dirname(__file__))):
-    if roller.endswith('.py') and not roller.startswith('__'):
+    if roller.endswith('.py') and not roller.startswith('_'):
         __import__("%s.%s" % (rollers, roller.split('.')[0]))
\ No newline at end of file
--- a/orpg/dieroller/rollers/d20.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/dieroller/rollers/d20.py	Mon Mar 22 18:38:22 2010 -0600
@@ -17,99 +17,136 @@
 #   $Id: d20.py,v 1.9 2006/11/04 21:24:19 digitalxero Exp $
 #
 # Description: d20 die roller
-__version__ = "$Id: d20.py,v 1.9 2006/11/04 21:24:19 digitalxero Exp $"
-
 # d20 stands for "d20 system" not 20 sided die :)
 
-from std import std
-from orpg.dieroller.base import *
+from orpg.dieroller._base import roller_manager
+from std import StandardRoller
 
-class d20(std):
+class D20Roller(StandardRoller):
     name = "d20"
 
-    def __init__(self,source=[]):
-        std.__init__(self,source)
+    def _sucess(self, roll, unmodified, dc):
+        return (int(roll) >= dc or unmodified == 20) and unmodified != 1
 
-# these methods return new die objects for specific options
+    def attack(self, ac, mod=0, critical=20):
+        roll = self.history.pop()
+        roll.modified = roll.modified or roll.result
+        r = roll.modified[0]
+        if mod:
+            self.extend_roll(roll, 0, mod)
+
+        if self._sucess(roll, r, ac):
+            if int(roll) >= critical:
+                self.extra_string = "Critical "
+
+            self.extra_string += "Hit!"
+        else:
+            self.extra_string = "Miss!"
 
-    def attack(self,AC,mod,critical):
-        return d20attack(self,AC,mod,critical)
+        self.history.append(roll)
+
+    def dc(self, dc, mod=0):
+        roll = self.history.pop()
+        roll.modified = roll.modified or roll.result
+        r = roll.modified[0]
+        if mod:
+            self.extend_roll(roll, 0, mod)
 
-    def dc(self,DC,mod):
-        return d20dc(self,DC,mod)
+        if self._sucess(roll, r, dc):
+            self.extra_string = "Success!"
+        else:
+            self.extra_string = "Failure!"
 
-die_rollers.register(d20)
+        self.history.append(roll)
+
+roller_manager.register(D20Roller)
+
+#class d20(std):
+    #name = "old_d20"
 
-class d20dc(std):
-    def __init__(self,source=[],DC=10,mod=0):
-        std.__init__(self,source)
-        self.DC = DC
-        self.mod = mod
-        self.append(static_di(mod))
+    #def __init__(self,source=[]):
+        #std.__init__(self,source)
+
+## these methods return new die objects for specific options
 
-    def is_success(self):
-        return ((self.sum() >= self.DC or self.data[0] == 20) and self.data[0] != 1)
+    #def attack(self,AC,mod,critical):
+        #return d20attack(self,AC,mod,critical)
+
+    #def dc(self,DC,mod):
+        #return d20dc(self,DC,mod)
+
+#roller_manager.register(d20)
 
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr += "] = (" + str(self.sum()) + ")"
+#class d20dc(std):
+    #def __init__(self,source=[],DC=10,mod=0):
+        #std.__init__(self,source)
+        #self.DC = DC
+        #self.mod = mod
+        #self.append(static_di(mod))
+
+    #def is_success(self):
+        #return ((self.sum() >= self.DC or self.data[0] == 20) and self.data[0] != 1)
 
-        myStr += " vs DC " + str(self.DC)
+    #def __str__(self):
+        #myStr = "[" + str(self.data[0])
+        #for a in self.data[1:]:
+            #myStr += ","
+            #myStr += str(a)
+        #myStr += "] = (" + str(self.sum()) + ")"
 
-        if self.is_success():
-            myStr += " Success!"
-        else:
-            myStr += " Failure!"
+        #myStr += " vs DC " + str(self.DC)
 
-        return myStr
+        #if self.is_success():
+            #myStr += " Success!"
+        #else:
+            #myStr += " Failure!"
+
+        #return myStr
 
 
-class d20attack(std):
-    def __init__(self,source=[],AC=10,mod=0,critical=20):
-        std.__init__(self,source)
-        self.mod = mod
-        self.critical = critical
-        self.AC = AC
-        self.append(static_di(mod))
-        self.critical_check()
+#class d20attack(std):
+    #def __init__(self,source=[],AC=10,mod=0,critical=20):
+        #std.__init__(self,source)
+        #self.mod = mod
+        #self.critical = critical
+        #self.AC = AC
+        #self.append(static_di(mod))
+        #self.critical_check()
 
-    def attack(AC=10,mod=0,critical=20):
-        self.mod = mod
-        self.critical = critical
-        self.AC = AC
+    #def attack(AC=10,mod=0,critical=20):
+        #self.mod = mod
+        #self.critical = critical
+        #self.AC = AC
 
-    def critical_check(self):
-        self.critical_result = 0
-        self.critical_roll = 0
-        if self.data[0] >= self.critical and self.is_hit():
-            self.critical_roll = die_base(20) + self.mod
-            if self.critical_roll.sum() >= self.AC:
-                self.critical_result = 1
+    #def critical_check(self):
+        #self.critical_result = 0
+        #self.critical_roll = 0
+        #if self.data[0] >= self.critical and self.is_hit():
+            #self.critical_roll = die_base(20) + self.mod
+            #if self.critical_roll.sum() >= self.AC:
+                #self.critical_result = 1
 
-    def is_critical(self):
-        return self.critical_result
+    #def is_critical(self):
+        #return self.critical_result
 
-    def is_hit(self):
-        return ((self.sum() >= self.AC or self.data[0] == 20) and self.data[0] != 1)
+    #def is_hit(self):
+        #return ((self.sum() >= self.AC or self.data[0] == 20) and self.data[0] != 1)
 
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr += "] = (" + str(self.sum()) + ")"
+    #def __str__(self):
+        #myStr = "[" + str(self.data[0])
+        #for a in self.data[1:]:
+            #myStr += ","
+            #myStr += str(a)
+        #myStr += "] = (" + str(self.sum()) + ")"
 
-        myStr += " vs AC " + str(self.AC)
+        #myStr += " vs AC " + str(self.AC)
 
-        if self.is_critical():
-            myStr += " Critical"
+        #if self.is_critical():
+            #myStr += " Critical"
 
-        if self.is_hit():
-            myStr += " Hit!"
-        else:
-            myStr += " Miss!"
+        #if self.is_hit():
+            #myStr += " Hit!"
+        #else:
+            #myStr += " Miss!"
 
-        return myStr
+        #return myStr
--- a/orpg/dieroller/rollers/gurps.py	Tue Oct 06 16:03:42 2009 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,697 +0,0 @@
-#This program is free software; you can redistribute it and/or
-#modify it under the terms of the GNU General Public License
-#as published by the Free Software Foundation; either version 2
-#of the License, or (at your option) any later version.
-#
-#This program is distributed in the hope that it will be useful,
-#but WITHOUT ANY WARRANTY; without even the implied warranty of
-#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#GNU General Public License for more details.
-#
-#You should have received a copy of the GNU General Public License
-#along with this program; if not, write to the Free Software
-#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-# --
-#
-# File: gurps.py
-# Version:
-#   $Id: gurps.py,v 1.3
-#
-# Description: Modified Hero System die roller based on DJM and Heroman's Hero
-# dieroller
-#
-# GURPS is a trademark of Steve Jackson Games, and its rules and art are
-# copyrighted by Steve Jackson Games. All rights are reserved by Steve Jackson
-# Games. This game aid is the original creation of Naryt with help from Pyrandon
-# and is released for free distribution, and not for resale, under the
-# permissions granted in the Steve Jackson Games Online Policy.
-# http://www.sjgames.com/general/online_policy.html
-#
-# Errors should be reported to rpg@ormonds.net
-#
-# Changelog:
-# V 1.3  2007/03/23  Thomas M. Edwards <tmedwards@motoslave.net>
-#   Fixed gurpsskill, gurpsdefaultskill, and gurpssupernatural to correctly
-#   return a normal failure when the roll is 17 and the effective skill is 27+;
-#   previously, they would erroneously return a critical failure.  This fix also
-#   corrects the less serious issue whereby rolls of 17 and an effective skill
-#   of 17-26 would report "failure by X" instead of merely "failure", which is
-#   wrong as the only reason the roll failed was because a 17 was rolled, not
-#   because the roll exceeded the effective skill.
-# V 1.2 29 October 2006, added defaultskill (Rule of 20 [B344]), supernatural
-#   (Rule of 16 [B349]).  The frightcheck roll is now the actual Fright Check
-#   (with Rule of 14 [B360]) and a lookup oon the Fright Check Table if needed.
-#   The fightcheckfail roll is the old Fright Check Table lookup.
-#   Removes the Help roller as it was nothing but trouble, see
-#   http://openrpg.wrathof.com/repository/GURPS/GURPS_Roller_1.7.xml for help
-#   in using this roller.
-# V 1 Original gurps release 2006/05/28 00:00:00, modified crit_hit, crit_headblow, crit_miss, crit_unarm, spellfail, frightcheck and help_me
-#       Corrects numerous descriptions
-# v.1 original gurps release by Naryt 2005/10/17 16:34:00
-
-from time import time, clock
-import random
-
-from std import std
-from orpg.dieroller.base import *
-
-
-__version__ = "$Id: gurps.py,v 1.5 2007/05/06 16:42:55 digitalxero Exp $"
-
-# gurps
-
-class gurps(std):
-    name = "gurps"
-
-    def __init__(self,source=[]):
-        std.__init__(self,source)
-
-# these methods return new die objects for specific options
-
-# Original msk roll renamed to be easier to understand/remember
-    def skill(self,skill,mod):
-        return gurpsskill(self,skill,mod)
-
-    def defaultskill(self,stat,defaultlevel,mod):
-        return gurpsdefaultskill(self,stat,defaultlevel,mod)
-
-    def supernatural(self,skill,resistance,mod):
-        return gurpssupernatural(self,skill,resistance,mod)
-
-    def crit_hit(self):
-        return gurpscrit_hit(self)
-
-    def crit_headblow(self):
-        return gurpscrit_headblow(self)
-
-    def crit_miss(self):
-        return gurpscrit_miss(self)
-
-    def crit_unarm(self):
-        return gurpscrit_unarm(self)
-
-    def spellfail(self):
-        return gurpsspellfail(self)
-
-    def frightcheck(self,level,mod):
-        return gurpsfrightcheck(self,level,mod)
-
-    def frightcheckfail(self,mod):
-        return gurpsfrightcheckfail(self,mod)
-
-die_rollers.register(gurps)
-
-class gurpsskill(std):
-    def __init__(self,source=[],skill=0,mod=0):
-        std.__init__(self,source)
-        self.skill = skill
-        self.mod = mod
-
-    def is_success(self):
-        return (((self.sum()) <= self.skill+self.mod) and (self.sum() < 17))
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr +="]"
-        myStr += " = <b>" + str(self.sum()) + "</b>"
-        myStr += " vs <b>(" + str(self.skill+self.mod) + ")</b>"
-
-        Diff = abs((self.skill+self.mod) - self.sum())
-
-        if self.is_success():
-            if self.sum() == 3 or self.sum() == 4:
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b></font> [B556]"
-            elif self.sum() == 5 and (self.skill+self.mod > 14):
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +" </font> [B556]"
-            elif self.sum() == 6 and (self.skill+self.mod > 15):
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +" </font> [B556]"
-            else:
-                myStr += " or less <font color='#ff0000'><b>Success!</b> by " + str(Diff) +" </font>"
-        else:
-            if self.sum() == 18:
-                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
-#            elif self.sum() == 17 and (self.skill+self.mod < 16):
-#                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
-            elif self.sum() == 17:
-                if (self.skill+self.mod) < 16:
-                    myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
-                else:
-                    myStr += " or less <font color='#ff0000'><b>Failure!</b></font> [B556]"
-            elif  Diff > 9:
-                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b> by " + str(Diff) +" </font> [B556]"
-            else:
-                myStr += " or less <font color='#ff0000'><b>Failure!</b> by " + str(Diff) +" </font>"
-
-        return myStr
-
-class gurpsdefaultskill(std):
-    def __init__(self,source=[],stat=0,defaultlevel=0,mod=0):
-        std.__init__(self,source)
-        self.stat = stat
-        self.defaultlevel = defaultlevel
-        self.mod = mod
-
-    def is_success(self):
-        if self.stat < 21:
-            intSkillVal = self.stat + self.defaultlevel + self.mod
-        else:
-            intSkillVal = 20 + self.defaultlevel + self.mod
-        return (((self.sum()) <= intSkillVal) and (self.sum() < 17))
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr +="]"
-        myStr += " = <b>" + str(self.sum()) + "</b>"
-        strRule = ""
-        if self.stat < 21:
-            intSkillVal = self.stat + self.defaultlevel + self.mod
-        else:
-            intSkillVal = 20 + self.defaultlevel + self.mod
-            strRule = "<br />Rule of 20 in effect [B173, B344]"
-
-        myStr += " vs <b>(" + str(intSkillVal) + ")</b>"
-
-        Diff = abs((intSkillVal) - self.sum())
-
-        if self.is_success():
-            if self.sum() == 3 or self.sum() == 4:
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b></font> [B556]"
-            elif self.sum() == 5 and (intSkillVal > 14):
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +"</font> [B556]"
-            elif self.sum() == 6 and (intSkillVal > 15):
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +"</font> [B556]"
-            else:
-                myStr += " or less <font color='#ff0000'><b>Success!</b> by " + str(Diff) +"</font>"
-        else:
-            if self.sum() == 18:
-                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
-            elif self.sum() == 17:
-                if intSkillVal < 16:
-                    myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
-                else:
-                    myStr += " or less <font color='#ff0000'><b>Failure!</b></font> [B556]"
-            elif  Diff > 9:
-                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b> by " + str(Diff) +"</font> [B556]"
-            else:
-                myStr += " or less <font color='#ff0000'><b>Failure!</b> by " + str(Diff) +"</font>"
-
-        myStr += strRule
-        return myStr
-
-class gurpssupernatural(std):
-    def __init__(self,source=[],skill=0,resistance=0,mod=0):
-        std.__init__(self,source)
-        self.skill = skill
-        self.resistance = resistance
-        self.mod = mod
-
-    def is_success(self):
-        if self.skill+self.mod > 16:
-            if self.resistance > 16:
-                if self.resistance > self.skill+self.mod:
-                    newSkill = self.skill+self.mod
-                else:
-                    newSkill = self.resistance
-            else:
-                newSkill = 16
-        else:
-            newSkill = self.skill+self.mod
-        return (((self.sum()) <= newSkill) and (self.sum() < 17))
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr +="]"
-        myStr += " = <b>" + str(self.sum()) + "</b>"
-        strRule = ""
-        if self.skill+self.mod > 16:
-            if self.resistance > 16:
-                if self.resistance > self.skill+self.mod:
-                    newSkill = self.skill+self.mod
-                    strRule = "<br />Rule of 16:  Subject's Resistance is higher than skill, no change in skill [B349]"
-                else:
-                    newSkill = self.resistance
-                    strRule = "<br />Rule of 16:  Effective skill limited by subject's Resistance [B349]"
-            else:
-                newSkill = 16
-                strRule = "<br />Rule of 16:  Effective skill limited to 16 [B349]"
-        else:
-            newSkill = self.skill+self.mod
-        myStr += " vs <b>(" + str(newSkill) + ")</b>"
-
-        Diff = abs((newSkill) - self.sum())
-
-        if self.is_success():
-            if self.sum() == 3 or self.sum() == 4:
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b></font> [B556]"
-            elif self.sum() == 5 and (newSkill > 14):
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +" </font> [B556]"
-            elif self.sum() == 6 and (newSkill > 15):
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +" </font> [B556]"
-            else:
-                myStr += " or less <font color='#ff0000'><b>Success!</b> by " + str(Diff) +" </font>"
-        else:
-            if self.sum() == 18:
-                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
-            elif self.sum() == 17:
-                if newSkill < 16:
-                    myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
-                else:
-                    myStr += " or less <font color='#ff0000'><b>Failure!</b></font> [B556]"
-            elif  Diff > 9:
-                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b> by " + str(Diff) +" </font> [B556]"
-            else:
-                myStr += " or less <font color='#ff0000'><b>Failure!</b> by " + str(Diff) +" </font>"
-
-        myStr += strRule
-        return myStr
-
-class gurpscrit_hit(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0]) #Variable myStr holds text and first we put a [ into it and then adds the first die rolled
-        for a in self.data[1:]:             #This is a for loop.  It will do the next two lines of code for every die (except the first die which we handled in the line above) in the roll.
-            myStr += ","                  #Adds a comma after each die
-            myStr += str(a)           #Adds the value of each die.
-        myStr += "] = "                 #Adds ] = to the end of the string (note the += means append to whatever is already stored in the variable
-        myStr += str(self.sum())          #Finally we add the actual result of the roll and myStr contains something like [3,2,1] = 6
-
-        if self.sum() > 8 and self.sum() < 12:
-            myStr += " <font color='#ff0000'>The blow inflicts normal damage.</font> [B556]"
-        elif self.sum() == 12:
-            myStr += " <font color='#ff0000'>The blow inflicts normal damage, AND victim drops anything they hold--even if no damage penetrates DR.</font> [B556]"
-        elif self.sum() == 8:
-            myStr += " <font color='#ff0000'>Damage penetrating DR does double shock (-8 max) AND if it hits the victim's limb, it's crippled for 16-HT seconds (unless wound is enough to cripple permanently!).</font> [B556]"
-        elif self.sum() == 13 or self.sum() == 14 or self.sum() == 7:
-            myStr += " <font color='#ff0000'>If any damage penetrates DR, treat as major wound. See [B420] for major wounds.</font> [B556]"
-        elif self.sum() == 6 or self.sum() == 15:
-            myStr += " <font color='#ff0000'>The blow inflicts maximum normal damage.</font> [B556]"
-        elif self.sum() == 5 or self.sum() == 16:
-            myStr += " <font color='#ff0000'>The blow inflicts double damage.</font> [B556]"
-        elif self.sum() == 4 or self.sum() == 17:
-            myStr += " <font color='#ff0000'>The victim's DR protects at half value, rounded down, after applying any armor divisors.</font> [B556]"
-        elif self.sum() == 3 or self.sum() == 18 :
-            myStr += " <font color='#ff0000'>The blow inflicts triple damage.</font> [B556]"
-
-        return myStr
-
-class gurpscrit_headblow(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0]) #Variable myStr holds text and first we put a [ into it and then adds the first die rolled
-        for a in self.data[1:]:             #This is a for loop.  It will do the next two lines of code for every die (except the first die which we handled in the line above) in the roll.
-            myStr += ","                  #Adds a comma after each die
-            myStr += str(a)           #Adds the value of each die.
-        myStr += "] = "                 #Adds ] = to the end of the string (note the += means append to whatever is already stored in the variable
-        myStr += str(self.sum())          #Finally we add the actual result of the roll and myStr contains something like [3,2,1] = 6
-
-        if self.sum() > 8 and self.sum() < 12:
-            myStr += " <font color='#ff0000'>The blow inflicts normal damage.</font> [B556]"
-        elif self.sum() == 12 or self.sum() == 13:
-            myStr += " <font color='#ff0000'>Normal damage to the head, BUT if any penetrates DR victim is scarred (-1 to appearance, -2 if burning or corrosive attacks) OR, if <i>crushing</i> then victim deafened [see B422 for duration].</font> [B556]"
-        elif self.sum() == 8:
-            myStr += " <font color='#ff0000'>Normal damage to head, but victim knocked off balance: must Do Nothing until next turn (but can defend).</font> [B556]"
-        elif self.sum() == 14:
-            myStr += " <font color='#ff0000'>Normal damage to head, but victim drops their weapon.  If holding two weapons, roll randomly for which one is dropped.</font> [B556]"
-        elif self.sum() == 6 or self.sum() == 7:
-            myStr += " <font color='#ff0000'>If you aimed for face or skull, you hit an eye [B399];  otherwise, DR only half effective & if even 1 point damage penetrates it's a major wound [B420].  If you hit an eye and that should be impossible, treat as if a <b>4</b> were rolled, see [B556].</font> [B556]"
-        elif self.sum() == 15:
-            myStr += " <font color='#ff0000'>The blow inflicts maximum normal damage.</font> [B556]"
-        elif self.sum() == 16:
-            myStr += " <font color='#ff0000'>The blow inflicts double damage.</font> [B556]"
-        elif self.sum() == 4 or self.sum() == 5:
-            myStr += " <font color='#ff0000'>The victim's DR protects at half value, rounded up, after applying armor divisors AND if even 1 point penetrates it's a major wound [B420].</font> [B556]"
-        elif self.sum() == 17:
-            myStr += " <font color='#ff0000'>The victim's DR protects at half value, rounded up, after applying any armor divisors.</font> [B556]"
-        elif self.sum() == 3:
-            myStr += " <font color='#ff0000'>The blow inflicts maximum normal damage AND ignores all DR.</font> [B556]"
-        elif self.sum() == 18:
-            myStr += " <font color='#ff0000'>The blow inflicts triple damage.</font> [B556]"
-
-        return myStr
-
-class gurpscrit_miss(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0]) #Variable myStr holds text and first we put a [ into it and then adds the first die rolled
-        for a in self.data[1:]:             #This is a for loop.  It will do the next two lines of code for every die (except the first die which we handled in the line above) in the roll.
-            myStr += ","                  #Adds a comma after each die
-            myStr += str(a)           #Adds the value of each die.
-        myStr += "] = "                 #Adds ] = to the end of the string (note the += means append to whatever is already stored in the variable
-        myStr += str(self.sum())          #Finally we add the actual result of the roll and myStr contains something like [3,2,1] = 6
-
-        if self.sum() > 8 and self.sum() < 12:
-            myStr += " <font color='#ff0000'>You drop your weapon (& a <i>cheap</i> weapon breaks).</font> [B556]"
-        elif self.sum() == 12 or self.sum() == 8:
-            myStr += " <font color='#ff0000'>Your weapon turns in your hand;  must Ready it before it can be used again.</font> [B556]"
-        elif self.sum() == 13 or self.sum() == 7:
-            myStr += " <font color='#ff0000'>You lose your balance & can do nothing else (not even free actions) until next turn;  all defenses -2 until next turn.</font> [B556]"
-        elif self.sum() == 14:
-            yrdStr = str(int(random.uniform(1,7)))
-            myStr += " <font color='#ff0000'>A <i>swung</i> weapon flies from hand " + yrdStr + " yards (50% chance straight forward/backward) anyone on the target of the flying weapon makes a DX roll or takes half-damage; a <i>thrust</i> or <i>ranged</i> weapon is dropped (& a <i>cheap</i> weapon breaks).</font> [B556]"
-        elif self.sum() == 6:
-            myStr += " <font color='#ff0000'>You hit yourself in arm or leg (50/50 chance), doing half damage;  if impaling, piercing, or ranged attack, then roll again (if you hit yourself again then use that result).</font> [B556]"
-        elif self.sum() == 15:
-            myStr += " <font color='#ff0000'>You strain your shoulder!  Weapon arm crippled for 30 min;  do not drop weapon, but that arm is useless.</font> [B557]"
-        elif self.sum() == 16:
-            myStr += " <font color='#ff0000'>If <i>melee attack,</i>  you fall down!  If <i>ranged attack,</i> you lose your balance & can do nothing until next turn & all defenses -2 until next turn.</font> [B557]"
-        elif self.sum() == 5:
-            myStr += " <font color='#ff0000'>You hit yourself in the arm or leg (50/50 chance), doing normal damage;  if impaling, piercing, or ranged attack, then roll again (if you hit yourself again then use that result).</font> [B556]"
-        elif self.sum() == 4 or self.sum() == 3 or self.sum() == 17 or self.sum() == 18:
-            broke = int(random.uniform(3,19))
-            if broke >=5 and broke <=16:
-                brokestr = "it is dropped."
-            else:
-                brokestr = "the weapon also breaks!"
-            myStr += " <font color='#ff0000'>A normal weapon breaks [B485];  if solid crushing weapon OR fine, very fine, or magical weapon " + brokestr + "</font> [B556] Note, second for roll non-normal weapons already fingured into this result."
-
-        return myStr
-
-class gurpscrit_unarm(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0]) #Variable myStr holds text and first we put a [ into it and then adds the first die rolled
-        for a in self.data[1:]:             #This is a for loop.  It will do the next two lines of code for every die (except the first die which we handled in the line above) in the roll.
-            myStr += ","                  #Adds a comma after each die
-            myStr += str(a)           #Adds the value of each die.
-        myStr += "] = "                 #Adds ] = to the end of the string (note the += means append to whatever is already stored in the variable
-        myStr += str(self.sum())          #Finally we add the actual result of the roll and myStr contains something like [3,2,1] = 6
-
-        if self.sum() > 8 and self.sum() < 12:
-            myStr += " <font color='#ff0000'>You lose your balance;  you can do nothing else (not even free actions) until next turn, and all defenses -2 until next turn.</font> [B557]"
-        elif self.sum() == 12:
-            myStr += " <font color='#ff0000'>You trip; make a DX roll to avoid falling at -4 if kicking or twice the normal penatly for a technique that normally requires a DX to avoid injury on even a normal failure (e.g., Jump Kick).</font> [B557]"
-        elif self.sum() == 8:
-            myStr += " <font color='#ff0000'>You fall down!</font> [B557]"
-        elif self.sum() == 13:
-            myStr += " <font color='#ff0000'>You drop your guard:  all defenses -2 for the next turn & any Evaluate bonus or Feint penalties against you are doubled.  This is obvious to those around you.</font> [B557]"
-        elif self.sum() == 7 or self.sum() == 14:
-            myStr += " <font color='#ff0000'>You stumble:  <i>If attacking,</i> you advance one yard past opponent with them behind you (you are facing away); <i>if parrying</i> you fall down!</font> [B557]"
-        elif self.sum() == 15:
-            mslStr = str(int(random.uniform(1,4)))
-            myStr += " <font color='#ff0000'>You tear a muscle; " + mslStr + " HT damage to the limb used to attack (to one limb if two used to attack), & -3 to use it (-1 w/high pain thresh); also all atacks & defenses -1 until next turn.  If neck was injured -3 (-1 w/high pain thresh) applies to ALL actions.</font> [B557]"
-        elif self.sum() == 6:
-            myStr += " <font color='#ff0000'>You hit a solid object (wall, floor, etc.) & take crushing damage equalt to 1/2 of (your thrusting damage - your DR) (<i>EXCEPTION:</i> If attacking with natural weapons, such as claws or teeth, they <i>break</i> -1 damage on future attacks until you heal (for recovery, B422).</font> [B557]"
-        elif self.sum() == 5 or self.sum() == 16:
-            myStr += " <font color='#ff0000'>You hit a solid object (wall, floor, etc.) & take crushing damage = your thrusting damage - your DR (<i>EXCEPTION:</i> if opponent using impaling weapon, you fall on it & take damage based on your ST).  If attacking an opponent who is using an impaling weapon, you fall on <i>his weapon</i>.  You suffer the weapon's normal damage based on <i>your</i> <b>ST</b>.</font> [B557]"
-        elif self.sum() == 4:
-            myStr += " <font color='#ff0000'>If attacking or parrying with a limb, you strain the limb:  1 HP damage & it's crippled for 30 min. If biting, butting, etc., have moderate neck pain (B428) for next 20-HT min minimum of 1 minute.</font> [B557]"
-        elif self.sum() == 17:
-            myStr += " <font color='#ff0000'>If attacking or parrying with a limb, you strain the limb:  1 HP damage & it's crippled for 30 min. If IQ 3-5 animal, it loses its nerve & flees on next turn or surrenders if cornered.</font> [B557]"
-        elif self.sum() == 3 or self.sum() == 18 :
-            myStr += " <font color='#ff0000'>You knock yourself out!  Roll vs. HT every 30 min. to recover.</font> [B557]"
-
-        return myStr
-
-class gurpsspellfail(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr +="]"
-        myStr += " = <b>" + str(self.sum()) + "</b>"
-
-        if self.sum() == 10 or self.sum() == 11:
-            myStr += " <font color='#ff0000'>Spell produces nothing but a loud noise, bright flash, awful odor, etc.</font> [B236]"
-        elif self.sum() == 9:
-            myStr += " <font color='#ff0000'>Spell fails entirely.  Caster is stunned (IQ roll to recover).</font> [B236]"
-        elif self.sum() == 12:
-            myStr += " <font color='#ff0000'>Spell produces a weak and useless shadow of the intended effect.</font> [B236]"
-        elif self.sum() == 8:
-            myStr += " <font color='#ff0000'>Spell fails entirely.  Caster takes 1 point of damage.</font> [B236]"
-        elif self.sum() == 13:
-            myStr += " <font color='#ff0000'>Spell produces the reverse of the intended effect.</font> [B236]"
-        elif self.sum() == 7:
-            myStr += " <font color='#ff0000'>Spell affects someone or something other than the intended subject.</font> [B236]"
-        elif self.sum() == 14:
-            myStr += " <font color='#ff0000'>Spell seems to work, but it is only a useless illusion.</font> [B236]"
-        elif self.sum() == 5 or self.sum() == 6:
-            myStr += " <font color='#ff0000'>Spell is cast on one of the caster's companions (if harmful) or a random nearby foe (if beneficial).</font> [B236]"
-        elif self.sum() == 15 or self.sum() == 16:
-            myStr += " <font color='#ff0000'>Spell has the reverse of the intended, on the wrong target.  Roll randomly.</font> [B236]"
-        elif self.sum() == 4:
-            myStr += " <font color='#ff0000'>Spell is cast on caster (if harmful) or on a random nearby foe (if beneficial).</font> [B236]"
-        elif self.sum() == 17:
-            myStr += " <font color='#ff0000'>Spell fails entirely.  Caster temporarily forgets the spell.  Make a weekly IQ roll (after a week passes) until the spell is remembered.</font> [B236]"
-        elif self.sum() == 3:
-            myStr += " <font color='#ff0000'>Spell fails entirely.  Caster takes 1d of injury.</font> [B236]"
-        elif self.sum() == 18:
-            myStr += " <font color='#ff0000'>Spell fails entirely.  A demon or other malign entity appears and attacks the caster.  (GM may waive this if the caster and spell were both lily-white, pure good in intent.)</font> [B236]"
-
-        return myStr
-
-class gurpsfrightcheck(std):
-    def __init__(self,source=[],skill=0,mod=0):
-        std.__init__(self,source)
-        self.skill = skill
-        self.mod = mod
-
-    def is_success(self):
-        return (((self.sum()) <= self.skill+self.mod) and (self.sum() < 14))
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr +="]"
-        myStr += " = <b>" + str(self.sum()) + "</b>"
-
-        if self.skill+self.mod < 14:
-            myStr += " vs <b>(" + str(self.skill+self.mod) + ")</b>"
-            Diff = abs((self.skill+self.mod) - self.sum())
-        else:
-            myStr += " vs <b>(13)</b>"
-            Diff = abs(13 - self.sum())
-
-        if self.is_success():
-            if self.sum() == 3 or self.sum() == 4:
-                myStr += " or less <font color='#ff0000'><b>Critical Success!</b></font> [B556]"
-            else:
-                myStr += " or less <font color='#ff0000'><b>Success!</b> by " + str(Diff) +" </font>"
-        else:
-            myStr += " or less <font color='#ff0000'><b>Failure!</b> by " + str(Diff) +" </font>"
-
-        if self.skill + self.mod > 13:
-            myStr += " Rule of 14 in effect [B360]"
-
-        if not(self.is_success()):
-            intD1 = int(random.uniform(1,7))
-            intD2 = int(random.uniform(1,7))
-            intD3 = int(random.uniform(1,7))
-            intFright = intD1 + intD2 + intD3 + Diff
-            myStr += "<br />Rolling on Fright Check Table<br />[" + str(intD1) + "," + str(intD2) + "," + str(intD3) + "] ==> " + str(intFright - Diff) + " +  " + str(Diff) + " = " + str(intFright) + "<br />"
-            if intFright < 6:
-                myStr += "<font color='#ff0000'>Stunned for one second, then recover automatically.</font> [B360]"
-            elif intFright < 8:
-                myStr += "<font color='#ff0000'>Stunned for one second.  Every second after that, roll vs. unmodified Will to snap out of it.</font> [B360]"
-            elif intFright < 10:
-                myStr += "<font color='#ff0000'>Stunned for one second.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B360]"
-            elif intFright == 10:
-                strStun = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Stunned for " + strStun + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B360]"
-            elif intFright == 11:
-                strStun = str(int(random.uniform(2,13)))
-                myStr += "<font color='#ff0000'>Stunned for " + strStun + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B361]"
-            elif intFright == 12:
-                myStr += "<font color='#ff0000'>Lose your lunch.  Treat this as retching for (25-HT) seconds, and then roll vs. HT each second to recover [B428].</font> [B361]"
-            elif intFright == 13:
-                myStr += "<font color='#ff0000'>Acquire a new mental quirk.</font> [B361]"
-            elif intFright < 16:
-                strFP = str(int(random.uniform(1,7)))
-                strSeconds = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Lose " + strFP + " FP, and stunned for " + strSeconds + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B361]"
-            elif intFright == 16:
-                strSeconds = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Stunned for " + strSeconds + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.  Acquire a new mental quirk.</font> [B361]"
-            elif intFright == 17:
-                strMinutes = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes.  Every minute after that roll vs. HT to recover.</font> [B361]"
-            elif intFright == 18:
-                strMinutes = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.</font> [B361]"
-            elif intFright == 19:
-                strMinutes = str(int(random.uniform(2,13)))
-                myStr += "<font color='#ff0000'>Severe faint, lasting for " + strMinutes + " minutes.  Every minute after that roll vs. HT to recover.  Take 1 HP of injury.</font> [B361]"
-            elif intFright == 20:
-                strMinutes = str(int(random.uniform(4,25)))
-                strFP = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Faint bordering on shock, lastering for " + strMinutes + " minutes.  Also, lose " + strFP + " FP.</font> [B361]"
-            elif intFright == 21:
-                strMinutes = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Panic.  You run around screaming, sit down and cry, or do something else equally pointless for " + strMinutes + " minutes.  Every minute after that, roll vs. unmodified Will to snap out of it.</font> [B361]"
-            elif intFright == 22:
-                myStr += "<font color='#ff0000'>Acquire a new -10-point Delusion (B130).</font> [B361]"
-            elif intFright == 23:
-                myStr += "<font color='#ff0000'>Acquire a new -10-point Phobia (B148) or other -10-point mental disadvantage.</font> [B361]"
-            elif intFright == 24:
-                myStr += "<font color='#ff0000'>Major physical effect, set by the GM: hair turns white, age five years overnight, go partially deaf, etc.  (Acquire -15 points worth of physical disadvantages.  Each year of aging = -3 points.)</font> [B361]"
-            elif intFright == 25 :
-                myStr += "<font color='#ff0000'>If you already have a Phobia or other mental disadvantage that is logically related to the frightening incident, your self-control number becomes one step worse.  If not, or if your self-control number is already 6, add a new -10-point Phobia or other -10-point mental disadvantage.</font> [B361]"
-            elif intFright == 26:
-                strMinutes = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.  Also acquire a new -10-point Delusion (B130).</font> [B361]"
-            elif intFright == 27:
-                strMinutes = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.  Also acquire a new -10-point Phobia (B148) or other -10-point mental disadvantage.</font> [B361]"
-            elif intFright == 28:
-                myStr += "<font color='#ff0000'>Light coma.  You fall unconscious, rolling vs. HT every 30 minutes to recover.  For 6 hours after you come to, all skill rolls and attribute checks are at -2.</font> [B361]"
-            elif intFright == 29:
-                strHours = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.</font> [B361]"
-            elif intFright == 30:
-                strDays = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Catatonia.  Stare into space for " + strDays + " days.  Then roll vs. HT.  On a failed roll, remain catatonic for another " + strDays + " days, and so on.  If you have no medical care, lose 1 HP the first day, 2 HP the second day and so on.  If you survive and awaken, all skill rolls and attribute checks are at -2 for as many days as the catatonia lasted.</font> [B361]"
-            elif intFright == 31:
-                strMinutes = str(int(random.uniform(1,7)))
-                strFP = str(int(random.uniform(1,7)))
-                strInjury = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Seizure.  You lose control of your body and fall to the ground in a fit lasting " + strMinutes + " minutes and costing " + strFP + " FP.  Also, roll vs. HT.  On a failure, take " + strInjury + " HP of injury.  On a critical failure, you also lose 1 HT <i>permanently</i>.</font> [B361]"
-            elif intFright == 32:
-                strInjury = str(int(random.uniform(2,13)))
-                myStr += "<font color='#ff0000'>Stricken.  You fall to the ground, taking " + strInjury + " HP of injury in the form of a mild heart attack or stroke.</font> [B361]"
-            elif intFright == 33:
-                myStr += "<font color='#ff0000'>Total panic.  You are out of control; you might do anything (GM rolls 3d: the higher the roll, the more useless your reaction).  For instance, you might jump off a cliff to avoid the monster.  If you survive your first reaction, roll vs. Will to come out of the panic.  If you fail, the GM rolls again for another panic reaction, and so on!</font> [B361]"
-            elif intFright == 34:
-                myStr += "<font color='#ff0000'>Acquire a new -15-point Delusion (B130).</font> [B361]"
-            elif intFright == 35:
-                myStr += "<font color='#ff0000'>Acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.</font> [B361]"
-            elif intFright == 36:
-                myStr += "<font color='#ff0000'>Severe physical effect, set by the GM.  (Acquire -20 points worth of physical disadvantages, aging = -3 per year).</font> [B361]"
-            elif intFright == 37:
-                myStr += "<font color='#ff0000'>Severe physical effect, set by the GM.  (Acquire -30 points worth of physical disadvantages, aging = -3 per year).</font> [B361]"
-            elif intFright == 39:
-                strHours = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Delusion (B130).</font> [B361]"
-            elif intFright == 39:
-                strHours = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.</font> [B361]"
-            else:
-                strHours = str(int(random.uniform(1,7)))
-                myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.  Also lose 1 point of IQ <i>permanently</i>.  This automatically reduces all IQ-based skill, including magic spells, by 1.</font> [B361]"
-        return myStr
-
-class gurpsfrightcheckfail(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-        self.mod = mod
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr +="] + " + str(self.mod)
-        intFright = self.sum() + self.mod
-        myStr += " = <b>" + str(intFright) + "</b> "
-
-        if intFright < 6:
-            myStr += "<font color='#ff0000'>Stunned for one second, then recover automatically.</font> [B360]"
-        elif intFright < 8:
-            myStr += "<font color='#ff0000'>Stunned for one second.  Every second after that, roll vs. unmodified Will to snap out of it.</font> [B360]"
-        elif intFright < 10:
-            myStr += "<font color='#ff0000'>Stunned for one second.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B360]"
-        elif intFright == 10:
-            strStun = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Stunned for " + strStun + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B360]"
-        elif intFright == 11:
-            strStun = str(int(random.uniform(2,13)))
-            myStr += "<font color='#ff0000'>Stunned for " + strStun + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B361]"
-        elif intFright == 12:
-            myStr += "<font color='#ff0000'>Lose your lunch.  Treat this as retching for (25-HT) seconds, and then roll vs. HT each second to recover [B428].</font> [B361]"
-        elif intFright == 13:
-            myStr += "<font color='#ff0000'>Acquire a new mental quirk.</font> [B361]"
-        elif intFright < 16:
-            strFP = str(int(random.uniform(1,7)))
-            strSeconds = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Lose " + strFP + " FP, and stunned for " + strSeconds + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B361]"
-        elif intFright == 16:
-            strSeconds = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Stunned for " + strSeconds + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.  Acquire a new mental quirk.</font> [B361]"
-        elif intFright == 17:
-            strMinutes = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes.  Every minute after that roll vs. HT to recover.</font> [B361]"
-        elif intFright == 18:
-            strMinutes = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.</font> [B361]"
-        elif intFright == 19:
-            strMinutes = str(int(random.uniform(2,13)))
-            myStr += "<font color='#ff0000'>Severe faint, lasting for " + strMinutes + " minutes.  Every minute after that roll vs. HT to recover.  Take 1 HP of injury.</font> [B361]"
-        elif intFright == 20:
-            strMinutes = str(int(random.uniform(4,25)))
-            strFP = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Faint bordering on shock, lastering for " + strMinutes + " minutes.  Also, lose " + strFP + " FP.</font> [B361]"
-        elif intFright == 21:
-            strMinutes = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Panic.  You run around screaming, sit down and cry, or do something else equally pointless for " + strMinutes + " minutes.  Every minute after that, roll vs. unmodified Will to snap out of it.</font> [B361]"
-        elif intFright == 22:
-            myStr += "<font color='#ff0000'>Acquire a new -10-point Delusion (B130).</font> [B361]"
-        elif intFright == 23:
-            myStr += "<font color='#ff0000'>Acquire a new -10-point Phobia (B148) or other -10-point mental disadvantage.</font> [B361]"
-        elif intFright == 24:
-            myStr += "<font color='#ff0000'>Major physical effect, set by the GM: hair turns white, age five years overnight, go partially deaf, etc.  (Acquire -15 points worth of physical disadvantages.  Each year of aging = -3 points.)</font> [B361]"
-        elif intFright == 25 :
-            myStr += "<font color='#ff0000'>If you already have a Phobia or other mental disadvantage that is logically related to the frightening incident, your self-control number becomes one step worse.  If not, or if your self-control number is already 6, add a new -10-point Phobia or other -10-point mental disadvantage.</font> [B361]"
-        elif intFright == 26:
-            strMinutes = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.  Also acquire a new -10-point Delusion (B130).</font> [B361]"
-        elif intFright == 27:
-            strMinutes = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.  Also acquire a new -10-point Phobia (B148) or other -10-point mental disadvantage.</font> [B361]"
-        elif intFright == 28:
-            myStr += "<font color='#ff0000'>Light coma.  You fall unconscious, rolling vs. HT every 30 minutes to recover.  For 6 hours after you come to, all skill rolls and attribute checks are at -2.</font> [B361]"
-        elif intFright == 29:
-            strHours = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.</font> [B361]"
-        elif intFright == 30:
-            strDays = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Catatonia.  Stare into space for " + strDays + " days.  Then roll vs. HT.  On a failed roll, remain catatonic for another " + strDays + " days, and so on.  If you have no medical care, lose 1 HP the first day, 2 HP the second day and so on.  If you survive and awaken, all skill rolls and attribute checks are at -2 for as many days as the catatonia lasted.</font> [B361]"
-        elif intFright == 31:
-            strMinutes = str(int(random.uniform(1,7)))
-            strFP = str(int(random.uniform(1,7)))
-            strInjury = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Seizure.  You lose control of your body and fall to the ground in a fit lasting " + strMinutes + " minutes and costing " + strFP + " FP.  Also, roll vs. HT.  On a failure, take " + strInjury + " HP of injury.  On a critical failure, you also lose 1 HT <i>permanently</i>.</font> [B361]"
-        elif intFright == 32:
-            strInjury = str(int(random.uniform(2,13)))
-            myStr += "<font color='#ff0000'>Stricken.  You fall to the ground, taking " + strInjury + " HP of injury in the form of a mild heart attack or stroke.</font> [B361]"
-        elif intFright == 33:
-            myStr += "<font color='#ff0000'>Total panic.  You are out of control; you might do anything (GM rolls 3d: the higher the roll, the more useless your reaction).  For instance, you might jump off a cliff to avoid the monster.  If you survive your first reaction, roll vs. Will to come out of the panic.  If you fail, the GM rolls again for another panic reaction, and so on!</font> [B361]"
-        elif intFright == 34:
-            myStr += "<font color='#ff0000'>Acquire a new -15-point Delusion (B130).</font> [B361]"
-        elif intFright == 35:
-            myStr += "<font color='#ff0000'>Acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.</font> [B361]"
-        elif intFright == 36:
-            myStr += "<font color='#ff0000'>Severe physical effect, set by the GM.  (Acquire -20 points worth of physical disadvantages, aging = -3 per year).</font> [B361]"
-        elif intFright == 37:
-            myStr += "<font color='#ff0000'>Severe physical effect, set by the GM.  (Acquire -30 points worth of physical disadvantages, aging = -3 per year).</font> [B361]"
-        elif intFright == 39:
-            strHours = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Delusion (B130).</font> [B361]"
-        elif intFright == 39:
-            strHours = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.</font> [B361]"
-        else:
-            strHours = str(int(random.uniform(1,7)))
-            myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.  Also lose 1 point of IQ <i>permanently</i>.  This automatically reduces all IQ-based skill, including magic spells, by 1.</font> [B361]"
-
-        return myStr
--- a/orpg/dieroller/rollers/hero.py	Tue Oct 06 16:03:42 2009 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,232 +0,0 @@
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-# --
-#
-# File: Hero.py
-# Version:
-#   $Id: Hero.py,v .3 DJM & Heroman
-#
-# Description: Hero System die roller originally based on Posterboy's D20 Dieroller
-#
-# Changelog:
-# v.3 by Heroman
-# Added hl() to show hit location (+side), and hk() for Hit Location killing damage
-# (No random stun multiplier)
-# v.2 DJM
-# Removed useless modifiers from the Normal damage roller
-# Changed Combat Value roller and SKill roller so that positive numbers are bonuses,
-# negative numbers are penalties
-# Changed Killing damage roller to correct stun multiplier bug
-# Handled new rounding issues
-#
-# v.1 original release DJM
-
-__version__ = "$Id: hero.py,v 1.15 2006/11/04 21:24:19 digitalxero Exp $"
-
-from time import time, clock
-import random
-
-from std import std
-from orpg.dieroller.base import *
-
-# Hero stands for "Hero system" not 20 sided die :)
-
-class hero(std):
-    name = "hero"
-
-    def __init__(self,source=[]):
-        std.__init__(self,source)
-
-# these methods return new die objects for specific options
-
-    def k(self,mod):
-        return herok(self,mod)
-
-    def hl(self):
-        return herohl(self)
-
-    def hk(self):
-        return herohk(self)
-
-    def n(self):
-        return heron(self)
-
-    def cv(self,cv,mod):
-        return herocv(self,cv,mod)
-
-    def sk(self,sk,mod):
-        return herosk(self,sk,mod)
-
-die_rollers.register(hero)
-
-class herocv(std):
-    def __init__(self,source=[],cv=10,mod=0):
-        std.__init__(self,source)
-        self.cv = cv
-        self.mod = mod
-
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr += "] = (" + str(self.sum()) + ")"
-
-        myStr += " with a CV of " + str(self.cv)
-        myStr += " and a modifier of " + str(self.mod)
-        cvhit = 11 + self.cv - self.sum() + self.mod
-        myStr += " hits up to <b>DCV <font color='#ff0000'>" + str(cvhit) + "</font></b>"
-        return myStr
-
-class herosk(std):
-    def __init__(self,source=[],sk=11,mod=0):
-        std.__init__(self,source)
-        self.sk = sk
-        self.mod = mod
-
-    def is_success(self):
-        return (((self.sum()-self.mod) <= self.sk))
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        strAdd="] - "
-        swapmod=self.mod
-        if self.mod < 0:
-            strAdd= "] + "
-            swapmod= -self.mod
-        myStr += strAdd + str(swapmod)
-        modSum = self.sum()-self.mod
-        myStr += " = (" + str(modSum) + ")"
-        myStr += " vs " + str(self.sk)
-
-        if self.is_success():
-            myStr += " or less <font color='#ff0000'>Success!"
-        else:
-            myStr += " or less <font color='#ff0000'>Failure!"
-
-        Diff = self.sk - modSum
-        myStr += " by " + str(Diff) +" </font>"
-
-        return myStr
-
-class herok(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-        self.mod = mod
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr += "] = (<font color='#ff0000'><b>" + str(int(round(self.sum()))) + "</b></font>)"
-        stunx = random.randint(1,6)-1
-        if stunx <= 1:
-            stunx = 1
-        myStr += " <b>Body</b> and a stunx of (" + str(stunx)
-        stunx = stunx + self.mod
-        myStr += " + " + str(self.mod)
-        stunsum = round(self.sum()) * stunx
-        myStr += ") for a total of (<font color='#ff0000'><b>" + str(int(stunsum)) + "</b></font>) <b>Stun</b>"
-        return myStr
-
-class herohl(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-        self.mod = mod
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        side = random.randint(1,6)
-        sidestr = "Left "
-        if side >=4:
-            sidestr = "Right "
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr += "] = (<font color='#ff0000'><b>" + str(int(round(self.sum()))) + "</b></font>) "
-        location = int(round(self.sum()))
-        if location <= 5:
-            myStr += "Location: <B>Head</B>, StunX:<B>x5</B>, NStun:<B>x2</B>, Bodyx:<B>x2</B>"
-        elif location == 6:
-            myStr += "Location: <B>" + sidestr + "Hand</B>, StunX:<B>x1</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
-        elif location == 7:
-            myStr += "Location: <B>" + sidestr + "Arm</B>, StunX:<B>x2</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
-        elif location == 8:
-            myStr += "Location: <B>" + sidestr + "Arm</B>, StunX:<B>x2</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
-        elif location == 9:
-            myStr += "Location: <B>" + sidestr + "Shoulder</B>, StunX:<B>x3</B>, NStun:<B>x1</B>, Bodyx:<B>x1</B>"
-        elif location == 10:
-            myStr += "Location: <B>Chest</B>, StunX:<B>x3</B>, NStun:<B>x1</B>, Bodyx:<B>x1</B>"
-        elif location == 11:
-            myStr += "Location: <B>Chest</B>, StunX:<B>x3</B>, NStun:<B>x1</B>, Bodyx:<B>x1</B>"
-        elif location == 12:
-            myStr += "Location: <B>Stomach</B>, StunX:<B>x4</B>, NStun:<B>x1 1/2</B>, Bodyx:<B>x1</B>"
-        elif location == 13:
-            myStr += "Location: <B>Vitals</B>, StunX:<B>x4</B>, NStun:<B>x1 1/2</B>, Bodyx:<B>x2</B>"
-        elif location == 14:
-            myStr += "Location: <B>" + sidestr + "Thigh</B>, StunX:<B>x2</B>, NStun:<B>x1</B>, Bodyx:<B>x1</B>"
-        elif location == 15:
-            myStr += "Location: <B>" + sidestr + "Leg</B>, StunX:<B>x2</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
-        elif location == 16:
-            myStr += "Location: <B>" + sidestr + "Leg</B>, StunX:<B>x2</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
-        elif location >= 17:
-            myStr += "Location: <B>" + sidestr + "Foot</B>, StunX:<B>x1</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
-        return myStr
-
-class herohk(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-        self.mod = mod
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-        myStr += "] = (<font color='#ff0000'><b>" + str(int(round(self.sum()))) + "</b></font>)"
-        stunx = 1
-        myStr += " <b>Body</b> "
-        stunx = stunx + self.mod
-        stunsum = round(self.sum()) * stunx
-        myStr += " for a total of (<font color='#ff0000'><b>" + str(int(stunsum)) + "</b></font>) <b>Stun</b>"
-        return myStr
-
-class heron(std):
-    def __init__(self,source=[],mod=0):
-        std.__init__(self,source)
-        self.bodtot=0
-
-    def __str__(self):
-        myStr = "[" + str(self.data[0])
-        if self.data[0] == 6:
-            self.bodtot=self.bodtot+2
-        else:
-            self.bodtot=self.bodtot+1
-        if self.data[0] <= 1:
-            self.bodtot=self.bodtot-1
-        for a in self.data[1:]:
-            myStr += ","
-            myStr += str(a)
-            if a == 6:
-                self.bodtot=self.bodtot+2
-            else:
-                self.bodtot=self.bodtot+1
-            if a <= 1:
-                self.bodtot=self.bodtot-1
-        myStr += "] = (<font color='#ff0000'><b>" + str(self.bodtot) + "</b></font>)"
-        myStr += " <b>Body</b> and "
-        myStr += "(<font color='#ff0000'><b>" + str(int(round(self.sum()))) + "</b></font>) <b>Stun</b>"
-        return myStr
--- a/orpg/dieroller/rollers/runequest.py	Tue Oct 06 16:03:42 2009 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,696 +0,0 @@
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#
-#-------------------------------------------------------------------------
-#
-#  Usage:
-#
-#   Die  Roller: /dieroller rq
-#
-#   Skill  Roll: [1d100.skill(50,0,0)]         # ( skill%, modifer, MA% )
-#
-#   Parry  Roll: [1d100.parry(50,0,0,12)]      # ( skill%, modifer, MA%, Weapon/Shield AP )
-#
-#   Dodge  Roll: [1d100.parry(50,0,0)]         # ( skill%, modifer, MA% )
-#
-#   Attack Roll: [1d100.attack(50,0,0,2,9,3,0)]
-#       ( skill%, modifer, MA%, min weap dam, max weap dam, dam bonus, truesword )
-#
-#   Sorcery Roll: [1d100.sorcery(90,   0,   3,   6,   1,   1,    1)]
-#                               (sk, mod, pow, cer, int,  acc, mlt)
-#
-#
-#
-#   Skill Training Unlimited Roll: [1d100.trainskill(30,75)]       # (starting skill%, desired skill%)
-#   Skill Training Cost Limited:   [1d100.trainskillcost(1000, 50) # (payment, starting skill%)
-#   Skill Training Time Limited:   [1d100.trainskilltime(150, 50)  # (time, strting skill%)
-#
-#-------------------------------------------------------------------------
-# --
-#
-# File: rq.py
-# Version:
-#   $Id: rq.py,v .1 pelwer
-#
-# Description: Runequest die roller originally based on Heroman's Hero Dieroller
-#
-#
-# v.1 - pelwer - 2/5/2005
-#  o Original release
-# v.2 - pelwer - 10/30/2006
-#  o Ported to openrpg+ by removing dependance on whrandom
-#  o Fixed Riposte spelling
-#  o Deleted sorcalc - never used
-#  o Added Sorcery Fumble table to sorcery spell roller
-#
-
-__version__ = "$Id: runequest.py,v 1.4 2006/11/15 12:11:22 digitalxero Exp $"
-
-from time import time, clock
-import random
-from math import floor
-
-from std import std
-from orpg.dieroller.base import *
-
-# rq stands for "Runequest"
-
-class runequest(std):
-    name = "runequest"
-    def __init__(self,source=[]):
-        std.__init__(self,source)
-
-# these methods return new die objects for specific options
-
-    def skill(self,sk,mod,ma):
-        return rqskill(self,sk,mod,ma)
-
-    def parry(self,sk,mod,ma,AP):
-        return rqparry(self,sk,mod,ma,AP)
-
-    def dodge(self,sk,mod,ma):
-        return rqdodge(self,sk,mod,ma)
-
-    def attack(self,sk,mod,ma,mindam,maxdam,bondam,trueswd):
-        return rqattack(self,sk,mod,ma,mindam,maxdam,bondam,trueswd)
-
-    def sorcery(self,sk,mod,pow,cer,int,acc,mlt):
-        return rqsorcery(self,sk,mod,pow,cer,int,acc,mlt)
-
-    def trainskill(self,initial,final):
-        return rqtrainskill(self,initial,final)
-
-    def trainskillcost(self,cost,sk):
-        return rqtrainskillcost(self,cost,sk)
-
-    def trainskilltime(self,time,sk):
-        return rqtrainskilltime(self,time,sk)
-
-die_rollers.register(runequest)
-
-#  RQ Skill Training Cost/Time unlimited
-#
-# [1d100.trainskill(10,20)]
-#          initial skill%, final skill%
-#
-# sk    = skill %
-#
-#
-class rqtrainskill(std):
-    def __init__(self,source=[],initial=11,final=0):
-        std.__init__(self,source)
-        self.s = initial
-        self.f = final
-
-    def __str__(self):
-        myStr = "Unrestricted Training"
-
-        if self.s == 0:
-            myStr = "Initial training completed for Cost(50) Time(20) Skill(1 + modifier)"
-        else:
-            cost  = 0
-            time  = 0
-            myStr = "Training: "
-
-            while self.s < self.f and self.s < 75:
-                cost   += self.s * 5
-                time   += self.s * 1
-                self.s += random.uniform(1,4) + 1
-
-            myStr  = "Training completed:\n"
-            myStr += "\tCost(" + str(int(cost)) + ")\n"
-            myStr += "\tTime(" + str(int(time)) + ")\n"
-            myStr += "\tSkill(" + str(int(self.s)) + ")"
-
-        return myStr
-
-
-#  RQ Skill Training Cost Limited
-#
-# [1d100.trainskillcost(50,0)]
-#          cost, skill%
-#
-# cost  = cash for training
-# sk    = skill %
-#
-#
-class rqtrainskillcost(std):
-    def __init__(self,source=[],cost=11,sk=0):
-        std.__init__(self,source)
-        self.cost = cost
-        self.sk   = sk
-
-    def __str__(self):
-        myStr = ""
-
-        if self.sk == 0 and self.cost >= 50:
-            myStr = "Initial training completed for Cost(50), Time(50), Skill(1 + modifier)"
-        else:
-            cost  = 0
-            time  = 0
-            icost = self.sk * 5
-
-            myStr = "Training: "
-
-            while (cost + icost) < self.cost:
-                if self.sk >= 75:
-                    break
-
-                cost += icost
-                time += self.sk * 1
-                self.sk += random.uniform(1,4) + 1
-                icost = self.sk * 5
-
-            myStr  = "Training completed: "
-            myStr += "Cost(" + str(int(cost)) + ") "
-            myStr += "Time(" + str(int(time)) + ") "
-            myStr += "Skill(" + str(int(self.sk)) + ")"
-
-        return myStr
-
-
-#  RQ Skill Training Time Limited
-#
-# [1d100.trainskilltime(50,0)]
-#          time, skill%
-#
-# time  = time for training
-# sk    = skill %
-#
-#
-class rqtrainskilltime(std):
-    def __init__(self,source=[],time=11,sk=0):
-        std.__init__(self,source)
-        self.time = time
-        self.sk   = sk
-
-    def __str__(self):
-        myStr = ""
-
-        if self.sk == 0 and self.time >= 20:
-            myStr = "Initial training completed for Cost(50), Time(50), Skill(1 + modifier)"
-        else:
-            cost  = 0
-            time  = 0
-            itime = self.sk * 1
-
-            myStr = "Trainingsss: "
-
-            while (time + itime) < self.time:
-                if self.sk >= 75:
-                    break
-
-                cost += self.sk * 5
-                time += itime
-                self.sk += random.uniform(1,4) + 1
-                itime = self.sk * 5
-
-            myStr  = "Training completed: "
-            myStr += "Cost(" + str(int(cost)) + ") "
-            myStr += "Time(" + str(int(time)) + ") "
-            myStr += "Skill(" + str(int(self.sk)) + ")"
-
-        return myStr
-
-#  RQ Skill Roll
-#
-# [1d100.skill(50,0,0)]
-#          skill%, modifer, ma%
-#
-# sk    = skill %
-# mod   = modifier %
-# ma    = martial arts %
-# skill = sk + mod
-#
-# success   roll <= skill
-#
-# failure   roll > skill
-#
-# crit
-#     push( @{$::Cre{Weapons}{$weap_cnt}}, POSIX::floor( skill/20 ) );
-#
-# special
-#     push( @{$::Cre{Weapons}{$weap_cnt}}, POSIX::floor( $skill/5 ) );
-#
-# fumble: if ( $skill > 100 ) { $fum = 0; } else { $fum = 100 - $skill; }
-#             $fum = 100 - POSIX::floor( $fum/20 );
-#             if ( $fum == 100 ) { $fum = '00'; };
-#
-class rqskill(std):
-    def __init__(self,source=[],sk=11,mod=0,ma=0):
-        std.__init__(self,source)
-        self.sk  = sk
-        self.mod = mod
-        self.ma  = ma
-
-    def is_success(self):
-        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
-
-    def is_ma(self):
-        return (self.sum() <= self.ma)
-
-    def is_special(self):
-        return (self.sum() <= int(floor((self.sk + self.mod)/5)))
-
-    def is_critical(self):
-        return (self.sum() <= int(floor((self.sk + self.mod) / 20)))
-
-    def is_fumble(self):
-        if ( self.sk >= 100 ):
-            fum = 0
-        else:
-            fum = (100 - self.sk )
-        final_fum = ( 100 - int( floor( fum/20  ) ) )
-        return (  self.sum() >= final_fum )
-
-    def __str__(self):
-        strAdd="+"
-        swapmod= self.mod
-        if self.mod < 0:
-            strAdd= "-"
-            swapmod= -self.mod
-        modSum = self.sum()
-        # build output string
-        myStr = " (" + str(modSum) + ")"
-        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
-
-        if self.is_fumble():
-            myStr += " <b><font color=red>Fumble!</font></b>"
-        elif self.is_critical():
-            myStr += " <b><font color=green>Critical!</font></b>"
-        elif self.is_special():
-            myStr += " <i><font color=green>Special!</font></i>"
-        elif self.is_success() and self.is_ma():
-            myStr += " <i><font color=green>Special!</font></i>"
-        elif self.is_success():
-            myStr += " <font color=blue>Success!</font>"
-        else:
-            myStr += " <font color=red>Failure!</font>"
-
-        Diff = self.sk - modSum
-        myStr += " </font>"
-
-        return myStr
-
-#
-# RQ Parry Roll
-#
-# same as skill but with fumble dice and armor points
-#
-# [1d100.parry(50,0,0,12)]
-#             skill%, modifer, ma%, Weapon AP
-#
-
-class rqparry(std):
-    def __init__(self,source=[],sk=11,mod=0,ma=0,AP=0):
-        std.__init__(self,source)
-        self.sk = sk
-        self.mod = mod
-        self.ma  = ma
-        self.AP = AP
-
-    def is_success(self):
-        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
-
-    def is_special(self):
-        return (self.sum() <= int(floor((self.sk + self.mod) / 5)))
-
-    def is_ma(self):
-        return (self.sum() <= self.ma)
-
-    def is_riposte(self):
-        return (self.sum() <= (self.ma / 5))
-
-    def is_critical(self):
-        return ( (  self.sum() <= int( floor( ( self.sk + self.mod  )/20 ) ) ) )
-
-    def is_fumble(self):
-        if ( self.sk >= 100 ):
-            fum = 0
-        else:
-            fum = (100 - self.sk )
-        final_fum = ( 100 - int( floor( fum/20  ) ) )
-        return (  self.sum() >= final_fum )
-
-    def __str__(self):
-
-        # get fumble roll result in case needed
-        fum_roll = random.randint(1,100)
-
-        # get special AP
-        spec_AP = int( floor ( self.AP * 1.5 ) )
-
-        # figure out +/- for modifer
-        strAdd="+"
-        swapmod= self.mod
-        if self.mod < 0:
-            strAdd= "-"
-            swapmod= -self.mod
-        modSum = self.sum()
-
-        # build output string
-        myStr = " (" + str(modSum) + ")"
-        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
-
-        if self.is_fumble():
-            myStr += " <b><font color=red>Fumble!</font>  See Fumble Chart [" + str(fum_roll) + "]</b>"
-        elif self.is_critical() and self.is_riposte():
-            myStr += " <b><font color=green>Critical!</font> All damage blocked!</b>"
-            myStr += " Riposte next SR"
-        elif self.is_critical():
-            myStr += " <b><font color=green>Critical!</font> All damage blocked!</b>"
-        elif self.is_special and self.is_riposte():
-            myStr += " <i><font color=green>Special!</font> Weapon/Shield AP [" + str(spec_AP) + "]</i>"
-            myStr += " Riposte next SR"
-        elif self.is_special():
-            myStr += " <i><font color=green>Special!</font> Weapon/Shield AP [" + str(spec_AP) + "]</i>"
-        elif self.is_success() and self.is_ma():
-            myStr += " <i><font color=green>Special!</font> Weapon/Shield AP [" + str(spec_AP) + "]</i>"
-        elif self.is_success():
-            myStr += " <font color=blue>Success!</font> Weapon/Shield AP [" + str(self.AP) + "]"
-        else:
-            myStr += " <font color=red>Failure!</font>"
-
-        Diff = self.sk - modSum
-        myStr += " </font>"
-
-        return myStr
-
-# RQ Dodge Roll
-#
-# same as skill but with fumble dice and armor points
-#
-# [1d100.parry(50,0,0)]
-#             skill%, modifer, ma%
-#
-
-class rqdodge(std):
-    def __init__(self,source=[],sk=11,mod=0,ma=0,AP=0):
-        std.__init__(self,source)
-        self.sk = sk
-        self.mod = mod
-        self.ma  = ma
-        self.AP = AP
-
-    def is_success(self):
-        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
-
-    def is_special(self):
-        return (self.sum() <= int(floor((self.sk + self.mod) / 5)))
-
-    def is_ma(self):
-        return (self.sum() <= self.ma)
-
-    def is_riposte(self):
-        return (self.sum() <= (self.ma / 5))
-
-    def is_critical(self):
-        return ( (  self.sum() <= int( floor( ( self.sk + self.mod  )/20 ) ) ) )
-
-    def is_fumble(self):
-        if ( self.sk >= 100 ):
-            fum = 0
-        else:
-            fum = (100 - self.sk )
-        final_fum = ( 100 - int( floor( fum/20  ) ) )
-        return (  self.sum() >= final_fum )
-
-    def __str__(self):
-
-        # get fumble roll result in case needed
-        fum_roll = random.randint(1,100)
-
-        # get special AP
-        spec_AP = int( floor ( self.AP * 1.5 ) )
-
-        # figure out +/- for modifer
-        strAdd="+"
-        swapmod= self.mod
-        if self.mod < 0:
-            strAdd= "-"
-            swapmod= -self.mod
-        modSum = self.sum()
-
-        # build output string
-        myStr = " (" + str(modSum) + ")"
-        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
-
-        if self.is_fumble():
-            myStr += " <b><font color=red>Fumble!</font>  See Fumble Chart [" + str(fum_roll) + "]</b>"
-        elif self.is_critical() and self.is_riposte():
-            myStr += " <b><font color=green>Critical!</font> All damage dodged!</b>"
-            myStr += " Riposte on next SR"
-        elif self.is_critical():
-            myStr += " <b><font color=green>Critical!</font> All damage dodged!</b>"
-        elif self.is_special and self.is_riposte():
-            myStr += " <i><font color=green>Special!</font> Damage dodged</b>"
-            myStr += " Riposte on next SR"
-        elif self.is_special():
-            myStr += " <i><font color=green>Special!</font> Damage dodged</b>"
-        elif self.is_success() and self.is_ma():
-            myStr += " <i><font color=green>Special!</font> Damage dodged</b>"
-        elif self.is_success():
-            myStr += " <font color=blue>Success!</font> Damage dodged</b>"
-        else:
-            myStr += " <font color=red>Failure!</font>"
-
-        Diff = self.sk - modSum
-        myStr += " </font>"
-
-        return myStr
-
-
-
-#
-# RQ Attack Roll
-#
-# same as skill but with fumble dice and armor points
-#
-# [1d100.attack(50,0,0,2,9,3,1)]
-#             skill%, modifer, ma%, min weap dam, max weap dam, dam bonus, truesword_enabled
-#
-class rqattack(std):
-    def __init__(self,source=[],sk=11,mod=0,ma=0,mindam=0,maxdam=0,bondam=0,trueswd=0):
-        std.__init__(self,source)
-        self.sk = sk
-        self.mod = mod
-        self.ma  = ma
-        self.mindam = mindam
-        self.maxdam = maxdam
-        self.bondam = bondam
-        self.trueswd = trueswd
-
-    def is_success(self):
-        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
-
-    def is_ma(self):
-        return (self.sum() <= self.ma)
-
-    def is_special(self):
-        return (self.sum() <= int(floor((self.sk + self.mod) / 5)))
-
-    def is_critical(self):
-        return ((self.sum() <= int(floor((self.sk + self.mod) / 20))))
-
-    def is_supercritical(self):
-        return (self.sum() == 1)
-
-    def is_fumble(self):
-        if ( self.sk >= 100 ):
-            fum = 0
-        else:
-            fum = (100 - self.sk )
-        final_fum = ( 100 - int( floor( fum/20  ) ) )
-        return (  self.sum() >= final_fum )
-
-    def __str__(self):
-
-        # get fumble roll result in case needed
-        fum_roll = random.randint(1,100)
-
-        # get hit location roll result in case needed
-        location = random.randint(1,20)
-        myStr = " to the ["+ str(location) + "] "
-        if location < 5:
-            myStr += "<B>Right Leg</B>"
-        elif location < 9:
-            myStr += "<B>Left Leg</B>"
-        elif location < 12:
-            myStr += "<B>Abdomen</B>"
-        elif location < 13:
-            myStr += "<B>Chest</B>"
-        elif location < 16:
-            myStr += "<B>Right Arm</B>"
-        elif location < 19:
-            myStr += "<B>Left Arm</B>"
-        else:
-            myStr += "<B>Head</B>"
-        hit_loc = myStr
-
-
-        # get normal damage in case needed
-        norm_damage = random.randint(self.mindam*(self.trueswd+1),self.maxdam*(self.trueswd+1)) + self.bondam
-        norm_damage_string  = "{" + str( self.mindam*(self.trueswd+1) ) + "-"
-        norm_damage_string += str(self.maxdam*(self.trueswd+1)) + "+" + str(self.bondam)
-        norm_damage_string += "}[" + str(norm_damage) + "] "
-
-        # get special/critical damage in case needed
-        crit_damage = random.randint( self.mindam*(self.trueswd+2), self.maxdam*(self.trueswd+2) ) + self.bondam
-        crit_damage_string = "{" + str( self.mindam*(self.trueswd+2) ) + "-" + str(self.maxdam*(self.trueswd+2)) + "+" + str(self.bondam) + "}[" + str(crit_damage) + "] "
-
-        # get supercritical damage in case needed
-        super_damage = norm_damage + self.maxdam
-        super_damage_string  = "{" + str( self.mindam*(self.trueswd+1) ) + "-"
-        super_damage_string += str(self.maxdam*(self.trueswd+1)) + "+" + str(self.maxdam)
-        super_damage_string += "+" + str(self.bondam) + "}[" + str(super_damage) + "] "
-
-        # figure out +/- for modifer
-        strAdd="+"
-        swapmod= self.mod
-        if self.mod < 0:
-            strAdd= "-"
-            swapmod= -self.mod
-        modSum = self.sum()
-
-        # build output string
-        myStr = " (" + str(modSum) + ")"
-        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
-
-        if self.is_fumble():
-            myStr += " <b><font color=red>Fumble!</font>  See Fumble Chart [" + str(fum_roll) + "]</b>"
-        elif (self.is_supercritical() and self.is_success()):
-            myStr += " <b><font color=green>Super Critical!</font></b> Damage: " + str(super_damage_string) + "<u>No Armor Stops</u>" + str(hit_loc)
-        elif (self.is_critical() and self.is_success()):
-            myStr += " <b><font color=green>Critical!</font></b> Damage: " + str(crit_damage_string) + "<u>No Armor Stops</u>" + str(hit_loc)
-        elif ( self.is_special() and self.is_success() ):
-            myStr += " <i><font color=green>Special!</font></i> Damage: " + str(crit_damage_string) + str(hit_loc)
-        elif (self.is_success() and self.is_ma()):
-            myStr += " <i><font color=green>Special!</font></i> Damage: " + str(crit_damage_string) + str(hit_loc)
-        elif self.is_success():
-            myStr += " <font color=blue>Success!</font> Damage: " + str(norm_damage_string) + str(hit_loc)
-        else:
-            myStr += " <font color=red>Failure!</font>"
-
-        return myStr
-
-#
-#
-#   Sorcery Roll: [1d100.sorcery(90,   10,  5,   4,   3,   2,    1)]
-#                               (sk, mod, pow, cer, int,  acc, mlt)
-#
-# Ceremony: (+1d6% per strike rank spent on ceremony)
-# Intensity: (-3% per point of Intensity)
-# Duration: (-4% per point of Duration)
-# Range: (-5% per point of Range)
-# Multispell: (-10% per each spell over 1)
-# Acceleration: (-5% per point of Acceleration)
-# Hold: (-2% per point in spell Held)
-#
-class rqsorcery(std):
-    def __init__(self,source=[],sk=11,mod=0,pow=0,cer=0,int=0,acc=0,mlt=0):
-        std.__init__(self,source)
-        self.sk  = sk   # sorcery skill
-        self.mod = mod  # additional modifier ( from duration, range, etc )
-        self.pow = pow  # boost pow and additional pow ( from duration, range, etc )
-        self.cer = cer  # ceremony d6
-        self.int = int  # intensity ( -3% )
-        self.acc = acc  # accelerate ( -5% )
-        self.mlt = mlt  # multispell ( -10% )
-
-    def is_success(self):
-        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
-
-    def is_special(self):
-        return ( (  self.sum() <= int( floor( ( self.sk + self.mod  )/5  ) ) ) )
-
-    def is_critical(self):
-        return ( (  self.sum() <= int( floor( ( self.sk + self.mod  )/20 ) ) ) )
-
-    def is_fumble(self):
-        if ( self.sk >= 100 ):
-            fum = 0
-        else:
-            fum = (100 - self.sk )
-        final_fum = ( 100 - int( floor( fum/20  ) ) )
-        return (  self.sum() >= final_fum )
-
-    def __str__(self):
-
-        # get fumble roll result in case needed
-        fum_roll = random.randint(2,12)
-        if fum_roll == 12 :
-            fum_string = "<br /><font color=purple>Caster temporarily forgets spell. Make an INTx5 roll each day to remember.</font>"
-        if fum_roll == 11 :
-            fum_string = "<br /><font color=purple>Caster temporarily forgets spell. Make an INTx5 roll each hour to remember.  </font>"
-        if fum_roll == 10 :
-            fum_string = "<br /><font color=purple>Spell produces reverse of the intended effect.  </font>"
-        if fum_roll == 9 :
-            fum_string = "<br /><font color=purple>Caster is Stunned. Roll INTx3 to recover at SR 10 each round.  </font>"
-        if fum_roll == 8 :
-            fum_string = "<br /><font color=purple>Caster takes 2D6 Damage to THP  </font>"
-        if fum_roll == 7 :
-            fum_string = "<br /><font color=purple>Spell produces reverse of the intended effect at 2x Intensity.  </font>"
-        if fum_roll == 6 :
-            fum_string = "<br /><font color=purple>Spell is cast on companions (if harmful) or on random nearby foes (if beneficial)  </font>"
-        if fum_roll == 5 :
-            fum_string = "<br /><font color=purple>Caster takes 1d6 Damage to Head  </font>"
-        if fum_roll == 4 :
-            fum_string = "<br /><font color=purple>Spell is cast on caster (if harmful) or on random nearby foe (if beneficial)  </font>"
-        if fum_roll == 3 :
-            fum_string = "<br /><font color=purple>Caster takes 1d6 Damage to THP  </font>"
-        if fum_roll == 2 :
-            fum_string = "<br /><font color=purple>Caster takes 1 point of Damage to Head  </font>"
-
-            # roll ceremony
-        ceremony_roll = random.randint( self.cer, (self.cer*6) )
-
-        # subtract manipulations
-        extra_mod = self.mod
-        self.mod += ceremony_roll - self.int*3 - self.acc*5 - self.mlt*10
-
-        # add up power cost
-        extra_pow = self.pow
-        self.pow += self.int + self.mlt + self.acc
-        special_pow = int( floor( ( self.pow )/2  ) )
-
-        # figure out +/- for modifer
-        strAdd="+"
-        swapmod= self.mod
-        if self.mod < 0:
-            strAdd= "-"
-            swapmod= -self.mod
-        modSum = self.sum()
-
-        # build output string
-        myStr = " (" + str(modSum) + ")"
-        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
-
-        if self.is_fumble():
-            myStr += " <b><font color=red>Fumble!</font>  POW Cost: [" + str(self.pow) + "],</b> " + fum_string
-        elif self.is_critical():
-            myStr += " <b><font color=green>Critical!</font></b> POW Cost: [1] "
-        elif self.is_special():
-            myStr += " <i><font color=green>Special!</font></i> POW Cost: [" + str(special_pow) + "] "
-        elif self.is_success():
-            myStr += " <font color=blue>Success!</font> POW Cost: [" + str(self.pow) + "] "
-        else:
-            myStr += " <font color=red>Failure!</font> POW Cost: [1]"
-
-        # print spell details
-        myStr += "<br /> --- Other Modifiers:["    + str( extra_mod     ) + "], "
-        myStr += "Extra POW:[" + str( extra_pow     ) + "], "
-        myStr += "Ceremony:[+"          + str( ceremony_roll ) + "%], "
-        myStr += "Intensity(-3):["      + str( self.int      ) + "], "
-        myStr += "Accelerate(-5):["     + str( self.acc      ) + "], "
-        myStr += "Multispell(-10):["    + str( self.mlt      ) + "] ---"
-
-        return myStr
-
--- a/orpg/dieroller/rollers/savage.py	Tue Oct 06 16:03:42 2009 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,509 +0,0 @@
-# (at your option) any later version.
-# # This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-# --
-#
-# File: savage.py
-# Authors: Rich Finder
-#        : Alexandre Major
-# Maintainer:
-# Version: 0.2
-#
-# Description: Savage Worlds die roller
-# Permission was granted by Pinnacle to reprint the result descriptions from their tables on Apr 20, 2006 by Simon Lucas
-#
-
-__version__ = "$Id: savage.py,v 1.2 2007/05/06 16:42:55 digitalxero Exp $"
-
-import string
-from random import *
-
-from std import std
-from orpg.dieroller.base import *
-
-# Savage, as in Savage Worlds
-class sw(std):
-    #def __init__(self,source=[], wnd=1, loc="rnd", chmod=0):
-    def __init__(self,source=[],fmod=0):
-        std.__init__(self,source)
-
-# these methods return new die objects for specific options
-
-    def fright(self,fearmod=0):
-        return fright(self,fearmod=0)
-
-    def kob(self,wnd,loc):
-        return kob(self,wnd=1,loc="rnd")
-
-    def ooc(self):
-        return ooc(self)
-
-    def ract(self,chmod=0):
-        return ract(self,chmod=0)
-
-    def vcrit(self):
-        return vcrit(self)
-
-    def fortune(self):
-        return fortune(self)
-
-    def freak(self):
-        return freak(self)
-
-    def swdhelps(self):
-        return swdhelps(self)
-
-die_rollers.register(sw)
-
-class fright(std):
-    #-----------------The Fright Table
-    #  Rolls on the Fright - which is a 1d20 roll modified by the fear level of a monster.  This function automatically generates
-    #  The appropriate random number and then adds the fear modifier to the rol and displays the result of the roll with the effect
-    #  of that roll.
-    #  Usage:  [fright()]
-    #          [fright(6)] - if the fear modifier of the monster was 6
-    #-----------------
-    def __init__(self,fmod=0):
-        global fear
-        std.__init__(self)
-        fear=fmod
-
-    #def sum(self):
-
-    def __str__(self):
-        global fear
-        iroll = randint(1,20)
-        froll = iroll + fear
-        if froll >= 1 and froll <=4:
-            fresult = "Adrenaline Rush"
-            fdescription = "The hero's \"fight\" response takes over.  He adds +2 to all Trait and damage rolls on his next action."
-        elif froll >= 5 and froll <=8:
-            fresult = "Shaken"
-            fdescription = "The character is Shaken."
-        elif froll >=9 and froll <=12:
-            fresult = "Pankicked"
-            fdescription = "The character immediately moves his full Pace plus running die away from the danger and is Shaken."
-        elif froll >=13 and froll <=16:
-            fresult = "Minor Phobia"
-            fdescription = "The character gains a Minor Phobia Hindrance somehow associated with the trauma."
-        elif froll >=17 and froll <=18:
-            fresult = "Major Phobia"
-            fdescription = "The character gains a Major Phobia Hindrance."
-        elif froll >=19 and froll <= 20:
-            fresult = "The Mark of Fear"
-            fdescription = "The hero is Shaken and also suffers some cosmetic, physical alteration -- a white streak forms in the hero's hair, his eyes twitch constantly, or some other minor physical alteration.  This reduces his Charisma by 1."
-        else:
-            fresult = "Heart Attack"
-            fdescription = "The hero is so overwhelmed with fear that his heart stutters.  He becomes Incapacitated and must make a Vigor roll at -2.  If Successful, he's Shaken and can't attempt to recover for 1d4 rounds.  If he fails, he dies in 2d6 rounds.  A Healing roll at -4 saves the victim's life, but he remains Incapacitated."
-        myStr = "[" + str(iroll) + "+"+str(fear)+"="+str(froll)+"] ==> " + fresult +":  "+ fdescription
-        return myStr
-
-class kob(std):
-    #-------------------The Knockout Blow Table
-    #  This table used when a character has sustained more than 3 wounds.  The number wounds taken that sends a character to the
-    #  Knockout Blow Table is what gets sent with the kob command - not the total number of wounds the character currently has.
-    #  For example - a character has 2 wounds and is hit takes 2 more wounds, this will result in a total of 4 wounds, but the
-    #  number that gets sent to the kob roll is 2, because that is the number of wounds sustained that sent the character to the kob
-    #  table.
-    #
-    #  It is also important to note that if a called shot to particular area was made, that information should be sent with the "roll"
-    #  as well, because the KOB Table may need to determine some dramatic effects for permanent injuries, etc.  If a hit location was sent
-    #  the function will use that information.
-    #  Valid Hit Locations are:  h (head), g (guts), la (left arm), ra (right arm), rl (right leg), ll (left leg), c (crotch)
-    #  Usage = [kob(3)] - If 3 wounds were received that sent the player to the Knockout Blow Table - no called shot
-    #          [kob(3,"h") - If 3 wounds were received that sent the player to the Knockout Blow Table with a called shot to the head
-    #---------------------
-    global wound, loca
-    def __init__(self, wnd, loc="rnd"):
-        global wound, loca
-        std.__init__(self, wnd)
-        #Need to check to make sure that wnd is a number
-        if (int(wnd)):
-            wound = wnd
-            loca = loc
-        else:
-            mystr = "You need to supply a number for the wound."
-            return mystr
-
-    def __str__(self):
-        global wound, loca
-        itbl = "no"
-        if wound == 1:
-            wtype = "Battered and Bruised"
-            wdescription = "If your hero was previously Incapacitated, this result has no further effect. Otherwise, your hero's had the wind knocked out of him. Make a Spirit roll at the beginning of each round. If the roll is successful, he becomes Shaken and can return to the fight."
-        elif wound == 2:  #Need to roll on the Injury table as well
-            wtype = "Incapacitated"
-            wdescription = "Your hero is beaten badly enough to take him out of this fight. He's Incapacitated and must roll on the Injury Table."
-            itbl = "yes"
-        elif wound == 3:
-            wtype = "Bleeding Out"
-            wdescription = "Your hero is bleeding out and Incapacitated. Roll on the Injury Table and make a Vigor roll at the start of each combat round. A failure means the hero has lost too much blood and becomes mortally Wounded (see below; begin rolling for the Mortal Wound in the next round). With a success, he keeps bleeding and must roll again next round. With a raise, or a successful Healing roll, he stops bleeding and is Incapacitated."
-            itbl = "yes"
-        elif wound < 1:
-            wtype = "No Wounds?"
-            wdescription = "The Number of wounds specified was less than one...why are you consulting this chart?"
-        else:
-            wtype = "Mortal Wound"
-            wdescription = "Your hero has suffered a life-threatening wound and will not recover without aid. He is Incapacitated and must roll on the Injury Table. He must also make a Vigor roll at the start of each round. If the roll is failed, he passes on. A Healing roll stabilizes the victim but leaves him Incapacitated."
-            itbl = "yes"
-
-        if itbl == "yes":
-            #Determine if a Hit location was specified already
-            if loca.lower() == "h":
-                iroll = 11
-            elif loca.lower() == "g":
-                iroll = 5
-            elif loca.lower() == "ra":
-                iroll = 3
-                aroll = 2
-            elif loca.lower() == "la":
-                iroll = 3
-                aroll = 1
-            elif loca.lower() == "rl":
-                iroll = 10
-                lroll = 2
-            elif loca.lower() == "ll":
-                iroll = 10
-                lroll = 1
-            elif loca.lower() == "c":
-                iroll = 2
-            else:  #none of the above were provided...wo will need to determine randomly
-                iroll = randint(2,12)
-            #resolve the injury table stuff...
-            if iroll == 2:
-                iloc = "Unmentionables"
-                idescription = "The hero suffers an embarrassing and painful wound to the groin."
-            elif iroll == 3 or iroll == 4:
-                if loca != "ra" and loca != "la":  #  If a hit location was not specified (or not recognized) already, determine randomly
-                    aroll = randint(1,2)
-                if aroll == 1:
-                    warm = "Left"
-                else:
-                    warm = "Right"
-                iloc = warm + " Arm"
-                idescription = "The arm is rendered useless."
-            elif iroll >= 5 and iroll <= 9:  #will need to make another random roll
-                iloc = "Guts"
-                idescription = "Your hero catches one somewhere between the crotch and the chin."
-                groll = randint(1,6)
-                if groll == 1 or groll == 2:
-                    #idescription += " <b>Broken (" + str(groll) + ")</b> His Agility is reduced by a die type (min dr)."
-                    idescription += " <b>Broken (" + str(groll) + ")</b> His Agility is reduced by a die type (min d4)."
-                elif groll == 3 or groll == 4:
-                    idescription += " <b>Battered (" + str(groll) + ")</b> His Vigor is reduced by a die type (min d4)."
-                else:
-                    idescription += " <b>Busted (" + str(groll) + ")</b> His Strength is reduced by a die type (min d4)."
-            elif iroll == 10:
-                if loca != "ll" and loca != "rl":  #  If a hit location was not specified (or not recognized) already, determine randomly
-                    lroll = randint(1,2)
-                if lroll == 1:
-                    wleg = "Left"
-                else:
-                    wleg = "Right"
-                iloc = wleg + " Leg"
-                idescription = "The character's leg is crushed, broken, or mangled. His Pace is reduced by 1."
-            else:  #Will need to make another random roll for this one.
-                iloc = "Head"
-                idescription = "Your hero has suffered a grievous injury to his head."
-                hroll = randint(1,6)  #determine how the head is impacted by the wound
-                if hroll == 1 or hroll ==2:
-                    idescription += "<b>Hideous Scar (" + str(hroll) + ")</b>Your hero now has the Ugly Hindrance."
-                elif hroll == 3 or hroll == 4:
-                    idescription += "<b>Blinded (" + str(hroll) + ")</b> One or both of your hero's eyes was damaged. He gains the Bad Eyes Hindrance."
-                else:
-                    idescription += "<b>Brain Damage (" + str(hroll) + ")</b> Your hero suffers massive trauma to the head. His Smarts is reduced one die type (min d4)."
-            idescription += " Make a Vigor roll applying any wound modifiers. If the Vigor roll is failed, the injury is permanent regardless of healing. If the roll is successful, the effect goes away when all wounds are healed."
-            if iroll == 2:
-                idescription +=" If the injury is permanent, reproduction is out of the question without miracle surgery or magic."
-            if loca != "h" and loca != "g" and loca != "c" and loca != "rl" and loca != "ll" and loca != "ra" and loca != "la":
-                idescription +="<br><br><b>***If the attack that caused the Injury was directed at a specific body part, use that location instead of rolling randomly.***</b>"
-            myStr = "[" + wtype + "] ==>" + wdescription + "<br><br><b>Injury Table Result ("+ str(iroll) +"): </b> [" + iloc + "] ==> " + idescription
-        else:
-            myStr = "[" + wtype + "] ==>" + wdescription
-        return myStr
-
-class ract(std):
-    #----------------------The Reaction Table
-    #  This is used to randomly determine the general mood of NPCs toward the player characters.  This simulates a 2d6 roll
-    #  and displays the reaction.  This roll can be modified by the Charisma of the player(s).
-    #  Usage:  [ract()] - No Charisma modifier
-    #          [ract(2)] - A +2 Charisma modifier
-    #          [ract(-2)] - A -2 Charisma modifier
-    #----------------------
-    global charisma
-    def __init__(self,chmod=0):
-        global charisma
-        std.__init__(self)
-        charisma = chmod
-
-    def __str__(self):
-        global charisma
-        r1roll = randint(2,12)
-        rroll = r1roll + charisma
-        if rroll == 2:
-            reaction = "Hostile"
-            rdescription = "The NPC is openly hostile and does his best to stand in the hero's way. He won't help without an overwhelming reward or payment of some kind."
-        elif rroll >=3 and rroll <=4:
-            reaction = "Unfriendly"
-            rdescription = "The NPC is openly hostile and does his best to stand in the hero's way. He won't help without an overwhelming reward or payment of some kind."
-        elif rroll >=5 and rroll <=9:
-            reaction = "Neutral"
-            rdescription = "The NPC has no particular attitude, and will help for little reward if the task at hand is very easy. If the task is difficult, he'll require substantial payment of some kind."
-        elif rroll >=10 and rroll <=11:
-            reaction = "Friendly"
-            rdescription = "The NPC will go out of his way for the hero. He'll likely do easy tasks for free (or very little), and is willing to do more dangerous tasks for fair pay or other favors."
-        else:
-            reaction = "Helpful"
-            rdescription = "The NPC is anxious to help the hero, and will probably do so for little or no pay depending on the nature of the task."
-        #myStr = "[" + reaction + "(" + str(r1roll) + "+Charisma Mods("+str(charisma)+")="+str(rroll)+")] ==> " + rdescription
-        myStr = "["+str(r1roll)+"+"+str(charisma)+"(charisma modifier)="+str(rroll)+"] ==> "+reaction+":  "+rdescription
-        return myStr
-
-class ooc(std):
-    #--------------------The Out of Control Vehicle Table
-    #  This table is used when a vehicle is injured during combat and must determine what happens to the vehicle.  This is a 2d6
-    #  roll and displays the results of the roll.  This will also display altitude information for flying vehicles.
-    #  Usage:  [ooc()]
-    #--------------------
-    def __init__(self):
-        std.__init__(self)
-
-    def __str__(self):
-        ooroll = randint(2,12)
-        oodescripton = "Something"
-        if ooroll == 2:
-            ooeffect = "Roll Over"
-            rroll = randint(1,6)
-            oodescription = "The vehicle performs a Slip and rolls over "+ str(rroll)+ " time"
-            if rroll < 2:
-                oodescription +="s"
-            oodescription += " in that direction. Roll collision damage for the vehicle and everyone inside. Any exterior-mounted weapons or accessories are ruined."
-        elif ooroll == 3 or ooroll == 4:
-            ooeffect = "Spin"
-            sroll = randint(1,6)
-            froll = randint(1,12)
-            oodescription = "Move the vehicle "+str(sroll)+"\" in the direction of the maneuver, or "+str(sroll)+"\" away from a damaging blow. At the end of the Spin,the vehicle is facing is "+str(froll)+" o'clock."
-        elif ooroll >= 5 and ooroll <= 9:
-            ooeffect = "Skid"
-            sroll = randint(1,4)
-            oodescription = "Move the vehicle "+str(sroll)+"\" left or right (in the direction of a failed maneuver, or away from a damaging attack)."
-        elif ooroll == 10 or ooroll == 11:
-            ooeffect = "Slip"
-            sroll = randint(1,6)
-            oodescription = "Move the vehicle "+str(sroll)+"\" left or right (in the direction of a failed maneuver, or away from a damaging attack)."
-        else:
-            ooeffect = "Flip"
-            froll = randint(1,4)
-            oodescription = "The vehicle flips end over end "+str(froll)+" times. Move it forward that many increments of its own length. Roll collision damage for the vehicle, its passengers, and anything it hits. "
-            shroll = randint(1,2)
-            if shroll == 1:
-                oodescription += "<br><br><b>Note:</b> If the vehicle is slow and/or heavy (such as a tank) it Slips instead: "
-                sroll = randint(1,6)
-                oodescription += "Move the vehicle "+str(sroll)+"\" left or right (in the direction of a failed maneuver, or away from a damaging attack)."
-            else:
-                oodescription += "<br><br><b>Note (GM's discretion):</b> If the vehicle is slow and/or heavy (such as a tank) it Skids instead: "
-                sroll = randint(1,4)
-                oodescription += "Move the vehicle "+str(sroll)+"\" left or right (in the direction of a failed maneuver, or away from a damaging attack)."
-
-        oodescription += "<br><br>For flying vehicles conducting combat in the air, the vehicle"
-        altchange = randint(2,12)
-        if altchange == 2:
-            dwn = randint(2,20)
-            oodescription += " loses "+str(dwn)+"\" of altitude."
-        elif altchange == 3 or altchange == 4:
-            dwn = randint(1,10)
-            oodescription += " loses "+str(dwn)+"\" of altitude."
-        elif altchange >= 5 and altchange <= 9:
-            oodescription += " has no change in altitude."
-        else:
-            altup = randint(1,10)
-            oodescription += " gains "+str(altup)+"\" of altitude."
-        myStr = "[" + ooeffect + "(" + str(ooroll) + ")] ==> " + oodescription
-        return myStr
-
-class vcrit(std):
-    #----------------The Critical Hit Vehicle Table
-    #  This table generates a 2d6 roll to determine the Critical Hit results every time a vehicle takes a wound.  There are no
-    #  modifiers to this roll
-    #  Usage [vcrit()]
-    #----------------
-    def __init__(self):
-        std.__init__(self)
-
-    def __str__(self):
-        chitroll = randint(2,12)
-        if chitroll == 2:
-            cheffect = "Scratch and Dent"
-            chdescription = "The attack merely scratches the paint. There's no permanent damage."
-        elif chitroll == 3:
-            cheffect = "Engine"
-            chdescription = "The engine is hit. Oil leaks, pistons misfire, etc. Acceleration is halved (round down). This does not affect deceleration, however."
-        elif chitroll == 4:
-            cheffect = "Locomotion"
-            chdescription = "The wheels, tracks, or whatever have been hit. Halve the vehicle's Top Speed immediately. If the vehicle is pulled by animals, the shot hits one of them instead."
-        elif chitroll == 5:  #Need to make an additional roll to see what direction the vehicle can turn...
-            cheffect = "Controls"
-            troll = randint(1,2)
-            if troll == 1:
-                aturn = "left"
-            else:
-                aturn = "right"
-            chdescription = "The control system is hit. Until a Repair roll is made, the vehicle can only perform turns to the "+str(aturn)+". This may prohibit certain maneuvers as well."
-        elif chitroll >= 6 and chitroll <=8:
-            cheffect = "Chassis"
-            chdescription = "The vehicle suffers a hit in the body with no special effects."
-        elif chitroll == 9 or chitroll == 10:
-            cheffect = "Crew"
-            chdescription = "A random crew member is hit. The damage from the attack is rerolled. If the character is inside the vehicle, subtract the vehicle's Armor from the damage. Damage caused by an explosion affects all passengers in the vehicle."
-        elif chitroll == 11:
-            cheffect = "Weapon"
-            chdescription = "A random weapon on the side of the vehicle that was hit is destroyed and may no longer be used. If there is no weapon, this is a Chassis hit instead (The vehicle suffers a hit in the body with no special effects)."
-        else:
-            cheffect = "Wrecked"
-            chdescription = "The vehicle is wrecked and automatically goes Out of Control.<br><br><b>[Out of Control]</b> ==>"+str(ooc())
-        myStr = "["+cheffect+" ("+str(chitroll)+")] ==> "+chdescription
-        return myStr
-
-    def ooc(self):
-        return vcritooc(self)
-
-class swdhelps(std):
-    #Display help information for this die roller - it will list all the available commands, and how to use them
-    def __init__(self):
-        std.__init__(self)
-
-    def __str__(self):
-        myStr = "<table border='1' valign='top'>\
-        <tr>\
-            <td colspan='3'>This chart will show you the various commands you can use and what is required, etc.  The <i><b>italicized text</b></i> are optional variables.  Any text that is not italicized and is within parentheses is required.  About the only roll that has a required element is the Knockout Blow roll (kob).</td>\
-        </tr>\
-        <tr>\
-            <td align='center'><b>Die Command</b></td><td align='center' width='55%'><b>Description</b></td><td align='center'width='30%'><b>Example</b></td>\
-        </tr>\
-        <tr>\
-            <td><b>[fright(<i>monster's fear modifier</i>)]</b></td><td>Rolls on the <b>Fright Table</b>.  This command generates a number between 1 and 20 and displays the corresponding entry from the Fright Table.</td><td>[fright()]<br>[fright(6)]</td>\
-        </tr>\
-        <tr>\
-            <td><b>[kob(#ofWounds,<i>hitLocation</i>)]</b></td><td>Rolls on the <b>Knockout Blow Table</b> as well as the <b>Injury Table</b> if necessary.  The number of wounds must be specified, however, the location only needs to be specified if a particular body part was targeted.  If a hit location was targeted, then the following codes should be used:<br>\
-                <ul>\
-                    <li>h = head</li>\
-                    <li>g = guts/other vital areas</li>\
-                    <li>c = crotch/groin</li>\
-                    <li>la = left arm</li>\
-                    <li>ra = right arm</li>\
-                    <li>ll = left leg</li>\
-                    <li>rl = right leg</li>\
-                </ul><br>If no hit location is specified, the hit location will be determined when the roll on the Injury Table is necessary.  When specifiying a hit locations, the code must be entered within double quotes.</td><td><b>3 wounds, no called shot</b><br>[kob(3)]<br><b>2 wounds to the head</b><br>[kob(2,\"h\")]</td>\
-        </tr>\
-        <tr>\
-            <td><b>[ract(<i>Charisma Mods</i>)]</b></td><td>Rolls on the <b>Reaction Table</b>.  Will generate the initial reaction to the PCs.  If the Charisma modifiers are supplied, they will be taken into account as well.  Remember that this table is generally only consulted when the reaction of the NPC is comlpetely unknown to the GM.</td><td><b>Reaction no Charisma Mods</b><br>[ract()]<br><b>Reaction with +2 Charisma Mods</b><br>[ract(2)]</td>\
-        </tr>\
-        <tr>\
-            <td><b>[vcrit()]</b></td><td>Rolls on the <b>Critical Hit Table</b> for vehicles.  If a roll on the Out of Control Chart is necessary, it will automatically roll on that table as well.</td><td>[vcrit()]</td>\
-        </tr>\
-        <tr>\
-            <td><b>[ooc()]</b></td><td>Rolls on the <b>Out of Controll Table</b> for vehicles.  This roll will automatically determine any directions/movement rolls as well.</td><td>[ooc()]</td>\
-        </tr>\
-        <tr>\
-            <td><b>[fortune()]</b></td><td>Rolls on the <b>Fortune Table</b> for the Showdown Skirmish rules.  This roll will automatically roll on the <b>Freak Event Table</b> if necessary</td><td>[fortune()]</td>\
-        </tr>\
-        <tr>\
-            <td><b>[freak()]</b></td><td>Rolls on the <b>Freak Event Table</b>.</td><td>[freak()]</td>\
-        </tr>\
-        <tr>\
-            <td><b>[swdhelps()]</b></td><td>Displays this help list.</td><td>[swdhelps()]</td>\
-        </tr>\
-        </table>"
-        return myStr
-
-class fortune(std):
-    def __init___(self):
-        std.__init__(self)
-
-    def __str__(self):
-        forroll = randint(2,12)
-        if forroll == 2 or forroll == 12: #Need to roll on Freak Event Table
-            fortune = "Freak Event!"
-            fdescription = "Roll on the Freak Event Table.<br><br><b>[Freak Event Table]</b> ==> "+str(freak())
-        elif forroll == 3:
-            fortune = "Twist of Fate"
-            fdescription = "Take a benny from your opponent. If he does not have one, he must immediately remove any one Extra from play."
-        elif forroll == 4:
-            fortune = "The Quick and the Dead"
-            fdescription = "Swap one of your opponent's cards for any one of yours."
-        elif forroll == 5:
-            fortune = "Rally"
-            fdescription = "Pick any one unit on the board with Shaken figures. All those figures recover automatically."
-        elif forroll >= 6 and forroll <= 8:
-            fortune = "Hand of Fate"
-            fdescription = "Gain one extra benny."
-        elif forroll == 9:
-            fortune = "Close Call"
-            fdescription = "Any one of your opponent's units stumbles, becomes confused, or is otherwise disrupted. All its members suffer -2 to their trait rolls this round."
-        elif forroll == 10:
-            fortune = "Teamwork"
-            fdescription = "Pick any one other unit within 12\" of this one. Discard its Action Card. It acts on the Joker along with this unit, and gains the usual bonuses as well."
-        else:
-            fortune = "Out of Ammo"
-            fdescription = "Pick any one enemy unit. It's out of ammo or Power Points (your choice). If this result cannot be applied, you gain a benny instead."
-        myStr = "["+fortune+" ("+str(forroll)+")] ==>"+fdescription
-        return myStr
-
-    def freak(self):
-        return fortunefreak(self)
-
-class freak(std):
-    def __init__(self):
-        std.__init__(self)
-
-    def __str__(self):
-        feroll = randint(1,10)
-        if feroll == 1:
-            fevent = "Storm"
-            fedescription = "A sudden storm rolls in. Rain begins to pour and visibility is limited to 12\". All attack rolls are at -1, and black powder weapons don't work at all. The round after this event, all streams become impassable, even at fords. Only bridges remain."
-        elif feroll == 2:
-            fevent = "Fire!"
-            fedescription = "Fire breaks out on the board! Roll randomly among each occupied building, patch of trees, or other flammable terrain type. If none of these are occupied, roll randomly among all flammable terrain pieces. The entire building or forest catches fire this round and causes 2d6 damage to everything within. The fire continues for the rest of the game--unless a storm comes, which quenches it immediately.<br><br>At the beginning of each turn thereafter, roll 1d6 for each flammable structure within 4\" (adjacent buildings, another patch of forest, etc.). On a 4-6, that structure catches fire as well. Check to see if these new fires spread in the following rounds."
-        elif feroll == 3:
-            fevent = "Blood Ties"
-            fedescription = "One of the Wild Cards on the other side is related or has some other special bond with one of your heroes (a Wild Card of your choice). For the rest of the battle, these two won't attack each other directly unless there are no other targets on the board."
-        elif feroll == 4:
-            fevent = "Death of a Hero"
-            inspireroll = randint(1,2)
-            if inspireroll == 1:
-                fedescription ="The next time one of your Wild Cards dies, his noble sacrifice triggers new resolve in his companions.  When your next Wild Card is Incapacitated the rest of your force is inspired by his legacy and adds +1 to all their rolls until another of your Wild Cards is killed."
-            else:
-                fedescription = "The next time one of your Wild Cards dies, his noble sacrifice triggers bone-chilling dread in his companions. When your next Wild Card is Incapacitated the rest of your force is filled with dread. They subtract -1 from all their rolls for the rest of the game until an <i>enemy</i> Wild Card is slain."
-        elif feroll == 5:
-            fevent = "Fickle Fate"
-            fedescription = "Fate favors the underdog. The side with the fewest bennies draws until it has the same number as their foe. Place these in the common pool."
-        elif feroll == 6:
-            fevent = "Back from the Dead"
-            fedescription = "One of your dead was just knocked unconscious. He returns in the spot where he fell. If this is a Wild Card, he returns with but a single wound."
-        elif feroll == 7:
-            fevent = "Bitter Cold/Heat"
-            fedescription = "The weather heats up or cools down, depending on your environment. All troops become tired or bogged down and reduce their running rolls by half for the rest of the game."
-        elif feroll == 8:
-            fevent = "Battle Tested"
-            fedescription = "Any one of your units improves any one skill or attribute a die type immediately."
-        elif feroll == 9:
-            fevent = "The Fog"
-            fedescription = "Dense fog, mist, or smoke rolls drifts over the battlefield. Place two connected Large Burst Templates at the center of one randomly determined board edge. The fog drifts 2d6\" each round in a random direction (roll a d12 and read it like a clock facing). The fog \"bounces\" if it hits an edge in a random direction (so that it never leaves the field)."
-        else:
-            fevent = "Reinforcements"
-            fedescription = "A group of your most common currently-fielded troop type arrives on the field of battle! Place these troops in your deployment area. They act on the Joker this round and are dealt in normally hereafter."
-        myStr = "["+fevent+"("+str(feroll)+")] ==> "+fedescription
-        return myStr
-
-class rdm(std):  #If I get the time and the inspiration - I may try to incorporate a Random Table roller...  I need to think about this one.
-    def __init__(self):
-        std.__init__(self)
-
-    def __str__(self):
-        return myStr
--- a/orpg/dieroller/rollers/sr4.py	Tue Oct 06 16:03:42 2009 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,237 +0,0 @@
-## a vs die roller as used by WOD games
-#!/usr/bin/env python
-# Copyright (C) 2000-2001 The OpenRPG Project
-#
-#   openrpg-dev@lists.sourceforge.net
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-# --
-#
-# File: sr4.py
-# Author: Veggiesama, ripped straight from Michael Edwards (AKA akoman)
-# Maintainer:
-# Version: 1.1
-#
-# 1.1: Now with glitch and critical glitch detection!
-# 1.1: Cleaned up some of the output to make it simpler.
-#
-# Description: Modified from the original Shadowrun dieroller by akoman,
-#              but altered to follow the new Shadowrun 4th Ed dice system.
-#
-#              SR4 VS
-#              Typing [Xd6.vs(Y)] will roll X dice, checking each die
-#              roll against the MIN_TARGET_NUMBER (default: 5). If it
-#              meets or beats it, it counts as a hit. If the total hits
-#              meet or beat the Y value (threshold), there's a success.
-#
-#              SR4 EDGE VS
-#              Identical to the above function, except it looks like
-#              [Xd6.edge(Y)] and follows the "Rule of Six". That rule
-#              states any roll of 6 is counted as a hit and rerolled
-#              with a potential to score more hits. The "Edge" bonus
-#              dice must be included into X.
-#
-#              SR4 INIT
-#              Typing [Xd6.init(Y)] will roll X dice, checking each
-#              die for a hit. All hits are added to Y (the init attrib
-#              of the player), to give an Init Score for the combat.
-#
-#              SR4 EDGE INIT
-#              Typing [Xd6.initedge(Y)] or [Xd6.edgeinit(Y)] will do
-#              as above, except adding the possibility of Edge dice.
-#
-#              Note about non-traditional uses:
-#              - D6's are not required. This script will work with any
-#                die possible, and the "Rule of Six" will only trigger
-#                on the highest die roll possible. Not throughly tested.
-#              - If you want to alter the minimum target number (ex.
-#                score a hit on a 4, 5, or 6), scroll down and change
-#                the global value MIN_TARGET_NUMBER to your liking.
-
-__version__ = "1.1"
-
-from std import std
-from orpg.dieroller.base import *
-
-MIN_TARGET_NUMBER = 5
-GLITCH_NUMBER = 1
-
-class sr4(std):
-    name = "sr4"
-
-    def __init__(self,source=[]):
-        std.__init__(self,source)
-        self.threshold = None
-        self.init_attrib = None
-
-    def vs(self,threshold=0):
-        return sr4vs(self, threshold)
-
-    def edge(self,threshold=0):
-        return sr4vs(self, threshold, 1)
-
-    def init(self,init_attrib=0):
-        return sr4init(self, init_attrib)
-
-    def initedge(self,init_attrib=0):
-        return sr4init(self, init_attrib, 1)
-    def edgeinit(self,init_attrib=0):
-        return sr4init(self, init_attrib, 1)
-
-    def countEdge(self,num):
-        if num <= 1:
-            self
-        done = 1
-        for i in range(len(self.data)):
-            if (self.data[i].lastroll() >= num):
-                # counts every rerolled 6 as a hit
-                self.hits += 1
-                self.data[i].extraroll()
-                self.total += 1
-                done = 0
-            elif (self.data[i].lastroll() <= GLITCH_NUMBER):
-                self.ones += 1
-            self.total += 1
-        if done:
-            return self
-        else:
-            return self.countEdge(num)
-
-    def countHits(self,num):
-        for i in range(len(self.data)):
-            if (self.data[i].lastroll() >= MIN_TARGET_NUMBER):
-                # (Rule of Six taken into account in countEdge(), not here)
-                self.hits += 1
-            elif (self.data[i].lastroll() <= GLITCH_NUMBER):
-                self.ones += 1
-            self.total += 1
-
-    def __str__(self):
-        if len(self.data) > 0:
-            self.hits = 0
-            self.ones = 0
-            self.total = 0
-            for i in range(len(self.data)):
-                if (self.data[i].lastroll() >= MIN_TARGET_NUMBER):
-                    self.hits += 1
-                elif (self.data[i].lastroll() <= GLITCH_NUMBER):
-                    self.ones += 1
-                self.total += 1
-            firstpass = 0
-            myStr = "["
-            for a in self.data[0:]:
-                if firstpass != 0:
-                    myStr += ","
-                firstpass = 1
-                if a >= MIN_TARGET_NUMBER:
-                    myStr += "<B>" + str(a) + "</B>"
-                elif a <= GLITCH_NUMBER:
-                    myStr += "<i>" + str(a) + "</i>"
-                else:
-                    myStr += str(a)
-            myStr += "] " + CheckIfGlitch(self.ones, self.hits, self.total)
-            myStr += "Hits: (" + str(self.hits) + ")"
-        else:
-            myStr = "[] = (0)"
-        return myStr
-
-die_rollers.register(sr4)
-
-class sr4init(sr4):
-    def __init__(self,source=[],init_attrib=1,edge=0):
-        std.__init__(self,source)
-        if init_attrib < 2:
-            self.init_attrib = 2
-        else:
-            self.init_attrib = init_attrib
-        self.dicesides = self[0].sides
-        self.hits = 0
-        self.ones = 0
-        self.total = 0
-        if edge:
-            self.countEdge(self.dicesides)
-        self.countHits(self.dicesides)
-
-    def __str__(self):
-        if len(self.data) > 0:
-            firstpass = 0
-            myStr = "["
-            for a in self.data[0:]:
-                if firstpass != 0:
-                    myStr += ","
-                firstpass = 1
-                if a >= MIN_TARGET_NUMBER:
-                    myStr += "<B>" + str(a) + "</B>"
-                elif a <= GLITCH_NUMBER:
-                    myStr += "<i>" + str(a) + "</i>"
-                else:
-                    myStr += str(a)
-            myStr += "] " + CheckIfGlitch(self.ones, self.hits, self.total)
-            init_score = str(self.init_attrib + self.hits)
-            myStr += "InitScore: " + str(self.init_attrib) + "+"
-            myStr += str(self.hits) + " = (" + init_score + ")"
-        else:
-            myStr = "[] = (0)"
-        return myStr
-
-class sr4vs(sr4):
-    def __init__(self,source=[], threshold=1, edge=0):
-        std.__init__(self, source)
-        if threshold < 0:
-            self.threshold = 0
-        else:
-            self.threshold = threshold
-        self.dicesides = self[0].sides
-        self.hits = 0
-        self.ones = 0
-        self.total = 0
-        if edge:
-            self.countEdge(self.dicesides)
-        self.countHits(self.dicesides)
-
-    def __str__(self):
-        if len(self.data) > 0:
-            firstpass = 0
-            myStr = "["
-            for a in self.data[0:]:
-                if firstpass != 0:
-                    myStr += ","
-                firstpass = 1
-                if a >= MIN_TARGET_NUMBER:
-                    myStr += "<B>" + str(a) + "</B>"
-                elif a <= GLITCH_NUMBER:
-                    myStr += "<i>" + str(a) + "</i>"
-                else:
-                    myStr += str(a)
-            #myStr += "] Threshold=" + str(self.threshold)
-            myStr += "] vs " + str(self.threshold) + " "
-            myStr += CheckIfGlitch(self.ones, self.hits, self.total)
-            if self.hits >= self.threshold:
-                myStr += "*SUCCESS* "
-            else:
-                myStr += "*FAILURE* "
-            myStr += "Hits: (" + str(self.hits) + ")"
-        else:
-            myStr = "[] = (0)"
-        return myStr
-
-def CheckIfGlitch(ones, hits, total_dice):
-    if (ones * 2) >= total_dice:
-        if hits >= 1:
-            return "*GLITCH* "
-        else:
-            return "*CRITICAL GLITCH* "
-    else:
-        return ""
--- a/orpg/dieroller/rollers/std.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/dieroller/rollers/std.py	Mon Mar 22 18:38:22 2010 -0600
@@ -1,10 +1,138 @@
-from orpg.dieroller.base import die_base, die_rollers
+from orpg.dieroller._base import roller_manager, BaseRoller
 
-class std(die_base):
+class StandardRoller(BaseRoller):
+    """
+    Basic roller designed to accept any combination of dice & sides,
+    the results of each die are added up at the and providing a result
+    total for the roll
+    """
     name = "std"
 
+    def ascending(self):
+        """
+        Sort the results of each die in ascending order
+        """
+        roll = self.history.pop()
+        roll.sort()
+        self.history.append(roll)
+
+    def descending(self):
+        """
+        Sort the result of each die in descending order
+        """
+        roll = self.history.pop()
+        roll.sort(reverse=True)
+        self.history.append(roll)
+
+    def takeHighest(self, num):
+        """
+        Take the highest `num` results of the roll
+        eg [4d6.takeHighest(3)] => [4, 4, 6] = (14)
+        """
+        roll = self.history.pop()
+        roll.sort(reverse=True)
+        roll.modified = roll.result[:num] if not roll.modified else roll.modified[:num]
+        self.history.append(roll)
+
+    def takeLowest(self, num):
+        """
+        Take the lowest `num` results of the roll
+        eg [5d6.takeLowest(3)] => [1, 2, 3] = (6)
+        """
+        roll = self.history.pop()
+        roll.sort()
+        roll.modified = roll.result[:num] if not roll.modified else roll.modified[:num]
+        self.history.append(roll)
+
+    def extra(self, num):
+        """
+        Add an extra die for each current roll larger then `num`
+        eg [1d6.extra(2)] => [[3, 4]] = (7)
+        """
+        roll = self.history.pop()
+        roll.modified = roll.modified or roll.result
+
+        def do_extra(roll_):
+            for i in xrange(len(roll_)):
+                r = roll_.modified[i] if roll_.modified else roll_.result[i]
+                if not isinstance(r, (int, type(None))):
+                    do_extra(r)
+                elif r >= num:
+                    self.extend_roll(roll_, i)
+        do_extra(roll)
+        self.history.append(roll)
+
+    def open(self, num):
+        """
+        Similar to extra, but it keeps looping over the roll and doing more
+        rolls until no new rolls are larger then `num`
+        eg [1d6.open(2)] => [[3, 4, 5, 3, 2]] = (17)
+        """
+        roll = self.history.pop()
+        roll.modified = roll.modified or roll.result
+
+        def do_open(roll_):
+            for i in xrange(len(roll_)):
+                r = roll_.modified[i] if roll_.modified else roll_.result[i]
+                if not isinstance(r, (int, type(None))):
+                    do_open(r)
+                else:
+                    while r >= num:
+                        self.extend_roll(roll_, i)
+                        r = roll_.modified[i].result[-1]
+
+        do_open(roll)
+        self.history.append(roll)
+
+    def minroll(self, num):
+        """
+        Ensure that no die roll is less then `num`
+        eg [3d6.minroll(2)] => [2, 5, 3] = (10)
+        """
+        roll = self.history.pop()
+        roll.modified = roll.modified or roll.result
+        for i in xrange(len(roll)):
+            die = roll.modified[i]
+            while die < num:
+                die = self.extraroll(roll)
+            roll.modified[i] = die
+
+        self.history.append(roll)
+
+    def each(self, num):
+        """
+        Add `num` to each die of the original roll,
+        die added by extra or open are not included in this
+        eg [4d6.each(2)] => [[3, 2], [2, 2], [5, 2], [4, 2]] = (22)
+        eg [4d6.extra(4)] => [[3, 2], [2, 2], [5, 1, 2], [4, 6, 2]] = (29)
+        """
+        roll = self.history.pop()
+        roll.modified = roll.modified or roll.result
+        for i in xrange(len(roll)):
+            self.extend_roll(roll, i, num)
+        self.history.append(roll)
+
+    def vs(self, target):
+        """
+        Checks each roll against `target` and set 1 for sucess 0 for failure,
+        this results in the total being the number of successes.
+        eg [5d10.vs(7)] => [1, 0, 0, 1, 0] = (2)
+        eg [5d20.vs(10)] => [1, 1, 0, 1, 1] = (4)
+        """
+        roll = self.history.pop()
+        roll.modified = roll.modified or roll.result
+        for i in xrange(len(roll)):
+            die = roll.modified[i]
+            roll.modified[i] = 1 if die >= target else 0
+
+roller_manager.register(StandardRoller)
+
+from orpg.dieroller.base import die_base
+class std(die_base):
+    name = "old_std"
+
     def __init__(self,source=[]):
-        die_base.__init__(self,source)
+        die_base.__init__(self, source)
 
     #  Examples of adding member functions through inheritance.
 
@@ -78,6 +206,4 @@
             else:
                 retValue += setValue
 
-        return retValue
-
-die_rollers.register(std)
\ No newline at end of file
+        return retValue
\ No newline at end of file
--- a/orpg/dieroller/rollers/wod.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/dieroller/rollers/wod.py	Mon Mar 22 18:38:22 2010 -0600
@@ -31,62 +31,125 @@
 # for compatibility with Mage die rolls.
 # Threshhold addition by robert t childers
 
-__version__ = "$Id: wod.py,v 1.14 2007/05/09 19:57:00 digitalxero Exp $"
+
+from orpg.dieroller._base import BaseRoller, roller_manager
 
-from std import std
-from orpg.dieroller.base import *
+class WODRoller(BaseRoller):
+    """
+    World of Darkness die roller
+    This roller is designed for d10's only and always expects a vs target
+    use the .vs method to set the target, all future rolls will use that
+    target, unless you use .vs again
+    """
+    name = "wod"
+    format_string = "{roll_string}{rolls} {extra}"
+    format_extra = "vs {target} result of {wod_result}"
+    target = 5
+    _thr = 0
 
-class wod(std):
-    name = "wod"
+    def build_extra(self):
+        try:
+            roll = self.history.pop()
+        except Exception:
+            print self.rolls
+            raise
 
-    def __init__(self,source=[],target=0,targetthr=0):
-        std.__init__(self,source)
-        self.target = target
-        self.targetthr = targetthr
+        thr = self._thr + 0
+        success = 0
+        botch = False
+        for r in roll:
+            if r >= self.target or r == 10:
+                success += 1
+                if thr > 0:
+                    thr -= 1
+                    success -= 1
+                else:
+                    botch = True
+            elif r == 1:
+                success -= 1
+
+            if botch and success < 0:
+                success = 0
 
-    def vs(self,target):
-        self.target = target
-        return self
+        if success < 0:
+            self.extra_string = self.format_extra.format(target=self.target,
+                                                  wod_result='a botch')
+        elif success > 0:
+            self.extra_string = self.format_extra.format(target=self.target,
+                                        wod_result='(' + str(success) + ')')
+        else:
+            self.extra_string = self.format_extra.format(target=self.target,
+                                                  wod_result='a failure')
 
-    def thr(self,targetthr):
-        self.targetthr = targetthr
-        return self
+        self.history.append(roll)
+
+    def vs(self, target=5):
+        """
+        set the target for all future rolls to use
+        """
+        self.target = target
 
-    def sum(self):
-        rolls = []
-        s = 0
-        s1 = self.targetthr
-        botch = 0
-        for a in self.data:
-            rolls.extend(a.gethistory())
-        for r in rolls:
-            if r >= self.target or r == 10:
-                s += 1
-                if s1 >0:
-                    s1 -= 1
-                    s -= 1
-                else:
-                    botch = 1
-            elif r == 1:
-                s -= 1
-            if botch == 1 and s < 0:
-                s = 0
-        return s
+    def thr(self, thr=0):
+        """
+        set the thr for all future rolls to use
+        """
+        self._thr = thr
+
+roller_manager.register(WODRoller)
+
+#Old
+#from std import std
+#class wod(std):
+    #name = "old_wod"
+
+    #def __init__(self,source=[],target=0,targetthr=0):
+        #std.__init__(self,source)
+        #self.target = target
+        #self.targetthr = targetthr
+
+    #def vs(self,target):
+        #self.target = target
+        #return self
+
+    #def thr(self,targetthr):
+        #self.targetthr = targetthr
+        #return self
 
-    def __str__(self):
-        if len(self.data) > 0:
-            myStr = "[" + str(self.data[0])
-            for a in self.data[1:]:
-                myStr += ","
-                myStr += str(a)
-            if self.sum() < 0:
-                myStr += "] vs " +str(self.target)+" result of a botch"
-            elif self.sum() == 0:
-                myStr += "] vs " +str(self.target)+" result of a failure"
-            else:
-                myStr += "] vs " +str(self.target)+" result of (" + str(self.sum()) + ")"
+    #def sum(self):
+        #rolls = []
+        #s = 0
+        #s1 = self.targetthr
+        #botch = 0
+        #for a in self.data:
+            #rolls.extend(a.gethistory())
+        #for r in rolls:
+            #if r >= self.target or r == 10:
+                #s += 1
+                #if s1 >0:
+                    #s1 -= 1
+                    #s -= 1
+                #else:
+                    #botch = 1
+            #elif r == 1:
+                #s -= 1
+            #if botch == 1 and s < 0:
+                #s = 0
+        #return s
+
+    #def __str__(self):
+        #if len(self.data) > 0:
+            #myStr = "[" + str(self.data[0])
+            #for a in self.data[1:]:
+                #myStr += ","
+                #myStr += str(a)
+            #if self.sum() < 0:
+                #myStr += "] vs " +str(self.target)+" result of a botch"
+            #elif self.sum() == 0:
+                #myStr += "] vs " +str(self.target)+" result of a failure"
+            #else:
+                #myStr += "] vs " +str(self.target)+" result of (" + str(self.sum()) + ")"
 
 
-        return myStr
+        #return myStr
 
-die_rollers.register(wod)
\ No newline at end of file
+#roller_manager.register(wod)
\ No newline at end of file
--- a/orpg/dieroller/rollers/wodex.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/dieroller/rollers/wodex.py	Mon Mar 22 18:38:22 2010 -0600
@@ -31,261 +31,419 @@
 #              Much thanks to whoever wrote the original shadowrun roller (akoman I believe)
 
 
-from std import std
-from orpg.dieroller.base import *
+from orpg.dieroller._base import roller_manager
+from std import StandardRoller
 
-__version__ = "$Id: wodex.py,v 1.9 2007/05/06 16:42:55 digitalxero Exp $"
-
-class wodex(std):
+class WODExRoller(StandardRoller):
     name = "wodex"
+    wod_string = "{roll_string}{rolls} Results: {extra}"
+    _std = True
 
-    def __init__(self,source=[]):
-        std.__init__(self,source)
+    def __call__(self, roll_string):
+        self._std = True
+        return super(WODExRoller, self).__call__(roll_string)
+
+    def build_extra(self):
+        if self._std:
+            self.format_string = super(WODExRoller, self).format_string
+        else:
+            self.format_string = self.wod_string
 
-    def vs(self,actualtarget=6):
-        return oldwodVs(self,actualtarget,(6))
+    def vs(self, target=6, mintn=6, maxtn=10):
+        """
+        Rolls a vs on each target between `mintn` and `maxtn`
+        making your selected `target` bold
+        It shows results for each tn
+        """
+        self._std = False
+
+        #Some Sanity Checks
+        target = max(2, min(10, target))
+        mintn = min(max(2, mintn), target)
+        maxtn = max(min(10, maxtn), target)
+
+        roll = self.history.pop()
 
-    def wod(self,actualtarget=8):
-        return newwodVs(self,actualtarget,(8))
+        for tn in xrange(mintn, maxtn+1):
+            success = 0
+            result = '({success} vs {tn}) '
+            for r in roll:
+                if r >= tn:
+                    success += 1
+                elif r == 1:
+                    success -= 1
 
-    def exalt(self, actualtarget=7):
-        return exaltVs(self, actualtarget)
+            if tn == target:
+                result = '<b>' + result + '</b>'
+
+            self.extra_string += result.format(success=success, tn=tn)
+
+        self.history.append(roll)
 
-    def exaltDmg(self, actualtarget=7):
-        return exaltDmg(self, actualtarget)
+    def wod(self, target=8, mintn=8, maxtn=8):
+        """
+        Rolls a vs on each target between `mintn` and `maxtn`
+        making your selected `target` bold
+        It shows results for each tn
+        This is an openended roll on 10, but extra rolls do not cound as
+        failures and thus are not subtracted from the successes
+        """
+        self._std = False
 
-    def vswide(self,actualtarget=6,maxtarget=10):    #wide simply means it reports TNs from 2 to a specified max.
-        return oldwodVs(self,actualtarget,2,maxtarget)
+        #Some Sanity Checks
+        target = max(2, min(10, target))
+        mintn = min(max(2, mintn), target)
+        maxtn = max(min(10, maxtn), target)
 
-die_rollers.register(wodex)
+        self.open(10)
+
+        roll = self.history.pop()
 
-class oldwodVs(std):
-    def __init__(self,source=[],actualtarget=6,mintn=2,maxtn=10):
-        std.__init__(self, source)
-        if actualtarget > 10:
-            actualtarget = 10
-        if mintn > 10:
-            mintn = 10
-        if maxtn > 10:
-            maxtn = 10
-        if actualtarget < 2:
-            self.target = 2
-        else:
-            self.target = actualtarget
-        #if the target number is higher than max (Mainly for wide rolls) then increase max to tn
-        if actualtarget > maxtn:
-            maxtn = actualtarget
-        if actualtarget < mintn:
-            mintn = actualtarget
-        #store minimum for later use as well, also in result printing section.
-        if mintn < 2:
-            self.mintn = 2
-        else:
-            self.mintn = mintn
-        self.maxtn = maxtn #store for later use in printing results. (Yeah, these comments are now disordered)
+        for tn in xrange(mintn, maxtn+1):
+            success = 0
+            result = '({success} vs {tn}) '
+            for r in roll:
+                if not isinstance(r, int):
+                    for i in xrange(len(r)):
+                        num = r.modified[i] if r.modified else r.result[i]
+                        if num >= tn:
+                            success += 1
+                        elif num == 1 and not i:
+                            success -= 1
+                else:
+                    if r >= tn:
+                        success += 1
+                    elif r == 1:
+                        success -= 1
+
+            if tn == target:
+                result = '<b>' + result + '</b>'
+
+            self.extra_string += result.format(success=success, tn=tn)
+            success = 0
+
+        self.history.append(roll)
+
+    def exalt(self, target=7):
+        """
+        Totals the sucess vs `target` and adds an extra success for each
+        die that is a natural 10
+        """
+        self._std = False
+
+        #Some Sanity Checks
+        target = max(2, min(10, target))
+
+        roll = self.history.pop()
 
-        # WoD etc uses d10 but i've left it so it can roll anything openended
-        # self.openended(self[0].sides)
+        success = 0
+        botch = False
+        for r in roll:
+            if r >= target:
+                success += 1
+            if r == 10:
+                success += 1
+            if r == 1:
+                botch = True
+
+        if success == 0:
+            self.extra_string = 'BOTCH!' if botch else 'Failure'
+        else:
+            rs = '{num} Success'
+            rs += 'es' if success > 1 else ''
+            self.extra_string = rs.format(num=success)
+
+        self.history.append(roll)
+
+    def exaltDmg(self, target=7):
+        """
+        Totals the sucess vs `target`
+        """
+        self._std = False
 
-    #count successes, by looping through each die, and checking it against the currently set TN
-    #1's subtract successes.
-    def __sum__(self):
-        s = 0
-        for r in self.data:
-            if r >= self.target:
-                s += 1
-            elif r == 1:
-                s -= 1
-        return s
+        #Some Sanity Checks
+        target = max(2, min(10, target))
+
+        roll = self.history.pop()
+
+        success = 0
+        botch = False
+        for r in roll:
+            if r >= target:
+                success += 1
+            if r == 1:
+                botch = True
 
-    #a modified sum, but this one takes a target argument, and is there because otherwise it is difficult to loop through
-    #tns counting successes against each one without changing target, which is rather dangerous as the original TN could
-    #easily be lost. 1s subtract successes from everything.
-    def xsum(self,curtarget):
-        s = 0
-        for r in self.data:
-            if r >= curtarget:
-                s += 1
-            elif r == 1:
-                s -= 1
-        return s
+        if success == 0:
+            self.extra_string = 'BOTCH!' if botch else 'Failure'
+        else:
+            rs = '{num} Success'
+            rs += 'es' if success > 1 else ''
+            self.extra_string = rs.format(num=success)
+
+        self.history.append(roll)
+
+    def vswide(self, target=6, maxtn=10):
+        """
+        Wide is just vs(target, 2, maxtn)
+        """
+        self.vs(target, maxtn=maxtn)
+
+roller_manager.register(WODExRoller)
 
 
-    def __str__(self):
-        if len(self.data) > 0:
-            myStr = "[" + str(self.data[0])
-            for a in self.data[1:]:
-                myStr += ","
-                myStr += str(a)
-            myStr += "] Results: "
-            #cycle through from mintn to maxtn, summing successes for each separate TN
-            for targ in range(self.mintn,self.maxtn+1):
-                if (targ == self.target):
-                    myStr += "<b>"
-                myStr += "(" + str(self.xsum(targ)) + "&nbsp;vs&nbsp;" + str(targ) + ") "
-                if (targ == self.target):
-                    myStr += "</b>"
-        else:
-            myStr = "[] = (0)"
+#old
+#from std import std
+#class wodex(std):
+    #name = "old_wodex"
+
+    #def __init__(self,source=[]):
+        #std.__init__(self,source)
+
+    #def vs(self, actualtarget=6):
+        #return oldwodVs(self,actualtarget,(6))
 
-        return myStr
+    #def wod(self,actualtarget=8):
+        #return newwodVs(self,actualtarget,(8))
+
+    #def exalt(self, actualtarget=7):
+        #return exaltVs(self, actualtarget)
+
+    #def exaltDmg(self, actualtarget=7):
+        #return exaltDmg(self, actualtarget)
+
+    #def vswide(self,actualtarget=6,maxtarget=10):    #wide simply means it reports TNs from 2 to a specified max.
+        #return oldwodVs(self,actualtarget,2,maxtarget)
+
+#roller_manager.register(wodex)
 
-class newwodVs(std):
-    def __init__(self,source=[],actualtarget=8,mintn=8,maxtn=8):
-        std.__init__(self, source)
-        if actualtarget > 30:
-            actualtarget = 30
-        if mintn > 10:
-            mintn = 10
-        if maxtn > 10:
-            maxtn = 10
-        if actualtarget < 2:
-            self.target = 2
-        else:
-            self.target = actualtarget
-        #if the target number is higher than max (Mainly for wide rolls) then increase max to tn
-        if actualtarget > maxtn:
-            maxtn = actualtarget
-        if actualtarget < mintn:
-            mintn = actualtarget
-        #store minimum for later use as well, also in result printing section.
-        if mintn < 2:
-            self.mintn = 2
-        else:
-            self.mintn = mintn
-        self.maxtn = maxtn #store for later use in printing results. (Yeah, these comments are now disordered)
-
-        # WoD etc uses d10 but i've left it so it can roll anything openended
-        # self.openended(self[0].sides)
+#class oldwodVs(std):
+    #def __init__(self,source=[],actualtarget=6,mintn=2,maxtn=10):
+        #std.__init__(self, source)
+        #if actualtarget > 10:
+            #actualtarget = 10
+        #if mintn > 10:
+            #mintn = 10
+        #if maxtn > 10:
+            #maxtn = 10
+        #if actualtarget < 2:
+            #self.target = 2
+        #else:
+            #self.target = actualtarget
+        ##if the target number is higher than max (Mainly for wide rolls) then increase max to tn
+        #if actualtarget > maxtn:
+            #maxtn = actualtarget
+        #if actualtarget < mintn:
+            #mintn = actualtarget
+        ##store minimum for later use as well, also in result printing section.
+        #if mintn < 2:
+            #self.mintn = 2
+        #else:
+            #self.mintn = mintn
+        #self.maxtn = maxtn #store for later use in printing results. (Yeah, these comments are now disordered)
 
-    #a modified sum, but this one takes a target argument, and is there because otherwise it is difficult to loop through
-    #tns counting successes against each one without changing target, which is rather dangerous as the original TN could
-    #easily be lost. 1s subtract successes from original but not re-rolls.
-    def xsum(self,curtarget,subones=1):
-        s = 0
-        done = 1
-        for r in self.data:
-            if r >= curtarget:
-                s += 1
-            elif ((r == 1) and (subones == 1)):
-                s -= 1
-        if r == 10:
-            done = 0
-            subones = 0
-            self.append(di(10))
-        if done == 1:
-            return s
-        else:
-            return self.xsum(0)
+        ## WoD etc uses d10 but i've left it so it can roll anything openended
+        ## self.openended(self[0].sides)
 
-    def openended(self,num):
-        if num <= 1:
-            self
-        done = 1
-        for i in range(len(self.data)):
-            if self.data[i].lastroll() == num:
-                self.data[i].extraroll()
-                done = 0
-        if done:
-            return self
-        else:
-            return self.openended(num)
+    ##count successes, by looping through each die, and checking it against the currently set TN
+    ##1's subtract successes.
+    #def __sum__(self):
+        #s = 0
+        #for r in self.data:
+            #if r >= self.target:
+                #s += 1
+            #elif r == 1:
+                #s -= 1
+        #return s
+
+    ##a modified sum, but this one takes a target argument, and is there because otherwise it is difficult to loop through
+    ##tns counting successes against each one without changing target, which is rather dangerous as the original TN could
+    ##easily be lost. 1s subtract successes from everything.
+    #def xsum(self,curtarget):
+        #s = 0
+        #for r in self.data:
+            #if r >= curtarget:
+                #s += 1
+            #elif r == 1:
+                #s -= 1
+        #return s
 
 
-    def __str__(self):
-        if len(self.data) > 0:
-            myStr = "[" + str(self.data[0])
-            for a in self.data[1:]:
-                myStr += ","
-                myStr += str(a)
-            myStr += "] Results: "
-            #cycle through from mintn to maxtn, summing successes for each separate TN
-            for targ in range(self.mintn,self.maxtn+1):
-                if (targ == self.target):
-                    myStr += "<b>"
-                myStr += "(" + str(self.xsum(targ)) + "&nbsp;vs&nbsp;" + str(targ) + ") "
-                if (targ == self.target):
-                    myStr += "</b>"
-        else:
-            myStr = "[] = (0)"
+    #def __str__(self):
+        #if len(self.data) > 0:
+            #myStr = "[" + str(self.data[0])
+            #for a in self.data[1:]:
+                #myStr += ","
+                #myStr += str(a)
+            #myStr += "] Results: "
+            ##cycle through from mintn to maxtn, summing successes for each separate TN
+            #for targ in range(self.mintn,self.maxtn+1):
+                #if (targ == self.target):
+                    #myStr += "<b>"
+                #myStr += "(" + str(self.xsum(targ)) + "&nbsp;vs&nbsp;" + str(targ) + ") "
+                #if (targ == self.target):
+                    #myStr += "</b>"
+        #else:
+            #myStr = "[] = (0)"
 
-        return myStr
+        #return myStr
 
-class exaltVs(std):
-    def __init__(self, source=[], actualtarget=7):
-        std.__init__(self, source)
+#class newwodVs(std):
+    #def __init__(self,source=[],actualtarget=8,mintn=8,maxtn=8):
+        #std.__init__(self, source)
+        #if actualtarget > 30:
+            #actualtarget = 30
+        #if mintn > 10:
+            #mintn = 10
+        #if maxtn > 10:
+            #maxtn = 10
+        #if actualtarget < 2:
+            #self.target = 2
+        #else:
+            #self.target = actualtarget
+        ##if the target number is higher than max (Mainly for wide rolls) then increase max to tn
+        #if actualtarget > maxtn:
+            #maxtn = actualtarget
+        #if actualtarget < mintn:
+            #mintn = actualtarget
+        ##store minimum for later use as well, also in result printing section.
+        #if mintn < 2:
+            #self.mintn = 2
+        #else:
+            #self.mintn = mintn
+        #self.maxtn = maxtn #store for later use in printing results. (Yeah, these comments are now disordered)
 
-        if actualtarget > 10:
-            actualtarget = 10
-
-        if actualtarget < 2:
-            self.target = 2
-        else:
-            self.target = actualtarget
+        ## WoD etc uses d10 but i've left it so it can roll anything openended
+        ## self.openended(self[0].sides)
 
-
-    def xsum(self, target):
-        s = 0
+    ##a modified sum, but this one takes a target argument, and is there because otherwise it is difficult to loop through
+    ##tns counting successes against each one without changing target, which is rather dangerous as the original TN could
+    ##easily be lost. 1s subtract successes from original but not re-rolls.
+    #def xsum(self,curtarget,subones=1):
+        #s = 0
+        #done = 1
+        #for r in self.data:
+            #if r >= curtarget:
+                #s += 1
+            #elif ((r == 1) and (subones == 1)):
+                #s -= 1
+        #if r == 10:
+            #done = 0
+            #subones = 0
+            #self.append(di(10))
+        #if done == 1:
+            #return s
+        #else:
+            #return self.xsum(0)
 
-        for r in self.data:
-            if r >= target:
-                s += 1
-            if r == 10:
-                s += 1
-
-        return s
+    #def openended(self,num):
+        #if num <= 1:
+            #self
+        #done = 1
+        #for i in range(len(self.data)):
+            #if self.data[i].lastroll() == num:
+                #self.data[i].extraroll()
+                #done = 0
+        #if done:
+            #return self
+        #else:
+            #return self.openended(num)
 
 
-    def __str__(self):
-        if len(self.data) > 0:
-            myStr = str(self.data)
-            myStr += " Results: "
+    #def __str__(self):
+        #if len(self.data) > 0:
+            #myStr = "[" + str(self.data[0])
+            #for a in self.data[1:]:
+                #myStr += ","
+                #myStr += str(a)
+            #myStr += "] Results: "
+            ##cycle through from mintn to maxtn, summing successes for each separate TN
+            #for targ in range(self.mintn,self.maxtn+1):
+                #if (targ == self.target):
+                    #myStr += "<b>"
+                #myStr += "(" + str(self.xsum(targ)) + "&nbsp;vs&nbsp;" + str(targ) + ") "
+                #if (targ == self.target):
+                    #myStr += "</b>"
+        #else:
+            #myStr = "[] = (0)"
+
+        #return myStr
+
+#class exaltVs(std):
+    #def __init__(self, source=[], actualtarget=7):
+        #std.__init__(self, source)
 
-            succ = self.xsum(self.target)
-            if succ == 0 and 1 in self.data:
-                myStr += 'BOTCH!'
-            elif succ == 0:
-                myStr += str(succ) + " Failure"
-            elif succ == 1:
-                myStr += str(succ) + " Success"
-            else:
-                myStr += str(succ) + " Successes"
+        #if actualtarget > 10:
+            #actualtarget = 10
+
+        #if actualtarget < 2:
+            #self.target = 2
+        #else:
+            #self.target = actualtarget
+
+
+    #def xsum(self, target):
+        #s = 0
 
-            return myStr
+        #for r in self.data:
+            #if r >= target:
+                #s += 1
+            #if r == 10:
+                #s += 1
 
-class exaltDmg(std):
-    def __init__(self, source=[], actualtarget=7):
-        std.__init__(self, source)
-        if actualtarget > 10:
-            actualtarget = 10
+        #return s
+
+
+    #def __str__(self):
+        #if len(self.data) > 0:
+            #myStr = str(self.data)
+            #myStr += " Results: "
 
-        if actualtarget < 2:
-            self.target = 2
-        else:
-            self.target = actualtarget
+            #succ = self.xsum(self.target)
+            #if succ == 0 and 1 in self.data:
+                #myStr += 'BOTCH!'
+            #elif succ == 0:
+                #myStr += str(succ) + " Failure"
+            #elif succ == 1:
+                #myStr += str(succ) + " Success"
+            #else:
+                #myStr += str(succ) + " Successes"
 
-    def xsum(self, target):
-        s = 0
+            #return myStr
 
-        for r in self.data:
-            if r >= target:
-                s += 1
-        return s
+#class exaltDmg(std):
+    #def __init__(self, source=[], actualtarget=7):
+        #std.__init__(self, source)
+        #if actualtarget > 10:
+            #actualtarget = 10
+
+        #if actualtarget < 2:
+            #self.target = 2
+        #else:
+            #self.target = actualtarget
 
-    def __str__(self):
-        if len(self.data) > 0:
-            myStr = str(self.data)
-            myStr += " Results: "
+    #def xsum(self, target):
+        #s = 0
 
-            succ = self.xsum(self.target)
+        #for r in self.data:
+            #if r >= target:
+                #s += 1
+        #return s
+
+    #def __str__(self):
+        #if len(self.data) > 0:
+            #myStr = str(self.data)
+            #myStr += " Results: "
 
-            if succ == 0 and 1 in self.data:
-                myStr += 'BOTCH!'
-            elif succ == 0:
-                myStr += str(succ) + " Failure"
-            elif succ == 1:
-                myStr += str(succ) + " Success"
-            else:
-                myStr += str(succ) + " Successes"
+            #succ = self.xsum(self.target)
 
-            return myStr
+            #if succ == 0 and 1 in self.data:
+                #myStr += 'BOTCH!'
+            #elif succ == 0:
+                #myStr += str(succ) + " Failure"
+            #elif succ == 1:
+                #myStr += str(succ) + " Success"
+            #else:
+                #myStr += str(succ) + " Successes"
+
+            #return myStr
--- a/orpg/dieroller/utils.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/dieroller/utils.py	Mon Mar 22 18:38:22 2010 -0600
@@ -32,33 +32,61 @@
 import re
 
 import orpg.dieroller.rollers
-from orpg.dieroller.base import die_rollers
+from orpg.dieroller._base import BaseRoller
+
+from orpg.tools.decorators import deprecated
 
-class roller_manager(object):
+class RollerManager(object):
+    _rollers = {}
+    _roller = None
+
     def __new__(cls):
         it = cls.__dict__.get("__it__")
         if it is not None:
             return it
         cls.__it__ = it = object.__new__(cls)
-        it._init()
-
         return it
 
-    def _init(self):
-        self.setRoller('std')
+    def register(self, roller):
+        if not self._rollers.has_key(roller.name):
+            self._rollers[roller.name] = roller
+
+    def list(self):
+        return self._rollers.keys()
+
+    def _get_roller(self):
+        return self._roller.name
+    def _set_roller(self, value):
+        if not isinstance(value, basestring):
+            raise TypeError("roller must be a string")
+
+        if value not in self._rollers:
+            raise KeyError("UNKNOWN ROLLER")
+
+        self._roller = self._rollers[value]
+    roller = property(_get_roller, _set_roller)
 
-    def setRoller(self, roller_class):
-        try:
-            self.roller_class = die_rollers[roller_class]
-        except KeyError:
-            raise Exception("Invalid die roller!")
+    def process_roll(self, roll_string):
+        if isinstance(self.roller, BaseRoller):
+            return self.roller(roll_string)
+        else:
+            #Ok we are using an OLD roller, so have fun with eval and shit
+            return self.proccessRoll(roll_string)
 
-    def getRoller(self):
-        return self.roller_class.name
+    #All these Methods are depreciated and will go away soon
+    @deprecated("setRoller has been depreciated, use the roller property")
+    def setRoller(self, name):
+        self.roller = name
 
+    @deprecated("getRoller has been depreciated, use the roller property")
+    def getRoller(self):
+        return self.roller
+
+    @deprecated("listRollers has been depreciated, use the list method")
     def listRollers(self):
-        return die_rollers.keys()
+        return self.list()
 
+    @deprecated("stdDieToDClass has been depreciated, convert your roller to the new style")
     def stdDieToDClass(self,match):
         s = match.group(0)
         (num,sides) = s.split('d')
@@ -77,6 +105,7 @@
         return ''.join(ret)
 
     #  Use this to convert ndm-style (3d6) dice to d_base format
+    @deprecated("convertTheDieString has been depreciated, convert your roller to the new style")
     def convertTheDieString(self,s):
         reg = re.compile("(?:\d+|\([0-9\*/\-\+]+\))\s*[a-zA-Z]+\s*[\dFf]+")
         (result, num_matches) = reg.subn(self.stdDieToDClass, s)
@@ -89,5 +118,8 @@
                 pass
         return result
 
+    @deprecated("proccessRoll has been depreciated, convert your roller to the new style")
     def proccessRoll(self, s):
         return str(eval(self.convertTheDieString(s)))
+
+roller_manager = RollerManager()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/external/pyparsing.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,3707 @@
+# module pyparsing.py
+#
+# Copyright (c) 2003-2009  Paul T. McGuire
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+#from __future__ import generators
+
+__doc__ = \
+"""
+pyparsing module - Classes and methods to define and execute parsing grammars
+
+The pyparsing module is an alternative approach to creating and executing simple grammars,
+vs. the traditional lex/yacc approach, or the use of regular expressions.  With pyparsing, you
+don't need to learn a new syntax for defining grammars or matching expressions - the parsing module
+provides a library of classes that you use to construct the grammar directly in Python.
+
+Here is a program to parse "Hello, World!" (or any greeting of the form "<salutation>, <addressee>!")::
+
+    from pyparsing import Word, alphas
+
+    # define grammar of a greeting
+    greet = Word(alphas) + "," + Word(alphas) + "!"
+
+    hello = "Hello, World!"
+    print hello, "->", greet.parseString(hello)
+
+The program outputs the following::
+
+    Hello, World! -> ['Hello', ',', 'World', '!']
+
+The Python representation of the grammar is quite readable, owing to the self-explanatory
+class names, and the use of '+', '|' and '^' operators.
+
+The parsed results returned from parseString() can be accessed as a nested list, a dictionary, or an
+object with named attributes.
+
+The pyparsing module handles some of the problems that are typically vexing when writing text parsers:
+ - extra or missing whitespace (the above program will also handle "Hello,World!", "Hello  ,  World  !", etc.)
+ - quoted strings
+ - embedded comments
+"""
+
+__version__ = "1.5.2"
+__versionTime__ = "17 February 2009 19:45"
+__author__ = "Paul McGuire <ptmcg@users.sourceforge.net>"
+
+import string
+from weakref import ref as wkref
+import copy
+import sys
+import warnings
+import re
+import sre_constants
+#~ sys.stderr.write("testing pyparsing module, version %s, %s\n" % (__version__,__versionTime__))
+
+__all__ = [
+'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty',
+'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal',
+'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or',
+'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException',
+'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException',
+'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter', 'Upcase',
+'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore',
+'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col',
+'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString',
+'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'getTokensEndLoc', 'hexnums',
+'htmlComment', 'javaStyleComment', 'keepOriginalText', 'line', 'lineEnd', 'lineStart', 'lineno',
+'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral',
+'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables',
+'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity',
+'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd',
+'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute',
+'indentedBlock', 'originalTextFor',
+]
+
+
+"""
+Detect if we are running version 3.X and make appropriate changes
+Robert A. Clark
+"""
+if sys.version_info[0] > 2 or sys.version_info[1] >= 6:
+    _PY3K = True
+    _MAX_INT = sys.maxsize
+    basestring = str
+else:
+    _PY3K = False
+    _MAX_INT = sys.maxint
+
+if not _PY3K:
+    def _ustr(obj):
+        """Drop-in replacement for str(obj) that tries to be Unicode friendly. It first tries
+           str(obj). If that fails with a UnicodeEncodeError, then it tries unicode(obj). It
+           then < returns the unicode object | encodes it with the default encoding | ... >.
+        """
+        if isinstance(obj,unicode):
+            return obj
+
+        try:
+            # If this works, then _ustr(obj) has the same behaviour as str(obj), so
+            # it won't break any existing code.
+            return str(obj)
+
+        except UnicodeEncodeError:
+            # The Python docs (http://docs.python.org/ref/customization.html#l2h-182)
+            # state that "The return value must be a string object". However, does a
+            # unicode object (being a subclass of basestring) count as a "string
+            # object"?
+            # If so, then return a unicode object:
+            return unicode(obj)
+            # Else encode it... but how? There are many choices... :)
+            # Replace unprintables with escape codes?
+            #return unicode(obj).encode(sys.getdefaultencoding(), 'backslashreplace_errors')
+            # Replace unprintables with question marks?
+            #return unicode(obj).encode(sys.getdefaultencoding(), 'replace')
+            # ...
+else:
+    _ustr = str
+    unichr = chr
+
+if not _PY3K:
+    def _str2dict(strg):
+        return dict([(c,0) for c in strg])
+else:
+    _str2dict = set
+
+def _xml_escape(data):
+    """Escape &, <, >, ", ', etc. in a string of data."""
+
+    # ampersand must be replaced first
+    from_symbols = '&><"\''
+    to_symbols = ['&'+s+';' for s in "amp gt lt quot apos".split()]
+    for from_,to_ in zip(from_symbols, to_symbols):
+        data = data.replace(from_, to_)
+    return data
+
+class _Constants(object):
+    pass
+
+if not _PY3K:
+    alphas     = string.lowercase + string.uppercase
+else:
+    alphas     = string.ascii_lowercase + string.ascii_uppercase
+nums       = string.digits
+hexnums    = nums + "ABCDEFabcdef"
+alphanums  = alphas + nums
+_bslash = chr(92)
+printables = "".join([ c for c in string.printable if c not in string.whitespace ])
+
+class ParseBaseException(Exception):
+    """base exception class for all parsing runtime exceptions"""
+    # Performance tuning: we construct a *lot* of these, so keep this
+    # constructor as small and fast as possible
+    def __init__(self, pstr, loc=0, msg=None, elem=None):
+        self.loc = loc
+        if msg is None:
+            self.msg = pstr
+            self.pstr = ""
+        else:
+            self.msg = msg
+            self.pstr = pstr
+        self.parserElement = elem
+
+    def __getattr__(self, aname):
+        """supported attributes by name are:
+            - lineno - returns the line number of the exception text
+            - col - returns the column number of the exception text
+            - line - returns the line containing the exception text
+        """
+        if(aname == "lineno"):
+            return lineno(self.loc, self.pstr)
+        elif(aname in ("col", "column")):
+            return col(self.loc, self.pstr)
+        elif(aname == "line"):
+            return line(self.loc, self.pstr)
+        else:
+            raise AttributeError(aname)
+
+    def __str__(self):
+        return "%s (at char %d), (line:%d, col:%d)" % \
+               (self.msg, self.loc, self.lineno, self.column)
+    def __repr__(self):
+        return _ustr(self)
+    def markInputline(self, markerString = ">!<"):
+        """Extracts the exception line from the input string, and marks
+           the location of the exception with a special symbol.
+        """
+        line_str = self.line
+        line_column = self.column - 1
+        if markerString:
+            line_str = "".join([line_str[:line_column],
+                                 markerString, line_str[line_column:]])
+        return line_str.strip()
+    def __dir__(self):
+        return "loc msg pstr parserElement lineno col line " \
+               "markInputLine __str__ __repr__".split()
+
+class ParseException(ParseBaseException):
+    """exception thrown when parse expressions don't match class;
+       supported attributes by name are:
+        - lineno - returns the line number of the exception text
+        - col - returns the column number of the exception text
+        - line - returns the line containing the exception text
+    """
+    pass
+
+class ParseFatalException(ParseBaseException):
+    """user-throwable exception thrown when inconsistent parse content
+       is found; stops all parsing immediately"""
+    pass
+
+class ParseSyntaxException(ParseFatalException):
+    """just like ParseFatalException, but thrown internally when an
+       ErrorStop indicates that parsing is to stop immediately because
+       an unbacktrackable syntax error has been found"""
+    def __init__(self, pe):
+        super(ParseSyntaxException, self).__init__(
+            pe.pstr, pe.loc, pe.msg, pe.parserElement)
+
+#~ class ReparseException(ParseBaseException):
+    #~ """Experimental class - parse actions can raise this exception to cause
+        #~ pyparsing to reparse the input string:
+        #~ - with a modified input string, and/or
+        #~ - with a modified start location
+        #~ Set the values of the ReparseException in the constructor, and raise the
+        #~ exception in a parse action to cause pyparsing to use the new string/location.
+        #~ Setting the values as None causes no change to be made.
+        #~ """
+    #~ def __init_(self, newstring, restartLoc):
+        #~ self.newParseText = newstring
+        #~ self.reparseLoc = restartLoc
+
+class RecursiveGrammarException(Exception):
+    """exception thrown by validate() if the grammar could be improperly recursive"""
+    def __init__(self, parseElementList):
+        self.parseElementTrace = parseElementList
+
+    def __str__(self):
+        return "RecursiveGrammarException: %s" % self.parseElementTrace
+
+class _ParseResultsWithOffset(object):
+    def __init__(self,p1,p2):
+        self.tup = (p1,p2)
+    def __getitem__(self,i):
+        return self.tup[i]
+    def __repr__(self):
+        return repr(self.tup)
+    def setOffset(self,i):
+        self.tup = (self.tup[0],i)
+
+class ParseResults(object):
+    """Structured parse results, to provide multiple means of access to the parsed data:
+       - as a list (len(results))
+       - by list index (results[0], results[1], etc.)
+       - by attribute (results.<resultsName>)
+       """
+    __slots__ = ("__toklist", "__tokdict", "__doinit", "__name", "__parent", "__accumNames", "__weakref__")
+    def __new__(cls, toklist, name=None, asList=True, modal=True):
+        if isinstance(toklist, cls):
+            return toklist
+        retobj = object.__new__(cls)
+        retobj.__doinit = True
+        return retobj
+
+    # Performance tuning: we construct a *lot* of these, so keep this
+    # constructor as small and fast as possible
+    def __init__(self, toklist, name=None, asList=True, modal=True):
+        if self.__doinit:
+            self.__doinit = False
+            self.__name = None
+            self.__parent = None
+            self.__accumNames = {}
+            if isinstance(toklist, list):
+                self.__toklist = toklist[:]
+            else:
+                self.__toklist = [toklist]
+            self.__tokdict = dict()
+
+        if name:
+            if not modal:
+                self.__accumNames[name] = 0
+            if isinstance(name,int):
+                name = _ustr(name) # will always return a str, but use _ustr for consistency
+            self.__name = name
+            if not toklist in (None,'',[]):
+                if isinstance(toklist,basestring):
+                    toklist = [ toklist ]
+                if asList:
+                    if isinstance(toklist,ParseResults):
+                        self[name] = _ParseResultsWithOffset(toklist.copy(),0)
+                    else:
+                        self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]),0)
+                    self[name].__name = name
+                else:
+                    try:
+                        self[name] = toklist[0]
+                    except (KeyError,TypeError,IndexError):
+                        self[name] = toklist
+
+    def __getitem__(self, i):
+        if isinstance(i, (int,slice)):
+            return self.__toklist[i]
+        else:
+            if i not in self.__accumNames:
+                return self.__tokdict[i][-1][0]
+            else:
+                return ParseResults([ v[0] for v in self.__tokdict[i] ])
+
+    def __setitem__(self, k, v):
+        if isinstance(v,_ParseResultsWithOffset):
+            self.__tokdict[k] = self.__tokdict.get(k,list()) + [v]
+            sub = v[0]
+        elif isinstance(k,int):
+            self.__toklist[k] = v
+            sub = v
+        else:
+            self.__tokdict[k] = self.__tokdict.get(k,list()) + [_ParseResultsWithOffset(v,0)]
+            sub = v
+        if isinstance(sub,ParseResults):
+            sub.__parent = wkref(self)
+
+    def __delitem__(self, i):
+        if isinstance(i,(int,slice)):
+            mylen = len(self.__toklist)
+            del self.__toklist[i]
+
+            # convert int to slice
+            if isinstance(i, int):
+                if i < 0:
+                    i += mylen
+                i = slice(i, i+1)
+            # get removed indices
+            removed = list(range(*i.indices(mylen)))
+            removed.reverse()
+            # fixup indices in token dictionary
+            for name in self.__tokdict:
+                occurrences = self.__tokdict[name]
+                for j in removed:
+                    for k, (value, position) in enumerate(occurrences):
+                        occurrences[k] = _ParseResultsWithOffset(value, position - (position > j))
+        else:
+            del self.__tokdict[i]
+
+    def __contains__(self, k):
+        return k in self.__tokdict
+
+    def __len__(self): return len(self.__toklist)
+    def __bool__(self): return len(self.__toklist) > 0
+    __nonzero__ = __bool__
+    def __iter__(self): return iter(self.__toklist)
+    def __reversed__(self): return iter(reversed(self.__toklist))
+    def keys(self):
+        """Returns all named result keys."""
+        return self.__tokdict.keys()
+
+    def pop(self, index=-1):
+        """Removes and returns item at specified index (default=last).
+           Will work with either numeric indices or dict-key indicies."""
+        ret = self[index]
+        del self[index]
+        return ret
+
+    def get(self, key, defaultValue=None):
+        """Returns named result matching the given key, or if there is no
+           such name, then returns the given defaultValue or None if no
+           defaultValue is specified."""
+        if key in self:
+            return self[key]
+        else:
+            return defaultValue
+
+    def insert(self, index, insStr):
+        self.__toklist.insert(index, insStr)
+        # fixup indices in token dictionary
+        for name in self.__tokdict:
+            occurrences = self.__tokdict[name]
+            for k, (value, position) in enumerate(occurrences):
+                occurrences[k] = _ParseResultsWithOffset(value, position + (position > index))
+
+    def items(self):
+        """Returns all named result keys and values as a list of tuples."""
+        return [(k,self[k]) for k in self.__tokdict]
+
+    def values(self):
+        """Returns all named result values."""
+        return [ v[-1][0] for v in self.__tokdict.values() ]
+
+    def __getattr__(self, name):
+        if name not in self.__slots__:
+            if name in self.__tokdict:
+                if name not in self.__accumNames:
+                    return self.__tokdict[name][-1][0]
+                else:
+                    return ParseResults([ v[0] for v in self.__tokdict[name] ])
+            else:
+                return ""
+        return None
+
+    def __add__(self, other):
+        ret = self.copy()
+        ret += other
+        return ret
+
+    def __iadd__(self, other):
+        if other.__tokdict:
+            offset = len(self.__toklist)
+            addoffset = (lambda a: (a<0 and offset) or (a+offset))
+            otheritems = other.__tokdict.items()
+            otherdictitems = [(k, _ParseResultsWithOffset(v[0],addoffset(v[1])))
+                              for (k,vlist) in otheritems for v in vlist]
+            for k,v in otherdictitems:
+                self[k] = v
+                if isinstance(v[0],ParseResults):
+                    v[0].__parent = wkref(self)
+
+        self.__toklist += other.__toklist
+        self.__accumNames.update(other.__accumNames)
+        del other
+        return self
+
+    def __repr__(self):
+        return "(%s, %s)" % (repr(self.__toklist), repr(self.__tokdict))
+
+    def __str__(self):
+        out = "["
+        sep = ""
+        for i in self.__toklist:
+            if isinstance(i, ParseResults):
+                out += sep + _ustr(i)
+            else:
+                out += sep + repr(i)
+            sep = ", "
+        out += "]"
+        return out
+
+    def _asStringList(self, sep=''):
+        out = []
+        for item in self.__toklist:
+            if out and sep:
+                out.append(sep)
+            if isinstance(item, ParseResults):
+                out += item._asStringList()
+            else:
+                out.append(_ustr(item))
+        return out
+
+    def asList(self):
+        """Returns the parse results as a nested list of matching tokens, all converted to strings."""
+        out = []
+        for res in self.__toklist:
+            if isinstance(res,ParseResults):
+                out.append(res.asList())
+            else:
+                out.append(res)
+        return out
+
+    def asDict(self):
+        """Returns the named parse results as dictionary."""
+        return dict(self.items())
+
+    def copy(self):
+        """Returns a new copy of a ParseResults object."""
+        ret = ParseResults(self.__toklist)
+        ret.__tokdict = self.__tokdict.copy()
+        ret.__parent = self.__parent
+        ret.__accumNames.update(self.__accumNames)
+        ret.__name = self.__name
+        return ret
+
+    def asXML(self, doctag=None, namedItemsOnly=False, indent="", formatted=True):
+        """Returns the parse results as XML. Tags are created for tokens and lists that have defined results names."""
+        nl = "\n"
+        out = []
+        namedItems = dict([ (v[1],k) for (k,vlist) in self.__tokdict.items()
+                             for v in vlist ])
+        nextLevelIndent = indent + "  "
+
+        # collapse out indents if formatting is not desired
+        if not formatted:
+            indent = ""
+            nextLevelIndent = ""
+            nl = ""
+
+        selfTag = None
+        if doctag is not None:
+            selfTag = doctag
+        else:
+            if self.__name:
+                selfTag = self.__name
+
+        if not selfTag:
+            if namedItemsOnly:
+                return ""
+            else:
+                selfTag = "ITEM"
+
+        out += [ nl, indent, "<", selfTag, ">" ]
+
+        worklist = self.__toklist
+        for i,res in enumerate(worklist):
+            if isinstance(res,ParseResults):
+                if i in namedItems:
+                    out += [ res.asXML(namedItems[i],
+                                       namedItemsOnly and doctag is None,
+                                       nextLevelIndent,
+                                       formatted)]
+                else:
+                    out += [ res.asXML(None,
+                                       namedItemsOnly and doctag is None,
+                                       nextLevelIndent,
+                                       formatted)]
+            else:
+                # individual token, see if there is a name for it
+                resTag = None
+                if i in namedItems:
+                    resTag = namedItems[i]
+                if not resTag:
+                    if namedItemsOnly:
+                        continue
+                    else:
+                        resTag = "ITEM"
+                xmlBodyText = _xml_escape(_ustr(res))
+                out += [ nl, nextLevelIndent, "<", resTag, ">",
+                         xmlBodyText,
+                         "</", resTag, ">" ]
+
+        out += [ nl, indent, "</", selfTag, ">" ]
+        return "".join(out)
+
+    def __lookup(self,sub):
+        for k,vlist in self.__tokdict.items():
+            for v,loc in vlist:
+                if sub is v:
+                    return k
+        return None
+
+    def getName(self):
+        """Returns the results name for this token expression."""
+        if self.__name:
+            return self.__name
+        elif self.__parent:
+            par = self.__parent()
+            if par:
+                return par.__lookup(self)
+            else:
+                return None
+        elif (len(self) == 1 and
+              len(self.__tokdict) == 1 and
+              self.__tokdict.values()[0][0][1] in (0,-1)):
+            return self.__tokdict.keys()[0]
+        else:
+            return None
+
+    def dump(self,indent='',depth=0):
+        """Diagnostic method for listing out the contents of a ParseResults.
+           Accepts an optional indent argument so that this string can be embedded
+           in a nested display of other data."""
+        out = []
+        out.append(indent+_ustr(self.asList()))
+        keys = self.items()
+        keys.sort()
+        for k,v in keys:
+            if out:
+                out.append('\n')
+            out.append("%s%s- %s: " % (indent,('  '*depth), k))
+            if isinstance(v,ParseResults):
+                if v.keys():
+                    #~ out.append('\n')
+                    out.append(v.dump(indent,depth+1))
+                    #~ out.append('\n')
+                else:
+                    out.append(_ustr(v))
+            else:
+                out.append(_ustr(v))
+        #~ out.append('\n')
+        return "".join(out)
+
+    # add support for pickle protocol
+    def __getstate__(self):
+        return (self.__toklist,
+                 (self.__tokdict.copy(),
+                   self.__parent is not None and self.__parent() or None,
+                   self.__accumNames,
+                   self.__name))
+
+    def __setstate__(self,state):
+        self.__toklist = state[0]
+        self.__tokdict, \
+            par, \
+            inAccumNames, \
+            self.__name = state[1]
+        self.__accumNames = {}
+        self.__accumNames.update(inAccumNames)
+        if par is not None:
+            self.__parent = wkref(par)
+        else:
+            self.__parent = None
+
+    def __dir__(self):
+        return dir(super(ParseResults,self)) + self.keys()
+
+def col (loc,strg):
+    """Returns current column within a string, counting newlines as line separators.
+    The first column is number 1.
+
+    Note: the default parsing behavior is to expand tabs in the input string
+    before starting the parsing process.  See L{I{ParserElement.parseString}<ParserElement.parseString>} for more information
+    on parsing strings containing <TAB>s, and suggested methods to maintain a
+    consistent view of the parsed string, the parse location, and line and column
+    positions within the parsed string.
+    """
+    return (loc<len(strg) and strg[loc] == '\n') and 1 or loc - strg.rfind("\n", 0, loc)
+
+def lineno(loc,strg):
+    """Returns current line number within a string, counting newlines as line separators.
+    The first line is number 1.
+
+    Note: the default parsing behavior is to expand tabs in the input string
+    before starting the parsing process.  See L{I{ParserElement.parseString}<ParserElement.parseString>} for more information
+    on parsing strings containing <TAB>s, and suggested methods to maintain a
+    consistent view of the parsed string, the parse location, and line and column
+    positions within the parsed string.
+    """
+    return strg.count("\n",0,loc) + 1
+
+def line(loc, strg):
+    """Returns the line of text containing loc within a string, counting newlines as line separators.
+       """
+    lastCR = strg.rfind("\n", 0, loc)
+    nextCR = strg.find("\n", loc)
+    if nextCR > 0:
+        return strg[lastCR+1:nextCR]
+    else:
+        return strg[lastCR+1:]
+
+def _defaultStartDebugAction(instring, loc, expr):
+    print ("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % (lineno(loc,instring), col(loc,instring)))
+
+def _defaultSuccessDebugAction(instring, startloc, endloc, expr, toks):
+    print ("Matched " + _ustr(expr) + " -> " + str(toks.asList()))
+
+def _defaultExceptionDebugAction(instring, loc, expr, exc):
+    print ("Exception raised:" + _ustr(exc))
+
+def nullDebugAction(*args):
+    """'Do-nothing' debug action, to suppress debugging output during parsing."""
+    pass
+
+class ParserElement(object):
+    """Abstract base level parser element class."""
+    DEFAULT_WHITE_CHARS = " \n\t\r"
+
+    def setDefaultWhitespaceChars(chars):
+        """Overrides the default whitespace chars
+        """
+        ParserElement.DEFAULT_WHITE_CHARS = chars
+    setDefaultWhitespaceChars = staticmethod(setDefaultWhitespaceChars)
+
+    def __init__(self, savelist=False):
+        self.parseAction = list()
+        self.failAction = None
+        #~ self.name = "<unknown>"  # don't define self.name, let subclasses try/except upcall
+        self.strRepr = None
+        self.resultsName = None
+        self.saveAsList = savelist
+        self.skipWhitespace = True
+        self.whiteChars = ParserElement.DEFAULT_WHITE_CHARS
+        self.copyDefaultWhiteChars = True
+        self.mayReturnEmpty = False # used when checking for left-recursion
+        self.keepTabs = False
+        self.ignoreExprs = list()
+        self.debug = False
+        self.streamlined = False
+        self.mayIndexError = True # used to optimize exception handling for subclasses that don't advance parse index
+        self.errmsg = ""
+        self.modalResults = True # used to mark results names as modal (report only last) or cumulative (list all)
+        self.debugActions = (None, None, None) #custom debug actions
+        self.re = None
+        self.callPreparse = True # used to avoid redundant calls to preParse
+        self.callDuringTry = False
+
+    def copy(self):
+        """Make a copy of this ParserElement.  Useful for defining different parse actions
+           for the same parsing pattern, using copies of the original parse element."""
+        cpy = copy.copy(self)
+        cpy.parseAction = self.parseAction[:]
+        cpy.ignoreExprs = self.ignoreExprs[:]
+        if self.copyDefaultWhiteChars:
+            cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS
+        return cpy
+
+    def setName(self, name):
+        """Define name for this expression, for use in debugging."""
+        self.name = name
+        self.errmsg = "Expected " + self.name
+        if hasattr(self,"exception"):
+            self.exception.msg = self.errmsg
+        return self
+
+    def setResultsName(self, name, listAllMatches=False):
+        """Define name for referencing matching tokens as a nested attribute
+           of the returned parse results.
+           NOTE: this returns a *copy* of the original ParserElement object;
+           this is so that the client can define a basic element, such as an
+           integer, and reference it in multiple places with different names.
+        """
+        newself = self.copy()
+        newself.resultsName = name
+        newself.modalResults = not listAllMatches
+        return newself
+
+    def setBreak(self,breakFlag = True):
+        """Method to invoke the Python pdb debugger when this element is
+           about to be parsed. Set breakFlag to True to enable, False to
+           disable.
+        """
+        if breakFlag:
+            _parseMethod = self._parse
+            def breaker(instring, loc, doActions=True, callPreParse=True):
+                import pdb
+                pdb.set_trace()
+                return _parseMethod(instring, loc, doActions, callPreParse)
+            breaker._originalParseMethod = _parseMethod
+            self._parse = breaker
+        else:
+            if hasattr(self._parse,"_originalParseMethod"):
+                self._parse = self._parse._originalParseMethod
+        return self
+
+    def _normalizeParseActionArgs(f):
+        """Internal method used to decorate parse actions that take fewer than 3 arguments,
+           so that all parse actions can be called as f(s,l,t)."""
+        STAR_ARGS = 4
+
+        try:
+            restore = None
+            if isinstance(f,type):
+                restore = f
+                f = f.__init__
+            if not _PY3K:
+                codeObj = f.func_code
+            else:
+                codeObj = f.code
+            if codeObj.co_flags & STAR_ARGS:
+                return f
+            numargs = codeObj.co_argcount
+            if not _PY3K:
+                if hasattr(f,"im_self"):
+                    numargs -= 1
+            else:
+                if hasattr(f,"__self__"):
+                    numargs -= 1
+            if restore:
+                f = restore
+        except AttributeError:
+            try:
+                if not _PY3K:
+                    call_im_func_code = f.__call__.im_func.func_code
+                else:
+                    call_im_func_code = f.__code__
+
+                # not a function, must be a callable object, get info from the
+                # im_func binding of its bound __call__ method
+                if call_im_func_code.co_flags & STAR_ARGS:
+                    return f
+                numargs = call_im_func_code.co_argcount
+                if not _PY3K:
+                    if hasattr(f.__call__,"im_self"):
+                        numargs -= 1
+                else:
+                    if hasattr(f.__call__,"__self__"):
+                        numargs -= 0
+            except AttributeError:
+                if not _PY3K:
+                    call_func_code = f.__call__.func_code
+                else:
+                    call_func_code = f.__call__.__code__
+                # not a bound method, get info directly from __call__ method
+                if call_func_code.co_flags & STAR_ARGS:
+                    return f
+                numargs = call_func_code.co_argcount
+                if not _PY3K:
+                    if hasattr(f.__call__,"im_self"):
+                        numargs -= 1
+                else:
+                    if hasattr(f.__call__,"__self__"):
+                        numargs -= 1
+
+
+        #~ print ("adding function %s with %d args" % (f.func_name,numargs))
+        if numargs == 3:
+            return f
+        else:
+            if numargs > 3:
+                def tmp(s,l,t):
+                    return f(f.__call__.__self__, s,l,t)
+            if numargs == 2:
+                def tmp(s,l,t):
+                    return f(l,t)
+            elif numargs == 1:
+                def tmp(s,l,t):
+                    return f(t)
+            else: #~ numargs == 0:
+                def tmp(s,l,t):
+                    return f()
+            try:
+                tmp.__name__ = f.__name__
+            except (AttributeError,TypeError):
+                # no need for special handling if attribute doesnt exist
+                pass
+            try:
+                tmp.__doc__ = f.__doc__
+            except (AttributeError,TypeError):
+                # no need for special handling if attribute doesnt exist
+                pass
+            try:
+                tmp.__dict__.update(f.__dict__)
+            except (AttributeError,TypeError):
+                # no need for special handling if attribute doesnt exist
+                pass
+            return tmp
+    _normalizeParseActionArgs = staticmethod(_normalizeParseActionArgs)
+
+    def setParseAction(self, *fns, **kwargs):
+        """Define action to perform when successfully matching parse element definition.
+           Parse action fn is a callable method with 0-3 arguments, called as fn(s,loc,toks),
+           fn(loc,toks), fn(toks), or just fn(), where:
+            - s   = the original string being parsed (see note below)
+            - loc = the location of the matching substring
+            - toks = a list of the matched tokens, packaged as a ParseResults object
+           If the functions in fns modify the tokens, they can return them as the return
+           value from fn, and the modified list of tokens will replace the original.
+           Otherwise, fn does not need to return any value.
+
+           Note: the default parsing behavior is to expand tabs in the input string
+           before starting the parsing process.  See L{I{parseString}<parseString>} for more information
+           on parsing strings containing <TAB>s, and suggested methods to maintain a
+           consistent view of the parsed string, the parse location, and line and column
+           positions within the parsed string.
+           """
+        self.parseAction = list(map(self._normalizeParseActionArgs, list(fns)))
+        self.callDuringTry = ("callDuringTry" in kwargs and kwargs["callDuringTry"])
+        return self
+
+    def addParseAction(self, *fns, **kwargs):
+        """Add parse action to expression's list of parse actions. See L{I{setParseAction}<setParseAction>}."""
+        self.parseAction += list(map(self._normalizeParseActionArgs, list(fns)))
+        self.callDuringTry = self.callDuringTry or ("callDuringTry" in kwargs and kwargs["callDuringTry"])
+        return self
+
+    def setFailAction(self, fn):
+        """Define action to perform if parsing fails at this expression.
+           Fail acton fn is a callable function that takes the arguments
+           fn(s,loc,expr,err) where:
+            - s = string being parsed
+            - loc = location where expression match was attempted and failed
+            - expr = the parse expression that failed
+            - err = the exception thrown
+           The function returns no value.  It may throw ParseFatalException
+           if it is desired to stop parsing immediately."""
+        self.failAction = fn
+        return self
+
+    def _skipIgnorables(self, instring, loc):
+        exprsFound = True
+        while exprsFound:
+            exprsFound = False
+            for e in self.ignoreExprs:
+                try:
+                    while 1:
+                        loc,dummy = e._parse(instring, loc)
+                        exprsFound = True
+                except ParseException:
+                    pass
+        return loc
+
+    def preParse(self, instring, loc):
+        if self.ignoreExprs:
+            loc = self._skipIgnorables(instring, loc)
+
+        if self.skipWhitespace:
+            wt = self.whiteChars
+            instrlen = len(instring)
+            while loc < instrlen and instring[loc] in wt:
+                loc += 1
+
+        return loc
+
+    def parseImpl(self, instring, loc, doActions=True):
+        return loc, []
+
+    def postParse(self, instring, loc, tokenlist):
+        return tokenlist
+
+    #~ @profile
+    def _parseNoCache(self, instring, loc, doActions=True, callPreParse=True):
+        debugging = (self.debug) #and doActions)
+
+        if debugging or self.failAction:
+            #~ print ("Match",self,"at loc",loc,"(%d,%d)" % (lineno(loc,instring), col(loc,instring)))
+            if (self.debugActions[0]):
+                self.debugActions[0](instring, loc, self)
+            if callPreParse and self.callPreparse:
+                preloc = self.preParse(instring, loc)
+            else:
+                preloc = loc
+            tokensStart = loc
+            try:
+                try:
+                    loc,tokens = self.parseImpl(instring, preloc, doActions)
+                except IndexError:
+                    raise ParseException(instring, len(instring), self.errmsg, self)
+            except ParseBaseException, err:
+                #~ print ("Exception raised:", err)
+                if self.debugActions[2]:
+                    self.debugActions[2](instring, tokensStart, self, err)
+                if self.failAction:
+                    self.failAction(instring, tokensStart, self, err)
+                raise
+        else:
+            if callPreParse and self.callPreparse:
+                preloc = self.preParse(instring, loc)
+            else:
+                preloc = loc
+            tokensStart = loc
+            if self.mayIndexError or loc >= len(instring):
+                try:
+                    loc,tokens = self.parseImpl(instring, preloc, doActions)
+                except IndexError:
+                    raise ParseException(instring, len(instring), self.errmsg, self)
+            else:
+                loc,tokens = self.parseImpl(instring, preloc, doActions)
+
+        tokens = self.postParse(instring, loc, tokens)
+
+        retTokens = ParseResults(tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults)
+        if self.parseAction and (doActions or self.callDuringTry):
+            if debugging:
+                try:
+                    for fn in self.parseAction:
+                        tokens = fn(instring, tokensStart, retTokens)
+                        if tokens is not None:
+                            retTokens = ParseResults(tokens,
+                                                      self.resultsName,
+                                                      asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),
+                                                      modal=self.modalResults)
+                except ParseBaseException, err:
+                    #~ print "Exception raised in user parse action:", err
+                    if (self.debugActions[2]):
+                        self.debugActions[2](instring, tokensStart, self, err)
+                    raise
+            else:
+                for fn in self.parseAction:
+                    tokens = fn(instring, tokensStart, retTokens)
+                    if tokens is not None:
+                        retTokens = ParseResults(tokens,
+                                                  self.resultsName,
+                                                  asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),
+                                                  modal=self.modalResults)
+
+        if debugging:
+            #~ print ("Matched",self,"->",retTokens.asList())
+            if (self.debugActions[1]):
+                self.debugActions[1](instring, tokensStart, loc, self, retTokens)
+
+        return loc, retTokens
+
+    def tryParse(self, instring, loc):
+        try:
+            return self._parse(instring, loc, doActions=False)[0]
+        except ParseFatalException:
+            raise ParseException(instring, loc, self.errmsg, self)
+
+    # this method gets repeatedly called during backtracking with the same arguments -
+    # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression
+    def _parseCache(self, instring, loc, doActions=True, callPreParse=True):
+        lookup = (self,instring,loc,callPreParse,doActions)
+        if lookup in ParserElement._exprArgCache:
+            value = ParserElement._exprArgCache[ lookup ]
+            if isinstance(value,Exception):
+                raise value
+            return value
+        else:
+            try:
+                value = self._parseNoCache(instring, loc, doActions, callPreParse)
+                ParserElement._exprArgCache[ lookup ] = (value[0],value[1].copy())
+                return value
+            except ParseBaseException, pe:
+                ParserElement._exprArgCache[ lookup ] = pe
+                raise
+
+    _parse = _parseNoCache
+
+    # argument cache for optimizing repeated calls when backtracking through recursive expressions
+    _exprArgCache = {}
+    def resetCache():
+        ParserElement._exprArgCache.clear()
+    resetCache = staticmethod(resetCache)
+
+    _packratEnabled = False
+    def enablePackrat():
+        """Enables "packrat" parsing, which adds memoizing to the parsing logic.
+           Repeated parse attempts at the same string location (which happens
+           often in many complex grammars) can immediately return a cached value,
+           instead of re-executing parsing/validating code.  Memoizing is done of
+           both valid results and parsing exceptions.
+
+           This speedup may break existing programs that use parse actions that
+           have side-effects.  For this reason, packrat parsing is disabled when
+           you first import pyparsing.  To activate the packrat feature, your
+           program must call the class method ParserElement.enablePackrat().  If
+           your program uses psyco to "compile as you go", you must call
+           enablePackrat before calling psyco.full().  If you do not do this,
+           Python will crash.  For best results, call enablePackrat() immediately
+           after importing pyparsing.
+        """
+        if not ParserElement._packratEnabled:
+            ParserElement._packratEnabled = True
+            ParserElement._parse = ParserElement._parseCache
+    enablePackrat = staticmethod(enablePackrat)
+
+    def parseString(self, instring, parseAll=False):
+        """Execute the parse expression with the given string.
+           This is the main interface to the client code, once the complete
+           expression has been built.
+
+           If you want the grammar to require that the entire input string be
+           successfully parsed, then set parseAll to True (equivalent to ending
+           the grammar with StringEnd()).
+
+           Note: parseString implicitly calls expandtabs() on the input string,
+           in order to report proper column numbers in parse actions.
+           If the input string contains tabs and
+           the grammar uses parse actions that use the loc argument to index into the
+           string being parsed, you can ensure you have a consistent view of the input
+           string by:
+            - calling parseWithTabs on your grammar before calling parseString
+              (see L{I{parseWithTabs}<parseWithTabs>})
+            - define your parse action using the full (s,loc,toks) signature, and
+              reference the input string using the parse action's s argument
+            - explictly expand the tabs in your input string before calling
+              parseString
+        """
+        ParserElement.resetCache()
+        if not self.streamlined:
+            self.streamline()
+            #~ self.saveAsList = True
+        for e in self.ignoreExprs:
+            e.streamline()
+        if not self.keepTabs:
+            instring = instring.expandtabs()
+        try:
+            loc, tokens = self._parse(instring, 0)
+            if parseAll:
+                loc = self.preParse(instring, loc)
+                StringEnd()._parse(instring, loc)
+        except ParseBaseException, exc:
+            # catch and re-raise exception from here, clears out pyparsing internal stack trace
+            raise exc
+        else:
+            return tokens
+
+    def scanString(self, instring, maxMatches=_MAX_INT):
+        """Scan the input string for expression matches.  Each match will return the
+           matching tokens, start location, and end location.  May be called with optional
+           maxMatches argument, to clip scanning after 'n' matches are found.
+
+           Note that the start and end locations are reported relative to the string
+           being parsed.  See L{I{parseString}<parseString>} for more information on parsing
+           strings with embedded tabs."""
+        if not self.streamlined:
+            self.streamline()
+        for e in self.ignoreExprs:
+            e.streamline()
+
+        if not self.keepTabs:
+            instring = _ustr(instring).expandtabs()
+        instrlen = len(instring)
+        loc = 0
+        preparseFn = self.preParse
+        parseFn = self._parse
+        ParserElement.resetCache()
+        matches = 0
+        try:
+            while loc <= instrlen and matches < maxMatches:
+                try:
+                    preloc = preparseFn(instring, loc)
+                    nextLoc,tokens = parseFn(instring, preloc, callPreParse=False)
+                except ParseException:
+                    loc = preloc+1
+                else:
+                    matches += 1
+                    yield tokens, preloc, nextLoc
+                    loc = nextLoc
+        except ParseBaseException, pe:
+            raise pe
+
+    def transformString(self, instring):
+        """Extension to scanString, to modify matching text with modified tokens that may
+           be returned from a parse action.  To use transformString, define a grammar and
+           attach a parse action to it that modifies the returned token list.
+           Invoking transformString() on a target string will then scan for matches,
+           and replace the matched text patterns according to the logic in the parse
+           action.  transformString() returns the resulting transformed string."""
+        out = []
+        lastE = 0
+        # force preservation of <TAB>s, to minimize unwanted transformation of string, and to
+        # keep string locs straight between transformString and scanString
+        self.keepTabs = True
+        try:
+            for t,s,e in self.scanString(instring):
+                out.append(instring[lastE:s])
+                if t:
+                    if isinstance(t,ParseResults):
+                        out += t.asList()
+                    elif isinstance(t,list):
+                        out += t
+                    else:
+                        out.append(t)
+                lastE = e
+            out.append(instring[lastE:])
+            return "".join(map(_ustr,out))
+        except ParseBaseException, pe:
+            raise pe
+
+    def searchString(self, instring, maxMatches=_MAX_INT):
+        """Another extension to scanString, simplifying the access to the tokens found
+           to match the given parse expression.  May be called with optional
+           maxMatches argument, to clip searching after 'n' matches are found.
+        """
+        try:
+            return ParseResults([ t for t,s,e in self.scanString(instring, maxMatches) ])
+        except ParseBaseException, pe:
+            raise pe
+
+    def __add__(self, other):
+        """Implementation of + operator - returns And"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return And([ self, other ])
+
+    def __radd__(self, other):
+        """Implementation of + operator when left operand is not a ParserElement"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return other + self
+
+    def __sub__(self, other):
+        """Implementation of - operator, returns And with error stop"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return And([ self, And._ErrorStop(), other ])
+
+    def __rsub__(self, other):
+        """Implementation of - operator when left operand is not a ParserElement"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return other - self
+
+    def __mul__(self,other):
+        if isinstance(other,int):
+            minElements, optElements = other,0
+        elif isinstance(other,tuple):
+            other = (other + (None, None))[:2]
+            if other[0] is None:
+                other = (0, other[1])
+            if isinstance(other[0],int) and other[1] is None:
+                if other[0] == 0:
+                    return ZeroOrMore(self)
+                if other[0] == 1:
+                    return OneOrMore(self)
+                else:
+                    return self*other[0] + ZeroOrMore(self)
+            elif isinstance(other[0],int) and isinstance(other[1],int):
+                minElements, optElements = other
+                optElements -= minElements
+            else:
+                raise TypeError("cannot multiply 'ParserElement' and ('%s','%s') objects", type(other[0]),type(other[1]))
+        else:
+            raise TypeError("cannot multiply 'ParserElement' and '%s' objects", type(other))
+
+        if minElements < 0:
+            raise ValueError("cannot multiply ParserElement by negative value")
+        if optElements < 0:
+            raise ValueError("second tuple value must be greater or equal to first tuple value")
+        if minElements == optElements == 0:
+            raise ValueError("cannot multiply ParserElement by 0 or (0,0)")
+
+        if (optElements):
+            def makeOptionalList(n):
+                if n>1:
+                    return Optional(self + makeOptionalList(n-1))
+                else:
+                    return Optional(self)
+            if minElements:
+                if minElements == 1:
+                    ret = self + makeOptionalList(optElements)
+                else:
+                    ret = And([self]*minElements) + makeOptionalList(optElements)
+            else:
+                ret = makeOptionalList(optElements)
+        else:
+            if minElements == 1:
+                ret = self
+            else:
+                ret = And([self]*minElements)
+        return ret
+
+    def __rmul__(self, other):
+        return self.__mul__(other)
+
+    def __or__(self, other):
+        """Implementation of | operator - returns MatchFirst"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return MatchFirst([ self, other ])
+
+    def __ror__(self, other):
+        """Implementation of | operator when left operand is not a ParserElement"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return other | self
+
+    def __xor__(self, other):
+        """Implementation of ^ operator - returns Or"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return Or([ self, other ])
+
+    def __rxor__(self, other):
+        """Implementation of ^ operator when left operand is not a ParserElement"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return other ^ self
+
+    def __and__(self, other):
+        """Implementation of & operator - returns Each"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return Each([ self, other ])
+
+    def __rand__(self, other):
+        """Implementation of & operator when left operand is not a ParserElement"""
+        if isinstance(other, basestring):
+            other = Literal(other)
+        if not isinstance(other, ParserElement):
+            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
+                          SyntaxWarning, stacklevel=2)
+            return None
+        return other & self
+
+    def __invert__(self):
+        """Implementation of ~ operator - returns NotAny"""
+        return NotAny(self)
+
+    def __call__(self, name):
+        """Shortcut for setResultsName, with listAllMatches=default::
+             userdata = Word(alphas).setResultsName("name") + Word(nums+"-").setResultsName("socsecno")
+           could be written as::
+             userdata = Word(alphas)("name") + Word(nums+"-")("socsecno")
+           """
+        return self.setResultsName(name)
+
+    def suppress(self):
+        """Suppresses the output of this ParserElement; useful to keep punctuation from
+           cluttering up returned output.
+        """
+        return Suppress(self)
+
+    def leaveWhitespace(self):
+        """Disables the skipping of whitespace before matching the characters in the
+           ParserElement's defined pattern.  This is normally only used internally by
+           the pyparsing module, but may be needed in some whitespace-sensitive grammars.
+        """
+        self.skipWhitespace = False
+        return self
+
+    def setWhitespaceChars(self, chars):
+        """Overrides the default whitespace chars
+        """
+        self.skipWhitespace = True
+        self.whiteChars = chars
+        self.copyDefaultWhiteChars = False
+        return self
+
+    def parseWithTabs(self):
+        """Overrides default behavior to expand <TAB>s to spaces before parsing the input string.
+           Must be called before parseString when the input grammar contains elements that
+           match <TAB> characters."""
+        self.keepTabs = True
+        return self
+
+    def ignore(self, other):
+        """Define expression to be ignored (e.g., comments) while doing pattern
+           matching; may be called repeatedly, to define multiple comment or other
+           ignorable patterns.
+        """
+        if isinstance(other, Suppress):
+            if other not in self.ignoreExprs:
+                self.ignoreExprs.append(other)
+        else:
+            self.ignoreExprs.append(Suppress(other))
+        return self
+
+    def setDebugActions(self, startAction, successAction, exceptionAction):
+        """Enable display of debugging messages while doing pattern matching."""
+        self.debugActions = (startAction or _defaultStartDebugAction,
+                             successAction or _defaultSuccessDebugAction,
+                             exceptionAction or _defaultExceptionDebugAction)
+        self.debug = True
+        return self
+
+    def setDebug(self, flag=True):
+        """Enable display of debugging messages while doing pattern matching.
+           Set flag to True to enable, False to disable."""
+        if flag:
+            self.setDebugActions(_defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction)
+        else:
+            self.debug = False
+        return self
+
+    def __str__(self):
+        return self.name
+
+    def __repr__(self):
+        return _ustr(self)
+
+    def streamline(self):
+        self.streamlined = True
+        self.strRepr = None
+        return self
+
+    def checkRecursion(self, parseElementList):
+        pass
+
+    def validate(self, validateTrace=[]):
+        """Check defined expressions for valid structure, check for infinite recursive definitions."""
+        self.checkRecursion([])
+
+    def parseFile(self, file_or_filename, parseAll=False):
+        """Execute the parse expression on the given file or filename.
+           If a filename is specified (instead of a file object),
+           the entire file is opened, read, and closed before parsing.
+        """
+        try:
+            file_contents = file_or_filename.read()
+        except AttributeError:
+            f = open(file_or_filename, "rb")
+            file_contents = f.read()
+            f.close()
+        try:
+            return self.parseString(file_contents, parseAll)
+        except ParseBaseException, exc:
+            # catch and re-raise exception from here, clears out pyparsing internal stack trace
+            raise exc
+
+    def getException(self):
+        return ParseException("",0,self.errmsg,self)
+
+    def __getattr__(self,aname):
+        if aname == "myException":
+            self.myException = ret = self.getException();
+            return ret;
+        else:
+            raise AttributeError("no such attribute " + aname)
+
+    def __eq__(self,other):
+        if isinstance(other, ParserElement):
+            return self is other or self.__dict__ == other.__dict__
+        elif isinstance(other, basestring):
+            try:
+                self.parseString(_ustr(other), parseAll=True)
+                return True
+            except ParseBaseException:
+                return False
+        else:
+            return super(ParserElement,self)==other
+
+    def __ne__(self,other):
+        return not (self == other)
+
+    def __hash__(self):
+        return hash(id(self))
+
+    def __req__(self,other):
+        return self == other
+
+    def __rne__(self,other):
+        return not (self == other)
+
+
+class Token(ParserElement):
+    """Abstract ParserElement subclass, for defining atomic matching patterns."""
+    def __init__(self):
+        super(Token,self).__init__(savelist=False)
+        #self.myException = ParseException("",0,"",self)
+
+    def setName(self, name):
+        s = super(Token,self).setName(name)
+        self.errmsg = "Expected " + self.name
+        #s.myException.msg = self.errmsg
+        return s
+
+
+class Empty(Token):
+    """An empty token, will always match."""
+    def __init__(self):
+        super(Empty,self).__init__()
+        self.name = "Empty"
+        self.mayReturnEmpty = True
+        self.mayIndexError = False
+
+
+class NoMatch(Token):
+    """A token that will never match."""
+    def __init__(self):
+        super(NoMatch,self).__init__()
+        self.name = "NoMatch"
+        self.mayReturnEmpty = True
+        self.mayIndexError = False
+        self.errmsg = "Unmatchable token"
+        #self.myException.msg = self.errmsg
+
+    def parseImpl(self, instring, loc, doActions=True):
+        exc = self.myException
+        exc.loc = loc
+        exc.pstr = instring
+        raise exc
+
+
+class Literal(Token):
+    """Token to exactly match a specified string."""
+    def __init__(self, matchString):
+        super(Literal,self).__init__()
+        self.match = matchString
+        self.matchLen = len(matchString)
+        try:
+            self.firstMatchChar = matchString[0]
+        except IndexError:
+            warnings.warn("null string passed to Literal; use Empty() instead",
+                          SyntaxWarning, stacklevel=2)
+            self.__class__ = Empty
+        self.name = '"%s"' % _ustr(self.match)
+        self.errmsg = "Expected " + self.name
+        self.mayReturnEmpty = False
+        #self.myException.msg = self.errmsg
+        self.mayIndexError = False
+
+    # Performance tuning: this routine gets called a *lot*
+    # if this is a single character match string  and the first character matches,
+    # short-circuit as quickly as possible, and avoid calling startswith
+    #~ @profile
+    def parseImpl(self, instring, loc, doActions=True):
+        if (instring[loc] == self.firstMatchChar and
+            (self.matchLen==1 or instring.startswith(self.match,loc))):
+            return loc+self.matchLen, self.match
+        #~ raise ParseException(instring, loc, self.errmsg)
+        exc = self.myException
+        exc.loc = loc
+        exc.pstr = instring
+        raise exc
+_L = Literal
+
+class Keyword(Token):
+    """Token to exactly match a specified string as a keyword, that is, it must be
+       immediately followed by a non-keyword character.  Compare with Literal::
+         Literal("if") will match the leading 'if' in 'ifAndOnlyIf'.
+         Keyword("if") will not; it will only match the leading 'if in 'if x=1', or 'if(y==2)'
+       Accepts two optional constructor arguments in addition to the keyword string:
+       identChars is a string of characters that would be valid identifier characters,
+       defaulting to all alphanumerics + "_" and "$"; caseless allows case-insensitive
+       matching, default is False.
+    """
+    DEFAULT_KEYWORD_CHARS = alphanums+"_$"
+
+    def __init__(self, matchString, identChars=DEFAULT_KEYWORD_CHARS, caseless=False):
+        super(Keyword,self).__init__()
+        self.match = matchString
+        self.matchLen = len(matchString)
+        try:
+            self.firstMatchChar = matchString[0]
+        except IndexError:
+            warnings.warn("null string passed to Keyword; use Empty() instead",
+                          SyntaxWarning, stacklevel=2)
+        self.name = '"%s"' % self.match
+        self.errmsg = "Expected " + self.name
+        self.mayReturnEmpty = False
+        #self.myException.msg = self.errmsg
+        self.mayIndexError = False
+        self.caseless = caseless
+        if caseless:
+            self.caselessmatch = matchString.upper()
+            identChars = identChars.upper()
+        self.identChars = _str2dict(identChars)
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if self.caseless:
+            if ((instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and
+                 (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) and
+                 (loc == 0 or instring[loc-1].upper() not in self.identChars)):
+                return loc+self.matchLen, self.match
+        else:
+            if (instring[loc] == self.firstMatchChar and
+                (self.matchLen==1 or instring.startswith(self.match,loc)) and
+                (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and
+                (loc == 0 or instring[loc-1] not in self.identChars)):
+                return loc+self.matchLen, self.match
+        #~ raise ParseException(instring, loc, self.errmsg)
+        exc = self.myException
+        exc.loc = loc
+        exc.pstr = instring
+        raise exc
+
+    def copy(self):
+        c = super(Keyword,self).copy()
+        c.identChars = Keyword.DEFAULT_KEYWORD_CHARS
+        return c
+
+    def setDefaultKeywordChars(chars):
+        """Overrides the default Keyword chars
+        """
+        Keyword.DEFAULT_KEYWORD_CHARS = chars
+    setDefaultKeywordChars = staticmethod(setDefaultKeywordChars)
+
+class CaselessLiteral(Literal):
+    """Token to match a specified string, ignoring case of letters.
+       Note: the matched results will always be in the case of the given
+       match string, NOT the case of the input text.
+    """
+    def __init__(self, matchString):
+        super(CaselessLiteral,self).__init__(matchString.upper())
+        # Preserve the defining literal.
+        self.returnString = matchString
+        self.name = "'%s'" % self.returnString
+        self.errmsg = "Expected " + self.name
+        #self.myException.msg = self.errmsg
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if instring[ loc:loc+self.matchLen ].upper() == self.match:
+            return loc+self.matchLen, self.returnString
+        #~ raise ParseException(instring, loc, self.errmsg)
+        exc = self.myException
+        exc.loc = loc
+        exc.pstr = instring
+        raise exc
+
+class CaselessKeyword(Keyword):
+    def __init__(self, matchString, identChars=Keyword.DEFAULT_KEYWORD_CHARS):
+        super(CaselessKeyword,self).__init__(matchString, identChars, caseless=True)
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if ((instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and
+             (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars)):
+            return loc+self.matchLen, self.match
+        #~ raise ParseException(instring, loc, self.errmsg)
+        exc = self.myException
+        exc.loc = loc
+        exc.pstr = instring
+        raise exc
+
+class Word(Token):
+    """Token for matching words composed of allowed character sets.
+       Defined with string containing all allowed initial characters,
+       an optional string containing allowed body characters (if omitted,
+       defaults to the initial character set), and an optional minimum,
+       maximum, and/or exact length.  The default value for min is 1 (a
+       minimum value < 1 is not valid); the default values for max and exact
+       are 0, meaning no maximum or exact length restriction.
+    """
+    def __init__(self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False):
+        super(Word,self).__init__()
+        self.initCharsOrig = initChars
+        self.initChars = _str2dict(initChars)
+        if bodyChars :
+            self.bodyCharsOrig = bodyChars
+            self.bodyChars = _str2dict(bodyChars)
+        else:
+            self.bodyCharsOrig = initChars
+            self.bodyChars = _str2dict(initChars)
+
+        self.maxSpecified = max > 0
+
+        if min < 1:
+            raise ValueError("cannot specify a minimum length < 1; use Optional(Word()) if zero-length word is permitted")
+
+        self.minLen = min
+
+        if max > 0:
+            self.maxLen = max
+        else:
+            self.maxLen = _MAX_INT
+
+        if exact > 0:
+            self.maxLen = exact
+            self.minLen = exact
+
+        self.name = _ustr(self)
+        self.errmsg = "Expected " + self.name
+        #self.myException.msg = self.errmsg
+        self.mayIndexError = False
+        self.asKeyword = asKeyword
+
+        if ' ' not in self.initCharsOrig+self.bodyCharsOrig and (min==1 and max==0 and exact==0):
+            if self.bodyCharsOrig == self.initCharsOrig:
+                self.reString = "[%s]+" % _escapeRegexRangeChars(self.initCharsOrig)
+            elif len(self.bodyCharsOrig) == 1:
+                self.reString = "%s[%s]*" % \
+                    (re.escape(self.initCharsOrig),
+                     _escapeRegexRangeChars(self.bodyCharsOrig),)
+            else:
+                self.reString = "[%s][%s]*" % \
+                    (_escapeRegexRangeChars(self.initCharsOrig),
+                     _escapeRegexRangeChars(self.bodyCharsOrig),)
+            if self.asKeyword:
+                self.reString = r"\b"+self.reString+r"\b"
+            try:
+                self.re = re.compile(self.reString)
+            except:
+                self.re = None
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if self.re:
+            result = self.re.match(instring,loc)
+            if not result:
+                exc = self.myException
+                exc.loc = loc
+                exc.pstr = instring
+                raise exc
+
+            loc = result.end()
+            return loc,result.group()
+
+        if not(instring[ loc ] in self.initChars):
+            #~ raise ParseException(instring, loc, self.errmsg)
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+        start = loc
+        loc += 1
+        instrlen = len(instring)
+        bodychars = self.bodyChars
+        maxloc = start + self.maxLen
+        maxloc = min(maxloc, instrlen)
+        while loc < maxloc and instring[loc] in bodychars:
+            loc += 1
+
+        throwException = False
+        if loc - start < self.minLen:
+            throwException = True
+        if self.maxSpecified and loc < instrlen and instring[loc] in bodychars:
+            throwException = True
+        if self.asKeyword:
+            if (start>0 and instring[start-1] in bodychars) or (loc<instrlen and instring[loc] in bodychars):
+                throwException = True
+
+        if throwException:
+            #~ raise ParseException(instring, loc, self.errmsg)
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+
+        return loc, instring[start:loc]
+
+    def __str__(self):
+        try:
+            return super(Word,self).__str__()
+        except:
+            pass
+
+
+        if self.strRepr is None:
+
+            def charsAsStr(s):
+                if len(s)>4:
+                    return s[:4]+"..."
+                else:
+                    return s
+
+            if (self.initCharsOrig != self.bodyCharsOrig):
+                self.strRepr = "W:(%s,%s)" % (charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig))
+            else:
+                self.strRepr = "W:(%s)" % charsAsStr(self.initCharsOrig)
+
+        return self.strRepr
+
+
+class Regex(Token):
+    """Token for matching strings that match a given regular expression.
+       Defined with string specifying the regular expression in a form recognized by the inbuilt Python re module.
+    """
+    def __init__(self, pattern, flags=0):
+        """The parameters pattern and flags are passed to the re.compile() function as-is. See the Python re module for an explanation of the acceptable patterns and flags."""
+        super(Regex,self).__init__()
+
+        if len(pattern) == 0:
+            warnings.warn("null string passed to Regex; use Empty() instead",
+                          SyntaxWarning, stacklevel=2)
+
+        self.pattern = pattern
+        self.flags = flags
+
+        try:
+            self.re = re.compile(self.pattern, self.flags)
+            self.reString = self.pattern
+        except sre_constants.error:
+            warnings.warn("invalid pattern (%s) passed to Regex" % pattern,
+                          SyntaxWarning, stacklevel=2)
+            raise
+
+        self.name = _ustr(self)
+        self.errmsg = "Expected " + self.name
+        #self.myException.msg = self.errmsg
+        self.mayIndexError = False
+        self.mayReturnEmpty = True
+
+    def parseImpl(self, instring, loc, doActions=True):
+        result = self.re.match(instring,loc)
+        if not result:
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+
+        loc = result.end()
+        d = result.groupdict()
+        ret = ParseResults(result.group())
+        if d:
+            for k in d:
+                ret[k] = d[k]
+        return loc,ret
+
+    def __str__(self):
+        try:
+            return super(Regex,self).__str__()
+        except:
+            pass
+
+        if self.strRepr is None:
+            self.strRepr = "Re:(%s)" % repr(self.pattern)
+
+        return self.strRepr
+
+
+class QuotedString(Token):
+    """Token for matching strings that are delimited by quoting characters.
+    """
+    def __init__(self, quoteChar, escChar=None, escQuote=None, multiline=False, unquoteResults=True, endQuoteChar=None):
+        """
+           Defined with the following parameters:
+            - quoteChar - string of one or more characters defining the quote delimiting string
+            - escChar - character to escape quotes, typically backslash (default=None)
+            - escQuote - special quote sequence to escape an embedded quote string (such as SQL's "" to escape an embedded ") (default=None)
+            - multiline - boolean indicating whether quotes can span multiple lines (default=False)
+            - unquoteResults - boolean indicating whether the matched text should be unquoted (default=True)
+            - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=None => same as quoteChar)
+        """
+        super(QuotedString,self).__init__()
+
+        # remove white space from quote chars - wont work anyway
+        quoteChar = quoteChar.strip()
+        if len(quoteChar) == 0:
+            warnings.warn("quoteChar cannot be the empty string",SyntaxWarning,stacklevel=2)
+            raise SyntaxError()
+
+        if endQuoteChar is None:
+            endQuoteChar = quoteChar
+        else:
+            endQuoteChar = endQuoteChar.strip()
+            if len(endQuoteChar) == 0:
+                warnings.warn("endQuoteChar cannot be the empty string",SyntaxWarning,stacklevel=2)
+                raise SyntaxError()
+
+        self.quoteChar = quoteChar
+        self.quoteCharLen = len(quoteChar)
+        self.firstQuoteChar = quoteChar[0]
+        self.endQuoteChar = endQuoteChar
+        self.endQuoteCharLen = len(endQuoteChar)
+        self.escChar = escChar
+        self.escQuote = escQuote
+        self.unquoteResults = unquoteResults
+
+        if multiline:
+            self.flags = re.MULTILINE | re.DOTALL
+            self.pattern = r'%s(?:[^%s%s]' % \
+                (re.escape(self.quoteChar),
+                  _escapeRegexRangeChars(self.endQuoteChar[0]),
+                  (escChar is not None and _escapeRegexRangeChars(escChar) or ''))
+        else:
+            self.flags = 0
+            self.pattern = r'%s(?:[^%s\n\r%s]' % \
+                (re.escape(self.quoteChar),
+                  _escapeRegexRangeChars(self.endQuoteChar[0]),
+                  (escChar is not None and _escapeRegexRangeChars(escChar) or ''))
+        if len(self.endQuoteChar) > 1:
+            self.pattern += (
+                '|(?:' + ')|(?:'.join(["%s[^%s]" % (re.escape(self.endQuoteChar[:i]),
+                                                    _escapeRegexRangeChars(self.endQuoteChar[i]))
+                                       for i in range(len(self.endQuoteChar)-1,0,-1)]) + ')'
+           )
+        if escQuote:
+            self.pattern += (r'|(?:%s)' % re.escape(escQuote))
+        if escChar:
+            self.pattern += (r'|(?:%s.)' % re.escape(escChar))
+            self.escCharReplacePattern = re.escape(self.escChar)+"(.)"
+        self.pattern += (r')*%s' % re.escape(self.endQuoteChar))
+
+        try:
+            self.re = re.compile(self.pattern, self.flags)
+            self.reString = self.pattern
+        except sre_constants.error:
+            warnings.warn("invalid pattern (%s) passed to Regex" % self.pattern,
+                          SyntaxWarning, stacklevel=2)
+            raise
+
+        self.name = _ustr(self)
+        self.errmsg = "Expected " + self.name
+        #self.myException.msg = self.errmsg
+        self.mayIndexError = False
+        self.mayReturnEmpty = True
+
+    def parseImpl(self, instring, loc, doActions=True):
+        result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None
+        if not result:
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+
+        loc = result.end()
+        ret = result.group()
+
+        if self.unquoteResults:
+
+            # strip off quotes
+            ret = ret[self.quoteCharLen:-self.endQuoteCharLen]
+
+            if isinstance(ret,basestring):
+                # replace escaped characters
+                if self.escChar:
+                    ret = re.sub(self.escCharReplacePattern,"\g<1>",ret)
+
+                # replace escaped quotes
+                if self.escQuote:
+                    ret = ret.replace(self.escQuote, self.endQuoteChar)
+
+        return loc, ret
+
+    def __str__(self):
+        try:
+            return super(QuotedString,self).__str__()
+        except:
+            pass
+
+        if self.strRepr is None:
+            self.strRepr = "quoted string, starting with %s ending with %s" % (self.quoteChar, self.endQuoteChar)
+
+        return self.strRepr
+
+
+class CharsNotIn(Token):
+    """Token for matching words composed of characters *not* in a given set.
+       Defined with string containing all disallowed characters, and an optional
+       minimum, maximum, and/or exact length.  The default value for min is 1 (a
+       minimum value < 1 is not valid); the default values for max and exact
+       are 0, meaning no maximum or exact length restriction.
+    """
+    def __init__(self, notChars, min=1, max=0, exact=0):
+        super(CharsNotIn,self).__init__()
+        self.skipWhitespace = False
+        self.notChars = notChars
+
+        if min < 1:
+            raise ValueError("cannot specify a minimum length < 1; use Optional(CharsNotIn()) if zero-length char group is permitted")
+
+        self.minLen = min
+
+        if max > 0:
+            self.maxLen = max
+        else:
+            self.maxLen = _MAX_INT
+
+        if exact > 0:
+            self.maxLen = exact
+            self.minLen = exact
+
+        self.name = _ustr(self)
+        self.errmsg = "Expected " + self.name
+        self.mayReturnEmpty = (self.minLen == 0)
+        #self.myException.msg = self.errmsg
+        self.mayIndexError = False
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if instring[loc] in self.notChars:
+            #~ raise ParseException(instring, loc, self.errmsg)
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+
+        start = loc
+        loc += 1
+        notchars = self.notChars
+        maxlen = min(start+self.maxLen, len(instring))
+        while loc < maxlen and \
+              (instring[loc] not in notchars):
+            loc += 1
+
+        if loc - start < self.minLen:
+            #~ raise ParseException(instring, loc, self.errmsg)
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+
+        return loc, instring[start:loc]
+
+    def __str__(self):
+        try:
+            return super(CharsNotIn, self).__str__()
+        except:
+            pass
+
+        if self.strRepr is None:
+            if len(self.notChars) > 4:
+                self.strRepr = "!W:(%s...)" % self.notChars[:4]
+            else:
+                self.strRepr = "!W:(%s)" % self.notChars
+
+        return self.strRepr
+
+class White(Token):
+    """Special matching class for matching whitespace.  Normally, whitespace is ignored
+       by pyparsing grammars.  This class is included when some whitespace structures
+       are significant.  Define with a string containing the whitespace characters to be
+       matched; default is " \\t\\r\\n".  Also takes optional min, max, and exact arguments,
+       as defined for the Word class."""
+    whiteStrs = {
+        " " : "<SPC>",
+        "\t": "<TAB>",
+        "\n": "<LF>",
+        "\r": "<CR>",
+        "\f": "<FF>",
+    }
+    def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0):
+        super(White,self).__init__()
+        self.matchWhite = ws
+        self.setWhitespaceChars("".join([c for c in self.whiteChars if c not in self.matchWhite]))
+        #~ self.leaveWhitespace()
+        self.name = ("".join([White.whiteStrs[c] for c in self.matchWhite]))
+        self.mayReturnEmpty = True
+        self.errmsg = "Expected " + self.name
+        #self.myException.msg = self.errmsg
+
+        self.minLen = min
+
+        if max > 0:
+            self.maxLen = max
+        else:
+            self.maxLen = _MAX_INT
+
+        if exact > 0:
+            self.maxLen = exact
+            self.minLen = exact
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if not(instring[ loc ] in self.matchWhite):
+            #~ raise ParseException(instring, loc, self.errmsg)
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+        start = loc
+        loc += 1
+        maxloc = start + self.maxLen
+        maxloc = min(maxloc, len(instring))
+        while loc < maxloc and instring[loc] in self.matchWhite:
+            loc += 1
+
+        if loc - start < self.minLen:
+            #~ raise ParseException(instring, loc, self.errmsg)
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+
+        return loc, instring[start:loc]
+
+
+class _PositionToken(Token):
+    def __init__(self):
+        super(_PositionToken,self).__init__()
+        self.name=self.__class__.__name__
+        self.mayReturnEmpty = True
+        self.mayIndexError = False
+
+class GoToColumn(_PositionToken):
+    """Token to advance to a specific column of input text; useful for tabular report scraping."""
+    def __init__(self, colno):
+        super(GoToColumn,self).__init__()
+        self.col = colno
+
+    def preParse(self, instring, loc):
+        if col(loc,instring) != self.col:
+            instrlen = len(instring)
+            if self.ignoreExprs:
+                loc = self._skipIgnorables(instring, loc)
+            while loc < instrlen and instring[loc].isspace() and col(loc, instring) != self.col :
+                loc += 1
+        return loc
+
+    def parseImpl(self, instring, loc, doActions=True):
+        thiscol = col(loc, instring)
+        if thiscol > self.col:
+            raise ParseException(instring, loc, "Text not in expected column", self)
+        newloc = loc + self.col - thiscol
+        ret = instring[ loc: newloc ]
+        return newloc, ret
+
+class LineStart(_PositionToken):
+    """Matches if current position is at the beginning of a line within the parse string"""
+    def __init__(self):
+        super(LineStart,self).__init__()
+        self.setWhitespaceChars(ParserElement.DEFAULT_WHITE_CHARS.replace("\n",""))
+        self.errmsg = "Expected start of line"
+        #self.myException.msg = self.errmsg
+
+    def preParse(self, instring, loc):
+        preloc = super(LineStart,self).preParse(instring,loc)
+        if instring[preloc] == "\n":
+            loc += 1
+        return loc
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if not(loc==0 or
+                (loc == self.preParse(instring, 0)) or
+                (instring[loc-1] == "\n")): #col(loc, instring) != 1:
+            #~ raise ParseException(instring, loc, "Expected start of line")
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+        return loc, []
+
+class LineEnd(_PositionToken):
+    """Matches if current position is at the end of a line within the parse string"""
+    def __init__(self):
+        super(LineEnd,self).__init__()
+        self.setWhitespaceChars(ParserElement.DEFAULT_WHITE_CHARS.replace("\n",""))
+        self.errmsg = "Expected end of line"
+        #self.myException.msg = self.errmsg
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if loc<len(instring):
+            if instring[loc] == "\n":
+                return loc+1, "\n"
+            else:
+                #~ raise ParseException(instring, loc, "Expected end of line")
+                exc = self.myException
+                exc.loc = loc
+                exc.pstr = instring
+                raise exc
+        elif loc == len(instring):
+            return loc+1, []
+        else:
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+
+class StringStart(_PositionToken):
+    """Matches if current position is at the beginning of the parse string"""
+    def __init__(self):
+        super(StringStart,self).__init__()
+        self.errmsg = "Expected start of text"
+        #self.myException.msg = self.errmsg
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if loc != 0:
+            # see if entire string up to here is just whitespace and ignoreables
+            if loc != self.preParse(instring, 0):
+                #~ raise ParseException(instring, loc, "Expected start of text")
+                exc = self.myException
+                exc.loc = loc
+                exc.pstr = instring
+                raise exc
+        return loc, []
+
+class StringEnd(_PositionToken):
+    """Matches if current position is at the end of the parse string"""
+    def __init__(self):
+        super(StringEnd,self).__init__()
+        self.errmsg = "Expected end of text"
+        #self.myException.msg = self.errmsg
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if loc < len(instring):
+            #~ raise ParseException(instring, loc, "Expected end of text")
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+        elif loc == len(instring):
+            return loc+1, []
+        elif loc > len(instring):
+            return loc, []
+        else:
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+
+class WordStart(_PositionToken):
+    """Matches if the current position is at the beginning of a Word, and
+       is not preceded by any character in a given set of wordChars
+       (default=printables). To emulate the \b behavior of regular expressions,
+       use WordStart(alphanums). WordStart will also match at the beginning of
+       the string being parsed, or at the beginning of a line.
+    """
+    def __init__(self, wordChars = printables):
+        super(WordStart,self).__init__()
+        self.wordChars = _str2dict(wordChars)
+        self.errmsg = "Not at the start of a word"
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if loc != 0:
+            if (instring[loc-1] in self.wordChars or
+                instring[loc] not in self.wordChars):
+                exc = self.myException
+                exc.loc = loc
+                exc.pstr = instring
+                raise exc
+        return loc, []
+
+class WordEnd(_PositionToken):
+    """Matches if the current position is at the end of a Word, and
+       is not followed by any character in a given set of wordChars
+       (default=printables). To emulate the \b behavior of regular expressions,
+       use WordEnd(alphanums). WordEnd will also match at the end of
+       the string being parsed, or at the end of a line.
+    """
+    def __init__(self, wordChars = printables):
+        super(WordEnd,self).__init__()
+        self.wordChars = _str2dict(wordChars)
+        self.skipWhitespace = False
+        self.errmsg = "Not at the end of a word"
+
+    def parseImpl(self, instring, loc, doActions=True):
+        instrlen = len(instring)
+        if instrlen>0 and loc<instrlen:
+            if (instring[loc] in self.wordChars or
+                instring[loc-1] not in self.wordChars):
+                #~ raise ParseException(instring, loc, "Expected end of word")
+                exc = self.myException
+                exc.loc = loc
+                exc.pstr = instring
+                raise exc
+        return loc, []
+
+
+class ParseExpression(ParserElement):
+    """Abstract subclass of ParserElement, for combining and post-processing parsed tokens."""
+    def __init__(self, exprs, savelist = False):
+        super(ParseExpression,self).__init__(savelist)
+        if isinstance(exprs, list):
+            self.exprs = exprs
+        elif isinstance(exprs, basestring):
+            self.exprs = [ Literal(exprs) ]
+        else:
+            try:
+                self.exprs = list(exprs)
+            except TypeError:
+                self.exprs = [ exprs ]
+        self.callPreparse = False
+
+    def __getitem__(self, i):
+        return self.exprs[i]
+
+    def append(self, other):
+        self.exprs.append(other)
+        self.strRepr = None
+        return self
+
+    def leaveWhitespace(self):
+        """Extends leaveWhitespace defined in base class, and also invokes leaveWhitespace on
+           all contained expressions."""
+        self.skipWhitespace = False
+        self.exprs = [ e.copy() for e in self.exprs ]
+        for e in self.exprs:
+            e.leaveWhitespace()
+        return self
+
+    def ignore(self, other):
+        if isinstance(other, Suppress):
+            if other not in self.ignoreExprs:
+                super(ParseExpression, self).ignore(other)
+                for e in self.exprs:
+                    e.ignore(self.ignoreExprs[-1])
+        else:
+            super(ParseExpression, self).ignore(other)
+            for e in self.exprs:
+                e.ignore(self.ignoreExprs[-1])
+        return self
+
+    def __str__(self):
+        try:
+            return super(ParseExpression,self).__str__()
+        except:
+            pass
+
+        if self.strRepr is None:
+            self.strRepr = "%s:(%s)" % (self.__class__.__name__, _ustr(self.exprs))
+        return self.strRepr
+
+    def streamline(self):
+        super(ParseExpression,self).streamline()
+
+        for e in self.exprs:
+            e.streamline()
+
+        # collapse nested And's of the form And(And(And(a,b), c), d) to And(a,b,c,d)
+        # but only if there are no parse actions or resultsNames on the nested And's
+        # (likewise for Or's and MatchFirst's)
+        if (len(self.exprs) == 2):
+            other = self.exprs[0]
+            if (isinstance(other, self.__class__) and
+                 not(other.parseAction) and
+                 other.resultsName is None and
+                 not other.debug):
+                self.exprs = other.exprs[:] + [ self.exprs[1] ]
+                self.strRepr = None
+                self.mayReturnEmpty |= other.mayReturnEmpty
+                self.mayIndexError  |= other.mayIndexError
+
+            other = self.exprs[-1]
+            if (isinstance(other, self.__class__) and
+                 not(other.parseAction) and
+                 other.resultsName is None and
+                 not other.debug):
+                self.exprs = self.exprs[:-1] + other.exprs[:]
+                self.strRepr = None
+                self.mayReturnEmpty |= other.mayReturnEmpty
+                self.mayIndexError  |= other.mayIndexError
+
+        return self
+
+    def setResultsName(self, name, listAllMatches=False):
+        ret = super(ParseExpression,self).setResultsName(name,listAllMatches)
+        return ret
+
+    def validate(self, validateTrace=[]):
+        tmp = validateTrace[:]+[self]
+        for e in self.exprs:
+            e.validate(tmp)
+        self.checkRecursion([])
+
+class And(ParseExpression):
+    """Requires all given ParseExpressions to be found in the given order.
+       Expressions may be separated by whitespace.
+       May be constructed using the '+' operator.
+    """
+
+    class _ErrorStop(Empty):
+        def __init__(self, *args, **kwargs):
+            super(Empty,self).__init__(*args, **kwargs)
+            self.leaveWhitespace()
+
+    def __init__(self, exprs, savelist = True):
+        super(And,self).__init__(exprs, savelist)
+        self.mayReturnEmpty = True
+        for e in self.exprs:
+            if not e.mayReturnEmpty:
+                self.mayReturnEmpty = False
+                break
+        self.setWhitespaceChars(exprs[0].whiteChars)
+        self.skipWhitespace = exprs[0].skipWhitespace
+        self.callPreparse = True
+
+    def parseImpl(self, instring, loc, doActions=True):
+        # pass False as last arg to _parse for first element, since we already
+        # pre-parsed the string as part of our And pre-parsing
+        loc, resultlist = self.exprs[0]._parse(instring, loc, doActions, callPreParse=False)
+        errorStop = False
+        for e in self.exprs[1:]:
+            if isinstance(e, And._ErrorStop):
+                errorStop = True
+                continue
+            if errorStop:
+                try:
+                    loc, exprtokens = e._parse(instring, loc, doActions)
+                except ParseSyntaxException:
+                    raise
+                except ParseBaseException, pe:
+                    raise ParseSyntaxException(pe)
+                except IndexError, ie:
+                    raise ParseSyntaxException(ParseException(instring, len(instring), self.errmsg, self))
+            else:
+                loc, exprtokens = e._parse(instring, loc, doActions)
+            if exprtokens or exprtokens.keys():
+                resultlist += exprtokens
+        return loc, resultlist
+
+    def __iadd__(self, other):
+        if isinstance(other, basestring):
+            other = Literal(other)
+        return self.append(other) #And([ self, other ])
+
+    def checkRecursion(self, parseElementList):
+        subRecCheckList = parseElementList[:] + [ self ]
+        for e in self.exprs:
+            e.checkRecursion(subRecCheckList)
+            if not e.mayReturnEmpty:
+                break
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        if self.strRepr is None:
+            self.strRepr = "{" + " ".join([ _ustr(e) for e in self.exprs ]) + "}"
+
+        return self.strRepr
+
+
+class Or(ParseExpression):
+    """Requires that at least one ParseExpression is found.
+       If two expressions match, the expression that matches the longest string will be used.
+       May be constructed using the '^' operator.
+    """
+    def __init__(self, exprs, savelist = False):
+        super(Or,self).__init__(exprs, savelist)
+        self.mayReturnEmpty = False
+        for e in self.exprs:
+            if e.mayReturnEmpty:
+                self.mayReturnEmpty = True
+                break
+
+    def parseImpl(self, instring, loc, doActions=True):
+        maxExcLoc = -1
+        maxMatchLoc = -1
+        maxException = None
+        for e in self.exprs:
+            try:
+                loc2 = e.tryParse(instring, loc)
+            except ParseException, err:
+                if err.loc > maxExcLoc:
+                    maxException = err
+                    maxExcLoc = err.loc
+            except IndexError:
+                if len(instring) > maxExcLoc:
+                    maxException = ParseException(instring,len(instring),e.errmsg,self)
+                    maxExcLoc = len(instring)
+            else:
+                if loc2 > maxMatchLoc:
+                    maxMatchLoc = loc2
+                    maxMatchExp = e
+
+        if maxMatchLoc < 0:
+            if maxException is not None:
+                raise maxException
+            else:
+                raise ParseException(instring, loc, "no defined alternatives to match", self)
+
+        return maxMatchExp._parse(instring, loc, doActions)
+
+    def __ixor__(self, other):
+        if isinstance(other, basestring):
+            other = Literal(other)
+        return self.append(other) #Or([ self, other ])
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        if self.strRepr is None:
+            self.strRepr = "{" + " ^ ".join([ _ustr(e) for e in self.exprs ]) + "}"
+
+        return self.strRepr
+
+    def checkRecursion(self, parseElementList):
+        subRecCheckList = parseElementList[:] + [ self ]
+        for e in self.exprs:
+            e.checkRecursion(subRecCheckList)
+
+
+class MatchFirst(ParseExpression):
+    """Requires that at least one ParseExpression is found.
+       If two expressions match, the first one listed is the one that will match.
+       May be constructed using the '|' operator.
+    """
+    def __init__(self, exprs, savelist = False):
+        super(MatchFirst,self).__init__(exprs, savelist)
+        if exprs:
+            self.mayReturnEmpty = False
+            for e in self.exprs:
+                if e.mayReturnEmpty:
+                    self.mayReturnEmpty = True
+                    break
+        else:
+            self.mayReturnEmpty = True
+
+    def parseImpl(self, instring, loc, doActions=True):
+        maxExcLoc = -1
+        maxException = None
+        for e in self.exprs:
+            try:
+                ret = e._parse(instring, loc, doActions)
+                return ret
+            except ParseException, err:
+                if err.loc > maxExcLoc:
+                    maxException = err
+                    maxExcLoc = err.loc
+            except IndexError:
+                if len(instring) > maxExcLoc:
+                    maxException = ParseException(instring,len(instring),e.errmsg,self)
+                    maxExcLoc = len(instring)
+
+        # only got here if no expression matched, raise exception for match that made it the furthest
+        else:
+            if maxException is not None:
+                raise maxException
+            else:
+                raise ParseException(instring, loc, "no defined alternatives to match", self)
+
+    def __ior__(self, other):
+        if isinstance(other, basestring):
+            other = Literal(other)
+        return self.append(other) #MatchFirst([ self, other ])
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        if self.strRepr is None:
+            self.strRepr = "{" + " | ".join([ _ustr(e) for e in self.exprs ]) + "}"
+
+        return self.strRepr
+
+    def checkRecursion(self, parseElementList):
+        subRecCheckList = parseElementList[:] + [ self ]
+        for e in self.exprs:
+            e.checkRecursion(subRecCheckList)
+
+
+class Each(ParseExpression):
+    """Requires all given ParseExpressions to be found, but in any order.
+       Expressions may be separated by whitespace.
+       May be constructed using the '&' operator.
+    """
+    def __init__(self, exprs, savelist = True):
+        super(Each,self).__init__(exprs, savelist)
+        self.mayReturnEmpty = True
+        for e in self.exprs:
+            if not e.mayReturnEmpty:
+                self.mayReturnEmpty = False
+                break
+        self.skipWhitespace = True
+        self.initExprGroups = True
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if self.initExprGroups:
+            self.optionals = [ e.expr for e in self.exprs if isinstance(e,Optional) ]
+            self.multioptionals = [ e.expr for e in self.exprs if isinstance(e,ZeroOrMore) ]
+            self.multirequired = [ e.expr for e in self.exprs if isinstance(e,OneOrMore) ]
+            self.required = [ e for e in self.exprs if not isinstance(e,(Optional,ZeroOrMore,OneOrMore)) ]
+            self.required += self.multirequired
+            self.initExprGroups = False
+        tmpLoc = loc
+        tmpReqd = self.required[:]
+        tmpOpt  = self.optionals[:]
+        matchOrder = []
+
+        keepMatching = True
+        while keepMatching:
+            tmpExprs = tmpReqd + tmpOpt + self.multioptionals + self.multirequired
+            failed = []
+            for e in tmpExprs:
+                try:
+                    tmpLoc = e.tryParse(instring, tmpLoc)
+                except ParseException:
+                    failed.append(e)
+                else:
+                    matchOrder.append(e)
+                    if e in tmpReqd:
+                        tmpReqd.remove(e)
+                    elif e in tmpOpt:
+                        tmpOpt.remove(e)
+            if len(failed) == len(tmpExprs):
+                keepMatching = False
+
+        if tmpReqd:
+            missing = ", ".join([ _ustr(e) for e in tmpReqd ])
+            raise ParseException(instring,loc,"Missing one or more required elements (%s)" % missing)
+
+        # add any unmatched Optionals, in case they have default values defined
+        matchOrder += list(e for e in self.exprs if isinstance(e,Optional) and e.expr in tmpOpt)
+
+        resultlist = []
+        for e in matchOrder:
+            loc,results = e._parse(instring,loc,doActions)
+            resultlist.append(results)
+
+        finalResults = ParseResults([])
+        for r in resultlist:
+            dups = {}
+            for k in r.keys():
+                if k in finalResults.keys():
+                    tmp = ParseResults(finalResults[k])
+                    tmp += ParseResults(r[k])
+                    dups[k] = tmp
+            finalResults += ParseResults(r)
+            for k,v in dups.items():
+                finalResults[k] = v
+        return loc, finalResults
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        if self.strRepr is None:
+            self.strRepr = "{" + " & ".join([ _ustr(e) for e in self.exprs ]) + "}"
+
+        return self.strRepr
+
+    def checkRecursion(self, parseElementList):
+        subRecCheckList = parseElementList[:] + [ self ]
+        for e in self.exprs:
+            e.checkRecursion(subRecCheckList)
+
+
+class ParseElementEnhance(ParserElement):
+    """Abstract subclass of ParserElement, for combining and post-processing parsed tokens."""
+    def __init__(self, expr, savelist=False):
+        super(ParseElementEnhance,self).__init__(savelist)
+        if isinstance(expr, basestring):
+            expr = Literal(expr)
+        self.expr = expr
+        self.strRepr = None
+        if expr is not None:
+            self.mayIndexError = expr.mayIndexError
+            self.mayReturnEmpty = expr.mayReturnEmpty
+            self.setWhitespaceChars(expr.whiteChars)
+            self.skipWhitespace = expr.skipWhitespace
+            self.saveAsList = expr.saveAsList
+            self.callPreparse = expr.callPreparse
+            self.ignoreExprs.extend(expr.ignoreExprs)
+
+    def parseImpl(self, instring, loc, doActions=True):
+        if self.expr is not None:
+            return self.expr._parse(instring, loc, doActions, callPreParse=False)
+        else:
+            raise ParseException("",loc,self.errmsg,self)
+
+    def leaveWhitespace(self):
+        self.skipWhitespace = False
+        self.expr = self.expr.copy()
+        if self.expr is not None:
+            self.expr.leaveWhitespace()
+        return self
+
+    def ignore(self, other):
+        if isinstance(other, Suppress):
+            if other not in self.ignoreExprs:
+                super(ParseElementEnhance, self).ignore(other)
+                if self.expr is not None:
+                    self.expr.ignore(self.ignoreExprs[-1])
+        else:
+            super(ParseElementEnhance, self).ignore(other)
+            if self.expr is not None:
+                self.expr.ignore(self.ignoreExprs[-1])
+        return self
+
+    def streamline(self):
+        super(ParseElementEnhance,self).streamline()
+        if self.expr is not None:
+            self.expr.streamline()
+        return self
+
+    def checkRecursion(self, parseElementList):
+        if self in parseElementList:
+            raise RecursiveGrammarException(parseElementList+[self])
+        subRecCheckList = parseElementList[:] + [ self ]
+        if self.expr is not None:
+            self.expr.checkRecursion(subRecCheckList)
+
+    def validate(self, validateTrace=[]):
+        tmp = validateTrace[:]+[self]
+        if self.expr is not None:
+            self.expr.validate(tmp)
+        self.checkRecursion([])
+
+    def __str__(self):
+        try:
+            return super(ParseElementEnhance,self).__str__()
+        except:
+            pass
+
+        if self.strRepr is None and self.expr is not None:
+            self.strRepr = "%s:(%s)" % (self.__class__.__name__, _ustr(self.expr))
+        return self.strRepr
+
+
+class FollowedBy(ParseElementEnhance):
+    """Lookahead matching of the given parse expression.  FollowedBy
+    does *not* advance the parsing position within the input string, it only
+    verifies that the specified parse expression matches at the current
+    position.  FollowedBy always returns a null token list."""
+    def __init__(self, expr):
+        super(FollowedBy,self).__init__(expr)
+        self.mayReturnEmpty = True
+
+    def parseImpl(self, instring, loc, doActions=True):
+        self.expr.tryParse(instring, loc)
+        return loc, []
+
+
+class NotAny(ParseElementEnhance):
+    """Lookahead to disallow matching with the given parse expression.  NotAny
+    does *not* advance the parsing position within the input string, it only
+    verifies that the specified parse expression does *not* match at the current
+    position.  Also, NotAny does *not* skip over leading whitespace. NotAny
+    always returns a null token list.  May be constructed using the '~' operator."""
+    def __init__(self, expr):
+        super(NotAny,self).__init__(expr)
+        #~ self.leaveWhitespace()
+        self.skipWhitespace = False  # do NOT use self.leaveWhitespace(), don't want to propagate to exprs
+        self.mayReturnEmpty = True
+        self.errmsg = "Found unwanted token, "+_ustr(self.expr)
+        #self.myException = ParseException("",0,self.errmsg,self)
+
+    def parseImpl(self, instring, loc, doActions=True):
+        try:
+            self.expr.tryParse(instring, loc)
+        except (ParseException,IndexError):
+            pass
+        else:
+            #~ raise ParseException(instring, loc, self.errmsg)
+            exc = self.myException
+            exc.loc = loc
+            exc.pstr = instring
+            raise exc
+        return loc, []
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        if self.strRepr is None:
+            self.strRepr = "~{" + _ustr(self.expr) + "}"
+
+        return self.strRepr
+
+
+class ZeroOrMore(ParseElementEnhance):
+    """Optional repetition of zero or more of the given expression."""
+    def __init__(self, expr):
+        super(ZeroOrMore,self).__init__(expr)
+        self.mayReturnEmpty = True
+
+    def parseImpl(self, instring, loc, doActions=True):
+        tokens = []
+        try:
+            loc, tokens = self.expr._parse(instring, loc, doActions, callPreParse=False)
+            hasIgnoreExprs = (len(self.ignoreExprs) > 0)
+            while 1:
+                if hasIgnoreExprs:
+                    preloc = self._skipIgnorables(instring, loc)
+                else:
+                    preloc = loc
+                loc, tmptokens = self.expr._parse(instring, preloc, doActions)
+                if tmptokens or tmptokens.keys():
+                    tokens += tmptokens
+        except (ParseException,IndexError):
+            pass
+
+        return loc, tokens
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        if self.strRepr is None:
+            self.strRepr = "[" + _ustr(self.expr) + "]..."
+
+        return self.strRepr
+
+    def setResultsName(self, name, listAllMatches=False):
+        ret = super(ZeroOrMore,self).setResultsName(name,listAllMatches)
+        ret.saveAsList = True
+        return ret
+
+
+class OneOrMore(ParseElementEnhance):
+    """Repetition of one or more of the given expression."""
+    def parseImpl(self, instring, loc, doActions=True):
+        # must be at least one
+        loc, tokens = self.expr._parse(instring, loc, doActions, callPreParse=False)
+        try:
+            hasIgnoreExprs = (len(self.ignoreExprs) > 0)
+            while 1:
+                if hasIgnoreExprs:
+                    preloc = self._skipIgnorables(instring, loc)
+                else:
+                    preloc = loc
+                loc, tmptokens = self.expr._parse(instring, preloc, doActions)
+                if tmptokens or tmptokens.keys():
+                    tokens += tmptokens
+        except (ParseException,IndexError):
+            pass
+
+        return loc, tokens
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        if self.strRepr is None:
+            self.strRepr = "{" + _ustr(self.expr) + "}..."
+
+        return self.strRepr
+
+    def setResultsName(self, name, listAllMatches=False):
+        ret = super(OneOrMore,self).setResultsName(name,listAllMatches)
+        ret.saveAsList = True
+        return ret
+
+class _NullToken(object):
+    def __bool__(self):
+        return False
+    __nonzero__ = __bool__
+    def __str__(self):
+        return ""
+
+_optionalNotMatched = _NullToken()
+class Optional(ParseElementEnhance):
+    """Optional matching of the given expression.
+       A default return string can also be specified, if the optional expression
+       is not found.
+    """
+    def __init__(self, exprs, default=_optionalNotMatched):
+        super(Optional,self).__init__(exprs, savelist=False)
+        self.defaultValue = default
+        self.mayReturnEmpty = True
+
+    def parseImpl(self, instring, loc, doActions=True):
+        try:
+            loc, tokens = self.expr._parse(instring, loc, doActions, callPreParse=False)
+        except (ParseException,IndexError):
+            if self.defaultValue is not _optionalNotMatched:
+                if self.expr.resultsName:
+                    tokens = ParseResults([ self.defaultValue ])
+                    tokens[self.expr.resultsName] = self.defaultValue
+                else:
+                    tokens = [ self.defaultValue ]
+            else:
+                tokens = []
+        return loc, tokens
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        if self.strRepr is None:
+            self.strRepr = "[" + _ustr(self.expr) + "]"
+
+        return self.strRepr
+
+
+class SkipTo(ParseElementEnhance):
+    """Token for skipping over all undefined text until the matched expression is found.
+       If include is set to true, the matched expression is also parsed (the skipped text
+       and matched expression are returned as a 2-element list).  The ignore
+       argument is used to define grammars (typically quoted strings and comments) that
+       might contain false matches.
+    """
+    def __init__(self, other, include=False, ignore=None, failOn=None):
+        super(SkipTo, self).__init__(other)
+        self.ignoreExpr = ignore
+        self.mayReturnEmpty = True
+        self.mayIndexError = False
+        self.includeMatch = include
+        self.asList = False
+        if failOn is not None and isinstance(failOn, basestring):
+            self.failOn = Literal(failOn)
+        else:
+            self.failOn = failOn
+        self.errmsg = "No match found for "+_ustr(self.expr)
+        #self.myException = ParseException("",0,self.errmsg,self)
+
+    def parseImpl(self, instring, loc, doActions=True):
+        startLoc = loc
+        instrlen = len(instring)
+        expr = self.expr
+        failParse = False
+        while loc <= instrlen:
+            try:
+                if self.failOn:
+                    try:
+                        self.failOn.tryParse(instring, loc)
+                    except ParseBaseException:
+                        pass
+                    else:
+                        failParse = True
+                        raise ParseException(instring, loc, "Found expression " + str(self.failOn))
+                    failParse = False
+                if self.ignoreExpr is not None:
+                    while 1:
+                        try:
+                            loc = self.ignoreExpr.tryParse(instring,loc)
+                            print "found ignoreExpr, advance to", loc
+                        except ParseBaseException:
+                            break
+                expr._parse(instring, loc, doActions=False, callPreParse=False)
+                skipText = instring[startLoc:loc]
+                if self.includeMatch:
+                    loc,mat = expr._parse(instring,loc,doActions,callPreParse=False)
+                    if mat:
+                        skipRes = ParseResults(skipText)
+                        skipRes += mat
+                        return loc, [ skipRes ]
+                    else:
+                        return loc, [ skipText ]
+                else:
+                    return loc, [ skipText ]
+            except (ParseException,IndexError):
+                if failParse:
+                    raise
+                else:
+                    loc += 1
+        exc = self.myException
+        exc.loc = loc
+        exc.pstr = instring
+        raise exc
+
+class Forward(ParseElementEnhance):
+    """Forward declaration of an expression to be defined later -
+       used for recursive grammars, such as algebraic infix notation.
+       When the expression is known, it is assigned to the Forward variable using the '<<' operator.
+
+       Note: take care when assigning to Forward not to overlook precedence of operators.
+       Specifically, '|' has a lower precedence than '<<', so that::
+          fwdExpr << a | b | c
+       will actually be evaluated as::
+          (fwdExpr << a) | b | c
+       thereby leaving b and c out as parseable alternatives.  It is recommended that you
+       explicitly group the values inserted into the Forward::
+          fwdExpr << (a | b | c)
+    """
+    def __init__(self, other=None):
+        super(Forward,self).__init__(other, savelist=False)
+
+    def __lshift__(self, other):
+        if isinstance(other, basestring):
+            other = Literal(other)
+        self.expr = other
+        self.mayReturnEmpty = other.mayReturnEmpty
+        self.strRepr = None
+        self.mayIndexError = self.expr.mayIndexError
+        self.mayReturnEmpty = self.expr.mayReturnEmpty
+        self.setWhitespaceChars(self.expr.whiteChars)
+        self.skipWhitespace = self.expr.skipWhitespace
+        self.saveAsList = self.expr.saveAsList
+        self.ignoreExprs.extend(self.expr.ignoreExprs)
+        return None
+
+    def leaveWhitespace(self):
+        self.skipWhitespace = False
+        return self
+
+    def streamline(self):
+        if not self.streamlined:
+            self.streamlined = True
+            if self.expr is not None:
+                self.expr.streamline()
+        return self
+
+    def validate(self, validateTrace=[]):
+        if self not in validateTrace:
+            tmp = validateTrace[:]+[self]
+            if self.expr is not None:
+                self.expr.validate(tmp)
+        self.checkRecursion([])
+
+    def __str__(self):
+        if hasattr(self,"name"):
+            return self.name
+
+        self._revertClass = self.__class__
+        self.__class__ = _ForwardNoRecurse
+        try:
+            if self.expr is not None:
+                retString = _ustr(self.expr)
+            else:
+                retString = "None"
+        finally:
+            self.__class__ = self._revertClass
+        return self.__class__.__name__ + ": " + retString
+
+    def copy(self):
+        if self.expr is not None:
+            return super(Forward,self).copy()
+        else:
+            ret = Forward()
+            ret << self
+            return ret
+
+class _ForwardNoRecurse(Forward):
+    def __str__(self):
+        return "..."
+
+class TokenConverter(ParseElementEnhance):
+    """Abstract subclass of ParseExpression, for converting parsed results."""
+    def __init__(self, expr, savelist=False):
+        super(TokenConverter,self).__init__(expr)#, savelist)
+        self.saveAsList = False
+
+class Upcase(TokenConverter):
+    """Converter to upper case all matching tokens."""
+    def __init__(self, *args):
+        super(Upcase,self).__init__(*args)
+        warnings.warn("Upcase class is deprecated, use upcaseTokens parse action instead",
+                      DeprecationWarning,stacklevel=2)
+
+    def postParse(self, instring, loc, tokenlist):
+        return list(map(string.upper, tokenlist))
+
+
+class Combine(TokenConverter):
+    """Converter to concatenate all matching tokens to a single string.
+       By default, the matching patterns must also be contiguous in the input string;
+       this can be disabled by specifying 'adjacent=False' in the constructor.
+    """
+    def __init__(self, expr, joinString="", adjacent=True):
+        super(Combine,self).__init__(expr)
+        # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself
+        if adjacent:
+            self.leaveWhitespace()
+        self.adjacent = adjacent
+        self.skipWhitespace = True
+        self.joinString = joinString
+
+    def ignore(self, other):
+        if self.adjacent:
+            ParserElement.ignore(self, other)
+        else:
+            super(Combine, self).ignore(other)
+        return self
+
+    def postParse(self, instring, loc, tokenlist):
+        retToks = tokenlist.copy()
+        del retToks[:]
+        retToks += ParseResults([ "".join(tokenlist._asStringList(self.joinString)) ], modal=self.modalResults)
+
+        if self.resultsName and len(retToks.keys())>0:
+            return [ retToks ]
+        else:
+            return retToks
+
+class Group(TokenConverter):
+    """Converter to return the matched tokens as a list - useful for returning tokens of ZeroOrMore and OneOrMore expressions."""
+    def __init__(self, expr):
+        super(Group,self).__init__(expr)
+        self.saveAsList = True
+
+    def postParse(self, instring, loc, tokenlist):
+        return [ tokenlist ]
+
+class Dict(TokenConverter):
+    """Converter to return a repetitive expression as a list, but also as a dictionary.
+       Each element can also be referenced using the first token in the expression as its key.
+       Useful for tabular report scraping when the first column can be used as a item key.
+    """
+    def __init__(self, exprs):
+        super(Dict,self).__init__(exprs)
+        self.saveAsList = True
+
+    def postParse(self, instring, loc, tokenlist):
+        for i,tok in enumerate(tokenlist):
+            if len(tok) == 0:
+                continue
+            ikey = tok[0]
+            if isinstance(ikey,int):
+                ikey = _ustr(tok[0]).strip()
+            if len(tok)==1:
+                tokenlist[ikey] = _ParseResultsWithOffset("",i)
+            elif len(tok)==2 and not isinstance(tok[1],ParseResults):
+                tokenlist[ikey] = _ParseResultsWithOffset(tok[1],i)
+            else:
+                dictvalue = tok.copy() #ParseResults(i)
+                del dictvalue[0]
+                if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.keys()):
+                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue,i)
+                else:
+                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0],i)
+
+        if self.resultsName:
+            return [ tokenlist ]
+        else:
+            return tokenlist
+
+
+class Suppress(TokenConverter):
+    """Converter for ignoring the results of a parsed expression."""
+    def postParse(self, instring, loc, tokenlist):
+        return []
+
+    def suppress(self):
+        return self
+
+
+class OnlyOnce(object):
+    """Wrapper for parse actions, to ensure they are only called once."""
+    def __init__(self, methodCall):
+        self.callable = ParserElement._normalizeParseActionArgs(methodCall)
+        self.called = False
+    def __call__(self,s,l,t):
+        if not self.called:
+            results = self.callable(s,l,t)
+            self.called = True
+            return results
+        raise ParseException(s,l,"")
+    def reset(self):
+        self.called = False
+
+def traceParseAction(f):
+    """Decorator for debugging parse actions."""
+    f = ParserElement._normalizeParseActionArgs(f)
+    def z(*paArgs):
+        thisFunc = f.func_name
+        s,l,t = paArgs[-3:]
+        if len(paArgs)>3:
+            thisFunc = paArgs[0].__class__.__name__ + '.' + thisFunc
+        sys.stderr.write(">>entering %s(line: '%s', %d, %s)\n" % (thisFunc,line(l,s),l,t))
+        try:
+            ret = f(*paArgs)
+        except Exception, exc:
+            sys.stderr.write("<<leaving %s (exception: %s)\n" % (thisFunc,exc))
+            raise
+        sys.stderr.write("<<leaving %s (ret: %s)\n" % (thisFunc,ret))
+        return ret
+    try:
+        z.__name__ = f.__name__
+    except AttributeError:
+        pass
+    return z
+
+#
+# global helpers
+#
+def delimitedList(expr, delim=",", combine=False):
+    """Helper to define a delimited list of expressions - the delimiter defaults to ','.
+       By default, the list elements and delimiters can have intervening whitespace, and
+       comments, but this can be overridden by passing 'combine=True' in the constructor.
+       If combine is set to True, the matching tokens are returned as a single token
+       string, with the delimiters included; otherwise, the matching tokens are returned
+       as a list of tokens, with the delimiters suppressed.
+    """
+    dlName = _ustr(expr)+" ["+_ustr(delim)+" "+_ustr(expr)+"]..."
+    if combine:
+        return Combine(expr + ZeroOrMore(delim + expr)).setName(dlName)
+    else:
+        return (expr + ZeroOrMore(Suppress(delim) + expr)).setName(dlName)
+
+def countedArray(expr):
+    """Helper to define a counted list of expressions.
+       This helper defines a pattern of the form::
+           integer expr expr expr...
+       where the leading integer tells how many expr expressions follow.
+       The matched tokens returns the array of expr tokens as a list - the leading count token is suppressed.
+    """
+    arrayExpr = Forward()
+    def countFieldParseAction(s,l,t):
+        n = int(t[0])
+        arrayExpr << (n and Group(And([expr]*n)) or Group(empty))
+        return []
+    return (Word(nums).setName("arrayLen").setParseAction(countFieldParseAction, callDuringTry=True) + arrayExpr)
+
+def _flatten(L):
+    if type(L) is not list: return [L]
+    if L == []: return L
+    return _flatten(L[0]) + _flatten(L[1:])
+
+def matchPreviousLiteral(expr):
+    """Helper to define an expression that is indirectly defined from
+       the tokens matched in a previous expression, that is, it looks
+       for a 'repeat' of a previous expression.  For example::
+           first = Word(nums)
+           second = matchPreviousLiteral(first)
+           matchExpr = first + ":" + second
+       will match "1:1", but not "1:2".  Because this matches a
+       previous literal, will also match the leading "1:1" in "1:10".
+       If this is not desired, use matchPreviousExpr.
+       Do *not* use with packrat parsing enabled.
+    """
+    rep = Forward()
+    def copyTokenToRepeater(s,l,t):
+        if t:
+            if len(t) == 1:
+                rep << t[0]
+            else:
+                # flatten t tokens
+                tflat = _flatten(t.asList())
+                rep << And([ Literal(tt) for tt in tflat ])
+        else:
+            rep << Empty()
+    expr.addParseAction(copyTokenToRepeater, callDuringTry=True)
+    return rep
+
+def matchPreviousExpr(expr):
+    """Helper to define an expression that is indirectly defined from
+       the tokens matched in a previous expression, that is, it looks
+       for a 'repeat' of a previous expression.  For example::
+           first = Word(nums)
+           second = matchPreviousExpr(first)
+           matchExpr = first + ":" + second
+       will match "1:1", but not "1:2".  Because this matches by
+       expressions, will *not* match the leading "1:1" in "1:10";
+       the expressions are evaluated first, and then compared, so
+       "1" is compared with "10".
+       Do *not* use with packrat parsing enabled.
+    """
+    rep = Forward()
+    e2 = expr.copy()
+    rep << e2
+    def copyTokenToRepeater(s,l,t):
+        matchTokens = _flatten(t.asList())
+        def mustMatchTheseTokens(s,l,t):
+            theseTokens = _flatten(t.asList())
+            if  theseTokens != matchTokens:
+                raise ParseException("",0,"")
+        rep.setParseAction(mustMatchTheseTokens, callDuringTry=True)
+    expr.addParseAction(copyTokenToRepeater, callDuringTry=True)
+    return rep
+
+def _escapeRegexRangeChars(s):
+    #~  escape these chars: ^-]
+    for c in r"\^-]":
+        s = s.replace(c,_bslash+c)
+    s = s.replace("\n",r"\n")
+    s = s.replace("\t",r"\t")
+    return _ustr(s)
+
+def oneOf(strs, caseless=False, useRegex=True):
+    """Helper to quickly define a set of alternative Literals, and makes sure to do
+       longest-first testing when there is a conflict, regardless of the input order,
+       but returns a MatchFirst for best performance.
+
+       Parameters:
+        - strs - a string of space-delimited literals, or a list of string literals
+        - caseless - (default=False) - treat all literals as caseless
+        - useRegex - (default=True) - as an optimization, will generate a Regex
+          object; otherwise, will generate a MatchFirst object (if caseless=True, or
+          if creating a Regex raises an exception)
+    """
+    if caseless:
+        isequal = (lambda a,b: a.upper() == b.upper())
+        masks = (lambda a,b: b.upper().startswith(a.upper()))
+        parseElementClass = CaselessLiteral
+    else:
+        isequal = (lambda a,b: a == b)
+        masks = (lambda a,b: b.startswith(a))
+        parseElementClass = Literal
+
+    if isinstance(strs,(list,tuple)):
+        symbols = list(strs[:])
+    elif isinstance(strs,basestring):
+        symbols = strs.split()
+    else:
+        warnings.warn("Invalid argument to oneOf, expected string or list",
+                      SyntaxWarning, stacklevel=2)
+
+    i = 0
+    while i < len(symbols)-1:
+        cur = symbols[i]
+        for j,other in enumerate(symbols[i+1:]):
+            if (isequal(other, cur)):
+                del symbols[i+j+1]
+                break
+            elif (masks(cur, other)):
+                del symbols[i+j+1]
+                symbols.insert(i,other)
+                cur = other
+                break
+        else:
+            i += 1
+
+    if not caseless and useRegex:
+        #~ print (strs,"->", "|".join([ _escapeRegexChars(sym) for sym in symbols]))
+        try:
+            if len(symbols)==len("".join(symbols)):
+                return Regex("[%s]" % "".join([ _escapeRegexRangeChars(sym) for sym in symbols]))
+            else:
+                return Regex("|".join([ re.escape(sym) for sym in symbols]))
+        except:
+            warnings.warn("Exception creating Regex for oneOf, building MatchFirst",
+                          SyntaxWarning, stacklevel=2)
+
+
+    # last resort, just use MatchFirst
+    return MatchFirst([ parseElementClass(sym) for sym in symbols ])
+
+def dictOf(key, value):
+    """Helper to easily and clearly define a dictionary by specifying the respective patterns
+       for the key and value.  Takes care of defining the Dict, ZeroOrMore, and Group tokens
+       in the proper order.  The key pattern can include delimiting markers or punctuation,
+       as long as they are suppressed, thereby leaving the significant key text.  The value
+       pattern can include named results, so that the Dict results can include named token
+       fields.
+    """
+    return Dict(ZeroOrMore(Group (key + value)))
+
+def originalTextFor(expr, asString=True):
+    """Helper to return the original, untokenized text for a given expression.  Useful to
+       restore the parsed fields of an HTML start tag into the raw tag text itself, or to
+       revert separate tokens with intervening whitespace back to the original matching
+       input text. Simpler to use than the parse action keepOriginalText, and does not
+       require the inspect module to chase up the call stack.  By default, returns a
+       string containing the original parsed text.
+
+       If the optional asString argument is passed as False, then the return value is a
+       ParseResults containing any results names that were originally matched, and a
+       single token containing the original matched text from the input string.  So if
+       the expression passed to originalTextFor contains expressions with defined
+       results names, you must set asString to False if you want to preserve those
+       results name values."""
+    locMarker = Empty().setParseAction(lambda s,loc,t: loc)
+    matchExpr = locMarker("_original_start") + expr + locMarker("_original_end")
+    if asString:
+        extractText = lambda s,l,t: s[t._original_start:t._original_end]
+    else:
+        def extractText(s,l,t):
+            del t[:]
+            t.insert(0, s[t._original_start:t._original_end])
+            del t["_original_start"]
+            del t["_original_end"]
+    matchExpr.setParseAction(extractText)
+    return matchExpr
+
+# convenience constants for positional expressions
+empty       = Empty().setName("empty")
+lineStart   = LineStart().setName("lineStart")
+lineEnd     = LineEnd().setName("lineEnd")
+stringStart = StringStart().setName("stringStart")
+stringEnd   = StringEnd().setName("stringEnd")
+
+_escapedPunc = Word(_bslash, r"\[]-*.$+^?()~ ", exact=2).setParseAction(lambda s,l,t:t[0][1])
+_printables_less_backslash = "".join([ c for c in printables if c not in  r"\]" ])
+_escapedHexChar = Combine(Suppress(_bslash + "0x") + Word(hexnums)).setParseAction(lambda s,l,t:unichr(int(t[0],16)))
+_escapedOctChar = Combine(Suppress(_bslash) + Word("0","01234567")).setParseAction(lambda s,l,t:unichr(int(t[0],8)))
+_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | Word(_printables_less_backslash,exact=1)
+_charRange = Group(_singleChar + Suppress("-") + _singleChar)
+_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group(OneOrMore(_charRange | _singleChar)).setResultsName("body") + "]"
+
+_expanded = lambda p: (isinstance(p,ParseResults) and ''.join([ unichr(c) for c in range(ord(p[0]),ord(p[1])+1) ]) or p)
+
+def srange(s):
+    r"""Helper to easily define string ranges for use in Word construction.  Borrows
+       syntax from regexp '[]' string range definitions::
+          srange("[0-9]")   -> "0123456789"
+          srange("[a-z]")   -> "abcdefghijklmnopqrstuvwxyz"
+          srange("[a-z$_]") -> "abcdefghijklmnopqrstuvwxyz$_"
+       The input string must be enclosed in []'s, and the returned string is the expanded
+       character set joined into a single string.
+       The values enclosed in the []'s may be::
+          a single character
+          an escaped character with a leading backslash (such as \- or \])
+          an escaped hex character with a leading '\0x' (\0x21, which is a '!' character)
+          an escaped octal character with a leading '\0' (\041, which is a '!' character)
+          a range of any of the above, separated by a dash ('a-z', etc.)
+          any combination of the above ('aeiouy', 'a-zA-Z0-9_$', etc.)
+    """
+    try:
+        return "".join([_expanded(part) for part in _reBracketExpr.parseString(s).body])
+    except:
+        return ""
+
+def matchOnlyAtCol(n):
+    """Helper method for defining parse actions that require matching at a specific
+       column in the input text.
+    """
+    def verifyCol(strg,locn,toks):
+        if col(locn,strg) != n:
+            raise ParseException(strg,locn,"matched token not at column %d" % n)
+    return verifyCol
+
+def replaceWith(replStr):
+    """Helper method for common parse actions that simply return a literal value.  Especially
+       useful when used with transformString().
+    """
+    def _replFunc(*args):
+        return [replStr]
+    return _replFunc
+
+def removeQuotes(s,l,t):
+    """Helper parse action for removing quotation marks from parsed quoted strings.
+       To use, add this parse action to quoted string using::
+         quotedString.setParseAction(removeQuotes)
+    """
+    return t[0][1:-1]
+
+def upcaseTokens(s,l,t):
+    """Helper parse action to convert tokens to upper case."""
+    return [ tt.upper() for tt in map(_ustr,t) ]
+
+def downcaseTokens(s,l,t):
+    """Helper parse action to convert tokens to lower case."""
+    return [ tt.lower() for tt in map(_ustr,t) ]
+
+def keepOriginalText(s,startLoc,t):
+    """Helper parse action to preserve original parsed text,
+       overriding any nested parse actions."""
+    try:
+        endloc = getTokensEndLoc()
+    except ParseException:
+        raise ParseFatalException("incorrect usage of keepOriginalText - may only be called as a parse action")
+    del t[:]
+    t += ParseResults(s[startLoc:endloc])
+    return t
+
+def getTokensEndLoc():
+    """Method to be called from within a parse action to determine the end
+       location of the parsed tokens."""
+    import inspect
+    fstack = inspect.stack()
+    try:
+        # search up the stack (through intervening argument normalizers) for correct calling routine
+        for f in fstack[2:]:
+            if f[3] == "_parseNoCache":
+                endloc = f[0].f_locals["loc"]
+                return endloc
+        else:
+            raise ParseFatalException("incorrect usage of getTokensEndLoc - may only be called from within a parse action")
+    finally:
+        del fstack
+
+def _makeTags(tagStr, xml):
+    """Internal helper to construct opening and closing tag expressions, given a tag name"""
+    if isinstance(tagStr,basestring):
+        resname = tagStr
+        tagStr = Keyword(tagStr, caseless=not xml)
+    else:
+        resname = tagStr.name
+
+    tagAttrName = Word(alphas,alphanums+"_-:")
+    if (xml):
+        tagAttrValue = dblQuotedString.copy().setParseAction(removeQuotes)
+        openTag = Suppress("<") + tagStr + \
+                Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue))) + \
+                Optional("/",default=[False]).setResultsName("empty").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(">")
+    else:
+        printablesLessRAbrack = "".join([ c for c in printables if c not in ">" ])
+        tagAttrValue = quotedString.copy().setParseAction(removeQuotes) | Word(printablesLessRAbrack)
+        openTag = Suppress("<") + tagStr + \
+                Dict(ZeroOrMore(Group(tagAttrName.setParseAction(downcaseTokens) + \
+                                       Optional(Suppress("=") + tagAttrValue)))) + \
+                Optional("/",default=[False]).setResultsName("empty").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(">")
+    closeTag = Combine(_L("</") + tagStr + ">")
+
+    openTag = openTag.setResultsName("start"+"".join(resname.replace(":"," ").title().split())).setName("<%s>" % tagStr)
+    closeTag = closeTag.setResultsName("end"+"".join(resname.replace(":"," ").title().split())).setName("</%s>" % tagStr)
+
+    return openTag, closeTag
+
+def makeHTMLTags(tagStr):
+    """Helper to construct opening and closing tag expressions for HTML, given a tag name"""
+    return _makeTags(tagStr, False)
+
+def makeXMLTags(tagStr):
+    """Helper to construct opening and closing tag expressions for XML, given a tag name"""
+    return _makeTags(tagStr, True)
+
+def withAttribute(*args,**attrDict):
+    """Helper to create a validating parse action to be used with start tags created
+       with makeXMLTags or makeHTMLTags. Use withAttribute to qualify a starting tag
+       with a required attribute value, to avoid false matches on common tags such as
+       <TD> or <DIV>.
+
+       Call withAttribute with a series of attribute names and values. Specify the list
+       of filter attributes names and values as:
+        - keyword arguments, as in (class="Customer",align="right"), or
+        - a list of name-value tuples, as in (("ns1:class", "Customer"), ("ns2:align","right"))
+       For attribute names with a namespace prefix, you must use the second form.  Attribute
+       names are matched insensitive to upper/lower case.
+
+       To verify that the attribute exists, but without specifying a value, pass
+       withAttribute.ANY_VALUE as the value.
+       """
+    if args:
+        attrs = args[:]
+    else:
+        attrs = attrDict.items()
+    attrs = [(k,v) for k,v in attrs]
+    def pa(s,l,tokens):
+        for attrName,attrValue in attrs:
+            if attrName not in tokens:
+                raise ParseException(s,l,"no matching attribute " + attrName)
+            if attrValue != withAttribute.ANY_VALUE and tokens[attrName] != attrValue:
+                raise ParseException(s,l,"attribute '%s' has value '%s', must be '%s'" %
+                                     (attrName, tokens[attrName], attrValue))
+    return pa
+withAttribute.ANY_VALUE = object()
+
+opAssoc = _Constants()
+opAssoc.LEFT = object()
+opAssoc.RIGHT = object()
+
+def operatorPrecedence(baseExpr, opList):
+    """Helper method for constructing grammars of expressions made up of
+       operators working in a precedence hierarchy.  Operators may be unary or
+       binary, left- or right-associative.  Parse actions can also be attached
+       to operator expressions.
+
+       Parameters:
+        - baseExpr - expression representing the most basic element for the nested
+        - opList - list of tuples, one for each operator precedence level in the
+          expression grammar; each tuple is of the form
+          (opExpr, numTerms, rightLeftAssoc, parseAction), where:
+           - opExpr is the pyparsing expression for the operator;
+              may also be a string, which will be converted to a Literal;
+              if numTerms is 3, opExpr is a tuple of two expressions, for the
+              two operators separating the 3 terms
+           - numTerms is the number of terms for this operator (must
+              be 1, 2, or 3)
+           - rightLeftAssoc is the indicator whether the operator is
+              right or left associative, using the pyparsing-defined
+              constants opAssoc.RIGHT and opAssoc.LEFT.
+           - parseAction is the parse action to be associated with
+              expressions matching this operator expression (the
+              parse action tuple member may be omitted)
+    """
+    ret = Forward()
+    lastExpr = baseExpr | (Suppress('(') + ret + Suppress(')'))
+    for i,operDef in enumerate(opList):
+        opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4]
+        if arity == 3:
+            if opExpr is None or len(opExpr) != 2:
+                raise ValueError("if numterms=3, opExpr must be a tuple or list of two expressions")
+            opExpr1, opExpr2 = opExpr
+        thisExpr = Forward()#.setName("expr%d" % i)
+        if rightLeftAssoc == opAssoc.LEFT:
+            if arity == 1:
+                matchExpr = FollowedBy(lastExpr + opExpr) + Group(lastExpr + OneOrMore(opExpr))
+            elif arity == 2:
+                if opExpr is not None:
+                    matchExpr = FollowedBy(lastExpr + opExpr + lastExpr) + Group(lastExpr + OneOrMore(opExpr + lastExpr))
+                else:
+                    matchExpr = FollowedBy(lastExpr+lastExpr) + Group(lastExpr + OneOrMore(lastExpr))
+            elif arity == 3:
+                matchExpr = FollowedBy(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) + \
+                          Group(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr)
+            else:
+                raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
+        elif rightLeftAssoc == opAssoc.RIGHT:
+            if arity == 1:
+                # try to avoid LR with this extra test
+                if not isinstance(opExpr, Optional):
+                    opExpr = Optional(opExpr)
+                matchExpr = FollowedBy(opExpr.expr + thisExpr) + Group(opExpr + thisExpr)
+            elif arity == 2:
+                if opExpr is not None:
+                    matchExpr = FollowedBy(lastExpr + opExpr + thisExpr) + Group(lastExpr + OneOrMore(opExpr + thisExpr))
+                else:
+                    matchExpr = FollowedBy(lastExpr + thisExpr) + Group(lastExpr + OneOrMore(thisExpr))
+            elif arity == 3:
+                matchExpr = FollowedBy(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) + \
+                          Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr)
+            else:
+                raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
+        else:
+            raise ValueError("operator must indicate right or left associativity")
+        if pa:
+            matchExpr.setParseAction(pa)
+        thisExpr << (matchExpr | lastExpr)
+        lastExpr = thisExpr
+    ret << lastExpr
+    return ret
+
+dblQuotedString = Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\x[0-9a-fA-F]+)|(?:\\.))*"').setName("string enclosed in double quotes")
+sglQuotedString = Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\x[0-9a-fA-F]+)|(?:\\.))*'").setName("string enclosed in single quotes")
+quotedString = Regex(r'''(?:"(?:[^"\n\r\\]|(?:"")|(?:\\x[0-9a-fA-F]+)|(?:\\.))*")|(?:'(?:[^'\n\r\\]|(?:'')|(?:\\x[0-9a-fA-F]+)|(?:\\.))*')''').setName("quotedString using single or double quotes")
+unicodeString = Combine(_L('u') + quotedString.copy())
+
+def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString):
+    """Helper method for defining nested lists enclosed in opening and closing
+       delimiters ("(" and ")" are the default).
+
+       Parameters:
+        - opener - opening character for a nested list (default="("); can also be a pyparsing expression
+        - closer - closing character for a nested list (default=")"); can also be a pyparsing expression
+        - content - expression for items within the nested lists (default=None)
+        - ignoreExpr - expression for ignoring opening and closing delimiters (default=quotedString)
+
+       If an expression is not provided for the content argument, the nested
+       expression will capture all whitespace-delimited content between delimiters
+       as a list of separate values.
+
+       Use the ignoreExpr argument to define expressions that may contain
+       opening or closing characters that should not be treated as opening
+       or closing characters for nesting, such as quotedString or a comment
+       expression.  Specify multiple expressions using an Or or MatchFirst.
+       The default is quotedString, but if no expressions are to be ignored,
+       then pass None for this argument.
+    """
+    if opener == closer:
+        raise ValueError("opening and closing strings cannot be the same")
+    if content is None:
+        if isinstance(opener,basestring) and isinstance(closer,basestring):
+            if len(opener) == 1 and len(closer)==1:
+                if ignoreExpr is not None:
+                    content = (Combine(OneOrMore(~ignoreExpr +
+                                                 CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS,exact=1))
+                                      ).setParseAction(lambda t:t[0].strip()))
+                else:
+                    content = (empty+CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS
+                                               ).setParseAction(lambda t:t[0].strip()))
+            else:
+                if ignoreExpr is not None:
+                    content = (Combine(OneOrMore(~ignoreExpr +
+                                                 ~Literal(opener) + ~Literal(closer) +
+                                                 CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1))
+                                      ).setParseAction(lambda t:t[0].strip()))
+                else:
+                    content = (Combine(OneOrMore(~Literal(opener) + ~Literal(closer) +
+                                                 CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1))
+                                      ).setParseAction(lambda t:t[0].strip()))
+        else:
+            raise ValueError("opening and closing arguments must be strings if no content expression is given")
+    ret = Forward()
+    if ignoreExpr is not None:
+        ret << Group(Suppress(opener) + ZeroOrMore(ignoreExpr | ret | content) + Suppress(closer))
+    else:
+        ret << Group(Suppress(opener) + ZeroOrMore(ret | content)  + Suppress(closer))
+    return ret
+
+def indentedBlock(blockStatementExpr, indentStack, indent=True):
+    """Helper method for defining space-delimited indentation blocks, such as
+       those used to define block statements in Python source code.
+
+       Parameters:
+        - blockStatementExpr - expression defining syntax of statement that
+            is repeated within the indented block
+        - indentStack - list created by caller to manage indentation stack
+            (multiple statementWithIndentedBlock expressions within a single grammar
+            should share a common indentStack)
+        - indent - boolean indicating whether block must be indented beyond the
+            the current level; set to False for block of left-most statements
+            (default=True)
+
+       A valid block must contain at least one blockStatement.
+    """
+    def checkPeerIndent(s,l,t):
+        if l >= len(s): return
+        curCol = col(l,s)
+        if curCol != indentStack[-1]:
+            if curCol > indentStack[-1]:
+                raise ParseFatalException(s,l,"illegal nesting")
+            raise ParseException(s,l,"not a peer entry")
+
+    def checkSubIndent(s,l,t):
+        curCol = col(l,s)
+        if curCol > indentStack[-1]:
+            indentStack.append(curCol)
+        else:
+            raise ParseException(s,l,"not a subentry")
+
+    def checkUnindent(s,l,t):
+        if l >= len(s): return
+        curCol = col(l,s)
+        if not(indentStack and curCol < indentStack[-1] and curCol <= indentStack[-2]):
+            raise ParseException(s,l,"not an unindent")
+        indentStack.pop()
+
+    NL = OneOrMore(LineEnd().setWhitespaceChars("\t ").suppress())
+    INDENT = Empty() + Empty().setParseAction(checkSubIndent)
+    PEER   = Empty().setParseAction(checkPeerIndent)
+    UNDENT = Empty().setParseAction(checkUnindent)
+    if indent:
+        smExpr = Group(Optional(NL) +
+                        FollowedBy(blockStatementExpr) +
+                        INDENT + (OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL))) + UNDENT)
+    else:
+        smExpr = Group(Optional(NL) +
+                        (OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL))))
+    blockStatementExpr.ignore(_bslash + LineEnd())
+    return smExpr
+
+alphas8bit = srange(r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]")
+punc8bit = srange(r"[\0xa1-\0xbf\0xd7\0xf7]")
+
+anyOpenTag,anyCloseTag = makeHTMLTags(Word(alphas,alphanums+"_:"))
+commonHTMLEntity = Combine(_L("&") + oneOf("gt lt amp nbsp quot").setResultsName("entity") +";").streamline()
+_htmlEntityMap = dict(zip("gt lt amp nbsp quot".split(),'><& "'))
+replaceHTMLEntity = lambda t : t.entity in _htmlEntityMap and _htmlEntityMap[t.entity] or None
+
+# it's easy to get these comment structures wrong - they're very common, so may as well make them available
+cStyleComment = Regex(r"/\*(?:[^*]*\*+)+?/").setName("C style comment")
+
+htmlComment = Regex(r"<!--[\s\S]*?-->")
+restOfLine = Regex(r".*").leaveWhitespace()
+dblSlashComment = Regex(r"\/\/(\\\n|.)*").setName("// comment")
+cppStyleComment = Regex(r"/(?:\*(?:[^*]*\*+)+?/|/[^\n]*(?:\n[^\n]*)*?(?:(?<!\\)|\Z))").setName("C++ style comment")
+
+javaStyleComment = cppStyleComment
+pythonStyleComment = Regex(r"#.*").setName("Python style comment")
+_noncomma = "".join([ c for c in printables if c != "," ])
+_commasepitem = Combine(OneOrMore(Word(_noncomma) +
+                                  Optional(Word(" \t") +
+                                            ~Literal(",") + ~LineEnd()))).streamline().setName("commaItem")
+commaSeparatedList = delimitedList(Optional(quotedString | _commasepitem, default="")).setName("commaSeparatedList")
+
+
+if __name__ == "__main__":
+
+    def test(teststring):
+        try:
+            tokens = simpleSQL.parseString(teststring)
+            tokenlist = tokens.asList()
+            print (teststring + "->"   + str(tokenlist))
+            print ("tokens = "         + str(tokens))
+            print ("tokens.columns = " + str(tokens.columns))
+            print ("tokens.tables = "  + str(tokens.tables))
+            print (tokens.asXML("SQL",True))
+        except ParseBaseException,err:
+            print (teststring + "->")
+            print (err.line)
+            print (" "*(err.column-1) + "^")
+            print (err)
+        print()
+
+    selectToken    = CaselessLiteral("select")
+    fromToken      = CaselessLiteral("from")
+
+    ident          = Word(alphas, alphanums + "_$")
+    columnName     = delimitedList(ident, ".", combine=True).setParseAction(upcaseTokens)
+    columnNameList = Group(delimitedList(columnName))#.setName("columns")
+    tableName      = delimitedList(ident, ".", combine=True).setParseAction(upcaseTokens)
+    tableNameList  = Group(delimitedList(tableName))#.setName("tables")
+    simpleSQL      = (selectToken + \
+                       ('*' | columnNameList).setResultsName("columns") + \
+                       fromToken + \
+                       tableNameList.setResultsName("tables"))
+
+    test("SELECT * from XYZZY, ABC")
+    test("select * from SYS.XYZZY")
+    test("Select A from Sys.dual")
+    test("Select AA,BB,CC from Sys.dual")
+    test("Select A, B, C from Sys.dual")
+    test("Select A, B, C from Sys.dual")
+    test("Xelect A, B, C from Sys.dual")
+    test("Select A, B, C frox Sys.dual")
+    test("Select")
+    test("Select ^^^ frox Sys.dual")
+    test("Select A, B, C from Sys.dual, Table2   ")
--- a/orpg/gametree/gametree.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/gametree.py	Mon Mar 22 18:38:22 2010 -0600
@@ -61,7 +61,6 @@
 
 compiled_illegal_node_name_regex = re.compile("(!@|@@|@!|::)")
 
-
 STD_MENU_DELETE = wx.NewId()
 STD_MENU_DESIGN = wx.NewId()
 STD_MENU_USE = wx.NewId()
@@ -70,8 +69,10 @@
 STD_MENU_SEND = wx.NewId()
 STD_MENU_SAVE = wx.NewId()
 STD_MENU_ICON = wx.NewId()
+STD_MENU_RENAME = wx.NewId()
 STD_MENU_CLONE = wx.NewId()
-STD_MENU_ABOUT = wx.NewId()
+STD_MENU_REPLACE = wx.NewId()
+##STD_MENU_ABOUT = wx.NewId()
 STD_MENU_INDEX = wx.NewId()
 STD_MENU_NAMESPACE = wx.NewId()
 STD_MENU_HTML = wx.NewId()
@@ -92,12 +93,67 @@
 TOP_TREE_PROP = wx.NewId()
 TOP_FEATURES = wx.NewId()
 
+class Namespace:
+    def __init__(self, name):
+        self.name = name
+        self.pathmap = {}
+        self.leafmap = {}
+
+    def get_handler(self, path):
+        if path in self.pathmap:
+            return self.pathmap[path]
+        if path in self.leafmap:
+            handler_list = self.leafmap[path]
+            if len(handler_list)==1:
+                return handler_list[0]
+        return None
+
+    def path_is_duplicate(self, path, handler):
+        return (path in self.pathmap and self.pathmap[path] is not handler)
+
+    def add_path(self, path, handler):
+        self.pathmap[path] = handler
+
+    def add_leaf(self, name, handler):
+        if name in self.leafmap:
+            handler_list = self.leafmap[name]
+            #handler_list.remove(handler) # make sure we don't duplicate
+            handler_list.append(handler)
+        else:
+            self.leafmap[name]=[handler] # create list of one item
+
+    def delete_path(self, path, handler):
+        if path in self.pathmap:
+            del self.pathmap[path]
+
+    def delete_leaf(self, name, handler):
+        if name in self.leafmap:
+            handler_list = self.leafmap[name]
+            handler_list.remove(handler)
+
+
+class MultiReplacer(wx.Dialog):
+    def __init__(self, parent, id, title, findValue=''):
+        wx.Dialog.__init__(self, parent, id, title, size=(250, 210))
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(wx.StaticText(self, -1, "Find this text:"), 0, wx.EXPAND)
+        self.find = wx.TextCtrl(self, -1, "")
+        self.find.SetValue(findValue)
+        sizer.Add(self.find, 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.StaticText(self, -1, "Replace with the following lines.\nEach line creates a new clone."), 0, wx.EXPAND)
+        self.replace = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE)
+        sizer.Add(self.replace, 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL))
+        self.SetSizer(sizer)
+        
+
 class game_tree(wx.TreeCtrl):
     @debugging
     def __init__(self, parent, id):
         wx.TreeCtrl.__init__(self,parent,id,  wx.DefaultPosition, wx.DefaultSize,style=wx.TR_EDIT_LABELS | wx.TR_HAS_BUTTONS)
         self.session = open_rpg.get_component('session')
-        self.chat = open_rpg.get_component('chat')
         self.mainframe = open_rpg.get_component('frame')
         self.build_img_list()
         self.build_std_menu()
@@ -128,7 +184,8 @@
         self.rename_flag = 0
         self.image_cache = {}
 
-        self.InitializeIndexes()
+        self.initialize_indexes()
+        self.infopost_validation_message = False
 
     @debugging
     def add_nodehandler(self, nodehandler, nodeclass):
@@ -158,10 +215,8 @@
         self.add_nodehandler('dnd3echar_handler', orpg.gametree.nodehandlers.dnd3e.dnd3echar_handler)
         self.add_nodehandler('dnd35char_handler', orpg.gametree.nodehandlers.dnd35.dnd35char_handler)
         self.add_nodehandler('macro_handler', orpg.gametree.nodehandlers.chatmacro.macro_handler)
-        self.add_nodehandler('bonus_handler', orpg.gametree.nodehandlers.forms.bonus_handler)
         self.add_nodehandler('map_miniature_handler', orpg.gametree.nodehandlers.map_miniature_nodehandler.map_miniature_handler)
         self.add_nodehandler('minilib_handler', orpg.gametree.nodehandlers.minilib.minilib_handler)
-        self.add_nodehandler('mini_handler', orpg.gametree.nodehandlers.minilib.mini_handler)
         self.add_nodehandler('rpg_grid_handler', orpg.gametree.nodehandlers.rpg_grid.rpg_grid_handler)
         self.add_nodehandler('d20char_handler', orpg.gametree.nodehandlers.d20.d20char_handler)
         self.add_nodehandler('SWd20char_handler', orpg.gametree.nodehandlers.StarWarsd20.SWd20char_handler)
@@ -217,14 +272,14 @@
         evt.Skip()
 
     @debugging
-    def locate_valid_tree(self, error, msg):
+    def locate_valid_tree(self, caption, message, failed_filename):
         """prompts the user to locate a new tree file or create a new one"""
-        response = wx.MessageDialog(self, msg, error, wx.YES|wx.NO|wx.ICON_ERROR)
+        response = wx.MessageBox(message, caption, wx.YES|wx.NO|wx.ICON_ERROR)
         if response == wx.YES:
             file = None
             dlg = wx.FileDialog(self, "Locate Gametree file",
                                 orpg.dirpath.dir_struct["user"],
-                                filename[ ((filename.rfind(os.sep))+len(os.sep)):],
+                                failed_filename[ ((failed_filename.rfind(os.sep))+len(os.sep)):],
                                 "Gametree (*.xml)|*.xml|All files (*.*)|*.*",
                                 wx.OPEN | wx.CHANGE_DIR)
             if dlg.ShowModal() == wx.ID_OK: file = dlg.GetPath()
@@ -247,7 +302,7 @@
             emsg = "Gametree Missing!\n"+filename+" cannot be found.\n\n"\
                  "Would you like to locate it?\n"\
                  "(Selecting 'No' will cause a new default gametree to be generated)"
-            self.locate_valid_tree("Gametree Error", emsg)
+            self.locate_valid_tree("Gametree Error", emsg, filename)
             return
         # open and parse file
         try:
@@ -268,14 +323,14 @@
                  "lastgood.xml WILL BE OVERWRITTEN NEXT TIME YOU RUN OPENRPG.\n\n"\
                  "Would you like to select a different gametree file to use?\n"\
                  "(Selecting 'No' will cause a new default gametree to be generated)"
-            self.locate_valid_tree("Corrupt Gametree!", emsg)
+            self.locate_valid_tree("Corrupt Gametree!", emsg, filename)
             return
         # check we got a gametree
         if self.xml_root.tag != "gametree":
             emsg = filename+" does not appear to be a valid gametree file.\n\n"\
                  "Would you like to select a different gametree file to use?\n"\
                  "(Selecting 'No' will cause a new default gametree to be generated)"
-            self.locate_valid_tree("Invalid Gametree!", emsg)
+            self.locate_valid_tree("Invalid Gametree!", emsg, filename)
             return
         try:
             # version = self.xml_root.get("version")
@@ -331,14 +386,16 @@
         self.std_menu.Append(STD_MENU_WHISPER,"Whisper To Player")
         self.std_menu.AppendSeparator()
         self.std_menu.Append(STD_MENU_ICON,"Change &Icon")
+        self.std_menu.Append(STD_MENU_RENAME,"Rename")
         self.std_menu.Append(STD_MENU_DELETE,"D&elete")
         self.std_menu.Append(STD_MENU_CLONE,"&Clone")
+        self.std_menu.Append(STD_MENU_REPLACE,"Clone with Replace")
         self.std_menu.AppendMenu(STD_MENU_NODE_SUBMENU,"Node &Usefulness",useful_menu)
         self.std_menu.AppendSeparator()
         self.std_menu.Append(STD_MENU_SAVE,"&Save Node")
         self.std_menu.Append(STD_MENU_HTML,"E&xport as HTML")
         self.std_menu.AppendSeparator()
-        self.std_menu.Append(STD_MENU_ABOUT,"&About")
+##        self.std_menu.Append(STD_MENU_ABOUT,"&About")
         self.std_menu.Append(STD_MENU_INDEX,"Index")
         self.std_menu.Append(STD_MENU_NAMESPACE,"Namespace")
         self.Bind(wx.EVT_MENU, self.on_send_to, id=STD_MENU_SEND)
@@ -352,8 +409,10 @@
         self.Bind(wx.EVT_MENU, self.on_node_pp, id=STD_MENU_PP)
         self.Bind(wx.EVT_MENU, self.on_save, id=STD_MENU_SAVE)
         self.Bind(wx.EVT_MENU, self.on_icon, id=STD_MENU_ICON)
+        self.Bind(wx.EVT_MENU, self.on_rename, id=STD_MENU_RENAME)
         self.Bind(wx.EVT_MENU, self.on_clone, id=STD_MENU_CLONE)
-        self.Bind(wx.EVT_MENU, self.on_about, id=STD_MENU_ABOUT)
+        self.Bind(wx.EVT_MENU, self.on_clone_and_replace, id=STD_MENU_REPLACE)
+##        self.Bind(wx.EVT_MENU, self.on_about, id=STD_MENU_ABOUT)
         self.Bind(wx.EVT_MENU, self.on_index, id=STD_MENU_INDEX)
         self.Bind(wx.EVT_MENU, self.on_namespace, id=STD_MENU_NAMESPACE)
         self.Bind(wx.EVT_MENU, self.on_send_to_chat, id=STD_MENU_CHAT)
@@ -438,29 +497,30 @@
                 obj = self.GetPyData(item)
                 selections = dlg.get_selections()
                 if len(selections) == len(opts):
-                    self.chat.ParsePost(obj.tohtml(),True,True)
+                    open_rpg.get_component('chat').ParsePost(obj.tohtml(), True, True, ParserContext(obj))
                 else:
                     player_ids = []
                     for s in selections:
                         player_ids.append(players[s][2])
-                    self.chat.whisper_to_players(obj.tohtml(),player_ids)
+                    open_rpg.get_component('chat').whisper_to_players(obj.tohtml(), player_ids, ParserContext(obj))
 
     @debugging
     def on_export_html(self, evt):
-        f = wx.FileDialog(self,"Select a file", self.last_save_dir,"","HTML (*.html)|*.html",wx.SAVE)
-        if f.ShowModal() == wx.ID_OK:
+        dlg = wx.FileDialog(self,"Select a file", self.last_save_dir,"","HTML (*.html)|*.html",wx.SAVE)
+        if dlg.ShowModal() == wx.ID_OK:
             item = self.GetSelection()
             obj = self.GetPyData(item)
-            type = f.GetFilterIndex()
-            with open(f.GetPath(),"w") as f:
+            type = dlg.GetFilterIndex()
+            with open(dlg.GetPath(),"w") as f:
                 data = "<html><head><title>"+obj.xml.get("name")+"</title></head>"
-                data += "<body bgcolor=\"#FFFFFF\" >"+obj.tohtml()+"</body></html>"
-                for tag in ("</tr>","</td>","</th>","</table>","</html>","</body>"):
+                html_str = obj.tohtml().replace("\n", "<br />")
+                data += "<body bgcolor=\"#FFFFFF\" >"+html_str+"</body></html>"
+                for tag in ("</tr>","</td>","</th>","</table>","</html>","</body>","<br />"):
                     data = data.replace(tag,tag+"\n")
                 f.write(data)
 
-            self.last_save_dir, throwaway = os.path.split(f.GetPath())
-        f.Destroy()
+            self.last_save_dir, throwaway = os.path.split(dlg.GetPath())
+        dlg.Destroy()
         os.chdir(self.root_dir)
 
     @debugging
@@ -524,24 +584,79 @@
         dlg.Destroy()
 
     @debugging
-    def on_clone(self, evt):
+    def on_rename(self, evt):
+        item = self.GetSelection()
+        dlg = wx.TextEntryDialog(self, "New name:", "Renaming tree item.")
+        if dlg.ShowModal() == wx.ID_OK:
+            new_name = dlg.GetValue()
+            if new_name:
+                self.SetItemText(item, dlg.GetValue())
+            else:
+                open_rpg.get_component('chat').InfoPost("New name cannot be blank.")
+
+    @debugging
+    def on_clone(self, evt, replace=False):
         item = self.GetSelection()
         obj = self.GetPyData(item)
         if obj.can_clone():
             parent_node = self.GetItemParent(item)
-            prev_sib = self.GetPrevSibling(item)
-            if not prev_sib.IsOk():
-                prev_sib = parent_node
-            clone_xml = ET.XML(ET.tostring(obj.xml))
+            clone_text_list = [ET.tostring(obj.xml)]
+            if replace:
+                clone_text_list = self.replace_for_clone(clone_text_list[0], self.GetItemText(item))
             if parent_node == self.root:
                 parent_xml = self.GetPyData(parent_node)
             else:
                 parent_xml = self.GetPyData(parent_node).xml
-            for i in range(len(parent_xml)):
-                if parent_xml[i] is obj.xml:
-                    parent_xml.insert(i, clone_xml)
-                    break
-            self.load_xml(clone_xml, parent_node, prev_sib)
+            for clone_text in clone_text_list:
+                clone_xml = ET.XML(clone_text)
+                # avoid the new item duplicating an existing name
+                base_name = clone_xml.get("name")
+                name_lc = base_name.lower()
+                parts = base_name.rsplit("_", 1)
+                if len(parts) == 2 and parts[1].isdigit():
+                    base_name = parts[0]
+                base_name += "_"
+                base_name_lc = base_name.lower()
+                pos = 0
+                existing_suffixes = []
+                name_already_exists = False
+                for i in range(len(parent_xml)):
+                    # optimise for many copies being added?
+                    sibling_name_lc = parent_xml[i].get("name", "").lower()
+                    if sibling_name_lc == name_lc:
+                        name_already_exists = True
+                    if sibling_name_lc.startswith(base_name_lc) and sibling_name_lc[len(base_name_lc):].isdigit():
+                        existing_suffixes.append(int(sibling_name_lc[len(base_name_lc):]))
+                    if parent_xml[i] is obj.xml:
+                        pos = i
+                if name_already_exists:
+                    suffix = 2
+                    while suffix in existing_suffixes:
+                        suffix += 1
+                    clone_xml.set("name", base_name+str(suffix))
+                parent_xml.insert(pos+1, clone_xml) #copy placed below
+                self.load_xml(clone_xml, parent_node, item)
+            
+    @debugging
+    def on_clone_and_replace(self, evt):
+        self.on_clone(evt, True)
+
+    @debugging
+    def replace_for_clone(self, xml_string, findValue=''):
+        replaced_values = []
+        dlg = MultiReplacer(None, -1, "Clone with Replace", findValue)
+        result = dlg.ShowModal()
+        dlg.Destroy()
+        if result == wx.ID_OK:
+            find = dlg.find.GetValue()
+            multi_replace = dlg.replace.GetValue()
+            if find and multi_replace:
+                for replace in multi_replace.split("\n"):
+                    # add in reverse order here so when added beneath original item, will be right order
+                    replaced_values.insert(0, xml_string.replace(find, replace))# make it case insensitive?
+            else:
+                open_rpg.get_component('chat').InfoPost("Clone with Replace requires values for both find and replace.")
+        return replaced_values
 
     @debugging
     def on_save(self, evt):
@@ -668,13 +783,13 @@
             msg.ShowModal()
             msg.Destroy()
 
-    @debugging
-    def on_about(self, evt):
-        item = self.GetSelection()
-        obj = self.GetPyData(item)
-        about = MyAboutBox(self,obj.about())
-        about.ShowModal()
-        about.Destroy()
+##    @debugging
+##    def on_about(self, evt):
+##        item = self.GetSelection()
+##        obj = self.GetPyData(item)
+##        about = MyAboutBox(self,obj.about())
+##        about.ShowModal()
+##        about.Destroy()
 
     @debugging
     def on_send_to_map(self, evt):
@@ -848,42 +963,46 @@
                     self.drag_obj = None
 
     @debugging
-    def AddBonus(self, path, handler):
-        if path in self.bonuses:
-            bonus_list = self.bonuses[path]
-            #bonus_list.remove(handler) # make sure we don't duplicate
-            bonus_list.append(handler)
-        else:
-            self.bonuses[path]=[handler] # create list of one item
-
-    @debugging
-    def RemoveBonus(self, path, handler):
-        if path in self.bonuses:
-            bonus_list = self.bonuses[path]
-            bonus_list.remove(handler)
-
-    @debugging
-    def GetPyDataByPath(self, path):
+    def get_handler_by_path(self, path, context):
         path = path.lower().strip()
-        if path in self.pathmap:
-            return self.pathmap[path]
-        if path in self.namemap:
-            handler_list = self.namemap[path]
-            if len(handler_list)==1:
-                return handler_list[0]
-        path_list = path.split("::")
-        if len(path_list) == 2 and path_list[0] in self.namespacemap:
-            namespace = self.namespacemap[path_list[0]]
-            if path_list[1] in namespace:
-                return namespace[path_list[1]]
+        namespace = None
+        # first, use the context to figure out the namespace
+        if isinstance(context.namespace_hint, core.node_handler):
+            namespace = self.get_namespace_by_name(self.get_namespace_name(context.namespace_hint.mytree_node))
+        if isinstance(context.namespace_hint, (str, unicode)):
+            namespace = self.get_namespace_by_name(context.namespace_hint.lower().strip())
+        if namespace:
+            handler = namespace.get_handler(path)
+            if handler is not None:
+                return handler
+        # failing with that, check the global path
+        handler = self.root_namespace.get_handler(path)
+        if handler is not None:
+            return handler
+        # finally check for the namespace_name::namespace_path format
+        path_bits = path.split("::")
+        if len(path_bits) >= 2:
+            namespace = self.get_namespace_by_name(path_bits[0])
+            if namespace:
+                handler = namespace.get_handler("::".join(path_bits[1:]))
+                if handler is not None:
+                    return handler
         return None
 
     @debugging
-    def ValidateMessage(self, message):
-        print message
+    def get_namespace_by_name(self, name):
+        if name and name in self.namespacemap:
+            return self.namespacemap[name]
+        return None
 
     @debugging
-    def SetReferenceable(self, item, value):
+    def validation_message(self, message):
+        print message
+        if self.infopost_validation_message:
+            open_rpg.get_component('chat').InfoPost(message)
+
+    @debugging
+    def set_referenceable(self, item, value):
         if value:
             self.SetItemTextColour(item, wx.BLACK)
         else:
@@ -893,11 +1012,11 @@
             handler.set_referenceable(value)
 
     @debugging
-    def IsReferenceable(self, item):
+    def is_referenceable(self, item):
         return self.GetItemTextColour(item) == wx.BLACK;
 
     @debugging
-    def SetNamespace(self, item, value):
+    def set_namespace(self, item, value):
         if value:
             self.SetItemBold(item, True)
         else:
@@ -907,36 +1026,37 @@
             handler.set_namespace(value)
 
     @debugging
-    def IsNamespace(self, item):
+    def is_namespace(self, item):
         return self.IsBold(item);
 
     @debugging
-    def ValidateName(self, name):
+    def validate_name(self, name):
         # can't include !@ @@ @! or ::
         return compiled_illegal_node_name_regex.search(name) is None
 
     @debugging
-    def ValidateNodePath(self, item):
-        while item.IsOk() and item != self.root:
-            if not self.ValidateName(self.GetItemText(item)):
-                return False
-            item = self.GetItemParent(item)
-        return True
+    def get_path_data(self, item):
+        namespace_name = None
+        namespace_path = None
+        path = self.GetItemText(item).lower().strip()
+        valid_path = self.validate_name(path)
+        parent = self.GetItemParent(item)
+        while parent.IsOk() and parent != self.root:
+            name = self.GetItemText(parent).lower().strip()
+            if not self.validate_name(name):
+                valid_path = False
+            if self.is_namespace(parent):
+                namespace_name = name
+                namespace_path = path
+            path = name+'::'+path
+            parent = self.GetItemParent(parent)
+        return path, valid_path, namespace_name, namespace_path
 
     @debugging
-    def GetItemPath(self, item):
-        path = self.GetItemText(item).lower().strip()
+    def get_namespace_name(self, item):
         parent = self.GetItemParent(item)
         while parent.IsOk() and parent != self.root:
-            path = self.GetItemText(parent).lower().strip()+'::'+path
-            parent = self.GetItemParent(parent)
-        return path
-
-    @debugging
-    def GetItemNamespace(self, item):
-        parent = self.GetItemParent(item)
-        while parent.IsOk() and parent != self.root:
-            if self.IsNamespace(parent):
+            if self.is_namespace(parent):
                 return self.GetItemText(parent).lower().strip()
             parent = self.GetItemParent(parent)
         return None
@@ -952,128 +1072,138 @@
 
     @debugging
     def on_index(self, evt):
+        self.infopost_validation_message = True
         item = self.GetSelection()
-        if self.IsReferenceable(item):
-            self.RemoveReferencedPathname(item)
-            self.SetReferenceable(item, False)
+        if self.is_referenceable(item):
+            self.on_delete_path(item)
+            self.set_referenceable(item, False)
         else:
-            self.OnNewReferencedPathname(item)
+            self.on_new_path(item)
+        self.infopost_validation_message = False
 
     @debugging
     def on_namespace(self, evt):
+        self.infopost_validation_message = True
         item = self.GetSelection()
-        if self.IsNamespace(item):
-            self.RemoveNamespace(item)
+        if self.is_namespace(item):
+            self.remove_namespace(item)
+            self.set_namespace(item, False)
         else:
-            self.OnNewNamespace(item)
+            self.on_new_namespace(item)
+        self.infopost_validation_message = False
 
     @debugging
-    def OnNewReferencedPathname(self, item):
+    def on_new_path(self, item):
         name = self.GetItemText(item).lower().strip()
         handler = self.GetPyData(item)
-        pathname = self.GetItemPath(item)
-        if not self.ValidateNodePath(item):
-            self.SetReferenceable(item, False)
-            self.ValidateMessage("illegal node name: "+pathname+".  Don't use !@ @@ @! or :: in node names.")
+        path, valid_path, namespace_name, namespace_path = self.get_path_data(item)
+        if not valid_path:
+            self.set_referenceable(item, False)
+            self.validation_message("Illegal node name: '"+path+"'.  Don't use !@ @@ @! or :: in node names.")
             return
-        if pathname in self.pathmap and self.pathmap[pathname]!=handler:
-            self.SetReferenceable(item, False)
-            self.ValidateMessage('duplicate pathname: '+pathname+'.  Only first node is referenceable.')
+        if self.root_namespace.path_is_duplicate(path, handler):
+            self.set_referenceable(item, False)
+            self.validation_message("Duplicate path: '"+path+"'.  Only first node is referenceable.")
             return
-    #      if not self.ItemHasChildren(item):
-        # update namespace map
-        namespacename = self.GetItemNamespace(item)
-        if namespacename is not None:
-            namespace = self.namespacemap[namespacename]
-            if name in namespace and namespace[name]!=handler:
-                self.SetReferenceable(item, False)
-                self.ValidateMessage('duplicate node name: '+name+', within namespace: '+namespacename+'.  Only first node is referenceable.')
+        namespace = self.get_namespace_by_name(namespace_name)
+        if namespace:
+            if namespace.path_is_duplicate(path, handler):
+                self.set_referenceable(item, False)
+                self.validation_message("Duplicate path: '"+namespace_path+"', within namespace: '"+namespace_name+"'.  Only first node is referenceable.")
                 return
-            else:
-                namespace[name]=handler
+            namespace.add_path(namespace_path, handler)
+        # add path only after checking namepace path uniqueness
+        self.set_referenceable(item, True)
+        self.root_namespace.add_path(path, handler)
+            
         if not self.ItemHasChildren(item):
-            # update leaf name map
-            if name in self.namemap:
-                handler_list = self.namemap[name]
-                #handler_list.remove(handler) # make sure we don't duplicate
-                handler_list.append(handler)
-            else:
-                self.namemap[name]=[handler] # create list of one item
-        # add to pathmap only after checking namepace name uniqueness
-        self.SetReferenceable(item, True)
-        self.pathmap[pathname]=handler
-
-    @debugging
-    def OnNewNamespace(self, item):
-        name = self.GetItemText(item).lower().strip()
-        if name in self.namespacemap:
-            self.SetNamespace(item, False)
-            self.ValidateMessage('duplicate namespace: '+name)
-        else:
-            namespace = {}
-            self.SetNamespace(item, True)
-            self.namespacemap[name]=namespace
-            self.traverse(item, self.OnNewNamespace_traverse, namespace)
+            self.root_namespace.add_leaf(name, handler)
+            if namespace:
+                namespace.add_leaf(name, handler)
 
     @debugging
-    def OnNewNamespace_traverse(self, item, namespace):
-        # this item just became part of the new namepace
-        # should we re-check its validity?
-        if self.IsReferenceable(item):# and not self.ItemHasChildren(item):
-            name = self.GetItemText(item).lower().strip()
-            if name in namespace:
-                self.SetReferenceable(item, False)
-                self.ValidateMessage('duplicate node name: '+name+', within new namespace: '+self.GetItemNamespace(item)+'.  Only first node is referenceable.')
-                return
-            else:
-                handler = self.GetPyData(item)
-                namespace[name]=handler
+    def on_new_namespace(self, item):
+        name = self.GetItemText(item).lower().strip()
+        # check for nested namspace
+        parent_namespace_name = self.get_namespace_name(item)
+        if parent_namespace_name is not None:
+            self.validation_message("Cannot nest namespaces ('"+name+"' inside namespace '"+parent_namespace_name+"')")
+            return
+        errors = []
+        self.traverse(item, self.check_no_namespaces_traverse, errors)
+        if errors:
+            list_of_names = ", ".join(errors)
+            self.validation_message("Cannot nest namespaces (container '"+name+"' has child namespaces: "+list_of_names+")")
+            return
+        if name in self.namespacemap:
+            self.set_namespace(item, False)
+            self.validation_message("Duplicate namespace: '"+name+"'")
+            return
+        namespace = Namespace(name) 
+        self.set_namespace(item, True)
+        self.namespacemap[name]=namespace
+        self.traverse(item, self.on_new_namespace_traverse, namespace)
 
     @debugging
-    def RemoveReferencedPathname(self, item):
-        name = self.GetItemText(item).lower().strip()
-        pathname = self.GetItemPath(item)
-        if pathname in self.pathmap:
-            del self.pathmap[pathname] # should we check the handler is identical before deleting it?
-        if not self.ItemHasChildren(item):
-            namespacename = self.GetItemNamespace(item)
-            if namespacename:
-                namespace = self.namespacemap[namespacename]
-                if namespace:
-                    if name in namespace:
-                        del namespace[name]
-            if name in self.namemap:
-                handler_list = self.namemap[name]
-                handler = self.GetPyData(item)
-                handler_list.remove(handler)
+    def check_no_namespaces_traverse(self, item, errors):
+        if self.is_namespace(item):
+            errors.append(self.GetItemText(item))
+        
+    @debugging
+    def on_new_namespace_traverse(self, item, namespace):
+        # this item just became part of the new namepace
+        # should we re-check its validity?
+        if self.is_referenceable(item):
+            name = self.GetItemText(item).lower().strip()
+            handler = self.GetPyData(item)
+            path, valid_path, namespace_name, namespace_path = self.get_path_data(item)
+            if namespace.path_is_duplicate(namespace_path, handler):
+                self.set_referenceable(item, False)
+                self.validation_message("Duplicate node path: '"+namespace_path+"', within new namespace: '"+namespace_name+"'.  Only first node is referenceable.")
+                return
+            namespace.add_path(namespace_path, handler)
+            if not self.ItemHasChildren(item):
+                namespace.add_leaf(name, handler)
 
     @debugging
-    def RemoveNamespace(self, item):
+    def on_delete_path(self, item):
+        name = self.GetItemText(item).lower().strip()
+        handler = self.GetPyData(item)
+        path, valid_path, namespace_name, namespace_path = self.get_path_data(item)
+        self.root_namespace.delete_path(path, handler)
+        namespace = self.get_namespace_by_name(namespace_name)
+        if namespace:
+            namespace.delete_path(namespace_path, handler)
+        if not self.ItemHasChildren(item):
+            self.root_namespace.delete_leaf(name, handler)
+            if namespace:
+                namespace.delete_leaf(name, handler)
+            
+    @debugging
+    def remove_namespace(self, item):
         name = self.GetItemText(item).lower().strip()
         del self.namespacemap[name]
-        self.SetNamespace(item, False)
 
     @debugging
-    def OnNewItem(self, item):
-        if self.IsReferenceable(item):
-            self.OnNewReferencedPathname(item)
-        if self.IsNamespace(item):
-            self.OnNewNamespace(item)
+    def on_new_item(self, item, data=None):
+        # the data is to make this function callable by traverse()
+        if self.is_referenceable(item):
+            self.on_new_path(item)
+        if self.is_namespace(item):
+            self.on_new_namespace(item)
 
     @debugging
-    def OnDeleteItem(self, item, data=None):
+    def on_delete_item(self, item, data=None):
         # the data is to make this function callable by traverse()
-        if self.IsReferenceable(item):
-            self.RemoveReferencedPathname(item)
-        if self.IsNamespace(item):
-            self.RemoveNamespace(item)
+        if self.is_referenceable(item):
+            self.on_delete_path(item)
+        if self.is_namespace(item):
+            self.remove_namespace(item)
 
     @debugging
-    def InitializeIndexes(self):
-        self.pathmap = {}
-        self.namemap = {}
+    def initialize_indexes(self):
+        self.root_namespace = Namespace("")
         self.namespacemap = {}
-        self.bonuses = {}
 
     @debugging
     def AppendItem(self, parent, text, image=-1, selectedImage=-1, data=None):
@@ -1081,7 +1211,7 @@
         if data:
             self.SetPyData(newitem, data)
         else:
-            self.OnNewItem(newitem)
+            self.on_new_item(newitem)
         return newitem
 
     @debugging
@@ -1090,7 +1220,7 @@
         if data:
             self.SetPyData(newitem, data)
         else:
-            self.OnNewItem(newitem)
+            self.on_new_item(newitem)
         return newitem
 
     @debugging
@@ -1099,46 +1229,55 @@
         if data:
             self.SetPyData(newitem, data)
         else:
-            self.OnNewItem(newitem)
+            self.on_new_item(newitem)
         return newitem
 
     @debugging
     def SetPyData(self, item, handler):
         wx.TreeCtrl.SetPyData(self, item, handler)
         if item != self.GetRootItem():
-            self.SetReferenceable(item, handler.get_referenceable())
-            self.SetNamespace(item, handler.get_namespace())
-            self.OnNewItem(item)
+            self.set_referenceable(item, handler.get_referenceable())
+            self.set_namespace(item, handler.get_namespace())
+            self.on_new_item(item)
 
     @debugging
     def Delete(self, item):
-        self.traverse(item, self.OnDeleteItem)
-        self.OnDeleteItem(item)
+        self.on_delete_subtree(item)
         wx.TreeCtrl.Delete(self, item)
 
     @debugging
     def DeleteChildren(self, item):
-        self.traverse(item, self.OnDeleteItem)
+        self.traverse(item, self.on_delete_item)
         wx.TreeCtrl.DeleteChildren(self, item)
 
     @debugging
     def CollapseAndReset(self, item):
         if item is self.GetRootItem():
-            self.InitializeIndexes()
+            self.initialize_indexes()
         else:
-            self.traverse(item, self.OnDeleteItem)
+            self.traverse(item, self.on_delete_item)
         wx.TreeCtrl.CollapseAndReset(self, item)
 
     @debugging
     def DeleteAllItems(self):
-        self.InitializeIndexes()
+        self.initialize_indexes()
         wx.TreeCtrl.DeleteAllItems(self)
 
     @debugging
     def SetItemText(self, item, name):
-        self.OnDeleteItem(item)
+        self.on_delete_subtree(item)
         wx.TreeCtrl.SetItemText(self, item, name)
-        self.OnNewItem(item)
+        self.on_new_subtree(item)
+
+    @debugging
+    def on_delete_subtree(self, item):
+        self.traverse(item, self.on_delete_item)
+        self.on_delete_item(item)
+
+    @debugging
+    def on_new_subtree(self, item):
+        self.traverse(item, self.on_new_item)
+        self.on_new_item(item)
 
     @debugging
     def on_label_change(self, evt):
@@ -1149,8 +1288,8 @@
         if txt != "":
             handler = self.GetPyData(item)
             handler.xml.set('name',txt)
-            self.OnDeleteItem(item)
-            wx.CallAfter(self.OnNewItem, item)
+            self.on_delete_subtree(item)
+            wx.CallAfter(self.on_new_subtree, item)
         else:
             evt.Veto()
 
--- a/orpg/gametree/nodehandlers/chatmacro.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/chatmacro.py	Mon Mar 22 18:38:22 2010 -0600
@@ -46,15 +46,7 @@
         self.xml.find('text').text = txt
 
     def on_use(self,evt):
-        txt = self.get_value()
-        actionlist = txt.split("\n")
-        for line in actionlist:
-            if(line != ""):
-                if line[0] != "/": ## it's not a slash command
-                    action = self.chat.ParsePost(line, True, True)
-                else:
-                    action = line
-                    self.chat.chat_cmds.docmd(action)
+        self.chat.ParsePost(self.get_value(), True, True, ParserContext(self))
         return 1
 
     def get_design_panel(self,parent):
@@ -63,7 +55,6 @@
     def tohtml(self):
         title = self.xml.get("name")
         txt = self.get_value()
-        txt = string.replace(txt,'\n',"<br />")
         return "<P><b>"+title+":</b><br />"+txt
 
     def get_value(self):
@@ -92,7 +83,7 @@
         sizer.Add(wx.StaticText(self, -1, "Text Body:"), 0, wx.EXPAND)
         sizer.Add(self.text[P_BODY], 1, wx.EXPAND)
         sizer.Add(wx.Button(self, B_CHAT, "Send To Chat"),0,wx.EXPAND)
-        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+        self.text[P_TITLE].Bind(wx.EVT_KILL_FOCUS, self.on_text, id=P_TITLE)
         self.Bind(wx.EVT_TEXT, self.on_text, id=P_BODY)
         self.Bind(wx.EVT_BUTTON, self.handler.on_use, id=B_CHAT)
         self.SetSizer(sizer)
@@ -103,9 +94,6 @@
         id = evt.GetId()
         txt = self.text[id].GetValue()
         if id == P_TITLE:
-            if txt == "":
-                return
-            self.handler.xml.set('name',txt)
             self.handler.rename(txt)
         elif id == P_BODY:
             self.handler.set_text(txt)
--- a/orpg/gametree/nodehandlers/containers.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/containers.py	Mon Mar 22 18:38:22 2010 -0600
@@ -117,7 +117,7 @@
                 self.atts = child_xml
             else:
                 self.tree.load_xml(child_xml,self.mytree_node)
-        if not self.atts:
+        if self.atts is None:
             self.atts = ET.Element('group_atts')
             self.atts.set("cols","1")
             self.atts.set("border","1")
@@ -164,10 +164,9 @@
         wx.Panel.__init__(self, parent, -1)
         self.handler = handler
         sizer = wx.BoxSizer(wx.VERTICAL)
-        self.text = {   P_TITLE : wx.TextCtrl(self, P_TITLE, handler.xml.get('name'))
-                      }
+        self.text = wx.TextCtrl(self, P_TITLE, handler.xml.get('name'))
         sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
-        sizer.Add(self.text[P_TITLE], 0, wx.EXPAND)
+        sizer.Add(self.text, 0, wx.EXPAND)
         sizer.Add(wx.Size(10,10))
 
         radio_c = wx.RadioBox(self, GROUP_COLS, "Columns", choices=["1","2","3","4"])
@@ -190,7 +189,7 @@
         self.SetAutoLayout(True)
         self.Fit()
         parent.SetSize(self.GetBestSize())
-        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+        self.text.Bind(wx.EVT_KILL_FOCUS, self.on_text)
         self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=GROUP_BOR)
         self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=GROUP_COLS)
 
@@ -203,12 +202,7 @@
             self.handler.atts.set("border",str(index))
 
     def on_text(self,evt):
-        id = evt.GetId()
-        if id == P_TITLE:
-            txt = self.text[id].GetValue()
-            if txt != "":
-                self.handler.xml.set('name',txt)
-                self.handler.rename(txt)
+        self.handler.rename(self.text.GetValue())
 
 
 
@@ -266,7 +260,7 @@
                 self.atts = child_xml
             else:
                 self.tree.load_xml(child_xml,self.mytree_node)
-        if not self.atts:
+        if self.atts is None:
             self.atts = ET.Element('splitter_atts')
             self.atts.set("horizontal","0")
             self.xml.append(self.atts)
--- a/orpg/gametree/nodehandlers/core.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/core.py	Mon Mar 22 18:38:22 2010 -0600
@@ -49,9 +49,10 @@
 def getText(elem):
     if elem.text is None:
         return ''
-    return elem.text
+    return str(elem.text)
  
-
+def is_integer(string):
+    return string is not None and (string.isdigit() or (string != '' and string[0] == '-' and string[1:].isdigit()))
 
 #html defaults
 TH_BG = "#E9E9E9"
@@ -231,7 +232,7 @@
         return 1;
 
     def on_del(self,evt):
-        print "on del"
+        pass
 
     def on_new_data(self,xml):
         pass
@@ -243,7 +244,7 @@
         pass
 
     def on_send_to_chat(self,evt):
-        self.chat.ParsePost(self.tohtml(),True,True)
+        self.chat.ParsePost(self.tohtml(), True, True, ParserContext(self))
 
     def on_drop(self,evt):
         drag_obj = self.tree.drag_obj
@@ -270,6 +271,8 @@
         return ET.tostring(self.xml) #toxml(self.master_dom,pretty)
 
     def tohtml(self):
+        """ returns the value of the node plus perhaps some mark-up, such as the title of the node in bold
+        this function is used by send-to-chat, whisper, as well as pretty print and export as html """
         return self.xml.get("name")
 
     def delete(self):
@@ -307,7 +310,8 @@
         return None
 
     def get_html_panel(self,parent):
-        html_str = "<html><body bgcolor=\"#FFFFFF\" >"+self.tohtml()+"</body></html>"
+        html_str = self.tohtml().replace("\n", "<br />")
+        html_str = "<html><body bgcolor=\"#FFFFFF\" >"+html_str+"</body></html>"
         wnd = wx.html.HtmlWindow(parent,-1)
         html_str = self.chat.ParseDice(html_str)
         wnd.SetPage(html_str)
@@ -344,6 +348,8 @@
         return False
 
     def get_value(self):
+        """ returns the integer value of the node (as a string) if it has one, or None
+        this function is mostly used by the reference system and the bonus system """
         return None
         
 
@@ -480,7 +486,6 @@
 
     def on_design(self,evt):
         tlist = ['Title','URL']
-        print "design filename",self.xml.get('name')
         vlist = [self.xml.get("name"),
                  self.file_node.get("url")]
         dlg = orpgMultiTextEntry(self.tree.GetParent(),tlist,vlist,"File Loader Edit")
--- a/orpg/gametree/nodehandlers/d20.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/d20.py	Mon Mar 22 18:38:22 2010 -0600
@@ -574,7 +574,7 @@
         else:
             level = self.spells[ name ].get( 'level' )
             descr = self.spells[ name ].get( 'desc' )
-            use = self.spells[ name ].get( 'used' )
+            use = self.spells[ name ].get( 'used', '' )
             memrz = self.spells[ name ].get( 'memrz' )
             cname = self.char_hander.get_char_name()
             use += '+1'
@@ -641,7 +641,7 @@
         else:
             level = self.spells[ name ].get( 'level' )
             descr = self.spells[ name ].get( 'desc' )
-            use = self.spells[ name ].get( 'used' )
+            use = self.spells[ name ].get( 'used', '' )
             memrz = self.spells[ name ].get( 'memrz' )
             cname = self.char_hander.get_char_name()
             use += '+1'
@@ -709,7 +709,7 @@
         else:
             level = self.powers[ name ].get( 'level' )
             descr = self.powers[ name ].get( 'desc' )
-            use = self.powers[ name ].get( 'used' )
+            use = self.powers[ name ].get( 'used', '' )
             points = self.powers[ name ].get( 'point' )
             cpp = self.char_hander.get_char_pp('current1')
             fre = self.char_hander.get_char_pp('free')
--- a/orpg/gametree/nodehandlers/dnd35.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/dnd35.py	Mon Mar 22 18:38:22 2010 -0600
@@ -1,5 +1,4 @@
 import orpg.tools.orpg_settings
-import orpg.minidom
 from core import *
 from containers import *
 from string import *  #a 1.6003
--- a/orpg/gametree/nodehandlers/dnd3e.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/dnd3e.py	Mon Mar 22 18:38:22 2010 -0600
@@ -144,7 +144,6 @@
 # o- open
 #
 import orpg.tools.orpg_settings
-import orpg.minidom
 from core import *
 from containers import *
 from string import *  #a 1.6003
@@ -2604,7 +2603,7 @@
         else:
             level = self.spells[ name ].get( 'level' )
             descr = self.spells[ name ].get( 'desc' )
-            use = self.spells[ name ].get( 'used' )
+            use = self.spells[ name ].get( 'used', '' )
             memrz = self.spells[ name ].get( 'memrz' )
             use += '+1'
             charNameL=self.char_handler.child_handlers['general'].charName #a  1.5002
@@ -2799,7 +2798,7 @@
         else:
             level = self.spells[ name ].get( 'level' )
             descr = self.spells[ name ].get( 'desc' )
-            use = self.spells[ name ].get( 'used' )
+            use = self.spells[ name ].get( 'used', '' )
             memrz = self.spells[ name ].get( 'memrz' )
             use += '+1'
             left = eval( '%s - ( %s )' % ( memrz, use ) )
@@ -3002,7 +3001,7 @@
             level = int(self.powers[ name ].get( 'level' ))
             descr = self.powers[ name ].get( 'desc' )
             #use can be removed -mgt
-            #use = self.powers[ name ].get( 'used' )
+            #use = self.powers[ name ].get( 'used', '' )
             points = self.powers[ name ].get( 'point' )
             #cpp and fre are strings without the eval -mgt
             cpp = eval(self.char_handler.child_handlers['snp'].child_handlers['pp'].get_char_pp('current1'))          #a 1.5002
--- a/orpg/gametree/nodehandlers/forms.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/forms.py	Mon Mar 22 18:38:22 2010 -0600
@@ -63,7 +63,7 @@
                 self.atts = child_xml
             else:
                 self.tree.load_xml(child_xml,self.mytree_node)
-        if not self.atts:
+        if self.atts is None:
             self.atts = ET.Element('form')
             self.atts.set("width","400")
             self.atts.set("height","600")
@@ -152,7 +152,7 @@
         self.Fit()
         parent.SetSize(self.GetBestSize())
 
-        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+        self.text[P_TITLE].Bind(wx.EVT_KILL_FOCUS, self.on_text, id=P_TITLE)
         self.Bind(wx.EVT_TEXT, self.on_text, id=F_HEIGHT)
         self.Bind(wx.EVT_TEXT, self.on_text, id=F_WIDTH)
 
@@ -161,7 +161,6 @@
         txt = self.text[id].GetValue()
         if not len(txt): return
         if id == P_TITLE:
-            self.handler.xml.set('name',txt)
             self.handler.rename(txt)
         elif id == F_HEIGHT or id == F_WIDTH:
             try:
@@ -202,7 +201,7 @@
     #
 class textctrl_handler(node_handler):
     """ <nodehandler class="textctrl_handler" module="form" name="">
-           <text multiline='0' send_button='0' raw_mode='0' hide_title='0'>Text In Node</text>
+           <text multiline='0' send_button='0' hide_title='0'>Text In Node</text>
         </nodehandler>
     """
     def __init__(self,xml,tree_node):
@@ -210,8 +209,6 @@
         self.text_elem = self.xml.find('text')
         if self.text_elem.get("send_button") == "":
             self.text_elem.set("send_button","0")
-        if self.text_elem.get("raw_mode") == "":
-            self.text_elem.set("raw_mode","0")
         if self.text_elem.get("hide_title") == "":
             self.text_elem.set("hide_title","0")
 
@@ -227,9 +224,6 @@
     def is_multi_line(self):
         return int(self.text_elem.get("multiline",0))
 
-    def is_raw_send(self):
-        return int(self.text_elem.get("raw_mode",0))
-
     def is_hide_title(self):
         return int(self.text_elem.get("hide_title",0))
 
@@ -238,7 +232,6 @@
 
     def tohtml(self):
         txt = self.get_value()
-        txt = string.replace(txt,'\n',"<br />")
         if not self.is_hide_title():
             txt = "<b>"+self.xml.get("name")+":</b> "+txt
         return txt
@@ -247,7 +240,7 @@
         return getText(self.text_elem)
 
     def set_value(self, new_value):
-        self.text_elem.text = new_value
+        self.text_elem.text = str(new_value)
         
 
 
@@ -269,6 +262,10 @@
             sizer = wx.BoxSizer(wx.HORIZONTAL)
 
         txt = handler.get_value()
+        if self.handler.chat.contains_reference(txt):
+            txt = self.handler.chat.parse_multiline(txt, ParserContext(self.handler), False)
+            text_style |= wx.TE_READONLY
+        
         self.text = wx.TextCtrl(self, FORM_TEXT_CTRL, txt, style=text_style)
         sizer.Add(wx.StaticText(self, -1, handler.xml.get('name')+": "), 0, sizer_style)
         sizer.Add(wx.Size(5,0))
@@ -291,24 +288,11 @@
         self.handler.text_elem.text = txt
 
     def on_send(self,evt):
-        txt = self.text.GetValue()
-        if not self.handler.is_raw_send():
-            #self.chat.ParsePost(self.tohtml(),True,True)
-            self.chat.ParsePost(self.handler.tohtml(),True,True)
-            return 1
-        actionlist = txt.split("\n")
-        for line in actionlist:
-            if(line != ""):
-                if line[0] != "/": ## it's not a slash command
-                    self.chat.ParsePost(line,True,True)
-                else:
-                    action = line
-                    self.chat.chat_cmds.docmd(action)
+        self.chat.ParsePost(self.handler.tohtml(), True, True, ParserContext(self.handler))
         return 1
 
 F_MULTI = wx.NewId()
 F_SEND_BUTTON = wx.NewId()
-F_RAW_SEND = wx.NewId()
 F_HIDE_TITLE = wx.NewId()
 F_TEXT = wx.NewId()
 
@@ -321,8 +305,6 @@
         self.title = wx.TextCtrl(self, P_TITLE, handler.xml.get('name'))
         self.multi = wx.CheckBox(self, F_MULTI, " Multi-Line")
         self.multi.SetValue(handler.is_multi_line())
-        self.raw_send = wx.CheckBox(self, F_RAW_SEND, " Send as Macro")
-        self.raw_send.SetValue(handler.is_raw_send())
         self.hide_title = wx.CheckBox(self, F_HIDE_TITLE, " Hide Title")
         self.hide_title.SetValue(handler.is_hide_title())
         self.send_button = wx.CheckBox(self, F_SEND_BUTTON, " Send Button")
@@ -332,7 +314,6 @@
         sizer.Add(self.title, 0, wx.EXPAND)
         sizer.Add(wx.Size(10,10))
         sizer.Add(self.multi, 0, wx.EXPAND)
-        sizer.Add(self.raw_send, 0, wx.EXPAND)
         sizer.Add(self.hide_title, 0, wx.EXPAND)
         sizer.Add(self.send_button, 0 , wx.EXPAND)
         sizer.Add(wx.Size(10,10))
@@ -350,20 +331,16 @@
         self.SetSizer(sizer)
         self.SetAutoLayout(True)
 
-        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+        self.title.Bind(wx.EVT_KILL_FOCUS, self.on_text, id=P_TITLE)
         self.Bind(wx.EVT_TEXT, self.on_text, id=F_TEXT)
         self.Bind(wx.EVT_CHECKBOX, self.on_button, id=F_MULTI)
-        self.Bind(wx.EVT_CHECKBOX, self.on_raw_button, id=F_RAW_SEND)
         self.Bind(wx.EVT_CHECKBOX, self.on_hide_button, id=F_HIDE_TITLE)
         self.Bind(wx.EVT_CHECKBOX, self.on_send_button, id=F_SEND_BUTTON)
 
     def on_text(self,evt):
         id = evt.GetId()
         if id == P_TITLE:
-            txt = self.title.GetValue()
-            if not len(txt): return
-            self.handler.xml.set('name',txt)
-            self.handler.rename(txt)
+            self.handler.rename(self.title.GetValue())
         if id == F_TEXT:
             txt = self.text.GetValue()
             txt = strip_text(txt)
@@ -372,9 +349,6 @@
     def on_button(self,evt):
         self.handler.text_elem.set("multiline",str(bool2int(evt.Checked())))
 
-    def on_raw_button(self,evt):
-        self.handler.text_elem.set("raw_mode",str(bool2int(evt.Checked())))
-
     def on_hide_button(self,evt):
         self.handler.text_elem.set("hide_title",str(bool2int(evt.Checked())))
 
@@ -400,8 +374,8 @@
     """
     <nodehandler class="listbox_handler" module="forms" name="">
         <list type="1"  send_button='0' hide_title='0'>
-                <option value="" selected="" >Option Text I</option>
-                <option value="" selected="" >Option Text II</option>
+                <option caption="" selected="" >Option Text I</option>
+                <option caption="" selected="" >Option Text II</option>
         </list>
     </nodehandler>
     """
@@ -429,45 +403,14 @@
     def is_hide_title(self):
         return int(self.list.get("hide_title",0))
 
-    # single selection methods
-    def get_selected_node(self):
-        for opt in self.options:
-            if opt.get("selected") == "1": return opt
-        return None
-
-    def get_selected_index(self):
-        i = 0
-        for opt in self.options:
-            if opt.get("selected") == "1":
-                return i
-            i += 1
-        return 0
-
-    def get_selected_text(self):
-        node = self.get_selected_node()
-        if node:
-            return getText(node)
-        else:
-            return ""
-
-
-    # mult selection methods
-
-    def get_selections(self):
-        opts = []
-        for opt in self.options:
-            if opt.get("selected") == "1":
-                opts.append(opt)
-        return opts
-
-    def get_selections_text(self):
+    def get_selected_values(self):
         opts = []
         for opt in self.options:
             if opt.get("selected") == "1":
                 opts.append(getText(opt))
         return opts
 
-    def get_selections_index(self):
+    def get_selected_indexes(self):
         opts = []
         i = 0
         for opt in self.options:
@@ -489,29 +432,44 @@
 
     # misc methods
 
-    def get_options(self):
-        opts = []
+    def get_values(self):
+        values = []
+        for opt in self.options:
+            values.append(getText(opt))
+        return values
+
+    def get_captions(self):
+        captions = []
         for opt in self.options:
-            opts.append(getText(opt))
-        return opts
+            captions.append(opt.get("caption", ""))
+        return captions
+
+    def get_caption_or_if_none_the_value(self):
+        captions = self.get_captions()
+        values = self.get_values()
+        for index in range(len(captions)):
+            if captions[index]=="":
+                captions[index] = values[index]
+        return captions
 
     def get_option(self,index):
-        return getText(self.options[index])
+        return getText(self.options[index]), str(self.options[index].get("caption", ""))
 
-    def add_option(self,opt):
+    def add_option(self, index, value, caption):
         elem = ET.Element('option')
-        elem.set("value","0")
+        elem.set("caption",caption)
         elem.set("selected","0")
-        elem.text = opt
-        self.list.append(elem)
+        elem.text = value
+        self.list.insert(index, elem)
         self.options = self.list.findall('option')
 
     def remove_option(self,index):
         self.list.remove(self.options[index])
         self.options = self.list.findall('option')
 
-    def edit_option(self,index,value):
+    def edit_option(self,index,value,caption):
         self.options[index].text = value
+        self.options[index].set("caption",caption)
 
     def has_send_button(self):
         if self.list.get("send_button") == '0':
@@ -526,7 +484,7 @@
             return 1
 
     def tohtml(self):
-        opts = self.get_selections_text()
+        opts = self.get_selected_values()
         text = ""
         if not self.is_hide_title():
             text = "<b>"+self.xml.get("name")+":</b> "
@@ -535,7 +493,7 @@
         return text
 
     def get_value(self):
-        return "\n".join(self.get_selections_text())
+        return "\n".join(self.get_selected_values())
 
 F_LIST = wx.NewId()
 F_SEND = wx.NewId()
@@ -546,33 +504,30 @@
         wx.Panel.__init__(self, parent, -1)
         self.handler = handler
         self.chat = handler.chat
-        opts = handler.get_options()
-        cur_opt = handler.get_selected_text()
+        opts = handler.get_caption_or_if_none_the_value()
         type = handler.get_type()
         label = handler.xml.get('name')
 
         if type == L_DROP:
-            self.list = wx.ComboBox(self, F_LIST, cur_opt, choices=opts, style=wx.CB_READONLY)
+            self.list = wx.ComboBox(self, F_LIST, '', choices=opts, style=wx.CB_READONLY)
             if self.list.GetSize()[0] > 200:
                 self.list.Destroy()
-                self.list = wx.ComboBox(self, F_LIST, cur_opt, size=(200, -1), choices=opts, style=wx.CB_READONLY)
+                self.list = wx.ComboBox(self, F_LIST, '', size=(200, -1), choices=opts, style=wx.CB_READONLY)
         elif type == L_LIST:
             self.list = wx.ListBox(self,F_LIST,choices=opts)
         elif type == L_RADIO:
             self.list = wx.RadioBox(self,F_LIST,label,choices=opts,majorDimension=3)
         elif type == L_CHECK:
             self.list = wx.CheckListBox(self,F_LIST,choices=opts)
-            self.set_checks()
 
-        for i in handler.get_selections_text():
-            if type == L_DROP:
-                self.list.SetValue( i )
+        for i in handler.get_selected_indexes():
+            if type == L_CHECK:
+                self.list.Check(i)
             else:
-                self.list.SetStringSelection( i )
-
+                self.list.SetSelection(i)
+            
         if type == L_DROP:
             sizer = wx.BoxSizer(wx.HORIZONTAL)
-
         else:
             sizer = wx.BoxSizer(wx.VERTICAL)
 
@@ -597,46 +552,54 @@
             self.Bind(wx.EVT_COMBOBOX, self.on_change, id=F_LIST)
         elif type == L_LIST:
             self.Bind(wx.EVT_LISTBOX, self.on_change, id=F_LIST)
+            self.Bind(wx.EVT_LISTBOX_DCLICK, self.on_dclick, id=F_LIST)
         elif type == L_RADIO:
             self.Bind(wx.EVT_RADIOBOX, self.on_change, id=F_LIST)
         elif type == L_CHECK:
             self.Bind(wx.EVT_CHECKLISTBOX, self.on_check, id=F_LIST)
-
-
         self.type = type
 
 
     def on_change(self,evt):
         self.handler.set_selected_node(self.list.GetSelection())
 
+    def on_dclick(self, evt):
+        self.on_change(evt)
+        self.handler.on_send_to_chat(evt)
+
     def on_check(self,evt):
         for i in xrange(self.list.GetCount()):
             self.handler.set_selected_node(i, bool2int(self.list.IsChecked(i)))
 
-    def set_checks(self):
-        for i in self.handler.get_selections_index():
-            self.list.Check(i)
-
 
 
 BUT_ADD = wx.NewId()
+BUT_APP = wx.NewId()
 BUT_REM = wx.NewId()
 BUT_EDIT = wx.NewId()
 F_TYPE = wx.NewId()
 F_NO_TITLE = wx.NewId()
+LIST_CTRL = wx.NewId()
 
 class listbox_edit_panel(wx.Panel):
     def __init__(self, parent, handler):
         wx.Panel.__init__(self, parent, -1)
         self.handler = handler
-        sizer = wx.StaticBoxSizer(wx.StaticBox(self, -1, "List Box Properties"), wx.VERTICAL)
 
+        title_sizer = wx.BoxSizer(wx.HORIZONTAL)
         self.text = wx.TextCtrl(self, P_TITLE, handler.xml.get('name'))
+        title_sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
+        title_sizer.Add(self.text, 1, wx.EXPAND)
 
-        opts = handler.get_options()
-        self.listbox = wx.ListBox(self, F_LIST, choices=opts, style=wx.LB_HSCROLL|wx.LB_SINGLE|wx.LB_NEEDED_SB)
+        self.listbox = wx.ListCtrl(self, LIST_CTRL, style=wx.LC_REPORT)
+        self.listbox.InsertColumn(0, 'Value')
+        self.listbox.InsertColumn(1, 'Caption')
+        self.listbox.SetColumnWidth(0, 300)
+        self.listbox.SetColumnWidth(1, 54)
+        self.reload_options()
+        
         opts = ['Drop Down', 'List Box', 'Radio Box', 'Check List']
-        self.type_radios = wx.RadioBox(self,F_TYPE,"List Type",choices=opts)
+        self.type_radios = wx.RadioBox(self,F_TYPE, choices=opts)
         self.type_radios.SetSelection(handler.get_type())
 
         self.send_button = wx.CheckBox(self, F_SEND_BUTTON, " Send Button")
@@ -645,76 +608,92 @@
         self.hide_title = wx.CheckBox(self, F_NO_TITLE, " Hide Title")
         self.hide_title.SetValue(handler.is_hide_title())
 
+        checkboxes_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        checkboxes_sizer.Add(self.send_button, 1 , wx.EXPAND)
+        checkboxes_sizer.Add(wx.Size(10,10))
+        checkboxes_sizer.Add(self.hide_title, 1, wx.EXPAND)
+
+        text_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.value = wx.TextCtrl(self, -1)
+        text_sizer.Add(wx.StaticText(self, -1, "Value:"), 0, wx.EXPAND)
+        text_sizer.Add(self.value, 1, wx.EXPAND)
+        text_sizer.Add(wx.StaticText(self, -1, "Caption (optional):"), 0, wx.EXPAND)
+        self.caption = wx.TextCtrl(self, -1)
+        text_sizer.Add(self.caption, 1, wx.EXPAND)
+
         but_sizer = wx.BoxSizer(wx.HORIZONTAL)
-        but_sizer.Add(wx.Button(self, BUT_ADD, "Add"), 1, wx.EXPAND)
+        but_sizer.Add(wx.Button(self, BUT_APP, "Add To End"), 1, wx.EXPAND)
+        but_sizer.Add(wx.Size(10,10))
+        but_sizer.Add(wx.Button(self, BUT_ADD, "Insert"), 1, wx.EXPAND)
         but_sizer.Add(wx.Size(10,10))
         but_sizer.Add(wx.Button(self, BUT_EDIT, "Edit"), 1, wx.EXPAND)
         but_sizer.Add(wx.Size(10,10))
         but_sizer.Add(wx.Button(self, BUT_REM, "Remove"), 1, wx.EXPAND)
 
-        sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
-        sizer.Add(self.text, 0, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(title_sizer, 0, wx.EXPAND)
         sizer.Add(self.type_radios, 0, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
-        sizer.Add(self.send_button, 0 , wx.EXPAND)
-        sizer.Add(self.hide_title, 0, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
-        sizer.Add(wx.StaticText(self, -1, "Options:"), 0, wx.EXPAND)
+        sizer.Add(checkboxes_sizer, 0 , wx.EXPAND)
         sizer.Add(self.listbox,1,wx.EXPAND);
+        sizer.Add(text_sizer,0,wx.EXPAND)
         sizer.Add(but_sizer,0,wx.EXPAND)
-
+        
         self.SetSizer(sizer)
         self.SetAutoLayout(True)
         self.Fit()
         parent.SetSize(self.GetBestSize())
 
-        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+        self.text.Bind(wx.EVT_KILL_FOCUS, self.on_text, id=P_TITLE)
         self.Bind(wx.EVT_BUTTON, self.on_edit, id=BUT_EDIT)
         self.Bind(wx.EVT_BUTTON, self.on_remove, id=BUT_REM)
         self.Bind(wx.EVT_BUTTON, self.on_add, id=BUT_ADD)
+        self.Bind(wx.EVT_BUTTON, self.on_append, id=BUT_APP)
         self.Bind(wx.EVT_RADIOBOX, self.on_type, id=F_TYPE)
         self.Bind(wx.EVT_CHECKBOX, self.on_hide_button, id=F_NO_TITLE)
         self.Bind(wx.EVT_CHECKBOX, self.on_send_button, id=F_SEND_BUTTON)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_selected, id=LIST_CTRL)
 
     def on_type(self,evt):
         self.handler.set_type(evt.GetInt())
 
     def on_add(self,evt):
-        dlg = wx.TextEntryDialog(self, 'Enter option?','Add Option', '')
-        if dlg.ShowModal() == wx.ID_OK:
-            self.handler.add_option(dlg.GetValue())
-        dlg.Destroy()
+        index = self.listbox.GetFocusedItem()
+        self.handler.add_option(index, self.value.GetValue(), self.caption.GetValue())
+        self.reload_options()
+
+    def on_append(self,evt):
+        index = self.listbox.GetFocusedItem()
+        self.handler.add_option(self.listbox.GetItemCount(), self.value.GetValue(), self.caption.GetValue())
         self.reload_options()
 
     def on_remove(self,evt):
-        index = self.listbox.GetSelection()
+        index = self.listbox.GetFocusedItem()
         if index >= 0:
             self.handler.remove_option(index)
             self.reload_options()
 
     def on_edit(self,evt):
-        index = self.listbox.GetSelection()
+        index = self.listbox.GetFocusedItem()
         if index >= 0:
-            txt = self.handler.get_option(index)
-            dlg = wx.TextEntryDialog(self, 'Enter option?','Edit Option', txt)
-            if dlg.ShowModal() == wx.ID_OK:
-                self.handler.edit_option(index,dlg.GetValue())
-            dlg.Destroy()
+            self.handler.edit_option(index,self.value.GetValue(),self.caption.GetValue())
             self.reload_options()
 
+    def on_selected(self, evt):
+        index = self.listbox.GetFocusedItem()
+        if index >= 0:
+            (value, caption) = self.handler.get_option(index)
+            self.value.SetValue(value)
+            self.caption.SetValue(caption)
+
     def reload_options(self):
-        self.listbox.Clear()
-        for opt in self.handler.get_options():
-            self.listbox.Append(opt)
+        self.listbox.DeleteAllItems()
+        values = self.handler.get_values()
+        captions = self.handler.get_captions()
+        for index in range(len(values)):
+            self.listbox.Append((values[index],captions[index]))
 
-    def on_text(self,evt):
-        id = evt.GetId()
-        txt = self.text.GetValue()
-        if not len(txt): return
-        if id == P_TITLE:
-            self.handler.xml.set('name',txt)
-            self.handler.rename(txt)
+    def on_text(self, evt):
+        self.handler.rename(self.text.GetValue())
 
     def on_send_button(self,evt):
         self.handler.list.set("send_button", str( bool2int(evt.Checked()) ))
@@ -780,7 +759,7 @@
         sizer.Add(wx.StaticText(self, -1, "URL:"), 0, wx.EXPAND)
         sizer.Add(self.text[P_URL], 0, wx.EXPAND)
         self.SetSizer(sizer)
-        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+        self.text[P_TITLE].Bind(wx.EVT_KILL_FOCUS, self.on_text, id=P_TITLE)
         self.Bind(wx.EVT_TEXT, self.on_text, id=P_URL)
 
     def on_text(self,evt):
@@ -788,7 +767,6 @@
         txt = self.text[id].GetValue()
         if not len(txt): return
         if id == P_TITLE:
-            self.handler.xml.set('name',txt)
             self.handler.rename(txt)
         elif id == P_URL:
             self.handler.link.set('href',txt)
@@ -849,9 +827,19 @@
         return resource_panel(parent,self)
 
     def tohtml(self):
-        # decrement the current value or post a "nothing left" message
-        # print the multi-line macro
-        return "resource"
+        if self.get_current() < 1:
+            self.chat.InfoPost('No more of the resource left.  You must reset the resource to use it again.')
+            return ''
+        return self.spend_resource(1)
+
+    def spend_resource(self, amount):
+        current = self.get_current() - amount
+        self.resource.set("current",str(current))
+        macro_text = self.get_macro()
+        macro_text = macro_text.replace("_NAME_",self.xml.get("name"))
+        macro_text = macro_text.replace("_CHANGE_", str(amount))
+        macro_text = macro_text.replace("_CURRENT_", str(current))
+        return macro_text
 
     def use_checks(self):
         if self.resource.get("checks") == "1":
@@ -887,7 +875,8 @@
         self.chat = handler.chat
 
         sizer = wx.BoxSizer(wx.HORIZONTAL)
- #       sizer.Add(wx.Button(self, RESOURCE_RESET, "Reset"))
+        sizer.Add(wx.Button(self, RESOURCE_RESET, "Reset"))
+        sizer.Add(wx.Size(10,10))
         sizer.Add(wx.StaticText(self, -1, handler.xml.get('name')+": "), 1, wx.ALIGN_RIGHT)
         if self.handler.use_checks():
             grid = wx.GridSizer(1, 11, 0, 0)
@@ -908,13 +897,11 @@
             sizer.Add(self.number, 0, wx.ALIGN_RIGHT)
         sizer.Add(wx.Size(10,10), 0, wx.ALIGN_RIGHT)
         sizer.Add(wx.Button(self, RESOURCE_DONE,  "Apply"), 0, wx.ALIGN_RIGHT)
-  #      self.chat.InfoPost("res 10")
         sizer.SetMinSize(wx.Size(380,10))
         self.sizer = sizer
         self.SetSizer(sizer)
         self.SetAutoLayout(True)
         self.Fit()
-   #     self.chat.InfoPost("res 20")
         self.Bind(wx.EVT_BUTTON, self.on_reset, id=RESOURCE_RESET)
         self.Bind(wx.EVT_BUTTON, self.on_done,   id=RESOURCE_DONE)
 
@@ -937,15 +924,11 @@
                     current += 1
         else:
             # validate text
-            current = int(strip_text(self.number.GetValue()))
+            current = int(self.number.GetValue())
         change = self.handler.get_current()-current
         if change > 0:
-            macro_text = self.handler.get_macro()
-            macro_text = macro_text.replace("_NAME_",self.handler.xml.get("name"))
-            macro_text = macro_text.replace("_CHANGE_", str(change))
-            macro_text = macro_text.replace("_CURRENT_", str(current))
-            self.handler.chat.ParsePost(macro_text, True, True)
-            self.handler.resource.set("current",str(current))
+            macro_text = self.handler.spend_resource(change)
+            self.handler.chat.ParsePost(macro_text, True, True, ParserContext(self.handler))
 
 
 RES_EDIT_TITLE    = wx.NewId()
@@ -994,7 +977,7 @@
         self.Fit()
         parent.SetSize(self.GetBestSize())
 
-        self.Bind(wx.EVT_TEXT, self.on_title, id=RES_EDIT_TITLE)
+        self.title.Bind(wx.EVT_KILL_FOCUS, self.on_title)
         self.Bind(wx.EVT_TEXT, self.on_base, id=RES_EDIT_BASE)
         self.Bind(wx.EVT_TEXT, self.on_current, id=RES_EDIT_CURRENT)
         self.Bind(wx.EVT_RADIOBOX, self.on_type, id=RES_EDIT_CHECKS)
@@ -1002,9 +985,7 @@
 
 
     def on_title(self, evt):
-        if len(self.title.GetValue()):
-            self.handler.xml.set('name', self.title.GetValue())
-            self.handler.rename(self.title.GetValue())
+        self.handler.rename(self.title.GetValue())
 
     def on_base(self, evt):
         try:
@@ -1025,103 +1006,3 @@
 
     def on_macro(self,evt):
         self.handler.resource.text = self.macro.GetValue()
-
-
-#######################
-## bonus handler
-#######################
-
-class bonus_handler(node_handler):
-    """
-    <nodehandler class="bonus_handler" module="forms" name="">
-        <bonus value="2" type="optional">Multi-line list of node references</bonus>
-    </nodehandler>
-    """
-    def __init__(self,xml,tree_node):
-        node_handler.__init__(self,xml,tree_node)
-        self.bonus_xml = self.xml.find('bonus')
-        self.add_to_bonus_map()
-
-    def get_design_panel(self,parent):
-        return bonus_edit_panel(parent,self)
-
-    def get_use_panel(self,parent):# there is no 'use' for a bonus
-        return bonus_edit_panel(parent,self)
-
-    def tohtml(self):
-        return "bonus"# there is no 'send to chat' or 'pretty print'
-
-    def get_value(self):
-        return self.bonus_xml.get('value', '')
-
-    def delete(self):
-        self.remove_from_bonus_map()
-        return node_handler.delete(self)
-
-    def add_to_bonus_map(self):
-        for target in getText(self.bonus_xml).split('\n'):
-            self.tree.AddBonus(target, self)
-
-    def remove_from_bonus_map(self):
-        for target in getText(self.bonus_xml).split('\n'):
-            self.tree.RemoveBonus(target, self)
-        
-
-
-BONUS_EDIT_TITLE = wx.NewId()
-BONUS_EDIT_VALUE = wx.NewId()
-BONUS_EDIT_TYPE = wx.NewId()
-BONUS_EDIT_REF = wx.NewId()
-
-
-class bonus_edit_panel(wx.Panel):
-    def __init__(self, parent, handler):
-        wx.Panel.__init__(self, parent, -1)
-        self.handler = handler
-
-        sizer = wx.StaticBoxSizer(wx.StaticBox(self, -1, "Bonus Properties"), wx.VERTICAL)
-        sizer.Add(wx.StaticText(self, -1, "Name of bonus:"), 0, wx.EXPAND)
-        self.title = wx.TextCtrl(self, BONUS_EDIT_TITLE, self.handler.xml.get('name'))
-        sizer.Add(self.title, 0, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
-
-        sizer.Add(wx.StaticText(self, -1, "Size of bonus:"), 0, wx.EXPAND)
-        self.value = wx.TextCtrl(self, BONUS_EDIT_VALUE, self.handler.bonus_xml.get('value', ''))
-        sizer.Add(self.value, 0, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
-
-        sizer.Add(wx.StaticText(self, -1, "Type of bonus:"), 0, wx.EXPAND)
-        self.type = wx.TextCtrl(self, BONUS_EDIT_TYPE, self.handler.bonus_xml.get("type"))
-        sizer.Add(self.type, 0, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
-
-        sizer.Add(wx.StaticText(self, -1, "Add to the following nodes:"), 0, wx.EXPAND)
-        self.ref = wx.TextCtrl(self, BONUS_EDIT_REF, getText(self.handler.bonus_xml), style=wx.TE_MULTILINE)
-        sizer.Add(self.ref, 1,  wx.EXPAND)
-
-        self.SetSizer(sizer)
-        self.SetAutoLayout(True)
-        self.Fit()
-        parent.SetSize(self.GetBestSize())
-
-        self.Bind(wx.EVT_TEXT, self.on_title, id=BONUS_EDIT_TITLE)# too many calls - should call only upon close
-        self.Bind(wx.EVT_TEXT, self.on_value, id=BONUS_EDIT_VALUE)
-        self.Bind(wx.EVT_TEXT, self.on_type, id=BONUS_EDIT_TYPE)
-        self.Bind(wx.EVT_TEXT, self.on_ref, id=BONUS_EDIT_REF)
-
-
-    def on_title(self, evt):
-        if len(self.title.GetValue()):
-            self.handler.xml.set('name', self.title.GetValue())
-            self.handler.rename(self.title.GetValue())
-
-    def on_value(self, evt):
-        self.handler.bonus_xml.set('value', self.value.GetValue())
-
-    def on_type(self, evt):
-        self.handler.bonus_xml.set('type', self.type.GetValue())
-
-    def on_ref(self, evt):
-        self.handler.remove_from_bonus_map()
-        self.handler.bonus_xml.text = self.ref.GetValue()
-        self.handler.add_to_bonus_map()
--- a/orpg/gametree/nodehandlers/minilib.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/minilib.py	Mon Mar 22 18:38:22 2010 -0600
@@ -36,7 +36,6 @@
 import string
 import map_miniature_nodehandler
 import orpg.mapper.map_msg
-# import scriptkit
 
 # Constants
 TO_MINILIB_MAP = {'path':'url', 'label':'name', 'id':None, 'action':None}
@@ -52,8 +51,6 @@
 
 TAG_MINIATURE = 'miniature'
 
-COMPONENT_MAP = 'map'
-COMPONENT_SESSION = 'session'
 # <nodehandler name='?' module='minilib' class='minilib_handler'>
 #     <miniature name='?' url='?' unique='?'></miniature>
 # </nodehandler>
@@ -75,7 +72,6 @@
         self.myeditor = None
         self.mywindow = None
         self.tree_node = tree_node
-        # self.xml_dom = xml_dom
         self.update_leaves()
         self.sanity_check_nodes()
 
@@ -180,26 +176,41 @@
     def send_mini_to_map( self, mini, count=1, addName=True ):
         if mini == None:
             return
-        if mini.get( ATTRIBUTE_URL ) == '' or mini.get( ATTRIBUTE_URL ) == 'http://':
-            self.chat.ParsePost( self.chat.colorize(self.chat.syscolor, '"%s" is not a valid URL, the mini "%s" will not be added to the map' % ( mini.get( ATTRIBUTE_URL ), mini.get( ATTRIBUTE_NAME ) )) )
+        if mini.get(ATTRIBUTE_URL, '') == '' or mini.get(ATTRIBUTE_URL, '') == 'http://':
+            self.chat.InfoPost( self.chat.colorize(self.chat.syscolor, '"%s" is not a valid URL, the mini "%s" will not be added to the map' % ( mini.get( ATTRIBUTE_URL ), mini.get( ATTRIBUTE_NAME ) )) )
             return
-        session = open_rpg.get_component( COMPONENT_SESSION )
+        session = open_rpg.get_component('session')
         if (session.my_role() != session.ROLE_GM) and (session.my_role() != session.ROLE_PLAYER):
             open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use the miniature Layer")
             return
-        map = open_rpg.get_component(COMPONENT_MAP)
-        for loop in range( count ):
-            msg = self.get_miniature_XML( mini, addName)
-            msg = str("<map action='update'><miniatures>" + msg + "</miniatures></map>")
-            map.new_data( msg )
-            session.send( msg )
+        map = open_rpg.get_component('map')
+        if count > 1:
+            x = mini.get('posx', 0)
+            y = mini.get('posy', 0)
+            width = int(mini.get('width', 50))
+            height = int(mini.get('height', 50))
+            list_pos = map.canvas.layers['grid'].get_snapped_to_list_of_positions(x, y, width, height, count)
+        name = mini.get('name', '')
+        for loop in range(count):
+            if addName and count > 1 and name and map.layer_handlers[2].use_serial:
+                mini.set('name', name + " " + str(map.canvas.layers['miniatures'].next_serial()))
+            if count > 1:
+                mini.set('posx', str(list_pos[loop].x))
+                mini.set('posy', str(list_pos[loop].y))
+            msg = self.get_miniature_XML(mini, addName)
+            session.send(str("<map action='update'><miniatures>" + msg.get_all_xml() + "</miniatures></map>"))
+            msg.init_prop('selected', '1')# this will make the mini initially selected
+            map.new_data(str("<map action='update'><miniatures>" + msg.get_all_xml() + "</miniatures></map>"))
+        if count > 1:
+            mini.set('name', name)
+            mini.set('posx', str(x))
+            mini.set('posy', str(y))
 
-    def get_miniature_XML( self, mini_xml, addName = True ):
+    def get_miniature_XML(self, mini_xml, addName = True):
         msg = orpg.mapper.map_msg.mini_msg()
-        map = open_rpg.get_component( COMPONENT_MAP )
-        session = open_rpg.get_component( COMPONENT_SESSION )
-        msg.init_prop( ATTRIBUTE_ID, session.get_next_id() )
-        msg.init_prop('selected', '1')# this will make the mini initially selected
+        map = open_rpg.get_component('map')
+        session = open_rpg.get_component('session')
+        msg.init_prop(ATTRIBUTE_ID, session.get_next_id())
         for k in mini_xml.keys():
             # translate our attributes to map attributes
             key = FROM_MINILIB_MAP.get( k, k )
@@ -213,7 +224,7 @@
             label = mini_xml.get( ATTRIBUTE_NAME )
         else:
             label = ''
-        return msg.get_all_xml()
+        return msg
 
     def is_unique( self, mini ):
         unique = mini.get( ATTRIBUTE_UNIQUE )
@@ -237,21 +248,6 @@
         except:
             return None
 
-class mini_handler( node_handler ):
-    def __init__( self, xml, tree_node, handler ):
-        node_handler.__init__( self, xml, tree_node)
-        self.handler = handler
-
-    def on_ldclick( self, evt ):
-        self.handler.send_mini_to_map( self.xml )
-
-    def on_drop( self, evt ):
-        pass
-
-    def on_lclick( self, evt ):
-        print 'hi'
-        evt.Skip()
-
 class minilib_use_panel(wx.Panel):
     """This panel will be displayed when the user double clicks on the
     miniature library node.  It is a sorted listbox of miniature labels,
@@ -474,7 +470,7 @@
         self.AppendRows( count )
         node = self.handler.new_mini( {
           ATTRIBUTE_NAME :' ',
-          ATTRIBUTE_URL :'http://'} )# minidom.Element( TAG_MINIATURE )
+          ATTRIBUTE_URL :'http://'} )# Element( TAG_MINIATURE )
         self.update_all()
         #self.handler.xml.append( node )
 
--- a/orpg/gametree/nodehandlers/rpg_grid.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/rpg_grid.py	Mon Mar 22 18:38:22 2010 -0600
@@ -31,6 +31,9 @@
 from core import *
 from forms import *
 
+M_HTML = 1
+M_USE = 2
+M_DESIGN = 3
 class rpg_grid_handler(node_handler):
     """ Node handler for rpg grid tool
 <nodehandler module='rpg_grid' class='rpg_grid_handler' name='sample'>
@@ -68,35 +71,70 @@
         tree = self.tree
         icons = self.tree.icons
         tree.CollapseAndReset(self.mytree_node)
-        for row in self.grid.findall('row'):
+        rows = self.grid.findall('row')
+        for index in range(len(rows)):
+            row = rows[index]
             first_cell = row.find('cell')
-            name = first_cell.text
-            if name == None or name == '':
+            name = getText(first_cell)
+            if name == '':
                 name = "Row"
             new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
-            handler = grid_row_handler(row,new_tree_node,self)
+            handler = grid_row_handler(row, new_tree_node, self, index)
             tree.SetPyData(new_tree_node,handler)
 
     def tohtml(self):
-        border = self.grid.get("border")
+        border = self.grid.get("border", "")
         name = self.xml.get('name')
         rows = self.grid.findall('row')
         colspan = str(len(rows[0].findall('cell')))
         html_str = "<table border=\""+border+"\" align=center><tr bgcolor=\""+TH_BG+"\" ><th colspan="+colspan+">"+name+"</th></tr>"
-        for r in rows:
-            cells = r.findall('cell')
-            html_str += "<tr>"
-            for c in cells:
-                #html_str += "<td width='"+c.get('size')+"' >" bug here
-                html_str += "<td >"
-                text = c.text
-                if text == None or text == '':
-                    text = '<br />'
-                html_str += text + "</td>"
-            html_str += "</tr>"
+        for index in range(len(rows)):
+            html_str += "<tr><td >"
+            html_str += "</td><td >".join(self.calculate_cell_values(rows[index], M_HTML, index)[0])
+            html_str += "</td></tr>"
         html_str += "</table>"
         return html_str
 
+    def calculate_cell_values(self, row, mode, row_index):
+        """
+        mode indicates whether this is for the use dialog, the design dialog or for pretty print/send to chat
+        this is a complex function because each mode does slightly different calculations
+        """
+        cells = row.findall('cell')
+        length = len(cells)
+        values = ['']*length
+        readonly = [False]*length
+        valuesToBeSummed = []
+        col_two_is_total = self.is_col_two_total() and row_index != 0
+        
+        for i in range(length):
+            if i == 1 and col_two_is_total:
+                readonly[i] = True
+                continue
+            raw = getText(cells[i])
+            if raw == '':
+                #cells[i].text = raw # not needed?
+                if mode == M_HTML:
+                    raw = '<br />'
+            parsed = raw
+            if self.chat.contains_reference(raw):
+                parsed = self.chat.parse_multiline(raw, ParserContext(self), False)
+                if mode == M_USE:
+                    readonly[i] = True
+            if mode == M_DESIGN:
+                values[i] = raw
+            else:
+                values[i] = parsed
+            if i > 1:
+                valuesToBeSummed.append(parsed)
+        if col_two_is_total and length > 1:
+            value = 0
+            for i in range(len(valuesToBeSummed)):
+                if is_integer(valuesToBeSummed[i]):
+                    value += int(valuesToBeSummed[i])
+            values[1] = str(value)
+        return values, readonly
+
     def get_design_panel(self,parent):
         return rpg_grid_edit_panel(parent,self)
 
@@ -107,17 +145,21 @@
         return 1
 
     def is_autosized(self):
-        return self.grid.get("autosize")
+        return self.grid.get("autosize") == '1'
 
     def set_autosize(self,autosize=1):
         self.grid.set("autosize",str(autosize))
 
+    def is_col_two_total(self):
+        return self.grid.get("coltwoistotal") == '1'
+
 class grid_row_handler(node_handler):
     """ Node Handler grid row.
     """
-    def __init__(self,xml,tree_node,parent):
-        node_handler.__init__(self,xml,tree_node)
-        self.drag = False
+    def __init__(self, xml, tree_node, grid, index):
+        node_handler.__init__(self, xml, tree_node)
+        self.grid = grid
+        self.index = index
 
     def on_drop(self,evt):
         pass
@@ -126,28 +168,25 @@
         return 0;
 
     def tohtml(self):
-        cells = self.xml.findall('cell')
-        html_str = "<table border=1 align=center><tr >"
-        for c in cells: # should loop over rows first, then cells
-            html_str += "<td >"
-            text = c.text
-            if text == '' or text is None:
-                text = '<br />'
-            html_str += text + "</td>"
-            html_str += "</tr>"
-        html_str += "</table>"
+        border = self.grid.grid.get("border", "")
+        html_str = "<table border=\""+border+"\" align=center><tr ><td >"
+        html_str += "</td><td >".join(self.grid.calculate_cell_values(self.xml, M_HTML, self.index)[0])
+        html_str += "</td></tr></table>"
         return html_str
 
     def get_value(self):
         cells = self.xml.findall('cell')
-        if len(cells) == 2:
+        length = len(cells)
+        if self.grid.is_col_two_total() and length > 1:
+            return self.grid.calculate_cell_values(self.xml, M_USE, self.index)[0][1]
+        elif length >= 2:
             return getText(cells[1])
         else:
             return None
 
     def set_value(self, new_value):
         cells = self.xml.findall('cell')
-        if len(cells) == 2:
+        if len(cells) >= 2 and not self.grid.is_col_two_total():
             cells[1].text = new_value
 
 class MyCellEditor(wx.grid.PyGridCellEditor):
@@ -178,7 +217,6 @@
     def __init__(self):
         wx.grid.PyGridCellEditor.__init__(self)
 
-
     def Create(self, parent, id, evtHandler):
         """
         Called to create the control, which must derive from wxControl.
@@ -190,7 +228,6 @@
         if evtHandler:
             self._tc.PushEventHandler(evtHandler)
 
-
     def SetSize(self, rect):
         """
         Called to position/size the edit control within the cell rectangle.
@@ -199,15 +236,6 @@
         """
         self._tc.SetDimensions(rect.x+1, rect.y+1, rect.width+2, rect.height+2)
 
-
-    def Show(self, show, attr):
-        """
-        Show or hide the edit control.  You can use the attr (if not None)
-        to set colours or fonts for the control.
-        """
-        self.base_Show(show, attr)
-
-
     def BeginEdit(self, row, col, grid):
         """
         Fetch the value from the table and prepare the edit control
@@ -222,7 +250,6 @@
         # For this example, select the text
         self._tc.SetSelection(0, self._tc.GetLastPosition())
 
-
     def EndEdit(self, row, col, grid):
         """
         Complete the editing of the current cell. Returns True if the value
@@ -240,7 +267,6 @@
         self._tc.SetValue('')
         return changed
 
-
     def Reset(self):
         """
         Reset the value in the control back to its starting value.
@@ -249,7 +275,6 @@
         self._tc.SetValue(self.startValue)
         self._tc.SetInsertionPointEnd()
 
-
     def IsAcceptedKey(self, evt):
         """
         Return True to allow the given key to start editing: the base class
@@ -263,7 +288,6 @@
         return (not (evt.ControlDown() or evt.AltDown()) and
                 evt.GetKeyCode() != wx.WXK_SHIFT)
 
-
     def StartingKey(self, evt):
         """
         If the editor is enabled by pressing keys on the grid, this will be
@@ -286,13 +310,10 @@
         else:
             evt.Skip()
 
-
-
     def Destroy(self):
         """final cleanup"""
         self.base_Destroy()
 
-
     def Clone(self):
         """
         Create a new object which is the copy of this one
@@ -301,13 +322,13 @@
         return MyCellEditor()
 
 
-
 class rpg_grid(wx.grid.Grid):
     """grid for attacks"""
-    def __init__(self, parent, handler):
+    def __init__(self, parent, handler, mode):
         wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
         self.parent = parent
         self.handler = handler
+        self.mode = mode
 
         #  Registers a "custom" cell editor (really the example from Robin Dunn with minor mods
         self.RegisterDataType(wx.grid.GRID_VALUE_STRING, wx.grid.GridCellStringRenderer(),MyCellEditor())
@@ -320,7 +341,7 @@
         self.SetColLabelSize(0)
         self.set_col_widths()
 
-        for i in range(0,len(self.rows)):
+        for i in range(len(self.rows)):
             self.refresh_row(i)
 
         self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
@@ -345,7 +366,9 @@
         value = self.GetCellValue(row,col)
         cells = self.rows[row].findall('cell')
         cells[col].text = value
-        if col == 0:
+        if self.handler.is_col_two_total() and col > 1:
+            self.refresh_row(row)
+        elif col == 0:
             self.handler.refresh_rows()
 
     def set_col_widths(self):
@@ -357,62 +380,82 @@
             except:
                 continue
 
-    def refresh_row(self,rowi):
-        cells = self.rows[rowi].findall('cell')
-        for i in range(0,len(cells)):
-            text = cells[i].text
-            if text == None or text == '':
-                text = ''
-                cells[i].text = text
-            self.SetCellValue(rowi,i,text)
+    def refresh_row(self, row_index):
+        values, readonly = self.handler.calculate_cell_values(self.rows[row_index], self.mode, row_index)
+        for i in range(len(values)):
+            self.SetReadOnly(row_index, i, readonly[i])
+            self.SetCellValue(row_index, i, values[i])
+            
+    def append_row(self,evt=None):
+        self.insert_row(self.GetNumberRows())
 
     def add_row(self,evt=None):
+        self.insert_row(self.GetGridCursorRow())
+
+    def insert_row(self, index):
         cols = self.GetNumberCols()
         row = ET.Element('row')
-        for i in range(0,cols):
+        for i in range(cols):
             cell = ET.Element('cell')
             cell.text = ''
-            row.append(cell)
-        self.handler.grid.append(row)
-        self.AppendRows(1)
+            row.append(cell) 
+        self.handler.grid.insert(index, row)
+        self.InsertRows(index, 1)
         self.rows = self.handler.grid.findall('row')
         self.handler.refresh_rows()
+        if self.handler.is_col_two_total():
+            self.refresh_row(index)
+
+    def append_col(self,evt=None):
+        self.insert_col(self.GetNumberCols())
 
     def add_col(self,evt=None):
+        self.insert_col(self.GetGridCursorCol())
+
+    def insert_col(self, index):
         for r in self.rows:
             cell = ET.Element('cell')
             cell.text = ''
-            r.append(cell)
-        self.AppendCols(1)
+            r.insert(index, cell)
+        self.InsertCols(index, 1)
         self.set_col_widths()
+        if self.handler.is_col_two_total():
+            for i in range(len(self.rows)):
+                self.refresh_row(i)
+        if index==0:
+            self.handler.refresh_rows()
 
     def del_row(self,evt=None):
-        num = self.GetNumberRows()
-        if num == 1:
+        if self.GetNumberRows() == 1:
             return
-        self.handler.grid.remove(self.handler.grid[num-1])#always remove last row -- nasty
-        self.DeleteRows(num-1,1)
+        cursor_row = self.GetGridCursorRow()
+        self.handler.grid.remove(self.handler.grid[cursor_row])
+        self.DeleteRows(cursor_row,1)
         self.rows = self.handler.grid.findall('row')
         self.handler.refresh_rows()
 
     def del_col(self,evt=None):
-        num = self.GetNumberCols()
-        if num == 1:
+        if self.GetNumberCols() == 1:
             return
+        cursor_col = self.GetGridCursorCol()
         for r in self.rows:
             cells = r.findall('cell')
-            r.remove(r[num-1])# always remove the last column -- nasty
-        self.DeleteCols(num-1,1)
+            r.remove(r[cursor_col])
+        self.DeleteCols(cursor_col,1)
         self.set_col_widths()
-
+        if self.handler.is_col_two_total() and self.GetNumberCols() > 1:
+            for i in range(len(self.rows)):
+                self.refresh_row(i)
+        if cursor_col==0:
+            self.handler.refresh_rows()
 
 G_TITLE = wx.NewId()
-GRID_BOR = wx.NewId()
+G_GRID_BOR = wx.NewId()
 class rpg_grid_panel(wx.Panel):
     def __init__(self, parent, handler):
         wx.Panel.__init__(self, parent, -1)
         self.handler = handler
-        self.grid = rpg_grid(self,handler)
+        self.grid = rpg_grid(self, handler, M_USE)
         label = handler.xml.get('name')
         self.main_sizer = wx.BoxSizer(wx.VERTICAL)
         self.main_sizer.Add(wx.StaticText(self, -1, label+": "), 0, wx.EXPAND)
@@ -424,8 +467,11 @@
 
 
 G_AUTO_SIZE = wx.NewId()
+G_COL_2_IS_TOTAL = wx.NewId()
 G_ADD_ROW = wx.NewId()
+G_APP_ROW = wx.NewId()
 G_ADD_COL = wx.NewId()
+G_APP_COL = wx.NewId()
 G_DEL_ROW = wx.NewId()
 G_DEL_COL = wx.NewId()
 
@@ -433,59 +479,78 @@
     def __init__(self, parent, handler):
         wx.Panel.__init__(self, parent, -1)
         self.handler = handler
-        self.grid = rpg_grid(self,handler)
+        self.grid = rpg_grid(self, handler, M_DESIGN)
+        title_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        title_sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
         self.title = wx.TextCtrl(self, G_TITLE, handler.xml.get('name'))
+        title_sizer.Add(self.title, 1, wx.EXPAND)
 
-        radio_b = wx.RadioBox(self, GRID_BOR, "Border (HTML)", choices=["no","yes"])
-        border = handler.grid.get("border")
-        radio_b.SetSelection(int(border))
-
+        self.border = wx.CheckBox(self, G_GRID_BOR, " Border (HTML)")
+        if handler.grid.get("border") == '1':
+            self.border.SetValue(True)
+        else:
+            self.border.SetValue(False)
         self.auto_size = wx.CheckBox(self, G_AUTO_SIZE, " Auto Size")
-        if handler.is_autosized() == '1':
+        if handler.is_autosized():
             self.auto_size.SetValue(True)
         else:
             self.auto_size.SetValue(False)
-
-        sizer = wx.BoxSizer(wx.HORIZONTAL)
-        sizer.Add(wx.Button(self, G_ADD_ROW, "Add Row"), 1, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
-        sizer.Add(wx.Button(self, G_DEL_ROW, "Remove Row"), 1, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
-        sizer.Add(wx.Button(self, G_ADD_COL, "Add Column"), 1, wx.EXPAND)
-        sizer.Add(wx.Size(10,10))
-        sizer.Add(wx.Button(self, G_DEL_COL, "Remove Column"), 1, wx.EXPAND)
+        self.colTwoIsTotal = wx.CheckBox(self, G_COL_2_IS_TOTAL, " 2nd Col Is Total")
+        if handler.is_col_two_total():
+            self.colTwoIsTotal.SetValue(True)
+        else:
+            self.colTwoIsTotal.SetValue(False)
+        checkboxes_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        checkboxes_sizer.Add(self.border, 1, wx.EXPAND)
+        checkboxes_sizer.Add(self.auto_size, 1, wx.EXPAND)
+        checkboxes_sizer.Add(self.colTwoIsTotal, 1, wx.EXPAND)
+        
+        row_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        row_sizer.Add(wx.Button(self, G_APP_ROW, "Add Row To End"), 1, wx.EXPAND)
+        row_sizer.Add(wx.Size(10,10))
+        row_sizer.Add(wx.Button(self, G_ADD_ROW, "Insert Row"), 1, wx.EXPAND)
+        row_sizer.Add(wx.Size(10,10))
+        row_sizer.Add(wx.Button(self, G_DEL_ROW, "Remove Row"), 1, wx.EXPAND)
 
-        self.main_sizer = wx.StaticBoxSizer(wx.StaticBox(self,-1,"Grid"), wx.VERTICAL)
-        self.main_sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
-        self.main_sizer.Add(self.title, 0, wx.EXPAND)
-        self.main_sizer.Add(radio_b, 0, 0)
-        self.main_sizer.Add(self.auto_size, 0, 0)
-        self.main_sizer.Add(self.grid,1,wx.EXPAND)
-        self.main_sizer.Add(sizer,0,wx.EXPAND)
+        col_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        col_sizer.Add(wx.Button(self, G_APP_COL, "Add Column To End"), 1, wx.EXPAND)
+        col_sizer.Add(wx.Size(10,10))
+        col_sizer.Add(wx.Button(self, G_ADD_COL, "Insert Column"), 1, wx.EXPAND)
+        col_sizer.Add(wx.Size(10,10))
+        col_sizer.Add(wx.Button(self, G_DEL_COL, "Remove Column"), 1, wx.EXPAND)
 
-        self.SetSizer(self.main_sizer)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(title_sizer, 0, wx.EXPAND)
+        sizer.Add(checkboxes_sizer, 0, wx.EXPAND)
+        sizer.Add(self.grid,1,wx.EXPAND)
+        sizer.Add(row_sizer,0,wx.EXPAND)
+        sizer.Add(col_sizer,0,wx.EXPAND)
+
+        self.SetSizer(sizer)
         self.SetAutoLayout(True)
         self.Fit()
 
-        self.Bind(wx.EVT_TEXT, self.on_text, id=G_TITLE)
+        self.title.Bind(wx.EVT_KILL_FOCUS, self.on_text)
         self.Bind(wx.EVT_BUTTON, self.grid.add_row, id=G_ADD_ROW)
+        self.Bind(wx.EVT_BUTTON, self.grid.append_row, id=G_APP_ROW)
         self.Bind(wx.EVT_BUTTON, self.grid.del_row, id=G_DEL_ROW)
         self.Bind(wx.EVT_BUTTON, self.grid.add_col, id=G_ADD_COL)
+        self.Bind(wx.EVT_BUTTON, self.grid.append_col, id=G_APP_COL)
         self.Bind(wx.EVT_BUTTON, self.grid.del_col, id=G_DEL_COL)
-        self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=GRID_BOR)
+        self.Bind(wx.EVT_CHECKBOX, self.on_border, id=G_GRID_BOR)
         self.Bind(wx.EVT_CHECKBOX, self.on_auto_size, id=G_AUTO_SIZE)
+        self.Bind(wx.EVT_CHECKBOX, self.on_coltwoistotal, id=G_COL_2_IS_TOTAL)
 
     def on_auto_size(self,evt):
         self.handler.set_autosize(bool2int(evt.Checked()))
 
-    def on_radio_box(self,evt):
-        id = evt.GetId()
-        index = evt.GetInt()
-        if id == GRID_BOR:
-            self.handler.grid.set("border",str(index))
+    def on_border(self,evt):
+        self.handler.grid.set("border",str(bool2int(evt.Checked())))
+
+    def on_coltwoistotal(self, evt):
+        self.handler.grid.set("coltwoistotal",str(bool2int(evt.Checked())))
+        for i in range(len(self.grid.rows)):
+            self.grid.refresh_row(i)
 
     def on_text(self,evt):
-        txt = self.title.GetValue()
-        if txt != "":
-            self.handler.xml.set('name',txt)
-            self.handler.rename(txt)
+        self.handler.rename(self.title.GetValue())
--- a/orpg/gametree/nodehandlers/voxchat.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/gametree/nodehandlers/voxchat.py	Mon Mar 22 18:38:22 2010 -0600
@@ -40,7 +40,6 @@
 import orpg.tools.inputValidator
 
 import orpg.dirpath
-import orpg.minidom
 import orpg.networking.mplay_client
 from orpg.orpgCore import open_rpg
 
--- a/orpg/main.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/main.py	Mon Mar 22 18:38:22 2010 -0600
@@ -43,13 +43,13 @@
 import orpg.tools.rgbhex
 import orpg.gametree.gametree
 import orpg.chat.chatwnd
-import orpg.dieroller.utils
 import orpg.networking.mplay_client
 import orpg.networking.gsclient
 import orpg.mapper.map
 import orpg.mapper.images
 import orpg.tools.orpg_settings
 
+from orpg.dieroller import roller_manager
 from orpg.dirpath import dir_struct
 from orpg.tools.settings import settings
 from orpg.tools.orpg_log import logger
@@ -106,14 +106,13 @@
         self.Bind(wx.EVT_TIMER, self.session.update, self.ping_timer)
 
         # create roller manager
-        self.DiceManager = orpg.dieroller.utils.roller_manager()
-        self.DiceManager.setRoller(settings.get('dieroller'))
+        roller_manager.roller = settings.get('dieroller')
 
         #create password manager --SD 8/03
         self.password_manager = orpg.tools.passtool.PassTool()
         open_rpg.add_component('session', self.session)
         open_rpg.add_component('frame', self)
-        open_rpg.add_component('DiceManager', self.DiceManager)
+        open_rpg.add_component('DiceManager', roller_manager)
         open_rpg.add_component('password_manager', self.password_manager)
 
         # build frame windows
--- a/orpg/mapper/background.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/background.py	Mon Mar 22 18:38:22 2010 -0600
@@ -26,7 +26,7 @@
 # Description: This file contains some of the basic definitions for the chat
 # utilities in the orpg project.
 #
-__version__ = "$Id: background.py,v 1.29 2007/03/09 14:11:55 digitalxero Exp $"
+from __future__ import with_statement
 
 from base import *
 import thread
@@ -94,7 +94,7 @@
         self.type = BG_TEXTURE
         if self.img_path != path:
             try:
-                self.bg_bmp = ImageHandler.load(path, "texture", 0)
+                self.bg_bmp = ImageHandler.load(path, 'texture', 0).ConvertToBitmap()
                 if self.bg_bmp == None:
                     raise Exception, "Invalid image type!"
             except:
@@ -106,7 +106,7 @@
 
         self.type = BG_IMAGE
         if self.img_path != path:
-            self.bg_bmp = ImageHandler.load(path, "background", 0)
+            self.bg_bmp = ImageHandler.load(path, "background", 0).ConvertToBitmap()
             try:
                 if self.bg_bmp == None:
                     raise Exception, "Invalid image type!"
@@ -282,30 +282,29 @@
                 thread.start_new_thread(self.upload, (postdata, self.localPath, type))
 
     def upload(self, postdata, filename, type):
-        self.lock.acquire()
-        if type == 'Image' or type == 'Texture':
-            url = settings.get('ImageServerBaseURL')
-            file = urllib.urlopen(url, postdata)
-            recvdata = file.read()
-            file.close()
-            try:
-                xml = ET.XML(recvdata)
-                if xml.tag == 'path':
-                    path = xml.get('url')
-                    path = urllib.unquote(path)
+        with self.lock:
+            if type == 'Image' or type == 'Texture':
+                url = settings.get('ImageServerBaseURL')
+                f = urllib.urlopen(url, postdata)
+                recvdata = f.read()
+                f.close()
+                try:
+                    xml = ET.XML(recvdata)
+                    if xml.tag == 'path':
+                        path = xml.get('url')
+                        path = urllib.unquote(path)
+                        print path
 
-                    if type == 'Image':
-                        self.set_image(path, 1)
-                    else:
-                        self.set_texture(path)
+                        if type == 'Image':
+                            self.set_image(path, 1)
+                        else:
+                            self.set_texture(path)
 
-                    self.localPath = filename
-                    self.local = True
-                    self.localTime = time.time()
-                else:
-                    logger.general(xml.get('msg'))
-            except Exception, e:
-                logger.exception(traceback.format_exc())
-                logger.exception(recvdata)
-
-        self.lock.release()
+                        self.localPath = filename
+                        self.local = True
+                        self.localTime = time.time()
+                    else:
+                        logger.general(xml.get('msg'))
+                except Exception, e:
+                    logger.exception(traceback.format_exc())
+                    logger.exception(recvdata)
--- a/orpg/mapper/background_handler.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/background_handler.py	Mon Mar 22 18:38:22 2010 -0600
@@ -36,6 +36,7 @@
 from orpg.mapper.base import *
 
 from orpg.tools.settings import settings
+from orpg.dirpath import dir_struct
 
 class background_handler(base_layer_handler):
     def __init__(self, parent, id, canvas):
@@ -66,7 +67,7 @@
 
     def on_browse(self, evt):
         if self.bg_type.GetStringSelection() == 'Texture' or self.bg_type.GetStringSelection() == 'Image':
-            dlg = wx.FileDialog(None, "Select a Miniature to load", orpg.dirpath.dir_struct["user"]+'webfiles/', wildcard="Image files (*.bmp, *.gif, *.jpg, *.png)|*.bmp;*.gif;*.jpg;*.png", style=wx.OPEN)
+            dlg = wx.FileDialog(None, "Select a Miniature to load", dir_struct["user"], wildcard="Image files (*.bmp, *.gif, *.jpg, *.png)|*.bmp;*.gif;*.jpg;*.png", style=wx.OPEN)
             if not dlg.ShowModal() == wx.ID_OK:
                 dlg.Destroy()
                 return
@@ -84,20 +85,30 @@
                     min_url = open_rpg.get_component("cherrypy") + filename
                 except:
                     return
-                if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Textures' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Textures': url = settings.get('LocalImageBaseURL') + 'Textures/'
-                if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Maps' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Maps': url = settings.get('LocalImageBaseURL') + 'Maps/'
-                if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Miniatures' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Miniatures': url = settings.get('LocalImageBaseURL') + 'Miniatures/'
+
+                texture_dir = dir_struct["user"] + 'webfiles' + os.sep + 'Textures'
+                maps_dir = dir_struct["user"] + 'webfiles' + os.sep + 'Maps'
+                mini_dir = dir_struct["user"] + 'webfiles' + os.set + 'Miniatures'
+                if dlg.GetDirectory() == texture_dir:
+                    url = settings.get('LocalImageBaseURL') + 'Textures/'
+                if dlg.GetDirectory() == maps_dir:
+                    url = settings.get('LocalImageBaseURL') + 'Maps/'
+                if dlg.GetDirectory() == mini_dir:
+                    url = settings.get('LocalImageBaseURL') + 'Miniatures/'
+
                 path = url + filename
-                if self.bg_type.GetStringSelection() == 'Texture':
-                    self.canvas.layers['bg'].set_texture(path)
-                elif self.bg_type.GetStringSelection() == 'Image':
-                    self.size = self.canvas.layers['bg'].set_image(path,1)
+                if self.bg_type.GetStringSelection() == BG_TEXTURE:
+                    self.canvas.layers['bg'].set_texture(path, "texture", 1)
+                elif self.bg_type.GetStringSelection() == BG_IMAGE:
+                    self.size = self.canvas.layers['bg'].set_image(path, "background", 1)
+
                 self.update_info()
                 self.canvas.send_map_data()
                 self.canvas.Refresh(False)
 
     def update_info(self):
         bg_type = self.canvas.layers['bg'].get_type()
+        self.bg_type.SetSelection(bg_type)
         session=self.canvas.frame.session
         if (session.my_role() != session.ROLE_GM):
             self.url_path.Hide()
@@ -142,11 +153,11 @@
             return
         self.canvas.layers['bg'].set_color(self.color_button.GetBackgroundColour())
 
-        if self.bg_type.GetStringSelection() == 'Texture':
+        if self.bg_type.GetStringSelection() == BG_TEXTURE:
             self.canvas.layers['bg'].set_texture(self.url_path.GetValue())
-        elif self.bg_type.GetStringSelection() == 'Image':
-            self.size = self.canvas.layers['bg'].set_image(self.url_path.GetValue(),1)
-
+        elif self.bg_type.GetStringSelection() == BG_IMAGE:
+            self.size = self.canvas.layers['bg'].set_image(self.url_path.GetValue(), 1)
+        
         self.update_info()
         self.canvas.send_map_data()
         self.canvas.Refresh(False)
--- a/orpg/mapper/base_handler.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/base_handler.py	Mon Mar 22 18:38:22 2010 -0600
@@ -59,11 +59,11 @@
         props = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'compass.gif', 'Edit map properties', wx.ID_ANY )
         mapopen = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'open.bmp', 'Load a map', wx.ID_ANY, '#c0c0c0', wx.BITMAP_TYPE_BMP )
         mapsave = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'save.bmp', 'Save the map', wx.ID_ANY, '#c0c0c0', wx.BITMAP_TYPE_BMP )
-        self.buttonsizer.Add(self.zoom_in_button, 0, wx.EXPAND )
-        self.buttonsizer.Add(self.zoom_out_button, 0, wx.EXPAND )
-        self.buttonsizer.Add(props, 0, wx.EXPAND )
-        self.buttonsizer.Add(mapopen, 0, wx.EXPAND )
-        self.buttonsizer.Add(mapsave, 0, wx.EXPAND )
+        self.buttonsizer.Add(self.zoom_in_button, 0, wx.EXPAND)
+        self.buttonsizer.Add(self.zoom_out_button, 0, wx.EXPAND)
+        self.buttonsizer.Add(props, 0, wx.EXPAND)
+        self.buttonsizer.Add(mapopen, 0, wx.EXPAND)
+        self.buttonsizer.Add(mapsave, 0, wx.EXPAND)
         self.SetSizer(self.basesizer)
         self.SetAutoLayout(True)
         self.Fit()
@@ -75,6 +75,8 @@
         self.Bind(wx.EVT_BUTTON, self.map_frame.on_save, mapsave)
         self.Bind(wx.EVT_BUTTON, self.canvas.on_prop, props)
 
+        self.SetSizer(self.basesizer)
+
     def build_menu(self,label = "Map"):
         "Menu is built based on the type of grid (rectangle or hex) we have in use."
         # do main menu
--- a/orpg/mapper/base_msg.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/base_msg.py	Mon Mar 22 18:38:22 2010 -0600
@@ -25,7 +25,7 @@
 #
 # Description:
 #
-__version__ = "$Id: base_msg.py,v 1.9 2007/03/09 14:11:55 digitalxero Exp $"
+from __future__ import with_statement
 
 from threading import RLock
 from orpg.networking.mplay_client import *
@@ -216,15 +216,13 @@
     #  XML importers begin
 
     def _from_dom(self,xml,prop_func):
-        self.p_lock.acquire()
-        if xml.tag == self.tagname:
-            if xml.keys():
-                for k in xml.keys():
-                    prop_func(k,xml.get(k))
-        else:
-            self.p_lock.release()
-            raise Exception, "Error attempting to modify a " + self.tagname + " from a non-<" + self.tagname + "/> element"
-        self.p_lock.release()
+        with self.p_lock:
+            if xml.tag == self.tagname:
+                if xml.keys():
+                    for k in xml.keys():
+                        prop_func(k,xml.get(k))
+            else:
+                raise Exception, "Error attempting to modify a " + self.tagname + " from a non-<" + self.tagname + "/> element"
 
     def init_from_dom(self,xml):
     #  xml must be pointing to an empty tag.  Override in a derived class for <map/> and other similar tags.
--- a/orpg/mapper/fog.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/fog.py	Mon Mar 22 18:38:22 2010 -0600
@@ -28,6 +28,7 @@
 from random import Random
 from region import *
 import traceback
+import StringIO
 COURSE = 10
 
 class FogArea:
@@ -42,12 +43,12 @@
         if points == None:
             return result
 
-        for pairs in string.split( points, ';' ):
-            pair = string.split( pairs, ',' )
-            p = ET.Element( "point" )
-            p.set( "x", pair[0] )
-            p.set( "y", pair[1] )
-            result.append( p )
+        for pairs in string.split(points, ';'):
+            pair = string.split(pairs, ',')
+            p = ET.Element("point")
+            p.set("x", pair[0])
+            p.set("y", pair[1])
+            result.append(p)
 
         return result
 
@@ -57,27 +58,42 @@
 
         if localOutline != None and localOutline != "all" and localOutline != "none":
             localOutline = "points"
-        elem = ET.Element( "poly" )
+        elem = ET.Element("poly")
 
         if action == "del":
-            elem.set( "action", action )
-            elem.set( "outline", localOutline )
+            elem.set("action", action)
+            elem.set("outline", localOutline)
             if localOutline == 'points':
-                list = self.points_to_elements( self.outline )
+                list = self.points_to_elements(self.outline)
                 for p in list:
-                    elem.append( p )
+                    elem.append(p)
             return ET.tostring(elem)
 
-        elem.set( "action", action )
+        elem.set("action", action)
         if  localOutline != None:
-            elem.set( "outline", localOutline )
+            elem.set("outline", localOutline)
             if localOutline == 'points':
-                list = self.points_to_elements( self.outline )
+                list = self.points_to_elements(self.outline)
                 for p in list:
-                    elem.append( p )
+                    elem.append(p)
 
         return ET.tostring(elem)
 
+fog_stipple_xpm = '''/* XPM */'
+static char * hatch_xpm[] = {
+"8 8 2 1",
+" 	c #000000",
+".	c #FFFFFF",
+" .... ..",
+". .... .",
+".. .... ",
+"... ....",
+".... ...",
+" .... ..",
+". .... .",
+".. .... "};
+'''
+
 class fog_layer(layer_base):
     def __init__(self, canvas):
         self.canvas = canvas
@@ -93,6 +109,9 @@
         self.use_fog = False
         self.last_role = ""
 
+        img = wx.ImageFromStream(StringIO.StringIO(fog_stipple_xpm), wx.BITMAP_TYPE_XPM)
+        self.stipple_bmp = img.ConvertToBitmap()
+
     def clear(self):
         self.fogregion.Clear()
         self.use_fog = True
@@ -120,33 +139,37 @@
             return
 
         size = self.canvas.size
-        self.width = size[0]/COURSE+1
-        self.height = size[1]/COURSE+1
-        self.fog_bmp = wx.EmptyBitmap(self.width+2,self.height+2)
+        self.width = size[0]
+        self.height = size[1]
+        self.fog_bmp = wx.EmptyBitmap(self.width,self.height)
         self.fill_fog()
 
     def fill_fog(self):
         if not self.use_fog:
             return
 
-        if "__WXGTK__" in wx.PlatformInfo:
-            mdc = wx.MemoryDC()
-            mdc.SelectObject(self.fog_bmp)
-            mdc.SetPen(wx.TRANSPARENT_PEN)
-            if (self.canvas.frame.session.role == "GM"):
-                color = self.color
-            else:
-                color = wx.BLACK
-            self.last_role = self.canvas.frame.session.role
-            mdc.SetBrush(wx.Brush(color,wx.SOLID))
-            mdc.DestroyClippingRegion()
-            mdc.DrawRectangle(0, 0, self.width+2, self.height+2)
-            mdc.SetBrush(wx.Brush(wx.WHITE, wx.SOLID))
-            if self.fogregion.GetBox().GetWidth()>0:
-                mdc.SetClippingRegionAsRegion(self.fogregion)
-                mdc.DrawRectangle(0, 0, self.width+2, self.height+2)
-            mdc.SelectObject(wx.NullBitmap)
-            del mdc
+        mdc = wx.MemoryDC()
+        mdc.SelectObject(self.fog_bmp)
+        mdc.SetPen(wx.TRANSPARENT_PEN)
+
+        if self.canvas.frame.session.role == "GM":
+            brush = wx.Brush(wx.BLACK, wx.STIPPLE)
+            brush.SetStipple(self.stipple_bmp)
+        else:
+            brush = wx.Brush(self.color, wx.SOLID)
+            print str(self.color)
+        self.last_role = self.canvas.frame.session.role
+
+        mdc.SetBrush(brush)
+        mdc.DrawRectangle(0, 0, self.width, self.height)
+        mdc.SetBrush(wx.Brush(wx.WHITE, wx.SOLID))
+        if self.fogregion.GetBox().GetWidth()>0:
+            mdc.SetClippingRegionAsRegion(self.fogregion)
+            mdc.DrawRectangle(0, 0, self.width, self.height)
+        mdc.SelectObject(wx.NullBitmap)
+        del mdc
+        self.fog_bmp.SetMaskColour(wx.WHITE)
+
 
     def layerDraw(self, dc, topleft, size):
         if self.fog_bmp == None or not self.fog_bmp.Ok() or not self.use_fog:
@@ -155,44 +178,12 @@
         if self.last_role != self.canvas.frame.session.role:
             self.fill_fog()
 
-        if "__WXGTK__" not in wx.PlatformInfo:
-            gc = wx.GraphicsContext.Create(dc)
-            gc.SetBrush(wx.Brush(wx.BLACK))
-            if (self.canvas.frame.session.role == "GM"):
-                gc.SetBrush(wx.Brush(self.color))
-            rgn = wx.Region(0, 0, self.canvas.size[0]+2, self.canvas.size[1]+2)
-            if not self.fogregion.IsEmpty():
-                rgn.SubtractRegion(self.fogregion)
-            gc.ClipRegion(rgn)
-            gc.DrawRectangle(0, 0, self.canvas.size[0]+2, self.canvas.size[1]+2)
-        else:
-            sc = dc.GetUserScale()
-            bmp = wx.EmptyBitmap(size[0],size[1])
-            mdc = wx.MemoryDC()
-            mdc.BeginDrawing()
-            mdc.SelectObject(bmp)
-            mdc.SetPen(wx.TRANSPARENT_PEN)
-            mdc.SetBrush(wx.Brush(wx.WHITE, wx.SOLID))
-            mdc.DrawRectangle(0,0,size[0],size[1])
-            srct = [int(topleft[0]/(sc[0]*COURSE)), int(topleft[1]/(sc[1]*COURSE))]
-            srcsz = [int((int(size[0]/COURSE+1)*COURSE)/(sc[0]*COURSE))+2, int((int(size[1]/COURSE+1)*COURSE)/(sc[1]*COURSE))+2]
-            if (srct[0]+srcsz[0] > self.width):
-                srcsz[0] = self.width-srct[0]
-            if (srct[1]+srcsz[1] > self.height):
-                srcsz[1] = self.height-srct[1]
-            img = wx.ImageFromBitmap(self.fog_bmp).GetSubImage(wx.Rect(srct[0], srct[1], srcsz[0], srcsz[1]))
-            img.Rescale(srcsz[0]*COURSE*sc[0], srcsz[1]*COURSE*sc[1])
-            fog = wx.BitmapFromImage(img)
-            mdc.SetDeviceOrigin(-topleft[0], -topleft[1])
-            mdc.DrawBitmap(fog, srct[0]*COURSE*sc[0], srct[1]*COURSE*sc[1])
-            mdc.SetDeviceOrigin(0,0)
-            mdc.SetUserScale(1,1)
-            mdc.EndDrawing()
-            dc.SetUserScale(1,1)
-            dc.Blit(topleft[0], topleft[1], size[0], size[1], mdc,0,0,wx.AND)
-            dc.SetUserScale(sc[0],sc[1])
-            mdc.SelectObject(wx.NullBitmap)
-            del mdc
+        mdc = wx.MemoryDC()
+        mdc.SelectObject(self.fog_bmp)
+        dc.Blit(0, 0, self.canvas.size[0], self.canvas.size[1], mdc, 0, 0, wx.COPY, True)
+        mdc.SelectObject(wx.NullBitmap)
+        del mdc
+
 
     def createregn2(self, polyline, mode, show):
         regn = self.scanConvert(polyline)
@@ -211,7 +202,7 @@
             if not self.fogregion.IsEmpty():
                 self.fogregion.SubtractRegion(regn)
             else:
-                self.fogregion = wx.Region(0, 0, self.canvas.size[0]+2, self.canvas.size[1]+2)
+                self.fogregion = wx.Region(0, 0, self.canvas.size[0], self.canvas.size[1])
                 self.fogregion.SubtractRegion(regn)
             self.del_area(area, show)
 
@@ -229,15 +220,9 @@
         list = IRegion().scan_Convert(polypt)
         for i in list:
             if regn.IsEmpty():
-                if "__WXGTK__" not in wx.PlatformInfo:
-                    regn = wx.Region(i.left*COURSE, i.y*COURSE, i.right*COURSE+1-i.left*COURSE, 1*COURSE)
-                else:
-                    regn = wx.Region(i.left, i.y, i.right+1-i.left, 1)
+                regn = wx.Region(i.left*COURSE, i.y*COURSE, i.right*COURSE+1-i.left*COURSE, 1*COURSE)
             else:
-                if "__WXGTK__" not in wx.PlatformInfo:
-                    regn.Union(i.left*COURSE, i.y*COURSE, i.right*COURSE+1-i.left*COURSE, 1*COURSE)
-                else:
-                    regn.Union(i.left, i.y, i.right+1-i.left, 1)
+                regn.Union(i.left*COURSE, i.y*COURSE, i.right*COURSE+1-i.left*COURSE, 1*COURSE)
         return regn
 
     def add_area(self, area="", show="Yes"):
@@ -266,16 +251,10 @@
             fog_string = FogArea("all").toxml("del")
 
         while ri.HaveRects():
-            if "__WXGTK__" not in wx.PlatformInfo:
-                x1 = ri.GetX()/COURSE
-                x2 = x1+(ri.GetW()/COURSE)-1
-                y1 = ri.GetY()/COURSE
-                y2 = y1+(ri.GetH()/COURSE)-1
-            else:
-                x1 = ri.GetX()
-                x2 = x1+ri.GetW()-1
-                y1 = ri.GetY()
-                y2 = y1+ri.GetH()-1
+            x1 = ri.GetX()/COURSE
+            x2 = x1+(ri.GetW()/COURSE)-1
+            y1 = ri.GetY()/COURSE
+            y2 = y1+(ri.GetH()/COURSE)-1
 
             poly = FogArea(str(x1) + "," + str(y1) + ";" +
                           str(x2) + "," + str(y1) + ";" +
@@ -318,8 +297,8 @@
                     lastx = None
                     lasty = None
                     for point in l:
-                        x = point.get( "x" )
-                        y = point.get( "y" )
+                        x = point.get("x")
+                        y = point.get("y")
                         if (x != lastx or y != lasty):
                             polyline.append(IPoint().make(int(x), int(y)))
                         lastx = x
@@ -329,4 +308,5 @@
                     self.createregn2(polyline, action, "No")
 
             self.fill_fog()
-        except: pass
+        except Exception:
+            logger.general(traceback.format_exc())
--- a/orpg/mapper/fog_handler.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/fog_handler.py	Mon Mar 22 18:38:22 2010 -0600
@@ -49,8 +49,8 @@
         self.fogshow = wx.RadioButton(self, wx.ID_ANY, "Show", style=wx.RB_GROUP)
         self.foghide = wx.RadioButton(self, wx.ID_ANY, "Hide")
 
-        self.sizer.Add(self.foghide)
-        self.sizer.Add(self.fogshow)
+        self.sizer.Add(self.foghide, 0, wx.ALIGN_CENTER)
+        self.sizer.Add(self.fogshow, 0, wx.ALIGN_CENTER)
         self.sizer.Add(wx.Size(20,25),1)
 
 
--- a/orpg/mapper/fog_msg.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/fog_msg.py	Mon Mar 22 18:38:22 2010 -0600
@@ -39,20 +39,21 @@
         self.fogregion.Clear()
 
     def get_line(self,outline,action,output_act):
-        elem = ET.Element( "poly" )
-        if ( output_act ):
-            elem.set( "action", action )
-        if ( outline == 'all' ) or ( outline == 'none' ):
-            elem.set( "outline", outline )
+        elem = ET.Element("poly")
+        if output_act:
+            elem.set("action", action)
+
+        if outline == 'all' or outline == 'none':
+            elem.set("outline", outline)
         else:
-            elem.set( "outline", "points" )
-            for pair in string.split( outline, ";" ):
-                p = string.split( pair, "," )
-                point = ET.Element( "point" )
-                point.set( "x", p[ 0 ] )
-                point.set( "y", p[ 1 ] )
-                elem.append( point )
-        return elem.tostring()
+            elem.set("outline", "points")
+            for pair in string.split(outline, ";"):
+                p = string.split(pair, ",")
+                point = ET.Element("point")
+                point.set("x", p[ 0 ])
+                point.set("y", p[ 1 ])
+                elem.append(point)
+        return ET.tostring(elem)
 
     # convenience method to use if only this line is modified
     #   outputs a <map/> element containing only the changes to this line
@@ -115,7 +116,7 @@
             else:
                 polyline=[]
                 for node in l:
-                    polyline.append( IPoint().make( int(node.get("x")), int(node.get("y")) ) )
+                    polyline.append(IPoint().make(int(node.get("x")), int(node.get("y"))))
                 # pointarray = outline.split(";")
                 # for m in range(len(pointarray)):
                 #     pt=pointarray[m].split(",")
--- a/orpg/mapper/grid.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/grid.py	Mon Mar 22 18:38:22 2010 -0600
@@ -110,6 +110,35 @@
         else:
             return cmpPoint(int(pos.x),int(pos.y))
 
+    def get_snapped_to_list_of_positions(self, x, y, mini_width, mini_height, count, snap_to_align=SNAPTO_ALIGN_CENTER):
+        pos = cmpPoint(int(x), int(y))
+        pos = self.get_snapped_to_pos(pos, snap_to_align, mini_width, mini_height)
+        list_pos = []
+        col = 0
+        row = 0
+        # take into account size of image and map configuration
+        unit_height = 1#int(mini_height / self.unit_size)
+        unit_width = 1#int(mini_width / self.unit_size_y)
+        if self.mode == GRID_RECTANGLE:
+            col_vector = cmpPoint(self.unit_size*unit_width, 0)
+            row_vector = cmpPoint(0, self.unit_size*unit_height)
+        elif self.mode == GRID_ISOMETRIC:
+            height = unit_height*self.unit_size*self.size_ratio/self.iso_ratio
+            width = unit_width*self.unit_size*self.size_ratio
+            col_vector = cmpPoint(width/2, height/2)
+            row_vector = cmpPoint(-width/2, height/2)
+        else: # self.mode == GRID_HEXAGON
+            width = int(1.5*unit_width*self.unit_size/1.75)
+            col_vector = cmpPoint(width, self.unit_size*unit_height/2)
+            row_vector = cmpPoint(0, self.unit_size*unit_height)
+        for i in range(count):
+            list_pos.append(cmpPoint(pos.x + col_vector.x*col + row_vector.x*row, pos.y + col_vector.y*col + row_vector.y*row))
+            col += 1
+            if col >= sqrt(count):
+                col = 0
+                row += 1
+        return list_pos
+
     def set_rect_mode(self):
         "switch grid to rectangular mode"
         self.mode = GRID_RECTANGLE
--- a/orpg/mapper/images.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/images.py	Mon Mar 22 18:38:22 2010 -0600
@@ -25,7 +25,7 @@
 #
 # Description:
 #
-__version__ = "$Id: images.py,v 1.21 2007/12/11 04:07:15 digitalxero Exp $"
+from __future__ import with_statement
 
 import urllib
 import Queue
@@ -55,25 +55,24 @@
     def load(self, path, image_type, imageId):
         # Load an image, with a intermideary fetching image shown while it loads in a background thread
         if self.__cache.has_key(path):
-            return wx.ImageFromMime(self.__cache[path][1],
-                                    self.__cache[path][2]).ConvertToBitmap()
-        if not self.__fetching.has_key(path):
+            return wx.ImageFromMime(self.__cache[path][1], self.__cache[path][2])
+        if path not in self.__fetching:
             self.__fetching[path] = True
             #Start Image Loading Thread
             thread.start_new_thread(self.__loadThread,
                                     (path, image_type, imageId))
         else:
-            if self.__fetching[path] is True:
+            if self.__fetching[path]:
                 thread.start_new_thread(self.__loadCacheThread,
                                         (path, image_type, imageId))
-        return wx.Bitmap(dir_struct["icon"] + "fetching.png",
-                         wx.BITMAP_TYPE_PNG)
+        return wx.Image(dir_struct["icon"] + "fetching.png",
+                        wx.BITMAP_TYPE_PNG)
 
     def directLoad(self, path):
         # Directly load an image, no threads
-        if self.__cache.has_key(path):
+        if path in self.__cache:
             return wx.ImageFromMime(self.__cache[path][1],
-                                    self.__cache[path][2]).ConvertToBitmap()
+                                    self.__cache[path][2])
 
         uriPath = urllib.unquote(path)
         try:
@@ -81,9 +80,10 @@
             # We have to make sure that not only did we fetch something, but that
             # it was an image that we got back.
             if d[0] and d[1].getmaintype() == "image":
-                self.__cache[path] = (path, d[0], d[1].gettype(), None)
+                with self.__lock:
+                    self.__cache[path] = (path, d[0], d[1].gettype(), None)
                 return wx.ImageFromMime(self.__cache[path][1],
-                                        self.__cache[path][2]).ConvertToBitmap()
+                                        self.__cache[path][2])
             else:
                 logger.general("Image refused to load or URI did not "
                                "reference a valid image: " + path, True)
@@ -99,34 +99,34 @@
             cacheSize = int(settings.get("ImageCacheSize"))
         except:
             cacheSize = 32
-        cache = self.__cache.keys()
-        cache.sort()
-        for key in cache[cacheSize:]:
-            del self.__cache[key]
+        with self.__lock:
+            cache = self.__cache.keys()
+            cache.sort()
+            for key in cache[cacheSize:]:
+                del self.__cache[key]
 
     def flushCache(self):
         # This function will flush all images contained within the image cache.
-        self.__lock.acquire()
-        try:
+        with self.__lock:
             self.__cache = {}
             self.__fetching = {}
-        finally:
-            self.__lock.release()
             urllib.urlcleanup()
 
 #Private Methods
     def __loadThread(self, path, image_type, imageId):
         uriPath = urllib.unquote(path)
-        self.__lock.acquire()
+
         try:
             d = urllib.urlretrieve(uriPath)
             # We have to make sure that not only did we fetch something, but that
             # it was an image that we got back.
             if d[0] and d[1].getmaintype() == "image":
-                self.__cache[path] = (path, d[0], d[1].gettype(), imageId)
-                self.__queue.put((self.__cache[path], image_type, imageId))
-                if self.__fetching.has_key(path):
-                    del self.__fetching[path]
+                with self.__lock:
+                    self.__cache[path] = (path, d[0], d[1].gettype(), imageId)
+                    self.__queue.put((self.__cache[path], image_type, imageId))
+
+                    if path in self.__fetching:
+                        del self.__fetching[path]
             else:
                 logger.general("Image refused to load or URI did not "
                                "reference a valid image: " + path, True)
@@ -135,28 +135,28 @@
             self.__fetching[path] = False
             logger.general("Unable to resolve/open the specified URI; "
                            "image was NOT laoded: " + path, True)
-        finally:
-            self.__lock.release()
 
     def __loadCacheThread(self, path, image_type, imageId):
         try:
             st = time.time()
-            while self.__fetching.has_key(path) and self.__fetching[path] is not False:
+            while path in self.__fetching and self.__fetching[path] is not False:
                 time.sleep(0.025)
                 if (time.time()-st) > 120:
                     logger.general("Timeout: " + path, True)
+                    self.__fetching[path] = False
                     break
         except:
             self.__fetching[path] = False
             logger.general("Unable to resolve/open the specified URI; "
                            "image was NOT loaded: " + path, True)
             return
-        self.__lock.acquire()
-        try:
-            logger.debug("Adding Image to Queue from Cache: " + str(self.__cache[path]))
-            self.__queue.put((self.__cache[path], image_type, imageId))
-        finally:
-            self.__lock.release()
+
+        with self.__lock:
+            if path in self.__cache:
+                logger.debug("Adding Image to Queue from Cache: " + str(self.__cache[path]))
+                self.__queue.put((self.__cache[path], image_type, imageId))
+            else:
+                self.__loadThread(path, image_type, imageId)
 
     #Property Methods
     def _getCache(self):
--- a/orpg/mapper/map.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/map.py	Mon Mar 22 18:38:22 2010 -0600
@@ -67,6 +67,7 @@
         wx.ScrolledWindow.__init__(self, parent, ID,
                                    style=wx.HSCROLL|wx.VSCROLL|
                                    wx.FULL_REPAINT_ON_RESIZE|wx.SUNKEN_BORDER)
+        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
         self.frame = parent
         self.MAP_MODE = 1      #Mode 1 = MINI, 2 = DRAW, 3 = TAPE MEASURE
         self.layers = {}
@@ -166,18 +167,16 @@
 
         if not ImageHandler.Queue.empty():
             (path, image_type, imageId) = ImageHandler.Queue.get()
-            img = wx.ImageFromMime(path[1], path[2]).ConvertToBitmap()
-            try:
-                # Now, apply the image to the proper object
-                if image_type == "miniature":
-                    min = self.layers['miniatures'].get_miniature_by_id(imageId)
-                    min.set_bmp(img)
-                elif image_type == "background" or image_type == "texture":
-                    self.layers['bg'].bg_bmp = img
-                    if image_type == "background":
-                        self.set_size([img.GetWidth(), img.GetHeight()])
-            except:
-                pass
+            img = wx.ImageFromMime(path[1], path[2])
+            # Now, apply the image to the proper object
+            if image_type == "miniature":
+                min = self.layers['miniatures'].get_miniature_by_id(imageId)
+                if min:
+                    min.set_image(img)
+            elif image_type == "background" or image_type == "texture":
+                self.layers['bg'].bg_bmp = img.ConvertToBitmap()
+                if image_type == "background":
+                    self.set_size([img.GetWidth(), img.GetHeight()])
             # Flag that we now need to refresh!
             self.requireRefresh += 1
 
@@ -385,22 +384,15 @@
         """Calculate the distance between two pixels and returns it.  The calculated
         distance is the Euclidean Distance, which is:
         d = sqrt((x2 - x1)**2 + (y2 - y1)**2)"""
-        d = sqrt(abs((stop.x - start.x)**2 - (stop.y - start.y)**2))
+        d = sqrt((stop.x - start.x)**2 + (stop.y - start.y)**2)
         return d
 
-    def calcUnitDistance(self, start, stop, lineAngle):
+    def calcUnitDistance(self, start, stop):
         distance = self.calcPixelDistance(start, stop)
-        ln = "%0.2f" % lineAngle
         if self.layers['grid'].mode == GRID_HEXAGON:
-            if ln == "0.00" or ln == "359.99":
-                ud = distance / self.layers['grid'].unit_size_y
-            else:
-                ud = (sqrt(abs((stop.x - start.x)**2 + (stop.y - start.y)**2))) / self.layers['grid'].unit_size_y
+            ud = distance / self.layers['grid'].unit_size_y
         else:
-            if ln == "0.00" or ln == "359.99":
-                ud = distance / self.layers['grid'].unit_size
-            else:
-                ud = (sqrt(abs((stop.x - start.x)**2 + (stop.y - start.y)**2))) / self.layers['grid'].unit_size
+            ud = distance / self.layers['grid'].unit_size
         return ud
 
     def on_tape_motion(self, evt):
@@ -480,7 +472,7 @@
             # the text to match the line angle, calculate the angle
             # of the line.
             lineAngle = self.calcLineAngle(self.markerStart, self.markerStop)
-            distance = self.calcUnitDistance(self.markerStart, self.markerStop, lineAngle)
+            distance = self.calcUnitDistance(self.markerStart, self.markerStop)
             midPoint = (self.markerStart + self.markerStop)
             midPoint.x /= 2
             midPoint.y /= 2
@@ -606,9 +598,8 @@
 
     def on_prop(self, evt):
         self.session = open_rpg.get_component("session")
-        self.chat = open_rpg.get_component("chat")
         if (self.session.my_role() != self.session.ROLE_GM):
-            self.chat.InfoPost("You must be a GM to use this feature")
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
             return
         dlg = general_map_prop_dialog(self.frame.GetParent(),self.size,self.layers['bg'],self.layers['grid'])
         if dlg.ShowModal() == wx.ID_OK:
@@ -741,7 +732,7 @@
             self.map_version = MAP_VERSION
             self.Refresh(False)
         except:
-            print traceback.format_exc()
+            logger.general(traceback.format_exc())
 
     def re_ids_in_xml(self, xmlString):
         new_xml = ""
@@ -784,7 +775,6 @@
         wx.Panel.__init__(self, parent, id)
         self.canvas = MapCanvas(self, wx.ID_ANY)
         self.session = open_rpg.get_component('session')
-        self.chat = open_rpg.get_component('chat')
         self.top_frame = open_rpg.get_component('frame')
         self.root_dir = os.getcwd()
         self.current_layer = 2
@@ -803,15 +793,16 @@
         self.layer_handlers.append(map_handler(self.layer_tabs,-1,self.canvas))
         self.layer_tabs.AddPage(self.layer_handlers[5],"General")
         self.layer_tabs.SetSelection(2)
+
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.sizer.Add(self.canvas, 1, wx.EXPAND)
+        self.sizer.Add(self.layer_tabs, 0, wx.EXPAND)
+        self.SetSizerAndFit(self.sizer)
+
         self.Bind(FNB.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.on_layer_change)
-        self.Bind(wx.EVT_SIZE, self.on_size)
+        #self.Bind(wx.EVT_SIZE, self.on_size)
         self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
         self.load_default()
-        # size of tabs is diffrent on windows and linux :(
-        if wx.Platform == '__WXMSW__':
-            self.toolbar_height = 50
-        else:
-            self.toolbar_height = 55
 
     def OnLeave(self, evt):
         if "__WXGTK__" in wx.PlatformInfo:
@@ -819,7 +810,7 @@
 
     def load_default(self):
         if self.session.is_connected() and (self.session.my_role() != self.session.ROLE_GM) and (self.session.use_roles()):
-            self.chat.InfoPost("You must be a GM to use this feature")
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
             return
         with open(dir_struct["template"] + "default_map.xml") as f:
             self.new_data(f.read())
@@ -837,7 +828,7 @@
 
     def on_save(self,evt):
         if (self.session.my_role() != self.session.ROLE_GM):
-            self.chat.InfoPost("You must be a GM to use this feature")
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
 
             return
         d = wx.FileDialog(self.GetParent(), "Save map data", dir_struct["user"], "", "Map Files (*.xml)|*.xml", wx.SAVE)
@@ -860,7 +851,7 @@
     def on_open(self, evt):
         ext = "XML files (*.xml)|*.xml|All files (*.*)|*.*"
         if self.session.is_connected() and (self.session.my_role() != self.session.ROLE_GM) and (self.session.use_roles()):
-            self.chat.InfoPost("You must be a GM to use this feature")
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
             return
 
         d = wx.FileDialog(self.GetParent(), "Select a file", dir_struct["user"], "", ext, wx.OPEN)
@@ -1001,6 +992,7 @@
         return []
 
     def on_size(self, evt):
-        s = self.GetClientSizeTuple()
+        """s = self.GetClientSizeTuple()
         self.canvas.SetDimensions(0,0,s[0],s[1]-self.toolbar_height)
-        self.layer_tabs.SetDimensions(0,s[1]-self.toolbar_height,s[0],self.toolbar_height)
+        self.layer_tabs.SetDimensions(0,s[1]-self.toolbar_height,s[0],self.toolbar_height)"""
+        pass
--- a/orpg/mapper/map_handler.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/map_handler.py	Mon Mar 22 18:38:22 2010 -0600
@@ -34,21 +34,21 @@
 
     def build_ctrls(self):
         base_layer_handler.build_ctrls(self)
-        self.width = wx.TextCtrl(self, wx.ID_ANY, size=(75,25))
-        self.height = wx.TextCtrl(self, wx.ID_ANY, size=(75,25))
+        self.width = wx.TextCtrl(self, wx.ID_ANY)
+        self.height = wx.TextCtrl(self, wx.ID_ANY)
         self.apply_button = wx.Button(self, wx.ID_OK, "Apply", style=wx.BU_EXACTFIT)
         self.load_default = wx.Button(self, wx.ID_ANY, "Default Map", style=wx.BU_EXACTFIT)
-        self.sizer.Prepend(wx.Size(20,25),1)
-        self.sizer.Prepend(self.load_default, 0, wx.EXPAND)
-        self.sizer.Prepend(wx.Size(20,25))
-        self.sizer.Prepend(self.apply_button, 0, wx.EXPAND)
-        self.sizer.Prepend(wx.Size(20,25))
-        self.sizer.Prepend(self.height, 0, wx.EXPAND)
-        self.sizer.Prepend(wx.StaticText(self, -1, "Height: "),0,wx.ALIGN_CENTER)
-        self.sizer.Prepend(wx.Size(10,25))
-        self.sizer.Prepend(self.width, 0, wx.EXPAND)
-        self.sizer.Prepend(wx.StaticText(self, -1, "Width: "),0,wx.ALIGN_CENTER)
-        self.sizer.Prepend(wx.Size(10,25))
+
+        self.sizer.Add(wx.StaticText(self, -1, "Width: "),0, wx.ALIGN_CENTER)
+        self.sizer.Add(self.width, 0, wx.ALIGN_CENTER)
+        self.sizer.Add((6, 0))
+        self.sizer.Add(wx.StaticText(self, -1, "Height: "),0, wx.ALIGN_CENTER)
+        self.sizer.Add(self.height, 0, wx.ALIGN_CENTER)
+        self.sizer.Add((6, 0))
+        self.sizer.Add(self.apply_button, 0, wx.ALIGN_CENTER)
+        self.sizer.Add((6, 0))
+        self.sizer.Add(self.load_default, 0, wx.ALIGN_CENTER)
+
         self.Bind(wx.EVT_BUTTON, self.on_apply, self.apply_button)
         self.Bind(wx.EVT_BUTTON, self.on_load_default, self.load_default)
         self.update_info()
--- a/orpg/mapper/map_msg.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/map_msg.py	Mon Mar 22 18:38:22 2010 -0600
@@ -25,7 +25,8 @@
 #
 # Description:
 #
-__version__ = "$Id: map_msg.py,v 1.16 2007/03/09 14:11:55 digitalxero Exp $"
+
+from __future__ import with_statement
 
 #from base import *
 from base_msg import *
@@ -53,78 +54,87 @@
         map_element_msg_base.__init__(self,reentrant_lock_object)
 
     def init_from_dom(self,xml):
-        self.p_lock.acquire()
-        if xml.tag == self.tagname:
-            # If this is a map message, look for the "action=new"
-            # Notice we only do this when the root is a map tag
-            if self.tagname == "map" and 'action' in xml.attrib and xml.get("action") == "new":
-                self.clear()
-            # Process all of the properties in each tag
-            if xml.keys():
-                for k in xml.keys():
-                    self.init_prop(k,xml.get(k))
-            for c in xml:
-                name = c.tag
-                if not self.children.has_key(name):
-                    if name == "miniatures":
-                        self.children[name] = minis_msg(self.p_lock)
-                    elif name == "grid":
-                        self.children[name] = grid_msg(self.p_lock)
-                    elif name == "bg":
-                        self.children[name] = bg_msg(self.p_lock)
-                    elif name == "whiteboard":
-                        self.children[name] = whiteboard_msg(self.p_lock)
-                    elif name == "fog":
-                        self.children[name] = fog_msg(self.p_lock)
-                    else:
-                        print "Unrecognized tag " + name + " found in map_msg.init_from_dom - skipping"
+        with self.p_lock:
+            if xml.tag == self.tagname:
+
+                # If this is a map message, look for the "action=new"
+                # Notice we only do this when the root is a map tag
+                if self.tagname == "map" and 'action' in xml.attrib and xml.get("action") == "new":
+                    self.clear()
+
+                # Process all of the properties in each tag
+                if xml.keys():
+                    for k in xml.keys():
+                        self.init_prop(k,xml.get(k))
+                for c in xml:
+                    name = c.tag
+
+
+                    if not self.children.has_key(name):
+                        if name == "miniatures":
+                            self.children[name] = minis_msg(self.p_lock)
+
+                        elif name == "grid":
+                            self.children[name] = grid_msg(self.p_lock)
+                        elif name == "bg":
+                            self.children[name] = bg_msg(self.p_lock)
+                        elif name == "whiteboard":
+                            self.children[name] = whiteboard_msg(self.p_lock)
+                        elif name == "fog":
+                            self.children[name] = fog_msg(self.p_lock)
+                        else:
+                            print "Unrecognized tag " + name + " found in map_msg.init_from_dom - skipping"
+                            continue
+
+                    try:
+                        self.children[name].init_from_dom(c)
+
+                    except Exception, e:
+                        print "map_msg.init_from_dom() exception: "+str(e)
                         continue
-                try:
-                    self.children[name].init_from_dom(c)
-                except Exception, e:
-                    print "map_msg.init_from_dom() exception: "+str(e)
-                    continue
-        else:
-            self.p_lock.release()
-            raise Exception, "Error attempting to initialize a " + self.tagname + " from a non-<" + self.tagname + "/> element"
-        self.p_lock.release()
+
+            else:
+                raise Exception, "Error attempting to initialize a " + self.tagname + " from a non-<" + self.tagname + "/> element"
 
     def set_from_dom(self,xml):
-        self.p_lock.acquire()
-        if xml.tag == self.tagname:
-            # If this is a map message, look for the "action=new"
-            # Notice we only do this when the root is a map tag
-            if self.tagname == "map" and 'action' in xml.attrib and xml.get("action") == "new":
-                self.clear()
-            # Process all of the properties in each tag
-            if xml.keys():
-                for k in xml.keys():
-                    self.set_prop(k,xml.get(k))
-            for c in xml:
-                name = c.tag
-                if not self.children.has_key(name):
-                    if name == "miniatures":
-                        self.children[name] = minis_msg(self.p_lock)
-                    elif name == "grid":
-                        self.children[name] = grid_msg(self.p_lock)
-                    elif name == "bg":
-                        self.children[name] = bg_msg(self.p_lock)
-                    elif name == "whiteboard":
-                        self.children[name] = whiteboard_msg(self.p_lock)
-                    elif name == "fog":
-                        self.children[name] = fog_msg(self.p_lock)
-                    else:
-                        print "Unrecognized tag " + name + " found in map_msg.init_from_dom - skipping"
+        with self.p_lock:
+            if xml.tag == self.tagname:
+
+                # If this is a map message, look for the "action=new"
+                # Notice we only do this when the root is a map tag
+                if self.tagname == "map" and 'action' in xml.attrib and xml.get("action") == "new":
+                    self.clear()
+
+                # Process all of the properties in each tag
+                if xml.keys():
+                    for k in xml.keys():
+                        self.set_prop(k,xml.get(k))
+                for c in xml:
+                    name = c.tag
+
+
+                    if not self.children.has_key(name):
+                        if name == "miniatures":
+                            self.children[name] = minis_msg(self.p_lock)
+
+                        elif name == "grid":
+                            self.children[name] = grid_msg(self.p_lock)
+                        elif name == "bg":
+                            self.children[name] = bg_msg(self.p_lock)
+                        elif name == "whiteboard":
+                            self.children[name] = whiteboard_msg(self.p_lock)
+                        elif name == "fog":
+                            self.children[name] = fog_msg(self.p_lock)
+                        else:
+                            print "Unrecognized tag " + name + " found in map_msg.init_from_dom - skipping"
+                            continue
+                    try:
+                        self.children[name].set_from_dom(c)
+                    except Exception, e:
+                        print "map_msg.set_from_dom() exception: "+str(e)
                         continue
-                try:
-                    self.children[name].set_from_dom(c)
-                except Exception, e:
-                    print "map_msg.set_from_dom() exception: "+str(e)
-                    continue
-        else:
-            self.p_lock.release()
-            raise Exception, "Error attempting to set a " + self.tagname + " from a non-<" + self.tagname + "/> element in map"
-        self.p_lock.release()
+            else:
+                raise Exception, "Error attempting to set a " + self.tagname + " from a non-<" + self.tagname + "/> element in map"
 
     def get_all_xml(self, action="new", output_action=1):
         return map_element_msg_base.get_all_xml(self, action, output_action)
--- a/orpg/mapper/map_utils.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/map_utils.py	Mon Mar 22 18:38:22 2010 -0600
@@ -11,11 +11,11 @@
 # distance_between()
 # Returns the distance between two points
 #-----------------------------------------------------------------------
-def distance_between( x1, y1, x2, y2 ):
-    "Returns the distance between two points"
+def distance_between(x1, y1, x2, y2):
+    """Returns the distance between two points"""
     dx = x2 - x1
     dy = y2 - y1
-    return math.sqrt( dx*dx + dy*dy )
+    return math.sqrt(dx*dx + dy*dy)
 
 #-----------------------------------------------------------------------
 # proximity_test()
@@ -36,6 +36,7 @@
     x1,y1 = start_point
     x2,y2 = end_point
     xt,yt = test_point
+
     x1 = float(x1)
     x2 = float(x2)
     y1 = float(y1)
--- a/orpg/mapper/map_version.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/map_version.py	Mon Mar 22 18:38:22 2010 -0600
@@ -1,3 +1,2 @@
-## this gile old the map version ###
-
-MAP_VERSION = "2.1"
+# Version of the map protocol/file format.
+MAP_VERSION = '2.2'
--- a/orpg/mapper/miniatures.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/miniatures.py	Mon Mar 22 18:38:22 2010 -0600
@@ -74,8 +74,50 @@
         value = 1
     return value
 
+mini_draw_callback_list = []
+mini_additional_attribute_list = []
+
+def register_mini_draw_callback_function(func):
+    if func in mini_draw_callback_list:
+        mini_draw_callback_list.remove(func)
+    else:
+        mini_draw_callback_list.append(func)
+
+def handle_mini_draw_callback_functions(mini, dc):
+    for func in mini_draw_callback_list:
+        func(mini, dc)
+
+##def assign_xml_attributes_to_object(obj, xml, attribute_map):
+##    for attribute, attribute_type in attribute_map.items():
+##        if attribute in xml.attrib:
+##            value = xml.get(attribute)
+##            if attribute_type == 'int':
+##                value = int(xml.get(attribute))
+##            elif attribute_type == 'bool':
+##                if value == '1' or value == 'True':
+##                    value = True
+##                else:
+##                    value = False
+##            elif attribute_type == 'url':
+##                value = urllib.unquote(value)
+##            obj.__setattr__(attribute, value)
+##
+##def assign_obj_attributes_to_xml(obj, xml, attribute_map):
+##    for attribute, attribute_type in attribute_map.items():
+##        if attribute in obj.__dict__ and obj.__dict__[attribute] is not None:
+##            if attribute_type == 'bool':
+##                if obj.__dict__[attribute]:
+##                    value = '1'
+##                else:
+##                    value = '0'
+##            elif attribute_type == 'url':
+##                value = urllib.quote(obj.__dict__[attribute]).replace('%3A', ':')
+##            else:
+##                value = str(obj.__dict__[attribute])
+##            xml.set(attribute, value)
+            
 class BmpMiniature:
-    def __init__(self, id,path, bmp, pos=cmpPoint(0,0), heading=FACE_NONE,
+    def __init__(self, id, path, image, pos=cmpPoint(0,0), heading=FACE_NONE,
                  face=FACE_NONE, label="", locked=False, hide=False,
                  snap_to_align=SNAPTO_ALIGN_CENTER, zorder=0, width=0,
                  height=0, local=False, localPath='', localTime=-1):
@@ -83,7 +125,6 @@
         self.face = face
         self.label = label
         self.path = path
-        self.bmp = bmp
         self.pos = pos
         self.selected = False
         self.locked = locked
@@ -103,18 +144,27 @@
             self.height = 0
         else:
             self.height = height
-        self.right = bmp.GetWidth()
+        self.right = image.GetWidth()
         self.top = 0
-        self.bottom = bmp.GetHeight()
+        self.bottom = image.GetHeight()
         self.isUpdated = False
         self.gray = False
 
-    def __del__(self):
-        del self.bmp
-        self.bmp = None
+        self.set_image(image)
+
+    def set_image(self, image):
+        self.image = image
+        self.image.ConvertAlphaToMask()
+        self.generate_bmps();
 
-    def set_bmp(self, bmp):
-        self.bmp = bmp
+    def generate_bmps(self):
+        if self.width:
+            image = self.image.Copy()
+            image.Rescale(int(self.width), int(self.height))
+        else:
+            image = self.image
+        self.bmp = image.ConvertToBitmap()
+        self.bmp_gray = image.ConvertToGreyscale().ConvertToBitmap()
 
     def set_min_props(self, heading=FACE_NONE, face=FACE_NONE, label="",
                       locked=False, hide=False, width=0, height=0):
@@ -132,6 +182,7 @@
         self.width = int(width)
         self.height = int(height)
         self.isUpdated = True
+        self.generate_bmps()
 
     def hit_test(self, pt):
         rect = self.get_rect()
@@ -147,234 +198,209 @@
         return ret
 
     def draw(self, dc, mini_layer, op=wx.COPY):
-        if isinstance(self.bmp, tuple):
-            self.bmp = wx.ImageFromMime(self.bmp[1], self.bmp[2]).ConvertToBitmap()
-        if self.bmp != None and self.bmp.Ok():
-            # check if hidden and GM: we outline the mini in grey (little bit smaller than the actual size)
-            # and write the label in the center of the mini
-            if self.hide and mini_layer.canvas.frame.session.my_role() == mini_layer.canvas.frame.session.ROLE_GM:
-                # set the width and height of the image
-                if self.width and self.height:
-                    tmp_image = self.bmp.ConvertToImage()
-                    tmp_image.Rescale(int(self.width), int(self.height))
-                    tmp_image.ConvertAlphaToMask()
-                    self.bmp = tmp_image.ConvertToBitmap()
-                    mask = wx.Mask(self.bmp, wx.Colour(tmp_image.GetMaskRed(), tmp_image.GetMaskGreen(), tmp_image.GetMaskBlue()))
-                    self.bmp.SetMask(mask)
-                    del tmp_image
-                    del mask
-                self.left = 0
-                self.right = self.bmp.GetWidth()
-                self.top = 0
-                self.bottom = self.bmp.GetHeight()
-                # grey outline
-                graypen = wx.Pen("gray", 1, wx.DOT)
-                dc.SetPen(graypen)
+        # check if hidden and GM: we outline the mini in grey (little bit smaller than the actual size)
+        # and write the label in the center of the mini
+        if self.hide and mini_layer.canvas.frame.session.my_role() == mini_layer.canvas.frame.session.ROLE_GM:
+            self.left = 0
+            self.right = self.bmp.GetWidth()
+            self.top = 0
+            self.bottom = self.bmp.GetHeight()
+            # grey outline
+            graypen = wx.Pen("gray", 1, wx.DOT)
+            dc.SetPen(graypen)
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+            if self.bmp.GetWidth() <= 20:
+                xoffset = 1
+            else:
+                xoffset = 5
+            if self.bmp.GetHeight() <= 20:
+                yoffset = 1
+            else:
+                yoffset = 5
+            dc.DrawRectangle(self.pos.x + xoffset, self.pos.y + yoffset, self.bmp.GetWidth() - (xoffset * 2), self.bmp.GetHeight() - (yoffset * 2))
+            dc.SetBrush(wx.NullBrush)
+            dc.SetPen(wx.NullPen)
+
+            ## draw label in the center of the mini
+            label = mini_layer.get_mini_label(self)
+            if len(label):
+                dc.SetTextForeground(wx.RED)
+                (textWidth,textHeight) = dc.GetTextExtent(label)
+                x = self.pos.x +((self.bmp.GetWidth() - textWidth) /2) - 1
+                y = self.pos.y + (self.bmp.GetHeight() / 2)
+                dc.SetPen(wx.GREY_PEN)
+                dc.SetBrush(wx.LIGHT_GREY_BRUSH)
+                dc.DrawRectangle(x, y, textWidth+2, textHeight+2)
+                if (textWidth+2 > self.right):
+                    self.right += int((textWidth+2-self.right)/2)+1
+                    self.left -= int((textWidth+2-self.right)/2)+1
+                self.bottom = y+textHeight+2-self.pos.y
+                dc.SetPen(wx.NullPen)
+                dc.SetBrush(wx.NullBrush)
+                dc.DrawText(label, x+1, y+1)
+
+            #selected outline
+            if self.selected:
+                dc.SetPen(wx.RED_PEN)
                 dc.SetBrush(wx.TRANSPARENT_BRUSH)
-                #if width or height < 20 then offset = 1
-                if self.bmp.GetWidth() <= 20:
-                    xoffset = 1
-                else:
-                    xoffset = 5
-                if self.bmp.GetHeight() <= 20:
-                    yoffset = 1
-                else:
-                    yoffset = 5
-                dc.DrawRectangle(self.pos.x + xoffset, self.pos.y + yoffset, self.bmp.GetWidth() - (xoffset * 2), self.bmp.GetHeight() - (yoffset * 2))
+                dc.DrawRectangle(self.pos.x, self.pos.y, self.bmp.GetWidth(), self.bmp.GetHeight())
                 dc.SetBrush(wx.NullBrush)
                 dc.SetPen(wx.NullPen)
-                ## draw label in the center of the mini
-                label = mini_layer.get_mini_label(self)
-                if len(label):
-                    dc.SetTextForeground(wx.RED)
-                    (textWidth,textHeight) = dc.GetTextExtent(label)
-                    x = self.pos.x +((self.bmp.GetWidth() - textWidth) /2) - 1
-                    y = self.pos.y + (self.bmp.GetHeight() / 2)
-                    dc.SetPen(wx.GREY_PEN)
-                    dc.SetBrush(wx.LIGHT_GREY_BRUSH)
-                    dc.DrawRectangle(x, y, textWidth+2, textHeight+2)
-                    if (textWidth+2 > self.right):
-                        self.right += int((textWidth+2-self.right)/2)+1
-                        self.left -= int((textWidth+2-self.right)/2)+1
-                    self.bottom = y+textHeight+2-self.pos.y
-                    dc.SetPen(wx.NullPen)
-                    dc.SetBrush(wx.NullBrush)
-                    dc.DrawText(label, x+1, y+1)
+            return True
 
-                #selected outline
-                if self.selected:
-                    dc.SetPen(wx.RED_PEN)
-                    dc.SetBrush(wx.TRANSPARENT_BRUSH)
-                    dc.DrawRectangle(self.pos.x, self.pos.y, self.bmp.GetWidth(), self.bmp.GetHeight())
-                    dc.SetBrush(wx.NullBrush)
-                    dc.SetPen(wx.NullPen)
+        elif not self.hide:
+            # set the width and height of the image
+            if self.gray:
+                bmp = self.bmp_gray
+            else:
+                bmp = self.bmp
+            dc.DrawBitmap(bmp, self.pos.x, self.pos.y, True)
+            self.left = 0
+            self.right = self.bmp.GetWidth()
+            self.top = 0
+            self.bottom = self.bmp.GetHeight()
 
-                return True
-            elif self.hide:
-                return True
+            # Draw the facing marker if needed
+            if self.face != 0:
+                x_mid = self.pos.x + (self.bmp.GetWidth()/2)
+                x_right = self.pos.x + self.bmp.GetWidth()
+                y_mid = self.pos.y + (self.bmp.GetHeight()/2)
+                y_bottom = self.pos.y + self.bmp.GetHeight()
+
+                dc.SetPen(wx.WHITE_PEN)
+                dc.SetBrush(wx.RED_BRUSH)
+                triangle = []
 
-            else:
-                # set the width and height of the image
-                bmp = self.bmp
-                if self.width and self.height:
-                    tmp_image = self.bmp.ConvertToImage()
-                    tmp_image.Rescale(int(self.width), int(self.height))
-                    tmp_image.ConvertAlphaToMask()
-                    self.bmp = tmp_image.ConvertToBitmap()
-                    mask = wx.Mask(self.bmp, wx.Colour(tmp_image.GetMaskRed(), tmp_image.GetMaskGreen(), tmp_image.GetMaskBlue()))
-                    self.bmp.SetMask(mask)
-                    if self.gray:
-                        tmp_image = tmp_image.ConvertToGreyscale()
-                        bmp = tmp_image.ConvertToBitmap()
-                    else:
-                        bmp = self.bmp
-                dc.DrawBitmap(bmp, self.pos.x, self.pos.y, True)
-                self.left = 0
-                self.right = self.bmp.GetWidth()
-                self.top = 0
-                self.bottom = self.bmp.GetHeight()
-
-                # Draw the facing marker if needed
-                if self.face != 0:
-                    x_mid = self.pos.x + (self.bmp.GetWidth()/2)
-                    x_right = self.pos.x + self.bmp.GetWidth()
-                    y_mid = self.pos.y + (self.bmp.GetHeight()/2)
-                    y_bottom = self.pos.y + self.bmp.GetHeight()
-                    dc.SetPen(wx.WHITE_PEN)
-                    dc.SetBrush(wx.RED_BRUSH)
-                    triangle = []
+                # Figure out which direction to draw the marker!!
+                if self.face == FACE_WEST:
+                    triangle.append(cmpPoint(self.pos.x,self.pos.y))
+                    triangle.append(cmpPoint(self.pos.x - 5, y_mid))
+                    triangle.append(cmpPoint(self.pos.x, y_bottom))
+                elif self.face ==  FACE_EAST:
+                    triangle.append(cmpPoint(x_right, self.pos.y))
+                    triangle.append(cmpPoint(x_right + 5, y_mid))
+                    triangle.append(cmpPoint(x_right, y_bottom))
+                elif self.face ==  FACE_SOUTH:
+                    triangle.append(cmpPoint(self.pos.x, y_bottom))
+                    triangle.append(cmpPoint(x_mid, y_bottom + 5))
+                    triangle.append(cmpPoint(x_right, y_bottom))
+                elif self.face ==  FACE_NORTH:
+                    triangle.append(cmpPoint(self.pos.x, self.pos.y))
+                    triangle.append(cmpPoint(x_mid, self.pos.y - 5))
+                    triangle.append(cmpPoint(x_right, self.pos.y))
+                elif self.face == FACE_NORTHEAST:
+                    triangle.append(cmpPoint(x_mid, self.pos.y))
+                    triangle.append(cmpPoint(x_right + 5, self.pos.y - 5))
+                    triangle.append(cmpPoint(x_right, y_mid))
+                    triangle.append(cmpPoint(x_right, self.pos.y))
+                elif self.face == FACE_SOUTHEAST:
+                    triangle.append(cmpPoint(x_right, y_mid))
+                    triangle.append(cmpPoint(x_right + 5, y_bottom + 5))
+                    triangle.append(cmpPoint(x_mid, y_bottom))
+                    triangle.append(cmpPoint(x_right, y_bottom))
+                elif self.face == FACE_SOUTHWEST:
+                    triangle.append(cmpPoint(x_mid, y_bottom))
+                    triangle.append(cmpPoint(self.pos.x - 5, y_bottom + 5))
+                    triangle.append(cmpPoint(self.pos.x, y_mid))
+                    triangle.append(cmpPoint(self.pos.x, y_bottom))
+                elif self.face == FACE_NORTHWEST:
+                    triangle.append(cmpPoint(self.pos.x, y_mid))
+                    triangle.append(cmpPoint(self.pos.x - 5, self.pos.y - 5))
+                    triangle.append(cmpPoint(x_mid, self.pos.y))
+                    triangle.append(cmpPoint(self.pos.x, self.pos.y))
+                dc.DrawPolygon(triangle)
+                dc.SetBrush(wx.NullBrush)
+                dc.SetPen(wx.NullPen)
 
-                    # Figure out which direction to draw the marker!!
-                    if self.face == FACE_WEST:
-                        triangle.append(cmpPoint(self.pos.x,self.pos.y))
-                        triangle.append(cmpPoint(self.pos.x - 5, y_mid))
-                        triangle.append(cmpPoint(self.pos.x, y_bottom))
-                    elif self.face ==  FACE_EAST:
-                        triangle.append(cmpPoint(x_right, self.pos.y))
-                        triangle.append(cmpPoint(x_right + 5, y_mid))
-                        triangle.append(cmpPoint(x_right, y_bottom))
-                    elif self.face ==  FACE_SOUTH:
-                        triangle.append(cmpPoint(self.pos.x, y_bottom))
-                        triangle.append(cmpPoint(x_mid, y_bottom + 5))
-                        triangle.append(cmpPoint(x_right, y_bottom))
-                    elif self.face ==  FACE_NORTH:
-                        triangle.append(cmpPoint(self.pos.x, self.pos.y))
-                        triangle.append(cmpPoint(x_mid, self.pos.y - 5))
-                        triangle.append(cmpPoint(x_right, self.pos.y))
-                    elif self.face == FACE_NORTHEAST:
-                        triangle.append(cmpPoint(x_mid, self.pos.y))
-                        triangle.append(cmpPoint(x_right + 5, self.pos.y - 5))
-                        triangle.append(cmpPoint(x_right, y_mid))
-                        triangle.append(cmpPoint(x_right, self.pos.y))
-                    elif self.face == FACE_SOUTHEAST:
-                        triangle.append(cmpPoint(x_right, y_mid))
-                        triangle.append(cmpPoint(x_right + 5, y_bottom + 5))
-                        triangle.append(cmpPoint(x_mid, y_bottom))
-                        triangle.append(cmpPoint(x_right, y_bottom))
-                    elif self.face == FACE_SOUTHWEST:
-                        triangle.append(cmpPoint(x_mid, y_bottom))
-                        triangle.append(cmpPoint(self.pos.x - 5, y_bottom + 5))
-                        triangle.append(cmpPoint(self.pos.x, y_mid))
-                        triangle.append(cmpPoint(self.pos.x, y_bottom))
-                    elif self.face == FACE_NORTHWEST:
-                        triangle.append(cmpPoint(self.pos.x, y_mid))
-                        triangle.append(cmpPoint(self.pos.x - 5, self.pos.y - 5))
-                        triangle.append(cmpPoint(x_mid, self.pos.y))
-                        triangle.append(cmpPoint(self.pos.x, self.pos.y))
-                    dc.DrawPolygon(triangle)
-                    dc.SetBrush(wx.NullBrush)
-                    dc.SetPen(wx.NullPen)
+            # Draw the heading if needed
+            if self.heading:
+                x_adjust = 0
+                y_adjust = 4
+                x_half = self.bmp.GetWidth()/2
+                y_half = self.bmp.GetHeight()/2
+                x_quarter = self.bmp.GetWidth()/4
+                y_quarter = self.bmp.GetHeight()/4
+                x_3quarter = x_quarter*3
+                y_3quarter = y_quarter*3
+                x_full = self.bmp.GetWidth()
+                y_full = self.bmp.GetHeight()
+                x_center = self.pos.x + x_half
+                y_center = self.pos.y + y_half
 
-                # Draw the heading if needed
-                if self.heading:
-                    x_adjust = 0
-                    y_adjust = 4
-                    x_half = self.bmp.GetWidth()/2
-                    y_half = self.bmp.GetHeight()/2
-                    x_quarter = self.bmp.GetWidth()/4
-                    y_quarter = self.bmp.GetHeight()/4
-                    x_3quarter = x_quarter*3
-                    y_3quarter = y_quarter*3
-                    x_full = self.bmp.GetWidth()
-                    y_full = self.bmp.GetHeight()
-                    x_center = self.pos.x + x_half
-                    y_center = self.pos.y + y_half
-                    # Remember, the pen/brush must be a different color than the
-                    # facing marker!!!!  We'll use black/cyan for starters.
-                    # Also notice that we will draw the heading on top of the
-                    # larger facing marker.
-                    dc.SetPen(wx.BLACK_PEN)
-                    dc.SetBrush(wx.CYAN_BRUSH)
-                    triangle = []
+                # Remember, the pen/brush must be a different color than the
+                # facing marker!!!!  We'll use black/cyan for starters.
+                # Also notice that we will draw the heading on top of the
+                # larger facing marker.
+                dc.SetPen(wx.BLACK_PEN)
+                dc.SetBrush(wx.CYAN_BRUSH)
+                triangle = []
 
-                    # Figure out which direction to draw the marker!!
-                    if self.heading == FACE_NORTH:
-                        triangle.append(cmpPoint(x_center - x_quarter, y_center - y_half ))
-                        triangle.append(cmpPoint(x_center, y_center - y_3quarter ))
-                        triangle.append(cmpPoint(x_center + x_quarter, y_center - y_half ))
-                    elif self.heading ==  FACE_SOUTH:
-                        triangle.append(cmpPoint(x_center - x_quarter, y_center + y_half ))
-                        triangle.append(cmpPoint(x_center, y_center + y_3quarter ))
-                        triangle.append(cmpPoint(x_center + x_quarter, y_center + y_half ))
-                    elif self.heading == FACE_NORTHEAST:
-                        triangle.append(cmpPoint(x_center + x_quarter, y_center - y_half ))
-                        triangle.append(cmpPoint(x_center + x_3quarter, y_center - y_3quarter ))
-                        triangle.append(cmpPoint(x_center + x_half, y_center - y_quarter ))
-                    elif self.heading == FACE_EAST:
-                        triangle.append(cmpPoint(x_center + x_half, y_center - y_quarter ))
-                        triangle.append(cmpPoint(x_center + x_3quarter, y_center ))
-                        triangle.append(cmpPoint(x_center + x_half, y_center + y_quarter ))
-                    elif self.heading == FACE_SOUTHEAST:
-                        triangle.append(cmpPoint(x_center + x_half, y_center + y_quarter ))
-                        triangle.append(cmpPoint(x_center + x_3quarter, y_center + y_3quarter ))
-                        triangle.append(cmpPoint(x_center + x_quarter, y_center + y_half ))
-                    elif self.heading == FACE_SOUTHWEST:
-                        triangle.append(cmpPoint(x_center - x_quarter, y_center + y_half ))
-                        triangle.append(cmpPoint(x_center - x_3quarter, y_center + y_3quarter ))
-                        triangle.append(cmpPoint(x_center - x_half, y_center + y_quarter ))
-                    elif self.heading == FACE_WEST:
-                        triangle.append(cmpPoint(x_center - x_half, y_center + y_quarter ))
-                        triangle.append(cmpPoint(x_center - x_3quarter, y_center ))
-                        triangle.append(cmpPoint(x_center - x_half, y_center - y_quarter ))
-                    elif self.heading == FACE_NORTHWEST:
-                        triangle.append(cmpPoint(x_center - x_half, y_center - y_quarter ))
-                        triangle.append(cmpPoint(x_center - x_3quarter, y_center - y_3quarter ))
-                        triangle.append(cmpPoint(x_center - x_quarter, y_center - y_half ))
-                    dc.DrawPolygon(triangle)
-                    dc.SetBrush(wx.NullBrush)
-                    dc.SetPen(wx.NullPen)
-                #selected outline
-                if self.selected:
-                    dc.SetPen(wx.RED_PEN)
-                    dc.SetBrush(wx.TRANSPARENT_BRUSH)
-                    dc.DrawRectangle(self.pos.x, self.pos.y, self.bmp.GetWidth(), self.bmp.GetHeight())
-                    dc.SetBrush(wx.NullBrush)
-                    dc.SetPen(wx.NullPen)
-                # draw label
-                label = mini_layer.get_mini_label(self)
-                if len(label):
-                    dc.SetTextForeground(wx.RED)
-                    (textWidth,textHeight) = dc.GetTextExtent(label)
-                    x = self.pos.x +((self.bmp.GetWidth() - textWidth) /2) - 1
-                    y = self.pos.y + self.bmp.GetHeight() + 6
-                    dc.SetPen(wx.WHITE_PEN)
-                    dc.SetBrush(wx.WHITE_BRUSH)
-                    dc.DrawRectangle(x,y,textWidth+2,textHeight+2)
-                    if (textWidth+2 > self.right):
-                        self.right += int((textWidth+2-self.right)/2)+1
-                        self.left -= int((textWidth+2-self.right)/2)+1
-                    self.bottom = y+textHeight+2-self.pos.y
-                    dc.SetPen(wx.NullPen)
-                    dc.SetBrush(wx.NullBrush)
-                    dc.DrawText(label,x+1,y+1)
-                self.top-=5
-                self.bottom+=5
-                self.left-=5
-                self.right+=5
-
-                return True
-        else:
-            return False
+                # Figure out which direction to draw the marker!!
+                if self.heading == FACE_NORTH:
+                    triangle.append(cmpPoint(x_center - x_quarter, y_center - y_half ))
+                    triangle.append(cmpPoint(x_center, y_center - y_3quarter ))
+                    triangle.append(cmpPoint(x_center + x_quarter, y_center - y_half ))
+                elif self.heading ==  FACE_SOUTH:
+                    triangle.append(cmpPoint(x_center - x_quarter, y_center + y_half ))
+                    triangle.append(cmpPoint(x_center, y_center + y_3quarter ))
+                    triangle.append(cmpPoint(x_center + x_quarter, y_center + y_half ))
+                elif self.heading == FACE_NORTHEAST:
+                    triangle.append(cmpPoint(x_center + x_quarter, y_center - y_half ))
+                    triangle.append(cmpPoint(x_center + x_3quarter, y_center - y_3quarter ))
+                    triangle.append(cmpPoint(x_center + x_half, y_center - y_quarter ))
+                elif self.heading == FACE_EAST:
+                    triangle.append(cmpPoint(x_center + x_half, y_center - y_quarter ))
+                    triangle.append(cmpPoint(x_center + x_3quarter, y_center ))
+                    triangle.append(cmpPoint(x_center + x_half, y_center + y_quarter ))
+                elif self.heading == FACE_SOUTHEAST:
+                    triangle.append(cmpPoint(x_center + x_half, y_center + y_quarter ))
+                    triangle.append(cmpPoint(x_center + x_3quarter, y_center + y_3quarter ))
+                    triangle.append(cmpPoint(x_center + x_quarter, y_center + y_half ))
+                elif self.heading == FACE_SOUTHWEST:
+                    triangle.append(cmpPoint(x_center - x_quarter, y_center + y_half ))
+                    triangle.append(cmpPoint(x_center - x_3quarter, y_center + y_3quarter ))
+                    triangle.append(cmpPoint(x_center - x_half, y_center + y_quarter ))
+                elif self.heading == FACE_WEST:
+                    triangle.append(cmpPoint(x_center - x_half, y_center + y_quarter ))
+                    triangle.append(cmpPoint(x_center - x_3quarter, y_center ))
+                    triangle.append(cmpPoint(x_center - x_half, y_center - y_quarter ))
+                elif self.heading == FACE_NORTHWEST:
+                    triangle.append(cmpPoint(x_center - x_half, y_center - y_quarter ))
+                    triangle.append(cmpPoint(x_center - x_3quarter, y_center - y_3quarter ))
+                    triangle.append(cmpPoint(x_center - x_quarter, y_center - y_half ))
+                dc.DrawPolygon(triangle)
+                dc.SetBrush(wx.NullBrush)
+                dc.SetPen(wx.NullPen)
+            handle_mini_draw_callback_functions(self, dc)
+            #selected outline
+            if self.selected:
+                dc.SetPen(wx.RED_PEN)
+                dc.SetBrush(wx.TRANSPARENT_BRUSH)
+                dc.DrawRectangle(self.pos.x, self.pos.y, self.bmp.GetWidth(), self.bmp.GetHeight())
+                dc.SetBrush(wx.NullBrush)
+                dc.SetPen(wx.NullPen)
+            # draw label
+            label = mini_layer.get_mini_label(self)
+            if len(label):
+                dc.SetTextForeground(wx.RED)
+                (textWidth,textHeight) = dc.GetTextExtent(label)
+                x = self.pos.x +((self.bmp.GetWidth() - textWidth) /2) - 1
+                y = self.pos.y + self.bmp.GetHeight() + 6
+                dc.SetPen(wx.WHITE_PEN)
+                dc.SetBrush(wx.WHITE_BRUSH)
+                dc.DrawRectangle(x,y,textWidth+2,textHeight+2)
+                if (textWidth+2 > self.right):
+                    self.right += int((textWidth+2-self.right)/2)+1
+                    self.left -= int((textWidth+2-self.right)/2)+1
+                self.bottom = y+textHeight+2-self.pos.y
+                dc.SetPen(wx.NullPen)
+                dc.SetBrush(wx.NullBrush)
+                dc.DrawText(label,x+1,y+1)
+            self.top-=5
+            self.bottom+=5
+            self.left-=5
+            self.right+=5
+        return True
 
     def toxml(self, action="update"):
         if action == "del":
@@ -405,7 +431,7 @@
             xml_str += " hide='0'"
         if self.snap_to_align != None:
             xml_str += " align='" + str(self.snap_to_align) + "'"
-        if self.id != None:
+        if self.zorder != None:
             xml_str += " zorder='" + str(self.zorder) + "'"
         if self.width != None:
             xml_str += " width='" + str(self.width) + "'"
@@ -415,6 +441,9 @@
             xml_str += ' local="' + str(self.local) + '"'
             xml_str += ' localPath="' + str(urllib.quote(self.localPath).replace('%3A', ':')) + '"'
             xml_str += ' localTime="' + str(self.localTime) + '"'
+        for attribute in mini_additional_attribute_list:
+            if attribute in self.__dict__ and self.__dict__[attribute] is not None:
+                xml_str += ' '+attribute+'="'+self.__dict__[attribute]+'"'        
         xml_str += " />"
 
         if (action == "update" and self.isUpdated) or action == "new":
@@ -440,7 +469,7 @@
 
         if 'path' in xml.attrib:
             self.path = urllib.unquote(xml.get("path"))
-            self.set_bmp(ImageHandler.load(self.path, 'miniature', self.id))
+            self.set_image(ImageHandler.load(self.path, 'miniature', self.id))
 
         if 'locked' in xml.attrib:
             if xml.get("locked") == '1' or xml.get("locked") == 'True':
@@ -471,6 +500,9 @@
 
         if 'height' in xml.attrib:
             self.height = int(xml.get("height"))
+        for attribute in mini_additional_attribute_list:
+            if attribute in xml.attrib:
+                self.__dict__[attribute] = xml.get(attribute)
 
 ##-----------------------------
 ## miniature layer
@@ -484,6 +516,7 @@
 
         self.miniatures = []
         self.serial_number = 0
+        self.show_labels = True
 
         # Set the font of the labels to be the same as the chat window
         # only smaller.
@@ -528,9 +561,9 @@
         self.serial_number -= 1
 
     def add_miniature(self, id, path, pos=cmpPoint(0,0), label="", heading=FACE_NONE, face=FACE_NONE, width=0, height=0, local=False, localPath='', localTime=-1):
-        bmp = ImageHandler.load(path, 'miniature', id)
-        if bmp:
-            mini = BmpMiniature(id, path, bmp, pos, heading, face, label, zorder=self. get_next_highest_z(), width=width, height=height, local=local, localPath=localPath, localTime=localTime)
+        image = ImageHandler.load(path, "miniature", id)
+        if image:
+            mini = BmpMiniature(id, path, image, pos, heading, face, label, zorder=self.get_next_highest_z(), width=width, height=height, local=local, localPath=localPath, localTime=localTime)
             self.miniatures.append(mini)
 
             xml_str = "<map><miniatures>"
@@ -604,14 +637,14 @@
             if mini.selected:
                 selected.append(mini)
                 if minxy_pos is None:
-                    minxy_pos = wx.Point(mini.pos.x, mini.pos.y) 
+                    minxy_pos = wx.Point(mini.pos.x, mini.pos.y)
                     maxxy_pos = wx.Point(mini.pos.x, mini.pos.y)
                 else:
                     minxy_pos.x = min(minxy_pos.x, mini.pos.x)
                     minxy_pos.y = min(minxy_pos.y, mini.pos.y)
                     maxxy_pos.x = max(maxxy_pos.x, mini.pos.x)
                     maxxy_pos.y = max(maxxy_pos.y, mini.pos.y)
-        return selected, minxy_pos, maxxy_pos             
+        return selected, minxy_pos, maxxy_pos
 
     def layerToXML(self, action="update"):
         """ format  """
@@ -663,10 +696,11 @@
                     snap_to_align = int(c.get('align'))
                 if 'zorder' in c.attrib:
                     zorder = int(c.get('zorder'))
-                min = BmpMiniature(id, path, ImageHandler.load(path, 'miniature', id), pos, heading, face, label, locked, hide, snap_to_align, zorder, width, height)
+
+                mini = BmpMiniature(id, path, ImageHandler.load(path, 'miniature', id), pos, heading, face, label, locked, hide, snap_to_align, zorder, width, height)
                 if 'selected' in c.attrib and c.get('selected')=='1':
-                    min.selected = True
-                self.miniatures.append(min)
+                    mini.selected = True
+                self.miniatures.append(mini)
                 if 'local' in c.attrib and c.get('local') == 'True' and os.path.exists(urllib.unquote(c.get('localPath'))):
                     localPath = urllib.unquote(c.get('localPath'))
                     local = True
@@ -681,6 +715,10 @@
                         thread.start_new_thread(self.upload, (postdata, localPath, True))
                 #  collapse the zorder.  If the client behaved well, then nothing should change.
                 #    Otherwise, this will ensure that there's some kind of z-order
+                for attribute in mini_additional_attribute_list:
+                    if attribute in c.attrib:
+                        mini.__dict__[attribute] = c.get(attribute)
+        
                 self.collapse_zorder()
             else:
                 mini = self.get_miniature_by_id(id)
@@ -688,40 +726,42 @@
                     mini.takedom(c)
 
     def upload(self, postdata, filename, modify=False, pos=cmpPoint(0,0)):
-        self.lock.acquire()
-        url = settings.get('ImageServerBaseURL')
-        file = urllib.urlopen(url, postdata)
-        recvdata = file.read()
-        file.close()
-        try:
-            xml = ET.parse(recvdata).getroot()
-            if xml.tag == 'path':
-                path = xml.get('url')
-                path = urllib.unquote(path)
-                if not modify:
-                    start = path.rfind("/") + 1
-                    if self.canvas.parent.layer_handlers[2].auto_label:
-                        min_label = path[start:len(path)-4]
+        with self.lock:
+            url = settings.get('ImageServerBaseURL')
+            f = urllib.urlopen(url, postdata)
+            recvdata = f.read()
+            f.close()
+            try:
+                xml = ET.fromstring(recvdata)
+                if xml.tag == 'path':
+                    path = xml.get('url')
+                    path = urllib.unquote(path)
+                    if not modify:
+                        start = path.rfind("/") + 1
+                        if self.canvas.parent.layer_handlers[2].auto_label:
+                            min_label = path[start:len(path)-4]
+                        else:
+                            min_label = ""
+                        id = 'mini-' + self.canvas.frame.session.get_next_id()
+                        self.add_miniature(id, path, pos=pos, label=min_label,
+                                           local=True, localPath=filename,
+                                           localTime=time.time())
                     else:
-                        min_label = ""
-                    id = 'mini-' + self.canvas.frame.session.get_next_id()
-                    self.add_miniature(id, path, pos=pos, label=min_label,
-                                       local=True, localPath=filename,
-                                       localTime=time.time())
+                        self.miniatures[len(self.miniatures)-1].local = True
+                        self.miniatures[len(self.miniatures)-1].localPath = filename
+                        self.miniatures[len(self.miniatures)-1].localTime = time.time()
+                        self.miniatures[len(self.miniatures)-1].path = path
                 else:
-                    self.miniatures[len(self.miniatures)-1].local = True
-                    self.miniatures[len(self.miniatures)-1].localPath = filename
-                    self.miniatures[len(self.miniatures)-1].localTime = time.time()
-                    self.miniatures[len(self.miniatures)-1].path = path
-            else:
-                print xml.get('msg')
-        except Exception, e:
-            print e
-            print recvdata
-        finally:
-            urllib.urlcleanup()
-            self.lock.release()
+                    print xml.get('msg')
+            except Exception, e:
+                logger.general(e)
+                logger.general(recvdata)
+            finally:
+                urllib.urlcleanup()
 
     def get_mini_label(self, mini):
         # override this to change the label displayed under each mini (and the label on hidden minis)
-        return mini.label
+        if self.show_labels:
+            return mini.label
+        else:
+            return ""
--- a/orpg/mapper/miniatures_handler.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/miniatures_handler.py	Mon Mar 22 18:38:22 2010 -0600
@@ -42,7 +42,8 @@
 from orpg.tools.orpg_log import logger
 from orpg.tools.settings import settings
 
-LABEL_TOOL = wx.NewId()
+SHOW_LABELS_TOOL = wx.NewId()
+AUTO_LABEL_TOOL = wx.NewId()
 LAYER_TOOL = wx.NewId()
 MIN_LIST_TOOL = wx.NewId()
 MIN_TOOL = wx.NewId()
@@ -121,13 +122,11 @@
     def build_ctrls(self):
         base_layer_handler.build_ctrls(self)
         # add controls in reverse order! (unless you want them after the default tools)
-        self.auto_label_cb = wx.CheckBox(self, wx.ID_ANY, ' Auto Label ', (-1,-1),(-1,-1))
-        self.auto_label_cb.SetValue(self.auto_label)
         self.min_url = wx.ComboBox(self, wx.ID_ANY, "http://", style=wx.CB_DROPDOWN | wx.CB_SORT)
-        self.localBrowse = wx.Button(self, wx.ID_ANY, 'Browse')
+        self.localBrowse = wx.Button(self, wx.ID_ANY, 'Browse', style=wx.BU_EXACTFIT)
         minilist = createMaskedButton(self, dir_struct["icon"]+'questionhead.gif', 'Edit miniature properties', wx.ID_ANY)
         miniadd = wx.Button(self, wx.ID_OK, "Add Miniature", style=wx.BU_EXACTFIT)
-        self.sizer.Add(self.auto_label_cb,0,wx.ALIGN_CENTER)
+
         self.sizer.Add(self.min_url, 1, wx.ALIGN_CENTER)
         self.sizer.Add(miniadd, 0, wx.EXPAND)
         self.sizer.Add(self.localBrowse, 0, wx.EXPAND)
@@ -136,12 +135,12 @@
         self.Bind(wx.EVT_BUTTON, self.on_min_list, minilist)
         self.Bind(wx.EVT_BUTTON, self.on_miniature, miniadd)
         self.Bind(wx.EVT_BUTTON, self.on_browse, self.localBrowse)
-        self.Bind(wx.EVT_CHECKBOX, self.on_label, self.auto_label_cb)
+        #self.Bind(wx.EVT_CHECKBOX, self.on_label, self.auto_label_cb)
 
     def on_browse(self, evt):
         if not self.role_is_gm_or_player():
             return
-        dlg = wx.FileDialog(None, "Select a Miniature to load", dir_struct["user"]+'webfiles/', wildcard="Image files (*.bmp, *.gif, *.jpg, *.png)|*.bmp;*.gif;*.jpg;*.png", style=wx.OPEN)
+        dlg = wx.FileDialog(None, "Select a Miniature to load", dir_struct["user"], wildcard="Image files (*.bmp, *.gif, *.jpg, *.png)|*.bmp;*.gif;*.jpg;*.png", style=wx.OPEN)
         if not dlg.ShowModal() == wx.ID_OK:
             dlg.Destroy()
             return
@@ -164,9 +163,17 @@
                 min_url = open_rpg.get_component("cherrypy") + filename
             except:
                 return
-            if dlg.GetDirectory() == dir_struct["user"]+'webfiles/Textures' or dlg.GetDirectory() == dir_struct["user"]+'webfiles\Textures': min_url = settings.get('LocalImageBaseURL') + 'Textures/' + filename
-            if dlg.GetDirectory() == dir_struct["user"]+'webfiles/Maps' or dlg.GetDirectory() == dir_struct["user"]+'webfiles\Maps': min_url = settings.get('ImageServerBaseURL') + 'Maps/' + filename
-            if dlg.GetDirectory() == dir_struct["user"]+'webfiles/Miniatures' or dlg.GetDirectory() == dir_struct["user"]+'webfiles\Miniatures': min_url = settings.get('LocalImageBaseURL') + 'Miniatures/' + filename
+
+            texture_dir = dir_struct["user"] + 'webfiles' + os.sep + 'Textures'
+            maps_dir = dir_struct["user"] + 'webfiles' + os.sep + 'Maps'
+            mini_dir = dir_struct["user"] + 'webfiles' + os.set + 'Miniatures'
+            if dlg.GetDirectory() == texture_dir:
+                url = settings.get('LocalImageBaseURL') + 'Textures/'
+            if dlg.GetDirectory() == maps_dir:
+                url = settings.get('LocalImageBaseURL') + 'Maps/'
+            if dlg.GetDirectory() == mini_dir:
+                url = settings.get('LocalImageBaseURL') + 'Miniatures/'
+
             # build url
             if min_url == "" or min_url == "http://":
                 return
@@ -206,15 +213,18 @@
     def build_menu(self,label = "Miniature"):
         base_layer_handler.build_menu(self,label)
         self.main_menu.AppendSeparator()
-        self.main_menu.Append(LABEL_TOOL,"&Auto label","",1)
-        self.main_menu.Check(LABEL_TOOL,self.auto_label)
+        self.main_menu.Append(SHOW_LABELS_TOOL,"&Show labels","", 1)
+        self.main_menu.Check(SHOW_LABELS_TOOL, self.canvas.layers['miniatures'].show_labels)
+        self.main_menu.Append(AUTO_LABEL_TOOL,"&Auto label","",1)
+        self.main_menu.Check(AUTO_LABEL_TOOL,self.auto_label)
         self.main_menu.Append(SERIAL_TOOL,"&Number minis","",1)
         self.main_menu.Check(SERIAL_TOOL, self.use_serial)
         self.main_menu.Append(MAP_REFRESH_MINI_URLS,"&Refresh miniatures")       #  Add the menu item
         self.main_menu.AppendSeparator()
         self.main_menu.Append(MIN_MOVE, "Move")
         self.canvas.Bind(wx.EVT_MENU, self.on_map_board_menu_item, id=MAP_REFRESH_MINI_URLS)          #  Set the handler
-        self.canvas.Bind(wx.EVT_MENU, self.on_label, id=LABEL_TOOL)
+        self.canvas.Bind(wx.EVT_MENU, self.on_show_labels, id=SHOW_LABELS_TOOL)
+        self.canvas.Bind(wx.EVT_MENU, self.on_auto_label, id=AUTO_LABEL_TOOL)
         self.canvas.Bind(wx.EVT_MENU, self.on_serial, id=SERIAL_TOOL)
         # build miniature meenu
         self.min_menu = wx.Menu()
@@ -531,7 +541,7 @@
             x = dc.DeviceToLogicalX(0)
             y = dc.DeviceToLogicalY(0)
             self.canvas.layers['miniatures'].add_miniature(id, min_url, pos=cmpPoint(x,y), label=min_label)
-        except:
+        except Exception, e:
             # When there is an exception here, we should be decrementing the serial_number for reuse!!
             unablemsg= "Unable to load/resolve URL: " + min_url + " on resource \"" + min_label + "\"!!!\n\n"
             #print unablemsg
@@ -544,11 +554,14 @@
         #except Exception, e:
             #wx.MessageBox(str(e),"Miniature Error")
 
-    def on_label(self,evt):
+    def on_show_labels(self,evt):
+        show_labels = not self.canvas.layers['miniatures'].show_labels
+        self.canvas.layers['miniatures'].show_labels = show_labels
+        self.canvas.Refresh()
+
+    def on_auto_label(self,evt):
         self.auto_label = not self.auto_label
         self.auto_label_cb.SetValue(self.auto_label)
-        #self.send_map_data()
-        #self.Refresh()
 
     def on_min_list(self,evt):
         session = self.canvas.frame.session
@@ -568,7 +581,7 @@
         id = evt.GetId()
         if id == MAP_REFRESH_MINI_URLS:   # Note: this doesn't change the mini, so no need to update the map
             for mini in self.canvas.layers['miniatures'].miniatures:       #  For all minis
-                mini.set_bmp(ImageHandler.load(mini.path, 'miniature', mini.id))      #  Reload their bmp member
+                mini.set_image(ImageHandler.load(mini.path, 'miniature', mini.id))      #  Reload their bmp member
             self.canvas.Refresh(False)
 
 ####################################################################
@@ -754,7 +767,6 @@
 
     def set_mini_rclick_menu_item(self, label, callback_function):
         # remember you might want to call these at the end of your callback function:
-        # mini_handler.sel_rmin.isUpdated = True
         # canvas.Refresh(False)
         # canvas.send_map_data()
         if callback_function == None:
@@ -822,7 +834,7 @@
         for mini in mini_list:
             if mini.selected:
                 if found_selected_mini:
-                    return mini_list # at least 2 selected minis in list; return all 
+                    return mini_list # at least 2 selected minis in list; return all
                 found_selected_mini = mini
         if found_selected_mini:
             return [found_selected_mini]# return only selected
--- a/orpg/mapper/whiteboard.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/whiteboard.py	Mon Mar 22 18:38:22 2010 -0600
@@ -30,6 +30,9 @@
 
 from base import *
 from orpg.mapper.map_utils import *
+from random import randint
+
+from orpg.tools.settings import settings
 
 def cmp_zorder(first,second):
     f = first.zorder
@@ -59,7 +62,8 @@
         self.textcolor = color
         self.posx = pos.x
         self.posy = pos.y
-        self.font = wx.Font(self.pointsize, wx.DEFAULT, self.style, self.weight)
+        self.font = wx.Font(self.pointsize, wx.DEFAULT, self.style, self.weight,
+                            False, settings.get('defaultfont'))
         self.highlighted = False
         r,g,b = self.r_h.rgb_tuple(self.textcolor)
         self.highlight_color = self.r_h.hexstring(r^255, g^255, b^255)
@@ -168,49 +172,61 @@
                 self.textcolor = '#000000'
 
 class WhiteboardLine:
-    def __init__(self, id, line_string, upperleft, lowerright, color="#000000", width=1):
+    def __init__(self, id, color="#000000", width=1):
         self.scale = 1
         self.r_h = RGBHex()
         if color == '':
             color = "#000000"
         self.linecolor = color
         self.linewidth = width
-        self.lowerright = lowerright
-        self.upperleft = upperleft
-        self.selected = False
-        self.line_string = line_string
+        self.points = []
         self.id = id
         self.highlighted = False
         r,g,b = self.r_h.rgb_tuple(self.linecolor)
         self.highlight_color = self.r_h.hexstring(r^255, g^255, b^255)
 
+    def add_point(self, pos):
+        self.points.append(pos)
+
+    def approximate(self):
+        """
+        Approximate the line with fewer points.
+
+        For every group of three adjacent points (A, B and C), point B
+        is discarded if it's sufficiently close to AC.
+        """
+        new_points = []
+        i = 0
+        while i < len(self.points):
+            a = self.points[i]
+            new_points.append(a)
+            for j in range(i+1, len(self.points)-1):
+                b = self.points[j]
+                c = self.points[j+1]
+                if orpg.mapper.map_utils.proximity_test(a, c, b, 0.5):
+                    i += 1
+                else:
+                    break;
+            i += 1
+        self.points = new_points
+
+    def points_from_string(self, line_string):
+        self.points = []
+        for p in line_string.split(";"):
+            p = p.split(",")
+            if len(p) == 2:
+                self.add_point((int(p[0]), int(p[1])))
+
     def highlight(self, highlight=True):
         self.highlighted = highlight
 
-    def set_line_props(self, line_string="", upperleftx=0, upperlefty=0, lowerrightx=0, lowerrighty=0, color="#000000", width=1):
-        self.line_string = line_string
-        self.upperleft.x = upperleftx
-        self.upperleft.y = upperlefty
-        self.lowerright.x = lowerrightx
-        self.lowerright.y = lowerrighty
-        self.linecolor = color
-        self.linewidth = width
-
     def hit_test(self, pt):
-        coords = self.line_string.split(";")
-        stcords = coords[0].split(",")
-        oldicords = (int(stcords[0]),int(stcords[1]))
-        for coordinate_string_counter in range(1, len(coords)):
-            stcords = coords[coordinate_string_counter].split(",")
-            if stcords[0] == "":
-                return False
-
-            icords = (int(stcords[0]),int(stcords[1]))
-            if orpg.mapper.map_utils.proximity_test(oldicords,icords,pt,12):
-                return True
-
-            oldicords = icords
-
+        if self.points != []:
+            a = self.points[0]
+            for b in self.points[1:]:
+                if orpg.mapper.map_utils.proximity_test(a, b, pt, 12):
+                    return True
+                a = b
         return False
 
     def draw(self, parent, dc, op=wx.COPY):
@@ -221,24 +237,28 @@
         pen = wx.BLACK_PEN
         try:
             pen.SetColour(linecolor)
-        except:
-            pass
+        except Exception,e:
+            pen.SetColour('#000000')
+
         pen.SetWidth( self.linewidth )
         dc.SetPen( pen )
         dc.SetBrush(wx.BLACK_BRUSH)
         # draw lines
+
         dc.SetUserScale(self.scale,self.scale)
-        pointArray = self.line_string.split(";")
-        x2 = y2 = None
-        for m in range(len(pointArray)-1):
-            x = pointArray[m]
-            points = x.split(",")
-            if x2 != None:
-                dc.DrawLine(x2,y2,int(points[0]),int(points[1]))
-            x2 = int(points[0])
-            y2 = int(points[1])
-        dc.SetPen(wx.NullPen)
-        dc.SetBrush(wx.NullBrush)
+
+        if self.points != []:
+            a = self.points[0]
+            for b in self.points[1:]:
+                (xa, ya) = a
+                (xb, yb) = b
+                dc.DrawLine(xa, ya, xb, yb)
+                a = b
+
+        #pen.SetColour(wx.Colour(0,0,0))
+        #dc.SetPen(pen)
+        #dc.SetPen(wx.NullPen)
+        #dc.SetBrush(wx.NullBrush)
 
     def toxml(self, action="update"):
         if action == "del":
@@ -247,16 +267,21 @@
             return xml_str
 
         #  if there are any changes, make sure id is one of them
+        line_string = ';'.join([str(p[0]) + ',' + str(p[1])
+                                for p in self.points])
         xml_str = "<line"
         xml_str += " action='" + action + "'"
         xml_str += " id='" + str(self.id) + "'"
-        xml_str+= " line_string='" + self.line_string + "'"
-        if self.upperleft != None:
-            xml_str += " upperleftx='" + str(self.upperleft.x) + "'"
-            xml_str += " upperlefty='" + str(self.upperleft.y) + "'"
-        if self.lowerright != None:
-            xml_str+= " lowerrightx='" + str(self.lowerright.x) + "'"
-            xml_str+= " lowerrighty='" + str(self.lowerright.y) + "'"
+        ########
+        # the following lines may seem odd but are added to be backwards compatible with
+        # OpenRPG 1.8.0.X and earlier.  The last point was always repeated and variables
+        # upperleftx, upperlefty, lowerrightx, lowerrighty were mandatory (though unused).
+        if self.points[-1] != self.points[-2]:
+            line_string += ";"+str(self.points[-1][0])+","+str(self.points[-1][1])
+        xml_str += " upperleftx='0' upperlefty='0' lowerrightx='0' lowerrighty='0'"
+        ########
+        xml_str+= " line_string='" + line_string + "'"
+
         if self.linecolor != None:
             xml_str += " color='" + str(self.linecolor) + "'"
         if self.linewidth != None:
@@ -268,7 +293,8 @@
         return ''
 
     def takedom(self, xml):
-        self.line_string = xml.get("line_string")
+        line_string = xml.get("line_string")
+        self.points_from_line_string(line_string)
         self.id = xml.get("id")
 
         if 'upperleftx' in xml.attrib:
@@ -299,41 +325,22 @@
     def __init__(self, canvas):
         self.canvas = canvas
         layer_base.__init__(self)
+
         self.r_h = RGBHex()
-        self.id = -1
         self.lines = []
         self.texts = []
-        self.serial_number = 0
         self.color = "#000000"
         self.width = 1
         self.removedLines = []
 
-    def next_serial(self):
-        self.serial_number += 1
-        return self.serial_number
-
     def get_next_highest_z(self):
-        z = len(self.lines)+1
-
-        return z
-
-    def cleanly_collapse_zorder(self): pass
-
-    def collapse_zorder(self): pass
+        return len(self.lines)+1
 
-    def rollback_serial(self):
-        self.serial_number -= 1
+    def cleanly_collapse_zorder(self):
+        pass
 
-    def add_line(self, line_string="", upperleft=cmpPoint(0,0), lowerright=cmpPoint(0,0), color="#000000", width=1):
-        id = 'line-' + str(self.next_serial())
-        line = WhiteboardLine(id, line_string, upperleft, lowerright, color=self.color, width=self.width)
-        self.lines.append(line)
-        xml_str = "<map><whiteboard>"
-        xml_str += line.toxml("new")
-        xml_str += "</whiteboard></map>"
-        self.canvas.frame.session.send(xml_str)
-        self.canvas.Refresh(True)
-        return line
+    def collapse_zorder(self):
+        pass
 
     def get_line_by_id(self, id):
         for line in self.lines:
@@ -356,19 +363,18 @@
         if line:
             self.lines.remove(line)
             self.removedLines.append(line)
-        self.canvas.Refresh(True)
+        self.canvas.Refresh()
 
-    def undo_line(self):
+    def redo_line(self):
         if len(self.removedLines)>0:
-            line = self.removedLines[len(self.removedLines)-1]
-            self.removedLines.remove(line)
-            self.add_line(line.line_string, line.upperleft, line.lowerright, line.linecolor, line.linewidth)
+            line = self.removedLines.pop()
+            self.lines.append(line)
+            self.complete_line(line)
             self.canvas.Refresh(True)
 
     def del_all_lines(self):
         for i in xrange(len(self.lines)):
             self.del_line(self.lines[0])
-        print self.lines
 
     def del_text(self, text):
         xml_str = "<map><whiteboard>"
@@ -399,7 +405,7 @@
 
         return list_of_texts_matching
 
-    def hit_test_lines(self, pos, dc):
+    def hit_test_lines(self, pos):
         list_of_lines_matching = []
         if self.canvas.layers['fog'].use_fog == 1:
             if self.canvas.frame.session.role != "GM":
@@ -412,15 +418,10 @@
         return list_of_lines_matching
 
     def find_line(self, pt):
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
-        line_list = self.hit_test_lines(pt,dc)
+        line_list = self.hit_test_lines(pt)
         if line_list:
             return line_list[0]
-        else:
-            return None
+        return None
 
     def setcolor(self, color):
         r,g,b = color.Get()
@@ -436,8 +437,8 @@
         self.font = font
 
     def add_text(self, text_string, pos, style, pointsize, weight, color="#000000"):
-        id = 'text-' + str(self.next_serial())
-        text = WhiteboardText(id,text_string, pos, style, pointsize, weight, color)
+        id = 'text-' + self.canvas.session.get_next_id()
+        text = WhiteboardText(id, text_string, pos, style, pointsize, weight, color)
         self.texts.append(text)
         xml_str = "<map><whiteboard>"
         xml_str += text.toxml("new")
@@ -445,33 +446,7 @@
         self.canvas.frame.session.send(xml_str)
         self.canvas.Refresh(True)
 
-    def draw_working_line(self, dc, line_string):
-        scale = self.canvas.layers['grid'].mapscale
-        dc.SetPen(wx.BLACK_PEN)
-        dc.SetBrush(wx.BLACK_BRUSH)
-        pen = wx.BLACK_PEN
-        pen.SetColour(self.color)
-        pen.SetWidth(self.width)
-        dc.SetPen(pen)
-        dc.SetUserScale(scale,scale)
-        pointArray = line_string.split(";")
-        x2 = y2 = -999
-
-        for m in range(len(pointArray)-1):
-            x = pointArray[m]
-            points = x.split(",")
-            x1 = int(points[0])
-            y1 = int(points[1])
-            if x2 != -999:
-                dc.DrawLine(x2,y2,x1,y1)
-            x2 = x1
-            y2 = y1
-
-        dc.SetPen(wx.NullPen)
-        dc.SetBrush(wx.NullBrush)
-
     def layerToXML(self, action="update"):
-        """ format  """
         white_string = ""
         if self.lines:
             for l in self.lines:
@@ -480,19 +455,13 @@
             for l in self.texts:
                 white_string += l.toxml(action)
         if len(white_string):
-            s = "<whiteboard"
-            s += " serial='" + str(self.serial_number) + "'"
-            s += ">"
+            s = "<whiteboard>"
             s += white_string
             s += "</whiteboard>"
             return s
-        else:
-            return ""
+        return ""
 
     def layerTakeDOM(self, xml):
-        serial_number = xml.get('serial')
-        if serial_number != "":
-            self.serial_number = int(serial_number)
         for l in xml:
             nodename = l.tag
             action = l.get("action")
@@ -510,23 +479,16 @@
                 if nodename == "line":
                     try:
                         line_string = l.get('line_string')
-                        upperleftx = l.get('upperleftx')
-                        upperlefty = l.get('upperlefty')
-                        lowerrightx = l.get('lowerrightx')
-                        lowerrighty = l.get('lowerrighty')
-                        upperleft = wx.Point(int(upperleftx),int(upperlefty))
-                        lowerright = wx.Point(int(lowerrightx),int(lowerrighty))
                         color = l.get('color')
                         if color == '#0000000':
                             color = '#000000'
                         id = l.get('id')
                         width = int(l.get('width'))
                     except:
-                        line_string = upperleftx = upperlefty = lowerrightx = lowerrighty = color = 0
-                        print traceback.format_exc()
+                        logger.general(traceback.format_exc())
                         continue
-
-                    line = WhiteboardLine(id, line_string, upperleft, lowerright, color, width)
+                    line = WhiteboardLine(id, color, width)
+                    line.points_from_string(line_string)
                     self.lines.append(line)
                 elif nodename == "text":
                     try:
@@ -544,7 +506,7 @@
                         pos.x = int(posx)
                         pos.y = int(posy)
                     except:
-                        print traceback.format_exc()
+                        logger.general(traceback.format_exc())
                         continue
                     text = WhiteboardText(id, text_string, pos, style, pointsize, weight, color)
                     self.texts.append(text)
@@ -559,11 +521,20 @@
                     if text:
                         text.takedom(l)
 
-    def add_temp_line(self, line_string):
-        line = WhiteboardLine(0, line_string, wx.Point(0,0), wx.Point(0,0), color=self.color, width=self.width)
+    def add_temp_line(self):
+        id = 'line-' + self.canvas.session.get_next_id()
+        line = WhiteboardLine(id, color=self.color, width=self.width)
         self.lines.append(line)
         return line
 
     def del_temp_line(self, line):
         if line:
             self.lines.remove(line)
+
+    def complete_line(self, line):
+        line.approximate()
+        xml_str = "<map><whiteboard>"
+        xml_str += line.toxml("new")
+        xml_str += "</whiteboard></map>"
+        self.canvas.frame.session.send(xml_str)
+
--- a/orpg/mapper/whiteboard_handler.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/mapper/whiteboard_handler.py	Mon Mar 22 18:38:22 2010 -0600
@@ -33,28 +33,17 @@
 
 class whiteboard_handler(base_layer_handler):
     def __init__(self, parent, id, canvas):
+        base_layer_handler.__init__(self, parent, id, canvas)
         self.drawing_mode = 'Freeform'
-        self.line_string = "0,0;"
-        self.drawing = False
-        self.upperleft = wx.Point(0,0)
-        self.lowerright = wx.Point(0,0)
-        #polyline drawing vars
-        self.polypoints = 0
-        self.lastpoint = None
-        self.selected = None
+        self.working_line = None
+        self.cone_start = None
+        self.selected = None # this is either a line or text object
         #text drawing vars
         self.style = str(wx.NORMAL)
         self.weight = str(wx.NORMAL)
         self.pointsize = str(12)
-        self.text_selected_item = None
-        #self.r_h = RGBHex()
-        base_layer_handler.__init__(self, parent, id, canvas)
+        self.text_selected_item = None # this is the text being dealt with
         self.build_text_properties_menu()
-        self.wb = self.canvas.layers['whiteboard']
-        self.temp_circle = None
-        self.cone_start = None
-        self.temp_edge1 = None
-        self.temp_edge2 = None
 
     def build_ctrls(self):
         base_layer_handler.build_ctrls(self)
@@ -82,7 +71,7 @@
         self.sizer.Add(self.color_button, 0, wx.EXPAND)
         self.sizer.Add(wx.Size(20,25))
         self.Bind(wx.EVT_MOTION, self.on_motion)
-        self.Bind(wx.EVT_CHOICE, self.check_draw_mode, self.drawmode_ctrl)
+        self.Bind(wx.EVT_CHOICE, self.initialize_draw, self.drawmode_ctrl)
         self.Bind(wx.EVT_BUTTON, self.on_pen_color, self.color_button)
         self.Bind(wx.EVT_CHOICE, self.on_pen_width, self.widthList)
 
@@ -134,12 +123,12 @@
         self.canvas.Bind(wx.EVT_MENU, self.delete_all_lines, item)
         self.main_menu.AppendItem(item)
         item = wx.MenuItem(self.main_menu, wx.ID_ANY, "&Undo Last Deleted Line", "Undo Last Deleted Line")
-        self.canvas.Bind(wx.EVT_MENU, self.undo_line, item)
+        self.canvas.Bind(wx.EVT_MENU, self.redo_line, item)
         self.main_menu.AppendItem(item)
         self.line_menu = wx.Menu("Whiteboard Line")
         self.line_menu.SetTitle("Whiteboard Line")
         item = wx.MenuItem(self.line_menu, wx.ID_ANY, "&Remove", "Remove")
-        self.canvas.Bind(wx.EVT_MENU, self.on_line_menu_item, item)
+        self.canvas.Bind(wx.EVT_MENU, self.on_remove_line, item)
         self.line_menu.AppendItem(item)
         self.text_menu = wx.Menu("Whiteboard Text")
         self.text_menu.SetTitle("Whiteboard Text")
@@ -147,16 +136,12 @@
         self.canvas.Bind(wx.EVT_MENU, self.get_text_properties, item)
         self.text_menu.AppendItem(item)
         item = wx.MenuItem(self.text_menu, wx.ID_ANY, "&Remove", "Remove")
-        self.canvas.Bind(wx.EVT_MENU, self.on_text_menu_item, item)
+        self.canvas.Bind(wx.EVT_MENU, self.on_remove_text, item)
         self.text_menu.AppendItem(item)
 
     def do_line_menu(self,pos):
         self.canvas.PopupMenu(self.line_menu, pos)
 
-    def item_selected(self,evt):
-        item = evt.GetId()
-        self.item_selection = self.selection_list[item]
-
     def on_text_properties(self,evt):
         text_string = self.text_control.GetValue()
         if self.style_control.GetStringSelection() == 'Normal':
@@ -215,13 +200,13 @@
             self.ItemList = items
             self.tmpPos = pos
             for i in xrange(len(items)):
-                menu.Append(i+1, items[i].text_string)
-                self.canvas.Bind(wx.EVT_MENU, self.onItemSelect, id=i+1)
+                menu.Append(i, items[i].text_string)
+                self.canvas.Bind(wx.EVT_MENU, self.on_text_select, id=i)
             self.canvas.PopupMenu(menu)
         return
 
-    def onItemSelect(self, evt):
-        id = evt.GetId()-1
+    def on_text_select(self, evt):
+        id = evt.GetId()
         self.text_selected_item = self.ItemList[id]
         self.text_selected_item.selected = True
         if self.tmpPos == 'right':
@@ -231,18 +216,15 @@
 
     def on_right_down(self,evt):
         line = 0
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC(self.canvas)
-        self.canvas.PrepareDC(dc)
-        dc.SetUserScale(scale,scale)
+        dc = self.create_dc()
         pos = evt.GetLogicalPosition(dc)
         if self.drawing_mode == 'Text':
             self.on_text_right_down(evt, dc)
-        elif self.drawing and ((self.drawing_mode == 'Circle') or (self.drawing_mode == 'Cone')):
-            self.check_draw_mode()
-            self.drawing = False
-        elif (self.drawing_mode == 'Freeform') or (self.drawing_mode == 'Polyline') or (self.drawing_mode == 'Circle') or (self.drawing_mode == 'Cone'):
-            line_list = self.canvas.layers['whiteboard'].find_line(pos)
+        elif self.working_line:
+            # right-click re-sets line drawing
+            self.initialize_draw()
+        else:
+            line_list = self.canvas.layers['whiteboard'].find_line(pos) # arbitrarily pick to delete first line?
             if line_list:
                 self.sel_rline = self.canvas.layers['whiteboard'].get_line_by_id(line_list.id)
                 if self.sel_rline:
@@ -250,8 +232,6 @@
                     self.canvas.Refresh(False)
             else:
                 base_layer_handler.on_right_down(self,evt)
-        else:
-            base_layer_handler.on_right_down(self,evt)
         del dc
 
     def on_pen_color(self,evt):
@@ -269,16 +249,13 @@
         width = int(self.widthList.GetStringSelection())
         self.canvas.layers['whiteboard'].setwidth(width)
 
-    def undo_line(self,evt):
+    def redo_line(self,evt):
         session = self.canvas.frame.session
         if (session.my_role() != session.ROLE_GM) and (session.use_roles()):
             self.top_frame.openrpg.get_component("chat").InfoPost("You must be a GM to use this feature")
             return
-        self.canvas.layers['whiteboard'].undo_line()
-        dc = self.create_dc()
-        self.un_highlight(dc)
-        self.selected = None
-        del dc
+        self.un_highlight()
+        self.canvas.layers['whiteboard'].redo_line()
 
     def delete_all_lines(self,evt):
         session = self.canvas.frame.session
@@ -288,367 +265,178 @@
         dlg = wx.MessageDialog(self, "Are you sure you want to delete all lines?","Delete All Lines",wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
         if dlg.ShowModal() != wx.ID_YES:
             return
+        self.un_highlight()
         self.canvas.layers['whiteboard'].del_all_lines()
-        dc = self.create_dc()
-        self.un_highlight(dc)
-        self.selected = None
-        del dc
 
-    def on_line_menu_item(self, evt):
-        dc = self.create_dc()
-        self.un_highlight(dc)
+    def on_remove_line(self, evt):
+        self.un_highlight()
         self.canvas.layers['whiteboard'].del_line(self.sel_rline)
-        self.selected = None
-        del dc
 
-    def on_text_menu_item(self, evt):
-        dc = self.create_dc()
-        self.un_highlight(dc)
-        self.canvas.layers['whiteboard'].del_text(self.selected)
-        self.selected = None
-        del dc
+    def on_remove_text(self, evt):
+        self.un_highlight()
+        self.canvas.layers['whiteboard'].del_text(self.text_selected_item)
 
     # Check Draw Mode
     # Queries the GUI to see what mode to draw in
     # 05-09-2003 Snowdog
 
-    def check_draw_mode(self, evt=None):
+    def initialize_draw(self, evt=None):
         self.drawing_mode = self.drawmode_ctrl.GetStringSelection()
-
-        #because mode can be changed while a polyline is being created
-        #clear the current linestring and reset the polyline data
-        self.upperleft.x = self.upperleft.y = 0
-        self.lowerright.x = self.lowerright.y = 0
-        self.lastpoint = None #because the end check function is not called we must force its lastpoint var to None again
-        self.polypoints = 0
-        self.line_string = "0,0;"
-        if self.temp_circle:
-            self.canvas.layers['whiteboard'].del_temp_line(self.temp_circle)
-            if self.selected == self.temp_circle:
-                self.selected = None
-            self.canvas.Refresh(True)
-        self.temp_circle = None
+        self.remove_working_line()
+        self.un_highlight()
         self.cone_start  = None
 
-    # Altered on_left_up to toggle between
-    # drawing modes freeform vs polyline
-    # 05-09-2003  Snowdog
+    def remove_working_line(self):
+        if self.working_line is not None:
+            self.canvas.layers['whiteboard'].del_temp_line(self.working_line)
+            self.canvas.Refresh(True)
+        self.working_line = None
+
+    def finalize_working_line(self):
+        if self.working_line is not None:
+            self.canvas.layers['whiteboard'].complete_line(self.working_line)
+            #self.canvas.Refresh(True)
+        self.working_line = None
+
+    def initialize_working_line(self):
+        if self.working_line is not None:
+            self.canvas.layers['whiteboard'].del_temp_line(self.working_line)
+            self.canvas.Refresh(True)
+        self.working_line = self.canvas.layers['whiteboard'].add_temp_line()
+        
     def on_left_down(self,evt):
-        if not self.drawing:
-            self.check_draw_mode()
+        if not self.working_line:
+            self.initialize_draw()
         if self.drawing_mode == 'Freeform':
-            #Freeform mode ignores the inital down click
-            pass
+            self.freeform_start()
         elif self.drawing_mode == 'Polyline':
-            self.polyline_add_point( evt )
+            self.polyline_add_point(evt)
         elif self.drawing_mode == 'Text':
             self.on_text_left_down(evt)
         elif self.drawing_mode == 'Cone':
-            if self.cone_start == None:
+            if self.working_line:
+                self.draw_temporary_cone(evt)
+            else:
                 self.on_start_cone(evt)
-            else:
-                self.draw_temporary_cone(evt)
         elif self.drawing_mode == 'Circle':
             self.draw_temporary_circle(evt)
 
-    # Added handling for double clicks within the map
-    # 05-09-2003  Snowdog
     def on_left_dclick(self, evt):
-        if self.drawing_mode == 'Freeform':
-            #Freeform mode ignores the double click
-            pass
-        elif self.drawing_mode == 'Polyline':
-            self.polyline_last_point( evt )
-        elif self.drawing_mode == 'Text':
-            pass
-        elif self.drawing_mode == 'Circle' or self.drawing_mode == 'Cone':
-            self.canvas.layers['whiteboard'].del_temp_line(self.temp_circle)
-            #pointArray = self.temp_circle.line_string.split(";")
-            self.canvas.layers['whiteboard'].add_line(self.temp_circle.line_string)
-            self.temp_circle = None
+        if self.drawing_mode == 'Polyline' or self.drawing_mode == 'Circle' or self.drawing_mode == 'Cone':
+            self.finalize_working_line()
             self.cone_start  = None
-            self.drawing = False
 
-    # Altered on_left_up to toggle between
-    # drawing modes freeform vs polyline
-    # 05-09-2003  Snowdog
     def on_left_up(self,evt):
         if self.drawing_mode == 'Freeform':
-            self.on_freeform_left_up(evt)
-        elif self.drawing_mode == 'Polyline':
-            #Polyline mode relies on the down click
-            #not the mouse button release
-            pass
-        elif self.drawing_mode == 'Text':
-            pass
+            self.on_freeform_end(evt)
 
-    # Altered on_left_up to toggle between
-    # drawing modes freeform vs polyline
-    # 05-09-2003  Snowdog
     def on_motion(self,evt):
+        # do we check this too often here?
         session = self.canvas.frame.session
         if (session.my_role() != session.ROLE_GM) \
             and (session.my_role()!=session.ROLE_PLAYER) \
             and (session.use_roles()):
             return
-        if self.drawing_mode == 'Freeform':
-            if evt.m_leftDown:
+        if self.working_line:
+            if self.drawing_mode == 'Freeform' and evt.m_leftDown:
                 self.freeform_motion(evt)
-        elif self.drawing_mode == 'Polyline':
-            if self.drawing:
-                self.polyline_preview( evt )
-        dc = self.create_dc()
-        pos = evt.GetLogicalPosition(dc)
-        hit = self.canvas.layers['whiteboard'].hit_test_lines(pos,dc)
-        if hit:
-            self.highlight(hit,dc)
+            elif self.drawing_mode == 'Polyline':
+                self.polyline_motion(evt)
+            self.un_highlight()
         else:
-            self.un_highlight(dc)
-            hit = self.canvas.layers['whiteboard'].hit_test_text(pos,dc)
+            dc = self.create_dc()
+            pos = evt.GetLogicalPosition(dc)
+            hit = self.canvas.layers['whiteboard'].hit_test_lines(pos)
             if hit:
-                self.highlight(hit,dc)
+                self.highlight(hit)
             else:
-                self.un_highlight(dc)
-        del dc
+                self.un_highlight()
+                hit = self.canvas.layers['whiteboard'].hit_test_text(pos, dc)
+                if hit:
+                    self.highlight(hit)
+                else:
+                    self.un_highlight()
+            del dc
 
     def create_dc(self):
         scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
+        dc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(dc)
+        dc.SetUserScale(scale, scale)
         return dc
 
-    def highlight(self,hit,dc):
+    def highlight(self, hit):
+        if self.selected == hit[0]:
+            return;
         if self.selected:
             self.selected.highlight(False)
-            self.selected.draw(self.wb,dc)
         self.selected = hit[0]
-        self.selected.highlight()
-        self.selected.draw(self.wb,dc)
+        self.selected.highlight(True)
+        self.canvas.Refresh(True)
 
-    def un_highlight(self,dc):
+    def un_highlight(self):
         if self.selected:
             self.selected.highlight(False)
-            self.selected.draw(self.wb,dc)
-
-    # Polyline Add Point
-    # adds a new point to the polyline
-    # 05-09-2003  Snowdog
-    def polyline_add_point(self, evt):
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
-        pos = evt.GetLogicalPosition(dc)
-        #reset the bounding points
-        if pos.x < self.upperleft.x:
-            self.upperleft.x = pos.x
-        elif pos.x > self.lowerright.x:
-            self.lowerright.x = pos.x
-        if pos.y < self.upperleft.y:
-            self.upperleft.y = pos.y
-        elif pos.y > self.lowerright.y:
-            self.lowerright.y = pos.y
-
-        #if this point doens't end the line
-        #add a new point into the line string
-        if not self.polyline_end_check( pos ):
-            if self.drawing == True:
-                self.polypoints += 1 #add one to the point counter.
-                self.line_string += `pos.x` + "," + `pos.y` + ";"
-                self.canvas.layers['whiteboard'].draw_working_line(dc,self.line_string)
-            else: #start of line...
-                self.polypoints += 1 #add one to the point counter.
-                self.line_string = `pos.x` + "," + `pos.y` + ";"
-                self.upperleft.x = pos.x
-                self.upperleft.y = pos.y
-                self.lowerright.x = pos.x
-                self.lowerright.y = pos.y
-                self.drawing = True
-        else: #end of line. Send and reset vars for next line
-            self.drawing = False
-            if self.polypoints < 2:
-                #not enough points to form a line. Ignore line
-                pass
-            else:
-                #have enough points to create valid line
-                #check to role to make sure user can draw at all....
-                session = self.canvas.frame.session
-                if (session.my_role() != session.ROLE_GM) and (session.my_role()!=session.ROLE_PLAYER) and (session.use_roles()):
-                    open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use this feature")
-                    self.canvas.Refresh(False)
-                else:
-                    #user allowed to draw on whiteboard.. send polyline
-                    line = self.canvas.layers['whiteboard'].add_line(self.line_string,self.upperleft,self.lowerright)
-            #resetting variables for next line
-            self.upperleft.x = self.upperleft.y = 0
-            self.lowerright.x = self.lowerright.y = 0
-            self.polypoints = 0
-            self.line_string = "0,0;"
+            self.selected = None
+            self.canvas.Refresh(True)
 
-    # Polyline Last Point
-    # adds a final point to the polyline and ends it
-    # 05-09-2003  Snowdog
-    def polyline_last_point(self, evt):
-        #if we haven't started a line already. Ignore the click
-        if self.drawing != True:
-            return
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
-        pos = evt.GetLogicalPosition(dc)
-        #reset the bounding points
-        if pos.x < self.upperleft.x:
-            self.upperleft.x = pos.x
-        elif pos.x > self.lowerright.x:
-            self.lowerright.x = pos.x
-        if pos.y < self.upperleft.y:
-            self.upperleft.y = pos.y
-        elif pos.y > self.lowerright.y:
-            self.lowerright.y = pos.y
-        self.polypoints += 1 #add one to the point counter.
-        self.line_string += `pos.x` + "," + `pos.y` + ";"
-        self.canvas.layers['whiteboard'].draw_working_line(dc,self.line_string)
-        #end of line. Send and reset vars for next line
-        self.drawing = False
-        if self.polypoints < 2:
-            #not enough points to form a line. Ignore line
-            pass
-        else:
-            #have enough points to create valid line
-            #check to role to make sure user can draw at all....
-            session = self.canvas.frame.session
-            if (session.my_role() != session.ROLE_GM) and (session.my_role()!=session.ROLE_PLAYER) and (session.use_roles()):
-                open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use this feature")
-                self.canvas.Refresh(False)
-            else:
-                #user allowed to draw on whiteboard.. send polyline
-                line = self.canvas.layers['whiteboard'].add_line(self.line_string,self.upperleft,self.lowerright)
-        #resetting variables for next line
-        self.upperleft.x = self.upperleft.y = 0
-        self.lowerright.x = self.lowerright.y = 0
-        self.lastpoint = None #becuase the end check function is not called we must force its lastpoint var to None again
-        self.polypoints = 0
-        self.line_string = "0,0;"
+    def polyline_add_point(self, evt):
+        session = self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM) and (session.my_role()!=session.ROLE_PLAYER) and (session.use_roles()):
+            open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use this feature")
+            return;
 
-    # Polyline End Check
-    # checks to see if a double click has occured
-    # a second click on the LAST polyline point
-    # (or very close proximity) should cause the
-    # polyline even to complete and send
-    # 05-09-2003  Snowdog
-    def polyline_end_check(self, pos):
-        # check to see if the position of the give point is within POLYLINE_END_TOLERANCE
-        # if it is then the line has been completed and should be sent to the map just like
-        # the original freeform version is. A line with fewer than 2 points should be ignored
-        x_in = y_in = 0
-        tol = 5
-
-        #first point check
-        if type(self.lastpoint) == type(None):
-            self.lastpoint = wx.Point(pos.x,pos.y)
-            return 0 #not end of line
-        if ((self.lastpoint.x -tol) <= pos.x <= (self.lastpoint.x)):
-            x_in = 1
-        if ((self.lastpoint.y -tol) <= pos.y <= (self.lastpoint.y)):
-            y_in = 1
-        if x_in and y_in:
-            #point within tolerance. End line
-            self.lastpoint = None
-            return 1
-        #if we've reached here the point is NOT a terminal point. Reset the lastpoint and return False
-        self.lastpoint.x = pos.x
-        self.lastpoint.y = pos.y
-        return 0
-
-    # Polyline Preview
-    # display a temporary/momentary line to the user
-    # from the last point to mouse position
-    # 05-09-2003  Snowdog
-    def polyline_preview(self, evt):
-        if self.drawing != True:
-            #not enough points to form a line. Ignore line
-            return
-        if self.live_refresh.GetValue() == 0:
-            #not using live redraw
-            return
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
+        dc = self.create_dc()
         pos = evt.GetLogicalPosition(dc)
 
-        #reset the bounding points
-        if pos.x < self.upperleft.x:
-            self.upperleft.x = pos.x
-        elif pos.x > self.lowerright.x:
-            self.lowerright.x = pos.x
-        if pos.y < self.upperleft.y:
-            self.upperleft.y = pos.y
-        elif pos.y > self.lowerright.y:
-            self.lowerright.y = pos.y
+        if not self.working_line:
+            # new line
+            self.initialize_working_line()
+            self.working_line.add_point(pos)
+            self.working_line.add_point(pos)
+        elif not self.polyline_end_check(pos):
+            # add point
+            self.working_line.add_point(pos)
+        else:
+            # end line
+            self.finalize_working_line()
+        self.canvas.Refresh()
 
-        #redraw the line with a line connected to the cursor
-        temp_string = self.line_string
-        temp_string += `pos.x` + "," + `pos.y` + ";"
-        self.canvas.layers['whiteboard'].draw_working_line(dc,temp_string)
-        self.canvas.Refresh(True)
+    def polyline_end_check(self, pos):
+        """Check if the last two points are sufficiently close to consider the poly line as ended."""
+        tolerance = 5
+        (xa, ya) = self.working_line.points[-2]
+        (xb, yb) = self.working_line.points[-1]
+        if xa - tolerance <= xb <= xa + tolerance and ya - tolerance <= yb <= ya + tolerance:
+            self.working_line.points.pop()
+            return True
+        return False
 
-    # moved original on_motion to this function
-    # to allow alternate drawing method to be used
-    # 05-09-2003  Snowdog
-    def freeform_motion(self, evt):
-#        if not self.drawing:
-#            return
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
+    def polyline_motion(self, evt):
+        dc = self.create_dc()
         pos = evt.GetLogicalPosition(dc)
-        if pos.x < self.upperleft.x:
-            self.upperleft.x = pos.x
-        elif pos.x > self.lowerright.x:
-            self.lowerright.x = pos.x
-        if pos.y < self.upperleft.y:
-            self.upperleft.y = pos.y
-        elif pos.y > self.lowerright.y:
-            self.lowerright.y = pos.y
-        if evt.m_leftDown:
-            if self.drawing == True:
-                self.line_string += `pos.x` + "," + `pos.y` + ";"
-                self.canvas.layers['whiteboard'].draw_working_line(dc,self.line_string)
-            else:
-                self.line_string = `pos.x` + "," + `pos.y` + ";"
-                self.upperleft.x = pos.x
-                self.upperleft.y = pos.y
-                self.lowerright.x = pos.x
-                self.lowerright.y = pos.y
-            self.drawing = True
-        del dc
+        self.working_line.points[-1] = (pos.x, pos.y)
+        self.canvas.Refresh()
+
+    def freeform_start(self):
+        session = self.canvas.frame.session
+        if session.use_roles() and session.my_role() != session.ROLE_GM and session.my_role() != session.ROLE_PLAYER:
+            open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use this feature")
+            return
+        self.initialize_working_line()
 
-    # moved original on_left_up to this function
-    # to allow alternate drawing method to be used
-    # 05-09-2003  Snowdog
-    def on_freeform_left_up(self,evt):
-        if self.drawing == True:
-            self.drawing = False
-            session = self.canvas.frame.session
-            if (session.my_role() != session.ROLE_GM) and (session.my_role()!=session.ROLE_PLAYER) and (session.use_roles()):
-                open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use this feature")
-                self.canvas.Refresh(False)
-                return
-        #self.id +=1
-            line = self.canvas.layers['whiteboard'].add_line(self.line_string,self.upperleft,self.lowerright)
-            dc = self.create_dc()
-            for m in range(30):
-                line.highlight()
-                line.draw(self.wb,dc)
-                line.highlight(False)
-                line.draw(self.wb,dc)
-            del dc
-            self.upperleft.x = self.upperleft.y = 0
-            self.lowerright.x = self.lowerright.y = 0
+    def freeform_motion(self, evt):
+        dc = self.create_dc()
+        pos = evt.GetLogicalPosition(dc)
+        self.working_line.add_point(pos)
+        self.canvas.Refresh()
+
+    def on_freeform_end(self, evt):
+        # check this line is more than just a single left click up and down
+        if len(self.working_line.points) < 2:
+            self.remove_working_line()
+        else:
+            self.finalize_working_line()
 
     def on_text_left_down(self, evt):
         session = self.canvas.frame.session
@@ -656,12 +444,9 @@
             open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use this feature")
             self.canvas.Refresh(False)
             return
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
+        dc = self.create_dc()
         pos = evt.GetLogicalPosition(dc)
-        test_text = self.canvas.layers['whiteboard'].hit_test_text(pos,dc)
+        test_text = self.canvas.layers['whiteboard'].hit_test_text(pos, dc)
         if len(test_text) > 0:
             if len(test_text) > 1:
                 self.do_text_menu('left', test_text)
@@ -705,14 +490,12 @@
             open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use this feature")
             self.canvas.Refresh(False)
             return
+        self.initialize_working_line()
         self.cone_start = self.get_snapped_to_logical_pos(evt)
-        self.drawing = True
+        self.working_line.add_point(self.cone_start)
 
     def get_snapped_to_logical_pos(self, evt):
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
+        dc = self.create_dc()
         pos = evt.GetLogicalPosition(dc)
         if self.canvas.layers['grid'].snap:
             size = self.canvas.layers['grid'].unit_size
@@ -721,10 +504,7 @@
         return pos
 
     def draw_temporary_cone(self, evt):
-        scale = self.canvas.layers['grid'].mapscale
-        dc = wx.ClientDC( self.canvas )
-        self.canvas.PrepareDC( dc )
-        dc.SetUserScale(scale,scale)
+        dc = self.create_dc()
         pos = evt.GetLogicalPosition(dc)
         pos2 = self.get_snapped_to_logical_pos(evt)
         size = self.canvas.layers['grid'].unit_size #60
@@ -813,11 +593,7 @@
         pos = wx.Point(self.cone_start.x+x_diff, self.cone_start.y+y_diff)
         qos = wx.Point(self.cone_start.x-y_diff, self.cone_start.y+x_diff)# 90 degree rotation
         curve = `pos.x`+","+`pos.y`+";" + curve + `qos.x`+","+`qos.y`+";"
-        if(self.temp_circle):
-            self.canvas.layers['whiteboard'].del_temp_line(self.temp_circle)
-            if self.selected == self.temp_circle:
-                self.selected = None
-        self.temp_circle = self.canvas.layers['whiteboard'].add_temp_line(curve)
+        self.working_line.points_from_string(curve)
         self.canvas.Refresh(True)
 
     def draw_temporary_circle(self, evt):
@@ -831,11 +607,8 @@
         radius = int(int(self.radius.GetValue())/5)
         center = wx.Point(pos.x, pos.y+size*radius)
         curve  = self.calculate_circle(center, radius, size)
-        if(self.temp_circle):
-            self.canvas.layers['whiteboard'].del_temp_line(self.temp_circle)
-            self.selected = None
-        self.temp_circle = self.canvas.layers['whiteboard'].add_temp_line(curve)
-        self.drawing = True
+        self.initialize_working_line()
+        self.working_line.points_from_string(curve)
         self.canvas.Refresh(True)
 
     def calculate_circle(self, center, radius, size):
--- a/orpg/networking/meta_server_lib.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/networking/meta_server_lib.py	Mon Mar 22 18:38:22 2010 -0600
@@ -136,6 +136,7 @@
 
     return_hash = {}
 
+    etree = None
     for meta in all_metas:
         bad_meta = 0
         try:
@@ -151,6 +152,9 @@
 
     #  Create a servers element
     return_dom = ElementTree(Element("servers"))
+    if etree is None:
+        print 'Failed to connect to meta server(s).'
+        return return_dom
 
     #  get the nodes stored in return_hash
     sorter_func = byStartAttribute if sort_by == 'start' else byNameAttribute
--- a/orpg/networking/mplay_client.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/networking/mplay_client.py	Mon Mar 22 18:38:22 2010 -0600
@@ -347,7 +347,7 @@
             cmpType = 'zlib'
 
         el.set('cmpType', cmpType)
-        return el
+        return tostring(el)
 
     def log_msg(self,msg):
         try:
@@ -543,7 +543,7 @@
 
     def boot_player(self, id, boot_pwd = ""):
         el = messaging.build('boot', boot_pwd=boot_pwd)
-        self.send(el, id)
+        self.send(tostring(el), id)
 
 #---------------------------------------------------------
 # [START] Snowdog Password/Room Name altering code 12/02
@@ -606,7 +606,6 @@
         self.check_my_status()
 
     def send_sound(self, snd_xml):
-        #FIXME#
         if self.status == MPLAY_CONNECTED:
             self.outbox.put(snd_xml)
         self.check_my_status()
--- a/orpg/orpgCore.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/orpgCore.py	Mon Mar 22 18:38:22 2010 -0600
@@ -60,7 +60,8 @@
 
 class ORPGStorage(object):
     __components = {}
-    __depreciated = ['settings', 'log', 'dir_struct', 'validate', 'xml']
+    __depreciated = ['settings', 'log', 'dir_struct', 'validate', 'xml',
+                     'DiceManager']
 
     def __new__(cls):
         it = cls.__dict__.get("__it__")
@@ -95,3 +96,9 @@
         self.remove(key)
 
 open_rpg = ORPGStorage()
+
+class ParserContext:
+    def __init__(self, namespace_hint=None):
+        self.namespace_hint = namespace_hint # None, or a handler or a string indicating the namespace
+        self.lines = [] # the lines following after this line in a multi-line text
+
--- a/orpg/orpg_version.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/orpg_version.py	Mon Mar 22 18:38:22 2010 -0600
@@ -1,8 +1,8 @@
-VERSION = "1.8.0"
+VERSION = "1.8.0+ development"
 SERVER_MIN_CLIENT_VERSION = "1.7.1"
 
 #BUILD NUMBER FORMAT: "YYMMDD-##" where ## is the incremental daily build index (if needed)
-BUILD = "090712-00"
+BUILD = "091121-00"
 
 # This version is for network capability.
 PROTOCOL_VERSION = "1.2"
--- a/orpg/plugindb.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/plugindb.py	Mon Mar 22 18:38:22 2010 -0600
@@ -57,10 +57,10 @@
 
     def FetchList(self, parent):
         retlist = []
-        for litem in parent.findall('lobject'):
+        for litem in parent.find('list').findall('lobject'):
             if litem.get('type') == 'int':
                 retlist.append(int(litem.text))
-            if litem.get('type') == 'bool':
+            elif litem.get('type') == 'bool':
                 retlist.append(litem.text == 'True')
             elif litem.get('type') == 'float':
                 retlist.append(float(litem.text))
@@ -192,7 +192,7 @@
 
     def FetchDict(self, parent):
         retdict = {}
-        for ditem in parent.findall('dobject'):
+        for ditem in parent.find('dict').findall('dobject'):
             key = self.normal(ditem.get('name'))
 
             if ditem.get('type') == 'int':
@@ -228,10 +228,13 @@
         return retdict
 
     def safe(self, string):
+        string = str(string)
         return string.replace("<", "$$lt$$").replace(">", "$$gt$$")\
                .replace("&","$$amp$$").replace('"',"$$quote$$")
 
     def normal(self, string):
+        if string is None:
+            return ''
         return string.replace("$$lt$$", "<").replace("$$gt$$", ">")\
                .replace("$$amp$$","&").replace("$$quote$$",'"')
 
@@ -243,4 +246,4 @@
         with open(self.filename) as f:
             self.etree.parse(f)
 
-plugindb = PluginDB()
\ No newline at end of file
+plugindb = PluginDB()
--- a/orpg/templates/default_settings.xml	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/templates/default_settings.xml	Mon Mar 22 18:38:22 2010 -0600
@@ -1,122 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
 <ini>
-    <tab name="General" type="tab">
-        <tab name="Networking" type="grid">
-            <Heartbeat options="bool" value="1" help="This sends a message to the server to keep alive your connection when idle." />
-            <MetaServerBaseURL help="This is the URL that contains the server list." options="URL" value="http://www.openrpg.com/openrpg_servers.php"/>
-            <ImageServerBaseURL help="This is the URL that contains the server list." options="URL" value="http://openrpg.digitalxero.net/imgupload/index.php"/>
-            <LocalorRemote help="Decide to load files locally or remotely. CherryPy must be running for local files." options="Local | Remote" value="Remote"/>
-        </tab>
-        <tab name="Sound" type="grid">
-            <UnixSoundPlayer help="This is the path to the executable used by unix clients to play sounds." options="path to executable" value=""/>
-            <SendSound help="Path to sound file played when you send a message." options="Path to file" value=""/>
-            <RecvSound help="Path to sound file played when you receive a message." options="Path to file" value=""/>
-            <WhisperSound help="Path to sound file played when you receive a whisper." options="Path to file" value=""/>
-            <AddSound help="Path to sound file played when a new user joins the room." options="Path to file" value=""/>
-            <DelSound help="Path to sound file played when a user exits the room." options="Path to file" value=""/>
-        </tab>
-        <tab name="System" type="grid">
-          <PWMannager help="Setting this to On will make the Password Manager auto start when you start OpenRPG" options="On|Off" value="On"/>
-          <LoggingLevel help="Change this via the menu" options="DO NOT CHANGE" value="7"/>
-          <TabTheme help="This is set via menu options" options="customflat | customslant | set by menu options" value="slant&amp;colorful"/>
-          <TabTextColor help="This is the text color for Tabs" options="color in hex RRGGBB" value="#000000"/>
-          <TabBackgroundGradient help="This is the background color of tab areas." options="color in hex RRGGBB" value="#f7f3f7"/>
-          <TabGradientFrom help="This is the gradient color for BG tabs if you pick the custom setting" options="color in hex RRGGBB" value="#ffffff"/>
-          <TabGradientTo help="This is the gradient color for BG tabs if you pick the custom setting" options="color in hex RRGGBB" value="#007dff"/>
-          <ColorTree help="This option specifies whether or not your want your Gametree and Player List colored the same as your chat." options="bool" value="0"/>
-        </tab>
-        <tab name="Auto Updater" type="grid">
-            <release_url help="The URL to the packages.xml" options="URL" value="http://openrpg.sf.net/"/>
-            <auto_update help="Wether or not the updater automaticly downloads the files" options="On|Off" value="Off"/>
-            <fast_update help="If the patcher will auto get bug fixes" options="On|Off" value="On"/>
-        </tab>
-        <tab name="Tip of the Day" type="grid">
-            <tipotday_start help="You shouldnt need to modify this" options="int" value="0" />
-            <tipotday_enabled help="Show the tips or not" options="0|1" value="1" />
-        </tab>
+  <tab name="General" type="tab">
+    <tab name="Networking" type="grid">
+      <Heartbeat options="bool" value="1" help="This sends a message to the server to keep alive your connection when idle." />
+      <MetaServerBaseURL help="This is the URL that contains the server list." options="URL" value="http://www.openrpg.com/openrpg_servers.php" />
+      <ImageServerBaseURL help="This is the URL that contains the server list." options="URL" value="http://openrpg.digitalxero.net/imgupload/index.php" />
+      <LocalorRemote help="Decide to load files locally or remotely. CherryPy must be running for local files." options="Local | Remote" value="Remote" />
+    </tab>
+    <tab name="Sound" type="grid">
+      <UnixSoundPlayer help="This is the path to the executable used by unix clients to play sounds." options="path to executable" value="" />
+      <SendSound help="Path to sound file played when you send a message." options="Path to file" value="" />
+      <RecvSound help="Path to sound file played when you receive a message." options="Path to file" value="" />
+      <WhisperSound help="Path to sound file played when you receive a whisper." options="Path to file" value="" />
+      <AddSound help="Path to sound file played when a new user joins the room." options="Path to file" value="" />
+      <DelSound help="Path to sound file played when a user exits the room." options="Path to file" value="" />
+    </tab>
+    <tab name="System" type="grid">
+      <PWMannager help="Setting this to On will make the Password Manager auto start when you start OpenRPG" options="On|Off" value="On" />
+      <LoggingLevel help="Change this via the menu" options="DO NOT CHANGE" value="7" />
+      <TabTheme help="This is set via menu options" options="customflat | customslant | set by menu options" value="slant&amp;colorful" />
+      <TabTextColor help="This is the text color for Tabs" options="color in hex RRGGBB" value="#000000" />
+      <TabBackgroundGradient help="This is the background color of tab areas." options="color in hex RRGGBB" value="#f7f3f7" />
+      <TabGradientFrom help="This is the gradient color for BG tabs if you pick the custom setting" options="color in hex RRGGBB" value="#ffffff" />
+      <TabGradientTo help="This is the gradient color for BG tabs if you pick the custom setting" options="color in hex RRGGBB" value="#007dff" />
+      <ColorTree help="This option specifies whether or not your want your Gametree and Player List colored the same as your chat." options="bool" value="0" />
+    </tab>
+    <tab name="Auto Updater" type="grid">
+      <release_url help="The URL to the packages.xml" options="URL" value="http://openrpg.sf.net/" />
+      <auto_update help="Wether or not the updater automaticly downloads the files" options="On|Off" value="Off" />
+      <fast_update help="If the patcher will auto get bug fixes" options="On|Off" value="On" />
+    </tab>
+    <tab name="Tip of the Day" type="grid">
+      <tipotday_start help="You shouldnt need to modify this" options="int" value="0" />
+      <tipotday_enabled help="Show the tips or not" options="0|1" value="1" />
+    </tab>
+  </tab>
+  <tab name="Chat" type="tab">
+    <tab name="Chat Window" type="grid">
+      <player help="This is your name as it appears in chat." options="Any text" value="No Name" />
+      <Show_Images_In_Chat help="Allows Images to be displaied in the chat window." options="bool" value="0" />
+      <striphtml help="Set this to 1 to have HTML tags stripped from the chat window." options="bool" value="0" />
+      <Chat_Time_Indexing help="Allows messages to be prepended with their arrival time using either of two preset formats or a formated timestring (see time.strftime() in python docs)" options="bool" value="0" />
+      <gwtext help="This is attached prior to your group whispers(ie /gw Hello group would send '(GW): Hello group')" options="text" value="(GW): " />
+      <dcmsg help="This is the message that gets sent when you disconnect from a server." options="text" value="Disconnecting from server..." />
+      <buffersize help="This is the amount of backscroll allowed." options="int" value="1000" />
+      <PurgeAtBuffersizeTimesLines help="This option tells the program when to purge old history.\nWhen the buffer exceeds this number times the buffersize\nall history is removed and you are just left with a history \nas large as the number you set your buffersize to." options="int" value="5" />
+      <AutoPurgeAfterSave catagory="chat" help="When saving your log, this option will either automaticaly purge the buffer or not." options="bool" value="0" />
+      <TypingStatusAlias options="Any text" value="Typing" help="This is the text displayed in the Player list under status while you are typing." />
+      <IdleStatusAlias options="Any text" value="Idle" help="This is the text displayed in the Player list under status while you are not typing." />
+      <SuppressChatAutoComplete options="bool" value="0" help="Setting this to 1 will turn off auto complete in chat." />
+      <ShowIDInChat options="bool" value="1" help="Set this to have the Player Id show up next to the player name in chat." />
+      <TimeStampGameLog options="bool" value="1" help="Set this to 1 to have time stamps added to the log." />
+      <GameLogPrefix help="This text is the files name minus the extention of your log file." options="Any text" value="logs/Log " />
+      <dieroller options="std, wod, d20, hero" value="std" help="This sets the dieroller to use." />
+    </tab>
+    <tab name="Chat Tabs" type="grid">
+      <tabbedwhispers help="Set this to 1 to receive whispered messages in separate chat tabs ." options="bool" value="1" />
+      <GMWhisperTab help="Creates a tab for all GM whispers, tabbedwhispers being on is required for this too work" options="bool" value="1" />
+      <GroupWhisperTab help="Creates a tab for all Group whispers, tabbedwhispers being on is required for this too work" options="bool" value="1" />
     </tab>
-    <tab name="Chat" type="tab">
-        <tab name="Chat Window" type="grid">
-            <player help="This is your name as it appears in chat." options="Any text" value="No Name"/>
-            <Show_Images_In_Chat help="Allows Images to be displaied in the chat window." options="bool" value="0"/>
-            <striphtml help="Set this to 1 to have HTML tags stripped from the chat window." options="bool" value="0"/>
-            <Chat_Time_Indexing help="Allows messages to be prepended with their arrival time using either of two preset formats or a formated timestring (see time.strftime() in python docs)" options="bool" value="0"/>
-            <gwtext help="This is attached prior to your group whispers(ie /gw Hello group would send '(GW): Hello group')" options="text" value="(GW): "/>
-            <dcmsg help="This is the message that gets sent when you disconnect from a server." options="text" value="Disconnecting from server..."/>
-            <buffersize help="This is the amount of backscroll allowed." options="int" value="1000"/>
-            <PurgeAtBuffersizeTimesLines help="This option tells the program when to purge old history.\nWhen the buffer exceeds this number times the buffersize\nall history is removed and you are just left with a history \nas large as the number you set your buffersize to." options="int" value="5"/>
-            <AutoPurgeAfterSave catagory="chat" help="When saving your log, this option will either automaticaly purge the buffer or not." options="bool" value="0"/>
-            <TypingStatusAlias options="Any text" value="Typing" help="This is the text displayed in the Player list under status while you are typing." />
-            <IdleStatusAlias options="Any text" value="Idle" help="This is the text displayed in the Player list under status while you are not typing." />
-            <SuppressChatAutoComplete options="bool" value="0" help="Setting this to 1 will turn off auto complete in chat." />
-            <ShowIDInChat options="bool" value="1" help="Set this to have the Player Id show up next to the player name in chat." />
-            <TimeStampGameLog options="bool" value="1" help="Set this to 1 to have time stamps added to the log." />
-            <GameLogPrefix help="This text is the files name minus the extention of your log file." options="Any text" value="logs/Log "/>
-            <dieroller options="std, wod, d20, hero" value="std" help="This sets the dieroller to use." />
-        </tab>
-        <tab name="Chat Tabs" type="grid">
-            <tabbedwhispers help="Set this to 1 to receive whispered messages in separate chat tabs ." options="bool" value="1"/>
-            <GMWhisperTab help="Creates a tab for all GM whispers, tabbedwhispers being on is required for this too work" options="bool" value="1"/>
-            <GroupWhisperTab help="Creates a tab for all Group whispers, tabbedwhispers being on is required for this too work" options="bool" value="1"/>
-        </tab>
-        <tab name="Chat Toolbars" type="grid">
-            <Toolbar_On help="Turns the toolbar on or off" options="bool" value="1"/>
-            <DiceButtons_On help="Show the dice buttons in the toolbar?" options="bool" value="1"/>
-            <FormattingButtons_On help="Show the Formatting Buttons (Bold, italic, underline, color) in the toolbar?" options="bool" value="1"/>
-            <AliasTool_On help="Show the Alias Tool in the toolbar?" options="bool" value="1"/>
-            <aliasfile help="This is the filename of your last loaded Alias Lib" options="filename" value="sample" />
-        </tab>
-        <tab name="Chat Macros" type="grid">
-            <F1 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F1 macro key"/>
-            <F2 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F2 macro key"/>
-            <F3 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F3 macro key"/>
-            <F4 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F4 macro key"/>
-            <F5 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F5 macro key"/>
-            <F6 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F6 macro key"/>
-            <F7 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F7 macro key"/>
-            <F8 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F8 macro key"/>
-            <F9 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F9 macro key"/>
-            <F10 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F10 macro key"/>
-            <F11 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F11 macro key"/>
-            <F12 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F12 macro key"/>
-        </tab>
-        <tab name="Chat Styles" type="tab">
-            <tab name="Chat Colors" type="grid">
-                <bgcolor help="This is the background color of the chat window." options="color in hex RRGGBB" value="#ffffff"/>
-                <textcolor help="This is the default color used when text is printed into the chat window." options="color in hex RRGGBB" value="#000000"/>
-                <mytextcolor help="This is the color of your text in the chat window." options="color in hex RRGGBB" value="#000080"/>
-                <syscolor help="This is the color of system messages printed in the chat window." options="color in hex RRGGBB" value="#ff0000"/>
-                <infocolor help="This is the color of informational messages printed in the chat window." options="color in hex RRGGBB" value="#ff8000"/>
-                <emotecolor help="This is the color of your emotes in the chat window." options="color in hex RRGGBB" value="#008000"/>
-                <whispercolor help="This is the color of whisper messages in the chat window." options="color in hex RRGGBB" value="#ff8000"/>
-            </tab>
-            <tab name="Fonts" type="grid">
-                <defaultfont help="Set this to a preferred font to use at startup." options="a font name" value="Arial"/>
-                <defaultfontsize help="Set this to a preferred fontsize to use at startup." options="a font size" value="10"/>
-            </tab>
-        </tab>
+    <tab name="Chat Toolbars" type="grid">
+      <Toolbar_On help="Turns the toolbar on or off" options="bool" value="1" />
+      <DiceButtons_On help="Show the dice buttons in the toolbar?" options="bool" value="1" />
+      <FormattingButtons_On help="Show the Formatting Buttons (Bold, italic, underline, color) in the toolbar?" options="bool" value="1" />
+      <AliasTool_On help="Show the Alias Tool in the toolbar?" options="bool" value="1" />
+      <aliasfile help="This is the filename of your last loaded Alias Lib" options="filename" value="sample" />
+    </tab>
+    <tab name="Chat Macros" type="grid">
+      <F1 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F1 macro key" />
+      <F2 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F2 macro key" />
+      <F3 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F3 macro key" />
+      <F4 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F4 macro key" />
+      <F5 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F5 macro key" />
+      <F6 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F6 macro key" />
+      <F7 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F7 macro key" />
+      <F8 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F8 macro key" />
+      <F9 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F9 macro key" />
+      <F10 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F10 macro key" />
+      <F11 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F11 macro key" />
+      <F12 help="What you enter here will be sent to chat when this function key is pressed." options="text" value="/me found the F12 macro key" />
     </tab>
-    <tab name="Player List" type="grid">
-        <ColorizeRoles options="bool" value="1" help="Colorizes roles in the player list." />
-        <GMRoleColor help="Set the color for the GM Role" options="color in hex RRGGBB" value="#FF0000"/>
-        <PlayerRoleColor help="Set the color for the Player Role" options="color in hex RRGGBB" value="#000000"/>
-        <LurkerRoleColor help="Set the color for the Lurker Role" options="color in hex RRGGBB" value="#c6c6c6"/>
-    </tab>
-    <tab name="Map" type="grid">
-        <ImageCacheSize options="int" value="32" help="This sets the number of images to cache." />
-        <AlwaysShowMapScale options="bool" value="0" help="Setting this to 1 will keep the map scale displayed in the upper left corner of the map." />
+    <tab name="Chat Styles" type="tab">
+      <tab name="Chat Colors" type="grid">
+        <bgcolor help="This is the background color of the chat window." options="color in hex RRGGBB" value="#ffffff" />
+        <textcolor help="This is the default color used when text is printed into the chat window." options="color in hex RRGGBB" value="#000000" />
+        <mytextcolor help="This is the color of your text in the chat window." options="color in hex RRGGBB" value="#000080" />
+        <syscolor help="This is the color of system messages printed in the chat window." options="color in hex RRGGBB" value="#ff0000" />
+        <infocolor help="This is the color of informational messages printed in the chat window." options="color in hex RRGGBB" value="#ff8000" />
+        <emotecolor help="This is the color of your emotes in the chat window." options="color in hex RRGGBB" value="#008000" />
+        <whispercolor help="This is the color of whisper messages in the chat window." options="color in hex RRGGBB" value="#ff8000" />
+      </tab>
+      <tab name="Fonts" type="grid">
+        <defaultfont help="Set this to a preferred font to use at startup." options="a font name" value="Arial" />
+        <defaultfontsize help="Set this to a preferred fontsize to use at startup." options="a font size" value="10" />
+      </tab>
     </tab>
-    <tab name="Game Tree" type="grid">
-        <LoadGameTreeFeatures options="bool" value="1" help="Setting this to 1 will load the gametree features next time you run OpenRPG." />
-        <treedclick options="use, design, print, chat" value="use" help="This sets the action performed on a node when you double click it in the game tree." />
-        <SaveGameTreeOnExit help="Set this to 1 if you want your game tree to automaticaly be saved when you log out." options="bool" value="1"/>
-        <gametree help="This is the path on your computer pointing to the xml file\n for your tree." options="URL" value="myfiles/tree.xml"/>
-    </tab>
-    <tab name="Sever Colors" type="grid">
-        <RoomColor_Lobby  help="Sets the color used to display the 'Lobby' in the Room List in the Gameserver window" options="hex" value="#000080"/>
-        <RoomColor_Empty  help="Sets the color used to display empty rooms in the Room List in the Gameserver window" options="hex" value="#bebebe"/>
-        <RoomColor_Locked help="Sets the color used to display password protected rooms in the Room List in the Gameserver window" options="hex" value="#b70000"/>
-        <RoomColor_Active help="Sets the color used to display non-passworded non-empty rooms in the Room List in the Gameserver window" options="hex" value="#000000"/>
-    </tab>
-    <tab name="Plugins" type="tab">
-    </tab>
-</ini>
+  </tab>
+  <tab name="Player List" type="grid">
+    <ColorizeRoles options="bool" value="1" help="Colorizes roles in the player list." />
+    <GMRoleColor help="Set the color for the GM Role" options="color in hex RRGGBB" value="#FF0000" />
+    <PlayerRoleColor help="Set the color for the Player Role" options="color in hex RRGGBB" value="#000000" />
+    <LurkerRoleColor help="Set the color for the Lurker Role" options="color in hex RRGGBB" value="#c6c6c6" />
+  </tab>
+  <tab name="Map" type="grid">
+    <ImageCacheSize options="int" value="32" help="This sets the number of images to cache." />
+    <AlwaysShowMapScale options="bool" value="0" help="Setting this to 1 will keep the map scale displayed in the upper left corner of the map." />
+  </tab>
+  <tab name="Game Tree" type="grid">
+    <LoadGameTreeFeatures options="bool" value="1" help="Setting this to 1 will load the gametree features next time you run OpenRPG." />
+    <treedclick options="use, design, print, chat" value="use" help="This sets the action performed on a node when you double click it in the game tree." />
+    <SaveGameTreeOnExit help="Set this to 1 if you want your game tree to automaticaly be saved when you log out." options="bool" value="1" />
+    <gametree help="This is the path on your computer pointing to the xml file\n for your tree." options="URL" value="myfiles/tree.xml" />
+    <Use_Readonly_On help="Fields displayed within a Use dialog are read-only if they contain references.  The value of the reference is displayed.  Change these fields through the Design dialog." options="bool" value="1" />
+  </tab>
+  <tab name="Sever Colors" type="grid">
+    <RoomColor_Lobby help="Sets the color used to display the 'Lobby' in the Room List in the Gameserver window" options="hex" value="#000080" />
+    <RoomColor_Empty help="Sets the color used to display empty rooms in the Room List in the Gameserver window" options="hex" value="#bebebe" />
+    <RoomColor_Locked help="Sets the color used to display password protected rooms in the Room List in the Gameserver window" options="hex" value="#b70000" />
+    <RoomColor_Active help="Sets the color used to display non-passworded non-empty rooms in the Room List in the Gameserver window" options="hex" value="#000000" />
+  </tab>
+  <tab name="Plugins" type="tab"></tab>
+</ini>
\ No newline at end of file
--- a/orpg/templates/feature.xml	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/templates/feature.xml	Mon Mar 22 18:38:22 2010 -0600
@@ -49,9 +49,6 @@
       <nodehandler class="file_loader" icon="gear" module="core" name="Create New Chat Macro" version="1.0">
         <file name="macro.xml" />
       </nodehandler>
-      <nodehandler class="file_loader" icon="gear" module="core" name="Create New Bonus" version="1.0">
-        <file name="bonus.xml" />
-      </nodehandler>
       <nodehandler class="file_loader" icon="gear" module="core" name="Create New Miniature Library Tool" version="1.0">
         <file name="minlib.xml" />
       </nodehandler>
--- a/orpg/tests/test_client.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/tests/test_client.py	Mon Mar 22 18:38:22 2010 -0600
@@ -23,9 +23,6 @@
 def test_ping_timer_exists(client):
     assert hasattr(client, 'ping_timer')
 
-def test_DiceManager_exists(client):
-    assert hasattr(client, 'DiceManager')
-
 def test_password_manager_exists(client):
     assert hasattr(client, 'password_manager')
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tests/test_commands.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,36 @@
+import py
+from orpg.chat.commands import command_args_parser
+
+def pytest_generate_tests(metafunc):
+    if metafunc.function.__name__ == "test_parser":
+        for funcargs in test_parser_params:
+            metafunc.addcall(funcargs=funcargs)
+    else:
+        metafunc.addcall()
+
+test_parser_params = [
+{'cmdargs':'', 'expected_list':[], 'expected_map':{}},
+{'cmdargs':'one', 'expected_list':['one'], 'expected_map':{}},
+{'cmdargs':'one two three', 'expected_list':['one','two','three'], 'expected_map':{}},
+{'cmdargs':'"quoted1"', 'expected_list':['quoted1'], 'expected_map':{}},
+{'cmdargs':"'quoted2'", 'expected_list':['quoted2'], 'expected_map':{}},
+{'cmdargs':"'one' 'two'", 'expected_list':['one','two'], 'expected_map':{}},
+{'cmdargs':'"quoted spaces and = sign"', 'expected_list':['quoted spaces and = sign'], 'expected_map':{}},
+{'cmdargs':'"can\'t"', 'expected_list':["can't"], 'expected_map':{}},
+{'cmdargs':' one ', 'expected_list':['one'], 'expected_map':{}},
+{'cmdargs':' one two ', 'expected_list':['one','two'], 'expected_map':{}},
+{'cmdargs':'x=one', 'expected_list':[], 'expected_map':{'x':'one'}},
+{'cmdargs':'xyz_456=one', 'expected_list':[], 'expected_map':{'xyz_456':'one'}},
+{'cmdargs':'x=', 'expected_list':[], 'expected_map':{'x':''}},
+{'cmdargs':' x=one y="two" ', 'expected_list':[], 'expected_map':{'x':'one', 'y':'two'}},
+{'cmdargs':' x= y="" z=', 'expected_list':[], 'expected_map':{'x':'', 'y':'', 'z':''}},
+{'cmdargs':' one x=two "three" y="four"', 'expected_list':['one','three'], 'expected_map':{'x':'two', 'y':'four'}},
+{'cmdargs':'x=1 y=one x=2 y=two x=3', 'expected_list':[], 'expected_map':{'x':['1','2','3'], 'y':['one','two']}},
+]
+
+def test_parser(cmdargs, expected_list, expected_map):
+    result_list, result_map = command_args_parser(cmdargs)
+    assert result_list == expected_list
+    assert result_map == expected_map
+
+                
--- a/orpg/tests/test_dieroller.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/tests/test_dieroller.py	Mon Mar 22 18:38:22 2010 -0600
@@ -11,31 +11,50 @@
             metafunc.addcall(funcargs=funcargs)
 
 class TestDiceRolls:
-    params = {
-        'test_roll': [
-            [dict(roll='[1d20]', min_roll=1, max_roll=20) for x in xrange(100)],
-            [dict(roll='[1d20.minroll(5)]', min_roll=5,
+    _tests = [
+            [dict(roll='1d20', min_roll=1, max_roll=20) for x in xrange(100)],
+            [dict(roll='1d20.minroll(5)', min_roll=5,
                  max_roll=20) for x in xrange(100)],
-            [dict(roll='[4d6.each(5)]', min_roll=1, max_roll=6,
+            [dict(roll='4d6.each(5)', min_roll=1, max_roll=6,
                  num_results=4, extra=5) for x in xrange(100)],
-            [dict(roll='[4d6.takeHighest(3)]', min_roll=1, num_results=3,
+            [dict(roll='4d6.takeHighest(3)', min_roll=1, num_results=3,
+                 max_roll=6) for x in xrange(100)],
+            [dict(roll='(1+2+1)d6.takeHighest(3)', min_roll=1, num_results=3,
                  max_roll=6) for x in xrange(100)],
-            [dict(roll='[(1+2+1)d6.takeHighest(3)]', min_roll=1, num_results=3,
-                 max_roll=6) for x in xrange(100)],
-            [dict(roll='[4d6.takeHighest(3).minroll(3)]', min_roll=3,
-                 num_results=3, max_roll=6) for x in xrange(100)]],
+            [dict(roll='4d6.takeHighest(3).minroll(3)', min_roll=3,
+                 num_results=3, max_roll=6) for x in xrange(100)]]
+    params = {
+        'test_roll': _tests,
+        #'test_old_roll': _tests,
     }
 
     def test_roll(self, roll, min_roll, max_roll, num_results=1, extra=None):
-        from orpg.dieroller.utils import roller_manager
-        mgr = roller_manager()
-        results = eval(mgr.proccessRoll(roll))
+        from orpg.dieroller import roller_manager
+        r = roller_manager.process_roll(roll)
+        r = r.split('-->')[1].split('=')[1][1:].strip()
+        results = eval(r)
 
         assert isinstance(results, list)
-        assert len(results[0]) == num_results
+        assert len(results) == num_results
         for r in results:
             if extra is not None:
-                assert r[0][0] >= min_roll and r[0][0] <= max_roll
-                assert r[0][1] == extra
+                assert r[0] >= min_roll and r[0] <= max_roll
+                assert r[1] == extra
             else:
-                assert r[0] >= min_roll and r[0] <= max_roll
\ No newline at end of file
+                assert r >= min_roll and r <= max_roll
+
+    #def test_old_roll(self, roll, min_roll, max_roll, num_results=1, extra=None):
+        #from orpg.dieroller import roller_manager
+        #roller_manager.roller = 'old_std'
+        #r = roller_manager.process_roll(roll)
+        #r = r[3].split('=')[0].strip()
+        #results = eval(r)
+
+        #assert isinstance(results, list)
+        #assert len(results) == num_results
+        #for r in results:
+            #if extra is not None:
+                #assert r[0] >= min_roll and r[0] <= max_roll
+                #assert r[1] == extra
+            #else:
+                #assert r >= min_roll and r <= max_roll
\ No newline at end of file
--- a/orpg/tools/aliaslib.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/tools/aliaslib.py	Mon Mar 22 18:38:22 2010 -0600
@@ -1,6 +1,7 @@
 # Copyright (C) 2000-2001 The OpenRPG Project
 #
-#   openrpg-dev@lists.sourceforge.net
+#   openrpg-dev@lists.sourceforge.netfilter
+
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -117,9 +118,6 @@
         item = wx.MenuItem(transmitmenu, wx.ID_ANY, "Whisper\tCtrl+W", "Whisper")
         self.Bind(wx.EVT_MENU, self.OnMB_TransmitWhisper, item)
         transmitmenu.AppendItem(item)
-        item = wx.MenuItem(transmitmenu, wx.ID_ANY, "Macro\tCtrl+M", "Macro")
-        self.Bind(wx.EVT_MENU, self.OnMB_TransmitMacro, item)
-        transmitmenu.AppendItem(item)
         menu = wx.MenuBar()
         menu.Append(filemenu, "&File")
         menu.Append(aliasmenu, "&Alias")
@@ -312,6 +310,9 @@
 
     @debugging
     def OnMB_FilterDelete(self, event):
+        dlg = wx.MessageDialog(self, "Are you sure you want to delete that filter?","Delete Filter",wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
+        if dlg.ShowModal() != wx.ID_YES:
+            return
         if self.filterIdx != -1:
             self.selectFilterWnd.DeleteItem(self.filterIdx)
 
@@ -322,15 +323,11 @@
             defaultcolor = settings.get("mytextcolor")
             settings.set("mytextcolor", self.alias[1])
             self.chat.set_colors()
-        line = self.textWnd.GetValue().replace("\n", "<br />")
-        if self.checkFilterText.IsChecked() and self.filter != self.chat.defaultFilterName:
-            for rule in self.filterRegEx:
-                line = re.sub(rule[0], rule[1], line)
-        if len(line) > 1:
-            if len(line) > 1 and line[0] != "/":
-                self.chat.ParsePost(line, True, True)
-            else:
-                self.chat.chat_cmds.docmd(line)
+        line = self.textWnd.GetValue()
+        alias = self.alias[0]
+        if alias == self.chat.defaultAliasName:
+            alias = self.chat.session.get_my_info()[0]
+        self.chat.ParsePost(line, True, True, ParserContext(alias))
         if self.alias[1] != 'Default':
             settings.set("mytextcolor", defaultcolor)
             self.chat.set_colors()
@@ -341,11 +338,11 @@
     @debugging
     def OnMB_TransmitEmote(self, event):
         self.orpgframe.Freeze()
-        line = self.textWnd.GetValue().replace("\n", "<br />")
-        if self.checkFilterText.IsChecked() and self.filter != self.chat.defaultFilterName:
-            for rule in self.filterRegEx:
-                line = re.sub(rule[0], rule[1], line)
-        self.chat.emote_message(line)
+        line = self.textWnd.GetValue()
+        alias = self.alias[0]
+        if alias == self.chat.defaultAliasName:
+            alias = self.chat.session.get_my_info()[0]
+        self.chat.emote_message(line, ParserContext(alias))
         if self.checkClearText.IsChecked():
             self.textWnd.SetValue("")
         self.orpgframe.Thaw()
@@ -369,36 +366,12 @@
             selections = dlg.get_selections()
             for s in selections:
                 sendto.append(players[s][2])
-        line = self.textWnd.GetValue().replace("\n", "<br />")
-        if self.checkFilterText.IsChecked() and self.filter != self.chat.defaultFilterName:
-            for rule in self.filterRegEx:
-                line = re.sub(rule[0], rule[1], line)
+        line = self.textWnd.GetValue()
         if len(sendto):
-            self.chat.whisper_to_players(line, sendto)
-        if self.alias[1] != 'Default':
-            settings.set("mytextcolor", defaultcolor)
-            self.chat.set_colors()
-        if self.checkClearText.IsChecked():
-            self.textWnd.SetValue("")
-        self.orpgframe.Thaw()
-
-    @debugging
-    def OnMB_TransmitMacro(self, event):
-        self.orpgframe.Freeze()
-        if self.alias[1] != 'Default':
-            defaultcolor = settings.get("mytextcolor")
-            settings.set("mytextcolor", self.alias[1])
-            self.chat.set_colors()
-        lines = self.textWnd.GetValue().split("\n")
-        if self.checkFilterText.IsChecked() and self.filter != self.chat.defaultFilterName:
-            for rule in self.filterRegEx:
-                line = re.sub(rule[0], rule[1], line)
-        for line in lines:
-            if len(line) > 1:
-                if line[0] != "/":
-                    self.chat.ParsePost(line, True, True)
-                else:
-                    self.chat.chat_cmds.docmd(line)
+            alias = self.alias[0]
+            if alias == self.chat.defaultAliasName:
+                alias = self.chat.session.get_my_info()[0]
+            self.chat.whisper_to_players(line, sendto, ParserContext(alias))
         if self.alias[1] != 'Default':
             settings.set("mytextcolor", defaultcolor)
             self.chat.set_colors()
@@ -456,17 +429,14 @@
         self.sayBtn = wx.Button(self, wx.ID_ANY, "Say")
         self.emoteBtn = wx.Button(self, wx.ID_ANY, "Emote")
         self.whisperBtn = wx.Button(self, wx.ID_ANY, "Whisper")
-        self.macroBtn = wx.Button(self, wx.ID_ANY, "Macro")
         self.doneBtn = wx.Button(self, wx.ID_ANY, "Done")
         self.Bind(wx.EVT_BUTTON, self.OnMB_TransmitSend, self.sayBtn)
         self.Bind(wx.EVT_BUTTON, self.OnMB_TransmitEmote, self.emoteBtn)
         self.Bind(wx.EVT_BUTTON, self.OnMB_TransmitWhisper, self.whisperBtn)
-        self.Bind(wx.EVT_BUTTON, self.OnMB_TransmitMacro, self.macroBtn)
         self.Bind(wx.EVT_BUTTON, self.OnMB_FileExit, self.doneBtn)
         self.bottomBtnSizer.Add(self.sayBtn, 0, wx.EXPAND)
         self.bottomBtnSizer.Add(self.emoteBtn, 0, wx.EXPAND)
         self.bottomBtnSizer.Add(self.whisperBtn, 0, wx.EXPAND)
-        self.bottomBtnSizer.Add(self.macroBtn, 0, wx.EXPAND)
         self.bottomBtnSizer.Add(self.doneBtn, 0, wx.EXPAND|wx.ALIGN_RIGHT)
 
     @debugging
@@ -504,7 +474,7 @@
                 color = alias.get("color")
             else:
                 color = 'Default'
-            aname = self.MakeSafeHTML(alias.get("name"))
+            aname = self.MakeSafeHTML(alias.get("name", ''))
             alist.append([aname, color])
         alist.sort()
         self.aliasList = alist
@@ -514,7 +484,7 @@
             flist.append(filter.get("name"))
             sub = []
             for rule in filter.findall("rule"):
-                sub.append([self.MakeSafeHTML(rule.get("match")), self.MakeSafeHTML(rule.get("sub"))])
+                sub.append([self.MakeSafeHTML(rule.get("match", '')), self.MakeSafeHTML(rule.get("sub", ''))])
             self.regExList.append(sub)
         self.filterList = flist
         self.alias = -1
--- a/orpg/tools/orpg_settings.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/tools/orpg_settings.py	Mon Mar 22 18:38:22 2010 -0600
@@ -127,7 +127,7 @@
         rgbconvert = RGBHex()
 
         for i in xrange(0,len(self.changes)):
-            settings.set(self.changes[i][0], self.changes[i][1])
+            settings.set(str(self.changes[i][0]), self.changes[i][1])
             top_frame = open_rpg.get_component('frame')
 
             if self.changes[i][0] == 'bgcolor' or self.changes[i][0] == 'textcolor':
@@ -253,7 +253,7 @@
         self.SetColLabelValue(0,"Setting")
         self.SetColLabelValue(1,"Value")
         self.SetColLabelValue(2,"Available Options")
-        settings = grid_settings
+        self.settings = grid_settings
         for i in range(len(grid_settings)):
             self.SetCellValue(i, 0, grid_settings[i][0])
             self.SetCellValue(i, 1, grid_settings[i][1])
@@ -269,7 +269,7 @@
             return
         elif col == 0:
             name = self.GetCellValue(row,0)
-            str = settings[row][3]
+            str = self.settings[row][3]
             msg = wx.MessageBox(str,name)
             return
         elif col == 1:
--- a/orpg/tools/predTextCtrl.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/orpg/tools/predTextCtrl.py	Mon Mar 22 18:38:22 2010 -0600
@@ -298,6 +298,7 @@
 #    findWord(self,insert,st)
 
 #converted to ExpandoTextCtrl for multi-line input,  Credit: sirebral  -- SD 8-09
+#if you put it back to a TextCtrl remove the adjustment line from OnChar in chatwnd.py
 class predTextCtrl(ExpandoTextCtrl):
 
     # Initialization subroutine.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/old_rollers/__init__.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,8 @@
+import os, os.path
+
+rollers = 'plugins.old_rollers'
+rollers_path = __import__(rollers, {}, {}, [rollers.split('.')[-1]]).__path__
+
+for roller in os.listdir(os.path.abspath(os.path.dirname(__file__))):
+    if roller.endswith('.py') and not roller.startswith('_'):
+        __import__("%s.%s" % (rollers, roller.split('.')[0]))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/old_rollers/gurps.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,697 @@
+#This program is free software; you can redistribute it and/or
+#modify it under the terms of the GNU General Public License
+#as published by the Free Software Foundation; either version 2
+#of the License, or (at your option) any later version.
+#
+#This program is distributed in the hope that it will be useful,
+#but WITHOUT ANY WARRANTY; without even the implied warranty of
+#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#GNU General Public License for more details.
+#
+#You should have received a copy of the GNU General Public License
+#along with this program; if not, write to the Free Software
+#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# --
+#
+# File: gurps.py
+# Version:
+#   $Id: gurps.py,v 1.3
+#
+# Description: Modified Hero System die roller based on DJM and Heroman's Hero
+# dieroller
+#
+# GURPS is a trademark of Steve Jackson Games, and its rules and art are
+# copyrighted by Steve Jackson Games. All rights are reserved by Steve Jackson
+# Games. This game aid is the original creation of Naryt with help from Pyrandon
+# and is released for free distribution, and not for resale, under the
+# permissions granted in the Steve Jackson Games Online Policy.
+# http://www.sjgames.com/general/online_policy.html
+#
+# Errors should be reported to rpg@ormonds.net
+#
+# Changelog:
+# V 1.3  2007/03/23  Thomas M. Edwards <tmedwards@motoslave.net>
+#   Fixed gurpsskill, gurpsdefaultskill, and gurpssupernatural to correctly
+#   return a normal failure when the roll is 17 and the effective skill is 27+;
+#   previously, they would erroneously return a critical failure.  This fix also
+#   corrects the less serious issue whereby rolls of 17 and an effective skill
+#   of 17-26 would report "failure by X" instead of merely "failure", which is
+#   wrong as the only reason the roll failed was because a 17 was rolled, not
+#   because the roll exceeded the effective skill.
+# V 1.2 29 October 2006, added defaultskill (Rule of 20 [B344]), supernatural
+#   (Rule of 16 [B349]).  The frightcheck roll is now the actual Fright Check
+#   (with Rule of 14 [B360]) and a lookup oon the Fright Check Table if needed.
+#   The fightcheckfail roll is the old Fright Check Table lookup.
+#   Removes the Help roller as it was nothing but trouble, see
+#   http://openrpg.wrathof.com/repository/GURPS/GURPS_Roller_1.7.xml for help
+#   in using this roller.
+# V 1 Original gurps release 2006/05/28 00:00:00, modified crit_hit, crit_headblow, crit_miss, crit_unarm, spellfail, frightcheck and help_me
+#       Corrects numerous descriptions
+# v.1 original gurps release by Naryt 2005/10/17 16:34:00
+
+from time import time, clock
+import random
+
+from std import std
+from orpg.dieroller.base import *
+
+
+__version__ = "$Id: gurps.py,v 1.5 2007/05/06 16:42:55 digitalxero Exp $"
+
+# gurps
+
+class gurps(std):
+    name = "gurps"
+
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+
+# these methods return new die objects for specific options
+
+# Original msk roll renamed to be easier to understand/remember
+    def skill(self,skill,mod):
+        return gurpsskill(self,skill,mod)
+
+    def defaultskill(self,stat,defaultlevel,mod):
+        return gurpsdefaultskill(self,stat,defaultlevel,mod)
+
+    def supernatural(self,skill,resistance,mod):
+        return gurpssupernatural(self,skill,resistance,mod)
+
+    def crit_hit(self):
+        return gurpscrit_hit(self)
+
+    def crit_headblow(self):
+        return gurpscrit_headblow(self)
+
+    def crit_miss(self):
+        return gurpscrit_miss(self)
+
+    def crit_unarm(self):
+        return gurpscrit_unarm(self)
+
+    def spellfail(self):
+        return gurpsspellfail(self)
+
+    def frightcheck(self,level,mod):
+        return gurpsfrightcheck(self,level,mod)
+
+    def frightcheckfail(self,mod):
+        return gurpsfrightcheckfail(self,mod)
+
+die_rollers.register(gurps)
+
+class gurpsskill(std):
+    def __init__(self,source=[],skill=0,mod=0):
+        std.__init__(self,source)
+        self.skill = skill
+        self.mod = mod
+
+    def is_success(self):
+        return (((self.sum()) <= self.skill+self.mod) and (self.sum() < 17))
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr +="]"
+        myStr += " = <b>" + str(self.sum()) + "</b>"
+        myStr += " vs <b>(" + str(self.skill+self.mod) + ")</b>"
+
+        Diff = abs((self.skill+self.mod) - self.sum())
+
+        if self.is_success():
+            if self.sum() == 3 or self.sum() == 4:
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b></font> [B556]"
+            elif self.sum() == 5 and (self.skill+self.mod > 14):
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +" </font> [B556]"
+            elif self.sum() == 6 and (self.skill+self.mod > 15):
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +" </font> [B556]"
+            else:
+                myStr += " or less <font color='#ff0000'><b>Success!</b> by " + str(Diff) +" </font>"
+        else:
+            if self.sum() == 18:
+                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
+#            elif self.sum() == 17 and (self.skill+self.mod < 16):
+#                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
+            elif self.sum() == 17:
+                if (self.skill+self.mod) < 16:
+                    myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
+                else:
+                    myStr += " or less <font color='#ff0000'><b>Failure!</b></font> [B556]"
+            elif  Diff > 9:
+                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b> by " + str(Diff) +" </font> [B556]"
+            else:
+                myStr += " or less <font color='#ff0000'><b>Failure!</b> by " + str(Diff) +" </font>"
+
+        return myStr
+
+class gurpsdefaultskill(std):
+    def __init__(self,source=[],stat=0,defaultlevel=0,mod=0):
+        std.__init__(self,source)
+        self.stat = stat
+        self.defaultlevel = defaultlevel
+        self.mod = mod
+
+    def is_success(self):
+        if self.stat < 21:
+            intSkillVal = self.stat + self.defaultlevel + self.mod
+        else:
+            intSkillVal = 20 + self.defaultlevel + self.mod
+        return (((self.sum()) <= intSkillVal) and (self.sum() < 17))
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr +="]"
+        myStr += " = <b>" + str(self.sum()) + "</b>"
+        strRule = ""
+        if self.stat < 21:
+            intSkillVal = self.stat + self.defaultlevel + self.mod
+        else:
+            intSkillVal = 20 + self.defaultlevel + self.mod
+            strRule = "<br />Rule of 20 in effect [B173, B344]"
+
+        myStr += " vs <b>(" + str(intSkillVal) + ")</b>"
+
+        Diff = abs((intSkillVal) - self.sum())
+
+        if self.is_success():
+            if self.sum() == 3 or self.sum() == 4:
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b></font> [B556]"
+            elif self.sum() == 5 and (intSkillVal > 14):
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +"</font> [B556]"
+            elif self.sum() == 6 and (intSkillVal > 15):
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +"</font> [B556]"
+            else:
+                myStr += " or less <font color='#ff0000'><b>Success!</b> by " + str(Diff) +"</font>"
+        else:
+            if self.sum() == 18:
+                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
+            elif self.sum() == 17:
+                if intSkillVal < 16:
+                    myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
+                else:
+                    myStr += " or less <font color='#ff0000'><b>Failure!</b></font> [B556]"
+            elif  Diff > 9:
+                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b> by " + str(Diff) +"</font> [B556]"
+            else:
+                myStr += " or less <font color='#ff0000'><b>Failure!</b> by " + str(Diff) +"</font>"
+
+        myStr += strRule
+        return myStr
+
+class gurpssupernatural(std):
+    def __init__(self,source=[],skill=0,resistance=0,mod=0):
+        std.__init__(self,source)
+        self.skill = skill
+        self.resistance = resistance
+        self.mod = mod
+
+    def is_success(self):
+        if self.skill+self.mod > 16:
+            if self.resistance > 16:
+                if self.resistance > self.skill+self.mod:
+                    newSkill = self.skill+self.mod
+                else:
+                    newSkill = self.resistance
+            else:
+                newSkill = 16
+        else:
+            newSkill = self.skill+self.mod
+        return (((self.sum()) <= newSkill) and (self.sum() < 17))
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr +="]"
+        myStr += " = <b>" + str(self.sum()) + "</b>"
+        strRule = ""
+        if self.skill+self.mod > 16:
+            if self.resistance > 16:
+                if self.resistance > self.skill+self.mod:
+                    newSkill = self.skill+self.mod
+                    strRule = "<br />Rule of 16:  Subject's Resistance is higher than skill, no change in skill [B349]"
+                else:
+                    newSkill = self.resistance
+                    strRule = "<br />Rule of 16:  Effective skill limited by subject's Resistance [B349]"
+            else:
+                newSkill = 16
+                strRule = "<br />Rule of 16:  Effective skill limited to 16 [B349]"
+        else:
+            newSkill = self.skill+self.mod
+        myStr += " vs <b>(" + str(newSkill) + ")</b>"
+
+        Diff = abs((newSkill) - self.sum())
+
+        if self.is_success():
+            if self.sum() == 3 or self.sum() == 4:
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b></font> [B556]"
+            elif self.sum() == 5 and (newSkill > 14):
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +" </font> [B556]"
+            elif self.sum() == 6 and (newSkill > 15):
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b> by " + str(Diff) +" </font> [B556]"
+            else:
+                myStr += " or less <font color='#ff0000'><b>Success!</b> by " + str(Diff) +" </font>"
+        else:
+            if self.sum() == 18:
+                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
+            elif self.sum() == 17:
+                if newSkill < 16:
+                    myStr += " or less <font color='#ff0000'><b>Critical Failure!</b></font> [B556]"
+                else:
+                    myStr += " or less <font color='#ff0000'><b>Failure!</b></font> [B556]"
+            elif  Diff > 9:
+                myStr += " or less <font color='#ff0000'><b>Critical Failure!</b> by " + str(Diff) +" </font> [B556]"
+            else:
+                myStr += " or less <font color='#ff0000'><b>Failure!</b> by " + str(Diff) +" </font>"
+
+        myStr += strRule
+        return myStr
+
+class gurpscrit_hit(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0]) #Variable myStr holds text and first we put a [ into it and then adds the first die rolled
+        for a in self.data[1:]:             #This is a for loop.  It will do the next two lines of code for every die (except the first die which we handled in the line above) in the roll.
+            myStr += ","                  #Adds a comma after each die
+            myStr += str(a)           #Adds the value of each die.
+        myStr += "] = "                 #Adds ] = to the end of the string (note the += means append to whatever is already stored in the variable
+        myStr += str(self.sum())          #Finally we add the actual result of the roll and myStr contains something like [3,2,1] = 6
+
+        if self.sum() > 8 and self.sum() < 12:
+            myStr += " <font color='#ff0000'>The blow inflicts normal damage.</font> [B556]"
+        elif self.sum() == 12:
+            myStr += " <font color='#ff0000'>The blow inflicts normal damage, AND victim drops anything they hold--even if no damage penetrates DR.</font> [B556]"
+        elif self.sum() == 8:
+            myStr += " <font color='#ff0000'>Damage penetrating DR does double shock (-8 max) AND if it hits the victim's limb, it's crippled for 16-HT seconds (unless wound is enough to cripple permanently!).</font> [B556]"
+        elif self.sum() == 13 or self.sum() == 14 or self.sum() == 7:
+            myStr += " <font color='#ff0000'>If any damage penetrates DR, treat as major wound. See [B420] for major wounds.</font> [B556]"
+        elif self.sum() == 6 or self.sum() == 15:
+            myStr += " <font color='#ff0000'>The blow inflicts maximum normal damage.</font> [B556]"
+        elif self.sum() == 5 or self.sum() == 16:
+            myStr += " <font color='#ff0000'>The blow inflicts double damage.</font> [B556]"
+        elif self.sum() == 4 or self.sum() == 17:
+            myStr += " <font color='#ff0000'>The victim's DR protects at half value, rounded down, after applying any armor divisors.</font> [B556]"
+        elif self.sum() == 3 or self.sum() == 18 :
+            myStr += " <font color='#ff0000'>The blow inflicts triple damage.</font> [B556]"
+
+        return myStr
+
+class gurpscrit_headblow(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0]) #Variable myStr holds text and first we put a [ into it and then adds the first die rolled
+        for a in self.data[1:]:             #This is a for loop.  It will do the next two lines of code for every die (except the first die which we handled in the line above) in the roll.
+            myStr += ","                  #Adds a comma after each die
+            myStr += str(a)           #Adds the value of each die.
+        myStr += "] = "                 #Adds ] = to the end of the string (note the += means append to whatever is already stored in the variable
+        myStr += str(self.sum())          #Finally we add the actual result of the roll and myStr contains something like [3,2,1] = 6
+
+        if self.sum() > 8 and self.sum() < 12:
+            myStr += " <font color='#ff0000'>The blow inflicts normal damage.</font> [B556]"
+        elif self.sum() == 12 or self.sum() == 13:
+            myStr += " <font color='#ff0000'>Normal damage to the head, BUT if any penetrates DR victim is scarred (-1 to appearance, -2 if burning or corrosive attacks) OR, if <i>crushing</i> then victim deafened [see B422 for duration].</font> [B556]"
+        elif self.sum() == 8:
+            myStr += " <font color='#ff0000'>Normal damage to head, but victim knocked off balance: must Do Nothing until next turn (but can defend).</font> [B556]"
+        elif self.sum() == 14:
+            myStr += " <font color='#ff0000'>Normal damage to head, but victim drops their weapon.  If holding two weapons, roll randomly for which one is dropped.</font> [B556]"
+        elif self.sum() == 6 or self.sum() == 7:
+            myStr += " <font color='#ff0000'>If you aimed for face or skull, you hit an eye [B399];  otherwise, DR only half effective & if even 1 point damage penetrates it's a major wound [B420].  If you hit an eye and that should be impossible, treat as if a <b>4</b> were rolled, see [B556].</font> [B556]"
+        elif self.sum() == 15:
+            myStr += " <font color='#ff0000'>The blow inflicts maximum normal damage.</font> [B556]"
+        elif self.sum() == 16:
+            myStr += " <font color='#ff0000'>The blow inflicts double damage.</font> [B556]"
+        elif self.sum() == 4 or self.sum() == 5:
+            myStr += " <font color='#ff0000'>The victim's DR protects at half value, rounded up, after applying armor divisors AND if even 1 point penetrates it's a major wound [B420].</font> [B556]"
+        elif self.sum() == 17:
+            myStr += " <font color='#ff0000'>The victim's DR protects at half value, rounded up, after applying any armor divisors.</font> [B556]"
+        elif self.sum() == 3:
+            myStr += " <font color='#ff0000'>The blow inflicts maximum normal damage AND ignores all DR.</font> [B556]"
+        elif self.sum() == 18:
+            myStr += " <font color='#ff0000'>The blow inflicts triple damage.</font> [B556]"
+
+        return myStr
+
+class gurpscrit_miss(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0]) #Variable myStr holds text and first we put a [ into it and then adds the first die rolled
+        for a in self.data[1:]:             #This is a for loop.  It will do the next two lines of code for every die (except the first die which we handled in the line above) in the roll.
+            myStr += ","                  #Adds a comma after each die
+            myStr += str(a)           #Adds the value of each die.
+        myStr += "] = "                 #Adds ] = to the end of the string (note the += means append to whatever is already stored in the variable
+        myStr += str(self.sum())          #Finally we add the actual result of the roll and myStr contains something like [3,2,1] = 6
+
+        if self.sum() > 8 and self.sum() < 12:
+            myStr += " <font color='#ff0000'>You drop your weapon (& a <i>cheap</i> weapon breaks).</font> [B556]"
+        elif self.sum() == 12 or self.sum() == 8:
+            myStr += " <font color='#ff0000'>Your weapon turns in your hand;  must Ready it before it can be used again.</font> [B556]"
+        elif self.sum() == 13 or self.sum() == 7:
+            myStr += " <font color='#ff0000'>You lose your balance & can do nothing else (not even free actions) until next turn;  all defenses -2 until next turn.</font> [B556]"
+        elif self.sum() == 14:
+            yrdStr = str(int(random.uniform(1,7)))
+            myStr += " <font color='#ff0000'>A <i>swung</i> weapon flies from hand " + yrdStr + " yards (50% chance straight forward/backward) anyone on the target of the flying weapon makes a DX roll or takes half-damage; a <i>thrust</i> or <i>ranged</i> weapon is dropped (& a <i>cheap</i> weapon breaks).</font> [B556]"
+        elif self.sum() == 6:
+            myStr += " <font color='#ff0000'>You hit yourself in arm or leg (50/50 chance), doing half damage;  if impaling, piercing, or ranged attack, then roll again (if you hit yourself again then use that result).</font> [B556]"
+        elif self.sum() == 15:
+            myStr += " <font color='#ff0000'>You strain your shoulder!  Weapon arm crippled for 30 min;  do not drop weapon, but that arm is useless.</font> [B557]"
+        elif self.sum() == 16:
+            myStr += " <font color='#ff0000'>If <i>melee attack,</i>  you fall down!  If <i>ranged attack,</i> you lose your balance & can do nothing until next turn & all defenses -2 until next turn.</font> [B557]"
+        elif self.sum() == 5:
+            myStr += " <font color='#ff0000'>You hit yourself in the arm or leg (50/50 chance), doing normal damage;  if impaling, piercing, or ranged attack, then roll again (if you hit yourself again then use that result).</font> [B556]"
+        elif self.sum() == 4 or self.sum() == 3 or self.sum() == 17 or self.sum() == 18:
+            broke = int(random.uniform(3,19))
+            if broke >=5 and broke <=16:
+                brokestr = "it is dropped."
+            else:
+                brokestr = "the weapon also breaks!"
+            myStr += " <font color='#ff0000'>A normal weapon breaks [B485];  if solid crushing weapon OR fine, very fine, or magical weapon " + brokestr + "</font> [B556] Note, second for roll non-normal weapons already fingured into this result."
+
+        return myStr
+
+class gurpscrit_unarm(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0]) #Variable myStr holds text and first we put a [ into it and then adds the first die rolled
+        for a in self.data[1:]:             #This is a for loop.  It will do the next two lines of code for every die (except the first die which we handled in the line above) in the roll.
+            myStr += ","                  #Adds a comma after each die
+            myStr += str(a)           #Adds the value of each die.
+        myStr += "] = "                 #Adds ] = to the end of the string (note the += means append to whatever is already stored in the variable
+        myStr += str(self.sum())          #Finally we add the actual result of the roll and myStr contains something like [3,2,1] = 6
+
+        if self.sum() > 8 and self.sum() < 12:
+            myStr += " <font color='#ff0000'>You lose your balance;  you can do nothing else (not even free actions) until next turn, and all defenses -2 until next turn.</font> [B557]"
+        elif self.sum() == 12:
+            myStr += " <font color='#ff0000'>You trip; make a DX roll to avoid falling at -4 if kicking or twice the normal penatly for a technique that normally requires a DX to avoid injury on even a normal failure (e.g., Jump Kick).</font> [B557]"
+        elif self.sum() == 8:
+            myStr += " <font color='#ff0000'>You fall down!</font> [B557]"
+        elif self.sum() == 13:
+            myStr += " <font color='#ff0000'>You drop your guard:  all defenses -2 for the next turn & any Evaluate bonus or Feint penalties against you are doubled.  This is obvious to those around you.</font> [B557]"
+        elif self.sum() == 7 or self.sum() == 14:
+            myStr += " <font color='#ff0000'>You stumble:  <i>If attacking,</i> you advance one yard past opponent with them behind you (you are facing away); <i>if parrying</i> you fall down!</font> [B557]"
+        elif self.sum() == 15:
+            mslStr = str(int(random.uniform(1,4)))
+            myStr += " <font color='#ff0000'>You tear a muscle; " + mslStr + " HT damage to the limb used to attack (to one limb if two used to attack), & -3 to use it (-1 w/high pain thresh); also all atacks & defenses -1 until next turn.  If neck was injured -3 (-1 w/high pain thresh) applies to ALL actions.</font> [B557]"
+        elif self.sum() == 6:
+            myStr += " <font color='#ff0000'>You hit a solid object (wall, floor, etc.) & take crushing damage equalt to 1/2 of (your thrusting damage - your DR) (<i>EXCEPTION:</i> If attacking with natural weapons, such as claws or teeth, they <i>break</i> -1 damage on future attacks until you heal (for recovery, B422).</font> [B557]"
+        elif self.sum() == 5 or self.sum() == 16:
+            myStr += " <font color='#ff0000'>You hit a solid object (wall, floor, etc.) & take crushing damage = your thrusting damage - your DR (<i>EXCEPTION:</i> if opponent using impaling weapon, you fall on it & take damage based on your ST).  If attacking an opponent who is using an impaling weapon, you fall on <i>his weapon</i>.  You suffer the weapon's normal damage based on <i>your</i> <b>ST</b>.</font> [B557]"
+        elif self.sum() == 4:
+            myStr += " <font color='#ff0000'>If attacking or parrying with a limb, you strain the limb:  1 HP damage & it's crippled for 30 min. If biting, butting, etc., have moderate neck pain (B428) for next 20-HT min minimum of 1 minute.</font> [B557]"
+        elif self.sum() == 17:
+            myStr += " <font color='#ff0000'>If attacking or parrying with a limb, you strain the limb:  1 HP damage & it's crippled for 30 min. If IQ 3-5 animal, it loses its nerve & flees on next turn or surrenders if cornered.</font> [B557]"
+        elif self.sum() == 3 or self.sum() == 18 :
+            myStr += " <font color='#ff0000'>You knock yourself out!  Roll vs. HT every 30 min. to recover.</font> [B557]"
+
+        return myStr
+
+class gurpsspellfail(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr +="]"
+        myStr += " = <b>" + str(self.sum()) + "</b>"
+
+        if self.sum() == 10 or self.sum() == 11:
+            myStr += " <font color='#ff0000'>Spell produces nothing but a loud noise, bright flash, awful odor, etc.</font> [B236]"
+        elif self.sum() == 9:
+            myStr += " <font color='#ff0000'>Spell fails entirely.  Caster is stunned (IQ roll to recover).</font> [B236]"
+        elif self.sum() == 12:
+            myStr += " <font color='#ff0000'>Spell produces a weak and useless shadow of the intended effect.</font> [B236]"
+        elif self.sum() == 8:
+            myStr += " <font color='#ff0000'>Spell fails entirely.  Caster takes 1 point of damage.</font> [B236]"
+        elif self.sum() == 13:
+            myStr += " <font color='#ff0000'>Spell produces the reverse of the intended effect.</font> [B236]"
+        elif self.sum() == 7:
+            myStr += " <font color='#ff0000'>Spell affects someone or something other than the intended subject.</font> [B236]"
+        elif self.sum() == 14:
+            myStr += " <font color='#ff0000'>Spell seems to work, but it is only a useless illusion.</font> [B236]"
+        elif self.sum() == 5 or self.sum() == 6:
+            myStr += " <font color='#ff0000'>Spell is cast on one of the caster's companions (if harmful) or a random nearby foe (if beneficial).</font> [B236]"
+        elif self.sum() == 15 or self.sum() == 16:
+            myStr += " <font color='#ff0000'>Spell has the reverse of the intended, on the wrong target.  Roll randomly.</font> [B236]"
+        elif self.sum() == 4:
+            myStr += " <font color='#ff0000'>Spell is cast on caster (if harmful) or on a random nearby foe (if beneficial).</font> [B236]"
+        elif self.sum() == 17:
+            myStr += " <font color='#ff0000'>Spell fails entirely.  Caster temporarily forgets the spell.  Make a weekly IQ roll (after a week passes) until the spell is remembered.</font> [B236]"
+        elif self.sum() == 3:
+            myStr += " <font color='#ff0000'>Spell fails entirely.  Caster takes 1d of injury.</font> [B236]"
+        elif self.sum() == 18:
+            myStr += " <font color='#ff0000'>Spell fails entirely.  A demon or other malign entity appears and attacks the caster.  (GM may waive this if the caster and spell were both lily-white, pure good in intent.)</font> [B236]"
+
+        return myStr
+
+class gurpsfrightcheck(std):
+    def __init__(self,source=[],skill=0,mod=0):
+        std.__init__(self,source)
+        self.skill = skill
+        self.mod = mod
+
+    def is_success(self):
+        return (((self.sum()) <= self.skill+self.mod) and (self.sum() < 14))
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr +="]"
+        myStr += " = <b>" + str(self.sum()) + "</b>"
+
+        if self.skill+self.mod < 14:
+            myStr += " vs <b>(" + str(self.skill+self.mod) + ")</b>"
+            Diff = abs((self.skill+self.mod) - self.sum())
+        else:
+            myStr += " vs <b>(13)</b>"
+            Diff = abs(13 - self.sum())
+
+        if self.is_success():
+            if self.sum() == 3 or self.sum() == 4:
+                myStr += " or less <font color='#ff0000'><b>Critical Success!</b></font> [B556]"
+            else:
+                myStr += " or less <font color='#ff0000'><b>Success!</b> by " + str(Diff) +" </font>"
+        else:
+            myStr += " or less <font color='#ff0000'><b>Failure!</b> by " + str(Diff) +" </font>"
+
+        if self.skill + self.mod > 13:
+            myStr += " Rule of 14 in effect [B360]"
+
+        if not(self.is_success()):
+            intD1 = int(random.uniform(1,7))
+            intD2 = int(random.uniform(1,7))
+            intD3 = int(random.uniform(1,7))
+            intFright = intD1 + intD2 + intD3 + Diff
+            myStr += "<br />Rolling on Fright Check Table<br />[" + str(intD1) + "," + str(intD2) + "," + str(intD3) + "] ==> " + str(intFright - Diff) + " +  " + str(Diff) + " = " + str(intFright) + "<br />"
+            if intFright < 6:
+                myStr += "<font color='#ff0000'>Stunned for one second, then recover automatically.</font> [B360]"
+            elif intFright < 8:
+                myStr += "<font color='#ff0000'>Stunned for one second.  Every second after that, roll vs. unmodified Will to snap out of it.</font> [B360]"
+            elif intFright < 10:
+                myStr += "<font color='#ff0000'>Stunned for one second.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B360]"
+            elif intFright == 10:
+                strStun = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Stunned for " + strStun + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B360]"
+            elif intFright == 11:
+                strStun = str(int(random.uniform(2,13)))
+                myStr += "<font color='#ff0000'>Stunned for " + strStun + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B361]"
+            elif intFright == 12:
+                myStr += "<font color='#ff0000'>Lose your lunch.  Treat this as retching for (25-HT) seconds, and then roll vs. HT each second to recover [B428].</font> [B361]"
+            elif intFright == 13:
+                myStr += "<font color='#ff0000'>Acquire a new mental quirk.</font> [B361]"
+            elif intFright < 16:
+                strFP = str(int(random.uniform(1,7)))
+                strSeconds = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Lose " + strFP + " FP, and stunned for " + strSeconds + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B361]"
+            elif intFright == 16:
+                strSeconds = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Stunned for " + strSeconds + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.  Acquire a new mental quirk.</font> [B361]"
+            elif intFright == 17:
+                strMinutes = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes.  Every minute after that roll vs. HT to recover.</font> [B361]"
+            elif intFright == 18:
+                strMinutes = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.</font> [B361]"
+            elif intFright == 19:
+                strMinutes = str(int(random.uniform(2,13)))
+                myStr += "<font color='#ff0000'>Severe faint, lasting for " + strMinutes + " minutes.  Every minute after that roll vs. HT to recover.  Take 1 HP of injury.</font> [B361]"
+            elif intFright == 20:
+                strMinutes = str(int(random.uniform(4,25)))
+                strFP = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Faint bordering on shock, lastering for " + strMinutes + " minutes.  Also, lose " + strFP + " FP.</font> [B361]"
+            elif intFright == 21:
+                strMinutes = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Panic.  You run around screaming, sit down and cry, or do something else equally pointless for " + strMinutes + " minutes.  Every minute after that, roll vs. unmodified Will to snap out of it.</font> [B361]"
+            elif intFright == 22:
+                myStr += "<font color='#ff0000'>Acquire a new -10-point Delusion (B130).</font> [B361]"
+            elif intFright == 23:
+                myStr += "<font color='#ff0000'>Acquire a new -10-point Phobia (B148) or other -10-point mental disadvantage.</font> [B361]"
+            elif intFright == 24:
+                myStr += "<font color='#ff0000'>Major physical effect, set by the GM: hair turns white, age five years overnight, go partially deaf, etc.  (Acquire -15 points worth of physical disadvantages.  Each year of aging = -3 points.)</font> [B361]"
+            elif intFright == 25 :
+                myStr += "<font color='#ff0000'>If you already have a Phobia or other mental disadvantage that is logically related to the frightening incident, your self-control number becomes one step worse.  If not, or if your self-control number is already 6, add a new -10-point Phobia or other -10-point mental disadvantage.</font> [B361]"
+            elif intFright == 26:
+                strMinutes = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.  Also acquire a new -10-point Delusion (B130).</font> [B361]"
+            elif intFright == 27:
+                strMinutes = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.  Also acquire a new -10-point Phobia (B148) or other -10-point mental disadvantage.</font> [B361]"
+            elif intFright == 28:
+                myStr += "<font color='#ff0000'>Light coma.  You fall unconscious, rolling vs. HT every 30 minutes to recover.  For 6 hours after you come to, all skill rolls and attribute checks are at -2.</font> [B361]"
+            elif intFright == 29:
+                strHours = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.</font> [B361]"
+            elif intFright == 30:
+                strDays = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Catatonia.  Stare into space for " + strDays + " days.  Then roll vs. HT.  On a failed roll, remain catatonic for another " + strDays + " days, and so on.  If you have no medical care, lose 1 HP the first day, 2 HP the second day and so on.  If you survive and awaken, all skill rolls and attribute checks are at -2 for as many days as the catatonia lasted.</font> [B361]"
+            elif intFright == 31:
+                strMinutes = str(int(random.uniform(1,7)))
+                strFP = str(int(random.uniform(1,7)))
+                strInjury = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Seizure.  You lose control of your body and fall to the ground in a fit lasting " + strMinutes + " minutes and costing " + strFP + " FP.  Also, roll vs. HT.  On a failure, take " + strInjury + " HP of injury.  On a critical failure, you also lose 1 HT <i>permanently</i>.</font> [B361]"
+            elif intFright == 32:
+                strInjury = str(int(random.uniform(2,13)))
+                myStr += "<font color='#ff0000'>Stricken.  You fall to the ground, taking " + strInjury + " HP of injury in the form of a mild heart attack or stroke.</font> [B361]"
+            elif intFright == 33:
+                myStr += "<font color='#ff0000'>Total panic.  You are out of control; you might do anything (GM rolls 3d: the higher the roll, the more useless your reaction).  For instance, you might jump off a cliff to avoid the monster.  If you survive your first reaction, roll vs. Will to come out of the panic.  If you fail, the GM rolls again for another panic reaction, and so on!</font> [B361]"
+            elif intFright == 34:
+                myStr += "<font color='#ff0000'>Acquire a new -15-point Delusion (B130).</font> [B361]"
+            elif intFright == 35:
+                myStr += "<font color='#ff0000'>Acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.</font> [B361]"
+            elif intFright == 36:
+                myStr += "<font color='#ff0000'>Severe physical effect, set by the GM.  (Acquire -20 points worth of physical disadvantages, aging = -3 per year).</font> [B361]"
+            elif intFright == 37:
+                myStr += "<font color='#ff0000'>Severe physical effect, set by the GM.  (Acquire -30 points worth of physical disadvantages, aging = -3 per year).</font> [B361]"
+            elif intFright == 39:
+                strHours = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Delusion (B130).</font> [B361]"
+            elif intFright == 39:
+                strHours = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.</font> [B361]"
+            else:
+                strHours = str(int(random.uniform(1,7)))
+                myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.  Also lose 1 point of IQ <i>permanently</i>.  This automatically reduces all IQ-based skill, including magic spells, by 1.</font> [B361]"
+        return myStr
+
+class gurpsfrightcheckfail(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+        self.mod = mod
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr +="] + " + str(self.mod)
+        intFright = self.sum() + self.mod
+        myStr += " = <b>" + str(intFright) + "</b> "
+
+        if intFright < 6:
+            myStr += "<font color='#ff0000'>Stunned for one second, then recover automatically.</font> [B360]"
+        elif intFright < 8:
+            myStr += "<font color='#ff0000'>Stunned for one second.  Every second after that, roll vs. unmodified Will to snap out of it.</font> [B360]"
+        elif intFright < 10:
+            myStr += "<font color='#ff0000'>Stunned for one second.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B360]"
+        elif intFright == 10:
+            strStun = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Stunned for " + strStun + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B360]"
+        elif intFright == 11:
+            strStun = str(int(random.uniform(2,13)))
+            myStr += "<font color='#ff0000'>Stunned for " + strStun + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B361]"
+        elif intFright == 12:
+            myStr += "<font color='#ff0000'>Lose your lunch.  Treat this as retching for (25-HT) seconds, and then roll vs. HT each second to recover [B428].</font> [B361]"
+        elif intFright == 13:
+            myStr += "<font color='#ff0000'>Acquire a new mental quirk.</font> [B361]"
+        elif intFright < 16:
+            strFP = str(int(random.uniform(1,7)))
+            strSeconds = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Lose " + strFP + " FP, and stunned for " + strSeconds + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.</font> [B361]"
+        elif intFright == 16:
+            strSeconds = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Stunned for " + strSeconds + " seconds.  Every second after that, roll vs. Will, plus whatever bonuses or penalties you had on your original roll, to snap out of it.  Acquire a new mental quirk.</font> [B361]"
+        elif intFright == 17:
+            strMinutes = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes.  Every minute after that roll vs. HT to recover.</font> [B361]"
+        elif intFright == 18:
+            strMinutes = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.</font> [B361]"
+        elif intFright == 19:
+            strMinutes = str(int(random.uniform(2,13)))
+            myStr += "<font color='#ff0000'>Severe faint, lasting for " + strMinutes + " minutes.  Every minute after that roll vs. HT to recover.  Take 1 HP of injury.</font> [B361]"
+        elif intFright == 20:
+            strMinutes = str(int(random.uniform(4,25)))
+            strFP = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Faint bordering on shock, lastering for " + strMinutes + " minutes.  Also, lose " + strFP + " FP.</font> [B361]"
+        elif intFright == 21:
+            strMinutes = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Panic.  You run around screaming, sit down and cry, or do something else equally pointless for " + strMinutes + " minutes.  Every minute after that, roll vs. unmodified Will to snap out of it.</font> [B361]"
+        elif intFright == 22:
+            myStr += "<font color='#ff0000'>Acquire a new -10-point Delusion (B130).</font> [B361]"
+        elif intFright == 23:
+            myStr += "<font color='#ff0000'>Acquire a new -10-point Phobia (B148) or other -10-point mental disadvantage.</font> [B361]"
+        elif intFright == 24:
+            myStr += "<font color='#ff0000'>Major physical effect, set by the GM: hair turns white, age five years overnight, go partially deaf, etc.  (Acquire -15 points worth of physical disadvantages.  Each year of aging = -3 points.)</font> [B361]"
+        elif intFright == 25 :
+            myStr += "<font color='#ff0000'>If you already have a Phobia or other mental disadvantage that is logically related to the frightening incident, your self-control number becomes one step worse.  If not, or if your self-control number is already 6, add a new -10-point Phobia or other -10-point mental disadvantage.</font> [B361]"
+        elif intFright == 26:
+            strMinutes = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.  Also acquire a new -10-point Delusion (B130).</font> [B361]"
+        elif intFright == 27:
+            strMinutes = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Faint for " + strMinutes + " minutes and roll vs. HT immediately.  On a failed roll, take 1 HP of injury as you collapse.  Every minute after that roll vs. HT to recover.  Also acquire a new -10-point Phobia (B148) or other -10-point mental disadvantage.</font> [B361]"
+        elif intFright == 28:
+            myStr += "<font color='#ff0000'>Light coma.  You fall unconscious, rolling vs. HT every 30 minutes to recover.  For 6 hours after you come to, all skill rolls and attribute checks are at -2.</font> [B361]"
+        elif intFright == 29:
+            strHours = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.</font> [B361]"
+        elif intFright == 30:
+            strDays = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Catatonia.  Stare into space for " + strDays + " days.  Then roll vs. HT.  On a failed roll, remain catatonic for another " + strDays + " days, and so on.  If you have no medical care, lose 1 HP the first day, 2 HP the second day and so on.  If you survive and awaken, all skill rolls and attribute checks are at -2 for as many days as the catatonia lasted.</font> [B361]"
+        elif intFright == 31:
+            strMinutes = str(int(random.uniform(1,7)))
+            strFP = str(int(random.uniform(1,7)))
+            strInjury = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Seizure.  You lose control of your body and fall to the ground in a fit lasting " + strMinutes + " minutes and costing " + strFP + " FP.  Also, roll vs. HT.  On a failure, take " + strInjury + " HP of injury.  On a critical failure, you also lose 1 HT <i>permanently</i>.</font> [B361]"
+        elif intFright == 32:
+            strInjury = str(int(random.uniform(2,13)))
+            myStr += "<font color='#ff0000'>Stricken.  You fall to the ground, taking " + strInjury + " HP of injury in the form of a mild heart attack or stroke.</font> [B361]"
+        elif intFright == 33:
+            myStr += "<font color='#ff0000'>Total panic.  You are out of control; you might do anything (GM rolls 3d: the higher the roll, the more useless your reaction).  For instance, you might jump off a cliff to avoid the monster.  If you survive your first reaction, roll vs. Will to come out of the panic.  If you fail, the GM rolls again for another panic reaction, and so on!</font> [B361]"
+        elif intFright == 34:
+            myStr += "<font color='#ff0000'>Acquire a new -15-point Delusion (B130).</font> [B361]"
+        elif intFright == 35:
+            myStr += "<font color='#ff0000'>Acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.</font> [B361]"
+        elif intFright == 36:
+            myStr += "<font color='#ff0000'>Severe physical effect, set by the GM.  (Acquire -20 points worth of physical disadvantages, aging = -3 per year).</font> [B361]"
+        elif intFright == 37:
+            myStr += "<font color='#ff0000'>Severe physical effect, set by the GM.  (Acquire -30 points worth of physical disadvantages, aging = -3 per year).</font> [B361]"
+        elif intFright == 39:
+            strHours = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Delusion (B130).</font> [B361]"
+        elif intFright == 39:
+            strHours = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.</font> [B361]"
+        else:
+            strHours = str(int(random.uniform(1,7)))
+            myStr += "<font color='#ff0000'>Coma.  You fall unconcious for " + strHours + " hours.  At the end of the " + strHours + " hours, roll vs. HT to recover.  Continue to roll every " + strHours + " hours until you recover.  Also acquire a new -15-point Phobia (B148) or other -15-point mental disadvantage.  Also lose 1 point of IQ <i>permanently</i>.  This automatically reduces all IQ-based skill, including magic spells, by 1.</font> [B361]"
+
+        return myStr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/old_rollers/hero.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,232 @@
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# --
+#
+# File: Hero.py
+# Version:
+#   $Id: Hero.py,v .3 DJM & Heroman
+#
+# Description: Hero System die roller originally based on Posterboy's D20 Dieroller
+#
+# Changelog:
+# v.3 by Heroman
+# Added hl() to show hit location (+side), and hk() for Hit Location killing damage
+# (No random stun multiplier)
+# v.2 DJM
+# Removed useless modifiers from the Normal damage roller
+# Changed Combat Value roller and SKill roller so that positive numbers are bonuses,
+# negative numbers are penalties
+# Changed Killing damage roller to correct stun multiplier bug
+# Handled new rounding issues
+#
+# v.1 original release DJM
+
+__version__ = "$Id: hero.py,v 1.15 2006/11/04 21:24:19 digitalxero Exp $"
+
+from time import time, clock
+import random
+
+from std import std
+from orpg.dieroller.base import *
+
+# Hero stands for "Hero system" not 20 sided die :)
+
+class hero(std):
+    name = "hero"
+
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+
+# these methods return new die objects for specific options
+
+    def k(self,mod):
+        return herok(self,mod)
+
+    def hl(self):
+        return herohl(self)
+
+    def hk(self):
+        return herohk(self)
+
+    def n(self):
+        return heron(self)
+
+    def cv(self,cv,mod):
+        return herocv(self,cv,mod)
+
+    def sk(self,sk,mod):
+        return herosk(self,sk,mod)
+
+die_rollers.register(hero)
+
+class herocv(std):
+    def __init__(self,source=[],cv=10,mod=0):
+        std.__init__(self,source)
+        self.cv = cv
+        self.mod = mod
+
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr += "] = (" + str(self.sum()) + ")"
+
+        myStr += " with a CV of " + str(self.cv)
+        myStr += " and a modifier of " + str(self.mod)
+        cvhit = 11 + self.cv - self.sum() + self.mod
+        myStr += " hits up to <b>DCV <font color='#ff0000'>" + str(cvhit) + "</font></b>"
+        return myStr
+
+class herosk(std):
+    def __init__(self,source=[],sk=11,mod=0):
+        std.__init__(self,source)
+        self.sk = sk
+        self.mod = mod
+
+    def is_success(self):
+        return (((self.sum()-self.mod) <= self.sk))
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        strAdd="] - "
+        swapmod=self.mod
+        if self.mod < 0:
+            strAdd= "] + "
+            swapmod= -self.mod
+        myStr += strAdd + str(swapmod)
+        modSum = self.sum()-self.mod
+        myStr += " = (" + str(modSum) + ")"
+        myStr += " vs " + str(self.sk)
+
+        if self.is_success():
+            myStr += " or less <font color='#ff0000'>Success!"
+        else:
+            myStr += " or less <font color='#ff0000'>Failure!"
+
+        Diff = self.sk - modSum
+        myStr += " by " + str(Diff) +" </font>"
+
+        return myStr
+
+class herok(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+        self.mod = mod
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr += "] = (<font color='#ff0000'><b>" + str(int(round(self.sum()))) + "</b></font>)"
+        stunx = random.randint(1,6)-1
+        if stunx <= 1:
+            stunx = 1
+        myStr += " <b>Body</b> and a stunx of (" + str(stunx)
+        stunx = stunx + self.mod
+        myStr += " + " + str(self.mod)
+        stunsum = round(self.sum()) * stunx
+        myStr += ") for a total of (<font color='#ff0000'><b>" + str(int(stunsum)) + "</b></font>) <b>Stun</b>"
+        return myStr
+
+class herohl(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+        self.mod = mod
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        side = random.randint(1,6)
+        sidestr = "Left "
+        if side >=4:
+            sidestr = "Right "
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr += "] = (<font color='#ff0000'><b>" + str(int(round(self.sum()))) + "</b></font>) "
+        location = int(round(self.sum()))
+        if location <= 5:
+            myStr += "Location: <B>Head</B>, StunX:<B>x5</B>, NStun:<B>x2</B>, Bodyx:<B>x2</B>"
+        elif location == 6:
+            myStr += "Location: <B>" + sidestr + "Hand</B>, StunX:<B>x1</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
+        elif location == 7:
+            myStr += "Location: <B>" + sidestr + "Arm</B>, StunX:<B>x2</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
+        elif location == 8:
+            myStr += "Location: <B>" + sidestr + "Arm</B>, StunX:<B>x2</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
+        elif location == 9:
+            myStr += "Location: <B>" + sidestr + "Shoulder</B>, StunX:<B>x3</B>, NStun:<B>x1</B>, Bodyx:<B>x1</B>"
+        elif location == 10:
+            myStr += "Location: <B>Chest</B>, StunX:<B>x3</B>, NStun:<B>x1</B>, Bodyx:<B>x1</B>"
+        elif location == 11:
+            myStr += "Location: <B>Chest</B>, StunX:<B>x3</B>, NStun:<B>x1</B>, Bodyx:<B>x1</B>"
+        elif location == 12:
+            myStr += "Location: <B>Stomach</B>, StunX:<B>x4</B>, NStun:<B>x1 1/2</B>, Bodyx:<B>x1</B>"
+        elif location == 13:
+            myStr += "Location: <B>Vitals</B>, StunX:<B>x4</B>, NStun:<B>x1 1/2</B>, Bodyx:<B>x2</B>"
+        elif location == 14:
+            myStr += "Location: <B>" + sidestr + "Thigh</B>, StunX:<B>x2</B>, NStun:<B>x1</B>, Bodyx:<B>x1</B>"
+        elif location == 15:
+            myStr += "Location: <B>" + sidestr + "Leg</B>, StunX:<B>x2</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
+        elif location == 16:
+            myStr += "Location: <B>" + sidestr + "Leg</B>, StunX:<B>x2</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
+        elif location >= 17:
+            myStr += "Location: <B>" + sidestr + "Foot</B>, StunX:<B>x1</B>, NStun:<B>x1/2</B>, Bodyx:<B>x1/2</B>"
+        return myStr
+
+class herohk(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+        self.mod = mod
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr += "] = (<font color='#ff0000'><b>" + str(int(round(self.sum()))) + "</b></font>)"
+        stunx = 1
+        myStr += " <b>Body</b> "
+        stunx = stunx + self.mod
+        stunsum = round(self.sum()) * stunx
+        myStr += " for a total of (<font color='#ff0000'><b>" + str(int(stunsum)) + "</b></font>) <b>Stun</b>"
+        return myStr
+
+class heron(std):
+    def __init__(self,source=[],mod=0):
+        std.__init__(self,source)
+        self.bodtot=0
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        if self.data[0] == 6:
+            self.bodtot=self.bodtot+2
+        else:
+            self.bodtot=self.bodtot+1
+        if self.data[0] <= 1:
+            self.bodtot=self.bodtot-1
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+            if a == 6:
+                self.bodtot=self.bodtot+2
+            else:
+                self.bodtot=self.bodtot+1
+            if a <= 1:
+                self.bodtot=self.bodtot-1
+        myStr += "] = (<font color='#ff0000'><b>" + str(self.bodtot) + "</b></font>)"
+        myStr += " <b>Body</b> and "
+        myStr += "(<font color='#ff0000'><b>" + str(int(round(self.sum()))) + "</b></font>) <b>Stun</b>"
+        return myStr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/old_rollers/runequest.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,696 @@
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+#-------------------------------------------------------------------------
+#
+#  Usage:
+#
+#   Die  Roller: /dieroller rq
+#
+#   Skill  Roll: [1d100.skill(50,0,0)]         # ( skill%, modifer, MA% )
+#
+#   Parry  Roll: [1d100.parry(50,0,0,12)]      # ( skill%, modifer, MA%, Weapon/Shield AP )
+#
+#   Dodge  Roll: [1d100.parry(50,0,0)]         # ( skill%, modifer, MA% )
+#
+#   Attack Roll: [1d100.attack(50,0,0,2,9,3,0)]
+#       ( skill%, modifer, MA%, min weap dam, max weap dam, dam bonus, truesword )
+#
+#   Sorcery Roll: [1d100.sorcery(90,   0,   3,   6,   1,   1,    1)]
+#                               (sk, mod, pow, cer, int,  acc, mlt)
+#
+#
+#
+#   Skill Training Unlimited Roll: [1d100.trainskill(30,75)]       # (starting skill%, desired skill%)
+#   Skill Training Cost Limited:   [1d100.trainskillcost(1000, 50) # (payment, starting skill%)
+#   Skill Training Time Limited:   [1d100.trainskilltime(150, 50)  # (time, strting skill%)
+#
+#-------------------------------------------------------------------------
+# --
+#
+# File: rq.py
+# Version:
+#   $Id: rq.py,v .1 pelwer
+#
+# Description: Runequest die roller originally based on Heroman's Hero Dieroller
+#
+#
+# v.1 - pelwer - 2/5/2005
+#  o Original release
+# v.2 - pelwer - 10/30/2006
+#  o Ported to openrpg+ by removing dependance on whrandom
+#  o Fixed Riposte spelling
+#  o Deleted sorcalc - never used
+#  o Added Sorcery Fumble table to sorcery spell roller
+#
+
+__version__ = "$Id: runequest.py,v 1.4 2006/11/15 12:11:22 digitalxero Exp $"
+
+from time import time, clock
+import random
+from math import floor
+
+from std import std
+from orpg.dieroller.base import *
+
+# rq stands for "Runequest"
+
+class runequest(std):
+    name = "runequest"
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+
+# these methods return new die objects for specific options
+
+    def skill(self,sk,mod,ma):
+        return rqskill(self,sk,mod,ma)
+
+    def parry(self,sk,mod,ma,AP):
+        return rqparry(self,sk,mod,ma,AP)
+
+    def dodge(self,sk,mod,ma):
+        return rqdodge(self,sk,mod,ma)
+
+    def attack(self,sk,mod,ma,mindam,maxdam,bondam,trueswd):
+        return rqattack(self,sk,mod,ma,mindam,maxdam,bondam,trueswd)
+
+    def sorcery(self,sk,mod,pow,cer,int,acc,mlt):
+        return rqsorcery(self,sk,mod,pow,cer,int,acc,mlt)
+
+    def trainskill(self,initial,final):
+        return rqtrainskill(self,initial,final)
+
+    def trainskillcost(self,cost,sk):
+        return rqtrainskillcost(self,cost,sk)
+
+    def trainskilltime(self,time,sk):
+        return rqtrainskilltime(self,time,sk)
+
+die_rollers.register(runequest)
+
+#  RQ Skill Training Cost/Time unlimited
+#
+# [1d100.trainskill(10,20)]
+#          initial skill%, final skill%
+#
+# sk    = skill %
+#
+#
+class rqtrainskill(std):
+    def __init__(self,source=[],initial=11,final=0):
+        std.__init__(self,source)
+        self.s = initial
+        self.f = final
+
+    def __str__(self):
+        myStr = "Unrestricted Training"
+
+        if self.s == 0:
+            myStr = "Initial training completed for Cost(50) Time(20) Skill(1 + modifier)"
+        else:
+            cost  = 0
+            time  = 0
+            myStr = "Training: "
+
+            while self.s < self.f and self.s < 75:
+                cost   += self.s * 5
+                time   += self.s * 1
+                self.s += random.uniform(1,4) + 1
+
+            myStr  = "Training completed:\n"
+            myStr += "\tCost(" + str(int(cost)) + ")\n"
+            myStr += "\tTime(" + str(int(time)) + ")\n"
+            myStr += "\tSkill(" + str(int(self.s)) + ")"
+
+        return myStr
+
+
+#  RQ Skill Training Cost Limited
+#
+# [1d100.trainskillcost(50,0)]
+#          cost, skill%
+#
+# cost  = cash for training
+# sk    = skill %
+#
+#
+class rqtrainskillcost(std):
+    def __init__(self,source=[],cost=11,sk=0):
+        std.__init__(self,source)
+        self.cost = cost
+        self.sk   = sk
+
+    def __str__(self):
+        myStr = ""
+
+        if self.sk == 0 and self.cost >= 50:
+            myStr = "Initial training completed for Cost(50), Time(50), Skill(1 + modifier)"
+        else:
+            cost  = 0
+            time  = 0
+            icost = self.sk * 5
+
+            myStr = "Training: "
+
+            while (cost + icost) < self.cost:
+                if self.sk >= 75:
+                    break
+
+                cost += icost
+                time += self.sk * 1
+                self.sk += random.uniform(1,4) + 1
+                icost = self.sk * 5
+
+            myStr  = "Training completed: "
+            myStr += "Cost(" + str(int(cost)) + ") "
+            myStr += "Time(" + str(int(time)) + ") "
+            myStr += "Skill(" + str(int(self.sk)) + ")"
+
+        return myStr
+
+
+#  RQ Skill Training Time Limited
+#
+# [1d100.trainskilltime(50,0)]
+#          time, skill%
+#
+# time  = time for training
+# sk    = skill %
+#
+#
+class rqtrainskilltime(std):
+    def __init__(self,source=[],time=11,sk=0):
+        std.__init__(self,source)
+        self.time = time
+        self.sk   = sk
+
+    def __str__(self):
+        myStr = ""
+
+        if self.sk == 0 and self.time >= 20:
+            myStr = "Initial training completed for Cost(50), Time(50), Skill(1 + modifier)"
+        else:
+            cost  = 0
+            time  = 0
+            itime = self.sk * 1
+
+            myStr = "Trainingsss: "
+
+            while (time + itime) < self.time:
+                if self.sk >= 75:
+                    break
+
+                cost += self.sk * 5
+                time += itime
+                self.sk += random.uniform(1,4) + 1
+                itime = self.sk * 5
+
+            myStr  = "Training completed: "
+            myStr += "Cost(" + str(int(cost)) + ") "
+            myStr += "Time(" + str(int(time)) + ") "
+            myStr += "Skill(" + str(int(self.sk)) + ")"
+
+        return myStr
+
+#  RQ Skill Roll
+#
+# [1d100.skill(50,0,0)]
+#          skill%, modifer, ma%
+#
+# sk    = skill %
+# mod   = modifier %
+# ma    = martial arts %
+# skill = sk + mod
+#
+# success   roll <= skill
+#
+# failure   roll > skill
+#
+# crit
+#     push( @{$::Cre{Weapons}{$weap_cnt}}, POSIX::floor( skill/20 ) );
+#
+# special
+#     push( @{$::Cre{Weapons}{$weap_cnt}}, POSIX::floor( $skill/5 ) );
+#
+# fumble: if ( $skill > 100 ) { $fum = 0; } else { $fum = 100 - $skill; }
+#             $fum = 100 - POSIX::floor( $fum/20 );
+#             if ( $fum == 100 ) { $fum = '00'; };
+#
+class rqskill(std):
+    def __init__(self,source=[],sk=11,mod=0,ma=0):
+        std.__init__(self,source)
+        self.sk  = sk
+        self.mod = mod
+        self.ma  = ma
+
+    def is_success(self):
+        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
+
+    def is_ma(self):
+        return (self.sum() <= self.ma)
+
+    def is_special(self):
+        return (self.sum() <= int(floor((self.sk + self.mod)/5)))
+
+    def is_critical(self):
+        return (self.sum() <= int(floor((self.sk + self.mod) / 20)))
+
+    def is_fumble(self):
+        if ( self.sk >= 100 ):
+            fum = 0
+        else:
+            fum = (100 - self.sk )
+        final_fum = ( 100 - int( floor( fum/20  ) ) )
+        return (  self.sum() >= final_fum )
+
+    def __str__(self):
+        strAdd="+"
+        swapmod= self.mod
+        if self.mod < 0:
+            strAdd= "-"
+            swapmod= -self.mod
+        modSum = self.sum()
+        # build output string
+        myStr = " (" + str(modSum) + ")"
+        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
+
+        if self.is_fumble():
+            myStr += " <b><font color=red>Fumble!</font></b>"
+        elif self.is_critical():
+            myStr += " <b><font color=green>Critical!</font></b>"
+        elif self.is_special():
+            myStr += " <i><font color=green>Special!</font></i>"
+        elif self.is_success() and self.is_ma():
+            myStr += " <i><font color=green>Special!</font></i>"
+        elif self.is_success():
+            myStr += " <font color=blue>Success!</font>"
+        else:
+            myStr += " <font color=red>Failure!</font>"
+
+        Diff = self.sk - modSum
+        myStr += " </font>"
+
+        return myStr
+
+#
+# RQ Parry Roll
+#
+# same as skill but with fumble dice and armor points
+#
+# [1d100.parry(50,0,0,12)]
+#             skill%, modifer, ma%, Weapon AP
+#
+
+class rqparry(std):
+    def __init__(self,source=[],sk=11,mod=0,ma=0,AP=0):
+        std.__init__(self,source)
+        self.sk = sk
+        self.mod = mod
+        self.ma  = ma
+        self.AP = AP
+
+    def is_success(self):
+        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
+
+    def is_special(self):
+        return (self.sum() <= int(floor((self.sk + self.mod) / 5)))
+
+    def is_ma(self):
+        return (self.sum() <= self.ma)
+
+    def is_riposte(self):
+        return (self.sum() <= (self.ma / 5))
+
+    def is_critical(self):
+        return ( (  self.sum() <= int( floor( ( self.sk + self.mod  )/20 ) ) ) )
+
+    def is_fumble(self):
+        if ( self.sk >= 100 ):
+            fum = 0
+        else:
+            fum = (100 - self.sk )
+        final_fum = ( 100 - int( floor( fum/20  ) ) )
+        return (  self.sum() >= final_fum )
+
+    def __str__(self):
+
+        # get fumble roll result in case needed
+        fum_roll = random.randint(1,100)
+
+        # get special AP
+        spec_AP = int( floor ( self.AP * 1.5 ) )
+
+        # figure out +/- for modifer
+        strAdd="+"
+        swapmod= self.mod
+        if self.mod < 0:
+            strAdd= "-"
+            swapmod= -self.mod
+        modSum = self.sum()
+
+        # build output string
+        myStr = " (" + str(modSum) + ")"
+        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
+
+        if self.is_fumble():
+            myStr += " <b><font color=red>Fumble!</font>  See Fumble Chart [" + str(fum_roll) + "]</b>"
+        elif self.is_critical() and self.is_riposte():
+            myStr += " <b><font color=green>Critical!</font> All damage blocked!</b>"
+            myStr += " Riposte next SR"
+        elif self.is_critical():
+            myStr += " <b><font color=green>Critical!</font> All damage blocked!</b>"
+        elif self.is_special and self.is_riposte():
+            myStr += " <i><font color=green>Special!</font> Weapon/Shield AP [" + str(spec_AP) + "]</i>"
+            myStr += " Riposte next SR"
+        elif self.is_special():
+            myStr += " <i><font color=green>Special!</font> Weapon/Shield AP [" + str(spec_AP) + "]</i>"
+        elif self.is_success() and self.is_ma():
+            myStr += " <i><font color=green>Special!</font> Weapon/Shield AP [" + str(spec_AP) + "]</i>"
+        elif self.is_success():
+            myStr += " <font color=blue>Success!</font> Weapon/Shield AP [" + str(self.AP) + "]"
+        else:
+            myStr += " <font color=red>Failure!</font>"
+
+        Diff = self.sk - modSum
+        myStr += " </font>"
+
+        return myStr
+
+# RQ Dodge Roll
+#
+# same as skill but with fumble dice and armor points
+#
+# [1d100.parry(50,0,0)]
+#             skill%, modifer, ma%
+#
+
+class rqdodge(std):
+    def __init__(self,source=[],sk=11,mod=0,ma=0,AP=0):
+        std.__init__(self,source)
+        self.sk = sk
+        self.mod = mod
+        self.ma  = ma
+        self.AP = AP
+
+    def is_success(self):
+        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
+
+    def is_special(self):
+        return (self.sum() <= int(floor((self.sk + self.mod) / 5)))
+
+    def is_ma(self):
+        return (self.sum() <= self.ma)
+
+    def is_riposte(self):
+        return (self.sum() <= (self.ma / 5))
+
+    def is_critical(self):
+        return ( (  self.sum() <= int( floor( ( self.sk + self.mod  )/20 ) ) ) )
+
+    def is_fumble(self):
+        if ( self.sk >= 100 ):
+            fum = 0
+        else:
+            fum = (100 - self.sk )
+        final_fum = ( 100 - int( floor( fum/20  ) ) )
+        return (  self.sum() >= final_fum )
+
+    def __str__(self):
+
+        # get fumble roll result in case needed
+        fum_roll = random.randint(1,100)
+
+        # get special AP
+        spec_AP = int( floor ( self.AP * 1.5 ) )
+
+        # figure out +/- for modifer
+        strAdd="+"
+        swapmod= self.mod
+        if self.mod < 0:
+            strAdd= "-"
+            swapmod= -self.mod
+        modSum = self.sum()
+
+        # build output string
+        myStr = " (" + str(modSum) + ")"
+        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
+
+        if self.is_fumble():
+            myStr += " <b><font color=red>Fumble!</font>  See Fumble Chart [" + str(fum_roll) + "]</b>"
+        elif self.is_critical() and self.is_riposte():
+            myStr += " <b><font color=green>Critical!</font> All damage dodged!</b>"
+            myStr += " Riposte on next SR"
+        elif self.is_critical():
+            myStr += " <b><font color=green>Critical!</font> All damage dodged!</b>"
+        elif self.is_special and self.is_riposte():
+            myStr += " <i><font color=green>Special!</font> Damage dodged</b>"
+            myStr += " Riposte on next SR"
+        elif self.is_special():
+            myStr += " <i><font color=green>Special!</font> Damage dodged</b>"
+        elif self.is_success() and self.is_ma():
+            myStr += " <i><font color=green>Special!</font> Damage dodged</b>"
+        elif self.is_success():
+            myStr += " <font color=blue>Success!</font> Damage dodged</b>"
+        else:
+            myStr += " <font color=red>Failure!</font>"
+
+        Diff = self.sk - modSum
+        myStr += " </font>"
+
+        return myStr
+
+
+
+#
+# RQ Attack Roll
+#
+# same as skill but with fumble dice and armor points
+#
+# [1d100.attack(50,0,0,2,9,3,1)]
+#             skill%, modifer, ma%, min weap dam, max weap dam, dam bonus, truesword_enabled
+#
+class rqattack(std):
+    def __init__(self,source=[],sk=11,mod=0,ma=0,mindam=0,maxdam=0,bondam=0,trueswd=0):
+        std.__init__(self,source)
+        self.sk = sk
+        self.mod = mod
+        self.ma  = ma
+        self.mindam = mindam
+        self.maxdam = maxdam
+        self.bondam = bondam
+        self.trueswd = trueswd
+
+    def is_success(self):
+        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
+
+    def is_ma(self):
+        return (self.sum() <= self.ma)
+
+    def is_special(self):
+        return (self.sum() <= int(floor((self.sk + self.mod) / 5)))
+
+    def is_critical(self):
+        return ((self.sum() <= int(floor((self.sk + self.mod) / 20))))
+
+    def is_supercritical(self):
+        return (self.sum() == 1)
+
+    def is_fumble(self):
+        if ( self.sk >= 100 ):
+            fum = 0
+        else:
+            fum = (100 - self.sk )
+        final_fum = ( 100 - int( floor( fum/20  ) ) )
+        return (  self.sum() >= final_fum )
+
+    def __str__(self):
+
+        # get fumble roll result in case needed
+        fum_roll = random.randint(1,100)
+
+        # get hit location roll result in case needed
+        location = random.randint(1,20)
+        myStr = " to the ["+ str(location) + "] "
+        if location < 5:
+            myStr += "<B>Right Leg</B>"
+        elif location < 9:
+            myStr += "<B>Left Leg</B>"
+        elif location < 12:
+            myStr += "<B>Abdomen</B>"
+        elif location < 13:
+            myStr += "<B>Chest</B>"
+        elif location < 16:
+            myStr += "<B>Right Arm</B>"
+        elif location < 19:
+            myStr += "<B>Left Arm</B>"
+        else:
+            myStr += "<B>Head</B>"
+        hit_loc = myStr
+
+
+        # get normal damage in case needed
+        norm_damage = random.randint(self.mindam*(self.trueswd+1),self.maxdam*(self.trueswd+1)) + self.bondam
+        norm_damage_string  = "{" + str( self.mindam*(self.trueswd+1) ) + "-"
+        norm_damage_string += str(self.maxdam*(self.trueswd+1)) + "+" + str(self.bondam)
+        norm_damage_string += "}[" + str(norm_damage) + "] "
+
+        # get special/critical damage in case needed
+        crit_damage = random.randint( self.mindam*(self.trueswd+2), self.maxdam*(self.trueswd+2) ) + self.bondam
+        crit_damage_string = "{" + str( self.mindam*(self.trueswd+2) ) + "-" + str(self.maxdam*(self.trueswd+2)) + "+" + str(self.bondam) + "}[" + str(crit_damage) + "] "
+
+        # get supercritical damage in case needed
+        super_damage = norm_damage + self.maxdam
+        super_damage_string  = "{" + str( self.mindam*(self.trueswd+1) ) + "-"
+        super_damage_string += str(self.maxdam*(self.trueswd+1)) + "+" + str(self.maxdam)
+        super_damage_string += "+" + str(self.bondam) + "}[" + str(super_damage) + "] "
+
+        # figure out +/- for modifer
+        strAdd="+"
+        swapmod= self.mod
+        if self.mod < 0:
+            strAdd= "-"
+            swapmod= -self.mod
+        modSum = self.sum()
+
+        # build output string
+        myStr = " (" + str(modSum) + ")"
+        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
+
+        if self.is_fumble():
+            myStr += " <b><font color=red>Fumble!</font>  See Fumble Chart [" + str(fum_roll) + "]</b>"
+        elif (self.is_supercritical() and self.is_success()):
+            myStr += " <b><font color=green>Super Critical!</font></b> Damage: " + str(super_damage_string) + "<u>No Armor Stops</u>" + str(hit_loc)
+        elif (self.is_critical() and self.is_success()):
+            myStr += " <b><font color=green>Critical!</font></b> Damage: " + str(crit_damage_string) + "<u>No Armor Stops</u>" + str(hit_loc)
+        elif ( self.is_special() and self.is_success() ):
+            myStr += " <i><font color=green>Special!</font></i> Damage: " + str(crit_damage_string) + str(hit_loc)
+        elif (self.is_success() and self.is_ma()):
+            myStr += " <i><font color=green>Special!</font></i> Damage: " + str(crit_damage_string) + str(hit_loc)
+        elif self.is_success():
+            myStr += " <font color=blue>Success!</font> Damage: " + str(norm_damage_string) + str(hit_loc)
+        else:
+            myStr += " <font color=red>Failure!</font>"
+
+        return myStr
+
+#
+#
+#   Sorcery Roll: [1d100.sorcery(90,   10,  5,   4,   3,   2,    1)]
+#                               (sk, mod, pow, cer, int,  acc, mlt)
+#
+# Ceremony: (+1d6% per strike rank spent on ceremony)
+# Intensity: (-3% per point of Intensity)
+# Duration: (-4% per point of Duration)
+# Range: (-5% per point of Range)
+# Multispell: (-10% per each spell over 1)
+# Acceleration: (-5% per point of Acceleration)
+# Hold: (-2% per point in spell Held)
+#
+class rqsorcery(std):
+    def __init__(self,source=[],sk=11,mod=0,pow=0,cer=0,int=0,acc=0,mlt=0):
+        std.__init__(self,source)
+        self.sk  = sk   # sorcery skill
+        self.mod = mod  # additional modifier ( from duration, range, etc )
+        self.pow = pow  # boost pow and additional pow ( from duration, range, etc )
+        self.cer = cer  # ceremony d6
+        self.int = int  # intensity ( -3% )
+        self.acc = acc  # accelerate ( -5% )
+        self.mlt = mlt  # multispell ( -10% )
+
+    def is_success(self):
+        return (((self.sum() <= (self.sk + self.mod)) or (self.sum() <= 5)) and (self.sum() <= 95))
+
+    def is_special(self):
+        return ( (  self.sum() <= int( floor( ( self.sk + self.mod  )/5  ) ) ) )
+
+    def is_critical(self):
+        return ( (  self.sum() <= int( floor( ( self.sk + self.mod  )/20 ) ) ) )
+
+    def is_fumble(self):
+        if ( self.sk >= 100 ):
+            fum = 0
+        else:
+            fum = (100 - self.sk )
+        final_fum = ( 100 - int( floor( fum/20  ) ) )
+        return (  self.sum() >= final_fum )
+
+    def __str__(self):
+
+        # get fumble roll result in case needed
+        fum_roll = random.randint(2,12)
+        if fum_roll == 12 :
+            fum_string = "<br /><font color=purple>Caster temporarily forgets spell. Make an INTx5 roll each day to remember.</font>"
+        if fum_roll == 11 :
+            fum_string = "<br /><font color=purple>Caster temporarily forgets spell. Make an INTx5 roll each hour to remember.  </font>"
+        if fum_roll == 10 :
+            fum_string = "<br /><font color=purple>Spell produces reverse of the intended effect.  </font>"
+        if fum_roll == 9 :
+            fum_string = "<br /><font color=purple>Caster is Stunned. Roll INTx3 to recover at SR 10 each round.  </font>"
+        if fum_roll == 8 :
+            fum_string = "<br /><font color=purple>Caster takes 2D6 Damage to THP  </font>"
+        if fum_roll == 7 :
+            fum_string = "<br /><font color=purple>Spell produces reverse of the intended effect at 2x Intensity.  </font>"
+        if fum_roll == 6 :
+            fum_string = "<br /><font color=purple>Spell is cast on companions (if harmful) or on random nearby foes (if beneficial)  </font>"
+        if fum_roll == 5 :
+            fum_string = "<br /><font color=purple>Caster takes 1d6 Damage to Head  </font>"
+        if fum_roll == 4 :
+            fum_string = "<br /><font color=purple>Spell is cast on caster (if harmful) or on random nearby foe (if beneficial)  </font>"
+        if fum_roll == 3 :
+            fum_string = "<br /><font color=purple>Caster takes 1d6 Damage to THP  </font>"
+        if fum_roll == 2 :
+            fum_string = "<br /><font color=purple>Caster takes 1 point of Damage to Head  </font>"
+
+            # roll ceremony
+        ceremony_roll = random.randint( self.cer, (self.cer*6) )
+
+        # subtract manipulations
+        extra_mod = self.mod
+        self.mod += ceremony_roll - self.int*3 - self.acc*5 - self.mlt*10
+
+        # add up power cost
+        extra_pow = self.pow
+        self.pow += self.int + self.mlt + self.acc
+        special_pow = int( floor( ( self.pow )/2  ) )
+
+        # figure out +/- for modifer
+        strAdd="+"
+        swapmod= self.mod
+        if self.mod < 0:
+            strAdd= "-"
+            swapmod= -self.mod
+        modSum = self.sum()
+
+        # build output string
+        myStr = " (" + str(modSum) + ")"
+        myStr += " vs [" + str(self.sk) + strAdd + str(swapmod) + "]"
+
+        if self.is_fumble():
+            myStr += " <b><font color=red>Fumble!</font>  POW Cost: [" + str(self.pow) + "],</b> " + fum_string
+        elif self.is_critical():
+            myStr += " <b><font color=green>Critical!</font></b> POW Cost: [1] "
+        elif self.is_special():
+            myStr += " <i><font color=green>Special!</font></i> POW Cost: [" + str(special_pow) + "] "
+        elif self.is_success():
+            myStr += " <font color=blue>Success!</font> POW Cost: [" + str(self.pow) + "] "
+        else:
+            myStr += " <font color=red>Failure!</font> POW Cost: [1]"
+
+        # print spell details
+        myStr += "<br /> --- Other Modifiers:["    + str( extra_mod     ) + "], "
+        myStr += "Extra POW:[" + str( extra_pow     ) + "], "
+        myStr += "Ceremony:[+"          + str( ceremony_roll ) + "%], "
+        myStr += "Intensity(-3):["      + str( self.int      ) + "], "
+        myStr += "Accelerate(-5):["     + str( self.acc      ) + "], "
+        myStr += "Multispell(-10):["    + str( self.mlt      ) + "] ---"
+
+        return myStr
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/old_rollers/savage.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,509 @@
+# (at your option) any later version.
+# # This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# --
+#
+# File: savage.py
+# Authors: Rich Finder
+#        : Alexandre Major
+# Maintainer:
+# Version: 0.2
+#
+# Description: Savage Worlds die roller
+# Permission was granted by Pinnacle to reprint the result descriptions from their tables on Apr 20, 2006 by Simon Lucas
+#
+
+__version__ = "$Id: savage.py,v 1.2 2007/05/06 16:42:55 digitalxero Exp $"
+
+import string
+from random import *
+
+from std import std
+from orpg.dieroller.base import *
+
+# Savage, as in Savage Worlds
+class sw(std):
+    #def __init__(self,source=[], wnd=1, loc="rnd", chmod=0):
+    def __init__(self,source=[],fmod=0):
+        std.__init__(self,source)
+
+# these methods return new die objects for specific options
+
+    def fright(self,fearmod=0):
+        return fright(self,fearmod=0)
+
+    def kob(self,wnd,loc):
+        return kob(self,wnd=1,loc="rnd")
+
+    def ooc(self):
+        return ooc(self)
+
+    def ract(self,chmod=0):
+        return ract(self,chmod=0)
+
+    def vcrit(self):
+        return vcrit(self)
+
+    def fortune(self):
+        return fortune(self)
+
+    def freak(self):
+        return freak(self)
+
+    def swdhelps(self):
+        return swdhelps(self)
+
+die_rollers.register(sw)
+
+class fright(std):
+    #-----------------The Fright Table
+    #  Rolls on the Fright - which is a 1d20 roll modified by the fear level of a monster.  This function automatically generates
+    #  The appropriate random number and then adds the fear modifier to the rol and displays the result of the roll with the effect
+    #  of that roll.
+    #  Usage:  [fright()]
+    #          [fright(6)] - if the fear modifier of the monster was 6
+    #-----------------
+    def __init__(self,fmod=0):
+        global fear
+        std.__init__(self)
+        fear=fmod
+
+    #def sum(self):
+
+    def __str__(self):
+        global fear
+        iroll = randint(1,20)
+        froll = iroll + fear
+        if froll >= 1 and froll <=4:
+            fresult = "Adrenaline Rush"
+            fdescription = "The hero's \"fight\" response takes over.  He adds +2 to all Trait and damage rolls on his next action."
+        elif froll >= 5 and froll <=8:
+            fresult = "Shaken"
+            fdescription = "The character is Shaken."
+        elif froll >=9 and froll <=12:
+            fresult = "Pankicked"
+            fdescription = "The character immediately moves his full Pace plus running die away from the danger and is Shaken."
+        elif froll >=13 and froll <=16:
+            fresult = "Minor Phobia"
+            fdescription = "The character gains a Minor Phobia Hindrance somehow associated with the trauma."
+        elif froll >=17 and froll <=18:
+            fresult = "Major Phobia"
+            fdescription = "The character gains a Major Phobia Hindrance."
+        elif froll >=19 and froll <= 20:
+            fresult = "The Mark of Fear"
+            fdescription = "The hero is Shaken and also suffers some cosmetic, physical alteration -- a white streak forms in the hero's hair, his eyes twitch constantly, or some other minor physical alteration.  This reduces his Charisma by 1."
+        else:
+            fresult = "Heart Attack"
+            fdescription = "The hero is so overwhelmed with fear that his heart stutters.  He becomes Incapacitated and must make a Vigor roll at -2.  If Successful, he's Shaken and can't attempt to recover for 1d4 rounds.  If he fails, he dies in 2d6 rounds.  A Healing roll at -4 saves the victim's life, but he remains Incapacitated."
+        myStr = "[" + str(iroll) + "+"+str(fear)+"="+str(froll)+"] ==> " + fresult +":  "+ fdescription
+        return myStr
+
+class kob(std):
+    #-------------------The Knockout Blow Table
+    #  This table used when a character has sustained more than 3 wounds.  The number wounds taken that sends a character to the
+    #  Knockout Blow Table is what gets sent with the kob command - not the total number of wounds the character currently has.
+    #  For example - a character has 2 wounds and is hit takes 2 more wounds, this will result in a total of 4 wounds, but the
+    #  number that gets sent to the kob roll is 2, because that is the number of wounds sustained that sent the character to the kob
+    #  table.
+    #
+    #  It is also important to note that if a called shot to particular area was made, that information should be sent with the "roll"
+    #  as well, because the KOB Table may need to determine some dramatic effects for permanent injuries, etc.  If a hit location was sent
+    #  the function will use that information.
+    #  Valid Hit Locations are:  h (head), g (guts), la (left arm), ra (right arm), rl (right leg), ll (left leg), c (crotch)
+    #  Usage = [kob(3)] - If 3 wounds were received that sent the player to the Knockout Blow Table - no called shot
+    #          [kob(3,"h") - If 3 wounds were received that sent the player to the Knockout Blow Table with a called shot to the head
+    #---------------------
+    global wound, loca
+    def __init__(self, wnd, loc="rnd"):
+        global wound, loca
+        std.__init__(self, wnd)
+        #Need to check to make sure that wnd is a number
+        if (int(wnd)):
+            wound = wnd
+            loca = loc
+        else:
+            mystr = "You need to supply a number for the wound."
+            return mystr
+
+    def __str__(self):
+        global wound, loca
+        itbl = "no"
+        if wound == 1:
+            wtype = "Battered and Bruised"
+            wdescription = "If your hero was previously Incapacitated, this result has no further effect. Otherwise, your hero's had the wind knocked out of him. Make a Spirit roll at the beginning of each round. If the roll is successful, he becomes Shaken and can return to the fight."
+        elif wound == 2:  #Need to roll on the Injury table as well
+            wtype = "Incapacitated"
+            wdescription = "Your hero is beaten badly enough to take him out of this fight. He's Incapacitated and must roll on the Injury Table."
+            itbl = "yes"
+        elif wound == 3:
+            wtype = "Bleeding Out"
+            wdescription = "Your hero is bleeding out and Incapacitated. Roll on the Injury Table and make a Vigor roll at the start of each combat round. A failure means the hero has lost too much blood and becomes mortally Wounded (see below; begin rolling for the Mortal Wound in the next round). With a success, he keeps bleeding and must roll again next round. With a raise, or a successful Healing roll, he stops bleeding and is Incapacitated."
+            itbl = "yes"
+        elif wound < 1:
+            wtype = "No Wounds?"
+            wdescription = "The Number of wounds specified was less than one...why are you consulting this chart?"
+        else:
+            wtype = "Mortal Wound"
+            wdescription = "Your hero has suffered a life-threatening wound and will not recover without aid. He is Incapacitated and must roll on the Injury Table. He must also make a Vigor roll at the start of each round. If the roll is failed, he passes on. A Healing roll stabilizes the victim but leaves him Incapacitated."
+            itbl = "yes"
+
+        if itbl == "yes":
+            #Determine if a Hit location was specified already
+            if loca.lower() == "h":
+                iroll = 11
+            elif loca.lower() == "g":
+                iroll = 5
+            elif loca.lower() == "ra":
+                iroll = 3
+                aroll = 2
+            elif loca.lower() == "la":
+                iroll = 3
+                aroll = 1
+            elif loca.lower() == "rl":
+                iroll = 10
+                lroll = 2
+            elif loca.lower() == "ll":
+                iroll = 10
+                lroll = 1
+            elif loca.lower() == "c":
+                iroll = 2
+            else:  #none of the above were provided...wo will need to determine randomly
+                iroll = randint(2,12)
+            #resolve the injury table stuff...
+            if iroll == 2:
+                iloc = "Unmentionables"
+                idescription = "The hero suffers an embarrassing and painful wound to the groin."
+            elif iroll == 3 or iroll == 4:
+                if loca != "ra" and loca != "la":  #  If a hit location was not specified (or not recognized) already, determine randomly
+                    aroll = randint(1,2)
+                if aroll == 1:
+                    warm = "Left"
+                else:
+                    warm = "Right"
+                iloc = warm + " Arm"
+                idescription = "The arm is rendered useless."
+            elif iroll >= 5 and iroll <= 9:  #will need to make another random roll
+                iloc = "Guts"
+                idescription = "Your hero catches one somewhere between the crotch and the chin."
+                groll = randint(1,6)
+                if groll == 1 or groll == 2:
+                    #idescription += " <b>Broken (" + str(groll) + ")</b> His Agility is reduced by a die type (min dr)."
+                    idescription += " <b>Broken (" + str(groll) + ")</b> His Agility is reduced by a die type (min d4)."
+                elif groll == 3 or groll == 4:
+                    idescription += " <b>Battered (" + str(groll) + ")</b> His Vigor is reduced by a die type (min d4)."
+                else:
+                    idescription += " <b>Busted (" + str(groll) + ")</b> His Strength is reduced by a die type (min d4)."
+            elif iroll == 10:
+                if loca != "ll" and loca != "rl":  #  If a hit location was not specified (or not recognized) already, determine randomly
+                    lroll = randint(1,2)
+                if lroll == 1:
+                    wleg = "Left"
+                else:
+                    wleg = "Right"
+                iloc = wleg + " Leg"
+                idescription = "The character's leg is crushed, broken, or mangled. His Pace is reduced by 1."
+            else:  #Will need to make another random roll for this one.
+                iloc = "Head"
+                idescription = "Your hero has suffered a grievous injury to his head."
+                hroll = randint(1,6)  #determine how the head is impacted by the wound
+                if hroll == 1 or hroll ==2:
+                    idescription += "<b>Hideous Scar (" + str(hroll) + ")</b>Your hero now has the Ugly Hindrance."
+                elif hroll == 3 or hroll == 4:
+                    idescription += "<b>Blinded (" + str(hroll) + ")</b> One or both of your hero's eyes was damaged. He gains the Bad Eyes Hindrance."
+                else:
+                    idescription += "<b>Brain Damage (" + str(hroll) + ")</b> Your hero suffers massive trauma to the head. His Smarts is reduced one die type (min d4)."
+            idescription += " Make a Vigor roll applying any wound modifiers. If the Vigor roll is failed, the injury is permanent regardless of healing. If the roll is successful, the effect goes away when all wounds are healed."
+            if iroll == 2:
+                idescription +=" If the injury is permanent, reproduction is out of the question without miracle surgery or magic."
+            if loca != "h" and loca != "g" and loca != "c" and loca != "rl" and loca != "ll" and loca != "ra" and loca != "la":
+                idescription +="<br><br><b>***If the attack that caused the Injury was directed at a specific body part, use that location instead of rolling randomly.***</b>"
+            myStr = "[" + wtype + "] ==>" + wdescription + "<br><br><b>Injury Table Result ("+ str(iroll) +"): </b> [" + iloc + "] ==> " + idescription
+        else:
+            myStr = "[" + wtype + "] ==>" + wdescription
+        return myStr
+
+class ract(std):
+    #----------------------The Reaction Table
+    #  This is used to randomly determine the general mood of NPCs toward the player characters.  This simulates a 2d6 roll
+    #  and displays the reaction.  This roll can be modified by the Charisma of the player(s).
+    #  Usage:  [ract()] - No Charisma modifier
+    #          [ract(2)] - A +2 Charisma modifier
+    #          [ract(-2)] - A -2 Charisma modifier
+    #----------------------
+    global charisma
+    def __init__(self,chmod=0):
+        global charisma
+        std.__init__(self)
+        charisma = chmod
+
+    def __str__(self):
+        global charisma
+        r1roll = randint(2,12)
+        rroll = r1roll + charisma
+        if rroll == 2:
+            reaction = "Hostile"
+            rdescription = "The NPC is openly hostile and does his best to stand in the hero's way. He won't help without an overwhelming reward or payment of some kind."
+        elif rroll >=3 and rroll <=4:
+            reaction = "Unfriendly"
+            rdescription = "The NPC is openly hostile and does his best to stand in the hero's way. He won't help without an overwhelming reward or payment of some kind."
+        elif rroll >=5 and rroll <=9:
+            reaction = "Neutral"
+            rdescription = "The NPC has no particular attitude, and will help for little reward if the task at hand is very easy. If the task is difficult, he'll require substantial payment of some kind."
+        elif rroll >=10 and rroll <=11:
+            reaction = "Friendly"
+            rdescription = "The NPC will go out of his way for the hero. He'll likely do easy tasks for free (or very little), and is willing to do more dangerous tasks for fair pay or other favors."
+        else:
+            reaction = "Helpful"
+            rdescription = "The NPC is anxious to help the hero, and will probably do so for little or no pay depending on the nature of the task."
+        #myStr = "[" + reaction + "(" + str(r1roll) + "+Charisma Mods("+str(charisma)+")="+str(rroll)+")] ==> " + rdescription
+        myStr = "["+str(r1roll)+"+"+str(charisma)+"(charisma modifier)="+str(rroll)+"] ==> "+reaction+":  "+rdescription
+        return myStr
+
+class ooc(std):
+    #--------------------The Out of Control Vehicle Table
+    #  This table is used when a vehicle is injured during combat and must determine what happens to the vehicle.  This is a 2d6
+    #  roll and displays the results of the roll.  This will also display altitude information for flying vehicles.
+    #  Usage:  [ooc()]
+    #--------------------
+    def __init__(self):
+        std.__init__(self)
+
+    def __str__(self):
+        ooroll = randint(2,12)
+        oodescripton = "Something"
+        if ooroll == 2:
+            ooeffect = "Roll Over"
+            rroll = randint(1,6)
+            oodescription = "The vehicle performs a Slip and rolls over "+ str(rroll)+ " time"
+            if rroll < 2:
+                oodescription +="s"
+            oodescription += " in that direction. Roll collision damage for the vehicle and everyone inside. Any exterior-mounted weapons or accessories are ruined."
+        elif ooroll == 3 or ooroll == 4:
+            ooeffect = "Spin"
+            sroll = randint(1,6)
+            froll = randint(1,12)
+            oodescription = "Move the vehicle "+str(sroll)+"\" in the direction of the maneuver, or "+str(sroll)+"\" away from a damaging blow. At the end of the Spin,the vehicle is facing is "+str(froll)+" o'clock."
+        elif ooroll >= 5 and ooroll <= 9:
+            ooeffect = "Skid"
+            sroll = randint(1,4)
+            oodescription = "Move the vehicle "+str(sroll)+"\" left or right (in the direction of a failed maneuver, or away from a damaging attack)."
+        elif ooroll == 10 or ooroll == 11:
+            ooeffect = "Slip"
+            sroll = randint(1,6)
+            oodescription = "Move the vehicle "+str(sroll)+"\" left or right (in the direction of a failed maneuver, or away from a damaging attack)."
+        else:
+            ooeffect = "Flip"
+            froll = randint(1,4)
+            oodescription = "The vehicle flips end over end "+str(froll)+" times. Move it forward that many increments of its own length. Roll collision damage for the vehicle, its passengers, and anything it hits. "
+            shroll = randint(1,2)
+            if shroll == 1:
+                oodescription += "<br><br><b>Note:</b> If the vehicle is slow and/or heavy (such as a tank) it Slips instead: "
+                sroll = randint(1,6)
+                oodescription += "Move the vehicle "+str(sroll)+"\" left or right (in the direction of a failed maneuver, or away from a damaging attack)."
+            else:
+                oodescription += "<br><br><b>Note (GM's discretion):</b> If the vehicle is slow and/or heavy (such as a tank) it Skids instead: "
+                sroll = randint(1,4)
+                oodescription += "Move the vehicle "+str(sroll)+"\" left or right (in the direction of a failed maneuver, or away from a damaging attack)."
+
+        oodescription += "<br><br>For flying vehicles conducting combat in the air, the vehicle"
+        altchange = randint(2,12)
+        if altchange == 2:
+            dwn = randint(2,20)
+            oodescription += " loses "+str(dwn)+"\" of altitude."
+        elif altchange == 3 or altchange == 4:
+            dwn = randint(1,10)
+            oodescription += " loses "+str(dwn)+"\" of altitude."
+        elif altchange >= 5 and altchange <= 9:
+            oodescription += " has no change in altitude."
+        else:
+            altup = randint(1,10)
+            oodescription += " gains "+str(altup)+"\" of altitude."
+        myStr = "[" + ooeffect + "(" + str(ooroll) + ")] ==> " + oodescription
+        return myStr
+
+class vcrit(std):
+    #----------------The Critical Hit Vehicle Table
+    #  This table generates a 2d6 roll to determine the Critical Hit results every time a vehicle takes a wound.  There are no
+    #  modifiers to this roll
+    #  Usage [vcrit()]
+    #----------------
+    def __init__(self):
+        std.__init__(self)
+
+    def __str__(self):
+        chitroll = randint(2,12)
+        if chitroll == 2:
+            cheffect = "Scratch and Dent"
+            chdescription = "The attack merely scratches the paint. There's no permanent damage."
+        elif chitroll == 3:
+            cheffect = "Engine"
+            chdescription = "The engine is hit. Oil leaks, pistons misfire, etc. Acceleration is halved (round down). This does not affect deceleration, however."
+        elif chitroll == 4:
+            cheffect = "Locomotion"
+            chdescription = "The wheels, tracks, or whatever have been hit. Halve the vehicle's Top Speed immediately. If the vehicle is pulled by animals, the shot hits one of them instead."
+        elif chitroll == 5:  #Need to make an additional roll to see what direction the vehicle can turn...
+            cheffect = "Controls"
+            troll = randint(1,2)
+            if troll == 1:
+                aturn = "left"
+            else:
+                aturn = "right"
+            chdescription = "The control system is hit. Until a Repair roll is made, the vehicle can only perform turns to the "+str(aturn)+". This may prohibit certain maneuvers as well."
+        elif chitroll >= 6 and chitroll <=8:
+            cheffect = "Chassis"
+            chdescription = "The vehicle suffers a hit in the body with no special effects."
+        elif chitroll == 9 or chitroll == 10:
+            cheffect = "Crew"
+            chdescription = "A random crew member is hit. The damage from the attack is rerolled. If the character is inside the vehicle, subtract the vehicle's Armor from the damage. Damage caused by an explosion affects all passengers in the vehicle."
+        elif chitroll == 11:
+            cheffect = "Weapon"
+            chdescription = "A random weapon on the side of the vehicle that was hit is destroyed and may no longer be used. If there is no weapon, this is a Chassis hit instead (The vehicle suffers a hit in the body with no special effects)."
+        else:
+            cheffect = "Wrecked"
+            chdescription = "The vehicle is wrecked and automatically goes Out of Control.<br><br><b>[Out of Control]</b> ==>"+str(ooc())
+        myStr = "["+cheffect+" ("+str(chitroll)+")] ==> "+chdescription
+        return myStr
+
+    def ooc(self):
+        return vcritooc(self)
+
+class swdhelps(std):
+    #Display help information for this die roller - it will list all the available commands, and how to use them
+    def __init__(self):
+        std.__init__(self)
+
+    def __str__(self):
+        myStr = "<table border='1' valign='top'>\
+        <tr>\
+            <td colspan='3'>This chart will show you the various commands you can use and what is required, etc.  The <i><b>italicized text</b></i> are optional variables.  Any text that is not italicized and is within parentheses is required.  About the only roll that has a required element is the Knockout Blow roll (kob).</td>\
+        </tr>\
+        <tr>\
+            <td align='center'><b>Die Command</b></td><td align='center' width='55%'><b>Description</b></td><td align='center'width='30%'><b>Example</b></td>\
+        </tr>\
+        <tr>\
+            <td><b>[fright(<i>monster's fear modifier</i>)]</b></td><td>Rolls on the <b>Fright Table</b>.  This command generates a number between 1 and 20 and displays the corresponding entry from the Fright Table.</td><td>[fright()]<br>[fright(6)]</td>\
+        </tr>\
+        <tr>\
+            <td><b>[kob(#ofWounds,<i>hitLocation</i>)]</b></td><td>Rolls on the <b>Knockout Blow Table</b> as well as the <b>Injury Table</b> if necessary.  The number of wounds must be specified, however, the location only needs to be specified if a particular body part was targeted.  If a hit location was targeted, then the following codes should be used:<br>\
+                <ul>\
+                    <li>h = head</li>\
+                    <li>g = guts/other vital areas</li>\
+                    <li>c = crotch/groin</li>\
+                    <li>la = left arm</li>\
+                    <li>ra = right arm</li>\
+                    <li>ll = left leg</li>\
+                    <li>rl = right leg</li>\
+                </ul><br>If no hit location is specified, the hit location will be determined when the roll on the Injury Table is necessary.  When specifiying a hit locations, the code must be entered within double quotes.</td><td><b>3 wounds, no called shot</b><br>[kob(3)]<br><b>2 wounds to the head</b><br>[kob(2,\"h\")]</td>\
+        </tr>\
+        <tr>\
+            <td><b>[ract(<i>Charisma Mods</i>)]</b></td><td>Rolls on the <b>Reaction Table</b>.  Will generate the initial reaction to the PCs.  If the Charisma modifiers are supplied, they will be taken into account as well.  Remember that this table is generally only consulted when the reaction of the NPC is comlpetely unknown to the GM.</td><td><b>Reaction no Charisma Mods</b><br>[ract()]<br><b>Reaction with +2 Charisma Mods</b><br>[ract(2)]</td>\
+        </tr>\
+        <tr>\
+            <td><b>[vcrit()]</b></td><td>Rolls on the <b>Critical Hit Table</b> for vehicles.  If a roll on the Out of Control Chart is necessary, it will automatically roll on that table as well.</td><td>[vcrit()]</td>\
+        </tr>\
+        <tr>\
+            <td><b>[ooc()]</b></td><td>Rolls on the <b>Out of Controll Table</b> for vehicles.  This roll will automatically determine any directions/movement rolls as well.</td><td>[ooc()]</td>\
+        </tr>\
+        <tr>\
+            <td><b>[fortune()]</b></td><td>Rolls on the <b>Fortune Table</b> for the Showdown Skirmish rules.  This roll will automatically roll on the <b>Freak Event Table</b> if necessary</td><td>[fortune()]</td>\
+        </tr>\
+        <tr>\
+            <td><b>[freak()]</b></td><td>Rolls on the <b>Freak Event Table</b>.</td><td>[freak()]</td>\
+        </tr>\
+        <tr>\
+            <td><b>[swdhelps()]</b></td><td>Displays this help list.</td><td>[swdhelps()]</td>\
+        </tr>\
+        </table>"
+        return myStr
+
+class fortune(std):
+    def __init___(self):
+        std.__init__(self)
+
+    def __str__(self):
+        forroll = randint(2,12)
+        if forroll == 2 or forroll == 12: #Need to roll on Freak Event Table
+            fortune = "Freak Event!"
+            fdescription = "Roll on the Freak Event Table.<br><br><b>[Freak Event Table]</b> ==> "+str(freak())
+        elif forroll == 3:
+            fortune = "Twist of Fate"
+            fdescription = "Take a benny from your opponent. If he does not have one, he must immediately remove any one Extra from play."
+        elif forroll == 4:
+            fortune = "The Quick and the Dead"
+            fdescription = "Swap one of your opponent's cards for any one of yours."
+        elif forroll == 5:
+            fortune = "Rally"
+            fdescription = "Pick any one unit on the board with Shaken figures. All those figures recover automatically."
+        elif forroll >= 6 and forroll <= 8:
+            fortune = "Hand of Fate"
+            fdescription = "Gain one extra benny."
+        elif forroll == 9:
+            fortune = "Close Call"
+            fdescription = "Any one of your opponent's units stumbles, becomes confused, or is otherwise disrupted. All its members suffer -2 to their trait rolls this round."
+        elif forroll == 10:
+            fortune = "Teamwork"
+            fdescription = "Pick any one other unit within 12\" of this one. Discard its Action Card. It acts on the Joker along with this unit, and gains the usual bonuses as well."
+        else:
+            fortune = "Out of Ammo"
+            fdescription = "Pick any one enemy unit. It's out of ammo or Power Points (your choice). If this result cannot be applied, you gain a benny instead."
+        myStr = "["+fortune+" ("+str(forroll)+")] ==>"+fdescription
+        return myStr
+
+    def freak(self):
+        return fortunefreak(self)
+
+class freak(std):
+    def __init__(self):
+        std.__init__(self)
+
+    def __str__(self):
+        feroll = randint(1,10)
+        if feroll == 1:
+            fevent = "Storm"
+            fedescription = "A sudden storm rolls in. Rain begins to pour and visibility is limited to 12\". All attack rolls are at -1, and black powder weapons don't work at all. The round after this event, all streams become impassable, even at fords. Only bridges remain."
+        elif feroll == 2:
+            fevent = "Fire!"
+            fedescription = "Fire breaks out on the board! Roll randomly among each occupied building, patch of trees, or other flammable terrain type. If none of these are occupied, roll randomly among all flammable terrain pieces. The entire building or forest catches fire this round and causes 2d6 damage to everything within. The fire continues for the rest of the game--unless a storm comes, which quenches it immediately.<br><br>At the beginning of each turn thereafter, roll 1d6 for each flammable structure within 4\" (adjacent buildings, another patch of forest, etc.). On a 4-6, that structure catches fire as well. Check to see if these new fires spread in the following rounds."
+        elif feroll == 3:
+            fevent = "Blood Ties"
+            fedescription = "One of the Wild Cards on the other side is related or has some other special bond with one of your heroes (a Wild Card of your choice). For the rest of the battle, these two won't attack each other directly unless there are no other targets on the board."
+        elif feroll == 4:
+            fevent = "Death of a Hero"
+            inspireroll = randint(1,2)
+            if inspireroll == 1:
+                fedescription ="The next time one of your Wild Cards dies, his noble sacrifice triggers new resolve in his companions.  When your next Wild Card is Incapacitated the rest of your force is inspired by his legacy and adds +1 to all their rolls until another of your Wild Cards is killed."
+            else:
+                fedescription = "The next time one of your Wild Cards dies, his noble sacrifice triggers bone-chilling dread in his companions. When your next Wild Card is Incapacitated the rest of your force is filled with dread. They subtract -1 from all their rolls for the rest of the game until an <i>enemy</i> Wild Card is slain."
+        elif feroll == 5:
+            fevent = "Fickle Fate"
+            fedescription = "Fate favors the underdog. The side with the fewest bennies draws until it has the same number as their foe. Place these in the common pool."
+        elif feroll == 6:
+            fevent = "Back from the Dead"
+            fedescription = "One of your dead was just knocked unconscious. He returns in the spot where he fell. If this is a Wild Card, he returns with but a single wound."
+        elif feroll == 7:
+            fevent = "Bitter Cold/Heat"
+            fedescription = "The weather heats up or cools down, depending on your environment. All troops become tired or bogged down and reduce their running rolls by half for the rest of the game."
+        elif feroll == 8:
+            fevent = "Battle Tested"
+            fedescription = "Any one of your units improves any one skill or attribute a die type immediately."
+        elif feroll == 9:
+            fevent = "The Fog"
+            fedescription = "Dense fog, mist, or smoke rolls drifts over the battlefield. Place two connected Large Burst Templates at the center of one randomly determined board edge. The fog drifts 2d6\" each round in a random direction (roll a d12 and read it like a clock facing). The fog \"bounces\" if it hits an edge in a random direction (so that it never leaves the field)."
+        else:
+            fevent = "Reinforcements"
+            fedescription = "A group of your most common currently-fielded troop type arrives on the field of battle! Place these troops in your deployment area. They act on the Joker this round and are dealt in normally hereafter."
+        myStr = "["+fevent+"("+str(feroll)+")] ==> "+fedescription
+        return myStr
+
+class rdm(std):  #If I get the time and the inspiration - I may try to incorporate a Random Table roller...  I need to think about this one.
+    def __init__(self):
+        std.__init__(self)
+
+    def __str__(self):
+        return myStr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/old_rollers/sr4.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,237 @@
+## a vs die roller as used by WOD games
+#!/usr/bin/env python
+# Copyright (C) 2000-2001 The OpenRPG Project
+#
+#   openrpg-dev@lists.sourceforge.net
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# --
+#
+# File: sr4.py
+# Author: Veggiesama, ripped straight from Michael Edwards (AKA akoman)
+# Maintainer:
+# Version: 1.1
+#
+# 1.1: Now with glitch and critical glitch detection!
+# 1.1: Cleaned up some of the output to make it simpler.
+#
+# Description: Modified from the original Shadowrun dieroller by akoman,
+#              but altered to follow the new Shadowrun 4th Ed dice system.
+#
+#              SR4 VS
+#              Typing [Xd6.vs(Y)] will roll X dice, checking each die
+#              roll against the MIN_TARGET_NUMBER (default: 5). If it
+#              meets or beats it, it counts as a hit. If the total hits
+#              meet or beat the Y value (threshold), there's a success.
+#
+#              SR4 EDGE VS
+#              Identical to the above function, except it looks like
+#              [Xd6.edge(Y)] and follows the "Rule of Six". That rule
+#              states any roll of 6 is counted as a hit and rerolled
+#              with a potential to score more hits. The "Edge" bonus
+#              dice must be included into X.
+#
+#              SR4 INIT
+#              Typing [Xd6.init(Y)] will roll X dice, checking each
+#              die for a hit. All hits are added to Y (the init attrib
+#              of the player), to give an Init Score for the combat.
+#
+#              SR4 EDGE INIT
+#              Typing [Xd6.initedge(Y)] or [Xd6.edgeinit(Y)] will do
+#              as above, except adding the possibility of Edge dice.
+#
+#              Note about non-traditional uses:
+#              - D6's are not required. This script will work with any
+#                die possible, and the "Rule of Six" will only trigger
+#                on the highest die roll possible. Not throughly tested.
+#              - If you want to alter the minimum target number (ex.
+#                score a hit on a 4, 5, or 6), scroll down and change
+#                the global value MIN_TARGET_NUMBER to your liking.
+
+__version__ = "1.1"
+
+from std import std
+from orpg.dieroller.base import *
+
+MIN_TARGET_NUMBER = 5
+GLITCH_NUMBER = 1
+
+class sr4(std):
+    name = "sr4"
+
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+        self.threshold = None
+        self.init_attrib = None
+
+    def vs(self,threshold=0):
+        return sr4vs(self, threshold)
+
+    def edge(self,threshold=0):
+        return sr4vs(self, threshold, 1)
+
+    def init(self,init_attrib=0):
+        return sr4init(self, init_attrib)
+
+    def initedge(self,init_attrib=0):
+        return sr4init(self, init_attrib, 1)
+    def edgeinit(self,init_attrib=0):
+        return sr4init(self, init_attrib, 1)
+
+    def countEdge(self,num):
+        if num <= 1:
+            self
+        done = 1
+        for i in range(len(self.data)):
+            if (self.data[i].lastroll() >= num):
+                # counts every rerolled 6 as a hit
+                self.hits += 1
+                self.data[i].extraroll()
+                self.total += 1
+                done = 0
+            elif (self.data[i].lastroll() <= GLITCH_NUMBER):
+                self.ones += 1
+            self.total += 1
+        if done:
+            return self
+        else:
+            return self.countEdge(num)
+
+    def countHits(self,num):
+        for i in range(len(self.data)):
+            if (self.data[i].lastroll() >= MIN_TARGET_NUMBER):
+                # (Rule of Six taken into account in countEdge(), not here)
+                self.hits += 1
+            elif (self.data[i].lastroll() <= GLITCH_NUMBER):
+                self.ones += 1
+            self.total += 1
+
+    def __str__(self):
+        if len(self.data) > 0:
+            self.hits = 0
+            self.ones = 0
+            self.total = 0
+            for i in range(len(self.data)):
+                if (self.data[i].lastroll() >= MIN_TARGET_NUMBER):
+                    self.hits += 1
+                elif (self.data[i].lastroll() <= GLITCH_NUMBER):
+                    self.ones += 1
+                self.total += 1
+            firstpass = 0
+            myStr = "["
+            for a in self.data[0:]:
+                if firstpass != 0:
+                    myStr += ","
+                firstpass = 1
+                if a >= MIN_TARGET_NUMBER:
+                    myStr += "<B>" + str(a) + "</B>"
+                elif a <= GLITCH_NUMBER:
+                    myStr += "<i>" + str(a) + "</i>"
+                else:
+                    myStr += str(a)
+            myStr += "] " + CheckIfGlitch(self.ones, self.hits, self.total)
+            myStr += "Hits: (" + str(self.hits) + ")"
+        else:
+            myStr = "[] = (0)"
+        return myStr
+
+die_rollers.register(sr4)
+
+class sr4init(sr4):
+    def __init__(self,source=[],init_attrib=1,edge=0):
+        std.__init__(self,source)
+        if init_attrib < 2:
+            self.init_attrib = 2
+        else:
+            self.init_attrib = init_attrib
+        self.dicesides = self[0].sides
+        self.hits = 0
+        self.ones = 0
+        self.total = 0
+        if edge:
+            self.countEdge(self.dicesides)
+        self.countHits(self.dicesides)
+
+    def __str__(self):
+        if len(self.data) > 0:
+            firstpass = 0
+            myStr = "["
+            for a in self.data[0:]:
+                if firstpass != 0:
+                    myStr += ","
+                firstpass = 1
+                if a >= MIN_TARGET_NUMBER:
+                    myStr += "<B>" + str(a) + "</B>"
+                elif a <= GLITCH_NUMBER:
+                    myStr += "<i>" + str(a) + "</i>"
+                else:
+                    myStr += str(a)
+            myStr += "] " + CheckIfGlitch(self.ones, self.hits, self.total)
+            init_score = str(self.init_attrib + self.hits)
+            myStr += "InitScore: " + str(self.init_attrib) + "+"
+            myStr += str(self.hits) + " = (" + init_score + ")"
+        else:
+            myStr = "[] = (0)"
+        return myStr
+
+class sr4vs(sr4):
+    def __init__(self,source=[], threshold=1, edge=0):
+        std.__init__(self, source)
+        if threshold < 0:
+            self.threshold = 0
+        else:
+            self.threshold = threshold
+        self.dicesides = self[0].sides
+        self.hits = 0
+        self.ones = 0
+        self.total = 0
+        if edge:
+            self.countEdge(self.dicesides)
+        self.countHits(self.dicesides)
+
+    def __str__(self):
+        if len(self.data) > 0:
+            firstpass = 0
+            myStr = "["
+            for a in self.data[0:]:
+                if firstpass != 0:
+                    myStr += ","
+                firstpass = 1
+                if a >= MIN_TARGET_NUMBER:
+                    myStr += "<B>" + str(a) + "</B>"
+                elif a <= GLITCH_NUMBER:
+                    myStr += "<i>" + str(a) + "</i>"
+                else:
+                    myStr += str(a)
+            #myStr += "] Threshold=" + str(self.threshold)
+            myStr += "] vs " + str(self.threshold) + " "
+            myStr += CheckIfGlitch(self.ones, self.hits, self.total)
+            if self.hits >= self.threshold:
+                myStr += "*SUCCESS* "
+            else:
+                myStr += "*FAILURE* "
+            myStr += "Hits: (" + str(self.hits) + ")"
+        else:
+            myStr = "[] = (0)"
+        return myStr
+
+def CheckIfGlitch(ones, hits, total_dice):
+    if (ones * 2) >= total_dice:
+        if hits >= 1:
+            return "*GLITCH* "
+        else:
+            return "*CRITICAL GLITCH* "
+    else:
+        return ""
--- a/plugins/xxblank.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/plugins/xxblank.py	Mon Mar 22 18:38:22 2010 -0600
@@ -1,5 +1,8 @@
 import os
 import orpg.pluginhandler
+from orpg.mapper.miniatures_handler import *
+from orpg.orpgCore import *
+from wx import MessageDialog, OK, ICON_EXCLAMATION
 
 class Plugin(orpg.pluginhandler.PluginHandler):
     # Initialization subroutine.
@@ -48,7 +51,18 @@
         self.plugin_add_setting('Setting', 'Value', 'Options', 'Help message')
 
         #This is where you set any variables that need to be initalized when your plugin starts
-        self.sample_variable = {1:'one', 2:'two'}
+        self.sample_variable = {'one':1, 'two':2}
+
+        #You can use the plugin database feature to save and reload data between sessions
+        #The first parameter is arbitrary but should be the name of the plugin; it's a heading for all your data
+        #The second parameter is an arbitrary sub-heading and can be any string but avoid spaces and special charcacters
+        #The third parameter is the default value to return if the item cannot be found in the database
+        self.sample_variable = self.plugindb.GetDict("xxblank", "sample_variables", self.sample_variable)
+        # similarly GetList retrieves a list of things (bool, int, str, list or dict) and GetString a string
+
+        #You can add menu items to the right-click menus on the map's miniature's layer
+        m = open_rpg.get_component("map").layer_handlers[2]
+        m.set_mini_rclick_menu_item("Pirate", self.on_pirate)
 
     def plugin_disabled(self):
         #Here you need to remove any commands you added, and anything else you want to happen when you disable the plugin
@@ -66,6 +80,13 @@
         except:
             pass
 
+        #save to file your session variables
+        self.plugindb.SetDict("xxblank", "sample_variables", self.sample_variable)
+
+        #put the map menu back the way it was
+        m = open_rpg.get_component("map").layer_handlers[2]
+        m.set_mini_rclick_menu_item("Pirate", None)
+
     def on_test(self, cmdargs):
         #this is just an example function for a command you create.
         # cmdargs contains everything you typed after the command
@@ -86,6 +107,16 @@
                 self.chat.InfoPost(msg)
                 i += 1
 
+    def on_pirate(self, event):
+        #this is an example mini right-click menu item
+        #we'll just create a pop-up with some text in it.
+        m = open_rpg.get_component("map").layer_handlers[2]
+        # m.sel_rmin is the mini that was just right-clicked
+        message = m.sel_rmin.label + ' says, "Aaaarrrrrrrrrr!!!'
+        dlg = MessageDialog(None, message, 'Pirate', style=OK|ICON_EXCLAMATION)
+        dlg.ShowModal()
+        dlg.Destroy()
+
     def on_xml_recive(self,id, data,xml_dom):
         self.chat.InfoPost(self.name + ":: Message recived<br />" + data.replace("<","&lt;").replace(">","&gt;") +'<br />From id:' + str(id))
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxbonuses.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,301 @@
+
+import orpg.pluginhandler
+from orpg.mapper.miniatures_handler import *
+from orpg.orpgCore import *
+from orpg.orpg_windows import *
+from orpg.mapper.base_handler import base_layer_handler
+import wx
+import types
+from orpg.chat.chatwnd import compiled_simple_arithmetic_regex
+from orpg.chat.commands import command_args_parser
+from orpg.gametree.nodehandlers.core import node_handler
+
+
+class Bonus:
+    def __init__(self, number, path, bonus_type):
+        self.number = int(number) # amount added (can be negative)
+        self.path = path # fullpath or namespace path (not leaf)
+        self.type = bonus_type # arbitrary string or empty
+
+class Effect:
+    def __init__(self, name, desc, namespace_name, expire, bonus_list):
+        self.name = name
+        self.desc = desc
+        self.namespace_name = namespace_name
+        self.expire = expire
+        self.bonus_list = bonus_list
+
+# add these functions to the chat object
+def add_effect(self, effect):
+    self.effects.append(effect)
+    for bonus in effect.bonus_list:
+        self.add_bonus(effect.namespace_name, bonus)
+        
+def delete_effect(self, effect):
+    for bonus in effect.bonus_list:
+        self.delete_bonus(effect.namespace_name, bonus)
+    self.effects.remove(effect)
+        
+def add_bonus(self, namespace_name, bonus):
+    key = (namespace_name, bonus.path)
+    if key in self.bonus_map:
+        bonus_list = self.bonus_map[key]
+        bonus_list.append(bonus)
+    else:
+        self.bonus_map[key]=[bonus]
+        
+def delete_bonus(self, namespace_name, bonus):
+    key = (namespace_name, bonus.path)
+    if key in self.bonus_map:
+        bonus_list = self.bonus_map[key]
+        bonus_list.remove(bonus)
+
+# overrides the existing chat method    
+def my_handle_adding_bonuses(self, value, handler):
+    if compiled_simple_arithmetic_regex.match(value):
+        newint = eval(value)
+        fullpath, v, namespace_name, namespace_path = open_rpg.get_component('tree').get_path_data(handler.mytree_node)
+        if namespace_name:
+            key = (namespace_name, namespace_path)
+        else:
+            key = ("", fullpath)
+        if key in self.bonus_map:
+            typed_bonuses = {}
+            for bonus in self.bonus_map[key]:
+                if bonus.type:
+                    if bonus.type in typed_bonuses:
+                        typed_bonuses[bonus.type] = max(bonus.number, typed_bonuses[bonus.type])
+                    else:
+                        typed_bonuses[bonus.type] = bonus.number
+                else:
+                    newint += bonus.number
+            for number in typed_bonuses.values():
+                newint += number
+            value = str(newint)
+    return value
+            
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+        self.name = 'Bonuses and Effects'
+        self.author = 'David'
+        self.help = """Allows on-the-fly temporary modification of character sheet values.
+Requires OpenRPG 1.8.0+ development
+EXPERIMENTAL! In the process of being updated."""
+
+    def plugin_enabled(self):
+        c = open_rpg.get_component("chat")
+        c.bonus_map = {}
+        c.effects = []
+        self.old_handle_adding_bonuses = c.handle_adding_bonuses
+        c.handle_adding_bonuses = types.MethodType(my_handle_adding_bonuses, c, c.__class__)
+        c.add_bonus = types.MethodType(add_bonus, c, c.__class__)
+        c.delete_bonus = types.MethodType(delete_bonus, c, c.__class__)
+        c.add_effect = types.MethodType(add_effect, c, c.__class__)
+        c.delete_effect = types.MethodType(delete_effect, c, c.__class__)
+        self.plugin_addcommand('/bonus', self.on_bonus, '/bonus number target [type]')
+        self.plugin_addcommand('/effect', self.on_effect, '/effect name desc= alias= expire= number= path= [type=] number= path= [type=]')
+        self.plugin_commandalias('/b', '/bonus')
+        self.plugin_addcommand('/view_effects', self.on_view_effects, 'bring up the effects view (use this to delete effects)')
+        self.plugin_commandalias('/ve', '/view_effects')
+        # load effects from last session
+        db_effects_list = self.plugindb.GetList("xxbonuses", "effect_list", [])
+        for db_effect_data in db_effects_list:
+            # if the ordering of these data is messed with you will break old saved data
+            name = db_effect_data[0]
+            desc = db_effect_data[1]
+            namespace_name = db_effect_data[2]
+            expire = db_effect_data[3]
+            db_bonus_list = db_effect_data[4]
+            bonus_list = []
+            for b in db_bonus_list:
+                number = b[0] # int
+                path = b[1]
+                bonus_type = b[2]
+                bonus_list.append(Bonus(number, path, bonus_type))
+            open_rpg.get_component("chat").add_effect(Effect(name, desc, namespace_name, expire, bonus_list))
+        
+    def plugin_disabled(self):
+        c = open_rpg.get_component("chat")
+        #save list of effects
+        db_effects_list = []
+        for effect in c.effects:
+            db_bonus_list = []
+            for bonus in effect.bonus_list:
+                db_bonus_list.append([bonus.number, bonus.path, bonus.type])
+            db_effect_data = [effect.name, effect.desc, effect.namespace_name, effect.expire, db_bonus_list]
+            db_effects_list.append(db_effect_data)
+        self.plugindb.SetList("xxbonuses", "effect_list", db_effects_list)
+        c.handle_adding_bonuses = self.old_handle_adding_bonuses
+        del c.add_bonus
+        del c.delete_bonus
+        del c.add_effect
+        del c.delete_effect
+        del c.bonus_map
+        del c.effects
+        self.plugin_removecmd('/bonus')
+        self.plugin_removecmd('/b')
+        self.plugin_removecmd('/effect')
+        self.plugin_removecmd('/view_effects')
+        self.plugin_removecmd('/ve')
+
+    def on_bonus(self, cmdargs):
+        values, key_values = command_args_parser(cmdargs)
+        if len(values) < 2:
+            open_rpg.get_component("chat").InfoPost("/effect: Not enough params passed.")
+            return
+        try:
+            number = int(values[0])
+        except:
+            open_rpg.get_component("chat").InfoPost("First parameter must be an integer.")
+            return
+        path = values[1]
+        handler = open_rpg.get_component('tree').get_handler_by_path(path, open_rpg.get_component("chat").chat_cmds.context)
+        if handler is None:
+            open_rpg.get_component("chat").InfoPost("Second parameter must reference a node.")
+            return
+##        value = handler.get_value()
+##        try:
+##            value = int(value)
+##        except:
+##            open_rpg.get_component("chat").InfoPost("Value of node must be an integer.")
+##            return
+        if 'type' in key_values:
+            bonus_type = key_values['type']
+        else:
+            bonus_type = ''
+        fullpath, v, namespace_name, namespace_path = open_rpg.get_component('tree').get_path_data(handler.mytree_node)
+        if namespace_name:
+            effect = Effect("", "", namespace_name, "", [Bonus(number, namespace_path, bonus_type)])
+        else:
+            effect = Effect("", "", "", "", [Bonus(number, fullpath, bonus_type)])
+        open_rpg.get_component("chat").add_effect(effect)
+
+    def on_effect(self, cmdargs):
+        chat = open_rpg.get_component("chat")
+        values, key_values = command_args_parser(cmdargs)
+        if len(values) < 1:
+            chat.InfoPost("/effect: Not enough params passed.")
+            return
+        if 'number' not in key_values or 'path' not in key_values:
+            chat.InfoPost("/effect: No number/path params passed.")
+            return
+        name = values[0]
+        if 'desc' in key_values:
+            desc = key_values['desc']
+        else:
+            desc = ''
+        # determine namespace_name from one of various sources
+        if 'alias' in key_values:
+            namespace_name = key_values['alias'].lower().strip()
+            if namespace_name != "" and open_rpg.get_component('tree').get_namespace_by_name(namespace_name) is None:
+                chat.InfoPost("/effect: the alias passed was not a namespace.")
+                return
+        else:
+            context = chat.chat_cmds.context
+            if isinstance(context.namespace_hint, node_handler):
+                namespace_name = open_rpg.get_component('tree').get_namespace_name(context.namespace_hint.mytree_node)
+            elif isinstance(context.namespace_hint, (str, unicode)):
+                namespace_name = context.namespace_hint.lower().strip()
+            else:
+                namespace_name = ''
+        if 'expire' in key_values:
+            expire = key_values['expire']
+        else:
+            expire = ''
+        numbers = key_values['number']
+        if not isinstance(numbers, list):
+            numbers = [numbers]
+        paths = key_values['path']
+        if not isinstance(paths, list):
+            paths = [paths]
+        if len(numbers) != len(paths):
+            chat.InfoPost("/effect: number and path params must be paired.")
+            return
+        if 'type' in key_values:
+            types = key_values['type']
+            if not isinstance(types, list):
+                types = [types]
+            if len(numbers) != len(types):
+                chat.InfoPost("/effect: number and type params must be matched if any type is given.")
+                return
+        else:
+            types = [''] * len(numbers)
+        bonus_list = []
+        for i in range(len(numbers)):
+            # check validity of path / replace with fullpath (not leaf)
+            handler = open_rpg.get_component('tree').get_handler_by_path(paths[i], ParserContext(namespace_name))
+            if handler is None:
+                chat.InfoPost("Path parameters must be indexed nodes.")
+                return
+##            value = handler.get_value()
+##            try:
+##                value = int(value)
+##            except:
+##                chat.InfoPost("Value of node to receive bonus must be an integer.")
+##                return
+            fullpath, v, n, namespace_path = open_rpg.get_component('tree').get_path_data(handler.mytree_node)
+            if namespace_name:
+                path = namespace_path
+            else:
+                path = fullpath
+            bonus_list.append(Bonus(numbers[i], path, types[i]))
+        effect = Effect(name, desc, namespace_name, expire, bonus_list)
+        chat.add_effect(effect)
+
+    def on_view_effects(self, cmdargs):
+        frame = BonusFrame()
+        frame.Show()
+        #frame.Destroy()
+
+
+LIST_CTRL = wx.NewId()
+BUT_DEL = wx.NewId()
+BUT_DEL_ALL = wx.NewId()
+
+class BonusFrame(wx.Frame):
+    def __init__(self):
+        wx.Frame.__init__(self, None, -1)
+        self.SetTitle('List of bonuses and effects')
+        self.listbox = wx.ListCtrl(self, LIST_CTRL, style=wx.LC_REPORT)
+        self.listbox.InsertColumn(0, 'Name')
+        self.listbox.InsertColumn(1, 'Description')
+        self.listbox.InsertColumn(2, 'Alias')
+        self.listbox.InsertColumn(3, 'Expiration')
+        self.listbox.InsertColumn(4, 'Bonuses')
+        self.listbox.SetColumnWidth(0, 25)
+        self.listbox.SetColumnWidth(0, 80)
+        self.listbox.SetColumnWidth(0, 25)
+        self.listbox.SetColumnWidth(0, 40)
+        self.listbox.SetColumnWidth(0, 80)
+        self.effects = []
+        for effect in open_rpg.get_component("chat").effects:
+            bonus_description = ', '.join([str(bonus.number)+" "+bonus.path for bonus in effect.bonus_list])
+            self.listbox.Append((effect.name, effect.desc, effect.namespace_name, effect.expire, bonus_description))
+            self.effects.append(effect)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.listbox, 1, wx.EXPAND)
+        sizer.Add(wx.Button(self, BUT_DEL, "Delete"), 0, wx.EXPAND)
+        sizer.Add(wx.Button(self, BUT_DEL_ALL, "Delete All"), 0, wx.EXPAND)
+        self.SetSizer(sizer)
+        #self.SetAutoLayout(True)
+        self.Bind(wx.EVT_BUTTON, self.on_delete, id=BUT_DEL)
+        self.Bind(wx.EVT_BUTTON, self.on_delete_all, id=BUT_DEL_ALL)
+
+    def on_delete(self, evt):
+        index = self.listbox.GetFocusedItem()
+        if index >= 0:
+            effect = self.effects[index]
+            open_rpg.get_component("chat").delete_effect(effect)
+            self.effects[index:index+1] = []
+            self.listbox.DeleteItem(index)
+
+    def on_delete_all(self, evt):
+        dlg = wx.MessageDialog(self, "Are you sure you want to delete all bonuses and effects?","Delete All",wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
+        if dlg.ShowModal() != wx.ID_YES:
+            return
+        open_rpg.get_component("chat").effects = []
+        open_rpg.get_component("chat").bonus_map = {}
+        self.effects = []
+        self.listbox.DeleteAllItems()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxminimenu.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,124 @@
+
+import orpg.pluginhandler
+from orpg.mapper.miniatures_handler import *
+from orpg.orpgCore import *
+from orpg.orpg_windows import *
+from orpg.mapper.base_handler import base_layer_handler
+import wx
+import types
+
+# these functions will be added to the OpenRPG miniatures_handler, some override existing methods
+def my_on_node_menu_item(self, evt):
+    menuid = evt.GetId()
+    if menuid >= self.menuid0 and menuid < self.menuid0+100:
+        handler = open_rpg.get_component("tree").GetPyData(self.menuId_nodeId_map[menuid])
+        text = ''
+        if handler:
+            text = handler.get_value() # or should it be tohtml()?
+        #self.selectAliasName(self.sel_rmin.label)
+        open_rpg.get_component("chat").ParsePost(text, True, True, ParserContext(handler))
+
+def my_selectAliasName(self, aliasName):
+    names = open_rpg.get_component("chat").aliasList.GetItems()
+    index = 0
+    for name in names:
+        if name == aliasName:
+            open_rpg.get_component("chat").aliasList.SetSelection(index)
+            return
+        index += 1
+
+def my_recursiveAddMenuNode(self, tree_node_id, parent_menu, menuid, top_name):
+    if menuid == self.menuid0+100: # max of 100 added menu items
+        return
+    (child_id, cookie) = open_rpg.get_component("tree").GetFirstChild(tree_node_id)
+    if child_id.IsOk():
+        # a container means a sub-menu
+        menu = wx.Menu()
+        myName = top_name
+        if myName == None:
+            myName = open_rpg.get_component("tree").GetItemText(tree_node_id)
+        parent_menu.AppendMenu(menuid, myName, menu)
+        menuid += 1
+        while child_id.IsOk():
+            menuid = self.recursiveAddMenuNode(child_id, menu, menuid, None)
+            (child_id, cookie) = open_rpg.get_component("tree").GetNextChild(tree_node_id, cookie)
+    else:
+        # leaf node means a normal menu item
+        parent_menu.Append(menuid, open_rpg.get_component("tree").GetItemText(tree_node_id))
+        self.menuId_nodeId_map[menuid] = tree_node_id
+        menuid += 1
+    return menuid
+
+def my_on_mini_dclick(self, evt, mini):
+    """overrides the default function"""
+    if mini.label:
+        handler = open_rpg.get_component("tree").get_handler_by_path("mini_left_dclick", ParserContext(mini.label))
+        if handler != None:
+            event = 0
+            handler.on_use(event)
+
+def my_prepare_mini_rclick_menu(self, evt):
+    """overrides the default function"""
+    self.build_menu()
+    handler = open_rpg.get_component("tree").get_handler_by_path("mini_right_click", ParserContext(self.sel_rmin.label))
+    if handler is None:
+        return
+    parent_id = handler.mytree_node
+    if not parent_id:
+        return
+    self.min_menu.AppendSeparator()
+    self.menuId_nodeId_map = {}
+    self.recursiveAddMenuNode(parent_id, self.min_menu, self.menuid0, "Nodes")
+
+def my_bind_100_ids(self):
+    self.menuid0 = wx.NewId()
+    self.canvas.Bind(wx.EVT_MENU, self.on_node_menu_item, id=self.menuid0)
+    for i in range(1, 99):
+        wx.NewId()
+        self.canvas.Bind(wx.EVT_MENU, self.on_node_menu_item, id=self.menuid0+i)
+
+def my_unbind_100_ids(self):
+    for i in range(100):
+        self.canvas.Unbind(wx.EVT_MENU, id=self.menuid0+i)
+    self.build_menu()
+
+            
+class Plugin(orpg.pluginhandler.PluginHandler):
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+        self.name = 'Mini Menu'
+        self.author = 'David'
+        self.help = """add context menu and double-click popup to map minis.
+Requires OpenRPG 1.8.0+ development
+EXPERIMENTAL! In the process of being updated."""
+
+    def plugin_enabled(self):
+        # all we do is add methods to the miniatures_handler object, and call bind
+        m = open_rpg.get_component("map").layer_handlers[2]
+        self.old_prepare_mini_rclick_menu = m.prepare_mini_rclick_menu
+        self.old_on_mini_dclick = m.on_mini_dclick
+        m.prepare_mini_rclick_menu = types.MethodType(my_prepare_mini_rclick_menu, m, m.__class__)
+        m.on_mini_dclick           = types.MethodType(my_on_mini_dclick, m, m.__class__)
+        m.recursiveAddMenuNode     = types.MethodType(my_recursiveAddMenuNode, m, m.__class__)
+        m.selectAliasName          = types.MethodType(my_selectAliasName, m, m.__class__)
+        m.on_node_menu_item        = types.MethodType(my_on_node_menu_item, m, m.__class__)
+        m.bind_100_ids             = types.MethodType(my_bind_100_ids, m, m.__class__)
+        m.unbind_100_ids           = types.MethodType(my_unbind_100_ids, m, m.__class__)
+        m.bind_100_ids()
+    
+    def plugin_disabled(self):
+        # call unbind, put back old methods, delete methods that didn't exist before
+        m = open_rpg.get_component("map").layer_handlers[2]
+        m.unbind_100_ids()
+        m.prepare_mini_rclick_menu = self.old_prepare_mini_rclick_menu
+        m.on_mini_dclick = self.old_on_mini_dclick
+        del m.recursiveAddMenuNode
+        del m.selectAliasName
+        del m.on_node_menu_item
+        del m.bind_100_ids
+        del m.unbind_100_ids
+        del m.menuid0
+
+
+    
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxminiquicknote.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,68 @@
+import os
+import orpg.pluginhandler
+from orpg.mapper.miniatures_handler import *
+from orpg.orpgCore import *
+import wx
+
+
+class QuickNoteDialog(wx.Dialog):
+    def __init__(self):
+        wx.Dialog.__init__(self, None, -1, "Quick Note", size=(250, 210))
+        self.text = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.text, 1, wx.EXPAND)
+        self.SetSizer(sizer)
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+        self.name = 'Mini Quick Notes'
+        self.author = 'David'
+        self.help = """Allows you to add private notes when you right-click on a mini on the map.
+Note: it will try to save data between sessions but you must load plugin AFTER the map has been loaded
+so do not auto-load this plugin.  Also these notes are NOT shared with other users.
+Look at the mini superscript plugin for an example of how to do that.
+Requires OpenRPG 1.8.0+ development
+EXPERIMENTAL! In the process of being updated."""
+        self.save_on_exit = False;
+        
+    def plugin_enabled(self):
+        m = open_rpg.get_component("map").layer_handlers[2]
+        m.set_mini_rclick_menu_item("Quick Notes", self.on_quick_note)
+
+        db_notes_list = self.plugindb.GetList("xxminiquicknote", "notes", [])
+        matched = 0;
+        for mini in open_rpg.get_component("map").canvas.layers['miniatures'].miniatures:
+            print 'label:'+mini.label
+            print 'posx:'+str(mini.pos.x)
+            print 'posy:'+str(mini.pos.y)
+            for db_note in db_notes_list:
+                print 'db_note:'+str(db_note)
+                if mini.label == db_note[0] and mini.pos.x == db_note[1] and mini.pos.y == db_note[2]:
+                    mini.quicknote = db_note[3]
+                    db_notes_list.remove(db_note)
+                    matched += 1
+                    break
+
+    def plugin_disabled(self):
+        m = open_rpg.get_component("map").layer_handlers[2]
+        m.set_mini_rclick_menu_item("Quick Notes", None)
+
+        if self.save_on_exit:
+            db_notes_list = []
+            for mini in open_rpg.get_component("map").canvas.layers['miniatures'].miniatures:
+                if 'quicknote' in mini.__dict__:
+                    db_notes_list.append([mini.label, mini.pos.x, mini.pos.y, mini.quicknote])
+            self.plugindb.SetList("xxminiquicknote", "notes", db_notes_list)
+
+    def on_quick_note(self, event):
+        self.save_on_exit = True
+        m = open_rpg.get_component("map").layer_handlers[2]
+        # m.sel_rmin is the mini that was just right-clicked
+        if 'quicknote' not in m.sel_rmin.__dict__:
+            m.sel_rmin.quicknote = ''
+        dlg = QuickNoteDialog()
+        dlg.text.SetValue(m.sel_rmin.quicknote)
+        dlg.ShowModal()
+        m.sel_rmin.quicknote = dlg.text.GetValue()
+        dlg.Destroy()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxminisuperscript.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,52 @@
+import os
+import orpg.pluginhandler
+from orpg.mapper.miniatures import register_mini_draw_callback_function, mini_additional_attribute_list
+from orpg.mapper.miniatures_handler import *
+from orpg.orpgCore import *
+import wx
+
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+        self.name = 'Mini Superscript'
+        self.author = 'David'
+        self.help = """Adds a temporary custom superscript to miniatures on the map.
+Requires OpenRPG 1.8.0+ development
+EXPERIMENTAL! In the process of being updated."""
+
+    def plugin_enabled(self):
+        m = open_rpg.get_component("map").layer_handlers[2]
+        m.set_mini_rclick_menu_item("Superscript", self.on_superscript)
+        register_mini_draw_callback_function(self.on_draw)
+        mini_additional_attribute_list.append('superscript')
+
+    def plugin_disabled(self):
+        register_mini_draw_callback_function(self.on_draw)
+        m = open_rpg.get_component("map").layer_handlers[2]
+        m.set_mini_rclick_menu_item("Superscript", None)
+        #mini_additional_attribute_list.remove('superscript')
+
+    def on_draw(self, mini, dc):
+        if 'superscript' in mini.__dict__:
+            width, height = dc.GetTextExtent(mini.superscript)
+            dc.SetPen(wx.Pen('#FFFF00'))#yellow
+            dc.SetBrush(wx.Brush('#FFFF00'))#yellow
+            dc.DrawRectangle(mini.pos.x-1, mini.pos.y, width+2, height) 
+            dc.SetTextForeground(wx.BLACK)
+            dc.DrawText(mini.superscript, mini.pos.x, mini.pos.y)
+
+    def on_superscript(self, event):
+        m = open_rpg.get_component("map").layer_handlers[2]
+        # m.sel_rmin is the mini that was just right-clicked
+        if 'superscript' not in m.sel_rmin.__dict__:
+            m.sel_rmin.superscript = ''
+        dlg = wx.TextEntryDialog(None, "", "Superscript")
+        dlg.SetValue(m.sel_rmin.superscript)
+        if dlg.ShowModal() == wx.ID_OK:
+            m.sel_rmin.superscript = dlg.GetValue()
+            m.sel_rmin.isUpdated = True
+            open_rpg.get_component("map").canvas.Refresh(False)
+            open_rpg.get_component("map").canvas.send_map_data()
+        dlg.Destroy()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxminitooltip.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,28 @@
+
+import orpg.pluginhandler
+from orpg.mapper.miniatures_handler import *
+from orpg.orpgCore import *
+from orpg.orpg_windows import *
+from orpg.mapper.base_handler import base_layer_handler
+import wx
+import types
+
+def my_get_mini_tooltip(self, mini_list):
+    return ", ".join([mini.label for mini in mini_list])
+    
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+        self.name = 'Mini Tooltip'
+        self.author = 'David'
+        self.help = """Add a tooltip displaying the name of the mini.  For OpenRPG 1.8.0+"""
+
+    def plugin_enabled(self):
+        m = open_rpg.get_component("map").layer_handlers[2]
+        self.old_get_mini_tooltip = m.get_mini_tooltip
+        m.get_mini_tooltip = types.MethodType(my_get_mini_tooltip, m, m.__class__)
+        
+    def plugin_disabled(self):
+        m = open_rpg.get_component("map").layer_handlers[2]
+        m.get_mini_tooltip = self.old_get_mini_tooltip
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxold_rollers.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,16 @@
+import os
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+
+        # The Following code should be edited to contain the proper information
+        self.name = 'Old Rollers'
+        self.author = 'Dj Gilcrease'
+        self.help = 'A group of old style rollers that have or will not be converted to the new system'
+
+    def plugin_enabled(self):
+        import plugins.old_rollers
+
+    def plugin_disabled(self): pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxtextcompletion.py	Mon Mar 22 18:38:22 2010 -0600
@@ -0,0 +1,33 @@
+
+import orpg.pluginhandler
+from orpg.mapper.miniatures_handler import *
+from orpg.orpgCore import *
+from orpg.orpg_windows import *
+from orpg.mapper.base_handler import base_layer_handler
+import wx    
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+        self.name = 'Text Completion'
+        self.author = 'David'
+        self.help = """Text completion will be based on selecting a node name from the tree (not other player's names).
+Requires OpenRPG 1.8.0+ development
+EXPERIMENTAL! In the process of being updated."""
+
+    def plugin_enabled(self):
+        open_rpg.get_component('chat').get_completion_word_set = self.get_node_names_set
+        
+    def plugin_disabled(self):
+        open_rpg.get_component('chat').get_completion_word_set = open_rpg.get_component('chat').get_player_set
+        
+    def get_node_names_set(self):
+        # could take into account the selected alias
+        # could calculate once instead of re-calculating every time
+        names = set()
+        open_rpg.get_component('tree').traverse(open_rpg.get_component('tree').root, self.add_name, names)
+        return names
+
+    def add_name(self, item, data):
+        if open_rpg.get_component('tree').is_referenceable(item):
+            data.add(open_rpg.get_component('tree').GetItemText(item))
--- a/updater/_updater.py	Tue Oct 06 16:03:42 2009 -0600
+++ b/updater/_updater.py	Mon Mar 22 18:38:22 2010 -0600
@@ -161,6 +161,9 @@
         except urllib2.HTTPError:
             self.write("The update server seems to be down, try again later")
             return None
+        except urllib2.URLError:
+            print "Couldn't connect to update server.  Check internet access."
+            return None
         finally:
             try:
                 conn.close()