changeset 0:4385a7d0efd1 grumpy-goblin

Deleted and repushed it with the 'grumpy-goblin' branch. I forgot a y
author sirebral
date Tue, 14 Jul 2009 16:41:58 -0500
parents
children f24c6e431a15
files __init__.py data/SWd20/SWd20classes.xml data/SWd20/d20armor.xml data/SWd20/d20feats.xml data/SWd20/d20weapons.xml data/d20/d20armor.xml data/d20/d20classes.xml data/d20/d20divine.xml data/d20/d20feats.xml data/d20/d20powers.xml data/d20/d20spells.xml data/d20/d20weapons.xml data/dnd35/books.txt data/dnd35/classes.txt data/dnd35/dnd35armor.xml data/dnd35/dnd35character.xml data/dnd35/dnd35classes.xml data/dnd35/dnd35feats.xml data/dnd35/dnd35weapons.xml data/dnd35/feats.txt data/dnd3e/dnd3earmor.xml data/dnd3e/dnd3echaracter.xml data/dnd3e/dnd3eclasses.xml data/dnd3e/dnd3edivine.xml data/dnd3e/dnd3efeats.xml data/dnd3e/dnd3epowers.xml data/dnd3e/dnd3espells.xml data/dnd3e/dnd3eweapons.xml data/dnd3e/feats.txt data/dnd3e/skills.txt images/8ball.gif images/WAmisc7.ico images/WAmisc9.ico images/add_filter.gif images/apoc.gif images/arc.png images/b_d10.gif images/b_d100.gif images/b_d12.gif images/b_d20.gif images/b_d4.gif images/b_d6.gif images/b_d8.gif images/bold.gif images/book.gif images/bricktile.gif images/browser.gif images/bullet.gif images/car.gif images/ccmap.gif images/chess.gif images/circle.png images/clear.gif images/close_wnd.bmp images/compass.gif images/compass.ico images/connect.gif images/crosshair.gif images/cyborg.gif images/d10.gif images/d20.gif images/d20.ico images/d20.xpm images/d20_logo.gif images/d4.gif images/d8.gif images/dash.png images/defaultmap.png images/delete_filter.gif images/dice.bmp images/die.gif images/divider.png images/draw.gif images/drugs.gif images/earth.gif images/edit_filter.gif images/fetching.png images/flask.gif images/flask.ico images/fogoff.png images/fogon.png images/folder.gif images/form.png images/frame.bmp images/gear.gif images/goblin.gif images/goblin.ico images/grenade.gif images/grid.gif images/grid.ico images/gun1.gif images/gun2.gif images/help.gif images/hidefog.png images/html.gif images/html.ico images/icons.xml images/img.gif images/install.gif images/italic.gif images/knight.gif images/labtop.gif images/money.gif images/mouse.gif images/move.gif images/ninja.gif images/noplayer.gif images/note.gif images/note.ico images/open.bmp images/orc.gif images/oriental.gif images/pin.gif images/planet.gif images/player-whisper.gif images/player.gif images/python55.gif images/questionhead.gif images/r2d2.gif images/rectangle.png images/rome.gif images/save.bmp images/sflogo.png images/shades.gif images/showfog.png images/skull.gif images/skull_16.gif images/smsword2.gif images/spears.gif images/splash.gif images/splash.jpg images/splash1.jpg images/splash13.jpg images/splitwin.bmp images/startrek.gif images/sword.gif images/tab.bmp images/tab_close.png images/tab_on.png images/tabber.png images/tank1.gif images/tank2.gif images/tape.gif images/text.png images/thief.gif images/tiefighter.gif images/underlined.gif images/wizard1.gif images/wxPyButton.png images/wxWinButton.png images/zoom_in.gif images/zoom_out.gif license.txt myfiles/webfiles/Textures/Copyright Notice.txt myfiles/webfiles/Textures/grass-natural.jpg myfiles/webfiles/Textures/grass11.jpg myfiles/webfiles/Textures/sandwave.jpg myfiles/webfiles/Textures/steelpops11.jpg myfiles/webfiles/Textures/versa_anigre.jpg myfiles/webfiles/Textures/water06.jpg myfiles/webfiles/Textures/water20.jpg orpg/__init__.py orpg/chat/__init__.py orpg/chat/chat_msg.py orpg/chat/chat_util.py orpg/chat/chat_version.py orpg/chat/chatwnd.py orpg/chat/commands.py orpg/dieroller/HOWTO.txt orpg/dieroller/__init__.py orpg/dieroller/d20.py orpg/dieroller/die.py orpg/dieroller/dieroller.txt orpg/dieroller/gurps.py orpg/dieroller/hackmaster.py orpg/dieroller/hero.py orpg/dieroller/runequest.py orpg/dieroller/savage.py orpg/dieroller/shadowrun.py orpg/dieroller/sr4.py orpg/dieroller/srex.py orpg/dieroller/trinity.py orpg/dieroller/utils.py orpg/dieroller/wod.py orpg/dieroller/wodex.py orpg/dirpath/__init__.py orpg/dirpath/dirpath_tools.py orpg/gametree/__init__.py orpg/gametree/gametree.py orpg/gametree/gametree_version.py orpg/gametree/nodehandlers/StarWarsd20.py orpg/gametree/nodehandlers/__init__.py orpg/gametree/nodehandlers/chatmacro.py orpg/gametree/nodehandlers/containers.py orpg/gametree/nodehandlers/core.py orpg/gametree/nodehandlers/core.py~ orpg/gametree/nodehandlers/d20.py orpg/gametree/nodehandlers/dnd35.py orpg/gametree/nodehandlers/dnd3e.py orpg/gametree/nodehandlers/forms.py orpg/gametree/nodehandlers/map_miniature_nodehandler.py orpg/gametree/nodehandlers/minilib.py orpg/gametree/nodehandlers/minilib.py~ orpg/gametree/nodehandlers/nodehandler_version.py orpg/gametree/nodehandlers/rpg_grid.py orpg/gametree/nodehandlers/voxchat.py orpg/main.py orpg/map/__init__.py orpg/map/_background.py orpg/map/_canvas.py orpg/map/_circles.py orpg/map/_fog.py orpg/map/_grid.py orpg/map/_lines.py orpg/map/_minis.py orpg/map/_object.py orpg/map/_text.py orpg/map/_tiles.py orpg/mapper/__init__.py orpg/mapper/background.py orpg/mapper/background_handler.py orpg/mapper/background_msg.py orpg/mapper/base.py orpg/mapper/base_handler.py orpg/mapper/base_msg.py orpg/mapper/fog.py orpg/mapper/fog_handler.py orpg/mapper/fog_msg.py orpg/mapper/grid.py orpg/mapper/grid_handler.py orpg/mapper/grid_msg.py orpg/mapper/images.py orpg/mapper/isometric.py orpg/mapper/map.py orpg/mapper/map_handler.py orpg/mapper/map_msg.py orpg/mapper/map_prop_dialog.py orpg/mapper/map_utils.py orpg/mapper/map_version.py orpg/mapper/min_dialogs.py orpg/mapper/miniatures.py orpg/mapper/miniatures_handler.py orpg/mapper/miniatures_msg.py orpg/mapper/region.py orpg/mapper/whiteboard.py orpg/mapper/whiteboard_handler.py orpg/mapper/whiteboard_msg.py orpg/minidom.py orpg/networking/__init__.py orpg/networking/gsclient.py orpg/networking/meta_server_lib.py orpg/networking/mplay_client.py orpg/networking/mplay_groups.py orpg/networking/mplay_messaging.py orpg/networking/mplay_server.py orpg/networking/mplay_server_gui.py orpg/networking/server_plugins.py orpg/orpgCore.py orpg/orpg_version.py orpg/orpg_windows.py orpg/orpg_wx.py orpg/orpg_xml.py orpg/player_list.py orpg/plugindb.py orpg/pluginhandler.py orpg/pulldom.py orpg/systempath.py orpg/templates/about.html orpg/templates/default_LobbyMessage.html orpg/templates/default_Lobby_map.xml orpg/templates/default_alias.alias orpg/templates/default_ban_list.xml orpg/templates/default_gui.xml orpg/templates/default_ini.xml orpg/templates/default_layout.xml orpg/templates/default_map.xml orpg/templates/default_plugindb.xml orpg/templates/default_server_ini.xml orpg/templates/default_settings.xml orpg/templates/default_tree.xml orpg/templates/feature.xml orpg/templates/metaservers.cache orpg/templates/nodes/Bastion_adventure.xml orpg/templates/nodes/Darwin_adventure.xml orpg/templates/nodes/FFE_adventure.xml orpg/templates/nodes/Idiots_guide_to_openrpg.xml orpg/templates/nodes/MiniatureLibrary.xml orpg/templates/nodes/StarWars_d20character.xml orpg/templates/nodes/Userguide098.xml orpg/templates/nodes/Userguide13.xml orpg/templates/nodes/adnd_2e_char_sheet.xml orpg/templates/nodes/alias.xml orpg/templates/nodes/browser.xml orpg/templates/nodes/d20character.xml orpg/templates/nodes/d20sites.xml orpg/templates/nodes/d20srd.xml orpg/templates/nodes/d20wizards.xml orpg/templates/nodes/default_map.xml orpg/templates/nodes/die_macro.xml orpg/templates/nodes/die_roller_notes.xml orpg/templates/nodes/dnd3.5.xml orpg/templates/nodes/dnd3e.xml orpg/templates/nodes/encounter.xml orpg/templates/nodes/form.xml orpg/templates/nodes/grid.xml orpg/templates/nodes/group.xml orpg/templates/nodes/image.xml orpg/templates/nodes/link.xml orpg/templates/nodes/listbox.xml orpg/templates/nodes/macro.xml orpg/templates/nodes/minlib.xml orpg/templates/nodes/openrpg_links.xml orpg/templates/nodes/split.xml orpg/templates/nodes/tabber.xml orpg/templates/nodes/text.xml orpg/templates/nodes/textctrl.xml orpg/templates/nodes/u_idiots_guide_to_openrpg.xml orpg/templates/nodes/urloader.xml orpg/templates/nodes/wizards.xml orpg/templates/templates.xml orpg/tools/ButtonPanel.py orpg/tools/FlatNotebook.py orpg/tools/NotebookCtrl.py orpg/tools/PyAUI.py orpg/tools/__init__.py orpg/tools/aliaslib.py orpg/tools/inputValidator.py orpg/tools/metamenus.py orpg/tools/orpg_log.py orpg/tools/orpg_settings.py orpg/tools/orpg_sound.py orpg/tools/passtool.py orpg/tools/pluginui.py orpg/tools/predTextCtrl.py orpg/tools/pubsub.py orpg/tools/rgbhex.py orpg/tools/scriptkit.py orpg/tools/server_probe.py orpg/tools/toolBars.py orpg/tools/validate.py orpg/xmltramp.py platform.py plugins/__init__.py plugins/cherrypy/__init__.py plugins/cherrypy/_cpconfig.py plugins/cherrypy/_cpdefaults.py plugins/cherrypy/_cphttpserver.py plugins/cherrypy/_cphttptools.py plugins/cherrypy/_cpserver.py plugins/cherrypy/_cpthreadinglocal.py plugins/cherrypy/_cputil.py plugins/cherrypy/cperror.py plugins/cherrypy/cpg.py plugins/cherrypy/lib/__init__.py plugins/cherrypy/lib/aspect.py plugins/cherrypy/lib/cptools.py plugins/cherrypy/lib/csauthenticate.py plugins/cherrypy/lib/defaultformmask.py plugins/cherrypy/lib/filter/__init__.py plugins/cherrypy/lib/filter/basefilter.py plugins/cherrypy/lib/filter/baseurlfilter.py plugins/cherrypy/lib/filter/cachefilter.py plugins/cherrypy/lib/filter/decodingfilter.py plugins/cherrypy/lib/filter/encodingfilter.py plugins/cherrypy/lib/filter/gzipfilter.py plugins/cherrypy/lib/filter/logdebuginfofilter.py plugins/cherrypy/lib/filter/tidyfilter.py plugins/cherrypy/lib/filter/virtualhostfilter.py plugins/cherrypy/lib/filter/xmlrpcfilter.py plugins/cherrypy/lib/form.py plugins/cherrypy/lib/htmltools.py plugins/cherrypy/lib/httptools.py plugins/cherrypy/wsgiapp.py plugins/heya.wav plugins/images/1up.gif plugins/images/chocobo.gif plugins/images/darkside.gif plugins/images/fairy.gif plugins/images/flyingspaghetti.gif plugins/images/gnome.gif plugins/images/hood.gif plugins/images/icon_arrow.gif plugins/images/icon_biggrin.gif plugins/images/icon_confused.gif plugins/images/icon_cool.gif plugins/images/icon_cry.gif plugins/images/icon_e_biggrin.gif plugins/images/icon_e_confused.gif plugins/images/icon_e_geek.gif plugins/images/icon_e_sad.gif plugins/images/icon_e_surprised.gif plugins/images/icon_e_ugeek.gif plugins/images/icon_e_wink.gif plugins/images/icon_eek.gif plugins/images/icon_evil.gif plugins/images/icon_exclaim.gif plugins/images/icon_frown.gif plugins/images/icon_idea.gif plugins/images/icon_lol.gif plugins/images/icon_mad.gif plugins/images/icon_mrgreen.gif plugins/images/icon_neutral.gif plugins/images/icon_question.gif plugins/images/icon_razz.gif plugins/images/icon_redface.gif plugins/images/icon_rolleyes.gif plugins/images/icon_sad.gif plugins/images/icon_smile.gif plugins/images/icon_smile2.gif plugins/images/icon_surprised.gif plugins/images/icon_twisted.gif plugins/images/icon_wink.gif plugins/images/link.gif plugins/images/medusa.gif plugins/images/mimic.gif plugins/images/mummy.gif plugins/images/ogre.gif plugins/images/ros.gif plugins/images/rupee.gif plugins/images/samurai.gif plugins/images/skeleton.gif plugins/images/skull.gif plugins/images/smiley0.gif plugins/images/smiley1.gif plugins/images/smiley12.gif plugins/images/smiley13.gif plugins/images/smiley14.gif plugins/images/smiley2.gif plugins/images/smiley3.gif plugins/images/smiley4.gif plugins/images/smiley5.gif plugins/images/smiley6.gif plugins/images/smiley7.gif plugins/images/smiley9.gif plugins/images/zombie.gif plugins/inittool.xml plugins/inittool2.xml plugins/inittool2_player.xml plugins/quotebox.xml plugins/server/__init__.py plugins/server/base_plugin.py plugins/server/examplePlugin.py plugins/xxblank.py plugins/xxcac.py plugins/xxchatnotify.py plugins/xxcherrypy.py plugins/xxfontchng.py plugins/xxgsc.py plugins/xxgvm.py plugins/xxheroinit.py plugins/xxhiddendice.py plugins/xxinit.py plugins/xxinit2.py plugins/xxnamesound.py plugins/xxnote.py plugins/xxooc.py plugins/xxquotebox.py plugins/xxsimpleinit.py plugins/xxsmiley.py plugins/xxspell.py plugins/xxsrdlinker.py plugins/xxstatus.py plugins/xxurl2link.py pyver.py readme.txt rollback.py start_developer.py start_noupdate.py start_release.py start_server.py start_server_gui.py system_check.py
diffstat 464 files changed, 105685 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/SWd20/SWd20classes.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,47 @@
+<classes>
+  <class level="1" name="Big-Game Hunter" vd="d10"/>
+  <class level="1" name="Blockade Runner" vd="d6"/>
+  <class level="1" name="Bounty Hunter" vd="d10"/>
+  <class level="1" name="Commoner" vd="d0"/>
+  <class level="1" name="Crimelord" vd="d6"/>
+  <class level="1" name="Dark Force Witch" vd="d8"/>
+  <class level="1" name="Dark Side Devotee" vd="d8"/>
+  <class level="1" name="Dark Side Marauder" vd="d10"/>
+  <class level="1" name="Deep Space Pilot" vd="d6"/>
+  <class level="1" name="Diplomat" vd="d0"/>
+  <class level="1" name="Elite Trooper" vd="d10"/>
+  <class level="1" name="Emperor's Hand" vd="d8"/>
+  <class level="1" name="Expert" vd="d0"/>
+  <class level="1" name="First-Contact Specialist" vd="d6"/>
+  <class level="1" name="Force Adept" vd="d8"/>
+  <class level="1" name="Fringer" vd="d8"/>
+  <class level="1" name="Gand Findsman" vd="d8"/>
+  <class level="1" name="Imperial Inquisitor" vd="d10"/>
+  <class level="1" name="Jedi Ace" vd="d8"/>
+  <class level="1" name="Jedi Consular" vd="d8"/>
+  <class level="1" name="Jedi Guardian" vd="d10"/>
+  <class level="1" name="Jedi Investigator" vd="d8"/>
+  <class level="1" name="Jedi Master" vd="d8"/>
+  <class level="1" name="Jedi Weapon Master" vd="d10"/>
+  <class level="1" name="Martial Artist" vd="d10"/>
+  <class level="1" name="Master Gunner" vd="d6"/>
+  <class level="1" name="Naval Officer" vd="d6"/>
+  <class level="1" name="Noble" vd="d6"/>
+  <class level="1" name="Noghri Bodyguard" vd="d8"/>
+  <class level="1" name="Officer" vd="d8"/>
+  <class level="1" name="Privateer" vd="d10"/>
+  <class level="1" name="Scoundrel" vd="d6"/>
+  <class level="1" name="Scout" vd="d8"/>
+  <class level="1" name="Sector Ranger" vd="d8"/>
+  <class level="1" name="Sharpshooter" vd="d6"/>
+  <class level="1" name="Sith Acolyte" vd="d8"/>
+  <class level="1" name="Sith Lord" vd="d10"/>
+  <class level="1" name="Sith Warrior" vd="d10"/>
+  <class level="1" name="Slicer" vd="d6"/>
+  <class level="1" name="Soldier" vd="d10"/>
+  <class level="1" name="Starfighter Ace" vd="d8"/>
+  <class level="1" name="Starship Ace" vd="d8"/>
+  <class level="1" name="Tech Specialist" vd="d6"/>
+  <class level="1" name="Thug" vd="d0"/>
+  <class level="1" name="Vehicle Ace" vd="d8"/>
+</classes>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/SWd20/d20armor.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,46 @@
+<ac>
+<armor name="Blast helmet, vest" cost="500" type="Light" maxdex="5" bonus="2" checkpenalty="-1" weight="3" speed="10" speed6="6" >
+<description >This armour consists of a lightweight helmet and a composite vest that, when worn together, offer limited protection against shrapnel, mele weapons, slugthrowers, and blasters
+</description >
+</armor>
+<armor name="Combat jumpsuit" cost="1500" type="Light" maxdex="4" bonus="3" checkpenalty="-3" weight="8" speed="10" speed6="6" >
+<description >
+</description >
+</armor>
+<armor name="Padded flight suit" cost="800" type="Light" maxdex="4" bonus="2" checkpenalty="-2" weight="5" speed="10" speed6="6" >
+<description >
+</description >
+</armor>
+<armor name="Armoured flight suit" cost="4000" type="Medium" maxdex="3" bonus="4" checkpenalty="-4" weight="20" speed="8" speed6="4" >
+<description >
+</description >
+</armor>
+<armor name="Battle armour, padded" cost="2000" type="Medium" maxdex="3" bonus="4" checkpenalty="-4" weight="13" speed="8" speed6="4" >
+<description >
+</description >
+</armor>
+<armor name="Battle armour, medium" cost="6000" type="Medium" maxdex="2" bonus="5" checkpenalty="-5" weight="16" speed="8" speed6="4" >
+<description >
+</description >
+</armor>
+<armor name="Armoured spacesuit" cost="10000" type="Heavy" maxdex="1" bonus="6" checkpenalty="-6" weight="45" speed="6" speed6="2" >
+<description >
+</description >
+</armor>
+<armor name="Battle armor, heavy" cost="12000" type="Heavy" maxdex="0" bonus="7" checkpenalty="-7" weight="35" speed="6" speed6="2" >
+<description >
+</description >
+</armor>
+<armor name="Corellian powersuit" cost="10000" type="Powered" maxdex="0" bonus="4" checkpenalty="-4" weight="18" speed="8" speed6="4" >
+<description >
+</description >
+</armor>
+<armor name="Stormtrooper armour" cost="8000" type="Powered" maxdex="2" bonus="5" checkpenalty="-2" weight="16" speed="8" speed6="4" >
+<description >
+</description >
+</armor>
+<armor name="Battleframe" cost="12000" type="Powered" maxdex="0" bonus="3" checkpenalty="-8" weight="20" speed="6" speed6="2" >
+<description >
+</description >
+</armor>
+</ac>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/SWd20/d20feats.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,217 @@
+<feats>
+    <feat name='Absorb Energy' type='General' />
+    <feat name='Acrobatic' type='General' />
+    <feat name='Advanced Martial Arts' type='General' />
+    <feat name='Alertness' type='General' />
+    <feat name='Alter' type='General' />
+    <feat name='Ambidexterity' type='General' />
+    <feat name='Animal Affinity' type='General' />
+    <feat name='Armor Proficiency (heavy)' type='General' />
+    <feat name='Armor Proficiency (light)' type='General' />
+    <feat name='Armor Proficiency (medium)' type='General' />
+    <feat name='Armor Proficiency (powered)' type='General' />
+    <feat name='Athletic' type='General' />
+    <feat name='Attuned' type='General' />
+    <feat name='Aware' type='General' />
+    <feat name='Blind-fight' type='General' />
+    <feat name='Burst of Speed' type='General' />
+    <feat name='Cautious' type='General' />
+    <feat name='Cleave' type='General' />
+    <feat name='Combat Expertise' type='General' />
+    <feat name='Combat Reflexes' type='General' />
+    <feat name='Compassion' type='General' />
+    <feat name='Control' type='General' />
+    <feat name='Defensive Martial Arts' type='General' />
+    <feat name='Dissipate Energy' type='General' />
+    <feat name='Dodge' type='General' />
+    <feat name='Drain Force' type='General' />
+    <feat name='Endurance' type='General' />
+    <feat name='Exotic Weapon Proficiency (amphistaff)' type='General' />
+    <feat name='Exotic Weapon Proficiency (atlatl)' type='General' />
+    <feat name='Exotic Weapon Proficiency (bowcaster)' type='General' />
+    <feat name='Exotic Weapon Proficiency (cesta)' type='General' />
+    <feat name='Exotic Weapon Proficiency (double lightsaber)' type='General' />
+    <feat name='Exotic Weapon Proficiency (gaderffii)' type='General' />
+    <feat name='Exotic Weapon Proficiency (lightsaber)' type='General' />
+    <feat name='Exotic Weapon Proficiency (lightwhip)' type='General' />
+    <feat name='Exotic Weapon Proficiency (massassi lanvarok)' type='General' />
+    <feat name='Exotic Weapon Proficiency (plaeryin bol)' type='General' />
+    <feat name='Exotic Weapon Proficiency (quills)' type='General' />
+    <feat name='Exotic Weapon Proficiency (riot gun)' type='General' />
+    <feat name='Exotic Weapon Proficiency (san-ni staff)' type='General' />
+    <feat name='Exotic Weapon Proficiency (short lightsaber)' type='General' />
+    <feat name='Exotic Weapon Proficiency (sith lanvarok)' type='General' />
+    <feat name='Exotic Weapon Proficiency (sith sword)' type='General' />
+    <feat name='Exotic Weapon Proficiency (snare rifle)' type='General' />
+    <feat name='Exotic Weapon Proficiency (stinger)' type='General' />
+    <feat name='Exotic Weapon Proficiency (tsaisi)' type='General' />
+    <feat name='Expert Gunner' type='General' />
+    <feat name='Fame' type='General' />
+    <feat name='Far Shot' type='General' />
+    <feat name='Focus' type='General' />
+    <feat name='Force Dodge' type='General' />
+    <feat name='Force Flight' type='General' />
+    <feat name='Force Mastery' type='General' />
+    <feat name='Force Mind' type='General' />
+    <feat name='Force Pilot' type='General' />
+    <feat name='Force Shot' type='General' />
+    <feat name='Force Speed' type='General' />
+    <feat name='Force Whirlwind' type='General' />
+    <feat name='Force-Sensitive' type='General' />
+    <feat name='Frightful Presence' type='General' />
+    <feat name='Gearhead' type='General' />
+    <feat name='Great Cleave' type='General' />
+    <feat name='Great Fortitude' type='General' />
+    <feat name='Guided Shot' type='General' />
+    <feat name='Gunner' type='General' />
+    <feat name='Hatred' type='General' />
+    <feat name='Headstrong' type='General' />
+    <feat name='Heroic Surge' type='General' />
+    <feat name='High Force Mastery' type='General' />
+    <feat name='Improved Bantha Rush' type='General' />
+    <feat name='Improved Critical' type='General' />
+    <feat name='Improved Disarm' type='General' />
+    <feat name='Improved Force Mind' type='General' />
+    <feat name='Improved Initiative' type='General' />
+    <feat name='Improved Martial Arts' type='General' />
+    <feat name='Improved Trip' type='General' />
+    <feat name='Improved Two-weapon Fighting' type='General' />
+    <feat name='Infamy' type='General' />
+    <feat name='Influence' type='General' />
+    <feat name='Iron Will' type='General' />
+    <feat name='Knight Defense' type='General' />
+    <feat name='Knight Mind' type='General' />
+    <feat name='Knight Speed' type='General' />
+    <feat name='Lightning Reflexes' type='General' />
+    <feat name='Lightsaber Defense' type='General' />
+    <feat name='Link' type='General' />
+    <feat name='Low Profile' type='General' />
+    <feat name='Malevolant' type='General' />
+    <feat name='Maneuver Expertise' type='General' />
+    <feat name='Martial Arts' type='General' />
+    <feat name='Master Defense' type='General' />
+    <feat name='Master Mind' type='General' />
+    <feat name='Master Speed' type='General' />
+    <feat name='Mettle' type='General' />
+    <feat name='Mimic' type='General' />
+    <feat name='Mind Trick' type='General' />
+    <feat name='Mobility' type='General' />
+    <feat name='Multishot' type='General' />
+    <feat name='Nimble' type='General' />
+    <feat name='Persuasive' type='General' />
+    <feat name='Pinpoint Accuracy' type='General' />
+    <feat name='Point Blank Shot' type='General' />
+    <feat name='Power Attack' type='General' />
+    <feat name='Precise Shot' type='General' />
+    <feat name='Psychometry' type='General' />
+    <feat name='Quick Draw' type='General' />
+    <feat name='Quickness' type='General' />
+    <feat name='Rage' type='General' />
+    <feat name='Rapid Gunner' type='General' />
+    <feat name='Rapid Shot' type='General' />
+    <feat name='Rugged' type='General' />
+    <feat name='Run' type='General' />
+    <feat name='Sense' type='General' />
+    <feat name='Shapeshifter' type='General' />
+    <feat name='Sharp-eyed' type='General' />
+    <feat name='Shot On The Run' type='General' />
+    <feat name='Sith Sorcery' type='General' />
+    <feat name='Sith Sword Defense' type='General' />
+    <feat name='Sith Sword Expert Defense' type='General' />
+    <feat name='Sith Sword Mastery' type='General' />
+    <feat name='Skill Emphasis (Affect Mind)' type='General' />
+    <feat name='Skill Emphasis (Alchemy)' type='General' />
+    <feat name='Skill Emphasis (Appraise)' type='General' />
+    <feat name='Skill Emphasis (Astrogate)' type='General' />
+    <feat name='Skill Emphasis (Balance)' type='General' />
+    <feat name='Skill Emphasis (Battlemind)' type='General' />
+    <feat name='Skill Emphasis (Bluff)' type='General' />
+    <feat name='Skill Emphasis (Climb)' type='General' />
+    <feat name='Skill Emphasis (Computer Use)' type='General' />
+    <feat name='Skill Emphasis (Control Mind)' type='General' />
+    <feat name='Skill Emphasis (Craft)' type='General' />
+    <feat name='Skill Emphasis (Demolitions)' type='General' />
+    <feat name='Skill Emphasis (Diplomacy)' type='General' />
+    <feat name='Skill Emphasis (Disable Device)' type='General' />
+    <feat name='Skill Emphasis (Disguise)' type='General' />
+    <feat name='Skill Emphasis (Drain Energy)' type='General' />
+    <feat name='Skill Emphasis (Drain Knowledge)' type='General' />
+    <feat name='Skill Emphasis (Empathy)' type='General' />
+    <feat name='Skill Emphasis (Enhance Ability)' type='General' />
+    <feat name='Skill Emphasis (Enhance Senses)' type='General' />
+    <feat name='Skill Emphasis (Entertain)' type='General' />
+    <feat name='Skill Emphasis (Escape Artist)' type='General' />
+    <feat name='Skill Emphasis (Farseeing)' type='General' />
+    <feat name='Skill Emphasis (Fear)' type='General' />
+    <feat name='Skill Emphasis (Force Defense)' type='General' />
+    <feat name='Skill Emphasis (Force Grip)' type='General' />
+    <feat name='Skill Emphasis (Force Lightning)' type='General' />
+    <feat name='Skill Emphasis (Force Stealth)' type='General' />
+    <feat name='Skill Emphasis (Force Strike)' type='General' />
+    <feat name='Skill Emphasis (Forgery)' type='General' />
+    <feat name='Skill Emphasis (Friendship)' type='General' />
+    <feat name='Skill Emphasis (Gamble)' type='General' />
+    <feat name='Skill Emphasis (Gather Information)' type='General' />
+    <feat name='Skill Emphasis (Handle Animal)' type='General' />
+    <feat name='Skill Emphasis (Heal Another)' type='General' />
+    <feat name='Skill Emphasis (Heal Self)' type='General' />
+    <feat name='Skill Emphasis (Hide)' type='General' />
+    <feat name='Skill Emphasis (Illusion)' type='General' />
+    <feat name='Skill Emphasis (Intimidate)' type='General' />
+    <feat name='Skill Emphasis (Jump)' type='General' />
+    <feat name='Skill Emphasis (Knowledge)' type='General' />
+    <feat name='Skill Emphasis (Listen)' type='General' />
+    <feat name='Skill Emphasis (Move Object)' type='General' />
+    <feat name='Skill Emphasis (Move Silently)' type='General' />
+    <feat name='Skill Emphasis (Pilot)' type='General' />
+    <feat name='Skill Emphasis (Profession)' type='General' />
+    <feat name='Skill Emphasis (Repair)' type='General' />
+    <feat name='Skill Emphasis (Ride)' type='General' />
+    <feat name='Skill Emphasis (Search)' type='General' />
+    <feat name='Skill Emphasis (See Force)' type='General' />
+    <feat name='Skill Emphasis (Sense Motive)' type='General' />
+    <feat name='Skill Emphasis (Sleight of Hand)' type='General' />
+    <feat name='Skill Emphasis (Spot)' type='General' />
+    <feat name='Skill Emphasis (Survival)' type='General' />
+    <feat name='Skill Emphasis (Swim)' type='General' />
+    <feat name='Skill Emphasis (Telepathy)' type='General' />
+    <feat name='Skill Emphasis (Transfer Essence)' type='General' />
+    <feat name='Skill Emphasis (Treat Injury)' type='General' />
+    <feat name='Skill Emphasis (Tumble)' type='General' />
+    <feat name='Spacer' type='General' />
+    <feat name='Spring Attack' type='General' />
+    <feat name='Stamina' type='General' />
+    <feat name='Starship Dodge (space transport)' type='General' />
+    <feat name='Starship Dodge (starfighter)' type='General' />
+    <feat name='Starship Operation (capital ship)' type='General' />
+    <feat name='Starship Operation (space transport)' type='General' />
+    <feat name='Starship Operation (starfighter)' type='General' />
+    <feat name='Starship Point Blank Shot (capital ship)' type='General' />
+    <feat name='Starship Point Blank Shot (space transport)' type='General' />
+    <feat name='Starship Point Blank Shot (starfighter)' type='General' />
+    <feat name='Steady' type='General' />
+    <feat name='Stealthy' type='General' />
+    <feat name='Summon Storm' type='General' />
+    <feat name='Sunder' type='General' />
+    <feat name='Surgery' type='General' />
+    <feat name='Toughness' type='General' />
+    <feat name='Track' type='General' />
+    <feat name='Trick' type='General' />
+    <feat name='Trustworthy' type='General' />
+    <feat name='Two-weapon Fighting' type='General' />
+    <feat name='Vehicle Dodge' type='General' />
+    <feat name='Weapon Finesse' type='General' />
+    <feat name='Weapon Focus' type='General' />
+    <feat name='Weapons Group Proficiency (blaster pistols)' type='General' />
+    <feat name='Weapons Group Proficiency (blaster rifles)' type='General' />
+    <feat name='Weapons Group Proficiency (heavy weapons)' type='General' />
+    <feat name='Weapons Group Proficiency (primitive weapons)' type='General' />
+    <feat name='Weapons Group Proficiency (simple weapons)' type='General' />
+    <feat name='Weapons Group Proficiency (slug throwers)' type='General' />
+    <feat name='Weapons Group Proficiency (starship weapons)' type='General' />
+    <feat name='Weapons Group Proficiency (vehicle weapons)' type='General' />
+    <feat name='Weapons Group Proficiency (vibro weapons)' type='General' />
+    <feat name='Whirlwind Attack' type='General' />
+    <feat name='Wookiee Brachiation' type='General' />
+    <feat name='Zero-G Training' type='General' />
+</feats>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/SWd20/d20weapons.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,125 @@
+<weapons>
+    <weapon mod="0" name="Amphistaff" cost="5000" category="Exotic Weapon Proficiency (amphistaff)" size="Large" damage="1d6" critical="20" range="0" weight="2" type="Piercing/Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Atlatl" cost="50" category="Exotic Weapon Proficiency (atlatl)" size="Medium" damage="2d4" critical="20" range="0" weight="2" type="Bludgeoning" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Cannon)" cost="3000" category="Weapons Group Proficiency (heavy weapons)" size="Large" damage="4d8" critical="19" range="40" weight="18" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Carbine)" cost="900" category="Weapons Group Proficiency (blaster rifles)" size="Medium" damage="3d8" critical="19" range="20" weight="2" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (E-Web Repeating)" cost="8000" category="Weapons Group Proficiency (heavy weapons)" size="Large" damage="6d8" critical="19" range="80" weight="38" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Heavy Pistol)" cost="750" category="Weapons Group Proficiency (blaster pistols)" size="Medium" damage="3d8" critical="20" range="8" weight="1" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Heavy Repeating)" cost="4000" category="Weapons Group Proficiency (heavy weapons)" size="Large" damage="4d8" critical="19" range="30" weight="12" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Hold-out)" cost="300" category="Weapons Group Proficiency (blaster pistols)" size="Small" damage="3d4" critical="20" range="4" weight="0" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Light Repeating)" cost="2000" category="Weapons Group Proficiency (blaster rifles)" size="Large" damage="3d8" critical="19" range="40" weight="6" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Pistol)" cost="500" category="Weapons Group Proficiency (blaster pistols)" size="Small" damage="3d6" critical="20" range="10" weight="1" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Rifle, Sporting)" cost="800" category="Weapons Group Proficiency (blaster rifles)" size="Medium" damage="3d6" critical="19" range="40" weight="2" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Rifle)" cost="1000" category="Weapons Group Proficiency (blaster rifles)" size="Medium" damage="3d8" critical="19" range="40" weight="4" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Blaster (Sporting)" cost="300" category="Weapons Group Proficiency (blaster pistols)" size="Small" damage="3d4" critical="20" range="8" weight="1" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Bow" cost="300" category="Weapons Group Proficiency (primitive weapons)" size="Medium" damage="1d8" critical="20" range="12" weight="1" type="Piercing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Bowcaster" cost="1500" category="Exotic Weapon Proficiency (bowcaster)" size="Large" damage="3d10" critical="19" range="10" weight="8" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Cesta" cost="100" category="Exotic Weapon Proficiency (cesta)" size="Large" damage="2d4" critical="20" range="0" weight="2" type="Bludgeoning" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Club/Baton" cost="15" category="Weapons Group Proficiency (simple weapons)" size="Medium" damage="1d6" critical="20" range="0" weight="2" type="Bludgeoning" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Combat Gloves" cost="200" category="Weapons Group Proficiency (simple weapons)" size="Medium" damage="+2" critical="0" range="0" weight="1" type="Bludgeoning" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Force Pike" cost="500" category="Weapons Group Proficiency (vibro weapons)" size="Large" damage="2d8" critical="20" range="0" weight="2" type="Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Gaderffii" cost="50" category="Exotic Weapon Proficiency (gaderffii)" size="Large" damage="1d8" damage2= "1d6" critical="20" range="0" weight="2" type="Piercing/Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Grenade (Frag)" cost="200" category="Weapons Group Proficiency (simple weapons)" size="Tiny" damage="4d6+1" critical="0" range="4" weight="0" type="Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Grenade (Stun)" cost="250" category="Weapons Group Proficiency (simple weapons)" size="Tiny" damage="-" critical="0" range="4" weight="0" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Ion Gun (Pistol)" cost="250" category="Weapons Group Proficiency (blaster pistols)" size="Small" damage="3d6" critical="20" range="8" weight="1" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Ion Gun (Rifle)" cost="800" category="Weapons Group Proficiency (blaster rifles)" size="Medium" damage="3d8" critical="19" range="30" weight="2" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Knife" cost="25" category="Weapons Group Proficiency (simple weapons)" size="Small" damage="1d4" critical="20" range="0" weight="1" type="Piercing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Lightsaber" cost="3000" category="Exotic Weapon Proficiency (lightsaber)" size="Medium" damage="2d8" critical="19" range="0" weight="1" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Lightsaber (Double-bladed)" cost="7000" category="Exotic Weapon Proficiency (double lightsaber)" size="Medium" damage="2d8" damage2="2d8" critical="19" range="0" weight="2" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Net" cost="25" category="Weapons Group Proficiency (primitive weapons)" size="Medium" damage="-" critical="0" range="2" weight="4" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Quarterstaff" cost="65" category="Weapons Group Proficiency (simple weapons)" size="Large" damage="1d6" damage2 = "1d6" critical="20" range="0" weight="1" type="Bludgeoning" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Razorbug" cost="0" category="Weapons Group Proficiency (simple weapons)" size="Small" damage="2d6+2" critical="20" range="20" weight="1" type="Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Sling" cost="35" category="Weapons Group Proficiency (primitive weapons)" size="Small" damage="1d4" critical="20" range="6" weight="0" type="Bludgeoning" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Slugthrower (Pistol)" cost="275" category="Weapons Group Proficiency (slug throwers)" size="Small" damage="2d6" critical="20" range="10" weight="1" type="Piercing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Slugthrower (Rifle)" cost="300" category="Weapons Group Proficiency (slug throwers)" size="Medium" damage="2d8" critical="20" range="20" weight="4" type="Piercing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Spear" cost="60" category="Weapons Group Proficiency (primitive weapons)" size="Large" damage="1d8" critical="20" range="0" weight="1" type="Piercing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Stun Baton" cost="500" category="Weapons Group Proficiency (simple weapons)" size="Medium" damage="-" critical="20" range="0" weight="1" type="Bludgeoning" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Thermal Detonator" cost="200" category="Weapons Group Proficiency (simple weapons)" size="Tiny" damage="8d6+6" critical="20" range="4" weight="0" type="Energy" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Thud Bug" cost="0" category="Weapons Group Proficiency (simple weapons)" size="Small" damage="2d6" critical="20" range="20" weight="1" type="Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Tsaisi" cost="7500" category="Exotic Weapon Proficiency (tsaisi)" size="Medium" damage="1d6" critical="20" range="0" weight="1" type="Piercing/Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Vibro-Ax" cost="500" category="Weapons Group Proficiency (vibro weapons)" size="Large" damage="2d10" critical="20" range="0" weight="11" type="Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Vibroblade" cost="250" category="Weapons Group Proficiency (vibro weapons)" size="Medium" damage="2d6" critical="20" range="0" weight="2" type="Slashing" >
+    <description ></description >
+    </weapon>
+    <weapon mod="0" name="Vibrodagger" cost="200" category="Weapons Group Proficiency (vibro weapons)" size="Small" damage="2d4" critical="20" range="0" weight="1" type="Slashing" >
+    <description ></description >
+    </weapon>
+</weapons>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/d20/d20armor.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,75 @@
+<ac>
+<armor  name="Banded mail" cost="250" type="Heavy" maxdex="1" bonus="6" spellfailure="35" checkpenalty="-6" weight="35" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of overlapping strips of metal sewn to a backing of leather and chainmail. The strips cover vulnerable areas, while the chain and leather protect the joints and provide freedom of movement. Straps and buckles distribute the weight evenly. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Breastplate" cost="200" type="Medium" maxdex="3" bonus="5" spellfailure="25" checkpenalty="-4" weight="30" speed="20" speed20="15" speed30="20" >
+<description >A breastplate covers the front and back. It comes with a helmet and matching greaves (plates to cover the lower legs). A light suit or skirt of studded leather beneath the breastplate protects limbs without restricting
+movement much.
+</description >
+</armor>
+<armor  name="Buckler" cost="15" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="5" speed="30" speed20="20" speed30="30" >
+<description >This small metal shield is strapped to the forearm, allowing it to be worn and still use the hand. A bow or crossbow can be used without penalty. An off-hand weapon can be used, but a -1 penalty on attack rolls is imposed because of the extra weight on your arm. This penalty stacks with those for fighting with the off hand and, if appropriate, for fighting with two weapons. In any case, if a weapon is used in the off-hand, the character doesn't get the buckler's AC bonus for the rest of the round.
+</description >
+</armor>
+<armor  name="Chainmail" cost="150" type="Medium" maxdex="2" bonus="5" spellfailure="30" checkpenalty="-5" weight="40" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of interlocking metal rings. It includes a layer of quilted fabric underneath it to prevent chafing and to cushion the impact of blows. Several layers of mail are hung over vital areas. Most of the armor's weight hangs from the shoulders, making chainmail uncomfortable to wear for long periods of time. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Chainshirt" cost="100" type="Light" maxdex="4" bonus="4" spellfailure="20" checkpenalty="-2" weight="25" speed="30" speed20="20" speed30="30" >
+<description >A shirt of chainmail protects the torso while leaving the limbs free and mobile. A layer of quilted fabric underneath it prevents chafing and cushions the impact of blows. It comes with a steel cap.
+</description >
+</armor>
+<armor  name="Full Plate" cost="1500" type="Heavy" maxdex="1" bonus="8" spellfailure="35" checkpenalty="-6" weight="50" speed="20" speed20="15" speed30="20" >
+<description >This armor consists of shaped and fitted metal plates riveted and interlocked to cover the entire body. It includes gauntlets, heavy leather boots, and a visored helmet.
+</description >
+</armor>
+<armor  name="Half-Plate" cost="600" type="Heavy" maxdex="0" bonus="7" spellfailure="40" checkpenalty="-7" weight="50" speed="20" speed20="15" speed30="20" >
+<description >This armor is a combination of chainmail with metal plates (breastplate, epaulettes, elbow guards, gauntlets, tasses, and greaves) covering vital areas. Buckles and straps hold the whole suit together and distribute the weight, but the armor still hangs more loosely than full plate. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Hide" cost="15" type="Medium" maxdex="4" bonus="3" spellfailure="20" checkpenalty="-3" weight="25" speed="20" speed20="15" speed30="20" >
+<description >This armor is prepared from multiple layers of leather and animal hides. It is stiff and hard to move in.
+</description >
+</armor>
+<armor  name="Large SteelShield" cost="20" type="Shield" maxdex="100" bonus="2" spellfailure="15" checkpenalty="-2" weight="15" speed="30" speed20="20" speed30="30" >
+<description >A large shield is too heavy to use the shield hand for anything else.
+</description >
+</armor>
+<armor  name="Large Wooden Shield" cost="7" type="Shield" maxdex="100" bonus="2" spellfailure="15" checkpenalty="-2" weight="10" speed="30" speed20="20" speed30="30" >
+<description >A large shield is too heavy to use the shield hand for anything else.
+</description >
+</armor>
+<armor  name="Leather" cost="10" type="Light" maxdex="6" bonus="2" spellfailure="10" checkpenalty="0" weight="10" speed="30" speed20="20" speed30="30" >
+<description >The breastplate and shoulder protectors of this armor are made of leather that has been stiffened by boiling in oil. The rest of the armor is softer and more flexible leather.
+</description >
+</armor>
+<armor  name="Padded" cost="5" type="Light" maxdex="8" bonus="1" spellfailure="5" checkpenalty="0" weight="10" speed="30" speed20="20" speed30="30" >
+<description >Padded armor features quilted layers of cloth and batting.
+</description >
+</armor>
+<armor  name="Samll Steel Shield" cost="9" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="6" speed="30" speed20="20" speed30="30" >
+<description >A small shield's light weight lets a character carry other items in that hand (although the character cannot use weapons).
+</description >
+</armor>
+<armor  name="Scale Mail" cost="50" type="Medium" maxdex="3" bonus="4" spellfailure="25" checkpenalty="-4" weight="30" speed="20" speed20="15" speed30="20" >
+<description >This is a coat and leggings (and perhaps a separate skirt) of leather covered with overlapping pieces of metal, much like the scales of a fish. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Small Wooden Shield" cost="300" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="5" speed="30" speed20="20" speed30="30" >
+<description >A small shield's light weight lets a character carry other items in that hand (although the character cannot use weapons).
+</description >
+</armor>
+<armor  name="Splint mail" cost="200" type="Heavy" maxdex="0" bonus="6" spellfailure="40" checkpenalty="-7" weight="45" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of narrow vertical strips of metal riveted to a backing of leather that is worn over cloth padding. Flexible chainmail protects the joints. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Studded Leather" cost="25" type="Light" maxdex="5" bonus="3" spellfailure="15" checkpenalty="-1" weight="20" speed="30" speed20="20" speed30="30" >
+<description >This armor is made from tough but flexible leather (not hardened leather as with normal leather armor) reinforced with close-set metal rivets.
+</description >
+</armor>
+<armor  name="Tower Shield" cost="30" type="Shield" maxdex="100" bonus="0" spellfailure="50" checkpenalty="-10" weight="45" speed="30" speed20="20" speed30="30" >
+<description >This massive wooden shield is nearly as tall as the wielder. Basically, it is a portable wall meant to provide cover. It can provide up to total cover, depending on how far a character comes out from behind it. A tower shield, however, does not provide cover against targeted spells; a spellcaster can cast a spell on a character by targeting the shield. A tower shield cannot be used for the shield bash action.
+</description >
+</armor>
+</ac>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/d20/d20classes.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,608 @@
+<classes>
+<class level="1" name="Arcane Archer" hd="d8" >
+<requirements>Race: Elf or half-elf.
+Base Attack Bonus: +6.
+Feats: Weapon Focus (any bow other than a crossbow), Point Blank Shot, Precise Shot.
+Spellcasting: Ability to cast 1st-level arcane spells.</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>An arcane archer is proficient with all simple and martial weapons, light armor, medium armor, and shields.</wa_proficiency>
+<features>Enchant Arrow: At 1st level, every nonmagical arrow an arcane archer nocks and lets fly becomes enchanted, gaining a +1 enhancement bonus. An archer's magic arrows only function for her. For every two levels of arcane archer the character advances past 1st level in the prestige class, the magic arrows she creates gain +1 greater potency.
+
+Imbue Arrow: At 2nd level, an arcane archer gains this spell-like ability, allowing her to place an area spell upon an arrow. When the arrow is fired, the spell's area is centered upon where the arrow lands, even if the spell could normally be centered only on the caster. This ability allows the archer to use the bow's range rather than the spell's range. It takes a standard action to cast the spell and fire the arrow. The arrow must be fired in the round the spell is cast, or the spell is wasted.
+
+Seeker Arrow: At 4th level, the arcane archer can launch an arrow once per day at a target known to her within range, and the arrow travels to the target, even around corners. Only an unavoidable obstacle or the end of the arrow's range prevents the arrow's flight. This ability negates cover and concealment modifiers, but otherwise the attack is rolled normally. This is a spell-like ability. (Shooting the arrow is part of the action.)
+
+Phase Arrow: At 6th level, the arcane archer can launch an arrow once per day at a target known to her within range, and the arrow travels to the target in a straight path, passing through any nonmagical barrier or wall in its way. (A wall of force, a wall of fire, or the like stops the arrow.) This ability negates cover, concealment, and even armor modifiers, but otherwise the attack is rolled normally. This is a spell-like ability. (Shooting the arrow is part of the action.)
+
+Hail of Arrows: In lieu of her regular attacks, once per day the 8th-level arcane archer can fire an arrow at each and every target within range, to a maximum of one target for every arcane archer level she has earned. Each attack uses the archer's primary attack bonus, and each enemy may only be targeted by a single arrow. This is a spell-like ability.
+
+Arrow of Death: At 10th level, the arcane archer can enchant an arrow of death that forces the target, if damaged by the arrow's attack, to make a Fortitude save (DC 20) or be slain immediately. It takes one day to create an arrow of death, and the arrow only functions for the arcane archer who created it. The enchantment lasts no longer than one year, and the archer can only have one such arrow in existence at a time.</features>
+</class>
+<class level="1" name="Assassin" hd="d6" >
+<requirements>Move Silently: 8 ranks.
+Hide: 8 ranks.
+Disguise: 4 ranks.
+Special: In addition, he must kill someone for no other reason than to join the assassins.</requirements>
+<alignment>Evil</alignment>
+<wa_proficiency>Assassins are proficient with the crossbow (hand, light, or heavy), dagger (any type), dart, rapier, sap, shortbow (normal and composite), and short sword. Assassins are proficient with light armor but not with shields.</wa_proficiency>
+<features>Sneak Attack: Any time the assassin's target would be denied her Dexterity bonus to AC (whether she actually has a Dexterity bonus or not), the assassin's attack deals +1d6 points of damage. This extra damage increases by +1d6 points every other level (+2d6 at 3rd level, +3d6 at 5th level, and so on). Should the assassin score a critical hit with a sneak attack, this extra damage is not multiplied.
+
+It takes precision and penetration to hit a vital spot, so ranged attacks can only count as sneak attacks if the target is 30 feet away or less.
+
+With a sap or an unarmed strike, the assassin can make a sneak attack that deals subdual damage instead of normal damage. He cannot use a weapon that deals normal damage to deal subdual damage in a sneak attack, not even with the usual -4 penalty, because he must make optimal use of his weapon in order to execute the sneak attack.
+
+An assassin can only sneak attack living creatures with discernible anatomies-undead, constructs, oozes, plants, and incorporeal creatures lack vital areas to attack. Additionally, any creature immune to critical hits is similarly immune to sneak attacks. Also, the assassin must also be able to see the target well enough to pick out a vital spot and must be able to reach a vital spot. The assassin cannot sneak attack while striking at a creature with concealment or by striking the limbs of a creature whose vitals are beyond reach.
+
+If an assassin gets a sneak attack bonus from another source (such as rogue levels), the bonuses to damage stack.
+
+Death Attack: If the assassin studies his victim for 3 rounds and then makes a sneak attack with a melee weapon that successfully deals damage, the sneak attack has the additional effect of possibly either paralyzing or killing the target (assassin's choice). While studying the victim, the assassin can undertake other actions so long as his attention stays focused on the target and the target does not detect the assassin or recognize the assassin as an enemy. If the victim of such an attack fails her Fortitude saving throw (DC 10 + the assassin's class level + the assassin's Intelligence modifier) against the kill effect, she dies. If the saving throw fails against the paralysis effect, the victim's mind and body become enervated, rendering her completely helpless and unable to act for 1d6 rounds plus 1 round per level of the assassin. If the victim's saving throw succeeds, the attack is just a normal sneak attack. Once the assassin has completed the 3 rounds of study, he must make the death attack within the next 3 rounds. If a death attack is attempted and fails (the victim makes her save) or if the assassin does not launch the attack within 3 rounds of completing the study, 3 new rounds of study are required before he can attempt another death attack.
+
+Poison Use: Assassins are trained in the use of poison and never risk accidentally poisoning themselves when applying poison to a blade.
+
+Spells: Beginning at 1st level, an assassin gains the ability to cast a small number of arcane spells. To cast a spell, the assassin must have an Intelligence score of at least 10 + the spell's level, so an assassin with an Intelligence of 10 or lower cannot cast these spells. Assassin bonus spells are based on Intelligence, and saving throws against these spells have a DC of 10 + spell level + the assassin's Intelligence modifier (if any). When the assassin gets 0 spells of a given level, such as 0 1st-level spells at 1st level, the assassin gets only bonus spells. An assassin without a bonus spell for that level cannot yet cast a spell of that level. The assassin's spell list appears below. An assassin prepares and casts spells just as a wizard does.
+
+Saving Throw Bonus vs. Poison: Assassins train with poisons of all types and slowly grow more and more resistant to their effects. This is reflected by a natural saving throw bonus to all poisons gained at 2nd level that increases by +1 for every two levels the assassin gains (+1 at 2nd level, +2 at 4th level, +3 at 6th level, and so on).
+
+Uncanny Dodge: Starting at 2nd level, the assassin gains the extraordinary ability to react to danger before his senses would normally allow him to even be aware of it. At 2nd level and above, he retains his Dexterity bonus to AC (if any) regardless of being caught flat-footed or struck by an invisible attacker. (He still loses his Dexterity bonus to AC if immobilized.)
+
+At 5th level, the assassin can no longer be flanked, since he can react to opponents on opposite sides of him as easily as he can react to a single attacker. This defense denies rogues the ability to use flank attacks to sneak attack the assassin. The exception to this defense is that a rogue at least four levels higher than the assassin can flank him (and thus sneak attack him).
+
+At 10th level, the assassin gains an intuitive sense that alerts him to danger from traps, giving him a +1 bonus to Reflex saves made to avoid traps.
+
+If the assassin has another class that grants the uncanny dodge ability, add together all the class levels of the classes that grant the ability and determine the character's uncanny dodge ability on that basis.
+
+Assassins choose their spells from the following list:
+
+1st level-change self, detect poison, ghost sound, obscuring mist, spider climb.
+2nd level-alter self, darkness, pass without trace, undetectable alignment.
+3rd level-deeper darkness, invisibility, misdirection, nondetection.
+4th level-dimension door, freedom of movement, improved invisibility, poison.</features>
+</class>
+<class level="1" name="Barbarian" hd="d12" >
+<requirements>None</requirements>
+<alignment>Nonlawful</alignment>
+<wa_proficiency>A barbarian is proficient with all simple and martial weapons, light armor, medium armor, and shields.</wa_proficiency>
+<features>Barbarian Rage: Barbarian temporarily gains +4 to Strength, +4 to Constitution, and a +2 morale bonus on Will saves, but suffers a -2 penalty to AC.
+
+The increase in Constitution increases the barbarian's hit points by 2 points per level, but these hit points go away at the end of the rage when the Constitution score drops back to normal. While raging, a barbarian cannot use skills or abilities that require patience and concentration. (The only class skills he can't use while raging are Craft, Handle Animal, and Intuit Direction.) He can use any feat he might have except for Expertise, item creation feats, metamagic feats, and Skill Focus (if it's tied to a skill that requires patience or concentration).
+
+A fit of rage lasts for a number of rounds equal to 3 + the character's (newly improved) Constitution modifier. The barbarian may prematurely end the rage voluntarily. At the end of the rage, the barbarian is fatigued (-2 to Strength, -2 to Dexterity, can't charge or run) for the duration of that encounter (unless the barbarian is 20th level, when this limitation no longer applies). The barbarian can only fly into a rage once per encounter, and only a certain number of times per day (determined by level). Entering a rage takes no time itself, but the barbarian can only do it during his action.
+
+Starting at 15th level, the barbarian's rage bonuses become +6 to Strength, +6 to Constitution, and a +3 morale bonus to Will saves. (The AC penalty remains at -2.)
+
+Fast Movement: The barbarian has a speed faster than the norm for his race by +10 feet when wearing no armor, light armor, or medium armor (and not carrying a heavy load).
+
+Uncanny Dodge: At 2nd level and above, the barbarian retains his Dexterity bonus to AC (if any) if caught flat-footed or struck by an invisible attacker.
+
+At 5th level, the barbarian can no longer be flanked. The exception to this defense is that a rogue at least four levels higher than the barbarian can still flank.
+
+At 10th level, the barbarian gains a +1 bonus to Reflex saves made to avoid traps and a +1 dodge bonus to AC against attacks by traps. At 13th level, these bonuses rise to +2. At 16th, they rise to +3, and at 19th they rise to +4.
+
+Damage Reduction: Starting at 11th level, the barbarian gains the extraordinary ability to shrug off some amount of injury from each blow or attack. Subtract 1 from the damage the barbarian takes each time the barbarian is dealt damage. At 14th level, this damage reduction rises to 2. At 17th, it rises to 3. At 20th, it rises to 4. Damage reduction can reduce damage to 0 but not below 0.
+
+Illiteracy: Barbarians are the only characters that do not automatically know how to read and write. A barbarian must spend 2 skill points to gain the ability to read and write any language the barbarian is able to speak.
+
+Ex-Barbarians: A barbarian who becomes lawful loses the ability to rage and cannot gain more levels as a barbarian. The barbarian retains all the other benefits of the class.</features>
+</class>
+<class level="1" name="Bard" hd="d6" >
+<requirements>None</requirements>
+<alignment>Nonlawful</alignment>
+<wa_proficiency>A bard is proficient with all simple weapons. Additionally, the bard is proficient with one of the following weapons: longbow, composite longbow, longsword, rapier, sap, short composite bow, short sword, shortbow, or whip. Bards are proficient with light armor, medium armor, and shields.</wa_proficiency>
+<features>Spells: A bard casts arcane spells. The bard casts these spells without needing to memorize them beforehand or keep a spellbook. Bards receive bonus spells for high Charisma, and to cast a spell a bard must have a Charisma score at least equal to 10 + the level of the spell. The Difficulty Class for a saving throw against a bard's spell is 10 + the spell's level + the bard's Charisma modifier.
+
+Bardic Music: Once per day per level, a bard can use song or poetics to produce magical effects on those around him or her. While these abilities fall under the category of bardic music, they can include reciting poetry, chanting, singing lyrical songs, singing melodies, whistling, playing an instrument, or playing an instrument in combination with some spoken performance. As with casting a spell with a verbal component, a deaf bard suffers a 20% chance to fail with bardic music. If the bard fails, the attempt still counts against the daily limit.
+
+The Bardic Music effects are:
+
+* Inspire Courage: A bard with 3 or more ranks in Perform can to inspire courage in his or her allies. To be affected, an ally must hear the bard sing for a full round. The effect lasts as long as the bard sings and for 5 rounds after the bard stops singing (or 5 rounds after the ally can no longer hear the bard). While singing, the bard can fight but cannot cast spells, activate magic items by spell completion (such as scrolls), or activate magic items by magic word (such as wands). Affected allies receive a +2 morale bonus to saving throws against charm and fear effects and a +1 morale bonus to attack and weapon damage rolls. Inspire courage is a supernatural, mind-affecting ability.
+
+* Countersong: A bard with 3 or more ranks in Perform can counter magical effects that depend on sound (but not spells that simply have verbal components). As with inspire courage, a bard may sing, play, or recite a countersong while taking other mundane actions, but not magical actions. Each round of the countersong, the bard makes a Perform check. Any creature within 30 feet of the bard (including the bard) who is affected by a sonic or language-dependent magical attack may use the bard's Perform check result in place of his saving throw if, after rolling the saving throw, the Perform check result proves to be better. The bard may keep up the countersong for 10 rounds. Countersong is a supernatural ability.
+
+* Fascinate: A bard with 3 or more ranks in Perform can cause a single creature to become fascinated with him. The creature to be fascinated must be able to see and hear the bard and must be within 90 feet. The bard must also see the creature. The creature must be able to pay attention to the bard. The distraction of a nearby combat or other dangers prevents the ability from working. The bard makes a Perform check, and the target can negate the effect with a Will saving throw equal to or greater than the bard's check result. If the saving throw succeeds, the bard cannot attempt to fascinate that creature again for 24 hours. If the saving throw fails, the creature sits quietly and listens to the song for up to 1 round per level of the bard. While fascinated, the target's Spot and Listen checks suffer a -4 penalty. Any potential threat (such as an ally of the bard moving behind the fascinated creature) allows the fascinated creature a second saving throw against a new Perform check result. Any obvious threat, such as casting a spell, drawing a sword, or aiming, automatically breaks the effect.
+
+While fascinating (or attempting to fascinate) a creature, the bard must concentrate, as if casting or maintaining a spell. Fascinate is a spell-like, mind- affecting charm ability.
+
+* Inspire Competence: A bard with 6 or more ranks in Perform can help an ally succeed at a task. The ally must be able to see and hear the bard and must be within 30 feet. The bard must also see the creature. The ally gets a +2 competence bonus on his skill checks with a particular skill as long as he or she continues to hear the bard's music. The DM may rule that certain uses of this ability are infeasible. The bard can maintain the effect for 2 minutes (long enough for the ally to take 20). Inspire competence is a supernatural, mind-affecting ability.
+
+* Suggestion: A bard with 9 or more ranks in Perform can make a suggestion (as the spell) to a creature that he has already fascinated (see above). The suggestion doesn't count against the bard's daily limit on bardic music performances (one per day per level), but the fascination does. A Will saving throw (DC 13 + the bard's Charisma modifier) negates the effect. Suggestion is a spell-like, mind-affecting charm ability.
+
+* Inspire Greatness: A bard with 12 or more ranks in Perform can inspire greatness in another creature. For every three levels the bard attains beyond 9th, the bard can inspire greatness in one additional creature. To inspire greatness, the bard must sing and the creature must hear the bard sing for a full round, as with inspire courage. The creature must also be within 30 feet. A creature inspired with greatness gains temporary Hit Dice, attack bonuses, and saving throw bonuses as long as he or she hears the bard continue to sing and for 5 rounds thereafter. (All these bonuses are competence bonuses.)
+
+The target gains the following boosts:
+* +2 Hit Dice (d10s that grant temporary hit points).
+* +2 competence bonus on attacks.
+* +1 competence bonus on Fortitude saves.
+
+Apply the target's Constitution modifier, if any, to each bonus Hit Die. These extra Hit Dice count as regular Hit Dice for determining effects such as the sleep spell. Inspire greatness is a supernatural, mind-affecting enchantment ability.
+
+Bardic Knowledge: A bard may make a special bardic knowledge check with a bonus equal to his level + his Intelligence modifier to see whether he knows some relevant information about local notable people, legendary items, or noteworthy places. This check will not reveal the powers of a magic item but may give a hint as to its general function. The bard may not take 10 or take 20 on this check; this sort of knowledge is essentially random. The DM will determine the Difficulty Class of the check by referring to the table below. DC      Type of Knowledge
+--      -----------------10      Common, known by at least a substantial minority of the local population.
+20      Uncommon but available, known by only a few people in the area.
+25      Obscure, known by few, hard to come by.
+30      Extremely obscure, known by very few, possibly forgotten by most who once knew it, possibly known only by those who don't understand the significance of the knowledge.
+
+Ex-Bards: A bard who becomes lawful in alignment cannot progress in levels as a bard, though he retains all his bard abilities.</features>
+</class>
+<class level="1" name="Blackguard" hd="d10" >
+<requirements>Base Attack Bonus: +6.
+Knowledge (religion): 2 ranks.
+Hide: 5 ranks.
+Feats: Cleave, Sunder.
+Special: The blackguard must have made peaceful contact with an evil outsider who was summoned by him or someone else to have contracted the taint of true evil.</requirements>
+<alignment>Any Evil</alignment>
+<wa_proficiency>Blackguards are proficient with all simple and martial weapons, with all types of armor, and with shields.</wa_proficiency>
+<features>Detect Good: At will, the blackguard can detect good as a spell-like ability. This ability duplicates the effects of the spell detect good.
+
+Poison Use: Blackguards are skilled in the use of poison and never risk accidentally poisoning themselves when applying poison to a blade.
+
+Dark Blessing: A blackguard applies his Charisma modifier (if positive) as a bonus to all saving throws.
+
+Spells: Beginning at 1st level, a blackguard gains the ability to cast a small number of divine spells. To cast a spell, the blackguard must have a Wisdom score of at least 10 + the spell's level, so a blackguard with a Wisdom of 10 or lower cannot cast these spells. Blackguard bonus spells are based on Wisdom, and saving throws against these spells have a DC of 10 + spell level + the blackguard's Wisdom modifier. When the blackguard gets 0 spells of a given level, such as 0 1st-level spells at 1st level, he gets only bonus spells. (A blackguard without a bonus spell for that level cannot yet cast a spell of that level.) The blackguard's spell list appears below. A blackguard has access to any spell on the list and can freely choose which to prepare, just like a cleric. A blackguard prepares and casts spells just as a cleric does (though the blackguard cannot spontaneously cast cure or inflict spells).
+
+Smite Good: Once a day, a blackguard of 2nd level or higher may attempt to smite good with one normal melee attack. He adds his Charisma modifier (if positive) to his attack roll and deals 1 extra point of damage per class level. For example, a 9th-level blackguard armed with a longsword would deal 1d8+9 points of damage, plus any additional bonuses from high Strength or magical effects that normally apply. If the blackguard accidentally smites a creature that is not good, the smite has no effect but it is still used up for that day. Smite good is a supernatural ability.
+
+Aura of Despair: Beginning at 3rd level, the blackguard radiates a malign aura that causes enemies within 10 feet of him to suffer a -2 morale penalty on all saving throws. Aura of despair is a supernatural ability.
+
+Command Undead: When a blackguard reaches 3rd level, he gains the supernatural ability to command and rebuke undead. He commands undead as would a cleric of two levels lower.
+
+Sneak Attack: If a blackguard can catch an opponent when she is unable to defend herself effectively from his attack, he can strike a vital spot for extra damage. Basically, any time the blackguard's target would be denied her Dexterity bonus to AC (whether she actually has a Dexterity bonus or not), the blackguard's attack deals +1d6 points of damage at 4th level and an additional +1d6 points for every three levels thereafter (+2d6 at 7th level, +3d6 at 10th level, and so on). Should the blackguard score a critical hit with a sneak attack, this extra damage is not multiplied.
+
+Ranged attacks only count as sneak attacks if the target is 30 feet away or less. A blackguard cannot make a sneak attack to deal subdual damage. The blackguard must be able to see the target well enough to pick out a vital spot and must be able to reach a vital spot. He cannot sneak attack while striking at a creature with concealment or by striking the limbs of a creature whose vitals are beyond reach.
+
+A blackguard can only sneak attack living creatures with discernible anatomies. Undead, constructs, oozes, plants, and incorporeal creatures lack vital areas to attack. Additionally, any creature immune to critical hits is not subject to sneak attacks.
+
+If a blackguard gets a sneak attack bonus from another source (such as rogue levels), the bonuses to damage stack.
+
+Blackguards choose their spells from the following list:
+
+1st level-cause fear, cure light wounds, doom, inflict light wounds, magic weapon, summon monster I*.
+2nd level-bull's strength, cure moderate wounds, darkness, death knell, inflict moderate wounds, shatter, summon monster II*.
+3rd level-contagion, cure serious wounds, deeper darkness, inflict serious wounds, protection from elements, summon monster III*.
+4th level-cure critical wounds, freedom of movement, inflict critical wounds, poison, summon monster IV*.
+
+*Evil creatures only.
+
+Fallen Paladins
+
+Blackguards who possess levels of paladin (that is to say, are now ex-paladins) gain extra abilities the more levels of paladin they possess. Those who have tasted the light of goodness and justice and turned away make the foulest villains.</features>
+</class>
+<class level="1" name="Cleric" hd="d8" >
+<requirements>None</requirements>
+<alignment>Varies by deity. A cleric's alignment must be within one step of his deity's, and it may not be neutral unless the deity's alignment is neutral.</alignment>
+<wa_proficiency>Clerics are proficient with all simple weapons. Clerics are proficient with all types of armor (light, medium, and heavy) and with shields.</wa_proficiency>
+<features>Some deities have favored weapons, and clerics consider it a point of pride to wield them. A cleric whose deity's favored weapon is a martial weapon and who chooses War as one of his domains receives the Martial Weapon Proficiency feat related to that weapon for free, as well as the Weapon Focus feat related to that weapon.
+
+Spells: A cleric casts divine spells. A cleric may prepare and cast any spell on the cleric spell list, provided he can cast spells of that level. The Difficulty Class for a saving throw against a cleric's spell is 10 + the spell's level + the cleric's Wisdom modifier.
+
+Each cleric must choose a time at which he must spend an hour each day in quiet contemplation or supplication to regain his daily allotment of spells. Time spent resting has no effect on whether a cleric can prepare spells.
+
+In addition to his standard spells, a cleric gets one domain spell of each spell level, starting at 1st. When a cleric prepares a domain spell, it must come from one of his two domains.
+
+Deity, Domains, and Domain Spells: Choose a deity for your cleric. The cleric's deity influences his alignment, what magic he can perform, his values, and how others see him.
+
+Choose two from among the deity's domains for your cleric's domains. You can only select an alignment domain (such as Good) for your cleric if his alignment matches that domain.
+
+If your cleric is not devoted to a particular deity, you still select two domains to represent his spiritual inclinations and abilities (but the restriction on alignment domains still applies).
+
+Each domain gives your cleric access to a domain spell at each spell level, from 1st on up, as well as a granted power. Your cleric gets the granted powers of all the domains selected. With access to two domain spells at a given spell level, a cleric prepares one or the other each day. If a domain spell is not on the Cleric Spells list, a cleric can only prepare it in his domain slot.
+
+Spontaneous Casting: Good clerics (and neutral clerics of good deities) can channel stored spell energy into healing spells that they haven't prepared ahead of time. The cleric can "lose" a prepared spell in order to cast any cure spell of the same level or lower (a cure spell is any spell with "cure" in its name).
+
+An evil cleric (or a neutral cleric of an evil deity), on the other hand, can't convert prepared spells to cure spells but can convert them to inflict spells (an inflict spell is one with "inflict" in the title).
+
+A cleric who is neither good nor evil and whose deity is neither good nor evil can convert spells either to cure spells or to inflict spells (player's choice), depending on whether the cleric is more proficient at wielding positive or negative energy. Once the player makes this choice, it cannot be reversed. This choice also determines whether the neutral cleric turns or commands undead (see below).
+
+A cleric can't use spontaneous casting to convert domain spells into cure or inflict spells. These spells arise from the particular powers of the cleric's deity, not divine energy in general.
+
+Chaotic, Evil, Good, and Lawful Spells: A cleric can't cast spells of an alignment opposed to his own or to his deity's.
+
+Turn or Rebuke Undead: A good cleric (or a neutral cleric who worships a good deity) has the supernatural ability to turn undead. Evil clerics (and neutral clerics who worship evil deities) can rebuke such creatures. Neutral clerics of neutral deities can do one or the other (player's choice), depending on whether the cleric is more proficient at wielding positive or negative energy. Once the player makes this choice, it cannot be reversed. This choice also determines whether the neutral cleric can cast spontaneous cure or inflict spells (see above).
+
+A cleric may attempt to turn or rebuke undead a number of times per day equal to three plus his Charisma modifier.
+
+Extra Turning: As a feat, a cleric may take Extra Turning. This feat allows the cleric to turn undead four more times per day than normal. A cleric can take this feat multiple times, gaining four extra daily turning attempts each time.
+
+Bonus Languages: A cleric's list of bonus languages includes Celestial, Abyssal, and Infernal, in addition to the bonus languages available to the character because of his race.
+
+Ex-Clerics: A cleric who grossly violates the code of conduct expected by his god (generally acting in ways opposed to the god's alignment or purposes) loses all spells and class features and cannot gain levels as a cleric of that god until he atones.</features>
+</class>
+<class level="1" name="Druid" hd="d8" >
+<requirements>None</requirements>
+<alignment>Neutral good, lawful neutral, neutral, chaotic neutral, or neutral evil.</alignment>
+<wa_proficiency>Druids are proficient with the following weapons: club, dagger, dart, halfspear, longspear, quarterstaff, scimitar, sickle, shortspear, and sling. Their spiritual oaths prohibit them from using weapons other than these. They are proficient with light and medium armors but are prohibited from wearing metal armor (thus, they may wear only padded, leather, or hide armor). They are skilled with shields but must use only wooden ones. </wa_proficiency>
+<features>A druid who wears prohibited armor or wields a prohibited weapon is unable to use any of her magical powers while doing so and for 24 hours thereafter. (Note: A druid can use wooden items that have been altered by the ironwood spell so that they function as though they were steel.)
+
+Spells: A druid casts divine spells. A druid may prepare and cast any spell on the druid spell list provided she can cast spells of that level. She prepares and casts spells the way a cleric does (though she cannot lose a prepared spell to cast a cure spell in its place). To prepare or cast a spell, a druid must have a Wisdom score of at least 10 + the spell's level. The Difficulty Class for a saving throw against a druid's spell is 10 + the spell's level + the druid's Wisdom modifier. Bonus spells for druids are based on Wisdom.
+
+Chaotic, Evil, Good, and Lawful Spells: A druid can't cast spells of an alignment opposed to her own.
+
+Bonus Languages: A druid may substitute Sylvan for one of the bonus languages available to her. In addition, a druid knows the Druidic language. This secret language is known only to druids, and druids are forbidden from teaching it to nondruids. Druidic has its own alphabet.
+
+Nature Sense: A druid can identify plants and animals (their species and special traits) with perfect accuracy. The druid can determine whether water is safe to drink or dangerous.
+
+Animal Companion: A 1st-level druid may begin play with an animal companion. This animal is one that the druid has befriended with the spell animal friendship.
+
+Woodland Stride: Starting at 2nd level, a druid may move through natural thorns, briars, overgrown areas, and similar terrain at his or her normal speed and without suffering damage or other impairment. However, thorns, briars, and overgrown areas that are enchanted or magically manipulated to impede motion still affect the druid.
+
+Trackless Step: Starting at 3rd level, a druid leaves no trail in natural surroundings and cannot be tracked.
+
+Resist Nature's Lure: Starting at 4th level, a druid gains a +4 bonus to saving throws against the spell-like abilities of feys.
+
+Wild Shape: At 5th level, a druid gains the spell-like ability to polymorph self into a Small or Medium-size animal (but not a dire animal) and back again once per day. Unlike the standard use of the spell, however, the druid may only adopt one form. As stated in the spell description, the druid regains hit points as if he or she has rested for a day. The druid does not risk the standard penalty for being disoriented while in the wild shape.
+
+The druid can use this ability more times per day at 6th, 7th, 10th, 14th, and 18th level, as noted. In addition, the druid gains the ability to take the shape of a Large animal at 8th level, a Tiny animal at 11th level, and a Huge animal at 15th level. At 12th level or higher, she can take the form of a dire animal.
+
+At 16th level or higher, the druid may use wild shape to change into a Small, Medium-size, or Large air, earth, fire, or water elemental once per day. The druid gains all the elemental's special abilities. At 18th level, the druid can do this three times per day.
+
+Venom Immunity: At 9th level, a druid gains immunity to all organic poisons, including monster poisons but not mineral poisons or poison gas.
+
+A Thousand Faces: At 13th level, a druid gains the supernatural ability to change his or her appearance at will, as if using the spell alter self.
+
+Timeless Body: After achieving 15th level, a druid no longer suffers ability penalties for aging and cannot be magically aged. Any penalties she may have already suffered, however, remain in place. Bonuses still accrue, and the druid still dies of old age when her time is up.
+
+Ex-Druids: A druid who ceases to revere nature or who changes to a prohibited alignment loses all spells and druidic abilities and cannot gain levels as a druid until she atones.</features>
+</class>
+<class level="1" name="Dwarven Defender" hd="d12" >
+<requirements>Race: Dwarf.
+Base Attack Bonus: +7.
+Feats: Dodge, Endurance, Toughness.</requirements>
+<alignment>Any Lawful</alignment>
+<wa_proficiency>The dwarven defender is proficient with all simple and martial weapons, all types of armor, and shields.</wa_proficiency>
+<features>Defensive Stance: When he needs to, the defender can become a stalwart bastion of defense. In this defensive stance, a defender gains phenomenal strength and durability, but he cannot move from the spot he is defending. He gains the following benefits:+2 Strength, +4 Constitution, +2 resistance bonus on all saves, +4 dodge bonus to AC.
+
+While defending, a defender cannot use skills or abilities that would require him to shift his position, such as Move Silently or Jump. A defensive stance lasts for 3 rounds, plus the character's (newly improved) Constitution modifier. The defender may end the defense voluntarily prior to this limit. At the end of the defense, the defender is winded and suffers a -2 penalty to Strength for the duration of that encounter. The defender can only take his defensive stance a certain number of times per day as determined by his level. Taking the stance takes no time itself, but the defender can only do so during his action.
+
+Defensive Awareness: Starting at 2nd level, the dwarven defender gains the extraordinary ability to react to danger before his senses would normally allow him to even be aware of it. At 2nd level and above, he retains his Dexterity bonus to AC (if any) regardless of being caught flat-footed or struck by an invisible attacker. (He still loses any Dexterity bonus to AC if immobilized.)
+
+At 5th level, the dwarven defender can no longer be flanked, since he can react to opponents on opposite sides of him as easily as he can react to a single attacker. This defense denies rogues the ability to use flank attacks to sneak attack the dwarven defender. The exception to this defense is that a rogue at least 4 levels higher than the dwarven defender can flank him (and thus sneak attack him).
+
+At 10th level, the dwarven defender gains an intuitive sense that alerts him to danger from traps, giving him a +1 bonus to Reflex saves made to avoid traps.
+
+Defensive awareness is cumulative with uncanny dodge. If the dwarven defender has another class that grants the uncanny dodge ability, add together all the class levels of the classes that grant these two abilities and determine the character's defensive awareness ability on that basis.
+
+Damage Reduction: At 6th level, the dwarven defender gains the extraordinary ability to shrug off some amount of injury from each blow or attack. Subtract 3 from the damage the dwarven defender takes each time he is dealt damage. At 10th level, this damage reduction rises to 6. Damage reduction can reduce damage to 0 but not below 0. (That is, the defender cannot actually gain hit points in this manner.)</features>
+</class>
+<class level="1" name="Fighter" hd="d10" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The fighter is proficient in the use of all simple and martial weapons and all armor (heavy, medium, and light) and shields.</wa_proficiency>
+<features>Bonus Feats: At 1st level, the fighter gets a bonus feat in addition to the feat that any 1st-level character gets and the bonus feat granted to humans. The fighter gains an additional bonus feat at 2nd level and every two levels thereafter (4th, 6th, 8th, etc.). These bonus feats must be drawn from the following list: Ambidexterity, Blind-Fight, Combat Reflexes, Dodge (Mobility, Spring Attack), Exotic Weapon Proficiency*, Expertise (Improved Disarm, Improved Trip, Whirlwind Attack), Improved Critical*, Improved Initiative, Improved Unarmed Strike (Deflect Arrows, Stunning Fist), Mounted Combat (Mounted Archery, Trample, Ride-By Attack, Spirited Charge), Point Blank Shot (Far Shot, Precise Shot, Rapid Shot, Shot on the Run), Power Attack (Cleave, Improved Bull Rush, Sunder, Great Cleave), Quick Draw, Two- Weapon Fighting (Improved Two-Weapon Fighting), Weapon Finesse*, Weapon Focus*, Weapon Specialization*.
+
+Some of the bonus feats available to a fighter cannot be acquired until the fighter has gained one or more prerequisite feats; these feats are listed parenthetically after the prerequisite feat. A fighter can select feats marked with an asterisk (*) more than once, but it must be for a different weapon each time. A fighter must still meet all prerequisites for a feat, including ability score and base attack bonus minimums.
+
+Weapon Specialization: On achieving 4th level or higher, as a feat the fighter (and only the fighter) may take Weapon Specialization. Weapon Specialization adds a +2 damage bonus with a chosen weapon. The fighter must have Weapon Focus with that weapon to take Weapon Specialization. If the weapon is a ranged weapon, the damage bonus only applies if the target is within 30 feet, because only at that range can the fighter strike precisely enough to hit more effectively. The fighter may take this feat as a bonus feat or as a regular one.</features>
+</class>
+<class level="1" name="Loremasters" hd="d4" >
+<requirements>Spellcasting: Ability to cast seven different divinations, one of which must be 3rd level or higher.
+Two Knowledge Skills (Any Type): 10 ranks in each.
+Feats: Any three metamagic or item creation feats, plus Skill Focus (Knowledge [any individual Knowledge skill]).</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Loremasters gain no proficiency in any weapon or armor.</wa_proficiency>
+<features>Spells per Day: A loremaster continues training in magic as well as her field of research. Thus, when a new loremaster level is gained, the character gains new spells per day as if she had also gained a level in a spellcasting class she belonged to before she added the prestige class. She does not, however, gain any other benefit a character of that class would have gained (improved chance of controlling or rebuking undead, metamagic or item creation feats, and so on). This essentially means that she adds the level of loremaster to the level of some other spellcasting class the character has, then determines spells per day and caster level accordingly.
+
+If a character had more than one spellcasting class before she became a loremaster, she must decide to which class she adds each level of loremaster for purposes of determining spells per day when she adds the new level.
+
+Secret: In their studies, loremasters stumble upon all sorts of applicable knowledge and secrets. At 1st level and every two levels afterward (3rd, 5th, 7th, and 9th levels), the loremaster chooses one secret from Table: Loremaster Secrets. Her level plus Intelligence modifier determines which secrets she can choose. She can't choose the same secret twice.
+
+Lore: Loremasters gather knowledge. At 2nd level, they gain the ability to know legends or information regarding various topics, just like a bard can with bardic knowledge. The loremaster adds her level and her Intelligence modifier to the Knowledge check. See page 29 in the Player's Handbook for more information on bardic knowledge.
+
+Bonus Languages: Loremasters, in their laborious studies, learn new languages in order to access more knowledge. The loremaster can choose any new language at 4th and 8th level.
+
+Greater Lore: At 6th level, a loremaster gains the ability to identify magic items, as the spell, as an extraordinary ability. She may do this once per item examined.
+
+True Lore: At 10th level, once per day a loremaster can use her knowledge to gain the effects of a legend lore spell or an analyze dweomer spell. True lore is an extraordinary ability.</features>
+</class>
+<class level="1" name="Monk" hd="d8" >
+<requirements>None</requirements>
+<alignment>Any Lawful</alignment>
+<wa_proficiency>Monks are proficient with basic peasant weapons and special weapons whose use is part of monk training. The full list includes club, crossbow (light or heavy), dagger, handaxe, javelin, kama, nunchaku, quarterstaff, shuriken, siangham, and sling.
+
+A monk using a kama, nunchaku, or siangham can strike with his or her unarmed base attack, including her more favorable number of attacks per round (see below). His or her damage, however, is standard for the weapon (1d6, crit X2), not his or her unarmed damage. The weapon must be light, so a Small monk must use Tiny versions of these weapons in order to use the more favorable base attack.
+
+A monk adds her Wisdom bonus (if any) to AC, in addition to her normal Dexterity modifier, and her AC improves as she gains levels. (Only add this extra AC bonus if the total of the monk's Wisdom modifier and the number in the "AC Bonus" column is a positive number.) The Wisdom bonus and the AC bonus represent a preternatural awareness of danger, and a monk does not lose either even in situations when he or she loses her Dexterity modifier due to being unprepared, ambushed, stunned, and so on. (Monks do lose these AC bonuses when immobilized.)
+
+When wearing armor, a monk loses her AC bonus for Wisdom, AC bonus for class and level, favorable multiple unarmed attacks per round, and heightened movement. Furthermore, her special abilities all face the arcane spell failure chance that the armor type normally imposes.</wa_proficiency>
+<features>Unarmed Strike: A monk fighting unarmed gains the benefits of the Improved Unarmed Strike feat and thus does not provoke attacks of opportunity from armed opponents that she attacks.
+
+Making an off-hand attack makes no sense for a monk striking unarmed.
+
+A monk fighting with a one-handed weapon can make an unarmed strike as an off-hand attack, but she suffers the standard penalties for two-weapon fighting. Likewise, a monk with a weapon (other than a special monk weapon) in her off hand gets an extra attack with that weapon but suffers the usual penalties for two-weapon fighting and can't strike with a flurry of blows.
+
+Flurry of Blows: The monk may make one extra attack in a round at her highest base attack, but this attack and each other attack made that round suffer a -2 penalty apiece. This penalty applies for 1 round, so it affects attacks of opportunity the monk might make before her next action. The monk must use the full attack action to strike with a flurry of blows. A monk may also use the flurry of blows if armed with a special monk weapon (kama, nunchaku, or siangham). If armed with one such weapon, the monk makes the extra attack either with that weapon or unarmed. If armed with two such weapons, she uses one for the regular attack (or attacks) and the other for the extra attack. In any case, her damage bonus on the attack with her off hand is not reduced.
+
+Usually, a monk's unarmed strikes deal normal damage rather than subdual damage. However, she can choose to deal her damage as subdual damage when grappling.
+
+Stunning Attack: The monk can use this ability once per round, but no more than once per level per day. The monk must declare she is using a stun attack before making the attack roll (thus, a missed attack roll ruins the attempt). A foe struck by the monk is forced to make a Fortitude saving throw (DC 10 + one-half the monk's level + Wisdom modifier). In addition to receiving normal damage, If the saving throw fails, the opponent is stunned for 1 round. The stunning attack is a supernatural ability.
+
+Evasion: If a monk makes a successful Reflex saving throw against an attack that normally deals half damage on a successful save, the monk instead takes no damage. Evasion can only be used if the monk is wearing light armor or no armor. It is an extraordinary ability.
+
+Deflect Arrows: At 2nd level, a monk gains the Deflect Arrows feat, even if she doesn't have the prerequisite Dexterity score.
+
+Fast Movement: At 3rd level and higher, a monk moves faster than normal. A monk in armor (even light armor) or carrying a medium or heavy load loses this extra speed. A dwarf or a Small monk moves more slowly than a Medium-size monk.
+
+From 9th level on, the monk's running ability is actually a supernatural ability.
+
+Still Mind: At 3rd level, a monk gains a +2 bonus to saving throws against spells and effects from the Enchantment school.
+
+Slow Fall: At 4th level, the monk takes damage as if a fall were 20 feet shorter than it actually is. At 18th level, the monk can use a nearby wall to slow her descent and fall any distance without harm.
+
+Purity of Body: At 5th level, a monk gains immunity to all diseases except for magical diseases.
+
+Improved Trip: At 6th level, a monk gains the Improved Trip feat. She need not have taken the Expertise feat, normally a prerequisite.
+
+Wholeness of Body: At 7th level, a monk can cure her own wounds. She can cure up to twice her current level in hit points each day, and she can spread this healing out among several uses. Wholeness of body is a supernatural ability.
+
+Leap of the Clouds: At 7th level or higher, a monk's jumping distance (vertical or horizontal) is not limited according to her height.
+
+Improved Evasion: At 9th level, a monk only takes half damage on a failed save.
+
+Ki Strike: At 10th level, a monk's unarmed attack is empowered with ki. The unarmed strike damage from such an attack can deal damage to a creature with damage reduction as if the blow were made with a weapon with a +1 enhancement bonus. Ki strike is a supernatural ability.
+
+Diamond Body: At 11th level, a monk gains immunity to poison of all kinds. Diamond body is a supernatural ability.
+
+Abundant Step: At 12th level, a monk can slip magically between spaces, as per the spell dimension door, once per day. This is a spell-like ability, and the monk's effective casting level is one-half her actual level (rounded down).
+
+Diamond Soul: At 13th level, a monk gains spell resistance. Her spell resistance equals her level + 10.
+
+Quivering Palm: Starting at 15th level, a monk can use the quivering palm.
+
+The monk can use the quivering palm attack once a week, and she must announce her intent before making her attack roll. Creatures immune to critical hits cannot be affected. The monk must be of higher level than the target (or have more levels than the target's number of Hit Dice). If the monk strikes successfully and the target takes damage from the blow, the quivering palm attack succeeds. Thereafter the monk can choose to try to slay the victim at any later time within 1 day per level of the monk. The monk merely wills the target to die (a free action), and unless the target makes a Fortitude saving throw (DC 10 + one-half the monk's level + Wisdom modifier), it dies. If the saving throw is successful, the target is no longer in danger from that particular quivering palm attack (but may be affected by another one at a later time). Quivering palm is a supernatural ability.
+
+Timeless Body: After achieving 17th level, a monk no longer suffers ability penalties for aging and cannot be magically aged. (Any penalties she may have already suffered remain in place.) Bonuses still accrue, and the monk still dies of old age when her time is up.
+
+Tongue of the Sun and Moon: A monk of 17th level or above can speak with any living creature.
+
+Empty Body: At 19th level or higher, a monk can assume an ethereal state for 1 round per level per day, as per the spell etherealness. The monk may go ethereal on a number of different occasions during any single day as long as the total number of rounds spent ethereal does not exceed her level. Empty body is a supernatural ability.
+
+Perfect Self: At 20th level, a monk is forevermore treated as an outsider rather than as a humanoid. Additionally, the monk gains damage reduction 20/+1.
+
+Ex-Monks: A monk who becomes nonlawful cannot gain new levels as a monk but retain all monk abilities.</features>
+</class>
+<class level="1" name="Paladin" hd="d10" >
+<requirements>None</requirements>
+<alignment>Lawful</alignment>
+<wa_proficiency>Paladins are proficient with all simple and martial weapons, with all types of armor (heavy, medium, and light), and with shields.</wa_proficiency>
+<features>Detect Evil: At will, the paladin can detect evil as a spell-like ability. This ability duplicates the effects of the spell detect evil.
+
+Divine Grace: A paladin applies her Charisma modifier (if positive) as a bonus to all saving throws.
+
+Lay on Hands: Each day a paladin can cure a total number of hit points equal to the paladin's Charisma bonus (if any) times the paladin's level. The paladin can cure themselves. The paladin may choose to divide her curing among multiple recipients, and he or she doesn't have to use it all at once. Lay on hands is a spell-like ability whose use is a standard action.
+
+Alternatively, the paladin can use any or all of these points to deal damage to undead creatures. Treat this attack just like a touch spell. The paladin decides how many cure points to use as damage after successfully touching the undead creature.
+
+Divine Health: A paladin is immune to all diseases, including magical diseases.
+
+Aura of Courage: Beginning at 2nd level, a paladin is immune to fear (magical or otherwise). Allies within 10 feet of the paladin gain a +4 morale bonus on saving throws against fear effects. Granting the morale bonus to allies is a supernatural ability.
+
+Smite Evil: Once per day, a paladin of 2nd level or higher may attempt to smite evil with one normal melee attack. She adds her Charisma modifier (if positive) to the paladin's attack roll and deals 1 extra point of damage per level. If the paladin accidentally smites a creature that is not evil, the smite has no effect but it is still used up for that day. Smite evil is a supernatural ability.
+
+Remove Disease: Beginning at 3rd level, a paladin can remove disease, as per the spell remove disease, once per week. Remove disease is a spell-like ability for paladins.
+
+Turn Undead: The paladin may use this ability a number of times per day equal to three plus the paladin's Charisma modifier. The paladin turns undead as a cleric of two levels lower would.
+
+Extra Turning: As a feat, a paladin may take Extra Turning. This feat allows the paladin to turn undead four more times per day than normal. A paladin can take this feat multiple times, gaining four extra daily turning attempts each time.
+
+Spells: Beginning at 4th level, a paladin gains the ability to cast a small number of divine spells. To cast a spell, the paladin must have a Wisdom score of at least 10 + the spell's level. Paladin bonus spells are based on Wisdom, and saving throws against these spells have a Difficulty Class of 10 + spell level + Wisdom modifier. When the paladin gets 0 spells of a given level, such as 0 1st-level spells at 4th level, the paladin gets only bonus spells. A paladin has access to any spell on the paladin spell list and can freely choose which to prepare, just as a cleric can.
+
+A paladin prepares and casts spells just as a cleric does (though the paladin cannot use spontaneous casting to substitute a cure spell in place of a prepared spell).
+
+Through 3rd level, a paladin has no caster level. Starting at 4th level, a paladin's caster level is one-half his or her class level.
+
+Special Mount: Upon or after reaching 5th level, a paladin can call an unusually intelligent, strong, and loyal steed to serve him or her in her crusade against evil. This mount is usually a heavy warhorse (for a Medium-size paladin) or a warpony (for a Small paladin).
+
+Should the paladin's mount die, another cannot be called for a year and a day. The new mount has all the accumulated abilities due a mount of the paladin's level.
+
+The DM will provide information about the mount that responds to the paladin's call.
+
+Code of Conduct: A paladin must be of lawful good alignment and loses all special class abilities if she ever willingly commits an act of evil. Additionally, a paladin's code requires that she respect legitimate authority, act with honor (not lying, not cheating, not using poison, etc.), help those who need help (provided they do not use the help for evil or chaotic ends), and punish those that harm or threaten innocents.
+
+Associates: While she may adventure with characters of any good or neutral alignment, a paladin will never knowingly associate with evil characters. A paladin will not continue an association with someone who consistently offends her moral code. A paladin may only hire henchmen or accept followers who are lawful good.
+
+Ex-Paladins: A paladin who ceases to be lawful good, who willfully commits an evil act, or who grossly violates the code of conduct loses all special abilities and spells, including the service of the paladin's warhorse. She also may not progress in levels as a paladin. She regains her abilities if she atones for her violations, as appropriate.</features>
+</class>
+<class level="1" name="Ranger" hd="d10" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>A ranger is proficient with all simple and martial weapons, light armor, medium armor, and shields. </wa_proficiency>
+<features>When wearing light armor or no armor, a ranger can fight with two weapons as if he or she had the feats Ambidexterity and Two-Weapon Fighting. The ranger loses this special bonus when fighting in medium or heavy armor, or when using a double-headed weapon (such as a double sword).
+
+Spells: Beginning at 4th level, a ranger gains the ability to cast a small number of divine spells. To cast a spell, the ranger must have a Wisdom score of at least 10 + the spell's level. Ranger bonus spells are based on Wisdom, and saving throws against these spells have a Difficulty Class of 10 + spell level + Wisdom modifier. When the ranger gets 0 spells of a given level, such as 0 1st-level spells at 4th level, the ranger gets only bonus spells. A ranger without a bonus spell for that level cannot yet cast a spell of that level. A ranger has access to any spell on the ranger spell list and can freely choose which to prepare. A ranger prepares and casts spells just as a cleric does (though the ranger cannot use spontaneous casting to lose a spell and cast a cure or inflict spell in its place).
+
+Through 3rd level, a ranger has no caster level. Starting at 4th level, a ranger's caster level is one-half his class level.
+
+Track: A ranger gains Track as a bonus feat.
+
+Favored Enemy: At 1st level, a ranger may select a type of creature as a favored enemy. (A ranger can only select his own race as a favored enemy if he is evil.) Due to his extensive study of his foes and training in the proper techniques for combating them, the ranger gains a +1 bonus to Bluff, Listen, Sense Motive, Spot, and Wilderness Lore checks when using these skills against this type of creature. Likewise, he gets the same bonus to weapon damage rolls against creatures of this type. A ranger also gets the damage bonus with ranged weapons, but only against targets within 30 feet (the ranger cannot strike with deadly accuracy beyond that range). The bonus doesn't apply to damage against creatures that are immune to critical hits.
+
+At 5th level and at every five levels thereafter (10th, 15th, and 20th level), the ranger may select a new favored enemy, and the bonus associated with every previously selected favored enemy goes up by +1.
+
+* Rangers may not select "humanoid" or "outsider" as a favored enemy, but they may select a more narrowly defined type of humanoid or outsider. A ranger can only select his own race as a favored enemy if he is evil.
+
+Improved Two-Weapon Fighting: A ranger with a base attack bonus of at least +9 can choose to gain the Improved Two-Weapon Fighting feat even if he does not have the other prerequisites for the feat. The ranger must be wearing light armor or no armor in order to use this benefit. </features>
+</class>
+<class level="1" name="Rogue" hd="d6" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>A rogue's weapon training focuses on weapons suitable for stealth and sneak attacks. Thus, all rogues are proficient with the crossbow (hand or light), dagger (any type), dart, light mace, sap, shortbow (normal and composite), and short sword. Medium-size rogues are also proficient with certain weapons that are too big for Small rogues to use and conceal easily: club, heavy crossbow, heavy mace, morningstar, quarterstaff, and rapier. Rogues are proficient with light armor but not with shields. </wa_proficiency>
+<features>Sneak Attack: Any time the rogue's target would be denied a Dexterity bonus to AC (whether the target actually has a Dexterity bonus or not), or when the rogue flanks the target, the rogue's attack deals extra damage. The extra damage is +1d6 at 1st level and an additional 1d6 every two levels thereafter. Should the rogue score a critical hit with a sneak attack, this extra damage is not multiplied.
+
+Ranged attacks can only count as sneak attacks if the target is within 30 feet. The rogue can't strike with deadly accuracy from beyond that range.
+
+With a sap (blackjack) or an unarmed strike, the rogue can make a sneak attack that deals subdual damage instead of normal damage. The rogue cannot use a weapon that deals normal damage to deal subdual damage in a sneak attack, not even with the usual -4 penalty.
+
+A rogue can only sneak attack a living creature with a discernible anatomy. Any creature that is immune to critical hits is also not vulnerable to sneak attacks. The rogue must be able to see the target well enough to pick out a vital spot and must be able to reach a vital spot. The rogue cannot sneak attack while striking a creature with concealment or striking the limbs of a creature whose vitals are beyond reach.
+
+Traps: Rogues (and only rogues) can use the Search skill to locate traps when the task has a Difficulty Class higher than 20. Finding a nonmagical trap has a DC of at least 20, higher if it is well hidden. Finding a magic trap has a DC of 25 + the level of the spell used to create it.
+
+Rogues (and only rogues) can use the Disable Device skill to disarm magic traps. A magic trap generally has a DC of 25 + the level of the spell used to create it.
+
+A rogue who beats a trap's DC by 10 or more with a Disable Device check can generally study a trap, figure out how it works, and bypass it (with his party) without disarming it.
+
+Evasion: At 2nd level, a rogue gains evasion. If exposed to any effect that normally allows a character to attempt a Reflex saving throw for half damage, the rogue takes no damage with a successful saving throw. Evasion can only be used if the rogue is wearing light armor or no armor. It is an extraordinary ability.
+
+Uncanny Dodge: At 3rd level and above, she retains her Dexterity bonus to AC (if any) if caught flat-footed or struck by an invisible attacker.
+
+At 6th level , the rogue can no longer be flanked. Another rogue at least four levels higher can still flank.
+
+At 11th level, the rogue gains a +1 bonus to Reflex saves made to avoid traps and a +1 dodge bonus to AC against attacks by traps. At 14th level, these bonuses rise to +2. At 17th, they rise to +3, and at 20th they rise to +4.
+
+Special Abilities: On achieving 10th level and every three levels thereafter (13th, 16th, and 19th), a rogue chooses a special ability from among the following:
+
+Crippling Strike: When the rogue damages an opponent with a sneak attack, the target also takes 1 point of Strength damage.
+
+Defensive Roll: Once per day, when a rogue would be reduced to 0 hit points or less by damage in combat (from a weapon or other blow, not a spell or special ability), the rogue can attempt to roll with the damage. She makes a Reflex saving throw (DC = damage dealt) and, if successful, takes only half damage from the blow. The rogue must be aware of the attack and able to react to it in order to execute the defensive roll - if the Dexterity bonus to AC is denied, the rogue can't roll. Since this effect would not normally allow a character to make a Reflex save for half damage, the rogue's evasion ability does not apply to the defensive roll.
+
+Improved Evasion: The rogue takes only half damage on a failed save.
+
+Opportunist: Once per round, the rogue can make an attack of opportunity against an opponent who has just been struck for damage in melee by another character. This attack counts as the rogue's attacks of opportunity for that round. Even a rogue with the Combat Reflexes feat can't use the opportunist ability more than once per round.
+
+Skill Mastery: The rogue selects a number of skills equal to 3 + Intelligence modifier. When making a skill check with one of these skills, the rogue may take 10 even if stress and distractions would normally prevent the rogue from doing so. The rogue may gain this special ability multiple times, selecting additional skills for it to apply to each time.
+
+Slippery Mind: If a rogue with a slippery mind is affected by an enchantment and fails the saving throw, 1 round later the rogue can attempt the saving throw again. The rogue only gets this one extra chance to succeed. This is an extraordinary ability.
+
+Feat: A rogue may gain a feat in place of a special ability. </features>
+</class>
+<class level="1" name="Shadowdancer" hd="d8" >
+<requirements>Move Silently: 8 ranks.
+Hide: 10 ranks.
+Perform: 5 ranks.
+Feats: Dodge, Mobility, Combat Reflexes</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Shadowdancers are proficient with the club, crossbow (hand, light, or heavy), dagger (any type), dart, mace, morningstar, quarterstaff, rapier, sap, shortbow (normal and composite), and short sword. Shadowdancers are proficient with light armor but not with shields.</wa_proficiency>
+<features>Hide in Plain Sight: Shadowdancers can use the Hide skill even while being observed. As long as they are within 10 feet of some sort of shadow, shadowdancers can hide themselves from view in the open without anything to actually hide behind. They cannot, however, hide in their own shadows. Hide in plain sight is a supernatural ability.
+
+Evasion: At 2nd level, a shadowdancer gains evasion. If exposed to any effect that normally allows her to attempt a Reflex saving throw for half damage (such as a fireball), she takes no damage with a successful saving throw. The evasion ability can only be used if the shadowdancer is wearing light armor or no armor.
+
+Darkvision: At 2nd level, a shadowdancer can see in the dark as though she were permanently under the affect of a darkvision spell. This is a supernatural ability.
+
+Uncanny Dodge: Starting at 2nd level, the shadowdancer gains the extraordinary ability to react to danger before her senses would normally allow her to even be aware of it. At 2nd level and above, she retains her Dexterity bonus to AC (if any) regardless of being caught flat-footed or struck by an invisible attacker. (She still loses any Dexterity bonus to AC if immobilized.)
+
+At 5th level, the shadowdancer can no longer be flanked, since she can react to opponents on opposite sides of her as easily as she can react to a single attacker. This defense denies rogues the ability to use flank attacks to sneak attack the shadowdancer. The exception to this defense is that a rogue at least 4 levels higher than the shadowdancer can flank her (and thus sneak attack her).
+
+At 10th level, the shadowdancer gains an intuitive sense that alerts her to danger from traps, giving her a +1 bonus to Reflex saves made to avoid traps.
+
+If the shadowdancer has another class that grants the uncanny dodge ability, add together all the class levels of the classes that grant the ability and determine the character's uncanny dodge ability on that basis.
+
+Shadow Illusion: When a shadowdancer reaches 3rd level, she can create visual illusions from surrounding shadows. This spell-like ability is identical to the arcane spell silent image and may be employed once per day.
+
+Summon Shadow: At 3rd level, a shadowdancer can summon a shadow, an undead shade. Unlike a normal shadow, this shadow's alignment matches that of the shadowdancer. The summoned shadow cannot be turned, rebuked, or commanded by any third party. This shadow serves as a companion to the shadowdancer and can communicate intelligibly with the shadowdancer. Every third level gained by the shadowdancer allows her to summon an additional shadow and adds +2 HD (and the requisite base attack and base save bonus increases) to all her shadow companions.
+
+If a shadow companion is destroyed, or the shadowdancer chooses to dismiss it, the shadowdancer must attempt a Fortitude saving throw (DC 15). If the saving throw fails, the shadowdancer loses 200 experience points per shadowdancer level. A successful saving throw reduces the loss by half, to 100 XP per prestige class level. The shadowdancer's experience can never go below 0 as the result of a shadow's dismissal or destruction. A destroyed or dismissed shadow companion cannot be replaced for a year and a day.
+
+Shadow Jump: At 4th level, the shadowdancer gains the ability to travel between shadows as if by means of a dimension door spell. The limitation is that the magical transport must begin and end in an area with at least some shadow. The shadowdancer can jump up to a total of 20 feet each day in this way, although this may be a single jump of 20 feet or two jumps of 10 feet each. Every two levels thereafter, the distance a shadowdancer can jump each day doubles (40 feet at 6th level, 80 feet at 8th level, and 160 feet at 10th level). This amount can be split among many jumps, but each one, no matter how small, counts as a 10-foot increment. (A 6th-level shadowdancer who jumps 32 feet cannot jump again until the next day.)
+
+Defensive Roll: Starting at 5th level, the shadowdancer can roll with a potentially lethal blow to take less damage from it. Once per day, when a shadowdancer would be reduced to 0 hit points or less by damage in combat (from a weapon or other blow, not a spell or special ability), the shadowdancer can attempt to roll with the damage. She makes a Reflex saving throw (DC = damage dealt) and, if successful, takes only half damage from the blow. She must be aware of the attack and able to react to it in order to execute her defensive roll. If she is in a situation that would deny her any Dexterity bonus to AC, she can't attempt the defensive roll.
+
+Slippery Mind: This extraordinary ability, gained at 7th level, represents the shadowdancer's ability to wriggle free from magical effects that would otherwise control or compel her. If the shadowdancer is affected by an enchantment and fails her saving throw, 1 round later she can attempt her saving throw again. She only gets this one extra chance to succeed at her saving throw. If it fails as well, the spell's effects proceed normally.
+
+Improved Evasion: This extraordinary ability, gained at 10th level, works like evasion (see above). The shadowdancer takes no damage at all on successful saving throws against attacks that allow a Reflex saving throw for half damage (breath weapon, fireball, and so on). What's more, she takes only half damage even if she fails her saving throw, since the shadowdancer's reflexes allow her to get out of harm's way with incredible speed.</features>
+</class>
+<class level="1" name="Sorcerer" hd="d4" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Sorcerers are proficient with all simple weapons. They are not proficient with any type of armor, nor with shields. </wa_proficiency>
+<features>Spells: A sorcerer casts arcane spells. The number of spells a sorcerer knows is not affected by his Charisma bonus. The spells a sorcerer knows can be common spells chosen from the sorcerer and wizard spell list, or they can be unusual spells that the sorcerer has gained some understanding of by study.
+
+A sorcerer is limited to casting a certain number of spells of each level per day, but he need not prepare his spells in advance. The number of spells he can cast per day is improved by his bonus spells, if any.
+
+A sorcerer may use a higher-level slot to cast a lower-level spell if he so chooses. The spell is still treated as its actual level, not the level of the slot used to cast it.
+
+To learn or cast a spell, a sorcerer must have a Charisma score of at least 10 + the spell's level. The Difficulty Class for saving throws against sorcerer spells is 10 + the spell's level + the sorcerer's Charisma modifier.</features>
+</class>
+<class level="1" name="Wizard" hd="d4" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Wizards are skilled with the club, dagger, heavy crossbow, light crossbow, and quarterstaff. Wizards are not proficient with any type of armor nor with shields. </wa_proficiency>
+<features>Spells: A wizard casts arcane spells. She is limited to a certain number of spells of each spell level per day, according to her class level. A wizard must prepare spells ahead of time by getting a good night's sleep and spending 1 hour studying her spellbook. While studying, the wizard decides which spells to prepare. To learn, prepare, or cast a spell, a wizard must have an Intelligence score of at least 10 + the spell's level. A wizard's bonus spells are based on Intelligence. The Difficulty Class for saving throws against wizard spells is 10 + the spell's level + the wizard's Intelligence modifier.
+
+Bonus Languages: A wizard may substitute Draconic for one of the bonus languages available to the character.
+
+Familiar: A wizard can summon a familiar in exactly the same manner as a sorcerer. See the sorcerer description.
+
+Scribe Scroll: A wizard has the bonus item creation feat Scribe Scroll, enabling her to create magic scrolls.
+
+Bonus Feats: Every five levels, a wizard gains a bonus feat. This feat must be a metamagic feat, an item creation feat, or Spell Mastery.
+
+Spellbooks: Wizards must study their spellbooks each day to prepare their spells. A wizard cannot prepare any spell not recorded in her spellbook (except for read magic, which all wizards can prepare from memory).
+
+Spell Mastery: A wizard (and only a wizard) can take the special feat Spell Mastery. Each time the wizard takes this feat, choose a number of spells equal to the wizard's Intelligence modifier (they must be spells that the wizard already knows). From that point on, the wizard can prepare these spells without referring to a spellbook.
+
+School Specialization
+
+A school is one of eight groupings of spells, each defined by a common theme, such as illusion or necromancy. A wizard may specialize in one school of magic.
+
+Specialization allows a wizard to cast extra spells from the chosen school, but the wizard then never learns to cast spells from one or more other schools. Spells of the school or schools that the specialist gives up are not available to her, and she can't even cast such spells from scrolls or wands.
+
+The wizard must choose whether to specialize and how at 1st level. She may not change her specialization later.
+
+The specialist can prepare one additional spell (of the school selected as a specialty) per spell level each day.
+
+The specialist gains a +2 bonus to Spellcraft checks to learn the spells of her chosen school.
+
+The eight schools of arcane magic are Abjuration, Conjuration, Divination, Enchantment, Evocation, Illusion, Necromancy, and Transmutation. Spells that do not fall into any of these schools are called universal spells.
+
+Abjuration: To become an abjurer, a wizard must select a prohibited school or schools from the following choices: (1) either Conjuration, Enchantment, Evocation, Illusion, or Transmutation; or (2) both Divination and Necromancy.
+
+Conjuration: To become a conjurer, a wizard must select a prohibited school or schools from one of the following choices: (1) Evocation; (2) any two of the following three schools: Abjuration, Enchantment, and Illusion; (3) Transmutation, or (4) any three schools.
+
+Divination: To become a diviner, a wizard must select any other single school as a prohibited school.
+
+Enchantment: To become an enchanter, a wizard must select a prohibited school or schools from the following choices: (1) either Abjuration, Conjuration, Evocation, Illusion, or Transmutation; or (2) both Divination and Necromancy.
+
+Evocation: To become an evoker, a wizard must select a prohibited school or schools from one of the following choices: (1) Conjuration; (2) any two of the following three schools: Abjuration, Enchantment, and Illusion; (3) Transmutation; or (4) any three schools.
+
+Illusion: To become an illusionist, a wizard must select a prohibited school or schools from the following choices: (1) either Abjuration, Conjuration, Enchantment, Evocation, or Transmutation; or (2) both Divination and Necromancy.
+
+Necromancy: To become a necromancer, a wizard must select any other single school as a prohibited school.
+
+Transmutation: To become a transmuter, a wizard must select a prohibited school or schools from one of the following choices: (1) Conjuration; (2) Evocation; (2) any two of the following three schools: Abjuration, Enchantment, and Illusion; or (4) any three schools.
+
+Universal: Not a school, but a category for spells all wizards can learn. A wizard cannot select universal as a specialty school or as a school to which she does not have access.</features>
+</class>
+<class level="1" name="Adept" hd="d6" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Adepts are skilled with all simple weapons. Adepts are not proficient with any type of armor nor with shields</wa_proficiency>
+<features>Spells: An adept casts divine spells. She is limited to a certain number of spells of each spell level per day, according to her class level. Like a cleric, an adept may prepare and cast any spell on the adept list, provided she can cast spells of that level. Like a cleric, she prepares her spells ahead of time each day. The DC for a saving throw against an adept's spell is 10 + spell level + the adept's Wisdom modifier.
+
+Adepts, unlike wizards, do not acquire their spells from books or scrolls, nor prepare them through study. Instead, they meditate or pray for their spells, receiving them as divine inspiration or through their own strength of faith. Each adept must choose a time each day at which she must spend an hour in quiet contemplation or supplication to regain her daily allotment of spells. Time spent resting has no effect on whether an adept can prepare spells.
+
+When the adept gets 0 spells of a given level, she gets only bonus spells for that spell slot. An adept without a bonus spell for that level cannot yet cast a spell of that level. Bonus spells are based on Wisdom.
+
+Each adept has a particular holy symbol (as a divine focus) depending on the adept's magical tradition.
+
+Familiar: At 2nd level, an adept can call a familiar, just like a sorcerer or wizard can.
+
+Adept Spell List
+0 level-create water, cure minor wounds, detect magic, ghost sound, guidance, light, mending, purify food and drink, read magic.
+1st level-bless, burning hands, cause fear, command, comprehend languages, cure light wounds, detect chaos, detect evil, detect good, detect law, endure elements, obscuring mist, protection from chaos, protection from evil, protection from good, protection from law, sleep.
+2nd level-aid, animal trance, bull's strength, cat's grace, cure moderate wounds, darkness, delay poison, endurance, invisibility, mirror image, resist elements, see invisibility, web.
+3rd level-animate dead, bestow curse, contagion, continual flame, cure serious wounds, daylight, deeper darkness, lightning bolt, neutralize poison, remove curse, remove disease, tongues.
+4th level-cure critical wounds, minor creation, polymorph other, polymorph self, restoration, stoneskin, wall of fire.
+5th level-break enchantment, commune, heal, major creation, raise dead, true seeing, wall of stone.</features>
+</class>
+<class level="1" name="Aristocrat" hd="d8" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The aristocrat is proficient in the use of all simple and martial weapons and with all types of armor and shields.</wa_proficiency>
+<features>None</features>
+</class>
+<class level="1" name="Commoner" hd="d4" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The commoner is proficient with one simple weapon. He is not proficient with weapons, armor, or shields.</wa_proficiency>
+<features>None</features>
+</class>
+<class level="1" name="Expert" hd="d6" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The expert is proficient in the use of all simple weapons and with light armor but not shields. </wa_proficiency>
+<features>None</features>
+</class>
+<class level="1" name="Warrior" hd="d8" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The warrior is proficient in the use of all simple and martial weapons and all armor and shields.</wa_proficiency>
+<features>None</features>
+</class>
+</classes>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/d20/d20divine.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,326 @@
+<divine>
+<gift name='Resistance' memrz='0' level='0' desc='Subject gains +1 on saving throws.' />
+<gift name='Ray of Frost' memrz='0' level='0' desc='Ray deals 1d3 cold damage.' />
+<gift name='Detect Poison' memrz='0' level='0' desc='Detects poison in one creature or small object.' />
+<gift name='Daze' memrz='0' level='0' desc='Creature loses next action.' />
+<gift name='Flare' memrz='0' level='0' desc='Dazzles one creature (-1 attack).' />
+<gift name='Light' memrz='0' level='0' desc='Object shines like a torch.' />
+<gift name='Dancing Lights' memrz='0' level='0' desc='Figment torches or other lights.' />
+<gift name='Ghost Sound' memrz='0' level='0' desc='Figment sounds.' />
+<gift name='Disrupt Undead' memrz='0' level='0' desc='Deals 1d6 damage to one undead.' />
+<gift name='Mage Hand' memrz='0' level='0' desc='5-pound telekinesis.' />
+<gift name='Mending' memrz='0' level='0' desc='Makes minor repairs on an object.' />
+<gift name='Open/Close' memrz='0' level='0' desc='Opens or closes small or light things.' />
+<gift name='Arcane Mark' memrz='0' level='0' desc='Inscribes a personal rune (visible or invisible).' />
+<gift name='Detect Magic' memrz='0' level='0' desc='Detects gifts and magic items within 60 ft.' />
+<gift name='Prestidigitation' memrz='0' level='0' desc='Performs minor tricks.' />
+<gift name='Read Magic' memrz='0' level='0' desc='Read scrolls and giftbooks.' />
+<gift name='Alarm' memrz='0' level='1' desc='Wards an area for 2 hours/level.' />
+<gift name='Endure Elements' memrz='0' level='1' desc='Ignores 5 damage/round from one energy type.' />
+<gift name='Hold Portal' memrz='0' level='1' desc='Holds door shut.' />
+<gift name='Protection from Chaos/Evil/Good/Law' memrz='0' level='1' desc='+2 AC and saves, counter mind control, hedge out elementals and outsiders.' />
+<gift name='Shield' memrz='0' level='1' desc='Invisible disc gives cover and blocks magic missiles.' />
+<gift name='Grease' memrz='0' level='1' desc='Makes 10-ft. square or one object slippery.' />
+<gift name='Mage Armor' memrz='0' level='1' desc='Gives subject +4 armor bonus.' />
+<gift name='Mount' memrz='0' level='1' desc='Summons riding horse for 2 hr./level.' />
+<gift name='Obscuring Mist' memrz='0' level='1' desc='Fog surrounds you.' />
+<gift name='Summon Monster I' memrz='0' level='1' desc='Calls outsider to fight for you.' />
+<gift name='Unseen Servant' memrz='0' level='1' desc='Creates invisible force that obeys your commands.' />
+<gift name='Comprehend Languages' memrz='0' level='1' desc='Understands all spoken and written languages.' />
+<gift name='Detect Secret Doors' memrz='0' level='1' desc='Reveals hidden doors within 60 ft.' />
+<gift name='Detect Undead' memrz='0' level='1' desc='Reveals undead within 60 ft.' />
+<gift name='Identify' memrz='0' level='1' desc='Determines single feature of magic item.' />
+<gift name='True Strike' memrz='0' level='1' desc='Adds +20 bonus to your next attack roll.' />
+<gift name='Charm Person' memrz='0' level='1' desc='Makes one person your friend.' />
+<gift name='Hypnotism' memrz='0' level='1' desc='Fascinates 2d4 HD of creatures.' />
+<gift name='Sleep' memrz='0' level='1' desc='Put 2d4 HD of creatures into comatose slumber.' />
+<gift name='Magic Missile' memrz='0' level='1' desc='1d4+1 damage; +1 missile/two levels above 1st (max +5).' />
+<gift name='Tensers Floating Disk' memrz='0' level='1' desc='3-ft.-diameter horizontal disk that holds 100 lb./level.' />
+<gift name='Change Self' memrz='0' level='1' desc='Changes your appearance.' />
+<gift name='Color Spray' memrz='0' level='1' desc='Knocks unconscious, blinds, or stuns 1d6 weak creatures.' />
+<gift name='Nystuls Magical Aura' memrz='0' level='1' desc='Grants object false magic aura.' />
+<gift name='Nystuls Undetectable Aura' memrz='0' level='1' desc='Masks magic items aura.' />
+<gift name='Silent Image' memrz='0' level='1' desc='Creates minor illusion of your design.' />
+<gift name='Ventriloquism' memrz='0' level='1' desc='Throws voice for 1 min./level.' />
+<gift name='Cause Fear' memrz='0' level='1' desc='One creature flees for 1d4 rounds.' />
+<gift name='Chill Touch' memrz='0' level='1' desc='1 touch/level deals 1d6 damage and possibly 1 Str damage.' />
+<gift name='Ray of Enfeeblement' memrz='0' level='1' desc='Ray reduces Str by 1d6 points +1 point/two levels.' />
+<gift name='Animate Rope' memrz='0' level='1' desc='Makes a rope move at your command.' />
+<gift name='Burning Hands' memrz='0' level='1' desc='1d4 fire damage/level (max: 5d4).' />
+<gift name='Enlarge' memrz='0' level='1' desc='Object or creature grows +10%/level (max +50%).' />
+<gift name='Erase' memrz='0' level='1' desc='Mundane or magical writing vanishes.' />
+<gift name='Expeditious Retreat' memrz='0' level='1' desc='Doubles your speed.' />
+<gift name='Feather Fall' memrz='0' level='1' desc='Objects or creatures fall slowly.' />
+<gift name='Jump' memrz='0' level='1' desc='Subject gets +30 on Jump checks.' />
+<gift name='Magic Weapon' memrz='0' level='1' desc='Weapon gains +1 bonus.' />
+<gift name='Message' memrz='0' level='1' desc='Whispered conversation at distance.' />
+<gift name='Reduce' memrz='0' level='1' desc='Object or creature shrinks 10%/level (max 50%).' />
+<gift name='Shocking Grasp' memrz='0' level='1' desc='Touch delivers 1d8 +1/level electricity.' />
+<gift name='Spider Climb' memrz='0' level='1' desc='Grants ability to walk on walls and ceilings.' />
+<gift name='Arcane Lock' memrz='0' level='2' desc='Magically locks a portal or chest.' />
+<gift name='Obscure Object' memrz='0' level='2' desc='Masks object against divination.' />
+<gift name='Protection from Arrows' memrz='0' level='2' desc='Subject immune to most ranged attacks.' />
+<gift name='Resist Elements' memrz='0' level='2' desc='Ignores 12 damage/round from one energy type.' />
+<gift name='Fog Cloud' memrz='0' level='2' desc='Fog obscures vision.' />
+<gift name='Glitterdust' memrz='0' level='2' desc='Blinds creatures, outlines invisible creatures.' />
+<gift name='Melfs Acid Arrow' memrz='0' level='2' desc='Ranged touch attack; 2d4 damage for 1 round + 1 round/three levels.' />
+<gift name='Summon Monster II' memrz='0' level='2' desc='Calls outsider to fight for you.' />
+<gift name='Summon Swarm' memrz='0' level='2' desc='Summons swarm of small crawling or flying creatures.' />
+<gift name='Web' memrz='0' level='2' desc='Fills 10-ft. cube/level with sticky spider webs.' />
+<gift name='Detect Thoughts' memrz='0' level='2' desc='Allows listening to surface thoughts.' />
+<gift name='Locate Object' memrz='0' level='2' desc='Senses direction toward object (specific or type).' />
+<gift name='See Invisibility' memrz='0' level='2' desc='Reveals invisible creatures or objects.' />
+<gift name='Tashas Hideous Laughter' memrz='0' level='2' desc='Subject loses actions for 1d3 rounds.' />
+<gift name='Darkness' memrz='0' level='2' desc='20-ft. radius of supernatural darkness.' />
+<gift name='Daylight' memrz='0' level='2' desc='60-ft. radius of bright light.' />
+<gift name='Flaming Sphere' memrz='0' level='2' desc='Rolling ball of fire, 2d6 damage, lasts 1 round/level.' />
+<gift name='Shatter' memrz='0' level='2' desc='Sonic vibration damages objects or crystalline creatures.' />
+<gift name='Blur' memrz='0' level='2' desc='Attacks miss subject 20% of the time.' />
+<gift name='Continual Flame' memrz='0' level='2' desc='Makes a permanent, heatless torch.' />
+<gift name='Hypnotic Pattern' memrz='0' level='2' desc='Fascinates 2d4+1 HD/level of creatures.' />
+<gift name='Invisibility' memrz='0' level='2' desc='Subject is invisible for 10 min./level or until it attacks.' />
+<gift name='Leomunds Trap' memrz='0' level='2' desc='Makes item seem trapped.' />
+<gift name='Magic Mouth' memrz='0' level='2' desc='Speaks once when triggered.' />
+<gift name='Minor Image' memrz='0' level='2' desc='As silent image, plus some sound.' />
+<gift name='Mirror Image' memrz='0' level='2' desc='Creates decoy duplicates of you (1d4 +1/three levels, max 8).' />
+<gift name='Misdirection' memrz='0' level='2' desc='Misleads divinations for one creature or object.' />
+<gift name='Ghoul Touch' memrz='0' level='2' desc='Paralyzes one subject, who exudes stench (-2 penalty) nearby.' />
+<gift name='Scare' memrz='0' level='2' desc='Panics creatures up to 5 HD (15-ft. radius).' />
+<gift name='Spectral Hand' memrz='0' level='2' desc='Creates disembodied glowing hand to deliver touch attacks.' />
+<gift name='Alter Self' memrz='0' level='2' desc='As change self, plus more drastic changes.' />
+<gift name='Blindness/Deafness' memrz='0' level='2' desc='Makes subject blind or deaf.' />
+<gift name='Bulls Strength' memrz='0' level='2' desc='Subject gains 1d4+1 Str for 1 hr./level.' />
+<gift name='Cats Grace' memrz='0' level='2' desc='Subject gains 1d4+1 Dex for 1 hr./level.' />
+<gift name='Darkvision' memrz='0' level='2' desc='See 60 ft. in total darkness.' />
+<gift name='Endurance' memrz='0' level='2' desc='Gain 1d4+1 Con for 1 hr./level.' />
+<gift name='Knock' memrz='0' level='2' desc='Opens locked or magically sealed door.' />
+<gift name='Levitate' memrz='0' level='2' desc='Subject moves up and down at your direction.' />
+<gift name='Pyrotechnics' memrz='0' level='2' desc='Turns fire into blinding light or choking smoke.' />
+<gift name='Rope Trick' memrz='0' level='2' desc='Up to eight creatures hide in extradimensional space.' />
+<gift name='Whispering Wind' memrz='0' level='2' desc='Sends a short message one mile/level.' />
+<gift name='Dispel Magic' memrz='0' level='3' desc='Cancels magical gifts and effects.' />
+<gift name='Explosive Runes' memrz='0' level='3' desc='Deals 6d6 damage when read.' />
+<gift name='Magic Circle against Chaos/Evil/Good/Law' memrz='0' level='3' desc='As protection gifts, but 10-ft. radius and 10 min./level.' />
+<gift name='Nondetection' memrz='0' level='3' desc='Hides subject from divination, scrying.' />
+<gift name='Protection from Elements' memrz='0' level='3' desc='Absorb 12 damage/level from one kind of energy.' />
+<gift name='Flame Arrow' memrz='0' level='3' desc='Shoots flaming projectiles (extra damage) or fiery bolts (4d6 damage).' />
+<gift name='Phantom Steed' memrz='0' level='3' desc='Magical horse appears for 1 hour/level.' />
+<gift name='Sepia Snake Sigil' memrz='0' level='3' desc='Creates text symbol that immobilizes reader.' />
+<gift name='Sleet Storm' memrz='0' level='3' desc='Hampers vision and movement.' />
+<gift name='Stinking Cloud' memrz='0' level='3' desc='Nauseating vapors, 1 round/level.' />
+<gift name='Summon Monster III' memrz='0' level='3' desc='Calls outsider to fight for you.' />
+<gift name='Clairaudience/Clairvoyance' memrz='0' level='3' desc='Hear or see at a distance for 1 min./level.' />
+<gift name='Tongues' memrz='0' level='3' desc='Speak any language.' />
+<gift name='Hold Person' memrz='0' level='3' desc='Holds one person helpless; 1 round/level.' />
+<gift name='Suggestion' memrz='0' level='3' desc='Compels subject to follow stated course of action.' />
+<gift name='Fireball' memrz='0' level='3' desc='1d6 damage per level, 20-ft. radius.' />
+<gift name='Gust of Wind' memrz='0' level='3' desc='Blows away or knocks down smaller creatures.' />
+<gift name='Leomunds Tiny Hut' memrz='0' level='3' desc='Creates shelter for 10 creatures.' />
+<gift name='Lightning Bolt' memrz='0' level='3' desc='Electricity deals 1d6 damage/level.' />
+<gift name='Wind Wall' memrz='0' level='3' desc='Deflects arrows, smaller creatures, and gases.' />
+<gift name='Displacement' memrz='0' level='3' desc='Attacks miss subject 50%.' />
+<gift name='Illusory Script' memrz='0' level='3' desc='Only intended reader can decipher.' />
+<gift name='Invisibility Sphere' memrz='0' level='3' desc='Makes everyone within 10 ft. invisible.' />
+<gift name='Major Image' memrz='0' level='3' desc='As silent image, plus sound, smell and thermal effects.' />
+<gift name='Gentle Repose' memrz='0' level='3' desc='Preserves one corpse.' />
+<gift name='Halt Undead' memrz='0' level='3' desc='Immobilizes undead for 1 round/level.' />
+<gift name='Vampiric Touch' memrz='0' level='3' desc='Touch deals 1d6/two caster levels; caster gains damage as hp.' />
+<gift name='Blink' memrz='0' level='3' desc='You randomly vanish and reappear for 1 round/level.' />
+<gift name='Fly' memrz='0' level='3' desc='Subject flies at speed of 90.' />
+<gift name='Gaseous Form' memrz='0' level='3' desc='Subject becomes insubstantial and can fly slowly.' />
+<gift name='Greater Magic Weapon' memrz='0' level='3' desc='+1/three levels (max +5).' />
+<gift name='Haste' memrz='0' level='3' desc='Extra partial action and +4 AC.' />
+<gift name='Keen Edge' memrz='0' level='3' desc='Doubles normal weapons threat range.' />
+<gift name='Secret Page' memrz='0' level='3' desc='Changes one page to hide its real content.' />
+<gift name='Shrink Item' memrz='0' level='3' desc='Object shrinks to one-twelfth size.' />
+<gift name='Slow' memrz='0' level='3' desc='One subject/level takes only partial actions, -2 AC, -2 melee rolls.' />
+<gift name='Water Breathing' memrz='0' level='3' desc='Subjects can breathe underwater.' />
+<gift name='Dimensional Anchor' memrz='0' level='4' desc='Bars extradimensional movement.' />
+<gift name='Fire Trap' memrz='0' level='4' desc='Opened object deals 1d4 +1/level damage.' />
+<gift name='Minor Globe of Invulnerability' memrz='0' level='4' desc='Stops 1st- through 3rd-level gift effects.' />
+<gift name='Remove Curse' memrz='0' level='4' desc='Frees object or person from curse.' />
+<gift name='Stoneskin' memrz='0' level='4' desc='Stops blows, cuts, stabs, and slashes.' />
+<gift name='Evards Black Tentacles' memrz='0' level='4' desc='1d4 +1/level tentacles grapple randomly within 15 ft.' />
+<gift name='Leomunds Secure Shelter' memrz='0' level='4' desc='Creates sturdy cottage.' />
+<gift name='Minor Creation' memrz='0' level='4' desc='Creates one cloth or wood object.' />
+<gift name='Solid Fog' memrz='0' level='4' desc='Blocks vision and slows movement.' />
+<gift name='Summon Monster IV' memrz='0' level='4' desc='Calls outsider to fight for you.' />
+<gift name='Arcane Eye' memrz='0' level='4' desc='Invisible floating eye moves 30 ft./round.' />
+<gift name='Detect Scrying' memrz='0' level='4' desc='Alerts you of magical eavesdropping.' />
+<gift name='Locate Creature' memrz='0' level='4' desc='Indicates direction to familiar creature.' />
+<gift name='Scrying' memrz='0' level='4' desc='Spies on subject from a distance.' />
+<gift name='Charm Monster' memrz='0' level='4' desc='Makes monster believe it is your ally.' />
+<gift name='Confusion' memrz='0' level='4' desc='Makes subject behave oddly for 1 round/level.' />
+<gift name='Emotion' memrz='0' level='4' desc='Arouses strong emotion in subject.' />
+<gift name='Lesser Geas' memrz='0' level='4' desc='Commands subject of 7 HD or less.' />
+<gift name='Fire Shield' memrz='0' level='4' desc='Creatures attacking you take fire damage; you are protected from heat or cold.' />
+<gift name='Ice Storm' memrz='0' level='4' desc='Hail deals 5d6 damage in cylinder 40 ft. across.' />
+<gift name='Otilukes Resilient Sphere' memrz='0' level='4' desc='Force globe protects but traps one subject.' />
+<gift name='Shout' memrz='0' level='4' desc='Deafens all within cone and deals 2d6 damage.' />
+<gift name='Wall of Fire' memrz='0' level='4' desc='Deals 2d4 fire damage out to 10 ft. and 1d4 out to 20 ft.' />
+<gift name='Wall of Ice' memrz='0' level='4' desc='Ice plane creates wall with 15 hp +1/level, or hemisphere can trap creatures inside.' />
+<gift name='Hallucinatory Terrain' memrz='0' level='4' desc='Makes one type of terrain appear like another (field into forest, etc).' />
+<gift name='Illusory Wall' memrz='0' level='4' desc='Wall, floor, or ceiling looks real, but anything can pass through.' />
+<gift name='Improved Invisibility' memrz='0' level='4' desc='As invisibility, but subject can attack and stay invisible.' />
+<gift name='Phantasmal Killer' memrz='0' level='4' desc='Fearsome illusion kills subject or deals 3d6 damage.' />
+<gift name='Rainbow Pattern' memrz='0' level='4' desc='Lights prevent 24 HD of creatures from attacking or moving away.' />
+<gift name='Shadow Conjuration' memrz='0' level='4' desc='Mimics conjuring below 4th level.' />
+<gift name='Contagion' memrz='0' level='4' desc='Infects subject with chosen disease.' />
+<gift name='Enervation' memrz='0' level='4' desc='Subject gains 1d4 negative levels.' />
+<gift name='Fear' memrz='0' level='4' desc='Subjects within cone flee for 1 round/level.' />
+<gift name='Bestow Curse' memrz='0' level='4' desc='-6 to an ability; -4 on attacks, saves, and checks; or 50% chance of losing each action.' />
+<gift name='Dimension Door' memrz='0' level='4' desc='Teleports you and up to 500 lb.' />
+<gift name='Polymorph Other' memrz='0' level='4' desc='Gives one subject a new form.' />
+<gift name='Polymorph Self' memrz='0' level='4' desc='You assume a new form.' />
+<gift name='Rarys Mnemonic Enhancer' memrz='0' level='4' desc='Prepares extra gifts or retains one just cast. Wizard only.' />
+<gift name='Dismissal' memrz='0' level='5' desc='Forces a creature to return to native plane.' />
+<gift name='Cloudkill' memrz='0' level='5' desc='Kills 3 HD or less; 4-6 HD save or die.' />
+<gift name='Leomunds Secret Chest' memrz='0' level='5' desc='Hides expensive chest on Ethereal Plane; you retrieve it at will.' />
+<gift name='Lesser Planar Binding' memrz='0' level='5' desc='Traps outsider until it performs a task.' />
+<gift name='Major Creation' memrz='0' level='5' desc='As minor creation, plus stone and metal.' />
+<gift name='Mordenkainens Faithful Hound' memrz='0' level='5' desc='Phantom dog can guard, attack.' />
+<gift name='Summon Monster V' memrz='0' level='5' desc='Calls outsider to fight for you.' />
+<gift name='Wall of Iron' memrz='0' level='5' desc='30 hp/four levels; can topple onto foes.' />
+<gift name='Wall of Stone' memrz='0' level='5' desc='20 hp/four levels; can be shaped.' />
+<gift name='Contact Other Plane' memrz='0' level='5' desc='Ask question of extraplanar entity.' />
+<gift name='Prying Eyes' memrz='0' level='5' desc='1d4 floating eyes +1/level scout for you.' />
+<gift name='Rarys Telepathic Bond' memrz='0' level='5' desc='Link lets allies communicate.' />
+<gift name='Dominate Person' memrz='0' level='5' desc='Controls humanoid telepathically.' />
+<gift name='Feeblemind' memrz='0' level='5' desc='Subjects Int drops to 1.' />
+<gift name='Hold Monster' memrz='0' level='5' desc='As hold person, but any creature.' />
+<gift name='Mind Fog' memrz='0' level='5' desc='Subjects in fog get -10 Wis, Will checks.' />
+<gift name='Bigbys Interposing Hand' memrz='0' level='5' desc='Hand provides 90% cover against one opponent.' />
+<gift name='Cone of Cold' memrz='0' level='5' desc='1d6 cold damage/level.' />
+<gift name='Sending' memrz='0' level='5' desc='Delivers short message anywhere, instantly.' />
+<gift name='Wall of Force' memrz='0' level='5' desc='Wall is immune to damage.' />
+<gift name='Dream' memrz='0' level='5' desc='Sends message to anyone sleeping.' />
+<gift name='False Vision' memrz='0' level='5' desc='Fools scrying with an illusion.' />
+<gift name='Greater Shadow Conjuration' memrz='0' level='5' desc='As shadow conjuration, but up to 4th level and 40% real.' />
+<gift name='Mirage Arcana' memrz='0' level='5' desc='As hallucinatory terrain, plus structures.' />
+<gift name='Nightmare' memrz='0' level='5' desc='Sends vision dealing 1d10 damage, fatigue.' />
+<gift name='Persistent Image' memrz='0' level='5' desc='As major image, but no concentration required.' />
+<gift name='Seeming' memrz='0' level='5' desc='Changes appearance of one person/two levels.' />
+<gift name='Shadow Evocation' memrz='0' level='5' desc='Mimics evocation less than 5th level.' />
+<gift name='Animate Dead' memrz='0' level='5' desc='Creates undead skeletons and zombies.' />
+<gift name='Magic Jar' memrz='0' level='5' desc='Enables possession of another creature.' />
+<gift name='Animal Growth' memrz='0' level='5' desc='One animal/two levels doubles in size, HD.' />
+<gift name='Fabricate' memrz='0' level='5' desc='Transforms raw materials into finished items.' />
+<gift name='Passwall' memrz='0' level='5' desc='Breaches walls 1 ft. thick/level.' />
+<gift name='Stone Shape' memrz='0' level='5' desc='Sculpts stone into any form.' />
+<gift name='Telekinesis' memrz='0' level='5' desc='Lifts or moves 25 lb./level at long range.' />
+<gift name='Teleport' memrz='0' level='5' desc='Instantly transports you anywhere.' />
+<gift name='Transmute Mud to Rock' memrz='0' level='5' desc='Transforms two 10-ft. cubes/level.' />
+<gift name='Transmute Rock to Mud' memrz='0' level='5' desc='Transforms two 10-ft. cubes/level.' />
+<gift name='Permanency' memrz='0' level='5' desc='Makes certain gifts permanent; costs XP.' />
+<gift name='Antimagic Field' memrz='0' level='6' desc='Negates magic within 10 ft.' />
+<gift name='Globe of Invulnerability' memrz='0' level='6' desc='As minor globe, plus 4th level.' />
+<gift name='Greater Digifting' memrz='0' level='6' desc='As dispel magic, but +20 on check.' />
+<gift name='Guards and Wards' memrz='0' level='6' desc='Array of magic effects protect area.' />
+<gift name='Repulsion' memrz='0' level='6' desc='Creatures cant approach you.' />
+<gift name='Acid Fog' memrz='0' level='6' desc='Fog deals acid damage.' />
+<gift name='Planar Binding' memrz='0' level='6' desc='As lesser planar binding, but up to 16 HD.' />
+<gift name='Summon Monster VI' memrz='0' level='6' desc='Calls outsider to fight for you.' />
+<gift name='Analyze Dweomer' memrz='0' level='6' desc='Reveals magical aspects of subject.' />
+<gift name='Legend Lore' memrz='0' level='6' desc='Learn tales about a person, place, or thing.' />
+<gift name='True Seeing' memrz='0' level='6' desc='See all things as they really are.' />
+<gift name='Geas/Quest' memrz='0' level='6' desc='As lesser geas, plus it affects any creature.' />
+<gift name='Mass Suggestion' memrz='0' level='6' desc='As suggestion, plus one/level subjects.' />
+<gift name='Bigbys Forceful Hand' memrz='0' level='6' desc='Hand pushes creatures away.' />
+<gift name='Chain Lightning' memrz='0' level='6' desc='1d6 damage/level; secondary bolts.' />
+<gift name='Contingency' memrz='0' level='6' desc='Sets trigger condition for another gift.' />
+<gift name='Otilukes Freezing Sphere' memrz='0' level='6' desc='Freezes water or deals cold damage.' />
+<gift name='Greater Shadow Evocation' memrz='0' level='6' desc='As shadow evocation, but up to 5th level.' />
+<gift name='Mislead' memrz='0' level='6' desc='Turns you invisible and creates illusory double.' />
+<gift name='Permanent Image' memrz='0' level='6' desc='Includes sight, sound, and smell.' />
+<gift name='Programmed Image' memrz='0' level='6' desc='As major image, plus triggered by event.' />
+<gift name='Project Image' memrz='0' level='6' desc='Illusory double can talk and cast gifts.' />
+<gift name='Shades' memrz='0' level='6' desc='As shadow conjuration, but up to 5th level and 60% real.' />
+<gift name='Veil' memrz='0' level='6' desc='Changes appearance of group of creatures.' />
+<gift name='Circle of Death' memrz='0' level='6' desc='Kills 1d4 HD/level.' />
+<gift name='Control Water' memrz='0' level='6' desc='Raises, lowers, or parts bodies of water.' />
+<gift name='Control Weather' memrz='0' level='6' desc='Changes weather in local area.' />
+<gift name='Disintegrate' memrz='0' level='6' desc='Makes one creature or object vanish.' />
+<gift name='Eyebite' memrz='0' level='6' desc='Charm, fear, sicken or sleep one subject.' />
+<gift name='Flesh to Stone' memrz='0' level='6' desc='Turns subject creature into statue.' />
+<gift name='Mass Haste' memrz='0' level='6' desc='As haste, affects one/level subjects.' />
+<gift name='Mordenkainens Lucubration' memrz='0' level='6' desc='Recalls gift of 5th level or less. Wizard Only.' />
+<gift name='Move Earth' memrz='0' level='6' desc='Digs trenches and build hills.' />
+<gift name='Stone to Flesh' memrz='0' level='6' desc='Restores petrified creature.' />
+<gift name='Tensers Transformation' memrz='0' level='6' desc='You gain combat bonuses.' />
+<gift name='Banishment' memrz='0' level='7' desc='Banishes 2 HD/level extraplanar creatures.' />
+<gift name='Sequester' memrz='0' level='7' desc='Subject is invisible to sight and scrying.' />
+<gift name='gift Turning' memrz='0' level='7' desc='Reflect 1d4+6 gift levels back at caster.' />
+<gift name='Drawmijs Instant Summons' memrz='0' level='7' desc='Prepared object appears in your hand.' />
+<gift name='Mordenkainens Magnificent Mansion' memrz='0' level='7' desc='Door leads to extradimensional mansion.' />
+<gift name='Phase Door' memrz='0' level='7' desc='Invisible passage through wood or stone.' />
+<gift name='Power Word, Stun' memrz='0' level='7' desc='Stuns creature with up to 150 hp.' />
+<gift name='Summon Monster VII' memrz='0' level='7' desc='Calls outsider to fight for you.' />
+<gift name='Greater Scrying' memrz='0' level='7' desc='As scrying, but faster and longer.' />
+<gift name='Vision' memrz='0' level='7' desc='As legend lore, but quicker and strenuous.' />
+<gift name='Insanity' memrz='0' level='7' desc='Subject suffers continuous confusion.' />
+<gift name='Bigbys Grasping Hand' memrz='0' level='7' desc='Hand provides cover, pushes, or grapples.' />
+<gift name='Delayed Blast Fireball' memrz='0' level='7' desc='1d8 fire damage/level; you can delay blast for 5 rounds.' />
+<gift name='Forcecage' memrz='0' level='7' desc='Cube of force imprisons all inside.' />
+<gift name='Mordenkainens Sword' memrz='0' level='7' desc='Floating magic blade strikes opponents.' />
+<gift name='Prismatic Spray' memrz='0' level='7' desc='Rays hit subjects with variety of effects.' />
+<gift name='Mass Invisibility' memrz='0' level='7' desc='As invisibility, but affects all in range.' />
+<gift name='Shadow Walk' memrz='0' level='7' desc='Step into shadow to travel rapidly.' />
+<gift name='Simulacrum' memrz='0' level='7' desc='Creates partially real double of a creature.' />
+<gift name='Control Undead' memrz='0' level='7' desc='Undead dont attack you while under your command.' />
+<gift name='Finger of Death' memrz='0' level='7' desc='Kills one subject.' />
+<gift name='Ethereal Jaunt' memrz='0' level='7' desc='You become ethereal for 1 round/level.' />
+<gift name='Plane Shift' memrz='0' level='7' desc='Up to eight subjects travel to another plane.' />
+<gift name='Reverse Gravity' memrz='0' level='7' desc='Objects and creatures fall upward.' />
+<gift name='Statue' memrz='0' level='7' desc='Subject can become a statue at will.' />
+<gift name='Teleport without Error' memrz='0' level='7' desc='As teleport, but no off-target arrival.' />
+<gift name='Vanish' memrz='0' level='7' desc='As teleport, but affects a touched object.' />
+<gift name='Limited Wish' memrz='0' level='7' desc='Alters reality-within gift limits.' />
+<gift name='Mind Blank' memrz='0' level='8' desc='Subject is immune to mental/emotional magic and scrying.' />
+<gift name='Prismatic Wall' memrz='0' level='8' desc='Walls colors have array of effects.' />
+<gift name='Protection from gifts' memrz='0' level='8' desc='Confers +8 resistance bonus.' />
+<gift name='Greater Planar Binding' memrz='0' level='8' desc='As lesser planar binding, but up to 24 HD.' />
+<gift name='Incendiary Cloud' memrz='0' level='8' desc='Cloud deals 4d6 fire damage/round.' />
+<gift name='Maze' memrz='0' level='8' desc='Traps subject in extradimensional maze.' />
+<gift name='Power Word, Blind' memrz='0' level='8' desc='Blinds 200 hp worth of creatures.' />
+<gift name='Summon Monster VIII' memrz='0' level='8' desc='Calls outsider to fight for you.' />
+<gift name='Trap the Soul' memrz='0' level='8' desc='Imprisons subject within gem.' />
+<gift name='Discern Location' memrz='0' level='8' desc='Exact location of creature or object.' />
+<gift name='Antipathy' memrz='0' level='8' desc='Object or location affected by gift repels certain creatures.' />
+<gift name='Binding' memrz='0' level='8' desc='Array of techniques to imprison a creature.' />
+<gift name='Demand' memrz='0' level='8' desc='As sending, plus you can send suggestion.' />
+<gift name='Mass Charm' memrz='0' level='8' desc='As charm monster, but all within 30 ft.' />
+<gift name='Ottos Irresistible Dance' memrz='0' level='8' desc='Forces subject to dance.' />
+<gift name='Sympathy' memrz='0' level='8' desc='Object or location attracts certain creatures.' />
+<gift name='Bigbys Clenched Fist' memrz='0' level='8' desc='Large hand attacks your foes.' />
+<gift name='Otilukes Telekinetic Sphere' memrz='0' level='8' desc='As Otilukes resilient sphere, but you move sphere telekinetically.' />
+<gift name='Sunburst' memrz='0' level='8' desc='Blinds all within 10 ft., deals 3d6 damage.' />
+<gift name='Screen' memrz='0' level='8' desc='Illusion hides area from vision, scrying.' />
+<gift name='Clone' memrz='0' level='8' desc='Duplicate awakens when original dies.' />
+<gift name='Horrid Wilting' memrz='0' level='8' desc='Deals 1d8 damage/level within 30 ft.' />
+<gift name='Etherealness' memrz='0' level='8' desc='Travel to Ethereal Plane with companions.' />
+<gift name='Iron Body' memrz='0' level='8' desc='Your body becomes living iron.' />
+<gift name='Polymorph Any Object' memrz='0' level='8' desc='Changes any subject into anything else.' />
+<gift name='Symbol' memrz='0' level='8' desc='Triggered runes have array of effects.' />
+<gift name='Freedom' memrz='0' level='9' desc='Releases creature suffering imprisonment.' />
+<gift name='Imprisonment' memrz='0' level='9' desc='Entombs subject beneath the earth.' />
+<gift name='Mordenkainens Disjunction' memrz='0' level='9' desc='Dispels magic, disenchants magic items.' />
+<gift name='Prismatic Sphere' memrz='0' level='9' desc='As prismatic wall, but surrounds on all sides.' />
+<gift name='Gate' memrz='0' level='9' desc='Connects two planes for travel or summoning.' />
+<gift name='Power Word, Kill' memrz='0' level='9' desc='Kills one tough subject or many weak ones.' />
+<gift name='Summon Monster IX' memrz='0' level='9' desc='Calls outsider to fight for you.' />
+<gift name='Foresight' memrz='0' level='9' desc='Sixth sense warns of impending danger.' />
+<gift name='Dominate Monster' memrz='0' level='9' desc='As dominate person, but any creature.' />
+<gift name='Bigbys Crushing Hand' memrz='0' level='9' desc='As Bigbys interposing hand, but stronger.' />
+<gift name='Meteor Swarm' memrz='0' level='9' desc='Deals 24d6 fire damage, plus bursts.' />
+<gift name='Weird' memrz='0' level='9' desc='As phantasmal killer, but affects all within 30 ft.' />
+<gift name='Astral Projection' memrz='0' level='9' desc='Projects you and companions into Astral Plane.' />
+<gift name='Energy Drain' memrz='0' level='9' desc='Subject gains 2d4 negative levels.' />
+<gift name='Soul Bind' memrz='0' level='9' desc='Traps newly dead soul to prevent resurrection.' />
+<gift name='Wail of the Banshee' memrz='0' level='9' desc='Kills one creature/level.' />
+<gift name='Refuge' memrz='0' level='9' desc='Alters item to transport its possessor to you.' />
+<gift name='Shapechange' memrz='0' level='9' desc='Transforms you into any creature, and change forms once per round.' />
+<gift name='Teleportation Circle' memrz='0' level='9' desc='Circle teleports any creature inside to designated spot.' />
+<gift name='Temporal Stasis' memrz='0' level='9' desc='Puts subject into suspended animation.' />
+<gift name='Time Stop' memrz='0' level='9' desc='You act freely for 1d4+1 rounds.' />
+<gift name='Wish' memrz='0' level='9' desc='As limited wish, but with fewer limits.' />
+</divine>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/d20/d20feats.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,76 @@
+<feats>
+    <feat name='Blank' type='None' />
+    <feat name='Alertness' type='General' />
+    <feat name='Ambidexterity' type='General' />
+    <feat name='Armor Proficiency (heavy)' type='General' />
+    <feat name='Armor Proficiency (light)' type='General' />
+    <feat name='Armor Proficiency (medium)' type='General' />
+    <feat name='Blind-Fight' type='General' />
+    <feat name='Cleave' type='General' />
+    <feat name='Combat Reflexes' type='General' />
+    <feat name='Deflect Arrows' type='General' />
+    <feat name='Dodge' type='General' />
+    <feat name='Endurance' type='General' />
+    <feat name='Exotic Weapon Proficiency' type='General' />
+    <feat name='Expertise' type='General' />
+    <feat name='Point Blank Shot.' type='General' />
+    <feat name='Great Cleave' type='General' />
+    <feat name='Great Fortitude' type='General' />
+    <feat name='Improved Bull Rush' type='General' />
+    <feat name='Improved Critical' type='General' />
+    <feat name='Improved Disarm' type='General' />
+    <feat name='Improved Initiative' type='General' mod='4' />
+    <feat name='Improved Trip' type='General' />
+    <feat name='Improved Two-Weapon Fighting' type='General' />
+    <feat name='Improved Unarmed Strike' type='General' />
+    <feat name='Iron Will' type='General' />
+    <feat name='Leadership' type='General' />
+    <feat name='Lightning Reflexes' type='General' />
+    <feat name='Martial Weapon Proficiency' type='General' />
+    <feat name='Mobility' type='General' />
+    <feat name='Mounted Archery' type='General' />
+    <feat name='Mounted Combat' type='General' />
+    <feat name='Point Blank Shot' type='General' />
+    <feat name='Power Attack' type='General' />
+    <feat name='Precise Shot' type='General' />
+    <feat name='Quick Draw' type='General' />
+    <feat name='Rapid Shot' type='General' />
+    <feat name='Ride-By Attack' type='General' />
+    <feat name='Run' type='General' />
+    <feat name='Shield Proficiency' type='General' />
+    <feat name='Shot on the Run' type='General' />
+    <feat name='Simple Weapon Proficiency' type='General' />
+    <feat name='Skill Focus' type='General' />
+    <feat name='Spirited Charge' type='General' />
+    <feat name='Spring Attack' type='General' />
+    <feat name='Stunning Fist' type='General' />
+    <feat name='Sunder' type='General' />
+    <feat name='Toughness' type='General' />
+    <feat name='Track' type='General' />
+    <feat name='Trample' type='General' />
+    <feat name='Two-Weapon Fighting' type='General' />
+    <feat name='Weapon Finesse' type='General' />
+    <feat name='Weapon Focus' type='General' />
+    <feat name='Weapon Specialization' type='General' />
+    <feat name='Brew Potion' type='General' />
+    <feat name='Combat Casting' type='General' />
+    <feat name='Craft Magic Arms and Armor' type='General' />
+    <feat name='Craft Rod' type='General' />
+    <feat name='Craft Staff' type='General' />
+    <feat name='Craft Wand' type='General' />
+    <feat name='Craft Wondrous Item' type='General' />
+    <feat name='Empower Spell' type='General' />
+    <feat name='Enlarge Spell' type='General' />
+    <feat name='Extend Spell' type='General' />
+    <feat name='Extra Turning' type='General' />
+    <feat name='Forge Ring' type='General' />
+    <feat name='Heighten Spell' type='General' />
+    <feat name='Maximize Spell' type='General' />
+    <feat name='Quicken Spell' type='General' />
+    <feat name='Scribe Scroll' type='General' />
+    <feat name='Silent Spell' type='General' />
+    <feat name='Spell Focus' type='General' />
+    <feat name='Spell Mastery' type='General' />
+    <feat name='Spell Penetration' type='General' />
+    <feat name='Still Spell' type='General' />
+</feats>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/d20/d20powers.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,121 @@
+<powers>
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 0 test' level='0' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='1' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='2' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='3' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='Level 1 test' level='4' desc='Testing this' point='1' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+<power test='metacreativity' name='test' level='9' desc='Testing this' point='3' />
+</powers>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/d20/d20spells.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,326 @@
+<spells>
+<spell name='Resistance' memrz='0' level='0' desc='Subject gains +1 on saving throws.' />
+<spell name='Ray of Frost' memrz='0' level='0' desc='Ray deals 1d3 cold damage.' />
+<spell name='Detect Poison' memrz='0' level='0' desc='Detects poison in one creature or small object.' />
+<spell name='Daze' memrz='0' level='0' desc='Creature loses next action.' />
+<spell name='Flare' memrz='0' level='0' desc='Dazzles one creature (-1 attack).' />
+<spell name='Light' memrz='0' level='0' desc='Object shines like a torch.' />
+<spell name='Dancing Lights' memrz='0' level='0' desc='Figment torches or other lights.' />
+<spell name='Ghost Sound' memrz='0' level='0' desc='Figment sounds.' />
+<spell name='Disrupt Undead' memrz='0' level='0' desc='Deals 1d6 damage to one undead.' />
+<spell name='Mage Hand' memrz='0' level='0' desc='5-pound telekinesis.' />
+<spell name='Mending' memrz='0' level='0' desc='Makes minor repairs on an object.' />
+<spell name='Open/Close' memrz='0' level='0' desc='Opens or closes small or light things.' />
+<spell name='Arcane Mark' memrz='0' level='0' desc='Inscribes a personal rune (visible or invisible).' />
+<spell name='Detect Magic' memrz='0' level='0' desc='Detects spells and magic items within 60 ft.' />
+<spell name='Prestidigitation' memrz='0' level='0' desc='Performs minor tricks.' />
+<spell name='Read Magic' memrz='0' level='0' desc='Read scrolls and spellbooks.' />
+<spell name='Alarm' memrz='0' level='1' desc='Wards an area for 2 hours/level.' />
+<spell name='Endure Elements' memrz='0' level='1' desc='Ignores 5 damage/round from one energy type.' />
+<spell name='Hold Portal' memrz='0' level='1' desc='Holds door shut.' />
+<spell name='Protection from Chaos/Evil/Good/Law' memrz='0' level='1' desc='+2 AC and saves, counter mind control, hedge out elementals and outsiders.' />
+<spell name='Shield' memrz='0' level='1' desc='Invisible disc gives cover and blocks magic missiles.' />
+<spell name='Grease' memrz='0' level='1' desc='Makes 10-ft. square or one object slippery.' />
+<spell name='Mage Armor' memrz='0' level='1' desc='Gives subject +4 armor bonus.' />
+<spell name='Mount' memrz='0' level='1' desc='Summons riding horse for 2 hr./level.' />
+<spell name='Obscuring Mist' memrz='0' level='1' desc='Fog surrounds you.' />
+<spell name='Summon Monster I' memrz='0' level='1' desc='Calls outsider to fight for you.' />
+<spell name='Unseen Servant' memrz='0' level='1' desc='Creates invisible force that obeys your commands.' />
+<spell name='Comprehend Languages' memrz='0' level='1' desc='Understands all spoken and written languages.' />
+<spell name='Detect Secret Doors' memrz='0' level='1' desc='Reveals hidden doors within 60 ft.' />
+<spell name='Detect Undead' memrz='0' level='1' desc='Reveals undead within 60 ft.' />
+<spell name='Identify' memrz='0' level='1' desc='Determines single feature of magic item.' />
+<spell name='True Strike' memrz='0' level='1' desc='Adds +20 bonus to your next attack roll.' />
+<spell name='Charm Person' memrz='0' level='1' desc='Makes one person your friend.' />
+<spell name='Hypnotism' memrz='0' level='1' desc='Fascinates 2d4 HD of creatures.' />
+<spell name='Sleep' memrz='0' level='1' desc='Put 2d4 HD of creatures into comatose slumber.' />
+<spell name='Magic Missile' memrz='0' level='1' desc='1d4+1 damage; +1 missile/two levels above 1st (max +5).' />
+<spell name='Tensers Floating Disk' memrz='0' level='1' desc='3-ft.-diameter horizontal disk that holds 100 lb./level.' />
+<spell name='Change Self' memrz='0' level='1' desc='Changes your appearance.' />
+<spell name='Color Spray' memrz='0' level='1' desc='Knocks unconscious, blinds, or stuns 1d6 weak creatures.' />
+<spell name='Nystuls Magical Aura' memrz='0' level='1' desc='Grants object false magic aura.' />
+<spell name='Nystuls Undetectable Aura' memrz='0' level='1' desc='Masks magic items aura.' />
+<spell name='Silent Image' memrz='0' level='1' desc='Creates minor illusion of your design.' />
+<spell name='Ventriloquism' memrz='0' level='1' desc='Throws voice for 1 min./level.' />
+<spell name='Cause Fear' memrz='0' level='1' desc='One creature flees for 1d4 rounds.' />
+<spell name='Chill Touch' memrz='0' level='1' desc='1 touch/level deals 1d6 damage and possibly 1 Str damage.' />
+<spell name='Ray of Enfeeblement' memrz='0' level='1' desc='Ray reduces Str by 1d6 points +1 point/two levels.' />
+<spell name='Animate Rope' memrz='0' level='1' desc='Makes a rope move at your command.' />
+<spell name='Burning Hands' memrz='0' level='1' desc='1d4 fire damage/level (max: 5d4).' />
+<spell name='Enlarge' memrz='0' level='1' desc='Object or creature grows +10%/level (max +50%).' />
+<spell name='Erase' memrz='0' level='1' desc='Mundane or magical writing vanishes.' />
+<spell name='Expeditious Retreat' memrz='0' level='1' desc='Doubles your speed.' />
+<spell name='Feather Fall' memrz='0' level='1' desc='Objects or creatures fall slowly.' />
+<spell name='Jump' memrz='0' level='1' desc='Subject gets +30 on Jump checks.' />
+<spell name='Magic Weapon' memrz='0' level='1' desc='Weapon gains +1 bonus.' />
+<spell name='Message' memrz='0' level='1' desc='Whispered conversation at distance.' />
+<spell name='Reduce' memrz='0' level='1' desc='Object or creature shrinks 10%/level (max 50%).' />
+<spell name='Shocking Grasp' memrz='0' level='1' desc='Touch delivers 1d8 +1/level electricity.' />
+<spell name='Spider Climb' memrz='0' level='1' desc='Grants ability to walk on walls and ceilings.' />
+<spell name='Arcane Lock' memrz='0' level='2' desc='Magically locks a portal or chest.' />
+<spell name='Obscure Object' memrz='0' level='2' desc='Masks object against divination.' />
+<spell name='Protection from Arrows' memrz='0' level='2' desc='Subject immune to most ranged attacks.' />
+<spell name='Resist Elements' memrz='0' level='2' desc='Ignores 12 damage/round from one energy type.' />
+<spell name='Fog Cloud' memrz='0' level='2' desc='Fog obscures vision.' />
+<spell name='Glitterdust' memrz='0' level='2' desc='Blinds creatures, outlines invisible creatures.' />
+<spell name='Melfs Acid Arrow' memrz='0' level='2' desc='Ranged touch attack; 2d4 damage for 1 round + 1 round/three levels.' />
+<spell name='Summon Monster II' memrz='0' level='2' desc='Calls outsider to fight for you.' />
+<spell name='Summon Swarm' memrz='0' level='2' desc='Summons swarm of small crawling or flying creatures.' />
+<spell name='Web' memrz='0' level='2' desc='Fills 10-ft. cube/level with sticky spider webs.' />
+<spell name='Detect Thoughts' memrz='0' level='2' desc='Allows listening to surface thoughts.' />
+<spell name='Locate Object' memrz='0' level='2' desc='Senses direction toward object (specific or type).' />
+<spell name='See Invisibility' memrz='0' level='2' desc='Reveals invisible creatures or objects.' />
+<spell name='Tashas Hideous Laughter' memrz='0' level='2' desc='Subject loses actions for 1d3 rounds.' />
+<spell name='Darkness' memrz='0' level='2' desc='20-ft. radius of supernatural darkness.' />
+<spell name='Daylight' memrz='0' level='2' desc='60-ft. radius of bright light.' />
+<spell name='Flaming Sphere' memrz='0' level='2' desc='Rolling ball of fire, 2d6 damage, lasts 1 round/level.' />
+<spell name='Shatter' memrz='0' level='2' desc='Sonic vibration damages objects or crystalline creatures.' />
+<spell name='Blur' memrz='0' level='2' desc='Attacks miss subject 20% of the time.' />
+<spell name='Continual Flame' memrz='0' level='2' desc='Makes a permanent, heatless torch.' />
+<spell name='Hypnotic Pattern' memrz='0' level='2' desc='Fascinates 2d4+1 HD/level of creatures.' />
+<spell name='Invisibility' memrz='0' level='2' desc='Subject is invisible for 10 min./level or until it attacks.' />
+<spell name='Leomunds Trap' memrz='0' level='2' desc='Makes item seem trapped.' />
+<spell name='Magic Mouth' memrz='0' level='2' desc='Speaks once when triggered.' />
+<spell name='Minor Image' memrz='0' level='2' desc='As silent image, plus some sound.' />
+<spell name='Mirror Image' memrz='0' level='2' desc='Creates decoy duplicates of you (1d4 +1/three levels, max 8).' />
+<spell name='Misdirection' memrz='0' level='2' desc='Misleads divinations for one creature or object.' />
+<spell name='Ghoul Touch' memrz='0' level='2' desc='Paralyzes one subject, who exudes stench (-2 penalty) nearby.' />
+<spell name='Scare' memrz='0' level='2' desc='Panics creatures up to 5 HD (15-ft. radius).' />
+<spell name='Spectral Hand' memrz='0' level='2' desc='Creates disembodied glowing hand to deliver touch attacks.' />
+<spell name='Alter Self' memrz='0' level='2' desc='As change self, plus more drastic changes.' />
+<spell name='Blindness/Deafness' memrz='0' level='2' desc='Makes subject blind or deaf.' />
+<spell name='Bulls Strength' memrz='0' level='2' desc='Subject gains 1d4+1 Str for 1 hr./level.' />
+<spell name='Cats Grace' memrz='0' level='2' desc='Subject gains 1d4+1 Dex for 1 hr./level.' />
+<spell name='Darkvision' memrz='0' level='2' desc='See 60 ft. in total darkness.' />
+<spell name='Endurance' memrz='0' level='2' desc='Gain 1d4+1 Con for 1 hr./level.' />
+<spell name='Knock' memrz='0' level='2' desc='Opens locked or magically sealed door.' />
+<spell name='Levitate' memrz='0' level='2' desc='Subject moves up and down at your direction.' />
+<spell name='Pyrotechnics' memrz='0' level='2' desc='Turns fire into blinding light or choking smoke.' />
+<spell name='Rope Trick' memrz='0' level='2' desc='Up to eight creatures hide in extradimensional space.' />
+<spell name='Whispering Wind' memrz='0' level='2' desc='Sends a short message one mile/level.' />
+<spell name='Dispel Magic' memrz='0' level='3' desc='Cancels magical spells and effects.' />
+<spell name='Explosive Runes' memrz='0' level='3' desc='Deals 6d6 damage when read.' />
+<spell name='Magic Circle against Chaos/Evil/Good/Law' memrz='0' level='3' desc='As protection spells, but 10-ft. radius and 10 min./level.' />
+<spell name='Nondetection' memrz='0' level='3' desc='Hides subject from divination, scrying.' />
+<spell name='Protection from Elements' memrz='0' level='3' desc='Absorb 12 damage/level from one kind of energy.' />
+<spell name='Flame Arrow' memrz='0' level='3' desc='Shoots flaming projectiles (extra damage) or fiery bolts (4d6 damage).' />
+<spell name='Phantom Steed' memrz='0' level='3' desc='Magical horse appears for 1 hour/level.' />
+<spell name='Sepia Snake Sigil' memrz='0' level='3' desc='Creates text symbol that immobilizes reader.' />
+<spell name='Sleet Storm' memrz='0' level='3' desc='Hampers vision and movement.' />
+<spell name='Stinking Cloud' memrz='0' level='3' desc='Nauseating vapors, 1 round/level.' />
+<spell name='Summon Monster III' memrz='0' level='3' desc='Calls outsider to fight for you.' />
+<spell name='Clairaudience/Clairvoyance' memrz='0' level='3' desc='Hear or see at a distance for 1 min./level.' />
+<spell name='Tongues' memrz='0' level='3' desc='Speak any language.' />
+<spell name='Hold Person' memrz='0' level='3' desc='Holds one person helpless; 1 round/level.' />
+<spell name='Suggestion' memrz='0' level='3' desc='Compels subject to follow stated course of action.' />
+<spell name='Fireball' memrz='0' level='3' desc='1d6 damage per level, 20-ft. radius.' />
+<spell name='Gust of Wind' memrz='0' level='3' desc='Blows away or knocks down smaller creatures.' />
+<spell name='Leomunds Tiny Hut' memrz='0' level='3' desc='Creates shelter for 10 creatures.' />
+<spell name='Lightning Bolt' memrz='0' level='3' desc='Electricity deals 1d6 damage/level.' />
+<spell name='Wind Wall' memrz='0' level='3' desc='Deflects arrows, smaller creatures, and gases.' />
+<spell name='Displacement' memrz='0' level='3' desc='Attacks miss subject 50%.' />
+<spell name='Illusory Script' memrz='0' level='3' desc='Only intended reader can decipher.' />
+<spell name='Invisibility Sphere' memrz='0' level='3' desc='Makes everyone within 10 ft. invisible.' />
+<spell name='Major Image' memrz='0' level='3' desc='As silent image, plus sound, smell and thermal effects.' />
+<spell name='Gentle Repose' memrz='0' level='3' desc='Preserves one corpse.' />
+<spell name='Halt Undead' memrz='0' level='3' desc='Immobilizes undead for 1 round/level.' />
+<spell name='Vampiric Touch' memrz='0' level='3' desc='Touch deals 1d6/two caster levels; caster gains damage as hp.' />
+<spell name='Blink' memrz='0' level='3' desc='You randomly vanish and reappear for 1 round/level.' />
+<spell name='Fly' memrz='0' level='3' desc='Subject flies at speed of 90.' />
+<spell name='Gaseous Form' memrz='0' level='3' desc='Subject becomes insubstantial and can fly slowly.' />
+<spell name='Greater Magic Weapon' memrz='0' level='3' desc='+1/three levels (max +5).' />
+<spell name='Haste' memrz='0' level='3' desc='Extra partial action and +4 AC.' />
+<spell name='Keen Edge' memrz='0' level='3' desc='Doubles normal weapons threat range.' />
+<spell name='Secret Page' memrz='0' level='3' desc='Changes one page to hide its real content.' />
+<spell name='Shrink Item' memrz='0' level='3' desc='Object shrinks to one-twelfth size.' />
+<spell name='Slow' memrz='0' level='3' desc='One subject/level takes only partial actions, -2 AC, -2 melee rolls.' />
+<spell name='Water Breathing' memrz='0' level='3' desc='Subjects can breathe underwater.' />
+<spell name='Dimensional Anchor' memrz='0' level='4' desc='Bars extradimensional movement.' />
+<spell name='Fire Trap' memrz='0' level='4' desc='Opened object deals 1d4 +1/level damage.' />
+<spell name='Minor Globe of Invulnerability' memrz='0' level='4' desc='Stops 1st- through 3rd-level spell effects.' />
+<spell name='Remove Curse' memrz='0' level='4' desc='Frees object or person from curse.' />
+<spell name='Stoneskin' memrz='0' level='4' desc='Stops blows, cuts, stabs, and slashes.' />
+<spell name='Evards Black Tentacles' memrz='0' level='4' desc='1d4 +1/level tentacles grapple randomly within 15 ft.' />
+<spell name='Leomunds Secure Shelter' memrz='0' level='4' desc='Creates sturdy cottage.' />
+<spell name='Minor Creation' memrz='0' level='4' desc='Creates one cloth or wood object.' />
+<spell name='Solid Fog' memrz='0' level='4' desc='Blocks vision and slows movement.' />
+<spell name='Summon Monster IV' memrz='0' level='4' desc='Calls outsider to fight for you.' />
+<spell name='Arcane Eye' memrz='0' level='4' desc='Invisible floating eye moves 30 ft./round.' />
+<spell name='Detect Scrying' memrz='0' level='4' desc='Alerts you of magical eavesdropping.' />
+<spell name='Locate Creature' memrz='0' level='4' desc='Indicates direction to familiar creature.' />
+<spell name='Scrying' memrz='0' level='4' desc='Spies on subject from a distance.' />
+<spell name='Charm Monster' memrz='0' level='4' desc='Makes monster believe it is your ally.' />
+<spell name='Confusion' memrz='0' level='4' desc='Makes subject behave oddly for 1 round/level.' />
+<spell name='Emotion' memrz='0' level='4' desc='Arouses strong emotion in subject.' />
+<spell name='Lesser Geas' memrz='0' level='4' desc='Commands subject of 7 HD or less.' />
+<spell name='Fire Shield' memrz='0' level='4' desc='Creatures attacking you take fire damage; you are protected from heat or cold.' />
+<spell name='Ice Storm' memrz='0' level='4' desc='Hail deals 5d6 damage in cylinder 40 ft. across.' />
+<spell name='Otilukes Resilient Sphere' memrz='0' level='4' desc='Force globe protects but traps one subject.' />
+<spell name='Shout' memrz='0' level='4' desc='Deafens all within cone and deals 2d6 damage.' />
+<spell name='Wall of Fire' memrz='0' level='4' desc='Deals 2d4 fire damage out to 10 ft. and 1d4 out to 20 ft.' />
+<spell name='Wall of Ice' memrz='0' level='4' desc='Ice plane creates wall with 15 hp +1/level, or hemisphere can trap creatures inside.' />
+<spell name='Hallucinatory Terrain' memrz='0' level='4' desc='Makes one type of terrain appear like another (field into forest, etc).' />
+<spell name='Illusory Wall' memrz='0' level='4' desc='Wall, floor, or ceiling looks real, but anything can pass through.' />
+<spell name='Improved Invisibility' memrz='0' level='4' desc='As invisibility, but subject can attack and stay invisible.' />
+<spell name='Phantasmal Killer' memrz='0' level='4' desc='Fearsome illusion kills subject or deals 3d6 damage.' />
+<spell name='Rainbow Pattern' memrz='0' level='4' desc='Lights prevent 24 HD of creatures from attacking or moving away.' />
+<spell name='Shadow Conjuration' memrz='0' level='4' desc='Mimics conjuring below 4th level.' />
+<spell name='Contagion' memrz='0' level='4' desc='Infects subject with chosen disease.' />
+<spell name='Enervation' memrz='0' level='4' desc='Subject gains 1d4 negative levels.' />
+<spell name='Fear' memrz='0' level='4' desc='Subjects within cone flee for 1 round/level.' />
+<spell name='Bestow Curse' memrz='0' level='4' desc='-6 to an ability; -4 on attacks, saves, and checks; or 50% chance of losing each action.' />
+<spell name='Dimension Door' memrz='0' level='4' desc='Teleports you and up to 500 lb.' />
+<spell name='Polymorph Other' memrz='0' level='4' desc='Gives one subject a new form.' />
+<spell name='Polymorph Self' memrz='0' level='4' desc='You assume a new form.' />
+<spell name='Rarys Mnemonic Enhancer' memrz='0' level='4' desc='Prepares extra spells or retains one just cast. Wizard only.' />
+<spell name='Dismissal' memrz='0' level='5' desc='Forces a creature to return to native plane.' />
+<spell name='Cloudkill' memrz='0' level='5' desc='Kills 3 HD or less; 4-6 HD save or die.' />
+<spell name='Leomunds Secret Chest' memrz='0' level='5' desc='Hides expensive chest on Ethereal Plane; you retrieve it at will.' />
+<spell name='Lesser Planar Binding' memrz='0' level='5' desc='Traps outsider until it performs a task.' />
+<spell name='Major Creation' memrz='0' level='5' desc='As minor creation, plus stone and metal.' />
+<spell name='Mordenkainens Faithful Hound' memrz='0' level='5' desc='Phantom dog can guard, attack.' />
+<spell name='Summon Monster V' memrz='0' level='5' desc='Calls outsider to fight for you.' />
+<spell name='Wall of Iron' memrz='0' level='5' desc='30 hp/four levels; can topple onto foes.' />
+<spell name='Wall of Stone' memrz='0' level='5' desc='20 hp/four levels; can be shaped.' />
+<spell name='Contact Other Plane' memrz='0' level='5' desc='Ask question of extraplanar entity.' />
+<spell name='Prying Eyes' memrz='0' level='5' desc='1d4 floating eyes +1/level scout for you.' />
+<spell name='Rarys Telepathic Bond' memrz='0' level='5' desc='Link lets allies communicate.' />
+<spell name='Dominate Person' memrz='0' level='5' desc='Controls humanoid telepathically.' />
+<spell name='Feeblemind' memrz='0' level='5' desc='Subjects Int drops to 1.' />
+<spell name='Hold Monster' memrz='0' level='5' desc='As hold person, but any creature.' />
+<spell name='Mind Fog' memrz='0' level='5' desc='Subjects in fog get -10 Wis, Will checks.' />
+<spell name='Bigbys Interposing Hand' memrz='0' level='5' desc='Hand provides 90% cover against one opponent.' />
+<spell name='Cone of Cold' memrz='0' level='5' desc='1d6 cold damage/level.' />
+<spell name='Sending' memrz='0' level='5' desc='Delivers short message anywhere, instantly.' />
+<spell name='Wall of Force' memrz='0' level='5' desc='Wall is immune to damage.' />
+<spell name='Dream' memrz='0' level='5' desc='Sends message to anyone sleeping.' />
+<spell name='False Vision' memrz='0' level='5' desc='Fools scrying with an illusion.' />
+<spell name='Greater Shadow Conjuration' memrz='0' level='5' desc='As shadow conjuration, but up to 4th level and 40% real.' />
+<spell name='Mirage Arcana' memrz='0' level='5' desc='As hallucinatory terrain, plus structures.' />
+<spell name='Nightmare' memrz='0' level='5' desc='Sends vision dealing 1d10 damage, fatigue.' />
+<spell name='Persistent Image' memrz='0' level='5' desc='As major image, but no concentration required.' />
+<spell name='Seeming' memrz='0' level='5' desc='Changes appearance of one person/two levels.' />
+<spell name='Shadow Evocation' memrz='0' level='5' desc='Mimics evocation less than 5th level.' />
+<spell name='Animate Dead' memrz='0' level='5' desc='Creates undead skeletons and zombies.' />
+<spell name='Magic Jar' memrz='0' level='5' desc='Enables possession of another creature.' />
+<spell name='Animal Growth' memrz='0' level='5' desc='One animal/two levels doubles in size, HD.' />
+<spell name='Fabricate' memrz='0' level='5' desc='Transforms raw materials into finished items.' />
+<spell name='Passwall' memrz='0' level='5' desc='Breaches walls 1 ft. thick/level.' />
+<spell name='Stone Shape' memrz='0' level='5' desc='Sculpts stone into any form.' />
+<spell name='Telekinesis' memrz='0' level='5' desc='Lifts or moves 25 lb./level at long range.' />
+<spell name='Teleport' memrz='0' level='5' desc='Instantly transports you anywhere.' />
+<spell name='Transmute Mud to Rock' memrz='0' level='5' desc='Transforms two 10-ft. cubes/level.' />
+<spell name='Transmute Rock to Mud' memrz='0' level='5' desc='Transforms two 10-ft. cubes/level.' />
+<spell name='Permanency' memrz='0' level='5' desc='Makes certain spells permanent; costs XP.' />
+<spell name='Antimagic Field' memrz='0' level='6' desc='Negates magic within 10 ft.' />
+<spell name='Globe of Invulnerability' memrz='0' level='6' desc='As minor globe, plus 4th level.' />
+<spell name='Greater Dispelling' memrz='0' level='6' desc='As dispel magic, but +20 on check.' />
+<spell name='Guards and Wards' memrz='0' level='6' desc='Array of magic effects protect area.' />
+<spell name='Repulsion' memrz='0' level='6' desc='Creatures cant approach you.' />
+<spell name='Acid Fog' memrz='0' level='6' desc='Fog deals acid damage.' />
+<spell name='Planar Binding' memrz='0' level='6' desc='As lesser planar binding, but up to 16 HD.' />
+<spell name='Summon Monster VI' memrz='0' level='6' desc='Calls outsider to fight for you.' />
+<spell name='Analyze Dweomer' memrz='0' level='6' desc='Reveals magical aspects of subject.' />
+<spell name='Legend Lore' memrz='0' level='6' desc='Learn tales about a person, place, or thing.' />
+<spell name='True Seeing' memrz='0' level='6' desc='See all things as they really are.' />
+<spell name='Geas/Quest' memrz='0' level='6' desc='As lesser geas, plus it affects any creature.' />
+<spell name='Mass Suggestion' memrz='0' level='6' desc='As suggestion, plus one/level subjects.' />
+<spell name='Bigbys Forceful Hand' memrz='0' level='6' desc='Hand pushes creatures away.' />
+<spell name='Chain Lightning' memrz='0' level='6' desc='1d6 damage/level; secondary bolts.' />
+<spell name='Contingency' memrz='0' level='6' desc='Sets trigger condition for another spell.' />
+<spell name='Otilukes Freezing Sphere' memrz='0' level='6' desc='Freezes water or deals cold damage.' />
+<spell name='Greater Shadow Evocation' memrz='0' level='6' desc='As shadow evocation, but up to 5th level.' />
+<spell name='Mislead' memrz='0' level='6' desc='Turns you invisible and creates illusory double.' />
+<spell name='Permanent Image' memrz='0' level='6' desc='Includes sight, sound, and smell.' />
+<spell name='Programmed Image' memrz='0' level='6' desc='As major image, plus triggered by event.' />
+<spell name='Project Image' memrz='0' level='6' desc='Illusory double can talk and cast spells.' />
+<spell name='Shades' memrz='0' level='6' desc='As shadow conjuration, but up to 5th level and 60% real.' />
+<spell name='Veil' memrz='0' level='6' desc='Changes appearance of group of creatures.' />
+<spell name='Circle of Death' memrz='0' level='6' desc='Kills 1d4 HD/level.' />
+<spell name='Control Water' memrz='0' level='6' desc='Raises, lowers, or parts bodies of water.' />
+<spell name='Control Weather' memrz='0' level='6' desc='Changes weather in local area.' />
+<spell name='Disintegrate' memrz='0' level='6' desc='Makes one creature or object vanish.' />
+<spell name='Eyebite' memrz='0' level='6' desc='Charm, fear, sicken or sleep one subject.' />
+<spell name='Flesh to Stone' memrz='0' level='6' desc='Turns subject creature into statue.' />
+<spell name='Mass Haste' memrz='0' level='6' desc='As haste, affects one/level subjects.' />
+<spell name='Mordenkainens Lucubration' memrz='0' level='6' desc='Recalls spell of 5th level or less. Wizard Only.' />
+<spell name='Move Earth' memrz='0' level='6' desc='Digs trenches and build hills.' />
+<spell name='Stone to Flesh' memrz='0' level='6' desc='Restores petrified creature.' />
+<spell name='Tensers Transformation' memrz='0' level='6' desc='You gain combat bonuses.' />
+<spell name='Banishment' memrz='0' level='7' desc='Banishes 2 HD/level extraplanar creatures.' />
+<spell name='Sequester' memrz='0' level='7' desc='Subject is invisible to sight and scrying.' />
+<spell name='Spell Turning' memrz='0' level='7' desc='Reflect 1d4+6 spell levels back at caster.' />
+<spell name='Drawmijs Instant Summons' memrz='0' level='7' desc='Prepared object appears in your hand.' />
+<spell name='Mordenkainens Magnificent Mansion' memrz='0' level='7' desc='Door leads to extradimensional mansion.' />
+<spell name='Phase Door' memrz='0' level='7' desc='Invisible passage through wood or stone.' />
+<spell name='Power Word, Stun' memrz='0' level='7' desc='Stuns creature with up to 150 hp.' />
+<spell name='Summon Monster VII' memrz='0' level='7' desc='Calls outsider to fight for you.' />
+<spell name='Greater Scrying' memrz='0' level='7' desc='As scrying, but faster and longer.' />
+<spell name='Vision' memrz='0' level='7' desc='As legend lore, but quicker and strenuous.' />
+<spell name='Insanity' memrz='0' level='7' desc='Subject suffers continuous confusion.' />
+<spell name='Bigbys Grasping Hand' memrz='0' level='7' desc='Hand provides cover, pushes, or grapples.' />
+<spell name='Delayed Blast Fireball' memrz='0' level='7' desc='1d8 fire damage/level; you can delay blast for 5 rounds.' />
+<spell name='Forcecage' memrz='0' level='7' desc='Cube of force imprisons all inside.' />
+<spell name='Mordenkainens Sword' memrz='0' level='7' desc='Floating magic blade strikes opponents.' />
+<spell name='Prismatic Spray' memrz='0' level='7' desc='Rays hit subjects with variety of effects.' />
+<spell name='Mass Invisibility' memrz='0' level='7' desc='As invisibility, but affects all in range.' />
+<spell name='Shadow Walk' memrz='0' level='7' desc='Step into shadow to travel rapidly.' />
+<spell name='Simulacrum' memrz='0' level='7' desc='Creates partially real double of a creature.' />
+<spell name='Control Undead' memrz='0' level='7' desc='Undead dont attack you while under your command.' />
+<spell name='Finger of Death' memrz='0' level='7' desc='Kills one subject.' />
+<spell name='Ethereal Jaunt' memrz='0' level='7' desc='You become ethereal for 1 round/level.' />
+<spell name='Plane Shift' memrz='0' level='7' desc='Up to eight subjects travel to another plane.' />
+<spell name='Reverse Gravity' memrz='0' level='7' desc='Objects and creatures fall upward.' />
+<spell name='Statue' memrz='0' level='7' desc='Subject can become a statue at will.' />
+<spell name='Teleport without Error' memrz='0' level='7' desc='As teleport, but no off-target arrival.' />
+<spell name='Vanish' memrz='0' level='7' desc='As teleport, but affects a touched object.' />
+<spell name='Limited Wish' memrz='0' level='7' desc='Alters reality-within spell limits.' />
+<spell name='Mind Blank' memrz='0' level='8' desc='Subject is immune to mental/emotional magic and scrying.' />
+<spell name='Prismatic Wall' memrz='0' level='8' desc='Walls colors have array of effects.' />
+<spell name='Protection from Spells' memrz='0' level='8' desc='Confers +8 resistance bonus.' />
+<spell name='Greater Planar Binding' memrz='0' level='8' desc='As lesser planar binding, but up to 24 HD.' />
+<spell name='Incendiary Cloud' memrz='0' level='8' desc='Cloud deals 4d6 fire damage/round.' />
+<spell name='Maze' memrz='0' level='8' desc='Traps subject in extradimensional maze.' />
+<spell name='Power Word, Blind' memrz='0' level='8' desc='Blinds 200 hp worth of creatures.' />
+<spell name='Summon Monster VIII' memrz='0' level='8' desc='Calls outsider to fight for you.' />
+<spell name='Trap the Soul' memrz='0' level='8' desc='Imprisons subject within gem.' />
+<spell name='Discern Location' memrz='0' level='8' desc='Exact location of creature or object.' />
+<spell name='Antipathy' memrz='0' level='8' desc='Object or location affected by spell repels certain creatures.' />
+<spell name='Binding' memrz='0' level='8' desc='Array of techniques to imprison a creature.' />
+<spell name='Demand' memrz='0' level='8' desc='As sending, plus you can send suggestion.' />
+<spell name='Mass Charm' memrz='0' level='8' desc='As charm monster, but all within 30 ft.' />
+<spell name='Ottos Irresistible Dance' memrz='0' level='8' desc='Forces subject to dance.' />
+<spell name='Sympathy' memrz='0' level='8' desc='Object or location attracts certain creatures.' />
+<spell name='Bigbys Clenched Fist' memrz='0' level='8' desc='Large hand attacks your foes.' />
+<spell name='Otilukes Telekinetic Sphere' memrz='0' level='8' desc='As Otilukes resilient sphere, but you move sphere telekinetically.' />
+<spell name='Sunburst' memrz='0' level='8' desc='Blinds all within 10 ft., deals 3d6 damage.' />
+<spell name='Screen' memrz='0' level='8' desc='Illusion hides area from vision, scrying.' />
+<spell name='Clone' memrz='0' level='8' desc='Duplicate awakens when original dies.' />
+<spell name='Horrid Wilting' memrz='0' level='8' desc='Deals 1d8 damage/level within 30 ft.' />
+<spell name='Etherealness' memrz='0' level='8' desc='Travel to Ethereal Plane with companions.' />
+<spell name='Iron Body' memrz='0' level='8' desc='Your body becomes living iron.' />
+<spell name='Polymorph Any Object' memrz='0' level='8' desc='Changes any subject into anything else.' />
+<spell name='Symbol' memrz='0' level='8' desc='Triggered runes have array of effects.' />
+<spell name='Freedom' memrz='0' level='9' desc='Releases creature suffering imprisonment.' />
+<spell name='Imprisonment' memrz='0' level='9' desc='Entombs subject beneath the earth.' />
+<spell name='Mordenkainens Disjunction' memrz='0' level='9' desc='Dispels magic, disenchants magic items.' />
+<spell name='Prismatic Sphere' memrz='0' level='9' desc='As prismatic wall, but surrounds on all sides.' />
+<spell name='Gate' memrz='0' level='9' desc='Connects two planes for travel or summoning.' />
+<spell name='Power Word, Kill' memrz='0' level='9' desc='Kills one tough subject or many weak ones.' />
+<spell name='Summon Monster IX' memrz='0' level='9' desc='Calls outsider to fight for you.' />
+<spell name='Foresight' memrz='0' level='9' desc='Sixth sense warns of impending danger.' />
+<spell name='Dominate Monster' memrz='0' level='9' desc='As dominate person, but any creature.' />
+<spell name='Bigbys Crushing Hand' memrz='0' level='9' desc='As Bigbys interposing hand, but stronger.' />
+<spell name='Meteor Swarm' memrz='0' level='9' desc='Deals 24d6 fire damage, plus bursts.' />
+<spell name='Weird' memrz='0' level='9' desc='As phantasmal killer, but affects all within 30 ft.' />
+<spell name='Astral Projection' memrz='0' level='9' desc='Projects you and companions into Astral Plane.' />
+<spell name='Energy Drain' memrz='0' level='9' desc='Subject gains 2d4 negative levels.' />
+<spell name='Soul Bind' memrz='0' level='9' desc='Traps newly dead soul to prevent resurrection.' />
+<spell name='Wail of the Banshee' memrz='0' level='9' desc='Kills one creature/level.' />
+<spell name='Refuge' memrz='0' level='9' desc='Alters item to transport its possessor to you.' />
+<spell name='Shapechange' memrz='0' level='9' desc='Transforms you into any creature, and change forms once per round.' />
+<spell name='Teleportation Circle' memrz='0' level='9' desc='Circle teleports any creature inside to designated spot.' />
+<spell name='Temporal Stasis' memrz='0' level='9' desc='Puts subject into suspended animation.' />
+<spell name='Time Stop' memrz='0' level='9' desc='You act freely for 1d4+1 rounds.' />
+<spell name='Wish' memrz='0' level='9' desc='As limited wish, but with fewer limits.' />
+</spells>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/d20/d20weapons.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,255 @@
+<weapons>
+<weapon mod="0" name="Antimatter rifle" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="6d10" critical="x2" range="10" weight="10" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Axe, orc double" cost="60" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="x3" range="0" weight="25" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Axe, throwing" cost="8" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x2" range="10" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Battleaxe" cost="10" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="7" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Blowgun" cost="1" category="Asian Weapons-Ranged" size="Small" damage="1" critical="x2" range="10" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Chain, spiked" cost="25" category="Exotic Weapons-Melee" size="Large" damage="2d4" critical="x2" range="0" weight="15" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Club" cost="0" category="Simple Weapons-Melee" size="Medium" damage="1d6" critical="x2" range="10" weight="3" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Crossbow, hand" cost="100" category="Exotic Weapons-Ranged" size="Tiny" damage="1d4" critical="19-20/x2" range="30" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Crossbow, heavy" cost="50" category="Simple Weapons-Ranged" size="Medium" damage="1d10" critical="19-20/x2" range="120" weight="9" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Crossbow, light" cost="35" category="Simple Weapons-Ranged" size="Small" damage="1d8" critical="19-20/x2" range="80" weight="6" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Crossbow, repeating" cost="250" category="Exotic Weapons-Ranged" size="Medium" damage="1d8" critical="19-20/x2" range="80" weight="16" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Dagger" cost="2" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="19-20/x2" range="10" weight="1" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Dagger, punching" cost="2" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="x3" range="0" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Dart" cost="0" category="Simple Weapons-Ranged" size="Small" damage="1d4" critical="x2" range="20" weight="0" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Falchion" cost="75" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="18-29/x2" range="0" weight="16" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Flail, dire" cost="90" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="x2" range="0" weight="20" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Flail, heavy" cost="15" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="19-20/x2" range="0" weight="20" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Flail, light" cost="8" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="5" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Flamer" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="3d6*" critical="-" range="20" weight="8" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Gauntlet" cost="2 gp" category="Simple Weapons-Melee" size="Unarmed" damage="*" critical="*" range="0" weight="2" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Gauntlet, spiked" cost="5" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Glaive" cost="8" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Greataxe" cost="20" category="Martial Weapons-Melee" size="Large" damage="1d12" critical="x3" range="0" weight="20" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Greatclub" cost="5" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x2" range="0" weight="10" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Greatsword" cost="50" category="Martial Weapons-Melee" size="Large" damage="2d6" critical="19-20/x2" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Grenade launcher" cost="-1" category="Modern Weapons-Ranged" size="Large" damage="*" critical="*" range="200" weight="12" type="*" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Guisarme" cost="9" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Halberd" cost="10" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x3" range="0" weight="15" type="P&amp;S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Halfspear" cost="1" category="Simple Weapons-Melee" size="Medium" damage="1d6" critical="x3" range="20" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Hammer, gnome hooked" cost="20" category="Exotic Weapons-Melee" size="Medium" damage="1d6/1d4" critical="x3/x4" range="0" weight="6" type="B&amp;P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Hammer, light" cost="1" category="Martial Weapons-Melee" size="Small" damage="1d4" critical="x2" range="20" weight="2" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Handaxe" cost="6" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x3" range="0" weight="5" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Javelin" cost="1" category="Simple Weapons-Ranged" size="Medium" damage="1d6" critical="x2" range="30" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Kama" cost="2" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="2" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Kama, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Katana" cost="400" category="Asian Weapons-Melee" size="Large" damage="1d10" critical="19-20/x2" range="0" weight="6" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Kukri" cost="8" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="18-29/x2" range="0" weight="3" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Kusari-gama" cost="10" category="Asian Weapons-Melee" size="Medium" damage="1d6" critical="x2" range="0" weight="3" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Lance, heavy" cost="10" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Lance, light" cost="6" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x3" range="0" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Laser pistol" cost="-1" category="Futuristic Weapons-Ranged" size="Small" damage="2d10" critical="x2" range="100" weight="2" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Laser rifle" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="3d20" critical="x2" range="200" weight="7" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Longbow" cost="75" category="Martial Weapons-Ranged" size="Large" damage="1d8" critical="x3" range="100" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Longbow, composite" cost="100" category="Martial Weapons-Ranged" size="Large" damage="1d8" critical="x3" range="110" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Longspear" cost="5" category="Martial Weapons-Melee" size="Large" damage="1d8" critical="x3" range="0" weight="9" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Longsword" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="19-20/x2" range="0" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Mace, heavy" cost="12" category="Simple Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="12" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Mace, light" cost="5" category="Simple Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="6" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Morningstar" cost="8" category="Simple Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="8" type="B&amp;P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Musket" cost="500" category="Renaissance Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="150" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Net" cost="20" category="Exotic Weapons-Ranged" size="Medium" damage="0" critical="0" range="10" weight="10" type="-" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Nunchaku" cost="2" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="2" type="S" >
+<description >
+</description >
+</weapon>
+<weapon mod="0" name="Nunchaku, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Pick, heavy" cost="8" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="x4" range="0" weight="6" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Pick, light" cost="4" category="Martial Weapons-Melee" size="Small" damage="1d4" critical="x4" range="0" weight="4" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Pistol" cost="250" category="Renaissance Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="50" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Pistol, automatic" cost="-1" category="Modern Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="150" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Pistol, revolver" cost="-1" category="Modern Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="100" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Quarterstaff" cost="0" category="Simple Weapons-Melee" size="Large" damage="1d6" critical="x2" range="0" weight="4" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Ranseur" cost="10" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x3" range="0" weight="15" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Rapier" cost="20" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="18-20/x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Rifle, automatic" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="250" weight="12" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Rifle, repeater" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="200" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Sap" cost="1" category="Martial Weapons-Melee" size="Small" damage="1d6s" critical="x2" range="0" weight="3" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Scattergun" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="*" critical="*" range="10" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Scimitar" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="18-20/x2" range="0" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Scythe" cost="18" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x4" range="0" weight="12" type="P&amp;S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Shortbow" cost="30" category="Martial Weapons-Ranged" size="Medium" damage="1d6" critical="x3" range="60" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Shortbow, composite" cost="75" category="Martial Weapons-Ranged" size="Medium" damage="1d6" critical="x3" range="70" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Shortspear" cost="2" category="Simple Weapons-Melee" size="Large" damage="1d8" critical="x3" range="20" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Shuriken" cost="1" category="Exotic Weapons-Ranged" size="Tiny" damage="1" critical="x2" range="100" weight="0" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Siangham" cost="3" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Siangham, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Sickle" cost="6" category="Simple Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="3" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Sling" cost="1d4" category="Simple Weapons-Ranged" size="Small" damage="1d4" critical="x2" range="50" weight="0" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Sword, bastard" cost="35" category="Exotic Weapons-Melee" size="Medium" damage="1d10" critical="19-20/x2" range="0" weight="10" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Sword, short" cost="10" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="19-20/x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Sword, two-bladed" cost="100" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="19-20/X2" range="0" weight="30" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Trident" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="10" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Urgrosh, dwarven" cost="50" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d6" critical="x3" range="0" weight="15" type="S&amp;P" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Wakizashi" cost="300" category="Asian Weapons-Melee" size="Small" damage="1d6" critical="19-20/x2" range="0" weight="3" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Waraxe, dwarven" cost="30" category="Exotic Weapons-Melee" size="Medium" damage="1d10" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Warhammer" cost="12" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="8" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" name="Whip" cost="1" category="Exotic Weapons-Ranged" size="Small" damage="1d2s" critical="x2" range="15" weight="2" type="S" >
+<description ></description >
+</weapon>
+</weapons>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd35/books.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,110 @@
+AE^Arms and Equipment Guide
+BB^Bastion of Broken Souls
+BC^Book of Challenges: Dungeon Rooms, Puzzles, and Traps
+BE^Book of Exalted Deeds
+BV^Book of Vile Darkness
+CR^Champions of Ruin
+CV^Champions of Valor
+CSW^City of Splendors: Waterdeep
+CSQ^City of the Spider Queen
+CAd^Complete Adventurer
+CAr^Complete Arcane
+CD^Complete Divine
+CM^Complete Mage
+CP^Complete Psionic
+CW^Complete Warrior
+DG^D&amp;D Gazetteer
+DH^Deep Horizon
+DF^Defenders of the Faith: A Guidebook to Clerics and Paladins
+DD^Deities and Demigods
+Dr^Draconomicon
+DM^Dragon Magic
+DrF^Dragons of Faerun
+DCS^Dragonlance Campaign Setting
+DMG^Dungeon Master's Guide v.3.5
+DMG2^Dungeon Master's Guide II
+ECS^Eberron Campaign Setting
+EA^Enemies and Allies
+EL^Epic Level Handbook
+XPH^Expanded Psionics Handbook
+Rav^Expedition to Castle Ravenloft
+EH^Explorer's Handbook
+FP^Faiths &amp; Pantheons
+FE^Faiths of Eberron
+FLFD^Fantastic Locations: Fane of the Drow
+FLFR^Fantastic Locations: Fields of Ruin
+FLHP^Fantastic Locations: Hellspike Prison
+FF^Fiend Folio
+FCI^Fiendish Codex I: Hordes of the Abyss
+Fo^Forge of Fury, The
+FRCS^Forgotten Realms Campaign Setting
+Fr^Frostburn
+Gh^Ghostwalk
+GC^Grasp of the Emerald Claw
+HN^Heart of Nightfang Spire
+HBG^Hero Builder's Guidebook
+HB^Heroes of Battle
+HH^Heroes of Horror
+LM^Libris Mortis: The Book of the Dead
+LG^Living Greyhawk Gazetteer
+LF^Lord of the Iron Fortress
+LD^Lords of Darkness
+LoM^Lords of Madness
+LE^Lost Empires of Faerun
+MoE^Magic of Eberron
+Mag^Magic of Faerun
+MoI^Magic of Incarnum
+MP^Manual of the Planes
+MW^Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers
+MH^Miniatures Handbook
+Mon^Monster Compendium: Monsters of Faerun
+MM4^Monster Manual IV
+MM3^Monster Manual III
+MM2^Monster Manual II
+MM^Monster Manual v.3.5
+Mys^Mysteries of the Moonsea
+OA^Oriental Adventures
+PlH^Planar Handbook
+PE^Player's Guide to Eberron
+PG^Player's Guide to Faerun
+PH^Player's Handbook v.3.5
+PH*^Player's Handbook v.3.5
+PH2^Player's Handbook II
+PF^Power of Faerun
+RD^Races of Destiny
+RE^Races of Eberron
+Rac^Races of Faerun
+RS^Races of Stone
+RDr^Races of the Dragon
+RW^Races of the Wild
+RH^Red Hand of Doom
+RT^Return to the Temple of Elemental Evil
+Sa^Sandstorm
+SS^Savage Species
+SX^Secrets of Xen'drik
+SK^Serpent Kingdoms
+SL^Shadows of the Last War
+Sh^Sharn: City of Towers
+ShS^Shining South
+SM^Silver Marches
+SaS^Song and Silence: A Guidebook to Bards and Rogues
+SG^Sons of Gruumsh
+SD^Speaker in Dreams, The
+StS^Standing Stone, The
+Sto^Stormwrack
+SB^Stronghold Builder's Guidebook
+SC^Sunless Citadel, The
+SF^Sword and Fist: A Guidebook to Monks and Fighters
+TB^Tome and Blood: A Guidebook to Wizards and Sorcerers
+ToB^Tome of Battle: The Book of Nine Swords
+TM^Tome of Magic
+Una^Unapproachable East
+Und^Underdark
+UA^Unearthed Arcana
+VGD^Voyage of the Golden Dragon
+WB^Whispers of the Vampire's Blade
+UE^UE
+WL^WL
+FN^FN
+CoV^CoV
+^
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd35/classes.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,856 @@
+adept^DMG^107^This NPC class possesses a combination of arcane and divine skills.
+aristocrat^DMG^108^This NPC class contains people who are usually educated, wealthy individual who were born into high position.
+artificer^ECS^29^Artificers are perhaps the ultimate magical dabblers.
+barbarian^PH^24^A ferocious warrior who uses fury and instinct to bring down foes.
+bard^PH^26^A performer whose music works magic -- a wanderer, a tale-teller, and a jack-of-all trades.
+bardic sage^UA^49^A variant bard who focues his efforts on learning, research, and the power of knowledge.
+battle sorcerer^UA^56^A variant sorcerer who is a capable physical combatant.
+cleric^PH^30^A master of divine magic and a capable warrior as well.
+cloistered cleric^UA^50^A variant cleric who spends more time than other clerics in study and prayer and less in martial training.
+commoner^DCS^46^This NPC class contains the laborers of the world, such as innkeepers, servants, blacksmiths, farmers, and fisherfolk.
+commoner^DMG^108^This NPC class contains the laborers of the world, such as innkeepers, servants, blacksmiths, farmers, and fisherfolk.
+divine bard^UA^50^A variant bard who derives his special power from a divine tradition.
+domain wizard^UA^57^A variant wizard who uses the arcane domain system and selects a specific domain of spells.
+druid^PH*^33^One who draws energy from the natural world to cast divine spells and gain strange magical powers.
+druidic avenger^UA^51^A variant druid who channels her inner fury to wreak vengeance upon those who injure the natural world.
+dwarf cleric^RS^146^Racial substitution levels.
+dwarf fighter^RS^146^Racial substitution levels.
+dwarf sorcerer^RS^147^Racial substitution levels.
+eidolon^Gh^16^This variant class for ghosts emphasizes fighting abilities.
+eidoloncer^Gh^17^This variant class for ghosts emphasizes spellcasting abilities.
+elf paladin^RW^155^Racial substitution levels.
+elf ranger^RW^155^Racial substitution levels.
+elf wizard^RW^157^Racial substitution levels.
+epic barbarian^EL^8^An epic ferocious warrior who uses fury and instinct to bring down foes.
+epic bard^EL^9^An epic performer whose music works magic -- a wanderer, a tale-teller, and a jack-of-all trades.
+epic cleric^EL^10^An epic master of divine magic and a capable warrior as well.
+epic druid^EL^10^An epic druid draws energy from the natural world to cast divine spells and gain strange magical powers.
+epic fighter^EL^11^An epic warrior with exceptional combat capability and unequaled skill with weapons.
+epic monk^EL^12^An epic martial artist whose unarmed strikes hit fast and hard -- a master of exotic powers.
+epic paladin^EL^13^An epic champion of justice and destroyer of evil, protected and strengthened by an array of divine powers.
+epic psion^EL^22^An epic seeker after psionic secrets; a master of the mind and the thoughts of others.
+epic psychic warrior^EL^23^An epic warrior who combines combat skill with psionic powers.
+epic ranger^EL^14^An epic cunning, skilled warrior of the wilderness.
+epic rogue^EL^14^An epic tricky, skillful scout and spy who wins the battle by stealth rather than brute force.
+epic sorcerer^EL^15^An epic spellcaster with inborn magical ability.
+epic wizard^EL^16^An epic potent spellcaster schooled in the arcane arts.
+expert^DCS^46^This NPC class contains people such as skilled artisans, specialist laborers, and ingenious inventors.
+expert^DMG^109^This NPC class contains people such as skilled artisans, specialist laborers, and ingenious inventors.
+expert^UA^77^A generic class that can be a jack-of-all-trades or a master of a limited area of expertise.
+favored soul^CD^6^Favored souls cast divine spells by means of an innate connection rather than through laborious training and prayer, so their divine connection is natural rather than learned.
+favored soul^MH^5^The favored soul follows the path of the cleric but can channel divine power with surprising ease.
+fighter^PH*^37^A warrior with exceptional combat capability and unequaled skill with weapons.
+gnome bard^RS^147^Racial substitution levels.
+gnome illusionist^RS^148^Racial substitution levels.
+gnome ranger^RS^149^Racial substitution levels.
+goliath barbarian^RS^150^Racial substitution levels.
+goliath druid^RS^151^Racial substitution levels.
+goliath rogue^RS^152^Racial substitution levels.
+half-elf bard^RD^157^Racial substitution levels.
+half-elf fighter^RD^157^Racial substitution levels.
+half-elf ranger^RD^158^Racial substitution levels.
+half-elf barbarian^RD^158^Racial substitution levels.
+half-elf druid^RD^159^Racial substitution levels.
+half-elf paladin^RD^160^Racial substitution levels.
+halfling druid^RW^157^Racial substitution levels.
+halfling monk^RW^158^Racial substitution levels.
+halfling rogue^RW^159^Racial substitution levels.
+healer^MH^8^A healer is adept both at detecting the ailments of allies and understanding the coarse, unruly thoughts of beasts.
+hexblade^CW^5^Combining the dynamic powers of martial prowess and arcane might, the hexblade presents a deadly challenge to opponents unused to such a foe.
+marshal^MH^11^Trained in the basics of fighting, marshals possess a general knowledge of weapons and armor.
+monk^PH*^39^A martial artist whose unarmed strikes hit fast and hard -- a master of exotic powers.
+mystic^DCS^47^Mystics are spellcasters who have learned to channel divine energy without worshiping (or even acknowledging) any deity.
+ninja^CAd^5^Highly skilled spies and assassins, ninjas can master a broad range of skills and combat techniques.
+noble^DCS^50^Nobles have the ability to use their background, education, natural charm, and skills in social maneuvering to their advantage in day-to-day lives.
+paladin^PH*^42^A champion of justice and destroyer of evil, protected and strengthened by an array of divine powers.
+paladin of freedom^UA^53^A variant paladin who is dedicated to liberty and free thought.
+paladin of slaughter^UA^53^A variant paladin who is a brutal champion of chaos and evil, and who leaves only destruction trailing in his wake.
+paladin of tyranny^UA^54^A variant paladin who is a lawful evil villain bent on dominating those weaker than she.
+paladin variant^CW^13^A variant paladin class.
+planar ranger^UA^55^A variant ranger who roams the multiverse instead of the wilderness.
+psion^XPH^19^A seeker after psionic secrets; a master of the mind and the thoughts of others.
+psychic warrior^XPH^24^A warrior who combines combat skill with psionic powers.
+ranger^PH*^46^A cunning, skilled warrior of the wilderness.
+ranger variant^CW^13^A variant ranger class.
+raptoran cleric^RW^160^Racial substitution levels.
+raptoran fighter^RW^161^Racial substitution levels.
+raptoran sorcerer^RW^157^Racial substitution levels.
+rogue^PH*^49^A tricky, skillful scout and spy who wins the battle by stealth rather than brute force.
+samurai^CW^8^Wielding their signature katana and wakizashi simultaneously, samurai are as potent in melee as a fighter, although they are less versatile.
+samurai^OA^20^Samurai are professional warriors.
+savage bard^UA^50^A variant bard who is a warrior at heart and whose arcane powers strike fear into the enemies of his tribe.
+scout^CAd^10^A scout has some training in weapons and a unique combat style that favors fast movement and devastating attacks.
+shaman^OA^22^Shamans are intermediaries between the mortal world and the realm of spirits.
+shugenja^CD^10^The shugenja is a divine spellcaster who casts spells by attuning himself to the primal energies around him and focusing such energy through his body to produce magical effects.
+shugenja^OA^24^Shugenjas are divine spellcasters who cast spells by attuning themselves to the elements around them.
+sohei^OA^27^Sohei are warrior monks.
+sorcerer^PH*^51^A spellcaster with inborn magical ability.
+soulknife^XPH^26^A warrior who fights with an idealized blade of personal mental energy.
+specialized wizard variants^UA^59^Each variant specialized class gives up one of the standard specialist's class abilities in exchange for a new ability unique to the variant specialist.
+spellcaster^UA^77^A generic class that has an array of magical effects at her beck and call.
+spellthief^CAd^13^Spellthieves use skill and arcane magic to drain the abilities of their opponents and turn their foes' own powers against them.
+spirit shaman^CD^14^By bargaining with living spirits, the spirit shaman gains power over the natural world and mighty divine magic.
+swashbuckler^CW^11^The swashbuckler embodies the concepts of daring and panache.
+thug^UA^51^A variant fighter who is a street fighter and a survivor who learns to mix brute force with a bit of craftiness.
+totem barbarian^UA^48^A variant barbarian who dedicates himself to a totem creature.
+urban adept^Sh^167^A variant adept for Sharn.
+urban ranger^UA^55^A variant ranger who stalks the treacherous streets of the city.
+warlock^CAr^5^A supernatural character whose sinister powers are inborn abilities, not spells.
+warmage^CAr^10^A militant spellcaster whose training focuses on battlefield magic.
+warmage^MH^14^Warmages access their magic peculiarly, at least compared to the way wizards, sorcerers, and cleric do.
+warrior^DCS^53^This NPC class contains people such as soldiers, guards, and militia.
+warrior^DMG^109^This NPC class contains combatants such as soldiers, guards, and militia.
+warrior^UA^78^A generic class that is a basic combatant.
+wilder^XPH^29^A passionate, reckless talent who wields uncontrolled psionic power.
+wilderness rogue^UA^56^A variant rogue who prefers to put her skills to use in the great outdoors.
+wizard^PH*^55^A potent spellcaster schooled in the arcane arts.
+wu jen^OA^30^Wu jen are spellcasters with mysterious powers.
+wu jen^CAr^14^A mysterious wizard of the eastern world, whose arcane lore revolves around mastery of the elements.
+archivist^HH^82^Archivists seek out esoteric sources of divine lore, wherever those sources might be, securing those secrets for themselves and their fellow scholars.
+dread necromancer^HH^84^A practitioner of vile and forbidden arts, the dread necromancer roots about in graveyards, searching out moldering components for her obscene spells.
+psionic artificer^MoE^42^Psionic artificers are similar to artificers, but they craft psionic items instead of magic items.
+incarnate^MoI^20^Incarnum is a tool you can use to manipulate the physical manifestations of moral and ethical forces and wield them in righteous pursuit of an ideal.
+soulborn^MoI^25^As a soulborn, you use incarnum to enhance your natural combat ability.
+totemist^MoI^29^You channel the soul energy of magical beasts to make your soulmelds and claim them as your totems to acquire a share in their power.
+binder^TM^9^By drawing their seals and speaking words of power, the binder summons strange entities, bargains with them, and binds them to his service.
+shadowcaster^TM^111^The shadowcaster understands the true, primal power of darkness, attunes herself to the Plane of Shadow, and learns great shadow mysteries the equal of any mundane spell.
+truenamer^TM^198^If you want to understand the secret language of the universe, the truenamer class is for you.
+ardent^CP^5^An ardent's pursuit of various cosmic philosophies gives her access to psionic power in a unique way: through psionic mantles.
+divine mind^CP^9^A divine mind is a psionic character who channels the power of the divine through pisonic talent instead of faith.
+lurk^CP^13^A lurk is a psionic character who has honed her mental talents to a deadly focus.
+duergar racial class^CP^144^The duergar, or gray dwarves, lead lives of neverending toil in great underground foundry-cities.
+githyanki racial class^CP^146^Githyanki are an ancient race of martial humanoids residing on the Astral Plane.
+githzerai racial class^CP^147^The githzerai are attuned to the mysteries of the inner self and are considered a race of ascetics who harness the power of the mind and the spirit.
+half-giant racial class^CP^148^Human-giant hybrids, half-giants were bred by cruel sorcerer-kings who used them as warriors and laborers in a dry land.
+thri-kreen racial class^CP^149^Fierce hunters and faultless trackers, the thri-kreen are a race of insectfolk sometimes known as mantis warriors.
+erudite (variant psion)^CP^153^As an alternative to the standard psion class, the erudite is a psionic character who follows a scholarly and self-reflective road to power, instead of a merely self-conscious path like the psion follows.
+beguiler^PH2^6^If you delight in manipulating others, either to their disadvantage or for their own good, then the beguiler is the class for you.
+dragon shaman^PH2^11^If you gaze at dragons with awe and aspire to share their power and majesty, then the dragon shaman is the class for you.
+duskblade^PH2^19^If you find you can't choose between being an arcane spellcaster who zaps your enemies with powerful spells and a nimble, powerful front-line melee character who lays them low with a sword, the duskblade is the perfect class for you.
+knight^PH2^24^The knight class is a great choice if you want to play a tough, durable melee combatant whose strong personality allows you to manipulate your foes.
+crusader^ToB^8^Devoted knight, divine agent, instrument of vengeance, peerless fighting machine -- the crusader is a warrior dedicated to good, evil, law, chaos, or some other cause.
+swordsage^ToB^15^A master of martial maneuvers, the sword sage is a physical adept -- a blade wizard whose knowledge of the Sublime Way lets him unlock potent abilities, many of which are overtly supernatural or magical in nature.
+warblade^ToB^20^The warblade was born for conflict. Swift, strong, enduring, and utterly confident in his martial skills, he seeks to test himself against worthy foes.
+dragonfire adept^DM^24^Whether they are bold champions defending the weak and downtrodden, or merciless raiders seeking might and riches, dragonfire adepts are imposing figures who command the magic of dragonkind.
+abolisher {Prestige Class}^LoM^182^The abolisher is a crusader against that which taints, usurps, and replaces the ordered nature of things with alien desires and monstrous needs.
+acolyte of the skin {Prestige Class}^CAr^19^Acolytes of the skin seek to gain power by replacing their skin with that of a demon's.
+acolyte of the skin {Prestige Class}^TB^43^Acolytes of the skin seek to gain power by replacing their skin with that of a demon's.
+agent retriever {Prestige Class}^EL^24^Finding items, especially long-lost ones, is an agent retriever's specialty.
+Aglarondan griffonrider {Prestige Class}^Una^18^Soaring above the Yuirwood and the coasts of Aglarond, the famed Aglarondan griffonriders are an elite force of aerial knights who serve the Simbul and defend their homeland against attack.
+Akodo champion {Prestige Class}^OA^220^Akodo champions are the leaders of the mighty army of the Lion clan.
+alienist {Prestige Class}^CAr^21^Alienists deal with powers and entities from terrifyingly remote reaches of space and time.
+alienist {Prestige Class}^TB^45^Alienists deal with powers and entities from terrifyingly remote reaches of space and time.
+ancient master {Prestige Class}^SK^159^Those yuan-ti who grow mightier of mind and gain additional psionic powers are called ancient masters.
+animal lord {Prestige Class}^CAd^22^Each animal lord forms a bond with one group of animals.
+animal lord {Prestige Class}^MW^43^Each animal lord forms a bond with one group of animals.
+anointed knight {Prestige Class}^BE^49^The anointed knight is a holy soldier who has taken great pains to learn the intricacies of alchemy in order to become a more capable combatant.
+apostle of peace {Prestige Class}^BE^51^The apostle of peace is an advocate for nonviolent resolution of conflict.
+arachne {Prestige Class}^FP^182^Arachnes are priestesses of Lolth who have risen to the pinnacle of drow society, worshiping Lolth only for the power she grants.
+Arboreal Guardian {Prestige Class}^Gh^19^Within the Spirit Wood are the Arboreal Guardians, men and women dedicated to protecting and ministering the living repositories of elf and half-elf spirits.
+arcachnomancer {Prestige Class}^Und^28^Many creatures of the Underdark are drawn to the power of the spider and that of the master of spiders -- the arachnomancer.
+arcane archer {Prestige Class}^DMG^176^The arcane archer is a warrior skilled in using magic to supplement her combat prowess.
+arcane devotee {Prestige Class}^FRCS^40^Arcane devotees complement the divine magic of a church's clerical leaders and are among the most important and respected members of a deity's following.
+arcane devotee {Prestige Class}^PG^48^Arcane devotees complement the divine magic of a church's clerical leaders and are among the most important and respected members of a deity's following.
+arcane hierophant  {Prestige Class}^RW^108^Arcane hierophants wield a blending of arcane magic and divine magic with a heavy emphasis on nature and the elements.
+arcane trickster {Prestige Class}^DMG^177^Arcane tricksters combine their knowledge of spells with a taste for intrigue, larceny, or just plain mischief.
+arcane trickster {Prestige Class}^TB^47^Arcane tricksters combine their knowledge of spells with a taste for intrigue, larceny, or just plain mischief.
+archmage {Prestige Class}^DMG^178^The most advanced practitioners of arcane magic are frequently archmages.
+archmage {Prestige Class}^FRCS^41^The archmage gains strange powers and the ability to alter spells in remarkable ways, but must sacrifice some of her spell capability to master these arcane secrets.
+ardent dilettante  {Prestige Class}^PlH^55^A diversity of interests and a moderate level of ability in one skill can lead one to become an ardent dilettante.
+argent savant {Prestige Class}^CAr^24^The argent savant regards spells that evoke or apply magical force as the noblest and most fascinating spells at her disposal.
+ashworm dragoon  {Prestige Class}^Sa^66^Ashworm dragoons have formed a bond with a single ashworm that is so strong that it is almost an extension of their wills.
+assassin {Prestige Class}^DMG^180^The assassin is the master of dealing quick, lethal blows.
+astral dancer {Prestige Class}^PlH^63^A few skilled combatants learn to take advantage of conditions on the Astral Plane.
+atavist  {Prestige Class}^RE^133^The discipline of an atavist strengthens his bond to the ancestral spirit and to all other kalashtar that have embraced their unique heritage.
+auspician {Prestige Class}^FP^184^Auspicians, who manipulate luck as if it were the strings of a worn mandolin, give credence to their claims.
+bane of infidels {Prestige Class}^MW^46^The bane of infidels is the leader of a xenophobic tribe.
+battle maiden {Prestige Class}^OA^34^Battle maidens are the stuff of wonder and legend, an order of mounted female samurai whose swift, fearless attacks are renowned throughout the world.
+battle scion {Prestige Class}^UA^164^This prestige class is for the wielders of legendary weapons made for the hands of fighters, barbarians, rangers, monks, and the occasional paladin.
+battlerager  {Prestige Class}^Rac^178^Dwarven battleragers, or kuldjargh ('axe idiots'), are legendary berserker warriors who can enter a battle frenzy through ritualist singing.
+battlesmith  {Prestige Class}^RS^97^A battlesmith is a skilled dwarf armorer and weaponsmith who uses her experience in battle, as well as her masterful weaponsmithing and armorsmithing abilities, to create deadly items for her kinsmen to wield in defense of their homes.
+Bayushi deceiver {Prestige Class}^OA^225^The Bayushi are charged with the dirtiest work in the empire.
+bear warrior {Prestige Class}^CW^16^Bear warriors, through a special relationship with bear spirits, literally adopt a bear's strength in the rage of battle, actually transforming into bears while they fight.
+bear warrior {Prestige Class}^OA^36^Bear warriors, through a special relationship with bear spirits, literally adopt a bear's strength in the rage of battle, actually transforming into bears while they fight.
+beastmaster {Prestige Class}^CAd^26^A beastmaster feels more at home among the animals of nature than fellow sentient beings.
+beholder mage {Prestige Class}^LoM^42^Through ritual destruction of its central eye, a beholder can learn to channel and use magic much more quickly and efficiently than can almost any other race.
+beholder mage  {Prestige Class}^Mon^21^Through ritual destruction of its central eye, a beholder can learn to channel and use magic much more quickly and efficiently than can almost any other race.
+beloved of Valarian {Prestige Class}^BE^53^The beloved of Valarian are women who have foresworn the love of mortals to dedicate themselves entirely to the unicorn deity Valarian.
+berserk {Prestige Class}^DD^201^Berserks are warriors who dress themselves in bearskins, taking advantage of the fear most people have for wild animals and inviting the wild rage of the animal into the warrior's body.
+Black Blood cultist {Prestige Class}^CR^44^Black Blood cultists are savage fighters whose natural attacks become more fearsome as they increase in level.
+black blood hunter {Prestige Class}^PG^177^Malar grants exceptional power to those who supplement their bestial level of evil with truly vile acts.
+black flame zealot {Prestige Class}^CD^21^Trained in unholy rites, the black flame zealots use stealth, divine magic, and the zeal of fanaticism to destroy those who have given offense to their god.
+Black Flame zealot {Prestige Class}^Una^21^Trained in the rites of Kossuth's temple, the Black Flame zealots use stealth, divine magic, and the zeal of fanaticism to destroy those who have given offense to the Lord of Flames.
+blackguard {Prestige Class}^DMG^181^The blackguard is the quintessential black knight.
+blade bravo {Prestige Class}^RS^99^Blade bravos are gnomes who learn the ways of the rapier.
+blade dancer {Prestige Class}^OA^37^To blade dancers, the sword is more than a weapon -- it is an ally, a friend, a spirit companion.
+bladesinger {Prestige Class}^CW^17^Bladesingers are elves who have blended art, swordplay, and arcane magic into a harmonious whole.
+bladesinger {Prestige Class}^Rac^179^Bladesingers are elves who have blended art, swordplay, and arcane magic into a harmonious whole.
+bladesinger {Prestige Class}^TB^49^Bladesingers are elves who have blended art, swordplay, and arcane magic into a harmonious whole.
+blighter {Prestige Class}^CD^23^Blighters bring desolation wherever they tread.
+blighter {Prestige Class}^MW^48^Blighters bring desolation wherever they tread.
+blood magus {Prestige Class}^CAr^26^Blood magi are deceased spellcasters who gain an understanding of blood's importance when returned to life.
+blood magus {Prestige Class}^TB^50^Blood magi are deceased spellcasters who gain an understanding of blood's importance when returned to life.
+bloodhound {Prestige Class}^CAd^28^A bloodhound tracks down wrongdoers and brings them to whatever justice awaits them.
+bloodhound {Prestige Class}^MW^49^A bloodhound tracks down wrongdoers and brings them to whatever justice awaits them.
+bloodscaled fury {Prestige Class}^Dr^86^A bloodscaled fury is a dragon whose rage surpasses that of a human barbarian as the barbarian's rage surpasses a child's tantrum.
+bonded summoner  {Prestige Class}^MH^16^He who learns to leash the furies of the Elemental Planes is known as a bonded summoner.
+bone collector {Prestige Class}^Gh^21^A bone collector is a person who draws personal power from the destruction of undead.
+breachgnome {Prestige Class}^Rac^181^A breachgnome is a mighty gnome who is skilled in fighting in cramped conditions.
+cabinet trickster {Prestige Class}^RE^139^Cabinet tricksters are the changeling agents of the Cabinet of Faces.
+cancer mage  {Prestige Class}^BV^52^The cancer mage makes quick, poisonous attacks and then retreats.
+candle caster {Prestige Class}^TB^52^Also called 'spell chandlers,' these specialists fill their time fashioning candles, both for esthetics and for power.
+Cannith wand adept  {Prestige Class}^Sh^162^The Cannith wand adept is an artificer or other spellcaster who specializes in mastering wands.
+cavalier {Prestige Class}^CW^19^Representing the ultimate in mounted warfare, the cavalier is the quintessential knight in shining armor.
+cavalier  {Prestige Class}^SF^12^Representing the ultimate in mounted warfare, the cavalier is the quintessential knight in shining armor.
+cavelord {Prestige Class}^Und^30^A passion for the narrow, dim ways of the world burns in the breast of the cavelord.
+celebrant of Sharess {Prestige Class}^PG^178^Celebrants of Sharess are seducers and warriors, hedonists and pious champions of good.
+celestial mystic {Prestige Class}^BE^55^Celestial mystics seek to attain ultimate unity with the perfect good.
+cerebremancer  {Prestige Class}^XPH^141^Cerebremancers access both the arcane mysteries of spellcasting and the psionic powers of the mind.
+chameleon  {Prestige Class}^RD^111^Chameleons are dilettantes in every class and masters of none.
+champion of Corellon Larethian {Prestige Class}^RW^113^The champion of Corellon Larethian is a noble elf fighter, an elf knight or lord who can stand up to any orc or human warrior.
+champion of Gwynharwyf {Prestige Class}^BE^56^The champion of Gwynharwyf is a mortal barbarian who strives to emulate her sublime balance of fury and reserve while retaining a focus on good.
+chaotician {Prestige Class}^PlH^61^Chaoticians seek to enjoy the beauty of the unpredictable, and by seeking to emulate the philosophy of chaos in their actions, they create a fabulous journey through life in which nothing is a bore.
+church inquisitor {Prestige Class}^CD^26^The church inquisitor uncovers taint within the church and cuts it away.
+church inquisitor {Prestige Class}^DF^51^The church inquisitor uncovers taint within the church and cuts it away.
+cipher adept {Prestige Class}^PlH^58^Seamless integration brings the cipher adept bliss.
+Citadel elite {Prestige Class}^Sh^163^The elite agent of the Citadel is the best of the best, saved for the most dangerous, most important assignments or granted latitude to serve the King and Crown as he sees fit.
+cloud anchorite  {Prestige Class}^Fr^52^The cloud anchorite seeks a way to achieve immortality while maintaining life and awareness.
+cognition thief {Prestige Class}^PG^174^A cognition thief's subtle ability to worm her way into a target's very consciousness makes her the ultimate secret agent.
+coiled cabalist {Prestige Class}^SK^161^Standing apart from the priests of Sseth but careful never to draw their collective ire by openly opposing them, the members of the Coiled Cabal pursue the arcane arts largely in secrecy.
+combat medic {Prestige Class}^HB^99^The combat medic keeps her allies alive and tends to the fallen on the front lines of battle.
+consecrated harrier {Prestige Class}^CD^28^The consecrated harrier acts as a bounty hunter for her religion or organization.
+consecrated harrier {Prestige Class}^DF^52^The consecrated harrier acts as a bounty hunter for her religion or organization.
+contemplative {Prestige Class}^CD^30^Contemplatives devote their lives to cultivating a greater closeness with their deities.
+contemplative {Prestige Class}^DF^54^Contemplatives devote their lives to cultivating a greater closeness with their deities.
+cosmic descryer {Prestige Class}^EL^26^The cosmic descryer is interested in the infinite variety of the planes and fascinated by the different layers of the multiverse.
+cragtop archer {Prestige Class}^RS^101^Cragtop archers train their eyes and minds to find target at great distances, and to quickly compensate for wind, movement, and other factors that affect shots of such difficulty.
+Crinti shadow marauder  {Prestige Class}^ShS^23^Crinti shadow marauders combine the physical prowess of master rider with the stealth of the most cunning shadowdancers.
+cryokineticist {Prestige Class}^Fr^54^The cryokineticist is the master of cold psionic energy.
+cultist of the Shattered Peak  {Prestige Class}^LE^10^Cultists of the Shattered Peak possess skill at arms, stealth, and a smattering of ancient lore.
+daggerspell mage {Prestige Class}^CAd^31^Daggerspell mages work to perfect a unique fighting and spellcasting (arcane) style that relies on wielding a pair of daggers at all times.
+daggerspell shaper {Prestige Class}^CAd^36^Daggerspell shapers work to perfect a unique fighting and spellcasting (druid) style that relies on wielding a pair of daggers at all times.
+Daidoji bodyguard {Prestige Class}^OA^215^The Daidoji concentrate on defensive maneuvers and a style of fighting that induces their opponents to defeat themselves.
+dark hunter {Prestige Class}^CW^20^Dark hunters specialize in hunting down and eliminating creatures in the dark, twisting caves of the Underdark.
+Darkmask  {Prestige Class}^LD^33^Darkmasks strike a balance between their faith and their skills at stealth.
+darkrunner {Prestige Class}^LoM^186^Darkrunners devote their lives to traveling the haunted underground depths.
+darkwood stalker {Prestige Class}^CW^23^Some elves train as elite hunters of orcs; these hunters are called darkwood stalkers.
+dawncaller {Prestige Class}^RS^103^Dawncallers are goliath bards responsible for guarding their tribe throughout the night.
+death's chosen ^LM^41^If chosen by his would-be undead master, a death's chosen serves as the master's living minion.
+Deathwarden chanter {Prestige Class}^Gh^23^The Deathwarden chanters are the most prestigious, respected, and mysterious members of the dwarven Deathwarden clan.
+deep diviner {Prestige Class}^Und^32^Deep diviners are intimates of the earth and all that it hides.
+deepwarden {Prestige Class}^RS^105^Deepwardens are dwarves who serve as a living early warning system against threats from both the environment and other creatures.
+deepwood sniper {Prestige Class}^MW^52^A deepwood sniper is patient, careful, quiet , and deadly accurate.
+defender of Sealtiel {Prestige Class}^BE^58^Defenders of Sealtiel are sworn to uphold the ideals of Sealtiel, which includes fighting off forces of evil when they assault good.
+defiant {Prestige Class}^PlH^44^Defiants take the teaching of the Athar to heart in a way that grants them tremendous powers against those who claim to wield divine might.
+demonologist {Prestige Class}^BV^54^A demonologist is a mortal who has devoted his life to the study of demons.
+dervish {Prestige Class}^CW^25^The dervish epitomizes speeds, quickness, and abandon.
+devoted defender {Prestige Class}^SF^13^The devoted defender is a professional guardian.
+diabolist {Prestige Class}^BV^56^A diabolist does not serve devils -- she wants to be one.
+dirgesinger {Prestige Class}^LM^43^Dirgesingers voice melodies not of celebration and joy, but of sorrow and grief.
+disciple of Ashardalon {Prestige Class}^Dr^87^Disciples of Ashardalon bind fiendish spirits to their own hearts, eventually taking on the characteristics of demonic spawn themselves.
+disciple of Asmodeus {Prestige Class}^BV^57^A disciple of Asmodeus uses his power and influence to learn secrets, which in turn gains him more power.
+disciple of Baalzebul {Prestige Class}^BV^58^A disciple of Baalzebul uses deceit and trickery to get what she wants.
+disciple of Dispater {Prestige Class}^BV^60^A disciple of Dispater is a warlike general of evil.
+disciple of Mammon {Prestige Class}^BV^60^A disciple of Mammon takes what she wants any way she can.
+disciple of Mephistopheles {Prestige Class}^BV^62^A disciple of Mephistopheles wields hellfire as his weapon.
+disciple of Thrym {Prestige Class}^Fr^56^Aside from predictions of Ragnarok, disciples of Thrym spend a large portion of their time undermining those who serve Thor and Loki, the deities who conspired against Thrym.
+dispassionate watcher of Chronepsis {Prestige Class}^Dr^89^Taking the role of observers rather than active participants, dispassionate watchers of Chronepsis remain aloof from the events of the world.
+divine agent  {Prestige Class}^MP^24^The divine agent is a specially selected agent of her deity, and she acts in the service of that power or deity.
+divine champion {Prestige Class}^FRCS^42^Divine champions are mighty warriors who dedicate themselves to their deity's cause, defending holy ground, destroying enemies of the church, and slaying mythical beasts and clerics of opposing faiths.
+divine champion {Prestige Class}^PG^49^Divine champions are mighty warriors who dedicate themselves to their deity's cause, defending holy ground, destroying enemies of the church, and slaying mythical beasts and clerics of opposing faiths.
+divine crusader {Prestige Class}^CD^33^The divine crusader embodies devotion and dedication to a chosen deity.
+divine disciple {Prestige Class}^FRCS^43^Divine disciples interpret the divine will, act as teachers and guides to other members of the clergy, and arm lay followers of their deity with the power of their patron.
+divine disciple {Prestige Class}^PG^51^Divine disciples interpret the divine will, act as teachers and guides to other members of the clergy, and arm lay followers of their deity with the power of their patron.
+divine emissary {Prestige Class}^EL^27^Deities have need of powerful servants, many of whom are epic clerics, paladins, and other characters.
+divine oracle {Prestige Class}^CD^34^Some mortals hear the words of deities; these can be divine oracles.
+divine oracle {Prestige Class}^DF^56^Some mortals hear the words of deities; these can be divine oracles.
+divine prankster {Prestige Class}^RS^107^These gnomes embrace Garl's methods of teaching through harmless object lessons and dedicate their lives to acting as his agents in the world.
+divine seeker {Prestige Class}^FRCS^44^The divine seeker infiltrates dangerous places to rescue prisoners, reclaim stolen relics, or eliminate enemy leaders.
+divine seeker {Prestige Class}^PG^52^The divine seeker infiltrates dangerous places to rescue prisoners, reclaim stolen relics, or eliminate enemy leaders.
+doomdreamer  {Prestige Class}^RT^162^Doomdreamers are the elite among the ranks of the cult of Tharizdun.
+doomguide {Prestige Class}^FP^186^Doomguides belong to an elite order of spellcasting warriors in service to the Judge of the Damned.
+doomlord {Prestige Class}^PlH^47^The doomlord's life holds the greatest appeal for fighters and barbarians who enjoy smashing and destroying.
+dracolyte {Prestige Class}^Dr^122^The dracolyte takes up worship of the draconic gods.
+dragon ascendant {Prestige Class}^Dr^90^Dragon ascendants seek to transcend the limitations of material existence so as to become nothing less than deities.
+dragon disciple {Prestige Class}^DMG^183^Dragon disciples use their magic as a catalyst to ignite their dragon blood.
+dragon disciple {Prestige Class}^TB^55^Dragon disciples use their magic as a catalyst to ignite their dragon blood.
+dragon rider  {Prestige Class}^DCS^77^Dragon riders develop a strong bond with their mounts that allows the two to work together.
+dragon samurai {Prestige Class}^MH^18^Dragon samurai are dedicated warriors, members of a special, self-selected class who revered dragonkind and emulate dragons' ferocious martial abilities to the point of taking on some draconic traits.
+dragonkith {Prestige Class}^Dr^123^Dragonkith are creatures that serve and aid dragons.
+dragonmark heir  {Prestige Class}^ECS^73^Dragonmark heirs have the ability to improve the dragonmarks they have manifested, as well as to develop additional abilities related to their dragonmarks.
+dragonrider {Prestige Class}^Dr^124^Dragonriders soar through the clouds atop a draconic steed.
+dragonslayer {Prestige Class}^Dr^125^Dragonslayers combat dragons.
+dragonsong lyrist {Prestige Class}^Dr^127^The dragonsong lyrist taps into the power of dragonsong.
+dragonstalker {Prestige Class}^Dr^128^The dragonstalker uses stealth and guile to combat dragons.
+dread commando {Prestige Class}^HB^103^Dread commandos are the elite scouts and strike force of a well-trained mercenary band.
+dread pirate {Prestige Class}^CAd^39^A dread pirate has mastered every aspect of larceny on the high seas.
+dread pirate {Prestige Class}^SaS^5^A dread pirate has mastered every aspect of larceny on the high seas.
+dreadmaster {Prestige Class}^FP^188^Dreadmasters seek to rule absolutely, preferably through terror and domination.
+drow judicator {Prestige Class}^Und^33^A mortal imbued with fiendish cruelty, the drow judicator is a knight most foul.
+drunken master {Prestige Class}^CW^27^By weaving and staggering about as if inebriated, drunken boxers avoid many blows.
+drunken master {Prestige Class}^SF^14^By weaving and staggering about as if inebriated, drunken boxers avoid many blows.
+duelist {Prestige Class}^DMG^185^The duelist is a nimble, intelligent fighter trained in making precise attacks with light weapons.
+duelist {Prestige Class}^SF^17^The duelist is a nimble, intelligent fighter trained in making precise attacks with light weapons.
+dungeon delver {Prestige Class}^CAd^42^In many ways, the dungeon delver is the ultimate adventuring rogue.
+dungeon delver {Prestige Class}^SaS^7^In many ways, the dungeon delver is the ultimate adventuring rogue.
+durthan {Prestige Class}^Una^22^Durthans are an order of spellcasters who tap into the darker spirits of Rashemen.
+dwarven defender {Prestige Class}^DMG^186^The defender is a sponsored champion of a dwarven cause
+dweomerkeeper {Prestige Class}^FP^189^Dweomerkeepers are Mystra's shepherds, safeguarding the Weave against threats to its integrity.
+earth dreamer {Prestige Class}^RS^110^Earth dreamers move within the ancient dreams of the mountains, attuning themselves to their power and mastering strange abilities over the earth.
+effigy master {Prestige Class}^CAr^30^The effigy master is an expert in the imitation of true life.
+Eldeen ranger {Prestige Class}^ECS^74^An Eldeen ranger learns special techniques and abilities that help him to fulfill the goal of his sect.
+eldritch knight {Prestige Class}^DMG^187^Studying the martial and arcane arts to equal degree, the eldritch knight is a versatile combatant.
+elemental archon {Prestige Class}^FP^190^Elemental archons are servants of powerful, seemingly uncaring elemental forces who want to once and for all tip the balance in favor of their chosen element.
+elemental master {Prestige Class}^Dr^92^Elemental masters strive to attain the purity of perfect attunement with both the energy of their breath weapons and the elemental nature of their core.
+elemental savant {Prestige Class}^CAr^32^Elemental savants study the basic building blocks of existence -- air, earth, fire, and water -- learning to harness their powers.
+elemental savant {Prestige Class}^TB^57^Elemental savants study the basic building blocks of existence -- air, earth, fire, and water -- learning to harness their powers.
+elemental warrior {Prestige Class}^PlH^65^The elemental warrior sees that great strength comes from focusing on the most basic aspects of reality.
+elocater {Prestige Class}^XPH^142^Elocaters are renowned for their agile combat stratagems, using their knowledge of motion and space to set themselves up for quick attacks against slower opponents.
+elven high mage {Prestige Class}^Rac^182^Elven high mages are the masters of creating their own epic spells -- mythals that can grow to engulf entire cities.
+emancipated spawn  {Prestige Class}^SS^75^Those spawn of undead who find themselves free of their masters can begin to recall their former lives and a measure of redemption.
+emissary of Barachiel {Prestige Class}^BE^59^The emissaries of Barachiel are peacemakers, diplomats, and evangelists, as well as staunch opponents of evil and corruption.
+enlightened fist {Prestige Class}^CAr^34^Enlightened fists master the use of touch spells, creating new forms of combat with their fists.
+entropomancer {Prestige Class}^CD^36^Entropomancers gain attunement to the great nothingness they say lies at the center of the universe.
+ephemeral exemplar {Prestige Class}^LM^53^Ephemeral exemplars are paragons of incorporealness.
+epic arcane archer {Prestige Class}^EL^17^The epic arcane archer is a living extension of the bow, capable of achieving wonders of archery that cause lesser beings to gape in awe.
+epic assassin {Prestige Class}^EL^18^The epic assassin flits from shadow to shadow, lying in wait until his target is vulnerable, then striking like a cobra.
+epic blackguard {Prestige Class}^EL^19^The epic blackguard is a twisted reflection of the epic paladin, radiating evil power from every pore of his body.
+epic dwarven defender {Prestige Class}^EL^20^The epic dwarven defender becomes the very definition of immovable object.
+epic infiltrator {Prestige Class}^EL^28^The epic infiltrator is an agent of espionage, an undercover operative, and sometimes a saboteur.
+epic loremaster {Prestige Class}^EL^20^If the epic loremaster doesn't know something, it probably isn't worth knowing.
+epic psion {Prestige Class}^EL^22^The epic psion has evolved his inborn mental abilities, achieving mental mastery of lesser mentalities.
+epic psychic warrior {Prestige Class}^EL^23^The epic psychic warrior is a meld of mental and martial prowess.
+epic shadowdancer {Prestige Class}^EL^21^While the epic assassin udes the shadows, the epic shadowdancer becomes the shadows, indistinguishable from the darkness cloaking her.
+eunuch warlock {Prestige Class}^OA^38^Eunuch warlocks must be arcane spellcasters of significant ability, and they are often sorcerers rather than wu jen.
+evangelist {Prestige Class}^CD^39^Evangelists travel the world proclaiming their devotion to a particular deity, pantheon, or religious doctrine.
+Evereskan tomb guardian {Prestige Class}^PG^53^The Evereskan tomb guardians are on hand to ensure that the defenses of the tombs are very good indeed.
+exalted arcanist {Prestige Class}^BE^61^Exalted arcanists gain access to spells that channel celestial energy.
+exemplar {Prestige Class}^CAd^44^An exemplar focuses her energy on improving the skills she possesses until she can perform them with fluidity, grace, and art.
+exorcist of the Silver Flame {Prestige Class}^ECS^77^Exorcists of the Silver Flame lead the Church of the Silver Flame's efforts in combating extraplanar threats.
+exotic weapon master {Prestige Class}^CW^30^The only real requirement for exotic weapon master is commitment and perseverance.
+exotic weapon master {Prestige Class}^MW^53^The only real requirement for exotic weapon master is commitment and perseverance.
+extreme explorer {Prestige Class}^ECS^79^The extreme explorer is the iconic action hero of Eberron.
+eye of Gruumsh {Prestige Class}^CW^31^An orc or half-orc who heeds the call to serve Gruumsh in his image can become an eye of Gruumsh.
+eye of Gruumsh {Prestige Class}^MW^54^An orc or half-orc who heeds the call to serve Gruumsh in his image can become an eye of Gruumsh.
+eye of Horus-Re^PG^54^Eyes of Horus-Re are champions of good, sworn enemies of Set, and bane to undead.
+faith scion {Prestige Class}^UA^166^This prestige class is meant for characters who wield legendary weapons of divine power for claerics, druids, and paladins.
+fang of Lolth {Prestige Class}^SaS^8^Fangs of Lolth undergo a transformation that both provides them power and changes them in ways they may not necessarily want.
+Fang of Sseth {Prestige Class}^SK^162^The Fangs of Sseth constitute the strike force of the Vipers -- the stealthy killers who leave no trace.
+fatemaker {Prestige Class}^PlH^50^'There are two paths to take; one is easy, and that is its only reward.'
+fatespinner {Prestige Class}^CAr^37^A fatespinner has pulled back the curtain of chance, circumstance, and chaos to glimpse a deeper truth: probability.
+fatespinner {Prestige Class}^TB^58^A fatespinner has pulled back the curtain of chance, circumstance, and chaos to glimpse a deeper truth: probability.
+fiend of blasphemy  {Prestige Class}^FF^200^The fiend of blasphemy is a master of the infernal art of perverting the desire to worship and turning it toward the corrupt veneration of fiendish masters.
+fiend of corruption {Prestige Class}^FF^202^Fiends of corruption are preoccupied with corrupting mortals to ensure that their souls end up on the Lower Planes after death.
+fiend of possession {Prestige Class}^FF^204^A fiend of possession is an invasive presence that taints the very soul, corrupting from within.
+Fist of Hextor {Prestige Class}^SF^18^Fists of Hextor are templars sworn to the service of their unforgiving master.
+fist of Raziel {Prestige Class}^BE^62^The fists of Raziel represent a knightly order dedicated to the celestial patron of holy warfare against evil.
+Fist of Zuoken {Prestige Class}^XPH^144^The Fists of Zuoken ore members of an order of martial artists devoted to mastering their own physical and mental development while protecting psions and other psionic creatures.
+fleshwarper {Prestige Class}^LoM^189^The fleshwarper finds no greater canvas than flesh itself.
+Fochlucan lyrist {Prestige Class}^CAd^47^The Fochlucan lyrist is a legendary figure who serves as the herald and teacher to great kings, the champion of the common folk, and the keeper of lore long forgotten elsewhere.
+foe hunter {Prestige Class}^MW^56^The foe hunter has but one purpose in life: to kill creatures of the type she hates.
+forest master {Prestige Class}^FP^193^Guardians of the pristine wilderness and defenders of the ancient trees, forest masters are the living embodiments of sentient nature.
+forsaker {Prestige Class}^MW^57^The forsaker rebels against the magic of the fantastic world around him.
+frenzied berserker {Prestige Class}^CW^34^The frenzied berserker is constantly seeking out more conflict to feed her craving for battle.
+frenzied berserker {Prestige Class}^MW^59^The frenzied berserker is constantly seeking out more conflict to feed her craving for battle.
+frost mage {Prestige Class}^Fr^58^Frost mages usually hail from lands of cold, snow, and ice: tundra, glaciers even outer planes perpetually shrouded in winter.
+frostrager {Prestige Class}^Fr^60^The frostragers are powerful and dangerous warriors believed by some to be gifted from (and others cursed by) the frost giant deity Thrym with an unstable but powerful supernatural battle rage.
+gatecrasher {Prestige Class}^MP^26^Gatecrashers see themselves as cosmic free agents, independent forces who can influence the natives of the planes and even the dynamic forces of magic itself.
+geomancer {Prestige Class}^CD^41^To the geomancer, all magic is the same.
+geomancer {Prestige Class}^MW^60^To the geomancer, all magic is the same.
+geometer {Prestige Class}^CAr^39^The geometer is the master of written magic and spells inscribed within a perfectly rendered diagram.
+ghost slayer {Prestige Class}^Gh^26^The ghost slayer studies ghost so that they can be dispatched easily and no longer bother the living with their presence.
+ghost-faced killer^CAd^51^Ghost-faced killers act as assassins and spies for hire, a mercenary clan that hides behind a guise of open and honorable conduct.
+ghostwalker {Prestige Class}^SF^20^Ghostwalkers have abilities that point to some underlying, mysterious mysticism.
+giant-killer^SM^109^Giant-killers are great heroes so long as they are killing giants.
+gladiator {Prestige Class}^SF^21^Rich or poor, all gladiators face death whenever they step into the arena.
+glorious servitor {Prestige Class}^LE^13^Glorious servitors are exceptionally loyal and devout servants of the Mulhorandi gods.
+gnome artificer  {Prestige Class}^Mag^23^Gnome artificers dabble in technology to create fantastic devices, delving into shadow magic when their mundane equipment is insufficient for the task.
+gnome giant-slayer^CW^36^The gnome giant-slayer relies on a combination of agility, combat prowess, and pure craftiness to deal with foes.
+goldeye {Prestige Class}^FP^194^Goldeyes are agents and promulgators of commercial intercourse, seeking to increase the wealth of their communities and realms by promoting the exchange of coins in trade.
+goliath liberator {Prestige Class}^RS^112^Goliath liberators are experts at infiltrating giant dwellings, freeing the captives within, then exacting revenge on the giants while the freed goliaths escape.
+Gray Hand enforcer {Prestige Class}^CSW^77^Gray Hand enforcers are highly trained members of the Gray Hands, Waterdeep's elite, high-powered fighting force.
+Great Rift deep defender {Prestige Class}^ShS^24^The Great Rift deep defender has a keen understanding of the importance of making a stand.
+Great Rift skyguard {Prestige Class}^Rac^183^The hippogriff-mounted skyguards of the Great Rift patrol the skies, ever watchful for the enemies of the gold dwarves.
+Great Sea corsair {Prestige Class}^ShS^26^Adapt the dread pirate prestige class from Song and Silenceto create the Great Sea corsair prestige class.
+Green Star adept {Prestige Class}^CAr^41^A Green Star adept is the master of the strange and powerful magic derived from Alhazarde's glittering green starmetal.
+guardian paramount {Prestige Class}^EL^30^The guardian paramount is an extraordinary bodyguard, a protector of others who is skilled in preventing harm to his charge.
+guild thief {Prestige Class}^FRCS^45^Guild thieves are thieves who operate in urban areas as part of an organized thieves' guild.
+guild wizard of Waterdeep {Prestige Class}^Mag^26^The wizards of the order study and exchange information, create magic items to help support the guild's financial independence, and offer their services to others in the city as watch-wizards or fire guards.
+halfling outrider {Prestige Class}^CW^38^Halfling outriders are elite champions whose task is to warn their fellows of, and protect them from, danger.
+halfling outrider {Prestige Class}^SF^22^The halfling outrider is naturally skilled in the arts of riding and scouting.
+Halruaan elder {Prestige Class}^ShS^27^Halruaan elders are the epitomy of magic cast with panache, and their dazzling and unique displays of arcane force make them the most respected practitioners in the land.
+Halruaan magehound {Prestige Class}^ShS^29^Magehounds are Halruaa's inquisitors.
+hammer of Moradin {Prestige Class}^PG^56^An elite order of warrior-priests stands ready to defend the dwarven people against the onslaught of fell giants, dark elves, and goblinoids.
+hand of the Adama {Prestige Class}^ShS^31^The hand of the Adama is a benign leader, judge and jury, and protector of the common folk all rolled into one.
+Harper agent {Prestige Class}^PG^58^Harper agents are the 'field agents' of the Harper organization, acting directly to gather intelligence and eliminate threats to the greater good.
+Harper mage {Prestige Class}^Mag^28^The Harper mage has two principal responsibilities: They aid the Harpers with spells and arcane knowledge, and they study, record, and pass on ancient lore.
+Harper paragon {Prestige Class}^PG^181^A Harper paragon actively promotes the welfare of other creatures while preventing evil forces from preying on innocents.
+Harper priest {Prestige Class}^Mag^29^Some Harpers choose to pursue a closer relationship to the deities who inspired the creation of the Harpers.
+harper scout {Prestige Class}^FRCS^46^Harper scouts are members of the Harpers, a secret society dedicated to holding back evil, preserving knowledge, and maintaining the balance between civilization and the wild.
+hathran {Prestige Class}^FRCS^47^Hathrans comprise an elite sisterhood of spellcasters who lead Rashemen.
+hathran {Prestige Class}^PG^59^Hathrans comprise an elite sisterhood of spellcasters who lead Rashemen.
+havoc mage {Prestige Class}^MH^20^The havoc mage shares as much in common with a fighter as with a wizard.
+heartwarder {Prestige Class}^FP^196^Heartwarders are aesthetes and hedonists who actively seek out pleasure and beauty in all things and who nurture the creation of beautiful objects.
+heir of Siberys {Prestige Class}^ECS^80^The magic of a Siberys mark is undeniably powerful, and an heir of Siberys manifests one.
+henshin mystic {Prestige Class}^OA^39^Henshin mystics are members of a monastic order that teaches what they consider a great mystery of the universe: that humanity is capable of a transformation (henshin) into divinity.
+hexer {Prestige Class}^MW^63^The hexer uses the power of his gaze.
+Hida defender {Prestige Class}^OA^212^Hida defenders train in great armor, a unique characteristic that fits in well with the Crab philosophy of strength and endurance.
+hidecarved dragon {Prestige Class}^Dr^94^Hidecarved dragons are members of an enigmatic order of dragons and half-dragons.
+hierophant {Prestige Class}^DMG^188^A divine spellcaster who rises high in the service of his deity gains access to spells and abilities of which lesser faithful can only dream.
+hierophant {Prestige Class}^FRCS^48^A divine spellcaster who rises high in the service of his deity gains access to spells and abilities of which lesser faithful can only dream.
+high proselytizer {Prestige Class}^EL^31^The high proselytizer is the holy inspiration that begins religious movements.
+highland stalker {Prestige Class}^CAd^54^Highland stalkers are consummate trackers with an instinctive knowledge of their mountainous territories.
+hin fist {Prestige Class}^ShS^32^Adapt the sacred fist prestige class from Complete Divineto create a hin fist.
+Hoardstealer {Prestige Class}^Dr^130^The hoardstealer specializes in relieving wealthy individuals from large amounts of said wealth.
+holy liberator {Prestige Class}^CD^45^The holy liberator is a holy warrior, a distant cousin of the paladin.
+holy liberator {Prestige Class}^DF^57^The holy liberator is a holy warrior, a distant cousin of the paladin.
+hordebreaker {Prestige Class}^SM^110^The hordebreaker is a person who makes destroying the horde threat the perfect engine of orc destruction.
+horizon walker {Prestige Class}^DMG^189^The horizon walker is an unceasing traveler to the universe's most dangerous places.
+horned harbinger {Prestige Class}^FP^197^The horned harbingers are agents of the fallen Lord of Bones.
+hospitaler {Prestige Class}^CD^48^Hospitalers are a fighting force of necessity, sworn to poverty, obedience, and the defense of those in their care.
+hospitaler {Prestige Class}^DF^60^Hospitalers are a fighting force of necessity, sworn to poverty, obedience, and the defense of those in their care.
+hulking hurler {Prestige Class}^CW^40^Hulking hurlers belong to those races of generously proportioned creatures who enjoy nothing more than wrenching boulders, trees, and even buildings free of their earthly bonds and throwing them at their foes.
+hunter of the dead {Prestige Class}^CW^42^The hunter of the dead spends each restless night tracking undead to their lairs and cleansing the land of their foul presence.
+hunter of the dead {Prestige Class}^DF^62^The hunter of the dead spends each restless night tracking undead to their lairs and cleansing the land of their foul presence.
+iaijutsu master {Prestige Class}^OA^41^Iaijutsu masters harness their ki energy to strike with blinding speed and devastating power.
+iInitiate of the draconic mysteries {Prestige Class}^Dr^131^Some become students of draconic knowledge that leads to greater power.
+illithid body tamer {Prestige Class}^Und^35^Illithids who embrace the Tamer Creed believe that military might is the most important factor in their race's future mastery of the multiverse.
+illithid savant {Prestige Class}^SS^77^The illithid savant is an academic who deals in applied science, acquiring new knowledge from the brains he consumes.
+illithid slayer {Prestige Class}^XPH^146^Illithid slayers have dedicated their lives to the eradication of the mind flayer 'infection.'
+Imaskari vengeance taker {Prestige Class}^Und^37^A secret society dedicated to righting wrongs, the Imaskari vengeance takers are trained by hidden masters in the rites and rituals of revenge.
+incantatrix {Prestige Class}^Mag^31^The incantatrixes are the practitioners of metamagic in Faerun, studying spells that affect other spells and having a fondness for magic that thwarts extraplanar beings.
+incantatrix {Prestige Class}^PG^61^The incantatrixes are the definitive practitioners of metamagic in Faerun, devoting themselves to the study of spells and techniques that affect other spells.
+initiate of Pistis Sophia {Prestige Class}^BE^64^The path of these initiates requires great sacrifices (in the form of at least three sacred vows), but brings great rewards of spiritual power.
+Initiate of the Sevenfold Veil {Prestige Class}^CAr^44^A master of defensive magic, the Initiate of the Sevenfold Veil approaches the prismatic barrier by mastering one by one its constituent veils or layers.
+inquisitor {Prestige Class}^DCS^80^By definition, an inquisitor is one who inquires, someone who hunts for people, information, or answers.
+inquisitor of the Drowning Goddess {Prestige Class}^Und^39^Some kuo-toa monks go on to become inquisitors of the Drowning Goddess, who are tasked with protecting the community from inside threats.
+invisible blade {Prestige Class}^CW^44^Invisible blades are deadly fighters who prefer to use daggers and related weapons in combat.
+iron mind {Prestige Class}^RS^114^Elite warriors trained to resist mental compulsions of all kinds, members of the iron mind prestige class defend dwarf and gnome kingdoms against intrusions by mind flayers, dark elf enchanters, and the like.
+Jordain vizier {Prestige Class}^ShS^33^The Jordaini are a special servitor caste, though still upper class, in the magocracy of Halruaa.
+justice of weald and woe {Prestige Class}^CR^48^The justice of weald and woe is the go-to person when something unsavory -- usually involving the removal of humans -- needs doing.
+justiciar {Prestige Class}^CW^47^Justiciars make a living kicking the daylights out of criminals who desperately deserve it.
+justiciar of Taiia {Prestige Class}^DD^205^Justiciars of Taiia fulfill the role of carrying out Taiia's sentence against wrongdoers.
+justiciar of Tyr {Prestige Class}^PG^63^Justiciars are the very elite of Tyr's mortal servants, and they act as living embodiments of their god's portfolio.
+Keeper of the Cerulean Sign {Prestige Class}^LoM^194^The Cerulean Sign is an ancient rune of power, created untold eons ago by a race or deity long since vanished; now it has keepers.
+kensai {Prestige Class}^CW^49^The kensai masters body, mind, weapon, and will.
+king/queen of the wild^MW^65^Where nature's fury is at its height, there you'll find the kings and queens of the wild.
+kishi charger {Prestige Class}^OA^42^Kishi chargers are cavalry soldiers trained to make the greatest possible use of a horse's speed and a rider's agility.
+Knight of the Blue Moon {Prestige Class}^CSW^81^Knights of the Blue Moon are elite soldiers in the endless battle against the Mistress of the Night.
+knight of the Chalice {Prestige Class}^CW^53^The knight of the Chalice is a member of an elite knightly organization devoted to fighting demons and other evil outsiders.
+knight of the Chalice {Prestige Class}^DF^63^The knight of the Chalice is a member of an elite knightly organization devoted to fighting demons and other evil outsiders.
+Knight of the Crown {Prestige Class}^DCS^56^The Order of the Crown is the first tier of the Solamnic Knights.
+Knight of the Iron Glacier {Prestige Class}^Fr^62^The Knights of the Iron Glacier continue to honor the memory of General Aengrist in their deeds and actions.
+Knight of the Lily {Prestige Class}^DCS^63^The Knights of the Lily are the order of warriors within the Knights of Neraka.
+knight of the Middle Circle {Prestige Class}^DF^65^Knights of the Middle Circle provide security for Stargazer chapterhouses and may be called upon for similar service for allies of the Stargazers.
+Knight of the Rose {Prestige Class}^DCS^59^The Knights of the Rose are the highest tier of the Solamnic Knights.
+Knight of the Skull {Prestige Class}^DCS^65^Entering battle with strength and divine magic, Knights of the Skull are the spirit of the Dark Knights.
+Knight of the Sword {Prestige Class}^DCS^58^Knights of the Sword are warriors of the Solamnic Knights who fight with power and faith to defend justice and truth.
+Knight of the Thorn {Prestige Class}^DCS^66^The Knights of the Thorn are also known as the 'gray robes' for the ash-colored robes they wear to indicate that they do not serve the Orders of High Sorcery.
+knight protector {Prestige Class}^CW^55^Knight protectors are martial characters dedicated to restoring the ideals of knightly chivalry before they fade forever.
+Knight Protector of the Great Kingdom {Prestige Class}^SF^24^Knight protectors are martial characters dedicated to restoring the ideals of knightly chivalry before they fade forever.
+knight-errant of Silverymoon^SM^112^Charged with the safety of the city of Silverymoon and its citizens, the professional fighting force known as the Knights in Silver is often all that stands between Silverymoon and the dangers of the frontier.
+lasher {Prestige Class}^SF^25^The lasher prestige class uses the whip as an extension of herself.
+legendary dreadnought {Prestige Class}^EL^33^The legendary dreadnought is the ultimate foot soldier, an absolute force of destruction, a total warrior who excels at sheer combat prowess.
+legendary leader {Prestige Class}^HB^107^Legendary leaders are the stuff of bards' tales come to life.
+legendary tactician {Prestige Class}^DCS^81^Legendary tacticians are respected (or feared) for their ability to inspire their troops.
+lifedrinker {Prestige Class}^BV^63^Lifedrinkers are vampires who have been undead for a very long time.
+lion of Talisid {Prestige Class}^BE^65^The lions of Talisid protect nature and emulate their patron in more concrete ways.
+lord of tides {Prestige Class}^Sa^70^A lord of tides can sense the movement of magma, summon beings of elemental might, and open portals to the Elemental Planes.
+loredelver {Prestige Class}^RD^117^Loredelvers are illumian spellcasters who find and explore ruins, disable the magical protections that guard them, and sift through the ancient secrets found within.
+loremaster {Prestige Class}^DMG^191^Loremasters are spellcasters who concentrate on knowledge, valuing lore and secrets over gold.
+luckstealer {Prestige Class}^RW^118^As a luckstealer, you're part spellcaster, part professional gambler -- and 100% mischief-maker.
+Luiren marchwarden {Prestige Class}^ShS^35^The Luiren marchwarden is the defender of the frontier in the land of the halflings.
+lurking terror {Prestige Class}^LM^54^Lurking terrors are the quintessential hunting undead, displaying great prowess with their special abilities and amazing powers of stealth.
+maester {Prestige Class}^CAd^56^Maesters are the master crafters of the gnome world.
+mage of the Arcane Order {Prestige Class}^CAr^48^Also called a 'guildmage,' a member of this prestige class is a spellcaster who belongs to an academy and guild known as the Arcane Order.
+mage of the Arcane Order {Prestige Class}^TB^60^Also called a 'guildmage,' a member of this prestige class is a spellcaster who belongs to an academy and guild known as the Arcane Order.
+mage-killer^Mag^32^Mage-killers master magic designed for combat against other spellcasters.
+magelord {Prestige Class}^LE^17^Quick to anger, haughty, and proud of his Art, the magelord is an arcane spellcaster who studies an ancient magical tradition known for extremely fast and versatile spellcasting
+maho-bujin^OA^236^When Taint overcomes a character, she may find her way to the Festering Pit of Fu Leng and become a maho-bujin.
+maho-tsukai^OA^237^Maho (blood magic) wielders are called maho-tsukai.
+maiden of pain {Prestige Class}^PG^182^Loviatar's most dedicated servants, the maidens of pain, are depraved women who literally make pain their meat and drink.
+Mantis mercenary {Prestige Class}^OA^231^Mantis mercenaries make use of peasant weapons and a rolling motion.
+Maquar crusader {Prestige Class}^ShS^38^A Maquar crusader follows a strict code of conduct that not only limits what he can own or where he can live, but also limits the ways in which merchants can influence him.
+martyred champion of Ilmater {Prestige Class}^PG^184^Having already offered his life in sacrifice once, the martyred champion of Ilmater perseveres in Ilmater's faith.
+master alchemist {Prestige Class}^Mag^34^The master alchemist is a spellcaster who specializes in producing potions and elixirs that reproduce the effects of spells of 4th level or higher.
+master inquisitive {Prestige Class}^ECS^82^The master inquisitive takes the art of investigation and deduction to the ultimate level, rising to the top of the field.
+master of chains {Prestige Class}^SF^27^The master of chains is a combatant specializing in the use of chains -- specifically the spiked chain -- as a weapon.
+master of flies {Prestige Class}^SS^80^The master of flies is an intelligent swarm that can form a massive being at need, or a single creature that can dissolve into a cloud of vermin.
+master of many forms {Prestige Class}^CAd^58^A master of many forms has no shape that she calls her own.
+master of radiance {Prestige Class}^LM^44^Masters of radiance channel the pure, undiluted power of the sun.
+master of shrouds {Prestige Class}^DF^66^The master of shrouds is an evil spellcaster who magically seizes incorporeal undead and sets them to do her bidding.
+master of shrouds {Prestige Class}^LM^46^The master of shrouds is an evil spellcaster who magically seizes incorporeal undead and sets them to do her bidding.
+master of the unseen hand {Prestige Class}^CW^60^Masters of the unseen hand delight in crushing their foes with invisible force, flinging massive objects into the sky, and disarming enemies with a single thought.
+master of the Yuirwood {Prestige Class}^Una^24^The masters of the Yuirwood are an elite group of foresters who work to keep the ancient Yuirwood free of evil influence.
+master of vipers {Prestige Class}^SK^163^Outcast yuan-ti learn to hunt not only to feed themselves, but also to spread destruction far and wide for the pure pleasure of it.
+master samurai {Prestige Class}^SF^29^The master samurai is a military retainer of a feudal overlord; he practices a code of behavior that emphasizes the value of personal honor over life itself.
+master thrower {Prestige Class}^CW^58^Master throwers depend on quick reflexes, good planning, and deadly aim.
+master transmogrifist {Prestige Class}^CAr^51^The master transmogrifist is a sorcerer or wizard who has chosen to specialize in spells that change his form.
+master vampire {Prestige Class}^LM^55^Any vampire can create spawn, but it takes a very special vampire to rule over an entire gang of minions.
+menacing brute {Prestige Class}^RD^123^The menacing brute takes advantage of how must humans fear half-orcs, playing on that dread to make his living.
+metamind {Prestige Class}^XPH^147^Metaminds know that accumulating the most power in the shortest time is the key to psionic superiority.
+mindbender {Prestige Class}^CAr^54^Mindbenders seek to control the thoughts and dreams of others.
+mindbender {Prestige Class}^TB^63^Mindbenders seek to control the thoughts and dreams of others.
+mindspy {Prestige Class}^CW^62^By reading the minds of her enemies, a mindspy knows exactly what they're going to do a fraction of a second before they do it.
+Mirumoto niten master {Prestige Class}^OA^218^The Mirumoto school teaches a unique style of swordplay, rooted in this sense of duty.
+monk of the long death {Prestige Class}^PG^65^Monks of the long death are members of a macabre, secretive order of scholars seeking to understand the true nature of death.
+moonspeaker {Prestige Class}^RE^143^Bound to the magic of their lycanthrope ancestors, moonspeakers breathe the magic of the world, guided by the twelve moons of Eberron.
+Moonstar agent {Prestige Class}^CSW^84^Moonstar agents, also known as Teukiir, are members of the Tel Teukiira, a group founded by Khelben 'Blackstaff' Arunsun when he broke from the Harpers.
+morninglord of Lathander {Prestige Class}^PG^66^Morninglords are, in many ways, the epitome of the classical cleric archetype.
+mortal hunter {Prestige Class}^BV^64^Mortal hunters are fiends who specialize in killing mortals.
+Moto avenger {Prestige Class}^OA^228^The Moto avenger is dedicated to a war against the Shadowlands and its evils.
+mystic theurge {Prestige Class}^DMG^192^Blurring the line between divine and arcane, mystic theurges draw power from divine sources and musty tomes alike.
+mystic wanderer {Prestige Class}^Mag^35^Mystic wanderers are divine spellcasters who eschew normal church hierarchies and instead embrace freedom, wanderlust, and independence.
+naga overlord {Prestige Class}^SK^165^Naga overlords are evil masterminds who operate in secret, usually behind cults of devoted followers.
+Nar demonbinder {Prestige Class}^Una^25^Master of the black art of demon summoning, the Nar demonbinder keeps alive the sinister traditions of the old Empire of Narfell.
+nature's warrior^CW^63^Nature's warriors are defenders of the wild, protectors of the natural world . . . and often druids who have spent 'too much time' in wild shape form.
+Nentyar hunter {Prestige Class}^Una^28^Sworn to defend the great forests and serve the Nentyarch, druidic ruler of the Circle of Leth, the Nentyar hunters roam the wild lands of the East, uprooting foul and evil things.
+Night Mask deathbringer {Prestige Class}^CR^53^Night Mask deathbringers are highly trained members of the Westgate thieves' guild who have caught the favorable attention of the vampires in charge of the organization.
+nightcloak {Prestige Class}^FP^198^Nightcloaks are the apple of Shar's eye -- devoted to her vision, preserving her secrets, practicing her magic, as twisted and bitter as it is.
+nightsong enforcer {Prestige Class}^CAd^60^The enforcers of the Nightsong Guild focus on the stealth-centered combat training that rogues usually learn.
+nightsong infiltrator {Prestige Class}^CAd^62^The nightsong infiltrator is the perfect thief and the perfect spy.
+Ninja of the Crescent Moon {Prestige Class}^SF^30^The Ninja of the Crescent Moon is a mercenary clan whose members engage in sabotage and other covert missions for an outlandish fee.
+ninja spy {Prestige Class}^OA^43^True ninja spies are masters of exotic weapons, tools of stealth, and strange ki powers.
+occult slayer {Prestige Class}^CW^66^The occult slayer is driven to confront any arcane or divine spellcaster who crosses her path.
+ocular adept {Prestige Class}^FP^200^Ocular adepts have pledged their religious devotions to the alien entity known as the Great Mother, the deity matron of all beholders.
+Olin Gisir {Prestige Class}^LE^21^The Olin Gisiae are elite elf mages who have taken it upon themselves to guard dark secrets from the rest of the world
+ollam {Prestige Class}^CAd^66^In Dwarven, the world 'ollam' means teacher.
+oozemaster {Prestige Class}^MW^67^Oozemasters relate one-on-one with things that relate to nothing at all.
+orc scout {Prestige Class}^SM^114^Part wilderness warrior and part spy, the orc scout is a hero to his people.
+orc warlord {Prestige Class}^Rac^184^The orc warlord is a savage general of an unruly army, the leader of one of the deadly and all too common orc hordes that rampage down from the Spine of the World.
+Order of the Bow initiate {Prestige Class}^CW^68^By learning the meditative art of the Way of the Bow, the archer improves his discipline, precision, and spirituality.
+Order of the Bow Initiate {Prestige Class}^SF^32^By learning the meditative art of the Way of the Bow, the archer improves his discipline, precision, and spirituality.
+outcast champion {Prestige Class}^RD^126^Outcast champions bring hope to those who have no place in society.
+outlaw of the crimson road {Prestige Class}^SaS^10^An outlaw of the crimson road might be a revolutionary, a loyal supporter of some deposed ruler, or merely an ordinary individual who angered the wrong person at the wrong time.
+pale master {Prestige Class}^LM^47^Arcane casters can become pale masters, who draw on necromantic lore that provides a macabre power all its own.
+pale master {Prestige Class}^TB^64^Arcane casters can become pale masters, who draw on necromantic lore that provides a macabre power all its own.
+peerless archer {Prestige Class}^SM^115^The peerless archer devotes her life to perfecting her skill with the bow.
+peregrine runner {Prestige Class}^RS^116^When goliaths need to send a message to another tribe, they send an elite, fleet-of-foot warrior known as a peregrine runner.
+perfect wight {Prestige Class}^EL^34^The perfect wight is a master of skulking, the ultimate prowler and thief.
+pious templar {Prestige Class}^CD^50^Sworn to the defense of a temple site, the pious templar is a holy warrior blessed by her deity with combat prowess and great endurance.
+planar champion {Prestige Class}^MP^28^The planar champion moves between the planes, always driven to battle.
+planeshifter {Prestige Class}^MP^30^The planeshifter is a magical scholar and expert in planar travel, and through arcane research develops not only the ability to sense planar portals, but also the ability to create his own demiplane.
+platinum knight {Prestige Class}^Dr^133^The platinum knight protects good-aligned dragonkind from their natural enemies.
+prestige bard {Prestige Class}^UA^69^The prestige bard is a jack-of-all-trades, master of none.
+prestige paladin {Prestige Class}^UA^70^After training in the arts of combat and the mysteries of the divine, the prestige paladin is anointed as a holy warrior dedicated to the protection of law and goodness.
+prestige ranger {Prestige Class}^UA^71^The prestige ranger navigates the dark forests, craggy mountains, or desert wastes of her homeland with unparalleled skill.
+prime Underdark guide {Prestige Class}^Und^40^These skilled guides not only know how to overcome the physical challenges of the Underdark, but they also can help them over the social and cultural hurdles they are sure to face.
+primeval {Prestige Class}^Fr^65^The primeval is a warrior who has tapped into his racial memories to find and forge a bond with an ancient creature.
+prophet of Erathaoi {Prestige Class}^BE^66^The prophet of Erathaoi is a seer and visionary, a medium of the heavenly will, pronouncing judgment on corruption and evil in the world.
+psion uncarnate {Prestige Class}^XPH^148^Formless, fleshless, and unbound by the limits of corporeality -- this is the goal of every psion uncarnate.
+Purple Dragon knight {Prestige Class}^CW^70^Purple Dragon knights develop uncanny skills related to coordinating and leading soldiers.
+Purple Dragon knight {Prestige Class}^FRCS^49^Purple Dragon knights develop uncanny skills related to coordinating and leading soldiers.
+Purple Dragon knight {Prestige Class}^PG^68^Purple Dragon knights develop uncanny skills related to coordinating and leading soldiers.
+pyrokineticist {Prestige Class}^XPH^151^Pyrokineticists know that a little psionic power goes a long way -- for those interested in fire.
+quori nightmare {Prestige Class}^RE^148^The quori nightmare taps into the primal horrors and urges of the subconscious.
+radiant servant of Pelor {Prestige Class}^CD^52^The radiant servants of Pelor put the dogma of demonstrating strength through charity and modesty into living practice.
+rage mage {Prestige Class}^CW^72^The rage mage's approach to magic is based on the primal passion of magic more than the studious quasi-scientific approach.
+rainbow servant {Prestige Class}^CD^54^Those who have learned what the couatl temples have to offer are known as rainbow servants.
+Raumathari battlemage {Prestige Class}^Una^29^Employing sword and spell with dauntless courage and deadly force, the handful of Raumathari battlemages remaining in the world comprise a lonely and little-known order of adventurers, explorers, and mercenaries in search of battle.
+ravager {Prestige Class}^CW^73^The infamous ravager has dedicated himself to the service of Erythnul, deity of slaughter.
+ravager {Prestige Class}^SF^33^The infamous ravager has dedicated himself to the service of Erythnul, deity of slaughter.
+reachrunner {Prestige Class}^RE^153^Known for their mastery of the untamed world, some shifters rise above others in woodslore, physical ability, and stamina to claim the revered mantle of the reachrunner -- the greatest of shifter trackers and scouts.
+reaping mauler {Prestige Class}^CW^75^Reaping maulers are the back-breakers, the limb-twisters, and the neck-snappers among pit fighters.
+recaster {Prestige Class}^RE^157^To the recaster, the change her own body is capable of is a simple reflection of the mutability of the world around her.
+Red Avenger {Prestige Class}^SF^34^The Red Avenger is the master of ki,an ancient and formidable discipline that allows the user to accomplish the extraordinary.
+Red Wizard {Prestige Class}^DMG^193^The Red Wizards are the masters of Thay, the would-be magical overlords of the land of Faerun.
+Red Wizard {Prestige Class}^FRCS^50^The Red Wizards are the masters of Thay, the would-be magical overlords of the land of Faerun.
+reforged {Prestige Class}^RE^161^The reforged represents the realized ideal of the warforged's living aspects.
+righteous zealot {Prestige Class}^DCS^83^The righteous zealot is a person with a cause that directs every aspect of life.
+rimefire witch {Prestige Class}^Fr^67^A rimefire witch is one who has followed a mysterious call to the core of a rimefire iceberg and becomes infused with great power by the rimefire eidolon.
+risen martyr {Prestige Class}^BE^68^A risen martyr is an exalted character who continues in his earthly existence after his martyrdom in order to finish some unfinished task.
+ronin {Prestige Class}^CW^77^A ronin is a masterless warrior cast adrift in the world, but still clinging to the remnants of his former life.
+royal explorer {Prestige Class}^SaS^13^Some monarchs sponser crack teams of explorers.
+ruathar {Prestige Class}^RW^122^Also known as 'elf-friend' or 'star-friend,' a ruathar is a person of some other race who has earned the special friendship of the elven folk.
+runecaster {Prestige Class}^FRCS^51^Those that choose to master the ability to create runes of power are runecasters.
+runecaster {Prestige Class}^PG^69^Those that choose to master the ability to create runes of power are runecasters.
+runescarred berserker {Prestige Class}^Una^31^Deadly barbarians who bear magical runes carved into their flesh, runescarred berserkers are among the most feared of Rashemen's defenders.
+runesmith {Prestige Class}^RS^118^A runesmith has learned to harness the power of runes and can fling fireballsand other staple arcane spells even while encased in full plate armor.
+sacred exorcist {Prestige Class}^CD^56^Sacred exorcists hope to drive away the spiritual forces of evil, prevening them from causing harm to the bodies and souls of humanity.
+sacred exorcist {Prestige Class}^DF^68^Sacred exorcists hope to drive away the spiritual forces of evil, prevening them from causing harm to the bodies and souls of humanity.
+sacred fist {Prestige Class}^CD^59^Sacred fists are independent organizations found within many temples.
+sacred fist {Prestige Class}^DF^70^Sacred fists are independent organizations found within many temples.
+sacred purifier {Prestige Class}^LM^49^Sacred purifiers are priestly characters who specialize in destroying undead.
+sacred warder of Bahamut {Prestige Class}^Dr^96^Sacred warders of Bahamut protect others from the power of Tiamat's brood.
+sanctified mind {Prestige Class}^LoM^198^A sanctified mind believes that all evil-aligned psionics-using creatures must be crushed.
+sand shaper {Prestige Class}^Sa^76^Sand shapers are part prophet, part priest, part magician, and part assassin.
+savant aboleth {Prestige Class}^LoM^21^Savant aboleths are the eldest, most intelligent, wisest and most forceful of personality.
+scaled horror {Prestige Class}^SS^83^Scaled horrors are elite amphibious soldiers.
+scar enforcer {Prestige Class}^RD^130^Scar enforcers are angry, embittered half-elves who have rejected both sides of their ancestry.
+scion of Tem-Et-Nu^Sa^82^Paladins of the temple of Tem-Et-Nu are sometimes selected to become the guardians of the rivers.
+scorpion heritor {Prestige Class}^Sa^86^Scorpion heritors, through a special relationship with the scorpion spirit, gain the mystical abilities of the scorpion, and can even take its shape.
+scourge maiden {Prestige Class}^ShS^40^Scourge maidens are warrior-priestesses of Loviatar dedicated to pain and anguish.
+Sea Mother whip {Prestige Class}^Und^42^Devout worshipers of Blibdoolpoolp who seek closer communion with the Sea Mother often gain additional abilities in the Sea Mother whip prestige class.
+seeker of the Misty Isle {Prestige Class}^CD^61^Seekers search for the lost elves of Misty Isle.
+seeker of the song {Prestige Class}^CAr^56^Seekers of the song wield the power of music in ways that amaze even the most skilled bards.
+sentinel of Bharrai {Prestige Class}^BE^69^Respect for the power of nature, the desire to further the ends of good, and the resolve to destroy evil are the core beliefs of a sentinel of Bharrai.
+serpent slayer {Prestige Class}^SK^166^Some individuals devote their entire lives to thwarting the yuan-ti.
+Shaaryan hunter {Prestige Class}^PG^71^On the backs of their swift horses, Shaaryan hunters can run down even the fastest prey and either spear it with a lance or pelt it with arrows from horseback.
+shade hunter {Prestige Class}^CR^58^The shade hunter is a breed of adventurer who lives for the thrill of finding lost treasure, defeating ancient traps, and surviving deadly curses laid by the priests of dead gods.
+shadow adept {Prestige Class}^FRCS^52^Shadow adepts hurl themselves into the abyss of the Shadow Weave, immediately acquiring all the gifts available to casual students and discovering secrets unavailable to all but the most dedicated.
+shadow adept {Prestige Class}^PG^72^Shadow adepts hurl themselves into the abyss of the Shadow Weave, immediately acquiring all the gifts available to casual students and discovering secrets unavailable to all but the most dedicated.
+shadow scout {Prestige Class}^OA^44^The camouflage of a tiger, the stamina of a horse, the eyes of an eagle: these are the ingredients of the shadow scouts.
+shadow sentinel {Prestige Class}^RD^137^Shadow sentinels are elite illumian warriors who protect their people from githyanki raiders, demonic invastions, and hordes of barely imaginable monsters from the Plane of Shadow.
+shadow thief of Amn {Prestige Class}^PG^74^A shadow thief of Amn knows only her own minions, her coworkers, and her superior.
+shadowbane inquisitor {Prestige Class}^CAd^68^Shadowbane inquisitors battle incessantly against evil in whatever form it takes.
+shadowbane stalker {Prestige Class}^CAd^70^Shadowbane stalkers find evil hidden in civilized areas so that the martial arm of the order (the inquisitors) can spearhead the attack.
+shadowcraft mage {Prestige Class}^RS^120^Some gnomes have an even greater affinity for illusions than the average representative of their race, resulting in the prestige class known as the shadowcraft mage.
+shadowcrafter {Prestige Class}^Und^43^Shadowcrafters long ago mastered illusions and glamers.
+shadowdancer {Prestige Class}^DMG^194^Operating in the border between light and darkness, shadowdancers are nimble artists of deception.
+shadowmind {Prestige Class}^CAd^74^A shadowmind blends psionic powers and uncanny stealth into an effective whole.
+shapeshifter {Prestige Class}^OA^45^Shapeshifters must already have some means of changing their form before learning to master that change.
+Sharn skymage {Prestige Class}^Sh^165^By studying the properties of the manifest zone in which Sharn is situated, learning its intricacies and methods for manipulating it, a spellcaster can improve her magical or natural ability to fly.
+Shiba protector {Prestige Class}^OA^222^The warriors of the Shiba family are sworn to protect the Isawa family.
+shifter {Prestige Class}^MW^68^The shifter has no form that she calls her own.
+shining blade of Heironeous {Prestige Class}^CD^63^The shining blade of Heironeous is a member of an order of knights dedicated to prowess in melee combat.
+Shintao monk {Prestige Class}^OA^46^Shintao monks are dedicated to following the teachings of Shinsei.
+Shou disciple {Prestige Class}^Una^32^Shou disciples are martial artists who have studied or observed the monks of Kara-Tur and seek to emulate their style.
+silverstar {Prestige Class}^FP^201^Silverstars are dedicated advocates of freedom and tolerance, wanderers on the path of truth, and absolute foes of Shar.
+singh rager {Prestige Class}^OA^48^Singh ragers draw their furious strength from the noble lion.
+siren {Prestige Class}^SS^84^A harpy siren is an artist who constantly seeks to expand and improve upon her innate sonic ability.
+skullclan hunter {Prestige Class}^MH^20^The skullclan hunter is the acclaimed foe of unlife.
+skylord {Prestige Class}^BE^71^An elf crusader, the skylord uses his kinship with creatures of the sky and the power of the winds to fight evil.
+skypledged {Prestige Class}^RW^126^The skypledged represent a mystical tradition among the raptorans that hearkens back to an ancient pact with powerful lords of the Elemental Plane of Air.
+slaad brooder {Prestige Class}^SS^87^The brooder's sole purpose is to implant as many eggs pellets as he can to produce the widest possible range of progeny.
+slayer of Domiel {Prestige Class}^BE^73^Sometimes the skillset of an assassin is required for more noble pursuits.
+slime lord {Prestige Class}^PG^186^Slime lords, the most favored of Ghaunadar's servants, are not clerics; they are spies and infiltrators who can change their shapes in order to move unnoticed among members of any race.
+Soldier of Light {Prestige Class}^DD^208^The Soldiers of Light are a military order dedicated to open warfare against the minions of their church's enemies.
+soul eater {Prestige Class}^BV^66^The soul eater is a monstrous being that feeds on the very essence of life force.
+spell scion {Prestige Class}^UA^167^This prestige class is for characters who wield legendary weapons designed for use by arcane spellcasters, such as wizards, sorcerers, and sometimes bards.
+spellcarved soldier {Prestige Class}^RE^166^Spellcarved soldiers are warforged warriors who engrave magic runes into the plating of their inherently magic bodies, gaining remarkable defensive abilities.
+spelldancer {Prestige Class}^Mag^37^Spelldancers are an energetic sort of spellcaster who draw on the quasi-primal energy of song and dancing to power their magic.
+spellfire channeler {Prestige Class}^Mag^38^Those who practice their spellfire can hone their talent into a tool with fantastic abilities that most dabblers can only dream of.
+Spellguard of Silverymoon {Prestige Class}^PG^75^The Spellguard, Silverymoon's elite cadre of battle-trained arcane spellcasters, protects the city against the threat of hostile magic and aids the Knights in Silver against more mundane threats.
+spellsinger {Prestige Class}^Rac^185^Spellsingers are rare practitioners of an ancient elven bardic tradition.
+spellsword {Prestige Class}^CW^79^The dream of melding magic and weaponplay is fulfilled in the person of the spellsword.
+spellsword {Prestige Class}^TB^67^The dream of melding magic and weaponplay is fulfilled in the person of the spellsword.
+Spur Lord {Prestige Class}^LD^11^The Spur Lords are elite zealots of the church, wielding the dark power of Cyric and commanding the attention of even the most fanatical clerics.
+spymaster {Prestige Class}^CAd^76^Spymasters do their work quietly and in private, and they often have a cover identity.
+spymaster {Prestige Class}^SaS^14^Spymasters do their work quietly and in private, and they often have a cover identity.
+stalker of Kharash {Prestige Class}^BE^75^The stalkers of Kharash are a loose-knit order of rangers, rogues, and other characters devoted to fighting evil under Kharash's patronage.
+steel legionnaire {Prestige Class}^DCS^68^Steel legionnaires are members of the Legion of Steel.
+stoneblessed {Prestige Class}^RS^122^A stoneblessed bonds to the stone of the mountains, blending into a dwarf, gnome, or goliath community and making it her home.
+stonedeath assassin {Prestige Class}^RS^124^Most stonedeath assassins are hobgoblin rogues or rangers, but bugbears and even exceptional goblins have been known to undertake stonedeath training and learn the ways of infiltrating dwarf strongholds by disarming traps, weakening gates, and assassinating dwarf leaders.
+stonelord {Prestige Class}^CW^81^The earth whispers to special dwarves known as stonelords.
+stonespeaker guardian {Prestige Class}^RS^127^The stonespeaker guardian taps into the divine power of the earth itself to defend her fellow stonespeakers, as well as other goliaths and friendly races, from their enemies.
+stormlord {Prestige Class}^CD^65^Stormlords often live as brigands, indulging their personal desires for wealth, food, luxury items, and wanton behavior as they crave random, spectacular acts of violence.
+stormlord {Prestige Class}^FP^203^Stormlords are the chief agents of the Destroyer's wrath, inflicting destructive rampages wherever they wander in order to spread word of his endless fury.
+stormsinger {Prestige Class}^Fr^70^The stormsingers have learned the secret methods of harnessing the magic powers of music to influence and control the weather.
+stormtalon {Prestige Class}^RW^131^The stormtalons are consummate aerial warriors, using both their weapons and their razor-sharp foot talons to dive on their hapless foes.
+streetfighter {Prestige Class}^CAd^79^Streetfighters seek the challenges of the back alleys as a way of testing themselves and their experience in the wilder world.
+strifeleader {Prestige Class}^FP^204^Strifeleaders are the chief instruments of the Dark Sun, charged with spreading the One True Way of Cyric through force and deception.
+sublime chord {Prestige Class}^CAr^60^In return for abandoning her continuing study of bardic music, a sublime chord instead masters a number of spells more powerful than most bards can ever use.
+Suel arcanamach {Prestige Class}^CAr^63^Arcanamach formerly served as elite guards and agents for powerful wizards.
+Sun Soul monk {Prestige Class}^CSW^88^Monks of the Sun Soul Order believe that they each harbor a small fragment of the sun's divine essence.
+sunmaster {Prestige Class}^LE^25^The sunmasters are members of a sect within the church of Lathander who believe that the Morninglord is the living reincarnation of Amaunator.
+survivor {Prestige Class}^SS^89^Those who survive a program of frequent assaults and other dangers emerge a few weeks later -- tougher, faster, and less vulnerable to attacks.
+swanmay {Prestige Class}^BE^76^Swanmays are members of a secretive order sworn to protect wilderness areas from evil.
+swift scion {Prestige Class}^UA^168^This prestige class is for those who wield legendary weapons that make use of or improve the wielder's stealth, speed, or dexterity (in the general sense).
+sword dancer {Prestige Class}^FP^205^Sword dancers are expected to lead the drow migration and work to promote harmony between drow and surface-dwelling races.
+sword of righteousness {Prestige Class}^BE^77^Pursuit of a commitment to righteousness and purity that exceeds the norm is a quality of a sword of righteousness.
+sybil {Prestige Class}^SS^90^Steeped in ancient lore, or maddened by divine inspiration, the sybil is a reclusive prophet.
+tactical soldier {Prestige Class}^MH^22^The tactical soldier is the master of teamwork in melee.
+tainted sorcerer {Prestige Class}^UA^191^Tainted sorcerers find an easy path to tremendous magical power.
+tainted warrior {Prestige Class}^UA^193^When a character's taint threatens to exceed the capacity of his body and soul to contain it, he may become possessed by its evil power and transformed into a creature of taint.
+talon of Tiamat {Prestige Class}^Dr^134^The talon of Tiamat furthers the goals of evil dragonkind.
+Talontar blightlord {Prestige Class}^Una^34^Corrupt priests who revel in decay, the blightlords of Talona are feared and reviled throughout the Unapproachable East.
+tamer of beasts {Prestige Class}^MW^70^Through magic and his overwhelming concern for his charges, the tamer of beasts can make them tougher and more intelligent.
+tattooed monk {Prestige Class}^CW^82^Certain monastic orders bestow supernatural or spell-like powers on their members by inscribing magic tattoos on their skin.
+tattooed monk {Prestige Class}^OA^49^Certain monastic orders bestow supernatural or spell-like powers on their members by inscribing magic tattoos on their skin.
+techsmith {Prestige Class}^FP^206^Techsmiths are devoted to the development of new inventions and the progression of achievement in the name of the Wonderbringer.
+Telflammar shadowlord {Prestige Class}^Una^36^Above all the criminals of the Shadowmasters of Telflamm stand the Telflammar shadowlords,the secret captains of iniquity who demand unquestioned obedience from their numerous minions.
+tempest {Prestige Class}^CAd^81^A tempest is the point of calm within a whirling barrier of deadly blades.
+tempest {Prestige Class}^MW^72^A tempest is the point of calm within a whirling barrier of deadly blades.
+templar {Prestige Class}^DF^72^Sworn to the defense of a temple site, the templar is a holy warrior blessed by her deity with combat prowess and great endurance.
+temple raider of Olidammara {Prestige Class}^CD^67^The temple raiders are an elite cadre of thiees who worship the Laughing Rogue and specialize in stealing valuables and secret lore from the temples of other deities.
+temple raider of Olidammara {Prestige Class}^SaS^16^The temple raiders are an elite cadre of thiees who worship the Laughing Rogue and specialize in stealing valuables and secret lore from the temples of other deities.
+thaumaturgist {Prestige Class}^DMG^196^The thaumaturgist reaches out with divine power to other planes of existence, calling creatures there to do his bidding.
+Thayan gladiator {Prestige Class}^CR^63^Popular and skillful gladiators fill the arenas of Faerun from Calimshan to the Dragon Coast, but the brutal Thayan gladiators are the best of the best.
+Thayan knight {Prestige Class}^CW^85^Thayan knights have mastered the art of swordplay, are familiar with magic, and are loyal to none but the tattooed mages.
+Thayan knight {Prestige Class}^LD^64^Thayan knights have mastered the art of swordplay, are familiar with magic, and are loyal to none but the tattooed mages.
+Thayan slaver {Prestige Class}^Una^37^Thayan slavers are cruel marauders who use their awful abilities to abduct creatures and then break their wills.
+thief-acrobat^CAd^83^A thief-acrobat excels in getting in and getting out.
+thief-acrobat^SaS^18^A thief-acrobat excels in getting in and getting out.
+thrall of Demogorgon {Prestige Class}^BV^67^A thrall of Demogorgon thrives on the chaotic nature of mutation and deformity.
+thrall of Graz'zt^BV^68^The thrall of Graz'zt is a sinister, conniving, and thoroughly evil master of arcane lore and dark secrets.
+thrall of Juiblex {Prestige Class}^BV^70^A thrall of Juiblex oozes a horrible slime and is surrounded by a nauseating stench.
+thrall of Orcus {Prestige Class}^BV^71^A thrall of Orcus has devoted herself to the demon prince of undeath.
+thrallherd {Prestige Class}^XPH^153^Thrallherds manipulate the minds of others as if they were clay in the hands of a sculptor.
+tomb warden {Prestige Class}^LM^57^Tomb wardens serve as selfless, undying protectors of the dead.
+topaz guardian {Prestige Class}^LoM^203^Resolute crusaders, the topaz guardians are the elite initiates of the Holy Order of the Supernal Topaz Defenders.
+tribal protector {Prestige Class}^SF^35^The tribal protector is the battlefield champion of a savage humanoid race.
+troubadour of stars {Prestige Class}^BE^78^Bards who channel their celestial music through their mortal voices and instruments are troubadours of stars.
+true necromancer {Prestige Class}^LM^51^Those who seek to raise an unyielding obedience from the dead willingly tread the path of necromancy.
+true necromancer {Prestige Class}^TB^69^Those who seek to raise an unyielding obedience from the dead willingly tread the path of necromancy.
+unholy ravager of Tiamat {Prestige Class}^Dr^97^Those who devote themselves to Tiamat's cause become unholy ravagers of Tiamat.
+Union Sentinel {Prestige Class}^EL^35^A Union Sentinel is a member of an elite police force that guards the demiplane city of Union.
+urban soul {Prestige Class}^RD^141^Urban souls are the chosen champions of the deity Urbanus, charged with protecting city denzens from external dangers and from subtler threats to the city.
+ur-priest^BV^72^A small number of ur-priests have learned to tap into divine power and use it without praying to or worshiping a god.
+ur-priest^CD^70^A small number of ur-priests have learned to tap into divine power and use it without praying to or worshiping a god.
+vassal of Bahamut {Prestige Class}^BE^80^A vassal of Bahamut is a devout, nondraconic champion in the service of the Dragon King.
+vengeance knight {Prestige Class}^CR^67^Vengeance knights roam the Lands of Intrigue in search of those who have committed acts of treachery against their employers, the Knights of the Shield.
+verdant lord {Prestige Class}^MW^73^The verdant lord is the final defender of the forest.
+vermin keeper {Prestige Class}^Und^44^To a vermin keeper, insects are perfect killers.
+vermin lord {Prestige Class}^BV^73^The vermin lord offers itself as a host for all manner of parasitic organisms.
+vigilante {Prestige Class}^CAd^85^The vigilante combines magical and mundane investigative techniques to assess a crime scene.
+vigilante {Prestige Class}^SaS^20^The vigilante combines magical and mundane investigative techniques to assess a crime scene.
+virtuoso {Prestige Class}^CAd^89^The typical virtuoso is outgoing, charismatic, and gregarious.
+virtuoso {Prestige Class}^SaS^22^The typical virtuoso is outgoing, charismatic, and gregarious.
+visionary seeker {Prestige Class}^PlH^53^A visionary seeker knows how to navigate the mental plain stretching out ahead, finding landfall and truly discovering what it means to know.
+void disciple {Prestige Class}^CD^72^Void disciples understand that everything in the world contains all the basic elements, held together by the least tangible essence.
+void disciple {Prestige Class}^OA^51^Void disciples understand that everything in the world contains all the basic elements, held together by the least tangible essence.
+walker in the waste {Prestige Class}^Sa^89^A walker in the waste embodies the harsh, unforgiving nature of the desert.
+war chanter {Prestige Class}^CW^87^A war chanter's music flows across the battlefield like a raging torrent, catching friends and foes alike in its wake.
+war hulk {Prestige Class}^MH^22^The war hulk is a creature of great size and talent who is specifically trained to shock and awe opposing massed troops.
+war mind {Prestige Class}^XPH^155^War minds are expert fighters who claim to possess unequaled knowledge in the art of war.
+war weaver {Prestige Class}^HB^112^By weaving together strands of pure arcane power, the war weaver becomes a force to be reckoned with on the battlefield.
+war wizard of Cormyr {Prestige Class}^Mag^40^The Cormyrean war wizards are some of the most respected battle-mages in Faerun.
+warchief {Prestige Class}^MH^24^A warchief leads a primitive, aggressive tribe of humanoids, especially when they turn to marauding.
+warforged juggernaut {Prestige Class}^ECS^83^As a machine of war, the juggernaut is among the best at dealing damage and sustaining punishment.
+warmaster {Prestige Class}^SF^37^Warmasters are trained at the College of War and can become a formidable presence on the battlefield.
+warpriest {Prestige Class}^CD^74^Warpriests are fierce, earthy clerics who pray for peace but prepare for war.
+warpriest {Prestige Class}^DF^74^Warpriests are fierce, earthy clerics who pray for peace but prepare for war.
+warrior of darkness {Prestige Class}^BV^75^The warrior of darkness, sometimes called the dark knight, is a practitioner of black magic.
+warrior skald {Prestige Class}^Rac^186^Accompanying heroes of great renown, warrior skalds fight at their sides while composing the epics that will be told for centuries to come.
+warshaper {Prestige Class}^CW^89^The warshaper grows and evolves her own weapons and armor to suit the threat at hand.
+warsling sniper {Prestige Class}^Rac^188^The warsling sniper is an expert in the use of the weapon commonly associated with the halfling race.
+watch detective {Prestige Class}^MW^75^The watch detective specializes in solving mysteries.
+waverider {Prestige Class}^SS^93^The waverider and her companion animal defend their city with a vigor that exceeds either's individual powers.
+waveservant {Prestige Class}^FP^209^Waveservants server the Bitch Queen as both tribute gatherers and enforcers.
+wayfarer guide {Prestige Class}^CAr^65^The wayfarer guide focues on honing her skill at instantaneous magical transportation.
+wayfarer guide {Prestige Class}^TB^70^The wayfarer guide focues on honing her skill at instantaneous magical transportation.
+weapon master {Prestige Class}^SF^38^For weapon masters, the perfection of kiis found in the mastery of a single melee weapon.
+weapon master (kensei)^OA^53^For weapon masters, the perfection of kiis found in the mastery of a single melee weapon.
+wearer of purple {Prestige Class}^FP^210^Wearers of purple are members of the Cult of the Dragon who embrace the creation and veneration of the Sacred Ones, the great dracoliches of Faerun.
+weretouched master {Prestige Class}^ECS^85^Weretouched masters are shifters who learn to enhance their shifting ability to accentuate the power of their lycanthrope heritage.
+whisperknife {Prestige Class}^RW^135^The halfling whisperknife seeks to repay murder, theft, or humiliation in the same coin.
+wild mage {Prestige Class}^CAr^68^Wild mages aspire to cast spells without structure.
+wild plains outrider {Prestige Class}^CAd^92^Wild plains outriders work tirelessly to keep the plains as safe as such remote places can be.
+wild scout {Prestige Class}^SM^117^Wild scouts are the spies of the wilderness, traversing the open and wild country in search of valuable information.
+wildrunner {Prestige Class}^RW^139^Wildrunners give themselves almost wholly to nature, seeking to return to their untamed roots and eventually become fey creatures.
+windrider {Prestige Class}^MW^77^The windrider is a specialist in mounted combat, but hers is no ordinary mount.
+windwalker {Prestige Class}^FP^212^Windwalkers learn to shape the winds with their hands and ride them to lands as yet unseen.
+winterhaunt of Iborighu {Prestige Class}^Fr^72^As minions of the Frozen King, the winterhaunts of Iborighu lust for nothing less than eternal winter.
+witch hunter {Prestige Class}^OA^54^Witch hunters combine magical training with combat expertise to battle the spiritual forces of evil in the world.
+Wizard of High Sorcery {Prestige Class}^DCS^71^Once a wizard successfully completes (and survives) the Test of High Sorcery, his choices dictate his robe color and which deity of magic grants him power.
+wonderworker {Prestige Class}^BE^82^Wonderworkers sacrifice some of their spellcasting ability to grow closer to the ideal of goodness they revere.
+yakuza {Prestige Class}^OA^55^Yakuza represent the shadowy underworld and provide protection for the helpless.
+Yathchol webrider {Prestige Class}^Und^46^With their intimate understanding of webspinning and their familiarity with the Overweb, Yathchol webriders can move about the Underdark as they choose.
+yathrinshee {Prestige Class}^PG^187^Yathrinshees, the elite ranks of Kiaransalee's priests, are powerful masters of necromantic magic, both arcane and divine.
+yuan-ti cultist^SS^97^The mysteries of the evil deities of the yuan-ti are mastered by yuan-ti cultist masters.
+Zhentarim skymage {Prestige Class}^LD^102^These powerful spellcasters ride strange flying beasts and serve the Zhentarim by performing acts of espionage and causing unrest on the frontiers of civilization.
+Zhentarim spy {Prestige Class}^PG^77^The Zhentarim spy is probably the one Faerunians encounter most often -- even if they never realize it.
+knight phantom {Prestige Class}^FN^41^The knight phantom prestige class takes capable wizards and gradually turns them into capable melee fighters, without slowing their spellcasting too much.
+Dark Lantern {Prestige Class}^FN^68^The Dark Lanterns serve the crown of Breland as spies and assassins.
+Cyran avenger {Prestige Class}^FN^86^These survivors of the Day of Mourning seek to uncover the cause of the cataclysm and avenge their people against the architects of the Mournland.
+bone knight {Prestige Class}^FN^117^Bone knights are Karrn patriots, living protectors who fight alongside the undead legions of their land.
+silver pyromancer {Prestige Class}^FN^150^The silver pyromancer is an arcane champion of the Church of the Silver Flame, taking his place alongside clerics, paladins, and exorcists in the Church's cause.
+legacy champion {Prestige Class}^WL^19^You are so devoted to the history and chronicle of a particular item of legacy that you enjoy enhanced access to your item's legacy abilities.
+cataclysm mage {Prestige Class}^EH^58^Cataclysm mages seek after Eberron's most powerful mysteries, long lost to the past.
+thunder guide {Prestige Class}^EH^64^Bodyguards to nobles on safari, shepherds to spelunking university professors, and the real-life heroes of chronicle serials across Khorvaire, thunder guides provide the strong blades, keen senses, and local knowledge necessary to survive a trip across the Thunder Sea.
+windwright captain {Prestige Class}^EH^70^The self-proclaimed masters of sky and sea, the windwright captains are the finest pilots of airships and wind galleons on Eberron.
+knight of the pearl {Prestige Class}^Sto^52^The knight of the pearl is a loyal defender of the aventi people, dedicated to the service of Aventernus and his appointed kings.
+legendary captain {Prestige Class}^Sto^56^A legendary captain might be the commander of a fleet's flagship or a bloodthirsty pirate, but whatever the role, her reputation is widespread and her crew fanatically loyal.
+leviathan hunter {Prestige Class}^Sto^61^The leviathan hunter is dedicated to hunting down creatures of the perilous depths.
+scarlet corsair {Prestige Class}^Sto^65^The scarlet corsair relies on the reputation of her quick blade and terrible fighting skills to drive her prey before her.
+sea witch {Prestige Class}^Sto^68^A sea witch is a terrible chaotic mage who wields the powers of water and calls on the living horrors of the deep.
+stormcaster {Prestige Class}^Sto^72^The stormcaster is one who seeks to tap into the power of a strange and terrifying phenomenon: the raging storm.
+wavekeeper {Prestige Class}^Sto^76^Some druids feel the call of the primal deeps.
+Knight of the Flying Hunt {Prestige Class}^CoV^106^Defenders of Nimbral, protectors of the island realm's quiet, simple folk, noble soldiers answering to the powerful but mysterious Nimbral Lords -- the Knights of the Flying Hunt epitomize valor and grace in word, deed, and bearing.
+Knight of the Weave {Prestige Class}^CoV^111^Members of this mystic order of sacred defenders cherish the Weave like a fine wine.
+Moonsea skysentinel {Prestige Class}^CoV^117^Moonsea skysentinels are the eyes in the sky for the Knights of the North, scouting the landscape, looking for evidence of Zhentarim activity.
+Triadic knight {Prestige Class}^CoV^123^Triadic knights are holy warriors who worship the Triad of Tyr, Torm, and Ilmater.
+corrupt avenger {Prestige Class}^HH^88^A corrupt avenger accepts any cost to have his vengeance, even to the forfeit of his very soul.
+death delver {Prestige Class}^HH^93^The death delver is that rare individual, who, rather than fearing and avoiding death, delves as deeply into its mysteries as he can, to better understand and eventually gain some small power over it.
+dread witch {Prestige Class}^HH^98^The dread witch is a spellcaster who manipulates fear as readily and effectively as other casters manipulate magic itself.
+fiend-blooded^HH^102^With careful exploration, a spellcaster who feels a call from within can slowly bring the power of their fiendish lineage to the surface.
+purifier of the Hallowed Doctrine {Prestige Class}^HH^108^Purifiers of the Hallowed Doctrine consider themselves servants not of gods but of the spiritual well-being of the world itself.
+tainted scholar {Prestige Class}^HH^113^No secret is barred from the tainted scholar's grasp, and if such forbidden knowledge comes at the cost of his soul, he's willing to pay that price.
+alchemist savant {Prestige Class}^MoE^53^The alchemist savant excels in the capacity to break down the normal barriers that lie between alchemy and magic, between potion and alchemical fluid, between science and art.
+deadgrim {Prestige Class}^MoE^57^The deadgrim are an elite faction with in the Red Watchers, a new organization of undead hunters within Karrnath.
+dragon prophet {Prestige Class}^MoE^63^The dragon prophet is a member of one of the 'lesser races' who shares the dragons' ambitious goal of understanding the complex and convoluted draconic Prophecy.
+elemental scion of Zilargo {Prestige Class}^MoE^68^The elemental scion of Zilargo attempts to understand the true nature of the elements through bizarre methods.
+impure prince {Prestige Class}^MoE^73^Of those who make it their special mission to rid the world of aberrations and their masters, only impure princes can claim the distinction of using daelkyr-inspired corruptions and symbionts as their most effective tool in aberration cleansing.
+quori mindhunter {Prestige Class}^MoE^77^The quori mindhunter has a single mission: to hunt down and destroy the quori spirits that corrupt humanity, and the possessed Inspired that further the aims of the Dreaming Dark.
+renegade mastermaker {Prestige Class}^MoE^81^A renegade mastermaker applies the secrets of warforged creation methods to his own body, slowly replacing parts of his body with mechanical augmentations.
+vigilant sentinel of Aerenal {Prestige Class}^MoE^85^Part spy, part assassin, and completely loyal to the Sibling Kings and Aerenal's undying rulers, the sentinels roam across Eberron.
+incandescent champion {Prestige Class}^MoI^115^The incandescent champion seeks to dispense with barriers and obstacles both tangible and intangible so that she can touch the cosmic soul with her unveiled body, mind, and spirit.
+incarnum blade {Prestige Class}^MoI^121^Using a secret passed down through the generations, the incarnum blade shapes soul energy drawn from the greatest warriors of the past into a special soulmeld that is incorporated into his melee weapon of choice.
+ironsoul forgemaster {Prestige Class}^MoI^126^Only the ironsoul forgemaster can craft a weapon that combines these arts with the shaping of soul essence.
+necrocarnate {Prestige Class}^MoI^132^Dealers in death and torturers of souls, necrocarnates number among the most evil creatures in any world.
+sapphire hierarch {Prestige Class}^MoI^136^The elite members of an order of priests of law defend the temple, contemplate the mysteries of the Sapphire Eidolon, and seek to fulfill its single command by perfecting themselves and bringing order out of chaos wherever they find it.
+soulcaster {Prestige Class}^MoI^142^Soulcasters excel at incorporating soul energy into their magic.
+spinemeld warrior {Prestige Class}^MoI^147^When a spinemeld warrior trains, he is participating in a tradition that has long been venerated in skarn society.
+totem rager {Prestige Class}^MoI^153^The totem rager embodies the wrath of nature in its most bestial form.
+umbral disciple {Prestige Class}^MoI^158^The umbral disciple is a student of shadow in both a literal and a metaphysical sense.
+witchborn binder {Prestige Class}^MoI^162^Elite agents within the Vigilant Servants, a society whose members make it their business to frustrate the plans of the witchborn, witchborn binders are incarnum-wielding mage-hunters who can use the power of soul energy to create shields, traps, and shackles.
+gatekeeper mystagogue {Prestige Class}^PE^88^Heirs to a tradition over sixteen millennia old, the gatekeeper mystagogues stand among the greated foes of the daelkyr and their aberration spawn.
+high elemental binder {Prestige Class}^PE^104^As a high elemental binder, you can reach into the planes and immediately draw elemental beings into objects -- coating your armor in stone or your blade in fire.
+revenant blade {Prestige Class}^PE^108^The revenant blade is a Valenar elf who can draw on the skills of ancient heroes, the giant-slayers of Xen'drik.
+court herald {Prestige Class}^PF^108^The court herald prestige class is a modified version of the loremaster prestige class, which is described in the Dungeon Master's Guide.
+disciple of the eye {Prestige Class}^RDr^75^As a disciple of the eye, you know the messages that the eyes alone can impart.
+dracolexi {Prestige Class}^RDr^79^As a dracolexi, you try to understand that primordial vocabulary by devoting yourself to the study of ancient dialects and languages, hoping to discover how certain Draconic words were once uttered.
+dragon devotee {Prestige Class}^RDr^84^Some individuals feel the call of dragons more strongly, which may lead them into an attempt to awaken their blood and bring those traits to the fore.
+dragonheart mage {Prestige Class}^RDr^88^The dragonheart mage is perfect for the dedicated spellcaster who wishes to embrace the power of dragon blood while still advancing in magical expertise.
+Singer of Concordance {Prestige Class}^RDr^91^The Singers of Concordance are a small order of wandering draconic spiritual guides who begin as servitors of Io, the Ninefold Dragon, creator of all dragonkind.
+anima mage {Prestige Class}^TM^50^Anima mages see vestiges as mere tools, no different from spell component pouches or a wand of fireball.
+knight of the sacred seal {Prestige Class}^TM^54^A knight of the sacred seal is never alone because she has formed a true partnership with a single vestige.
+scion of Dantalion {Prestige Class}^TM^59^The scions believe that their destiny is to one day take up the crown of a long-forgotten human empire, bear the scepter of rulership, and rebuild the empire that could rival the stars.
+Tenebrous apostate {Prestige Class}^TM^63^The remnant of divinity once possessed by Orcus, Tenebrous is perhaps the only vestige still worshiped in some places as a god. Some followers, however, believe that Tenebrous is a separate deity, so these Tenebrous apostates revere him as such.
+witch slayer {Prestige Class}^TM^67^Witch slayers devote themselves to capturing and destroying those who share their souls with other entities.
+child of night {Prestige Class}^TM^117^They prefer to call themselves 'black transmogrifists,' but most know them as children of night.
+master of shadow {Prestige Class}^TM^121^Some driven or domineering souls seek nothing less than mastery of darkness itself -- the ability to turn the very shadows into their agents and allies.
+noctumancer {Prestige Class}^TM^125^Noctumancers bridge the gap between shadow and arcane magic.
+shadowblade {Prestige Class}^TM^129^Shadowblades are martial combatants with an innate link to shadow.
+shadowsmith {Prestige Class}^TM^132^Shadowcasters draw power from darkness, and masters of shadow command it, but no one truly manipulates the darkness as does the shadowsmith.
+acolyte of the ego {Prestige Class}^TM^204^By learning to speak their own truenames, acolytes of the ego strive to unlock hidden powers lost to the cosmos.
+bereft {Prestige Class}^TM^208^The bereft are a group of truenamers who devote themselves to mastering the word of unmaking, a powerful component of Truespeech purportedly able to unravel creation.
+brimstone speaker {Prestige Class}^TM^212^Brimstone speakers regard the secret language of truenames as nothing less than a gift from the gods.
+disciple of the word {Prestige Class}^TM^216^Disciples of the word are intellectual warrior monks who, through a deeper understanding of their truenames, transcend the limits of their mortal form.
+fiendbinder {Prestige Class}^TM^220^A fiendbinder seeks to unlock the truenames of demons, devils, and other vile fiends, and use that knowledge to bind them to service.
+merchant prince {Prestige Class}^PF^71^A merchant prince (known as a merchant princess if female) is a member of the merchant nobility who has acquired his position and wealth either by being born into a wealthy family or by earning every last coin himself.
+anarchic initiate {Prestige Class}^CP^17^The anarchic initiate is more than a wilder; he is an initiate to the truth that underlies the wildness in the depth of his being.
+ebon saint {Prestige Class}^CP^22^The ebon saint lives in the darkness, but seeks to exposehis enemies to the light.
+ectopic adept {Prestige Class}^CP^26^Ectoplasm is the preferred medium of creation for the ectopic adept, and his mind serves as the mold, kiln, and wheel upon which his works are turned.
+flayerspawn psychic {Prestige Class}^CP^30^Willing to sacrifice her life, appearance, and even her sanity, the flayerspawn psychic walks a dangerous road, growing in psionic power as she slowly embraces her secret mind flayer heritage.
+illumine soul {Prestige Class}^CP^33^The illumine soul is a living conduit of positive energy.
+soulbow {Prestige Class}^CP^36^In the tradition of the soulknife, a soulbow realizes the direct capacity of her own mind to give shape to weapons of psionic perfection.
+storm disciple {Prestige Class}^CP^40^A storm disciple is a character who decides that the best, most glorious way to serve his ideals is through the natural power, fury, and splendor of the storm.
+zerth cenobite {Prestige Class}^CP^43^The core of a zerth cenobite's studies involve strict meditation on the nature of time and the body's movements through it, culminating in a martial art known as zerthin.
+landforged walker {Prestige Class}^SX^123^Even as they speak for nature, landforged walkers coax the living bounty of the earth to grow on their metal hides, drawing power from the environment around them.
+primal scholar {Prestige Class}^SX^127^Primal scholars are spellcasters bent on uncovering and mastering the ancient magic that lies buried in the jungles, deserts, and mountains of Xen'drik.
+scorpion wraith {Prestige Class}^SX^130^Scorpion wraiths are the elite warriors of the drow.
+wearer of purple {Prestige Class}^DrF^67^Updated from Faiths and Pantheons.
+bloodclaw master {Prestige Class}^ToB^96^A bloodclaw master embraces the animal within to a degree that it becomes external, partially transforming him into the beast from which the discipline was inspired.
+bloodstorm blade {Prestige Class}^ToB^100^Other martial adepts rightfully look with wonder upon those who learn the bloodstorm style.
+deepstone sentinel {Prestige Class}^ToB^105^The Stone Dragon discipline traces its roots back to an ancient order of dwarves that used the power of the earth to enhance their combat style.
+eternal blade {Prestige Class}^ToB^109^An eternal blade is an elf who learns a variety of exotic fighting maneuvers with the aid and advice of the spirit of a mighty, ancient elf warrior.
+jade phoenix mage {Prestige Class}^ToB^113^Long ago, a fellowship of swordsages known as the Masters of the Jade Phoenix took up the study of arcane magic in search of a new martial discipline.
+master of nine {Prestige Class}^ToB^119^Some savants of the Nine Disciplines believe that none of the paths are complete, true disciplines in and of themselves.
+Ruby Knight vindicator {Prestige Class}^ToB^122^The Ruby Knights are a crusader order in the service of Wee Jas, goddess of death and magic.
+Shadow Sun ninja {Prestige Class}^ToB^126^A Shadow Sun ninja is a martial artist who studies the balance between good and evil, light and dark.
+diamond dragon {Prestige Class}^DM^30^The boldest of the sages study the link between gem dragons and psionics, learning to tap into what they refer to as the draconic psionic collective.
+dragon descendant {Prestige Class}^DM^34^A secret monastic order, dragon descendants tap into the power of their draconic heritage to call on their ancestors in times of need.
+dragon lord {Prestige Class}^DM^38^A dragon lord is the general at the head of an army, the emperor at the helm of an empire, or the warleader who dominates a battlefield.
+hand of the winged masters {Prestige Class}^DM^43^Dragons often need expert servants to be their eyes, ears, and hands in humanoid society.
+pact-bound adept^DM^46^Pact-bound adepts are sorcerers who have learned to transcend their beliefs about spells and magic and embrace arcane power as dragons do.
+swift wing {Prestige Class}^DM^50^Swift wings are church servants who see themselves as the fast-moving, hard-hitting crusaders of their god's cadre of worshipers.
+wyrm wizard {Prestige Class}^DM^55^Wyrm wizards are spellcasters who learn new spells not through research and experimentation but rather by tapping into the vast wealth of arcane knowledge possessed by dragons.
+sovereign speaker {Prestige Class}^FE^32^Although devotion to a single god enables some individuals to gain additional power, overriding
+escalation mage {Prestige Class}^FE^52^By giving themselves over to the Shadow and focusing on the dark side of magic, these arcane spellcasters learn how to bargain with their god to make their spells more effective -- for a price.
+argent fist {Prestige Class}^FE^70^Only a precious few possess the focus, the dedication, and the physical prowess to master the abilities of the argent fist, but few enemies can stand against those who do.
+thief of life {Prestige Class}^FE^84^When the prize is immortality, there is precious little a thief of life will not do to grasp it.
+planar shepherd {Prestige Class}^FE^105^Some druids, especially among the Greensingers or those who have dealt extensively with that sect, reject narrow interpretations of what constitutes the natural world.
+knight of the raven {Prestige Class}^Rav^200^Before evil descended on the land of Barovia, it was home to an order of virtuous champions, the Knights of the Raven.
+Lightbringer {Prestige Class}^Rav^204^The Lightbringers are an expansive guild of undead hunters that readily hands out charter memberships to anyone who wants to stamp out undead.
+aburant champion {Prestige Class}^CM^50^A warrior who dabbles in abjuration magic.
+eldritch disciple {Prestige Class}^CM^53^A multiclass warlock and divine spellcaster.
+eldritch theurge {Prestige Class}^CM^57^A multiclass warlock and arcane spellcaster.
+enlightened spirit {Prestige Class}^CM^60^A warlock who takes on celestial characteristics.
+holy scourge {Prestige Class}^CM^64^An arcane spellcaster that specializes in blasting evil.
+lyric thaumaturge {Prestige Class}^CM^67^A bard with enhanced spellcasting prowess.
+master specialist {Prestige Class}^CM^70^A wizard with greater mastery over a school of specialization.
+nightmare spinner {Prestige Class}^CM^74^An arcane spellcaster who weaves fear into illusions.
+ultimate magus {Prestige Class}^CM^77^A multiclass arcane preparation spellcaster and arcane spontaneous spellcaster.
+unseen seer {Prestige Class}^CM^81^A stealthy character who dabbles in divination magic.
+wild soul {Prestige Class}^CM^84^An arcanist who wields power from the realm of the fey.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd35/dnd35armor.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,87 @@
+<ac>
+<armor  name="Banded mail" cost="250" type="Heavy" maxdex="1" bonus="6" spellfailure="35" checkpenalty="-6" weight="35" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of overlapping strips of metal sewn to a backing of leather and chainmail. The strips cover vulnerable areas, while the chain and leather protect the joints and provide freedom of movement. Straps and buckles distribute the weight evenly. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Breastplate" cost="200" type="Medium" maxdex="3" bonus="5" spellfailure="25" checkpenalty="-4" weight="30" speed="20" speed20="15" speed30="20" >
+<description >A breastplate covers the front and back. It comes with a helmet and matching greaves (plates to cover the lower legs). A light suit or skirt of studded leather beneath the breastplate protects limbs without restricting
+movement much.
+</description >
+</armor>
+<armor  name="Buckler" cost="15" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="5" speed="30" speed20="20" speed30="30" >
+<description >This small metal shield is strapped to the forearm, allowing it to be worn and still use the hand. A bow or crossbow can be used without penalty. An off-hand weapon can be used, but a -1 penalty on attack rolls is imposed because of the extra weight on your arm. This penalty stacks with those for fighting with the off hand and, if appropriate, for fighting with two weapons. In any case, if a weapon is used in the off-hand, the character doesn't get the buckler's AC bonus for the rest of the round.
+</description >
+</armor>
+<armor  name="Chainmail" cost="150" type="Medium" maxdex="2" bonus="5" spellfailure="30" checkpenalty="-5" weight="40" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of interlocking metal rings. It includes a layer of quilted fabric underneath it to prevent chafing and to cushion the impact of blows. Several layers of mail are hung over vital areas. Most of the armor's weight hangs from the shoulders, making chainmail uncomfortable to wear for long periods of time. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Chainshirt" cost="100" type="Light" maxdex="4" bonus="4" spellfailure="20" checkpenalty="-2" weight="25" speed="30" speed20="20" speed30="30" >
+<description >A shirt of chainmail protects the torso while leaving the limbs free and mobile. A layer of quilted fabric underneath it prevents chafing and cushions the impact of blows. It comes with a steel cap.
+</description >
+</armor>
+<armor  name="Full Plate" cost="1500" type="Heavy" maxdex="1" bonus="8" spellfailure="35" checkpenalty="-6" weight="50" speed="20" speed20="15" speed30="20" >
+<description >This armor consists of shaped and fitted metal plates riveted and interlocked to cover the entire body. It includes gauntlets, heavy leather boots, and a visored helmet.
+</description >
+</armor>
+<armor  name="Half-Plate" cost="600" type="Heavy" maxdex="0" bonus="7" spellfailure="40" checkpenalty="-7" weight="50" speed="20" speed20="15" speed30="20" >
+<description >This armor is a combination of chainmail with metal plates (breastplate, epaulettes, elbow guards, gauntlets, tasses, and greaves) covering vital areas. Buckles and straps hold the whole suit together and distribute the weight, but the armor still hangs more loosely than full plate. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Hide" cost="15" type="Medium" maxdex="4" bonus="3" spellfailure="20" checkpenalty="-3" weight="25" speed="20" speed20="15" speed30="20" >
+<description >This armor is prepared from multiple layers of leather and animal hides. It is stiff and hard to move in.
+</description >
+</armor>
+<armor  name="Large SteelShield" cost="20" type="Shield" maxdex="100" bonus="2" spellfailure="15" checkpenalty="-2" weight="15" speed="30" speed20="20" speed30="30" >
+<description >A large shield is too heavy to use the shield hand for anything else.
+</description >
+</armor>
+<armor  name="Large Wooden Shield" cost="7" type="Shield" maxdex="100" bonus="2" spellfailure="15" checkpenalty="-2" weight="10" speed="30" speed20="20" speed30="30" >
+<description >A large shield is too heavy to use the shield hand for anything else.
+</description >
+</armor>
+<armor  name="Leather" cost="10" type="Light" maxdex="6" bonus="2" spellfailure="10" checkpenalty="0" weight="10" speed="30" speed20="20" speed30="30" >
+<description >The breastplate and shoulder protectors of this armor are made of leather that has been stiffened by boiling in oil. The rest of the armor is softer and more flexible leather.
+</description >
+</armor>
+<armor  name="Padded" cost="5" type="Light" maxdex="8" bonus="1" spellfailure="5" checkpenalty="0" weight="10" speed="30" speed20="20" speed30="30" >
+<description >Padded armor features quilted layers of cloth and batting.
+</description >
+</armor>
+<armor  name="Samll Steel Shield" cost="9" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="6" speed="30" speed20="20" speed30="30" >
+<description >A small shield's light weight lets a character carry other items in that hand (although the character cannot use weapons).
+</description >
+</armor>
+<armor  name="Scale Mail" cost="50" type="Medium" maxdex="3" bonus="4" spellfailure="25" checkpenalty="-4" weight="30" speed="20" speed20="15" speed30="20" >
+<description >This is a coat and leggings (and perhaps a separate skirt) of leather covered with overlapping pieces of metal, much like the scales of a fish. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Small Wooden Shield" cost="300" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="5" speed="30" speed20="20" speed30="30" >
+<description >A small shield's light weight lets a character carry other items in that hand (although the character cannot use weapons).
+</description >
+</armor>
+<armor  name="Splint mail" cost="200" type="Heavy" maxdex="0" bonus="6" spellfailure="40" checkpenalty="-7" weight="45" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of narrow vertical strips of metal riveted to a backing of leather that is worn over cloth padding. Flexible chainmail protects the joints. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Studded Leather" cost="25" type="Light" maxdex="5" bonus="3" spellfailure="15" checkpenalty="-1" weight="20" speed="30" speed20="20" speed30="30" >
+<description >This armor is made from tough but flexible leather (not hardened leather as with normal leather armor) reinforced with close-set metal rivets.
+</description >
+</armor>
+<armor  name="Tower Shield" cost="30" type="Shield" maxdex="100" bonus="0" spellfailure="50" checkpenalty="-10" weight="45" speed="30" speed20="20" speed30="30" >
+<description >This massive wooden shield is nearly as tall as the wielder. Basically, it is a portable wall meant to provide cover. It can provide up to total cover, depending on how far a character comes out from behind it. A tower shield, however, does not provide cover against targeted spells; a spellcaster can cast a spell on a character by targeting the shield. A tower shield cannot be used for the shield bash action.
+</description >
+</armor>
+<armor  name="Ring of Protection +1" cost="xx" type="Ring" maxdex="100" bonus="1" spellfailure="0" checkpenalty="0" weight="0" speed="30" speed20="20" speed30="30" >
+<description >A ring that gives Plus 1 AC
+</description >
+</armor>
+<armor  name="Monk Wisdom Bonus" cost="xx" type="NA" maxdex="100" bonus="4" spellfailure="0" checkpenalty="0" weight="0" speed="30" speed20="20" speed30="30" >
+<description >Bonus is Based on wisdom Bonus, and can not be removed unless I am uncontius
+</description >
+</armor>
+<armor  name="Inertial Armor Feat" cost="xx" type="NA" maxdex="100" bonus="4" spellfailure="0" checkpenalty="0" weight="0" speed="30" speed20="20" speed30="30" >
+<description >Armor Bonus based on the feat
+</description >
+</armor>
+</ac>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd35/dnd35character.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,113 @@
+
+<nodehandler class="d20char_handler" icon="knight" module="d20" name="D20 Character Tool">
+<howtouse>
+<howto>
+To use this you do:
+
+To use this you just need to add all the stuff you wish, set your stats ect, then click on the
++(plus) next too the gears, and right click.
+
+This will cause it to roll what is needed to roll, except dmg on spells, you will still need to
+roll that normaly, I will work on a way to add it, but for now it isnt required.
+</howto>
+</howtouse>
+  <general>
+    <name>Player Name</name>
+    <player>Your Name</player>
+    <race>none</race>
+    <alignment abbr="LG">none</alignment>
+    <deity>none</deity>
+    <size acmodifier="0">none</size>
+    <height>none</height>
+    <weight>none</weight>
+    <age>none</age>
+    <gender>none</gender>
+    <eyes>none</eyes>
+    <hair>none</hair>
+    <speed>30</speed>
+  </general>
+  <classes level="0"/>
+  <abilities>
+    <stat abbr="Str" base="0" name="Strength"/>
+    <stat abbr="Dex" base="0" name="Dexterity"/>
+    <stat abbr="Con" base="0" name="Constitution"/>
+    <stat abbr="Int" base="0" name="Intelligence"/>
+    <stat abbr="Wis" base="0" name="Wisdom"/>
+    <stat abbr="Cha" base="0" name="Charisma"/>
+  </abilities>
+<inventory>
+<plat>0</plat>
+<gold>0</gold>
+<silver>0</silver>
+<copper>0</copper>
+<generalgear>None</generalgear>
+<magicalgear>None</magicalgear>
+</inventory>
+  <saves>
+    <save base="0" magmod="0" miscmod="0" name="Fortitude" stat="Con"/>
+    <save base="0" magmod="0" miscmod="0" name="Reflex" stat="Dex"/>
+    <save base="0" magmod="0" miscmod="0" name="Will" stat="Wis"/>
+  </saves>
+  <hp current="0" max="0"/>
+  <pp current1="0" free="0" max1="0" maxfree="0"/>
+  <skills>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Alchemy" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Animal Empathy" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Appraise" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Balance" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Bluff" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="1" misc="0" name="Climb" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Concetration" rank="0" stat="Con" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Craft" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Decipher Script" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Diplomacy" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Disable Device" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Disguise" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Escape Artist" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Forgery" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Gather Information" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Handle Animal" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Heal" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Hide" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Innuendo" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Intimidate" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Intuit Direction" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Jump" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Arcana" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Architecture and Engineering" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Geography" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: History" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Local" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nature" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nobility and Royalty" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: The Planes" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Religion" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Listen" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Move Silently" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Open Lock" rank="0" stat="Dex" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Perform" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Pick Pocket" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Profession" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Read Lips" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Ride" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Scry" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Search" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Sense Motive" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Spellcraft" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Spot" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Swim" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Tumble" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Magic Device" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Rope" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Wilderness Lore" rank="0" stat="Wis" untrained="1"/>
+  </skills>
+  <feats/>
+  <spells/>
+  <powers/>
+<divine/>
+  <attacks>
+    <melee base="0" misc="0"/>
+    <ranged base="0" misc="0"/>
+  </attacks>
+  <ac misc="" natural=""/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd35/dnd35classes.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,779 @@
+<classes>
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 107" name="Adept" hd="d?" desc="This NPC class possesses a combination of arcane and divine skills." />
+    <class level="1" book="Heroes of Horror page 82" name="Archivist" hd="d?" desc="Archivists seek out esoteric sources of divine lore, wherever those sources might be, securing those secrets for themselves and their fellow scholars." />
+    <class level="1" book="Complete Psionic page 5" name="Ardent" hd="d?" desc="An ardent's pursuit of various cosmic philosophies gives her access to psionic power in a unique way: through psionic mantles." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 108" name="Aristocrat" hd="d?" desc="This NPC class contains people who are usually educated, wealthy individual who were born into high position." />
+    <class level="1" book="Eberron Campaign Setting page 29" name="Artificer" hd="d?" desc="Artificers are perhaps the ultimate magical dabblers." />
+    <class level="1" book="Player's Handbook v.3.5 page 24" name="Barbarian" hd="d?" desc="A ferocious warrior who uses fury and instinct to bring down foes." />
+    <class level="1" book="Player's Handbook v.3.5 page 26" name="Bard" hd="d?" desc="A performer whose music works magic -- a wanderer, a tale-teller, and a jack-of-all trades." />
+    <class level="1" book="Unearthed Arcana page 49" name="Bardic Sage" hd="d?" desc="A variant bard who focues his efforts on learning, research, and the power of knowledge." />
+    <class level="1" book="Unearthed Arcana page 56" name="Battle Sorcerer" hd="d?" desc="A variant sorcerer who is a capable physical combatant." />
+    <class level="1" book="Player's Handbook II page 6" name="Beguiler" hd="d?" desc="If you delight in manipulating others, either to their disadvantage or for their own good, then the beguiler is the class for you." />
+    <class level="1" book="Tome of Magic page 9" name="Binder" hd="d?" desc="By drawing their seals and speaking words of power, the binder summons strange entities, bargains with them, and binds them to his service." />
+    <class level="1" book="Player's Handbook v.3.5 page 30" name="Cleric" hd="d?" desc="A master of divine magic and a capable warrior as well." />
+    <class level="1" book="Unearthed Arcana page 50" name="Cloistered Cleric" hd="d?" desc="A variant cleric who spends more time than other clerics in study and prayer and less in martial training." />
+    <class level="1" book="Dragonlance Campaign Setting page 46" name="Commoner" hd="d?" desc="This NPC class contains the laborers of the world, such as innkeepers, servants, blacksmiths, farmers, and fisherfolk." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 8" name="Crusader" hd="d?" desc="Devoted knight, divine agent, instrument of vengeance, peerless fighting machine -- the crusader is a warrior dedicated to good, evil, law, chaos, or some other cause." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 41" name="Death's Chosen " hd="d?" desc="If chosen by his would-be undead master, a death's chosen serves as the master's living minion." />
+    <class level="1" book="Unearthed Arcana page 50" name="Divine Bard" hd="d?" desc="A variant bard who derives his special power from a divine tradition." />
+    <class level="1" book="Complete Psionic page 9" name="Divine Mind" hd="d?" desc="A divine mind is a psionic character who channels the power of the divine through pisonic talent instead of faith." />
+    <class level="1" book="Unearthed Arcana page 57" name="Domain Wizard" hd="d?" desc="A variant wizard who uses the arcane domain system and selects a specific domain of spells." />
+    <class level="1" book="Player's Handbook II page 11" name="Dragon Shaman" hd="d?" desc="If you gaze at dragons with awe and aspire to share their power and majesty, then the dragon shaman is the class for you." />
+    <class level="1" book="Dragon Magic page 24" name="Dragonfire Adept" hd="d?" desc="Whether they are bold champions defending the weak and downtrodden, or merciless raiders seeking might and riches, dragonfire adepts are imposing figures who command the magic of dragonkind." />
+    <class level="1" book="Heroes of Horror page 84" name="Dread Necromancer" hd="d?" desc="A practitioner of vile and forbidden arts, the dread necromancer roots about in graveyards, searching out moldering components for her obscene spells." />
+    <class level="1" book="Player's Handbook v.3.5 page 33" name="Druid" hd="d?" desc="One who draws energy from the natural world to cast divine spells and gain strange magical powers." />
+    <class level="1" book="Unearthed Arcana page 51" name="Druidic Avenger" hd="d?" desc="A variant druid who channels her inner fury to wreak vengeance upon those who injure the natural world." />
+    <class level="1" book="Complete Psionic page 144" name="Duergar Racial Class" hd="d?" desc="The duergar, or gray dwarves, lead lives of neverending toil in great underground foundry-cities." />
+    <class level="1" book="Player's Handbook II page 19" name="Duskblade" hd="d?" desc="If you find you can't choose between being an arcane spellcaster who zaps your enemies with powerful spells and a nimble, powerful front-line melee character who lays them low with a sword, the duskblade is the perfect class for you." />
+    <class level="1" book="Races of Stone page 146" name="Dwarf Cleric" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Stone page 146" name="Dwarf Fighter" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Stone page 147" name="Dwarf Sorcerer" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Ghostwalk page 16" name="Eidolon" hd="d?" desc="This variant class for ghosts emphasizes fighting abilities." />
+    <class level="1" book="Ghostwalk page 17" name="Eidoloncer" hd="d?" desc="This variant class for ghosts emphasizes spellcasting abilities." />
+    <class level="1" book="Races of the Wild page 155" name="Elf Paladin" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of the Wild page 155" name="Elf Ranger" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of the Wild page 157" name="Elf Wizard" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Epic Level Handbook page 8" name="Epic Barbarian" hd="d?" desc="An epic ferocious warrior who uses fury and instinct to bring down foes." />
+    <class level="1" book="Epic Level Handbook page 9" name="Epic Bard" hd="d?" desc="An epic performer whose music works magic -- a wanderer, a tale-teller, and a jack-of-all trades." />
+    <class level="1" book="Epic Level Handbook page 10" name="Epic Cleric" hd="d?" desc="An epic master of divine magic and a capable warrior as well." />
+    <class level="1" book="Epic Level Handbook page 10" name="Epic Druid" hd="d?" desc="An epic druid draws energy from the natural world to cast divine spells and gain strange magical powers." />
+    <class level="1" book="Epic Level Handbook page 11" name="Epic Fighter" hd="d?" desc="An epic warrior with exceptional combat capability and unequaled skill with weapons." />
+    <class level="1" book="Epic Level Handbook page 12" name="Epic Monk" hd="d?" desc="An epic martial artist whose unarmed strikes hit fast and hard -- a master of exotic powers." />
+    <class level="1" book="Epic Level Handbook page 13" name="Epic Paladin" hd="d?" desc="An epic champion of justice and destroyer of evil, protected and strengthened by an array of divine powers." />
+    <class level="1" book="Epic Level Handbook page 22" name="Epic Psion" hd="d?" desc="An epic seeker after psionic secrets; a master of the mind and the thoughts of others." />
+    <class level="1" book="Epic Level Handbook page 23" name="Epic Psychic Warrior" hd="d?" desc="An epic warrior who combines combat skill with psionic powers." />
+    <class level="1" book="Epic Level Handbook page 14" name="Epic Ranger" hd="d?" desc="An epic cunning, skilled warrior of the wilderness." />
+    <class level="1" book="Epic Level Handbook page 14" name="Epic Rogue" hd="d?" desc="An epic tricky, skillful scout and spy who wins the battle by stealth rather than brute force." />
+    <class level="1" book="Epic Level Handbook page 15" name="Epic Sorcerer" hd="d?" desc="An epic spellcaster with inborn magical ability." />
+    <class level="1" book="Epic Level Handbook page 16" name="Epic Wizard" hd="d?" desc="An epic potent spellcaster schooled in the arcane arts." />
+    <class level="1" book="Complete Psionic page 153" name="Erudite (variant Psion)" hd="d?" desc="As an alternative to the standard psion class, the erudite is a psionic character who follows a scholarly and self-reflective road to power, instead of a merely self-conscious path like the psion follows." />
+    <class level="1" book="Dragonlance Campaign Setting page 46" name="Expert" hd="d?" desc="This NPC class contains people such as skilled artisans, specialist laborers, and ingenious inventors." />
+    <class level="1" book="Player's Guide to Faerun page 54" name="Eye Of Horus-re" hd="d?" desc="Eyes of Horus-Re are champions of good, sworn enemies of Set, and bane to undead." />
+    <class level="1" book="Complete Divine page 6" name="Favored Soul" hd="d?" desc="Favored souls cast divine spells by means of an innate connection rather than through laborious training and prayer, so their divine connection is natural rather than learned." />
+    <class level="1" book="Heroes of Horror page 102" name="Fiend-blooded" hd="d?" desc="With careful exploration, a spellcaster who feels a call from within can slowly bring the power of their fiendish lineage to the surface." />
+    <class level="1" book="Player's Handbook v.3.5 page 37" name="Fighter" hd="d?" desc="A warrior with exceptional combat capability and unequaled skill with weapons." />
+    <class level="1" book="Complete Adventurer page 51" name="Ghost-faced Killer" hd="d?" desc="Ghost-faced killers act as assassins and spies for hire, a mercenary clan that hides behind a guise of open and honorable conduct." />
+    <class level="1" book="Silver Marches page 109" name="Giant-killer" hd="d?" desc="Giant-killers are great heroes so long as they are killing giants." />
+    <class level="1" book="Complete Psionic page 146" name="Githyanki Racial Class" hd="d?" desc="Githyanki are an ancient race of martial humanoids residing on the Astral Plane." />
+    <class level="1" book="Complete Psionic page 147" name="Githzerai Racial Class" hd="d?" desc="The githzerai are attuned to the mysteries of the inner self and are considered a race of ascetics who harness the power of the mind and the spirit." />
+    <class level="1" book="Races of Stone page 147" name="Gnome Bard" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Complete Warrior page 36" name="Gnome Giant-slayer" hd="d?" desc="The gnome giant-slayer relies on a combination of agility, combat prowess, and pure craftiness to deal with foes." />
+    <class level="1" book="Races of Stone page 148" name="Gnome Illusionist" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Stone page 149" name="Gnome Ranger" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Stone page 150" name="Goliath Barbarian" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Stone page 151" name="Goliath Druid" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Stone page 152" name="Goliath Rogue" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Destiny page 158" name="Half-elf Barbarian" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Destiny page 157" name="Half-elf Bard" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Destiny page 159" name="Half-elf Druid" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Destiny page 157" name="Half-elf Fighter" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Destiny page 160" name="Half-elf Paladin" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of Destiny page 158" name="Half-elf Ranger" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Complete Psionic page 148" name="Half-giant Racial Class" hd="d?" desc="Human-giant hybrids, half-giants were bred by cruel sorcerer-kings who used them as warriors and laborers in a dry land." />
+    <class level="1" book="Races of the Wild page 157" name="Halfling Druid" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of the Wild page 158" name="Halfling Monk" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of the Wild page 159" name="Halfling Rogue" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Miniatures Handbook page 8" name="Healer" hd="d?" desc="A healer is adept both at detecting the ailments of allies and understanding the coarse, unruly thoughts of beasts." />
+    <class level="1" book="Complete Warrior page 5" name="Hexblade" hd="d?" desc="Combining the dynamic powers of martial prowess and arcane might, the hexblade presents a deadly challenge to opponents unused to such a foe." />
+    <class level="1" book="Magic of Incarnum page 20" name="Incarnate" hd="d?" desc="Incarnum is a tool you can use to manipulate the physical manifestations of moral and ethical forces and wield them in righteous pursuit of an ideal." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 65" name="King/queen Of The Wild" hd="d?" desc="Where nature's fury is at its height, there you'll find the kings and queens of the wild." />
+    <class level="1" book="Player's Handbook II page 24" name="Knight" hd="d?" desc="The knight class is a great choice if you want to play a tough, durable melee combatant whose strong personality allows you to manipulate your foes." />
+    <class level="1" book="Silver Marches page 112" name="Knight-errant Of Silverymoon" hd="d?" desc="Charged with the safety of the city of Silverymoon and its citizens, the professional fighting force known as the Knights in Silver is often all that stands between Silverymoon and the dangers of the frontier." />
+    <class level="1" book="Complete Psionic page 13" name="Lurk" hd="d?" desc="A lurk is a psionic character who has honed her mental talents to a deadly focus." />
+    <class level="1" book="Magic of Faerun page 32" name="Mage-killer" hd="d?" desc="Mage-killers master magic designed for combat against other spellcasters." />
+    <class level="1" book="Oriental Adventures page 236" name="Maho-bujin" hd="d?" desc="When Taint overcomes a character, she may find her way to the Festering Pit of Fu Leng and become a maho-bujin." />
+    <class level="1" book="Oriental Adventures page 237" name="Maho-tsukai" hd="d?" desc="Maho (blood magic) wielders are called maho-tsukai." />
+    <class level="1" book="Miniatures Handbook page 11" name="Marshal" hd="d?" desc="Trained in the basics of fighting, marshals possess a general knowledge of weapons and armor." />
+    <class level="1" book="Player's Handbook v.3.5 page 39" name="Monk" hd="d?" desc="A martial artist whose unarmed strikes hit fast and hard -- a master of exotic powers." />
+    <class level="1" book="Dragonlance Campaign Setting page 47" name="Mystic" hd="d?" desc="Mystics are spellcasters who have learned to channel divine energy without worshiping (or even acknowledging) any deity." />
+    <class level="1" book="Complete Warrior page 63" name="Nature's Warrior" hd="d?" desc="Nature's warriors are defenders of the wild, protectors of the natural world . . . and often druids who have spent 'too much time' in wild shape form." />
+    <class level="1" book="Complete Adventurer page 5" name="Ninja" hd="d?" desc="Highly skilled spies and assassins, ninjas can master a broad range of skills and combat techniques." />
+    <class level="1" book="Dragonlance Campaign Setting page 50" name="Noble" hd="d?" desc="Nobles have the ability to use their background, education, natural charm, and skills in social maneuvering to their advantage in day-to-day lives." />
+    <class level="1" book="Dragon Magic page 46" name="Pact-bound Adept" hd="d?" desc="Pact-bound adepts are sorcerers who have learned to transcend their beliefs about spells and magic and embrace arcane power as dragons do." />
+    <class level="1" book="Player's Handbook v.3.5 page 42" name="Paladin" hd="d?" desc="A champion of justice and destroyer of evil, protected and strengthened by an array of divine powers." />
+    <class level="1" book="Unearthed Arcana page 53" name="Paladin Of Freedom" hd="d?" desc="A variant paladin who is dedicated to liberty and free thought." />
+    <class level="1" book="Unearthed Arcana page 53" name="Paladin Of Slaughter" hd="d?" desc="A variant paladin who is a brutal champion of chaos and evil, and who leaves only destruction trailing in his wake." />
+    <class level="1" book="Unearthed Arcana page 54" name="Paladin Of Tyranny" hd="d?" desc="A variant paladin who is a lawful evil villain bent on dominating those weaker than she." />
+    <class level="1" book="Complete Warrior page 13" name="Paladin Variant" hd="d?" desc="A variant paladin class." />
+    <class level="1" book="Unearthed Arcana page 55" name="Planar Ranger" hd="d?" desc="A variant ranger who roams the multiverse instead of the wilderness." />
+    <class level="1" book="Expanded Psionics Handbook page 19" name="Psion" hd="d?" desc="A seeker after psionic secrets; a master of the mind and the thoughts of others." />
+    <class level="1" book="Magic of Eberron page 42" name="Psionic Artificer" hd="d?" desc="Psionic artificers are similar to artificers, but they craft psionic items instead of magic items." />
+    <class level="1" book="Expanded Psionics Handbook page 24" name="Psychic Warrior" hd="d?" desc="A warrior who combines combat skill with psionic powers." />
+    <class level="1" book="Player's Handbook v.3.5 page 46" name="Ranger" hd="d?" desc="A cunning, skilled warrior of the wilderness." />
+    <class level="1" book="Complete Warrior page 13" name="Ranger Variant" hd="d?" desc="A variant ranger class." />
+    <class level="1" book="Races of the Wild page 160" name="Raptoran Cleric" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of the Wild page 161" name="Raptoran Fighter" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Races of the Wild page 157" name="Raptoran Sorcerer" hd="d?" desc="Racial substitution levels." />
+    <class level="1" book="Player's Handbook v.3.5 page 49" name="Rogue" hd="d?" desc="A tricky, skillful scout and spy who wins the battle by stealth rather than brute force." />
+    <class level="1" book="Complete Warrior page 8" name="Samurai" hd="d?" desc="Wielding their signature katana and wakizashi simultaneously, samurai are as potent in melee as a fighter, although they are less versatile." />
+    <class level="1" book="Unearthed Arcana page 50" name="Savage Bard" hd="d?" desc="A variant bard who is a warrior at heart and whose arcane powers strike fear into the enemies of his tribe." />
+    <class level="1" book="Sandstorm page 82" name="Scion Of Tem-et-nu" hd="d?" desc="Paladins of the temple of Tem-Et-Nu are sometimes selected to become the guardians of the rivers." />
+    <class level="1" book="Complete Adventurer page 10" name="Scout" hd="d?" desc="A scout has some training in weapons and a unique combat style that favors fast movement and devastating attacks." />
+    <class level="1" book="Tome of Magic page 111" name="Shadowcaster" hd="d?" desc="The shadowcaster understands the true, primal power of darkness, attunes herself to the Plane of Shadow, and learns great shadow mysteries the equal of any mundane spell." />
+    <class level="1" book="Oriental Adventures page 22" name="Shaman" hd="d?" desc="Shamans are intermediaries between the mortal world and the realm of spirits." />
+    <class level="1" book="Complete Divine page 10" name="Shugenja" hd="d?" desc="The shugenja is a divine spellcaster who casts spells by attuning himself to the primal energies around him and focusing such energy through his body to produce magical effects." />
+    <class level="1" book="Oriental Adventures page 27" name="Sohei" hd="d?" desc="Sohei are warrior monks." />
+    <class level="1" book="Player's Handbook v.3.5 page 51" name="Sorcerer" hd="d?" desc="A spellcaster with inborn magical ability." />
+    <class level="1" book="Magic of Incarnum page 25" name="Soulborn" hd="d?" desc="As a soulborn, you use incarnum to enhance your natural combat ability." />
+    <class level="1" book="Expanded Psionics Handbook page 26" name="Soulknife" hd="d?" desc="A warrior who fights with an idealized blade of personal mental energy." />
+    <class level="1" book="Unearthed Arcana page 59" name="Specialized Wizard Variants" hd="d?" desc="Each variant specialized class gives up one of the standard specialist's class abilities in exchange for a new ability unique to the variant specialist." />
+    <class level="1" book="Unearthed Arcana page 77" name="Spellcaster" hd="d?" desc="A generic class that has an array of magical effects at her beck and call." />
+    <class level="1" book="Complete Adventurer page 13" name="Spellthief" hd="d?" desc="Spellthieves use skill and arcane magic to drain the abilities of their opponents and turn their foes' own powers against them." />
+    <class level="1" book="Complete Divine page 14" name="Spirit Shaman" hd="d?" desc="By bargaining with living spirits, the spirit shaman gains power over the natural world and mighty divine magic." />
+    <class level="1" book="Complete Warrior page 11" name="Swashbuckler" hd="d?" desc="The swashbuckler embodies the concepts of daring and panache." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 15" name="Swordsage" hd="d?" desc="A master of martial maneuvers, the sword sage is a physical adept -- a blade wizard whose knowledge of the Sublime Way lets him unlock potent abilities, many of which are overtly supernatural or magical in nature." />
+    <class level="1" book="Complete Adventurer page 83" name="Thief-acrobat" hd="d?" desc="A thief-acrobat excels in getting in and getting out." />
+    <class level="1" book="Book of Vile Darkness page 68" name="Thrall Of Graz'zt" hd="d?" desc="The thrall of Graz'zt is a sinister, conniving, and thoroughly evil master of arcane lore and dark secrets." />
+    <class level="1" book="Complete Psionic page 149" name="Thri-kreen Racial Class" hd="d?" desc="Fierce hunters and faultless trackers, the thri-kreen are a race of insectfolk sometimes known as mantis warriors." />
+    <class level="1" book="Unearthed Arcana page 51" name="Thug" hd="d?" desc="A variant fighter who is a street fighter and a survivor who learns to mix brute force with a bit of craftiness." />
+    <class level="1" book="Unearthed Arcana page 48" name="Totem Barbarian" hd="d?" desc="A variant barbarian who dedicates himself to a totem creature." />
+    <class level="1" book="Magic of Incarnum page 29" name="Totemist" hd="d?" desc="You channel the soul energy of magical beasts to make your soulmelds and claim them as your totems to acquire a share in their power." />
+    <class level="1" book="Tome of Magic page 198" name="Truenamer" hd="d?" desc="If you want to understand the secret language of the universe, the truenamer class is for you." />
+    <class level="1" book="Book of Vile Darkness page 72" name="Ur-priest" hd="d?" desc="A small number of ur-priests have learned to tap into divine power and use it without praying to or worshiping a god." />
+    <class level="1" book="Sharn: City of Towers page 167" name="Urban Adept" hd="d?" desc="A variant adept for Sharn." />
+    <class level="1" book="Unearthed Arcana page 55" name="Urban Ranger" hd="d?" desc="A variant ranger who stalks the treacherous streets of the city." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 20" name="Warblade" hd="d?" desc="The warblade was born for conflict. Swift, strong, enduring, and utterly confident in his martial skills, he seeks to test himself against worthy foes." />
+    <class level="1" book="Complete Arcane page 5" name="Warlock" hd="d?" desc="A supernatural character whose sinister powers are inborn abilities, not spells." />
+    <class level="1" book="Complete Arcane page 10" name="Warmage" hd="d?" desc="A militant spellcaster whose training focuses on battlefield magic." />
+    <class level="1" book="Dragonlance Campaign Setting page 53" name="Warrior" hd="d?" desc="This NPC class contains people such as soldiers, guards, and militia." />
+    <class level="1" book="Oriental Adventures page 53" name="Weapon Master (kensei)" hd="d?" desc="For weapon masters, the perfection of kiis found in the mastery of a single melee weapon." />
+    <class level="1" book="Expanded Psionics Handbook page 29" name="Wilder" hd="d?" desc="A passionate, reckless talent who wields uncontrolled psionic power." />
+    <class level="1" book="Unearthed Arcana page 56" name="Wilderness Rogue" hd="d?" desc="A variant rogue who prefers to put her skills to use in the great outdoors." />
+    <class level="1" book="Player's Handbook v.3.5 page 55" name="Wizard" hd="d?" desc="A potent spellcaster schooled in the arcane arts." />
+    <class level="1" book="Oriental Adventures page 30" name="Wu Jen" hd="d?" desc="Wu jen are spellcasters with mysterious powers." />
+    <class level="1" book="Savage Species page 97" name="Yuan-ti Cultist" hd="d?" desc="The mysteries of the evil deities of the yuan-ti are mastered by yuan-ti cultist masters." />
+    <class level="1" book="Lords of Madness page 182" name="Abolisher {Prestige Class}" hd="d?" desc="The abolisher is a crusader against that which taints, usurps, and replaces the ordered nature of things with alien desires and monstrous needs." />
+    <class level="1" book="Complete Mage page 50" name="Aburant Champion {Prestige Class}" hd="d?" desc="A warrior who dabbles in abjuration magic." />
+    <class level="1" book="Tome of Magic page 204" name="Acolyte Of The Ego {Prestige Class}" hd="d?" desc="By learning to speak their own truenames, acolytes of the ego strive to unlock hidden powers lost to the cosmos." />
+    <class level="1" book="Complete Arcane page 19" name="Acolyte Of The Skin {Prestige Class}" hd="d?" desc="Acolytes of the skin seek to gain power by replacing their skin with that of a demon's." />
+    <class level="1" book="Epic Level Handbook page 24" name="Agent Retriever {Prestige Class}" hd="d?" desc="Finding items, especially long-lost ones, is an agent retriever's specialty." />
+    <class level="1" book="Unapproachable East page 18" name="Aglarondan Griffonrider {Prestige Class}" hd="d?" desc="Soaring above the Yuirwood and the coasts of Aglarond, the famed Aglarondan griffonriders are an elite force of aerial knights who serve the Simbul and defend their homeland against attack." />
+    <class level="1" book="Oriental Adventures page 220" name="Akodo Champion {Prestige Class}" hd="d?" desc="Akodo champions are the leaders of the mighty army of the Lion clan." />
+    <class level="1" book="Magic of Eberron page 53" name="Alchemist Savant {Prestige Class}" hd="d?" desc="The alchemist savant excels in the capacity to break down the normal barriers that lie between alchemy and magic, between potion and alchemical fluid, between science and art." />
+    <class level="1" book="Complete Arcane page 21" name="Alienist {Prestige Class}" hd="d?" desc="Alienists deal with powers and entities from terrifyingly remote reaches of space and time." />
+    <class level="1" book="Complete Psionic page 17" name="Anarchic Initiate {Prestige Class}" hd="d?" desc="The anarchic initiate is more than a wilder; he is an initiate to the truth that underlies the wildness in the depth of his being." />
+    <class level="1" book="Serpent Kingdoms page 159" name="Ancient Master {Prestige Class}" hd="d?" desc="Those yuan-ti who grow mightier of mind and gain additional psionic powers are called ancient masters." />
+    <class level="1" book="Tome of Magic page 50" name="Anima Mage {Prestige Class}" hd="d?" desc="Anima mages see vestiges as mere tools, no different from spell component pouches or a wand of fireball." />
+    <class level="1" book="Complete Adventurer page 22" name="Animal Lord {Prestige Class}" hd="d?" desc="Each animal lord forms a bond with one group of animals." />
+    <class level="1" book="Book of Exalted Deeds page 49" name="Anointed Knight {Prestige Class}" hd="d?" desc="The anointed knight is a holy soldier who has taken great pains to learn the intricacies of alchemy in order to become a more capable combatant." />
+    <class level="1" book="Book of Exalted Deeds page 51" name="Apostle Of Peace {Prestige Class}" hd="d?" desc="The apostle of peace is an advocate for nonviolent resolution of conflict." />
+    <class level="1" book="Faiths &amp; Pantheons page 182" name="Arachne {Prestige Class}" hd="d?" desc="Arachnes are priestesses of Lolth who have risen to the pinnacle of drow society, worshiping Lolth only for the power she grants." />
+    <class level="1" book="Ghostwalk page 19" name="Arboreal Guardian {Prestige Class}" hd="d?" desc="Within the Spirit Wood are the Arboreal Guardians, men and women dedicated to protecting and ministering the living repositories of elf and half-elf spirits." />
+    <class level="1" book="Underdark page 28" name="Arcachnomancer {Prestige Class}" hd="d?" desc="Many creatures of the Underdark are drawn to the power of the spider and that of the master of spiders -- the arachnomancer." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 176" name="Arcane Archer {Prestige Class}" hd="d?" desc="The arcane archer is a warrior skilled in using magic to supplement her combat prowess." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 40" name="Arcane Devotee {Prestige Class}" hd="d?" desc="Arcane devotees complement the divine magic of a church's clerical leaders and are among the most important and respected members of a deity's following." />
+    <class level="1" book="Races of the Wild page 108" name="Arcane Hierophant  {Prestige Class}" hd="d?" desc="Arcane hierophants wield a blending of arcane magic and divine magic with a heavy emphasis on nature and the elements." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 177" name="Arcane Trickster {Prestige Class}" hd="d?" desc="Arcane tricksters combine their knowledge of spells with a taste for intrigue, larceny, or just plain mischief." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 178" name="Archmage {Prestige Class}" hd="d?" desc="The most advanced practitioners of arcane magic are frequently archmages." />
+    <class level="1" book="Planar Handbook page 55" name="Ardent Dilettante  {Prestige Class}" hd="d?" desc="A diversity of interests and a moderate level of ability in one skill can lead one to become an ardent dilettante." />
+    <class level="1" book="Faiths of Eberron page 70" name="Argent Fist {Prestige Class}" hd="d?" desc="Only a precious few possess the focus, the dedication, and the physical prowess to master the abilities of the argent fist, but few enemies can stand against those who do." />
+    <class level="1" book="Complete Arcane page 24" name="Argent Savant {Prestige Class}" hd="d?" desc="The argent savant regards spells that evoke or apply magical force as the noblest and most fascinating spells at her disposal." />
+    <class level="1" book="Sandstorm page 66" name="Ashworm Dragoon  {Prestige Class}" hd="d?" desc="Ashworm dragoons have formed a bond with a single ashworm that is so strong that it is almost an extension of their wills." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 180" name="Assassin {Prestige Class}" hd="d?" desc="The assassin is the master of dealing quick, lethal blows." />
+    <class level="1" book="Planar Handbook page 63" name="Astral Dancer {Prestige Class}" hd="d?" desc="A few skilled combatants learn to take advantage of conditions on the Astral Plane." />
+    <class level="1" book="Races of Eberron page 133" name="Atavist  {Prestige Class}" hd="d?" desc="The discipline of an atavist strengthens his bond to the ancestral spirit and to all other kalashtar that have embraced their unique heritage." />
+    <class level="1" book="Faiths &amp; Pantheons page 184" name="Auspician {Prestige Class}" hd="d?" desc="Auspicians, who manipulate luck as if it were the strings of a worn mandolin, give credence to their claims." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 46" name="Bane Of Infidels {Prestige Class}" hd="d?" desc="The bane of infidels is the leader of a xenophobic tribe." />
+    <class level="1" book="Oriental Adventures page 34" name="Battle Maiden {Prestige Class}" hd="d?" desc="Battle maidens are the stuff of wonder and legend, an order of mounted female samurai whose swift, fearless attacks are renowned throughout the world." />
+    <class level="1" book="Unearthed Arcana page 164" name="Battle Scion {Prestige Class}" hd="d?" desc="This prestige class is for the wielders of legendary weapons made for the hands of fighters, barbarians, rangers, monks, and the occasional paladin." />
+    <class level="1" book="Races of Faerun page 178" name="Battlerager  {Prestige Class}" hd="d?" desc="Dwarven battleragers, or kuldjargh ('axe idiots'), are legendary berserker warriors who can enter a battle frenzy through ritualist singing." />
+    <class level="1" book="Races of Stone page 97" name="Battlesmith  {Prestige Class}" hd="d?" desc="A battlesmith is a skilled dwarf armorer and weaponsmith who uses her experience in battle, as well as her masterful weaponsmithing and armorsmithing abilities, to create deadly items for her kinsmen to wield in defense of their homes." />
+    <class level="1" book="Oriental Adventures page 225" name="Bayushi Deceiver {Prestige Class}" hd="d?" desc="The Bayushi are charged with the dirtiest work in the empire." />
+    <class level="1" book="Complete Warrior page 16" name="Bear Warrior {Prestige Class}" hd="d?" desc="Bear warriors, through a special relationship with bear spirits, literally adopt a bear's strength in the rage of battle, actually transforming into bears while they fight." />
+    <class level="1" book="Complete Adventurer page 26" name="Beastmaster {Prestige Class}" hd="d?" desc="A beastmaster feels more at home among the animals of nature than fellow sentient beings." />
+    <class level="1" book="Monster Compendium: Monsters of Faerun page 21" name="Beholder Mage  {Prestige Class}" hd="d?" desc="Through ritual destruction of its central eye, a beholder can learn to channel and use magic much more quickly and efficiently than can almost any other race." />
+    <class level="1" book="Lords of Madness page 42" name="Beholder Mage {Prestige Class}" hd="d?" desc="Through ritual destruction of its central eye, a beholder can learn to channel and use magic much more quickly and efficiently than can almost any other race." />
+    <class level="1" book="Book of Exalted Deeds page 53" name="Beloved Of Valarian {Prestige Class}" hd="d?" desc="The beloved of Valarian are women who have foresworn the love of mortals to dedicate themselves entirely to the unicorn deity Valarian." />
+    <class level="1" book="Tome of Magic page 208" name="Bereft {Prestige Class}" hd="d?" desc="The bereft are a group of truenamers who devote themselves to mastering the word of unmaking, a powerful component of Truespeech purportedly able to unravel creation." />
+    <class level="1" book="Deities and Demigods page 201" name="Berserk {Prestige Class}" hd="d?" desc="Berserks are warriors who dress themselves in bearskins, taking advantage of the fear most people have for wild animals and inviting the wild rage of the animal into the warrior's body." />
+    <class level="1" book="Champions of Ruin page 44" name="Black Blood Cultist {Prestige Class}" hd="d?" desc="Black Blood cultists are savage fighters whose natural attacks become more fearsome as they increase in level." />
+    <class level="1" book="Player's Guide to Faerun page 177" name="Black Blood Hunter {Prestige Class}" hd="d?" desc="Malar grants exceptional power to those who supplement their bestial level of evil with truly vile acts." />
+    <class level="1" book="Complete Divine page 21" name="Black Flame Zealot {Prestige Class}" hd="d?" desc="Trained in unholy rites, the black flame zealots use stealth, divine magic, and the zeal of fanaticism to destroy those who have given offense to their god." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 181" name="Blackguard {Prestige Class}" hd="d?" desc="The blackguard is the quintessential black knight." />
+    <class level="1" book="Races of Stone page 99" name="Blade Bravo {Prestige Class}" hd="d?" desc="Blade bravos are gnomes who learn the ways of the rapier." />
+    <class level="1" book="Oriental Adventures page 37" name="Blade Dancer {Prestige Class}" hd="d?" desc="To blade dancers, the sword is more than a weapon -- it is an ally, a friend, a spirit companion." />
+    <class level="1" book="Complete Warrior page 17" name="Bladesinger {Prestige Class}" hd="d?" desc="Bladesingers are elves who have blended art, swordplay, and arcane magic into a harmonious whole." />
+    <class level="1" book="Complete Divine page 23" name="Blighter {Prestige Class}" hd="d?" desc="Blighters bring desolation wherever they tread." />
+    <class level="1" book="Complete Arcane page 26" name="Blood Magus {Prestige Class}" hd="d?" desc="Blood magi are deceased spellcasters who gain an understanding of blood's importance when returned to life." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 96" name="Bloodclaw Master {Prestige Class}" hd="d?" desc="A bloodclaw master embraces the animal within to a degree that it becomes external, partially transforming him into the beast from which the discipline was inspired." />
+    <class level="1" book="Complete Adventurer page 28" name="Bloodhound {Prestige Class}" hd="d?" desc="A bloodhound tracks down wrongdoers and brings them to whatever justice awaits them." />
+    <class level="1" book="Draconomicon page 86" name="Bloodscaled Fury {Prestige Class}" hd="d?" desc="A bloodscaled fury is a dragon whose rage surpasses that of a human barbarian as the barbarian's rage surpasses a child's tantrum." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 100" name="Bloodstorm Blade {Prestige Class}" hd="d?" desc="Other martial adepts rightfully look with wonder upon those who learn the bloodstorm style." />
+    <class level="1" book="Miniatures Handbook page 16" name="Bonded Summoner  {Prestige Class}" hd="d?" desc="He who learns to leash the furies of the Elemental Planes is known as a bonded summoner." />
+    <class level="1" book="Ghostwalk page 21" name="Bone Collector {Prestige Class}" hd="d?" desc="A bone collector is a person who draws personal power from the destruction of undead." />
+    <class level="1" book="FN page 117" name="Bone Knight {Prestige Class}" hd="d?" desc="Bone knights are Karrn patriots, living protectors who fight alongside the undead legions of their land." />
+    <class level="1" book="Races of Faerun page 181" name="Breachgnome {Prestige Class}" hd="d?" desc="A breachgnome is a mighty gnome who is skilled in fighting in cramped conditions." />
+    <class level="1" book="Tome of Magic page 212" name="Brimstone Speaker {Prestige Class}" hd="d?" desc="Brimstone speakers regard the secret language of truenames as nothing less than a gift from the gods." />
+    <class level="1" book="Races of Eberron page 139" name="Cabinet Trickster {Prestige Class}" hd="d?" desc="Cabinet tricksters are the changeling agents of the Cabinet of Faces." />
+    <class level="1" book="Book of Vile Darkness page 52" name="Cancer Mage  {Prestige Class}" hd="d?" desc="The cancer mage makes quick, poisonous attacks and then retreats." />
+    <class level="1" book="Tome and Blood: A Guidebook to Wizards and Sorcerers page 52" name="Candle Caster {Prestige Class}" hd="d?" desc="Also called 'spell chandlers,' these specialists fill their time fashioning candles, both for esthetics and for power." />
+    <class level="1" book="Sharn: City of Towers page 162" name="Cannith Wand Adept  {Prestige Class}" hd="d?" desc="The Cannith wand adept is an artificer or other spellcaster who specializes in mastering wands." />
+    <class level="1" book="Explorer's Handbook page 58" name="Cataclysm Mage {Prestige Class}" hd="d?" desc="Cataclysm mages seek after Eberron's most powerful mysteries, long lost to the past." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 12" name="Cavalier  {Prestige Class}" hd="d?" desc="Representing the ultimate in mounted warfare, the cavalier is the quintessential knight in shining armor." />
+    <class level="1" book="Complete Warrior page 19" name="Cavalier {Prestige Class}" hd="d?" desc="Representing the ultimate in mounted warfare, the cavalier is the quintessential knight in shining armor." />
+    <class level="1" book="Underdark page 30" name="Cavelord {Prestige Class}" hd="d?" desc="A passion for the narrow, dim ways of the world burns in the breast of the cavelord." />
+    <class level="1" book="Player's Guide to Faerun page 178" name="Celebrant Of Sharess {Prestige Class}" hd="d?" desc="Celebrants of Sharess are seducers and warriors, hedonists and pious champions of good." />
+    <class level="1" book="Book of Exalted Deeds page 55" name="Celestial Mystic {Prestige Class}" hd="d?" desc="Celestial mystics seek to attain ultimate unity with the perfect good." />
+    <class level="1" book="Expanded Psionics Handbook page 141" name="Cerebremancer  {Prestige Class}" hd="d?" desc="Cerebremancers access both the arcane mysteries of spellcasting and the psionic powers of the mind." />
+    <class level="1" book="Races of Destiny page 111" name="Chameleon  {Prestige Class}" hd="d?" desc="Chameleons are dilettantes in every class and masters of none." />
+    <class level="1" book="Races of the Wild page 113" name="Champion Of Corellon Larethian {Prestige Class}" hd="d?" desc="The champion of Corellon Larethian is a noble elf fighter, an elf knight or lord who can stand up to any orc or human warrior." />
+    <class level="1" book="Book of Exalted Deeds page 56" name="Champion Of Gwynharwyf {Prestige Class}" hd="d?" desc="The champion of Gwynharwyf is a mortal barbarian who strives to emulate her sublime balance of fury and reserve while retaining a focus on good." />
+    <class level="1" book="Planar Handbook page 61" name="Chaotician {Prestige Class}" hd="d?" desc="Chaoticians seek to enjoy the beauty of the unpredictable, and by seeking to emulate the philosophy of chaos in their actions, they create a fabulous journey through life in which nothing is a bore." />
+    <class level="1" book="Tome of Magic page 117" name="Child Of Night {Prestige Class}" hd="d?" desc="They prefer to call themselves 'black transmogrifists,' but most know them as children of night." />
+    <class level="1" book="Complete Divine page 26" name="Church Inquisitor {Prestige Class}" hd="d?" desc="The church inquisitor uncovers taint within the church and cuts it away." />
+    <class level="1" book="Planar Handbook page 58" name="Cipher Adept {Prestige Class}" hd="d?" desc="Seamless integration brings the cipher adept bliss." />
+    <class level="1" book="Sharn: City of Towers page 163" name="Citadel Elite {Prestige Class}" hd="d?" desc="The elite agent of the Citadel is the best of the best, saved for the most dangerous, most important assignments or granted latitude to serve the King and Crown as he sees fit." />
+    <class level="1" book="Frostburn page 52" name="Cloud Anchorite  {Prestige Class}" hd="d?" desc="The cloud anchorite seeks a way to achieve immortality while maintaining life and awareness." />
+    <class level="1" book="Player's Guide to Faerun page 174" name="Cognition Thief {Prestige Class}" hd="d?" desc="A cognition thief's subtle ability to worm her way into a target's very consciousness makes her the ultimate secret agent." />
+    <class level="1" book="Serpent Kingdoms page 161" name="Coiled Cabalist {Prestige Class}" hd="d?" desc="Standing apart from the priests of Sseth but careful never to draw their collective ire by openly opposing them, the members of the Coiled Cabal pursue the arcane arts largely in secrecy." />
+    <class level="1" book="Heroes of Battle page 99" name="Combat Medic {Prestige Class}" hd="d?" desc="The combat medic keeps her allies alive and tends to the fallen on the front lines of battle." />
+    <class level="1" book="Complete Divine page 28" name="Consecrated Harrier {Prestige Class}" hd="d?" desc="The consecrated harrier acts as a bounty hunter for her religion or organization." />
+    <class level="1" book="Complete Divine page 30" name="Contemplative {Prestige Class}" hd="d?" desc="Contemplatives devote their lives to cultivating a greater closeness with their deities." />
+    <class level="1" book="Heroes of Horror page 88" name="Corrupt Avenger {Prestige Class}" hd="d?" desc="A corrupt avenger accepts any cost to have his vengeance, even to the forfeit of his very soul." />
+    <class level="1" book="Epic Level Handbook page 26" name="Cosmic Descryer {Prestige Class}" hd="d?" desc="The cosmic descryer is interested in the infinite variety of the planes and fascinated by the different layers of the multiverse." />
+    <class level="1" book="Power of Faerun page 108" name="Court Herald {Prestige Class}" hd="d?" desc="The court herald prestige class is a modified version of the loremaster prestige class, which is described in the Dungeon Master's Guide." />
+    <class level="1" book="Races of Stone page 101" name="Cragtop Archer {Prestige Class}" hd="d?" desc="Cragtop archers train their eyes and minds to find target at great distances, and to quickly compensate for wind, movement, and other factors that affect shots of such difficulty." />
+    <class level="1" book="Shining South page 23" name="Crinti Shadow Marauder  {Prestige Class}" hd="d?" desc="Crinti shadow marauders combine the physical prowess of master rider with the stealth of the most cunning shadowdancers." />
+    <class level="1" book="Frostburn page 54" name="Cryokineticist {Prestige Class}" hd="d?" desc="The cryokineticist is the master of cold psionic energy." />
+    <class level="1" book="Lost Empires of Faerun page 10" name="Cultist Of The Shattered Peak  {Prestige Class}" hd="d?" desc="Cultists of the Shattered Peak possess skill at arms, stealth, and a smattering of ancient lore." />
+    <class level="1" book="FN page 86" name="Cyran Avenger {Prestige Class}" hd="d?" desc="These survivors of the Day of Mourning seek to uncover the cause of the cataclysm and avenge their people against the architects of the Mournland." />
+    <class level="1" book="Complete Adventurer page 31" name="Daggerspell Mage {Prestige Class}" hd="d?" desc="Daggerspell mages work to perfect a unique fighting and spellcasting (arcane) style that relies on wielding a pair of daggers at all times." />
+    <class level="1" book="Complete Adventurer page 36" name="Daggerspell Shaper {Prestige Class}" hd="d?" desc="Daggerspell shapers work to perfect a unique fighting and spellcasting (druid) style that relies on wielding a pair of daggers at all times." />
+    <class level="1" book="Oriental Adventures page 215" name="Daidoji Bodyguard {Prestige Class}" hd="d?" desc="The Daidoji concentrate on defensive maneuvers and a style of fighting that induces their opponents to defeat themselves." />
+    <class level="1" book="Complete Warrior page 20" name="Dark Hunter {Prestige Class}" hd="d?" desc="Dark hunters specialize in hunting down and eliminating creatures in the dark, twisting caves of the Underdark." />
+    <class level="1" book="FN page 68" name="Dark Lantern {Prestige Class}" hd="d?" desc="The Dark Lanterns serve the crown of Breland as spies and assassins." />
+    <class level="1" book="Lords of Darkness page 33" name="Darkmask  {Prestige Class}" hd="d?" desc="Darkmasks strike a balance between their faith and their skills at stealth." />
+    <class level="1" book="Lords of Madness page 186" name="Darkrunner {Prestige Class}" hd="d?" desc="Darkrunners devote their lives to traveling the haunted underground depths." />
+    <class level="1" book="Complete Warrior page 23" name="Darkwood Stalker {Prestige Class}" hd="d?" desc="Some elves train as elite hunters of orcs; these hunters are called darkwood stalkers." />
+    <class level="1" book="Races of Stone page 103" name="Dawncaller {Prestige Class}" hd="d?" desc="Dawncallers are goliath bards responsible for guarding their tribe throughout the night." />
+    <class level="1" book="Magic of Eberron page 57" name="Deadgrim {Prestige Class}" hd="d?" desc="The deadgrim are an elite faction with in the Red Watchers, a new organization of undead hunters within Karrnath." />
+    <class level="1" book="Heroes of Horror page 93" name="Death Delver {Prestige Class}" hd="d?" desc="The death delver is that rare individual, who, rather than fearing and avoiding death, delves as deeply into its mysteries as he can, to better understand and eventually gain some small power over it." />
+    <class level="1" book="Ghostwalk page 23" name="Deathwarden Chanter {Prestige Class}" hd="d?" desc="The Deathwarden chanters are the most prestigious, respected, and mysterious members of the dwarven Deathwarden clan." />
+    <class level="1" book="Underdark page 32" name="Deep Diviner {Prestige Class}" hd="d?" desc="Deep diviners are intimates of the earth and all that it hides." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 105" name="Deepstone Sentinel {Prestige Class}" hd="d?" desc="The Stone Dragon discipline traces its roots back to an ancient order of dwarves that used the power of the earth to enhance their combat style." />
+    <class level="1" book="Races of Stone page 105" name="Deepwarden {Prestige Class}" hd="d?" desc="Deepwardens are dwarves who serve as a living early warning system against threats from both the environment and other creatures." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 52" name="Deepwood Sniper {Prestige Class}" hd="d?" desc="A deepwood sniper is patient, careful, quiet , and deadly accurate." />
+    <class level="1" book="Book of Exalted Deeds page 58" name="Defender Of Sealtiel {Prestige Class}" hd="d?" desc="Defenders of Sealtiel are sworn to uphold the ideals of Sealtiel, which includes fighting off forces of evil when they assault good." />
+    <class level="1" book="Planar Handbook page 44" name="Defiant {Prestige Class}" hd="d?" desc="Defiants take the teaching of the Athar to heart in a way that grants them tremendous powers against those who claim to wield divine might." />
+    <class level="1" book="Book of Vile Darkness page 54" name="Demonologist {Prestige Class}" hd="d?" desc="A demonologist is a mortal who has devoted his life to the study of demons." />
+    <class level="1" book="Complete Warrior page 25" name="Dervish {Prestige Class}" hd="d?" desc="The dervish epitomizes speeds, quickness, and abandon." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 13" name="Devoted Defender {Prestige Class}" hd="d?" desc="The devoted defender is a professional guardian." />
+    <class level="1" book="Book of Vile Darkness page 56" name="Diabolist {Prestige Class}" hd="d?" desc="A diabolist does not serve devils -- she wants to be one." />
+    <class level="1" book="Dragon Magic page 30" name="Diamond Dragon {Prestige Class}" hd="d?" desc="The boldest of the sages study the link between gem dragons and psionics, learning to tap into what they refer to as the draconic psionic collective." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 43" name="Dirgesinger {Prestige Class}" hd="d?" desc="Dirgesingers voice melodies not of celebration and joy, but of sorrow and grief." />
+    <class level="1" book="Draconomicon page 87" name="Disciple Of Ashardalon {Prestige Class}" hd="d?" desc="Disciples of Ashardalon bind fiendish spirits to their own hearts, eventually taking on the characteristics of demonic spawn themselves." />
+    <class level="1" book="Book of Vile Darkness page 57" name="Disciple Of Asmodeus {Prestige Class}" hd="d?" desc="A disciple of Asmodeus uses his power and influence to learn secrets, which in turn gains him more power." />
+    <class level="1" book="Book of Vile Darkness page 58" name="Disciple Of Baalzebul {Prestige Class}" hd="d?" desc="A disciple of Baalzebul uses deceit and trickery to get what she wants." />
+    <class level="1" book="Book of Vile Darkness page 60" name="Disciple Of Dispater {Prestige Class}" hd="d?" desc="A disciple of Dispater is a warlike general of evil." />
+    <class level="1" book="Book of Vile Darkness page 60" name="Disciple Of Mammon {Prestige Class}" hd="d?" desc="A disciple of Mammon takes what she wants any way she can." />
+    <class level="1" book="Book of Vile Darkness page 62" name="Disciple Of Mephistopheles {Prestige Class}" hd="d?" desc="A disciple of Mephistopheles wields hellfire as his weapon." />
+    <class level="1" book="Races of the Dragon page 75" name="Disciple Of The Eye {Prestige Class}" hd="d?" desc="As a disciple of the eye, you know the messages that the eyes alone can impart." />
+    <class level="1" book="Tome of Magic page 216" name="Disciple Of The Word {Prestige Class}" hd="d?" desc="Disciples of the word are intellectual warrior monks who, through a deeper understanding of their truenames, transcend the limits of their mortal form." />
+    <class level="1" book="Frostburn page 56" name="Disciple Of Thrym {Prestige Class}" hd="d?" desc="Aside from predictions of Ragnarok, disciples of Thrym spend a large portion of their time undermining those who serve Thor and Loki, the deities who conspired against Thrym." />
+    <class level="1" book="Draconomicon page 89" name="Dispassionate Watcher Of Chronepsis {Prestige Class}" hd="d?" desc="Taking the role of observers rather than active participants, dispassionate watchers of Chronepsis remain aloof from the events of the world." />
+    <class level="1" book="Manual of the Planes page 24" name="Divine Agent  {Prestige Class}" hd="d?" desc="The divine agent is a specially selected agent of her deity, and she acts in the service of that power or deity." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 42" name="Divine Champion {Prestige Class}" hd="d?" desc="Divine champions are mighty warriors who dedicate themselves to their deity's cause, defending holy ground, destroying enemies of the church, and slaying mythical beasts and clerics of opposing faiths." />
+    <class level="1" book="Complete Divine page 33" name="Divine Crusader {Prestige Class}" hd="d?" desc="The divine crusader embodies devotion and dedication to a chosen deity." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 43" name="Divine Disciple {Prestige Class}" hd="d?" desc="Divine disciples interpret the divine will, act as teachers and guides to other members of the clergy, and arm lay followers of their deity with the power of their patron." />
+    <class level="1" book="Epic Level Handbook page 27" name="Divine Emissary {Prestige Class}" hd="d?" desc="Deities have need of powerful servants, many of whom are epic clerics, paladins, and other characters." />
+    <class level="1" book="Complete Divine page 34" name="Divine Oracle {Prestige Class}" hd="d?" desc="Some mortals hear the words of deities; these can be divine oracles." />
+    <class level="1" book="Races of Stone page 107" name="Divine Prankster {Prestige Class}" hd="d?" desc="These gnomes embrace Garl's methods of teaching through harmless object lessons and dedicate their lives to acting as his agents in the world." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 44" name="Divine Seeker {Prestige Class}" hd="d?" desc="The divine seeker infiltrates dangerous places to rescue prisoners, reclaim stolen relics, or eliminate enemy leaders." />
+    <class level="1" book="Return to the Temple of Elemental Evil page 162" name="Doomdreamer  {Prestige Class}" hd="d?" desc="Doomdreamers are the elite among the ranks of the cult of Tharizdun." />
+    <class level="1" book="Faiths &amp; Pantheons page 186" name="Doomguide {Prestige Class}" hd="d?" desc="Doomguides belong to an elite order of spellcasting warriors in service to the Judge of the Damned." />
+    <class level="1" book="Planar Handbook page 47" name="Doomlord {Prestige Class}" hd="d?" desc="The doomlord's life holds the greatest appeal for fighters and barbarians who enjoy smashing and destroying." />
+    <class level="1" book="Races of the Dragon page 79" name="Dracolexi {Prestige Class}" hd="d?" desc="As a dracolexi, you try to understand that primordial vocabulary by devoting yourself to the study of ancient dialects and languages, hoping to discover how certain Draconic words were once uttered." />
+    <class level="1" book="Draconomicon page 122" name="Dracolyte {Prestige Class}" hd="d?" desc="The dracolyte takes up worship of the draconic gods." />
+    <class level="1" book="Draconomicon page 90" name="Dragon Ascendant {Prestige Class}" hd="d?" desc="Dragon ascendants seek to transcend the limitations of material existence so as to become nothing less than deities." />
+    <class level="1" book="Dragon Magic page 34" name="Dragon Descendant {Prestige Class}" hd="d?" desc="A secret monastic order, dragon descendants tap into the power of their draconic heritage to call on their ancestors in times of need." />
+    <class level="1" book="Races of the Dragon page 84" name="Dragon Devotee {Prestige Class}" hd="d?" desc="Some individuals feel the call of dragons more strongly, which may lead them into an attempt to awaken their blood and bring those traits to the fore." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 183" name="Dragon Disciple {Prestige Class}" hd="d?" desc="Dragon disciples use their magic as a catalyst to ignite their dragon blood." />
+    <class level="1" book="Dragon Magic page 38" name="Dragon Lord {Prestige Class}" hd="d?" desc="A dragon lord is the general at the head of an army, the emperor at the helm of an empire, or the warleader who dominates a battlefield." />
+    <class level="1" book="Magic of Eberron page 63" name="Dragon Prophet {Prestige Class}" hd="d?" desc="The dragon prophet is a member of one of the 'lesser races' who shares the dragons' ambitious goal of understanding the complex and convoluted draconic Prophecy." />
+    <class level="1" book="Dragonlance Campaign Setting page 77" name="Dragon Rider  {Prestige Class}" hd="d?" desc="Dragon riders develop a strong bond with their mounts that allows the two to work together." />
+    <class level="1" book="Miniatures Handbook page 18" name="Dragon Samurai {Prestige Class}" hd="d?" desc="Dragon samurai are dedicated warriors, members of a special, self-selected class who revered dragonkind and emulate dragons' ferocious martial abilities to the point of taking on some draconic traits." />
+    <class level="1" book="Races of the Dragon page 88" name="Dragonheart Mage {Prestige Class}" hd="d?" desc="The dragonheart mage is perfect for the dedicated spellcaster who wishes to embrace the power of dragon blood while still advancing in magical expertise." />
+    <class level="1" book="Draconomicon page 123" name="Dragonkith {Prestige Class}" hd="d?" desc="Dragonkith are creatures that serve and aid dragons." />
+    <class level="1" book="Eberron Campaign Setting page 73" name="Dragonmark Heir  {Prestige Class}" hd="d?" desc="Dragonmark heirs have the ability to improve the dragonmarks they have manifested, as well as to develop additional abilities related to their dragonmarks." />
+    <class level="1" book="Draconomicon page 124" name="Dragonrider {Prestige Class}" hd="d?" desc="Dragonriders soar through the clouds atop a draconic steed." />
+    <class level="1" book="Draconomicon page 125" name="Dragonslayer {Prestige Class}" hd="d?" desc="Dragonslayers combat dragons." />
+    <class level="1" book="Draconomicon page 127" name="Dragonsong Lyrist {Prestige Class}" hd="d?" desc="The dragonsong lyrist taps into the power of dragonsong." />
+    <class level="1" book="Draconomicon page 128" name="Dragonstalker {Prestige Class}" hd="d?" desc="The dragonstalker uses stealth and guile to combat dragons." />
+    <class level="1" book="Heroes of Battle page 103" name="Dread Commando {Prestige Class}" hd="d?" desc="Dread commandos are the elite scouts and strike force of a well-trained mercenary band." />
+    <class level="1" book="Complete Adventurer page 39" name="Dread Pirate {Prestige Class}" hd="d?" desc="A dread pirate has mastered every aspect of larceny on the high seas." />
+    <class level="1" book="Heroes of Horror page 98" name="Dread Witch {Prestige Class}" hd="d?" desc="The dread witch is a spellcaster who manipulates fear as readily and effectively as other casters manipulate magic itself." />
+    <class level="1" book="Faiths &amp; Pantheons page 188" name="Dreadmaster {Prestige Class}" hd="d?" desc="Dreadmasters seek to rule absolutely, preferably through terror and domination." />
+    <class level="1" book="Underdark page 33" name="Drow Judicator {Prestige Class}" hd="d?" desc="A mortal imbued with fiendish cruelty, the drow judicator is a knight most foul." />
+    <class level="1" book="Complete Warrior page 27" name="Drunken Master {Prestige Class}" hd="d?" desc="By weaving and staggering about as if inebriated, drunken boxers avoid many blows." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 185" name="Duelist {Prestige Class}" hd="d?" desc="The duelist is a nimble, intelligent fighter trained in making precise attacks with light weapons." />
+    <class level="1" book="Complete Adventurer page 42" name="Dungeon Delver {Prestige Class}" hd="d?" desc="In many ways, the dungeon delver is the ultimate adventuring rogue." />
+    <class level="1" book="Unapproachable East page 22" name="Durthan {Prestige Class}" hd="d?" desc="Durthans are an order of spellcasters who tap into the darker spirits of Rashemen." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 186" name="Dwarven Defender {Prestige Class}" hd="d?" desc="The defender is a sponsored champion of a dwarven cause" />
+    <class level="1" book="Faiths &amp; Pantheons page 189" name="Dweomerkeeper {Prestige Class}" hd="d?" desc="Dweomerkeepers are Mystra's shepherds, safeguarding the Weave against threats to its integrity." />
+    <class level="1" book="Races of Stone page 110" name="Earth Dreamer {Prestige Class}" hd="d?" desc="Earth dreamers move within the ancient dreams of the mountains, attuning themselves to their power and mastering strange abilities over the earth." />
+    <class level="1" book="Complete Psionic page 22" name="Ebon Saint {Prestige Class}" hd="d?" desc="The ebon saint lives in the darkness, but seeks to exposehis enemies to the light." />
+    <class level="1" book="Complete Psionic page 26" name="Ectopic Adept {Prestige Class}" hd="d?" desc="Ectoplasm is the preferred medium of creation for the ectopic adept, and his mind serves as the mold, kiln, and wheel upon which his works are turned." />
+    <class level="1" book="Complete Arcane page 30" name="Effigy Master {Prestige Class}" hd="d?" desc="The effigy master is an expert in the imitation of true life." />
+    <class level="1" book="Eberron Campaign Setting page 74" name="Eldeen Ranger {Prestige Class}" hd="d?" desc="An Eldeen ranger learns special techniques and abilities that help him to fulfill the goal of his sect." />
+    <class level="1" book="Complete Mage page 53" name="Eldritch Disciple {Prestige Class}" hd="d?" desc="A multiclass warlock and divine spellcaster." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 187" name="Eldritch Knight {Prestige Class}" hd="d?" desc="Studying the martial and arcane arts to equal degree, the eldritch knight is a versatile combatant." />
+    <class level="1" book="Complete Mage page 57" name="Eldritch Theurge {Prestige Class}" hd="d?" desc="A multiclass warlock and arcane spellcaster." />
+    <class level="1" book="Faiths &amp; Pantheons page 190" name="Elemental Archon {Prestige Class}" hd="d?" desc="Elemental archons are servants of powerful, seemingly uncaring elemental forces who want to once and for all tip the balance in favor of their chosen element." />
+    <class level="1" book="Draconomicon page 92" name="Elemental Master {Prestige Class}" hd="d?" desc="Elemental masters strive to attain the purity of perfect attunement with both the energy of their breath weapons and the elemental nature of their core." />
+    <class level="1" book="Complete Arcane page 32" name="Elemental Savant {Prestige Class}" hd="d?" desc="Elemental savants study the basic building blocks of existence -- air, earth, fire, and water -- learning to harness their powers." />
+    <class level="1" book="Magic of Eberron page 68" name="Elemental Scion Of Zilargo {Prestige Class}" hd="d?" desc="The elemental scion of Zilargo attempts to understand the true nature of the elements through bizarre methods." />
+    <class level="1" book="Planar Handbook page 65" name="Elemental Warrior {Prestige Class}" hd="d?" desc="The elemental warrior sees that great strength comes from focusing on the most basic aspects of reality." />
+    <class level="1" book="Expanded Psionics Handbook page 142" name="Elocater {Prestige Class}" hd="d?" desc="Elocaters are renowned for their agile combat stratagems, using their knowledge of motion and space to set themselves up for quick attacks against slower opponents." />
+    <class level="1" book="Races of Faerun page 182" name="Elven High Mage {Prestige Class}" hd="d?" desc="Elven high mages are the masters of creating their own epic spells -- mythals that can grow to engulf entire cities." />
+    <class level="1" book="Savage Species page 75" name="Emancipated Spawn  {Prestige Class}" hd="d?" desc="Those spawn of undead who find themselves free of their masters can begin to recall their former lives and a measure of redemption." />
+    <class level="1" book="Book of Exalted Deeds page 59" name="Emissary Of Barachiel {Prestige Class}" hd="d?" desc="The emissaries of Barachiel are peacemakers, diplomats, and evangelists, as well as staunch opponents of evil and corruption." />
+    <class level="1" book="Complete Arcane page 34" name="Enlightened Fist {Prestige Class}" hd="d?" desc="Enlightened fists master the use of touch spells, creating new forms of combat with their fists." />
+    <class level="1" book="Complete Mage page 60" name="Enlightened Spirit {Prestige Class}" hd="d?" desc="A warlock who takes on celestial characteristics." />
+    <class level="1" book="Complete Divine page 36" name="Entropomancer {Prestige Class}" hd="d?" desc="Entropomancers gain attunement to the great nothingness they say lies at the center of the universe." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 53" name="Ephemeral Exemplar {Prestige Class}" hd="d?" desc="Ephemeral exemplars are paragons of incorporealness." />
+    <class level="1" book="Epic Level Handbook page 17" name="Epic Arcane Archer {Prestige Class}" hd="d?" desc="The epic arcane archer is a living extension of the bow, capable of achieving wonders of archery that cause lesser beings to gape in awe." />
+    <class level="1" book="Epic Level Handbook page 18" name="Epic Assassin {Prestige Class}" hd="d?" desc="The epic assassin flits from shadow to shadow, lying in wait until his target is vulnerable, then striking like a cobra." />
+    <class level="1" book="Epic Level Handbook page 19" name="Epic Blackguard {Prestige Class}" hd="d?" desc="The epic blackguard is a twisted reflection of the epic paladin, radiating evil power from every pore of his body." />
+    <class level="1" book="Epic Level Handbook page 20" name="Epic Dwarven Defender {Prestige Class}" hd="d?" desc="The epic dwarven defender becomes the very definition of immovable object." />
+    <class level="1" book="Epic Level Handbook page 28" name="Epic Infiltrator {Prestige Class}" hd="d?" desc="The epic infiltrator is an agent of espionage, an undercover operative, and sometimes a saboteur." />
+    <class level="1" book="Epic Level Handbook page 20" name="Epic Loremaster {Prestige Class}" hd="d?" desc="If the epic loremaster doesn't know something, it probably isn't worth knowing." />
+    <class level="1" book="Epic Level Handbook page 22" name="Epic Psion {Prestige Class}" hd="d?" desc="The epic psion has evolved his inborn mental abilities, achieving mental mastery of lesser mentalities." />
+    <class level="1" book="Epic Level Handbook page 23" name="Epic Psychic Warrior {Prestige Class}" hd="d?" desc="The epic psychic warrior is a meld of mental and martial prowess." />
+    <class level="1" book="Epic Level Handbook page 21" name="Epic Shadowdancer {Prestige Class}" hd="d?" desc="While the epic assassin udes the shadows, the epic shadowdancer becomes the shadows, indistinguishable from the darkness cloaking her." />
+    <class level="1" book="Faiths of Eberron page 52" name="Escalation Mage {Prestige Class}" hd="d?" desc="By giving themselves over to the Shadow and focusing on the dark side of magic, these arcane spellcasters learn how to bargain with their god to make their spells more effective -- for a price." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 109" name="Eternal Blade {Prestige Class}" hd="d?" desc="An eternal blade is an elf who learns a variety of exotic fighting maneuvers with the aid and advice of the spirit of a mighty, ancient elf warrior." />
+    <class level="1" book="Oriental Adventures page 38" name="Eunuch Warlock {Prestige Class}" hd="d?" desc="Eunuch warlocks must be arcane spellcasters of significant ability, and they are often sorcerers rather than wu jen." />
+    <class level="1" book="Complete Divine page 39" name="Evangelist {Prestige Class}" hd="d?" desc="Evangelists travel the world proclaiming their devotion to a particular deity, pantheon, or religious doctrine." />
+    <class level="1" book="Player's Guide to Faerun page 53" name="Evereskan Tomb Guardian {Prestige Class}" hd="d?" desc="The Evereskan tomb guardians are on hand to ensure that the defenses of the tombs are very good indeed." />
+    <class level="1" book="Book of Exalted Deeds page 61" name="Exalted Arcanist {Prestige Class}" hd="d?" desc="Exalted arcanists gain access to spells that channel celestial energy." />
+    <class level="1" book="Complete Adventurer page 44" name="Exemplar {Prestige Class}" hd="d?" desc="An exemplar focuses her energy on improving the skills she possesses until she can perform them with fluidity, grace, and art." />
+    <class level="1" book="Eberron Campaign Setting page 77" name="Exorcist Of The Silver Flame {Prestige Class}" hd="d?" desc="Exorcists of the Silver Flame lead the Church of the Silver Flame's efforts in combating extraplanar threats." />
+    <class level="1" book="Complete Warrior page 30" name="Exotic Weapon Master {Prestige Class}" hd="d?" desc="The only real requirement for exotic weapon master is commitment and perseverance." />
+    <class level="1" book="Eberron Campaign Setting page 79" name="Extreme Explorer {Prestige Class}" hd="d?" desc="The extreme explorer is the iconic action hero of Eberron." />
+    <class level="1" book="Complete Warrior page 31" name="Eye Of Gruumsh {Prestige Class}" hd="d?" desc="An orc or half-orc who heeds the call to serve Gruumsh in his image can become an eye of Gruumsh." />
+    <class level="1" book="Unearthed Arcana page 166" name="Faith Scion {Prestige Class}" hd="d?" desc="This prestige class is meant for characters who wield legendary weapons of divine power for claerics, druids, and paladins." />
+    <class level="1" book="Song and Silence: A Guidebook to Bards and Rogues page 8" name="Fang Of Lolth {Prestige Class}" hd="d?" desc="Fangs of Lolth undergo a transformation that both provides them power and changes them in ways they may not necessarily want." />
+    <class level="1" book="Serpent Kingdoms page 162" name="Fang Of Sseth {Prestige Class}" hd="d?" desc="The Fangs of Sseth constitute the strike force of the Vipers -- the stealthy killers who leave no trace." />
+    <class level="1" book="Planar Handbook page 50" name="Fatemaker {Prestige Class}" hd="d?" desc="'There are two paths to take; one is easy, and that is its only reward.'" />
+    <class level="1" book="Complete Arcane page 37" name="Fatespinner {Prestige Class}" hd="d?" desc="A fatespinner has pulled back the curtain of chance, circumstance, and chaos to glimpse a deeper truth: probability." />
+    <class level="1" book="Fiend Folio page 200" name="Fiend Of Blasphemy  {Prestige Class}" hd="d?" desc="The fiend of blasphemy is a master of the infernal art of perverting the desire to worship and turning it toward the corrupt veneration of fiendish masters." />
+    <class level="1" book="Fiend Folio page 202" name="Fiend Of Corruption {Prestige Class}" hd="d?" desc="Fiends of corruption are preoccupied with corrupting mortals to ensure that their souls end up on the Lower Planes after death." />
+    <class level="1" book="Fiend Folio page 204" name="Fiend Of Possession {Prestige Class}" hd="d?" desc="A fiend of possession is an invasive presence that taints the very soul, corrupting from within." />
+    <class level="1" book="Tome of Magic page 220" name="Fiendbinder {Prestige Class}" hd="d?" desc="A fiendbinder seeks to unlock the truenames of demons, devils, and other vile fiends, and use that knowledge to bind them to service." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 18" name="Fist Of Hextor {Prestige Class}" hd="d?" desc="Fists of Hextor are templars sworn to the service of their unforgiving master." />
+    <class level="1" book="Book of Exalted Deeds page 62" name="Fist Of Raziel {Prestige Class}" hd="d?" desc="The fists of Raziel represent a knightly order dedicated to the celestial patron of holy warfare against evil." />
+    <class level="1" book="Expanded Psionics Handbook page 144" name="Fist Of Zuoken {Prestige Class}" hd="d?" desc="The Fists of Zuoken ore members of an order of martial artists devoted to mastering their own physical and mental development while protecting psions and other psionic creatures." />
+    <class level="1" book="Complete Psionic page 30" name="Flayerspawn Psychic {Prestige Class}" hd="d?" desc="Willing to sacrifice her life, appearance, and even her sanity, the flayerspawn psychic walks a dangerous road, growing in psionic power as she slowly embraces her secret mind flayer heritage." />
+    <class level="1" book="Lords of Madness page 189" name="Fleshwarper {Prestige Class}" hd="d?" desc="The fleshwarper finds no greater canvas than flesh itself." />
+    <class level="1" book="Complete Adventurer page 47" name="Fochlucan Lyrist {Prestige Class}" hd="d?" desc="The Fochlucan lyrist is a legendary figure who serves as the herald and teacher to great kings, the champion of the common folk, and the keeper of lore long forgotten elsewhere." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 56" name="Foe Hunter {Prestige Class}" hd="d?" desc="The foe hunter has but one purpose in life: to kill creatures of the type she hates." />
+    <class level="1" book="Faiths &amp; Pantheons page 193" name="Forest Master {Prestige Class}" hd="d?" desc="Guardians of the pristine wilderness and defenders of the ancient trees, forest masters are the living embodiments of sentient nature." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 57" name="Forsaker {Prestige Class}" hd="d?" desc="The forsaker rebels against the magic of the fantastic world around him." />
+    <class level="1" book="Complete Warrior page 34" name="Frenzied Berserker {Prestige Class}" hd="d?" desc="The frenzied berserker is constantly seeking out more conflict to feed her craving for battle." />
+    <class level="1" book="Frostburn page 58" name="Frost Mage {Prestige Class}" hd="d?" desc="Frost mages usually hail from lands of cold, snow, and ice: tundra, glaciers even outer planes perpetually shrouded in winter." />
+    <class level="1" book="Frostburn page 60" name="Frostrager {Prestige Class}" hd="d?" desc="The frostragers are powerful and dangerous warriors believed by some to be gifted from (and others cursed by) the frost giant deity Thrym with an unstable but powerful supernatural battle rage." />
+    <class level="1" book="Manual of the Planes page 26" name="Gatecrasher {Prestige Class}" hd="d?" desc="Gatecrashers see themselves as cosmic free agents, independent forces who can influence the natives of the planes and even the dynamic forces of magic itself." />
+    <class level="1" book="Player's Guide to Eberron page 88" name="Gatekeeper Mystagogue {Prestige Class}" hd="d?" desc="Heirs to a tradition over sixteen millennia old, the gatekeeper mystagogues stand among the greated foes of the daelkyr and their aberration spawn." />
+    <class level="1" book="Complete Divine page 41" name="Geomancer {Prestige Class}" hd="d?" desc="To the geomancer, all magic is the same." />
+    <class level="1" book="Complete Arcane page 39" name="Geometer {Prestige Class}" hd="d?" desc="The geometer is the master of written magic and spells inscribed within a perfectly rendered diagram." />
+    <class level="1" book="Ghostwalk page 26" name="Ghost Slayer {Prestige Class}" hd="d?" desc="The ghost slayer studies ghost so that they can be dispatched easily and no longer bother the living with their presence." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 20" name="Ghostwalker {Prestige Class}" hd="d?" desc="Ghostwalkers have abilities that point to some underlying, mysterious mysticism." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 21" name="Gladiator {Prestige Class}" hd="d?" desc="Rich or poor, all gladiators face death whenever they step into the arena." />
+    <class level="1" book="Lost Empires of Faerun page 13" name="Glorious Servitor {Prestige Class}" hd="d?" desc="Glorious servitors are exceptionally loyal and devout servants of the Mulhorandi gods." />
+    <class level="1" book="Magic of Faerun page 23" name="Gnome Artificer  {Prestige Class}" hd="d?" desc="Gnome artificers dabble in technology to create fantastic devices, delving into shadow magic when their mundane equipment is insufficient for the task." />
+    <class level="1" book="Faiths &amp; Pantheons page 194" name="Goldeye {Prestige Class}" hd="d?" desc="Goldeyes are agents and promulgators of commercial intercourse, seeking to increase the wealth of their communities and realms by promoting the exchange of coins in trade." />
+    <class level="1" book="Races of Stone page 112" name="Goliath Liberator {Prestige Class}" hd="d?" desc="Goliath liberators are experts at infiltrating giant dwellings, freeing the captives within, then exacting revenge on the giants while the freed goliaths escape." />
+    <class level="1" book="City of Splendors: Waterdeep page 77" name="Gray Hand Enforcer {Prestige Class}" hd="d?" desc="Gray Hand enforcers are highly trained members of the Gray Hands, Waterdeep's elite, high-powered fighting force." />
+    <class level="1" book="Shining South page 24" name="Great Rift Deep Defender {Prestige Class}" hd="d?" desc="The Great Rift deep defender has a keen understanding of the importance of making a stand." />
+    <class level="1" book="Races of Faerun page 183" name="Great Rift Skyguard {Prestige Class}" hd="d?" desc="The hippogriff-mounted skyguards of the Great Rift patrol the skies, ever watchful for the enemies of the gold dwarves." />
+    <class level="1" book="Shining South page 26" name="Great Sea Corsair {Prestige Class}" hd="d?" desc="Adapt the dread pirate prestige class from Song and Silenceto create the Great Sea corsair prestige class." />
+    <class level="1" book="Complete Arcane page 41" name="Green Star Adept {Prestige Class}" hd="d?" desc="A Green Star adept is the master of the strange and powerful magic derived from Alhazarde's glittering green starmetal." />
+    <class level="1" book="Epic Level Handbook page 30" name="Guardian Paramount {Prestige Class}" hd="d?" desc="The guardian paramount is an extraordinary bodyguard, a protector of others who is skilled in preventing harm to his charge." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 45" name="Guild Thief {Prestige Class}" hd="d?" desc="Guild thieves are thieves who operate in urban areas as part of an organized thieves' guild." />
+    <class level="1" book="Magic of Faerun page 26" name="Guild Wizard Of Waterdeep {Prestige Class}" hd="d?" desc="The wizards of the order study and exchange information, create magic items to help support the guild's financial independence, and offer their services to others in the city as watch-wizards or fire guards." />
+    <class level="1" book="Complete Warrior page 38" name="Halfling Outrider {Prestige Class}" hd="d?" desc="Halfling outriders are elite champions whose task is to warn their fellows of, and protect them from, danger." />
+    <class level="1" book="Shining South page 27" name="Halruaan Elder {Prestige Class}" hd="d?" desc="Halruaan elders are the epitomy of magic cast with panache, and their dazzling and unique displays of arcane force make them the most respected practitioners in the land." />
+    <class level="1" book="Shining South page 29" name="Halruaan Magehound {Prestige Class}" hd="d?" desc="Magehounds are Halruaa's inquisitors." />
+    <class level="1" book="Player's Guide to Faerun page 56" name="Hammer Of Moradin {Prestige Class}" hd="d?" desc="An elite order of warrior-priests stands ready to defend the dwarven people against the onslaught of fell giants, dark elves, and goblinoids." />
+    <class level="1" book="Shining South page 31" name="Hand Of The Adama {Prestige Class}" hd="d?" desc="The hand of the Adama is a benign leader, judge and jury, and protector of the common folk all rolled into one." />
+    <class level="1" book="Dragon Magic page 43" name="Hand Of The Winged Masters {Prestige Class}" hd="d?" desc="Dragons often need expert servants to be their eyes, ears, and hands in humanoid society." />
+    <class level="1" book="Player's Guide to Faerun page 58" name="Harper Agent {Prestige Class}" hd="d?" desc="Harper agents are the 'field agents' of the Harper organization, acting directly to gather intelligence and eliminate threats to the greater good." />
+    <class level="1" book="Magic of Faerun page 28" name="Harper Mage {Prestige Class}" hd="d?" desc="The Harper mage has two principal responsibilities: They aid the Harpers with spells and arcane knowledge, and they study, record, and pass on ancient lore." />
+    <class level="1" book="Player's Guide to Faerun page 181" name="Harper Paragon {Prestige Class}" hd="d?" desc="A Harper paragon actively promotes the welfare of other creatures while preventing evil forces from preying on innocents." />
+    <class level="1" book="Magic of Faerun page 29" name="Harper Priest {Prestige Class}" hd="d?" desc="Some Harpers choose to pursue a closer relationship to the deities who inspired the creation of the Harpers." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 46" name="Harper Scout {Prestige Class}" hd="d?" desc="Harper scouts are members of the Harpers, a secret society dedicated to holding back evil, preserving knowledge, and maintaining the balance between civilization and the wild." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 47" name="Hathran {Prestige Class}" hd="d?" desc="Hathrans comprise an elite sisterhood of spellcasters who lead Rashemen." />
+    <class level="1" book="Miniatures Handbook page 20" name="Havoc Mage {Prestige Class}" hd="d?" desc="The havoc mage shares as much in common with a fighter as with a wizard." />
+    <class level="1" book="Faiths &amp; Pantheons page 196" name="Heartwarder {Prestige Class}" hd="d?" desc="Heartwarders are aesthetes and hedonists who actively seek out pleasure and beauty in all things and who nurture the creation of beautiful objects." />
+    <class level="1" book="Eberron Campaign Setting page 80" name="Heir Of Siberys {Prestige Class}" hd="d?" desc="The magic of a Siberys mark is undeniably powerful, and an heir of Siberys manifests one." />
+    <class level="1" book="Oriental Adventures page 39" name="Henshin Mystic {Prestige Class}" hd="d?" desc="Henshin mystics are members of a monastic order that teaches what they consider a great mystery of the universe: that humanity is capable of a transformation (henshin) into divinity." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 63" name="Hexer {Prestige Class}" hd="d?" desc="The hexer uses the power of his gaze." />
+    <class level="1" book="Oriental Adventures page 212" name="Hida Defender {Prestige Class}" hd="d?" desc="Hida defenders train in great armor, a unique characteristic that fits in well with the Crab philosophy of strength and endurance." />
+    <class level="1" book="Draconomicon page 94" name="Hidecarved Dragon {Prestige Class}" hd="d?" desc="Hidecarved dragons are members of an enigmatic order of dragons and half-dragons." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 188" name="Hierophant {Prestige Class}" hd="d?" desc="A divine spellcaster who rises high in the service of his deity gains access to spells and abilities of which lesser faithful can only dream." />
+    <class level="1" book="Player's Guide to Eberron page 104" name="High Elemental Binder {Prestige Class}" hd="d?" desc="As a high elemental binder, you can reach into the planes and immediately draw elemental beings into objects -- coating your armor in stone or your blade in fire." />
+    <class level="1" book="Epic Level Handbook page 31" name="High Proselytizer {Prestige Class}" hd="d?" desc="The high proselytizer is the holy inspiration that begins religious movements." />
+    <class level="1" book="Complete Adventurer page 54" name="Highland Stalker {Prestige Class}" hd="d?" desc="Highland stalkers are consummate trackers with an instinctive knowledge of their mountainous territories." />
+    <class level="1" book="Shining South page 32" name="Hin Fist {Prestige Class}" hd="d?" desc="Adapt the sacred fist prestige class from Complete Divineto create a hin fist." />
+    <class level="1" book="Draconomicon page 130" name="Hoardstealer {Prestige Class}" hd="d?" desc="The hoardstealer specializes in relieving wealthy individuals from large amounts of said wealth." />
+    <class level="1" book="Complete Divine page 45" name="Holy Liberator {Prestige Class}" hd="d?" desc="The holy liberator is a holy warrior, a distant cousin of the paladin." />
+    <class level="1" book="Complete Mage page 64" name="Holy Scourge {Prestige Class}" hd="d?" desc="An arcane spellcaster that specializes in blasting evil." />
+    <class level="1" book="Silver Marches page 110" name="Hordebreaker {Prestige Class}" hd="d?" desc="The hordebreaker is a person who makes destroying the horde threat the perfect engine of orc destruction." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 189" name="Horizon Walker {Prestige Class}" hd="d?" desc="The horizon walker is an unceasing traveler to the universe's most dangerous places." />
+    <class level="1" book="Faiths &amp; Pantheons page 197" name="Horned Harbinger {Prestige Class}" hd="d?" desc="The horned harbingers are agents of the fallen Lord of Bones." />
+    <class level="1" book="Complete Divine page 48" name="Hospitaler {Prestige Class}" hd="d?" desc="Hospitalers are a fighting force of necessity, sworn to poverty, obedience, and the defense of those in their care." />
+    <class level="1" book="Complete Warrior page 40" name="Hulking Hurler {Prestige Class}" hd="d?" desc="Hulking hurlers belong to those races of generously proportioned creatures who enjoy nothing more than wrenching boulders, trees, and even buildings free of their earthly bonds and throwing them at their foes." />
+    <class level="1" book="Complete Warrior page 42" name="Hunter Of The Dead {Prestige Class}" hd="d?" desc="The hunter of the dead spends each restless night tracking undead to their lairs and cleansing the land of their foul presence." />
+    <class level="1" book="Oriental Adventures page 41" name="Iaijutsu Master {Prestige Class}" hd="d?" desc="Iaijutsu masters harness their ki energy to strike with blinding speed and devastating power." />
+    <class level="1" book="Draconomicon page 131" name="Iinitiate Of The Draconic Mysteries {Prestige Class}" hd="d?" desc="Some become students of draconic knowledge that leads to greater power." />
+    <class level="1" book="Underdark page 35" name="Illithid Body Tamer {Prestige Class}" hd="d?" desc="Illithids who embrace the Tamer Creed believe that military might is the most important factor in their race's future mastery of the multiverse." />
+    <class level="1" book="Savage Species page 77" name="Illithid Savant {Prestige Class}" hd="d?" desc="The illithid savant is an academic who deals in applied science, acquiring new knowledge from the brains he consumes." />
+    <class level="1" book="Expanded Psionics Handbook page 146" name="Illithid Slayer {Prestige Class}" hd="d?" desc="Illithid slayers have dedicated their lives to the eradication of the mind flayer 'infection.'" />
+    <class level="1" book="Complete Psionic page 33" name="Illumine Soul {Prestige Class}" hd="d?" desc="The illumine soul is a living conduit of positive energy." />
+    <class level="1" book="Underdark page 37" name="Imaskari Vengeance Taker {Prestige Class}" hd="d?" desc="A secret society dedicated to righting wrongs, the Imaskari vengeance takers are trained by hidden masters in the rites and rituals of revenge." />
+    <class level="1" book="Magic of Eberron page 73" name="Impure Prince {Prestige Class}" hd="d?" desc="Of those who make it their special mission to rid the world of aberrations and their masters, only impure princes can claim the distinction of using daelkyr-inspired corruptions and symbionts as their most effective tool in aberration cleansing." />
+    <class level="1" book="Magic of Incarnum page 115" name="Incandescent Champion {Prestige Class}" hd="d?" desc="The incandescent champion seeks to dispense with barriers and obstacles both tangible and intangible so that she can touch the cosmic soul with her unveiled body, mind, and spirit." />
+    <class level="1" book="Magic of Faerun page 31" name="Incantatrix {Prestige Class}" hd="d?" desc="The incantatrixes are the practitioners of metamagic in Faerun, studying spells that affect other spells and having a fondness for magic that thwarts extraplanar beings." />
+    <class level="1" book="Magic of Incarnum page 121" name="Incarnum Blade {Prestige Class}" hd="d?" desc="Using a secret passed down through the generations, the incarnum blade shapes soul energy drawn from the greatest warriors of the past into a special soulmeld that is incorporated into his melee weapon of choice." />
+    <class level="1" book="Book of Exalted Deeds page 64" name="Initiate Of Pistis Sophia {Prestige Class}" hd="d?" desc="The path of these initiates requires great sacrifices (in the form of at least three sacred vows), but brings great rewards of spiritual power." />
+    <class level="1" book="Complete Arcane page 44" name="Initiate Of The Sevenfold Veil {Prestige Class}" hd="d?" desc="A master of defensive magic, the Initiate of the Sevenfold Veil approaches the prismatic barrier by mastering one by one its constituent veils or layers." />
+    <class level="1" book="Underdark page 39" name="Inquisitor Of The Drowning Goddess {Prestige Class}" hd="d?" desc="Some kuo-toa monks go on to become inquisitors of the Drowning Goddess, who are tasked with protecting the community from inside threats." />
+    <class level="1" book="Dragonlance Campaign Setting page 80" name="Inquisitor {Prestige Class}" hd="d?" desc="By definition, an inquisitor is one who inquires, someone who hunts for people, information, or answers." />
+    <class level="1" book="Complete Warrior page 44" name="Invisible Blade {Prestige Class}" hd="d?" desc="Invisible blades are deadly fighters who prefer to use daggers and related weapons in combat." />
+    <class level="1" book="Races of Stone page 114" name="Iron Mind {Prestige Class}" hd="d?" desc="Elite warriors trained to resist mental compulsions of all kinds, members of the iron mind prestige class defend dwarf and gnome kingdoms against intrusions by mind flayers, dark elf enchanters, and the like." />
+    <class level="1" book="Magic of Incarnum page 126" name="Ironsoul Forgemaster {Prestige Class}" hd="d?" desc="Only the ironsoul forgemaster can craft a weapon that combines these arts with the shaping of soul essence." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 113" name="Jade Phoenix Mage {Prestige Class}" hd="d?" desc="Long ago, a fellowship of swordsages known as the Masters of the Jade Phoenix took up the study of arcane magic in search of a new martial discipline." />
+    <class level="1" book="Shining South page 33" name="Jordain Vizier {Prestige Class}" hd="d?" desc="The Jordaini are a special servitor caste, though still upper class, in the magocracy of Halruaa." />
+    <class level="1" book="Champions of Ruin page 48" name="Justice Of Weald And Woe {Prestige Class}" hd="d?" desc="The justice of weald and woe is the go-to person when something unsavory -- usually involving the removal of humans -- needs doing." />
+    <class level="1" book="Deities and Demigods page 205" name="Justiciar Of Taiia {Prestige Class}" hd="d?" desc="Justiciars of Taiia fulfill the role of carrying out Taiia's sentence against wrongdoers." />
+    <class level="1" book="Player's Guide to Faerun page 63" name="Justiciar Of Tyr {Prestige Class}" hd="d?" desc="Justiciars are the very elite of Tyr's mortal servants, and they act as living embodiments of their god's portfolio." />
+    <class level="1" book="Complete Warrior page 47" name="Justiciar {Prestige Class}" hd="d?" desc="Justiciars make a living kicking the daylights out of criminals who desperately deserve it." />
+    <class level="1" book="Lords of Madness page 194" name="Keeper Of The Cerulean Sign {Prestige Class}" hd="d?" desc="The Cerulean Sign is an ancient rune of power, created untold eons ago by a race or deity long since vanished; now it has keepers." />
+    <class level="1" book="Complete Warrior page 49" name="Kensai {Prestige Class}" hd="d?" desc="The kensai masters body, mind, weapon, and will." />
+    <class level="1" book="Oriental Adventures page 42" name="Kishi Charger {Prestige Class}" hd="d?" desc="Kishi chargers are cavalry soldiers trained to make the greatest possible use of a horse's speed and a rider's agility." />
+    <class level="1" book="City of Splendors: Waterdeep page 81" name="Knight Of The Blue Moon {Prestige Class}" hd="d?" desc="Knights of the Blue Moon are elite soldiers in the endless battle against the Mistress of the Night." />
+    <class level="1" book="Complete Warrior page 53" name="Knight Of The Chalice {Prestige Class}" hd="d?" desc="The knight of the Chalice is a member of an elite knightly organization devoted to fighting demons and other evil outsiders." />
+    <class level="1" book="Dragonlance Campaign Setting page 56" name="Knight Of The Crown {Prestige Class}" hd="d?" desc="The Order of the Crown is the first tier of the Solamnic Knights." />
+    <class level="1" book="CoV page 106" name="Knight Of The Flying Hunt {Prestige Class}" hd="d?" desc="Defenders of Nimbral, protectors of the island realm's quiet, simple folk, noble soldiers answering to the powerful but mysterious Nimbral Lords -- the Knights of the Flying Hunt epitomize valor and grace in word, deed, and bearing." />
+    <class level="1" book="Frostburn page 62" name="Knight Of The Iron Glacier {Prestige Class}" hd="d?" desc="The Knights of the Iron Glacier continue to honor the memory of General Aengrist in their deeds and actions." />
+    <class level="1" book="Dragonlance Campaign Setting page 63" name="Knight Of The Lily {Prestige Class}" hd="d?" desc="The Knights of the Lily are the order of warriors within the Knights of Neraka." />
+    <class level="1" book="Defenders of the Faith: A Guidebook to Clerics and Paladins page 65" name="Knight Of The Middle Circle {Prestige Class}" hd="d?" desc="Knights of the Middle Circle provide security for Stargazer chapterhouses and may be called upon for similar service for allies of the Stargazers." />
+    <class level="1" book="Stormwrack page 52" name="Knight Of The Pearl {Prestige Class}" hd="d?" desc="The knight of the pearl is a loyal defender of the aventi people, dedicated to the service of Aventernus and his appointed kings." />
+    <class level="1" book="Expedition to Castle Ravenloft page 200" name="Knight Of The Raven {Prestige Class}" hd="d?" desc="Before evil descended on the land of Barovia, it was home to an order of virtuous champions, the Knights of the Raven." />
+    <class level="1" book="Dragonlance Campaign Setting page 59" name="Knight Of The Rose {Prestige Class}" hd="d?" desc="The Knights of the Rose are the highest tier of the Solamnic Knights." />
+    <class level="1" book="Tome of Magic page 54" name="Knight Of The Sacred Seal {Prestige Class}" hd="d?" desc="A knight of the sacred seal is never alone because she has formed a true partnership with a single vestige." />
+    <class level="1" book="Dragonlance Campaign Setting page 65" name="Knight Of The Skull {Prestige Class}" hd="d?" desc="Entering battle with strength and divine magic, Knights of the Skull are the spirit of the Dark Knights." />
+    <class level="1" book="Dragonlance Campaign Setting page 58" name="Knight Of The Sword {Prestige Class}" hd="d?" desc="Knights of the Sword are warriors of the Solamnic Knights who fight with power and faith to defend justice and truth." />
+    <class level="1" book="Dragonlance Campaign Setting page 66" name="Knight Of The Thorn {Prestige Class}" hd="d?" desc="The Knights of the Thorn are also known as the 'gray robes' for the ash-colored robes they wear to indicate that they do not serve the Orders of High Sorcery." />
+    <class level="1" book="CoV page 111" name="Knight Of The Weave {Prestige Class}" hd="d?" desc="Members of this mystic order of sacred defenders cherish the Weave like a fine wine." />
+    <class level="1" book="FN page 41" name="Knight Phantom {Prestige Class}" hd="d?" desc="The knight phantom prestige class takes capable wizards and gradually turns them into capable melee fighters, without slowing their spellcasting too much." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 24" name="Knight Protector Of The Great Kingdom {Prestige Class}" hd="d?" desc="Knight protectors are martial characters dedicated to restoring the ideals of knightly chivalry before they fade forever." />
+    <class level="1" book="Complete Warrior page 55" name="Knight Protector {Prestige Class}" hd="d?" desc="Knight protectors are martial characters dedicated to restoring the ideals of knightly chivalry before they fade forever." />
+    <class level="1" book="Secrets of Xen'drik page 123" name="Landforged Walker {Prestige Class}" hd="d?" desc="Even as they speak for nature, landforged walkers coax the living bounty of the earth to grow on their metal hides, drawing power from the environment around them." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 25" name="Lasher {Prestige Class}" hd="d?" desc="The lasher prestige class uses the whip as an extension of herself." />
+    <class level="1" book="WL page 19" name="Legacy Champion {Prestige Class}" hd="d?" desc="You are so devoted to the history and chronicle of a particular item of legacy that you enjoy enhanced access to your item's legacy abilities." />
+    <class level="1" book="Stormwrack page 56" name="Legendary Captain {Prestige Class}" hd="d?" desc="A legendary captain might be the commander of a fleet's flagship or a bloodthirsty pirate, but whatever the role, her reputation is widespread and her crew fanatically loyal." />
+    <class level="1" book="Epic Level Handbook page 33" name="Legendary Dreadnought {Prestige Class}" hd="d?" desc="The legendary dreadnought is the ultimate foot soldier, an absolute force of destruction, a total warrior who excels at sheer combat prowess." />
+    <class level="1" book="Heroes of Battle page 107" name="Legendary Leader {Prestige Class}" hd="d?" desc="Legendary leaders are the stuff of bards' tales come to life." />
+    <class level="1" book="Dragonlance Campaign Setting page 81" name="Legendary Tactician {Prestige Class}" hd="d?" desc="Legendary tacticians are respected (or feared) for their ability to inspire their troops." />
+    <class level="1" book="Stormwrack page 61" name="Leviathan Hunter {Prestige Class}" hd="d?" desc="The leviathan hunter is dedicated to hunting down creatures of the perilous depths." />
+    <class level="1" book="Book of Vile Darkness page 63" name="Lifedrinker {Prestige Class}" hd="d?" desc="Lifedrinkers are vampires who have been undead for a very long time." />
+    <class level="1" book="Expedition to Castle Ravenloft page 204" name="Lightbringer {Prestige Class}" hd="d?" desc="The Lightbringers are an expansive guild of undead hunters that readily hands out charter memberships to anyone who wants to stamp out undead." />
+    <class level="1" book="Book of Exalted Deeds page 65" name="Lion Of Talisid {Prestige Class}" hd="d?" desc="The lions of Talisid protect nature and emulate their patron in more concrete ways." />
+    <class level="1" book="Sandstorm page 70" name="Lord Of Tides {Prestige Class}" hd="d?" desc="A lord of tides can sense the movement of magma, summon beings of elemental might, and open portals to the Elemental Planes." />
+    <class level="1" book="Races of Destiny page 117" name="Loredelver {Prestige Class}" hd="d?" desc="Loredelvers are illumian spellcasters who find and explore ruins, disable the magical protections that guard them, and sift through the ancient secrets found within." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 191" name="Loremaster {Prestige Class}" hd="d?" desc="Loremasters are spellcasters who concentrate on knowledge, valuing lore and secrets over gold." />
+    <class level="1" book="Races of the Wild page 118" name="Luckstealer {Prestige Class}" hd="d?" desc="As a luckstealer, you're part spellcaster, part professional gambler -- and 100% mischief-maker." />
+    <class level="1" book="Shining South page 35" name="Luiren Marchwarden {Prestige Class}" hd="d?" desc="The Luiren marchwarden is the defender of the frontier in the land of the halflings." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 54" name="Lurking Terror {Prestige Class}" hd="d?" desc="Lurking terrors are the quintessential hunting undead, displaying great prowess with their special abilities and amazing powers of stealth." />
+    <class level="1" book="Complete Mage page 67" name="Lyric Thaumaturge {Prestige Class}" hd="d?" desc="A bard with enhanced spellcasting prowess." />
+    <class level="1" book="Complete Adventurer page 56" name="Maester {Prestige Class}" hd="d?" desc="Maesters are the master crafters of the gnome world." />
+    <class level="1" book="Complete Arcane page 48" name="Mage Of The Arcane Order {Prestige Class}" hd="d?" desc="Also called a 'guildmage,' a member of this prestige class is a spellcaster who belongs to an academy and guild known as the Arcane Order." />
+    <class level="1" book="Lost Empires of Faerun page 17" name="Magelord {Prestige Class}" hd="d?" desc="Quick to anger, haughty, and proud of his Art, the magelord is an arcane spellcaster who studies an ancient magical tradition known for extremely fast and versatile spellcasting" />
+    <class level="1" book="Player's Guide to Faerun page 182" name="Maiden Of Pain {Prestige Class}" hd="d?" desc="Loviatar's most dedicated servants, the maidens of pain, are depraved women who literally make pain their meat and drink." />
+    <class level="1" book="Oriental Adventures page 231" name="Mantis Mercenary {Prestige Class}" hd="d?" desc="Mantis mercenaries make use of peasant weapons and a rolling motion." />
+    <class level="1" book="Shining South page 38" name="Maquar Crusader {Prestige Class}" hd="d?" desc="A Maquar crusader follows a strict code of conduct that not only limits what he can own or where he can live, but also limits the ways in which merchants can influence him." />
+    <class level="1" book="Player's Guide to Faerun page 184" name="Martyred Champion Of Ilmater {Prestige Class}" hd="d?" desc="Having already offered his life in sacrifice once, the martyred champion of Ilmater perseveres in Ilmater's faith." />
+    <class level="1" book="Magic of Faerun page 34" name="Master Alchemist {Prestige Class}" hd="d?" desc="The master alchemist is a spellcaster who specializes in producing potions and elixirs that reproduce the effects of spells of 4th level or higher." />
+    <class level="1" book="Eberron Campaign Setting page 82" name="Master Inquisitive {Prestige Class}" hd="d?" desc="The master inquisitive takes the art of investigation and deduction to the ultimate level, rising to the top of the field." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 27" name="Master Of Chains {Prestige Class}" hd="d?" desc="The master of chains is a combatant specializing in the use of chains -- specifically the spiked chain -- as a weapon." />
+    <class level="1" book="Savage Species page 80" name="Master Of Flies {Prestige Class}" hd="d?" desc="The master of flies is an intelligent swarm that can form a massive being at need, or a single creature that can dissolve into a cloud of vermin." />
+    <class level="1" book="Complete Adventurer page 58" name="Master Of Many Forms {Prestige Class}" hd="d?" desc="A master of many forms has no shape that she calls her own." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 119" name="Master Of Nine {Prestige Class}" hd="d?" desc="Some savants of the Nine Disciplines believe that none of the paths are complete, true disciplines in and of themselves." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 44" name="Master Of Radiance {Prestige Class}" hd="d?" desc="Masters of radiance channel the pure, undiluted power of the sun." />
+    <class level="1" book="Tome of Magic page 121" name="Master Of Shadow {Prestige Class}" hd="d?" desc="Some driven or domineering souls seek nothing less than mastery of darkness itself -- the ability to turn the very shadows into their agents and allies." />
+    <class level="1" book="Defenders of the Faith: A Guidebook to Clerics and Paladins page 66" name="Master Of Shrouds {Prestige Class}" hd="d?" desc="The master of shrouds is an evil spellcaster who magically seizes incorporeal undead and sets them to do her bidding." />
+    <class level="1" book="Complete Warrior page 60" name="Master Of The Unseen Hand {Prestige Class}" hd="d?" desc="Masters of the unseen hand delight in crushing their foes with invisible force, flinging massive objects into the sky, and disarming enemies with a single thought." />
+    <class level="1" book="Unapproachable East page 24" name="Master Of The Yuirwood {Prestige Class}" hd="d?" desc="The masters of the Yuirwood are an elite group of foresters who work to keep the ancient Yuirwood free of evil influence." />
+    <class level="1" book="Serpent Kingdoms page 163" name="Master Of Vipers {Prestige Class}" hd="d?" desc="Outcast yuan-ti learn to hunt not only to feed themselves, but also to spread destruction far and wide for the pure pleasure of it." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 29" name="Master Samurai {Prestige Class}" hd="d?" desc="The master samurai is a military retainer of a feudal overlord; he practices a code of behavior that emphasizes the value of personal honor over life itself." />
+    <class level="1" book="Complete Mage page 70" name="Master Specialist {Prestige Class}" hd="d?" desc="A wizard with greater mastery over a school of specialization." />
+    <class level="1" book="Complete Warrior page 58" name="Master Thrower {Prestige Class}" hd="d?" desc="Master throwers depend on quick reflexes, good planning, and deadly aim." />
+    <class level="1" book="Complete Arcane page 51" name="Master Transmogrifist {Prestige Class}" hd="d?" desc="The master transmogrifist is a sorcerer or wizard who has chosen to specialize in spells that change his form." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 55" name="Master Vampire {Prestige Class}" hd="d?" desc="Any vampire can create spawn, but it takes a very special vampire to rule over an entire gang of minions." />
+    <class level="1" book="Races of Destiny page 123" name="Menacing Brute {Prestige Class}" hd="d?" desc="The menacing brute takes advantage of how must humans fear half-orcs, playing on that dread to make his living." />
+    <class level="1" book="Power of Faerun page 71" name="Merchant Prince {Prestige Class}" hd="d?" desc="A merchant prince (known as a merchant princess if female) is a member of the merchant nobility who has acquired his position and wealth either by being born into a wealthy family or by earning every last coin himself." />
+    <class level="1" book="Expanded Psionics Handbook page 147" name="Metamind {Prestige Class}" hd="d?" desc="Metaminds know that accumulating the most power in the shortest time is the key to psionic superiority." />
+    <class level="1" book="Complete Arcane page 54" name="Mindbender {Prestige Class}" hd="d?" desc="Mindbenders seek to control the thoughts and dreams of others." />
+    <class level="1" book="Complete Warrior page 62" name="Mindspy {Prestige Class}" hd="d?" desc="By reading the minds of her enemies, a mindspy knows exactly what they're going to do a fraction of a second before they do it." />
+    <class level="1" book="Oriental Adventures page 218" name="Mirumoto Niten Master {Prestige Class}" hd="d?" desc="The Mirumoto school teaches a unique style of swordplay, rooted in this sense of duty." />
+    <class level="1" book="Player's Guide to Faerun page 65" name="Monk Of The Long Death {Prestige Class}" hd="d?" desc="Monks of the long death are members of a macabre, secretive order of scholars seeking to understand the true nature of death." />
+    <class level="1" book="CoV page 117" name="Moonsea Skysentinel {Prestige Class}" hd="d?" desc="Moonsea skysentinels are the eyes in the sky for the Knights of the North, scouting the landscape, looking for evidence of Zhentarim activity." />
+    <class level="1" book="Races of Eberron page 143" name="Moonspeaker {Prestige Class}" hd="d?" desc="Bound to the magic of their lycanthrope ancestors, moonspeakers breathe the magic of the world, guided by the twelve moons of Eberron." />
+    <class level="1" book="City of Splendors: Waterdeep page 84" name="Moonstar Agent {Prestige Class}" hd="d?" desc="Moonstar agents, also known as Teukiir, are members of the Tel Teukiira, a group founded by Khelben 'Blackstaff' Arunsun when he broke from the Harpers." />
+    <class level="1" book="Player's Guide to Faerun page 66" name="Morninglord Of Lathander {Prestige Class}" hd="d?" desc="Morninglords are, in many ways, the epitome of the classical cleric archetype." />
+    <class level="1" book="Book of Vile Darkness page 64" name="Mortal Hunter {Prestige Class}" hd="d?" desc="Mortal hunters are fiends who specialize in killing mortals." />
+    <class level="1" book="Oriental Adventures page 228" name="Moto Avenger {Prestige Class}" hd="d?" desc="The Moto avenger is dedicated to a war against the Shadowlands and its evils." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 192" name="Mystic Theurge {Prestige Class}" hd="d?" desc="Blurring the line between divine and arcane, mystic theurges draw power from divine sources and musty tomes alike." />
+    <class level="1" book="Magic of Faerun page 35" name="Mystic Wanderer {Prestige Class}" hd="d?" desc="Mystic wanderers are divine spellcasters who eschew normal church hierarchies and instead embrace freedom, wanderlust, and independence." />
+    <class level="1" book="Serpent Kingdoms page 165" name="Naga Overlord {Prestige Class}" hd="d?" desc="Naga overlords are evil masterminds who operate in secret, usually behind cults of devoted followers." />
+    <class level="1" book="Unapproachable East page 25" name="Nar Demonbinder {Prestige Class}" hd="d?" desc="Master of the black art of demon summoning, the Nar demonbinder keeps alive the sinister traditions of the old Empire of Narfell." />
+    <class level="1" book="Magic of Incarnum page 132" name="Necrocarnate {Prestige Class}" hd="d?" desc="Dealers in death and torturers of souls, necrocarnates number among the most evil creatures in any world." />
+    <class level="1" book="Unapproachable East page 28" name="Nentyar Hunter {Prestige Class}" hd="d?" desc="Sworn to defend the great forests and serve the Nentyarch, druidic ruler of the Circle of Leth, the Nentyar hunters roam the wild lands of the East, uprooting foul and evil things." />
+    <class level="1" book="Champions of Ruin page 53" name="Night Mask Deathbringer {Prestige Class}" hd="d?" desc="Night Mask deathbringers are highly trained members of the Westgate thieves' guild who have caught the favorable attention of the vampires in charge of the organization." />
+    <class level="1" book="Faiths &amp; Pantheons page 198" name="Nightcloak {Prestige Class}" hd="d?" desc="Nightcloaks are the apple of Shar's eye -- devoted to her vision, preserving her secrets, practicing her magic, as twisted and bitter as it is." />
+    <class level="1" book="Complete Mage page 74" name="Nightmare Spinner {Prestige Class}" hd="d?" desc="An arcane spellcaster who weaves fear into illusions." />
+    <class level="1" book="Complete Adventurer page 60" name="Nightsong Enforcer {Prestige Class}" hd="d?" desc="The enforcers of the Nightsong Guild focus on the stealth-centered combat training that rogues usually learn." />
+    <class level="1" book="Complete Adventurer page 62" name="Nightsong Infiltrator {Prestige Class}" hd="d?" desc="The nightsong infiltrator is the perfect thief and the perfect spy." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 30" name="Ninja Of The Crescent Moon {Prestige Class}" hd="d?" desc="The Ninja of the Crescent Moon is a mercenary clan whose members engage in sabotage and other covert missions for an outlandish fee." />
+    <class level="1" book="Oriental Adventures page 43" name="Ninja Spy {Prestige Class}" hd="d?" desc="True ninja spies are masters of exotic weapons, tools of stealth, and strange ki powers." />
+    <class level="1" book="Tome of Magic page 125" name="Noctumancer {Prestige Class}" hd="d?" desc="Noctumancers bridge the gap between shadow and arcane magic." />
+    <class level="1" book="Complete Warrior page 66" name="Occult Slayer {Prestige Class}" hd="d?" desc="The occult slayer is driven to confront any arcane or divine spellcaster who crosses her path." />
+    <class level="1" book="Faiths &amp; Pantheons page 200" name="Ocular Adept {Prestige Class}" hd="d?" desc="Ocular adepts have pledged their religious devotions to the alien entity known as the Great Mother, the deity matron of all beholders." />
+    <class level="1" book="Lost Empires of Faerun page 21" name="Olin Gisir {Prestige Class}" hd="d?" desc="The Olin Gisiae are elite elf mages who have taken it upon themselves to guard dark secrets from the rest of the world" />
+    <class level="1" book="Complete Adventurer page 66" name="Ollam {Prestige Class}" hd="d?" desc="In Dwarven, the world 'ollam' means teacher." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 67" name="Oozemaster {Prestige Class}" hd="d?" desc="Oozemasters relate one-on-one with things that relate to nothing at all." />
+    <class level="1" book="Silver Marches page 114" name="Orc Scout {Prestige Class}" hd="d?" desc="Part wilderness warrior and part spy, the orc scout is a hero to his people." />
+    <class level="1" book="Races of Faerun page 184" name="Orc Warlord {Prestige Class}" hd="d?" desc="The orc warlord is a savage general of an unruly army, the leader of one of the deadly and all too common orc hordes that rampage down from the Spine of the World." />
+    <class level="1" book="Complete Warrior page 68" name="Order Of The Bow Initiate {Prestige Class}" hd="d?" desc="By learning the meditative art of the Way of the Bow, the archer improves his discipline, precision, and spirituality." />
+    <class level="1" book="Races of Destiny page 126" name="Outcast Champion {Prestige Class}" hd="d?" desc="Outcast champions bring hope to those who have no place in society." />
+    <class level="1" book="Song and Silence: A Guidebook to Bards and Rogues page 10" name="Outlaw Of The Crimson Road {Prestige Class}" hd="d?" desc="An outlaw of the crimson road might be a revolutionary, a loyal supporter of some deposed ruler, or merely an ordinary individual who angered the wrong person at the wrong time." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 47" name="Pale Master {Prestige Class}" hd="d?" desc="Arcane casters can become pale masters, who draw on necromantic lore that provides a macabre power all its own." />
+    <class level="1" book="Silver Marches page 115" name="Peerless Archer {Prestige Class}" hd="d?" desc="The peerless archer devotes her life to perfecting her skill with the bow." />
+    <class level="1" book="Races of Stone page 116" name="Peregrine Runner {Prestige Class}" hd="d?" desc="When goliaths need to send a message to another tribe, they send an elite, fleet-of-foot warrior known as a peregrine runner." />
+    <class level="1" book="Epic Level Handbook page 34" name="Perfect Wight {Prestige Class}" hd="d?" desc="The perfect wight is a master of skulking, the ultimate prowler and thief." />
+    <class level="1" book="Complete Divine page 50" name="Pious Templar {Prestige Class}" hd="d?" desc="Sworn to the defense of a temple site, the pious templar is a holy warrior blessed by her deity with combat prowess and great endurance." />
+    <class level="1" book="Manual of the Planes page 28" name="Planar Champion {Prestige Class}" hd="d?" desc="The planar champion moves between the planes, always driven to battle." />
+    <class level="1" book="Faiths of Eberron page 105" name="Planar Shepherd {Prestige Class}" hd="d?" desc="Some druids, especially among the Greensingers or those who have dealt extensively with that sect, reject narrow interpretations of what constitutes the natural world." />
+    <class level="1" book="Manual of the Planes page 30" name="Planeshifter {Prestige Class}" hd="d?" desc="The planeshifter is a magical scholar and expert in planar travel, and through arcane research develops not only the ability to sense planar portals, but also the ability to create his own demiplane." />
+    <class level="1" book="Draconomicon page 133" name="Platinum Knight {Prestige Class}" hd="d?" desc="The platinum knight protects good-aligned dragonkind from their natural enemies." />
+    <class level="1" book="Unearthed Arcana page 69" name="Prestige Bard {Prestige Class}" hd="d?" desc="The prestige bard is a jack-of-all-trades, master of none." />
+    <class level="1" book="Unearthed Arcana page 70" name="Prestige Paladin {Prestige Class}" hd="d?" desc="After training in the arts of combat and the mysteries of the divine, the prestige paladin is anointed as a holy warrior dedicated to the protection of law and goodness." />
+    <class level="1" book="Unearthed Arcana page 71" name="Prestige Ranger {Prestige Class}" hd="d?" desc="The prestige ranger navigates the dark forests, craggy mountains, or desert wastes of her homeland with unparalleled skill." />
+    <class level="1" book="Secrets of Xen'drik page 127" name="Primal Scholar {Prestige Class}" hd="d?" desc="Primal scholars are spellcasters bent on uncovering and mastering the ancient magic that lies buried in the jungles, deserts, and mountains of Xen'drik." />
+    <class level="1" book="Underdark page 40" name="Prime Underdark Guide {Prestige Class}" hd="d?" desc="These skilled guides not only know how to overcome the physical challenges of the Underdark, but they also can help them over the social and cultural hurdles they are sure to face." />
+    <class level="1" book="Frostburn page 65" name="Primeval {Prestige Class}" hd="d?" desc="The primeval is a warrior who has tapped into his racial memories to find and forge a bond with an ancient creature." />
+    <class level="1" book="Book of Exalted Deeds page 66" name="Prophet Of Erathaoi {Prestige Class}" hd="d?" desc="The prophet of Erathaoi is a seer and visionary, a medium of the heavenly will, pronouncing judgment on corruption and evil in the world." />
+    <class level="1" book="Expanded Psionics Handbook page 148" name="Psion Uncarnate {Prestige Class}" hd="d?" desc="Formless, fleshless, and unbound by the limits of corporeality -- this is the goal of every psion uncarnate." />
+    <class level="1" book="Heroes of Horror page 108" name="Purifier Of The Hallowed Doctrine {Prestige Class}" hd="d?" desc="Purifiers of the Hallowed Doctrine consider themselves servants not of gods but of the spiritual well-being of the world itself." />
+    <class level="1" book="Complete Warrior page 70" name="Purple Dragon Knight {Prestige Class}" hd="d?" desc="Purple Dragon knights develop uncanny skills related to coordinating and leading soldiers." />
+    <class level="1" book="Expanded Psionics Handbook page 151" name="Pyrokineticist {Prestige Class}" hd="d?" desc="Pyrokineticists know that a little psionic power goes a long way -- for those interested in fire." />
+    <class level="1" book="Magic of Eberron page 77" name="Quori Mindhunter {Prestige Class}" hd="d?" desc="The quori mindhunter has a single mission: to hunt down and destroy the quori spirits that corrupt humanity, and the possessed Inspired that further the aims of the Dreaming Dark." />
+    <class level="1" book="Races of Eberron page 148" name="Quori Nightmare {Prestige Class}" hd="d?" desc="The quori nightmare taps into the primal horrors and urges of the subconscious." />
+    <class level="1" book="Complete Divine page 52" name="Radiant Servant Of Pelor {Prestige Class}" hd="d?" desc="The radiant servants of Pelor put the dogma of demonstrating strength through charity and modesty into living practice." />
+    <class level="1" book="Complete Warrior page 72" name="Rage Mage {Prestige Class}" hd="d?" desc="The rage mage's approach to magic is based on the primal passion of magic more than the studious quasi-scientific approach." />
+    <class level="1" book="Complete Divine page 54" name="Rainbow Servant {Prestige Class}" hd="d?" desc="Those who have learned what the couatl temples have to offer are known as rainbow servants." />
+    <class level="1" book="Unapproachable East page 29" name="Raumathari Battlemage {Prestige Class}" hd="d?" desc="Employing sword and spell with dauntless courage and deadly force, the handful of Raumathari battlemages remaining in the world comprise a lonely and little-known order of adventurers, explorers, and mercenaries in search of battle." />
+    <class level="1" book="Complete Warrior page 73" name="Ravager {Prestige Class}" hd="d?" desc="The infamous ravager has dedicated himself to the service of Erythnul, deity of slaughter." />
+    <class level="1" book="Races of Eberron page 153" name="Reachrunner {Prestige Class}" hd="d?" desc="Known for their mastery of the untamed world, some shifters rise above others in woodslore, physical ability, and stamina to claim the revered mantle of the reachrunner -- the greatest of shifter trackers and scouts." />
+    <class level="1" book="Complete Warrior page 75" name="Reaping Mauler {Prestige Class}" hd="d?" desc="Reaping maulers are the back-breakers, the limb-twisters, and the neck-snappers among pit fighters." />
+    <class level="1" book="Races of Eberron page 157" name="Recaster {Prestige Class}" hd="d?" desc="To the recaster, the change her own body is capable of is a simple reflection of the mutability of the world around her." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 34" name="Red Avenger {Prestige Class}" hd="d?" desc="The Red Avenger is the master of ki,an ancient and formidable discipline that allows the user to accomplish the extraordinary." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 193" name="Red Wizard {Prestige Class}" hd="d?" desc="The Red Wizards are the masters of Thay, the would-be magical overlords of the land of Faerun." />
+    <class level="1" book="Races of Eberron page 161" name="Reforged {Prestige Class}" hd="d?" desc="The reforged represents the realized ideal of the warforged's living aspects." />
+    <class level="1" book="Magic of Eberron page 81" name="Renegade Mastermaker {Prestige Class}" hd="d?" desc="A renegade mastermaker applies the secrets of warforged creation methods to his own body, slowly replacing parts of his body with mechanical augmentations." />
+    <class level="1" book="Player's Guide to Eberron page 108" name="Revenant Blade {Prestige Class}" hd="d?" desc="The revenant blade is a Valenar elf who can draw on the skills of ancient heroes, the giant-slayers of Xen'drik." />
+    <class level="1" book="Dragonlance Campaign Setting page 83" name="Righteous Zealot {Prestige Class}" hd="d?" desc="The righteous zealot is a person with a cause that directs every aspect of life." />
+    <class level="1" book="Frostburn page 67" name="Rimefire Witch {Prestige Class}" hd="d?" desc="A rimefire witch is one who has followed a mysterious call to the core of a rimefire iceberg and becomes infused with great power by the rimefire eidolon." />
+    <class level="1" book="Book of Exalted Deeds page 68" name="Risen Martyr {Prestige Class}" hd="d?" desc="A risen martyr is an exalted character who continues in his earthly existence after his martyrdom in order to finish some unfinished task." />
+    <class level="1" book="Complete Warrior page 77" name="Ronin {Prestige Class}" hd="d?" desc="A ronin is a masterless warrior cast adrift in the world, but still clinging to the remnants of his former life." />
+    <class level="1" book="Song and Silence: A Guidebook to Bards and Rogues page 13" name="Royal Explorer {Prestige Class}" hd="d?" desc="Some monarchs sponser crack teams of explorers." />
+    <class level="1" book="Races of the Wild page 122" name="Ruathar {Prestige Class}" hd="d?" desc="Also known as 'elf-friend' or 'star-friend,' a ruathar is a person of some other race who has earned the special friendship of the elven folk." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 122" name="Ruby Knight Vindicator {Prestige Class}" hd="d?" desc="The Ruby Knights are a crusader order in the service of Wee Jas, goddess of death and magic." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 51" name="Runecaster {Prestige Class}" hd="d?" desc="Those that choose to master the ability to create runes of power are runecasters." />
+    <class level="1" book="Unapproachable East page 31" name="Runescarred Berserker {Prestige Class}" hd="d?" desc="Deadly barbarians who bear magical runes carved into their flesh, runescarred berserkers are among the most feared of Rashemen's defenders." />
+    <class level="1" book="Races of Stone page 118" name="Runesmith {Prestige Class}" hd="d?" desc="A runesmith has learned to harness the power of runes and can fling fireballsand other staple arcane spells even while encased in full plate armor." />
+    <class level="1" book="Complete Divine page 56" name="Sacred Exorcist {Prestige Class}" hd="d?" desc="Sacred exorcists hope to drive away the spiritual forces of evil, prevening them from causing harm to the bodies and souls of humanity." />
+    <class level="1" book="Complete Divine page 59" name="Sacred Fist {Prestige Class}" hd="d?" desc="Sacred fists are independent organizations found within many temples." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 49" name="Sacred Purifier {Prestige Class}" hd="d?" desc="Sacred purifiers are priestly characters who specialize in destroying undead." />
+    <class level="1" book="Draconomicon page 96" name="Sacred Warder Of Bahamut {Prestige Class}" hd="d?" desc="Sacred warders of Bahamut protect others from the power of Tiamat's brood." />
+    <class level="1" book="Lords of Madness page 198" name="Sanctified Mind {Prestige Class}" hd="d?" desc="A sanctified mind believes that all evil-aligned psionics-using creatures must be crushed." />
+    <class level="1" book="Sandstorm page 76" name="Sand Shaper {Prestige Class}" hd="d?" desc="Sand shapers are part prophet, part priest, part magician, and part assassin." />
+    <class level="1" book="Magic of Incarnum page 136" name="Sapphire Hierarch {Prestige Class}" hd="d?" desc="The elite members of an order of priests of law defend the temple, contemplate the mysteries of the Sapphire Eidolon, and seek to fulfill its single command by perfecting themselves and bringing order out of chaos wherever they find it." />
+    <class level="1" book="Lords of Madness page 21" name="Savant Aboleth {Prestige Class}" hd="d?" desc="Savant aboleths are the eldest, most intelligent, wisest and most forceful of personality." />
+    <class level="1" book="Savage Species page 83" name="Scaled Horror {Prestige Class}" hd="d?" desc="Scaled horrors are elite amphibious soldiers." />
+    <class level="1" book="Races of Destiny page 130" name="Scar Enforcer {Prestige Class}" hd="d?" desc="Scar enforcers are angry, embittered half-elves who have rejected both sides of their ancestry." />
+    <class level="1" book="Stormwrack page 65" name="Scarlet Corsair {Prestige Class}" hd="d?" desc="The scarlet corsair relies on the reputation of her quick blade and terrible fighting skills to drive her prey before her." />
+    <class level="1" book="Tome of Magic page 59" name="Scion Of Dantalion {Prestige Class}" hd="d?" desc="The scions believe that their destiny is to one day take up the crown of a long-forgotten human empire, bear the scepter of rulership, and rebuild the empire that could rival the stars." />
+    <class level="1" book="Sandstorm page 86" name="Scorpion Heritor {Prestige Class}" hd="d?" desc="Scorpion heritors, through a special relationship with the scorpion spirit, gain the mystical abilities of the scorpion, and can even take its shape." />
+    <class level="1" book="Secrets of Xen'drik page 130" name="Scorpion Wraith {Prestige Class}" hd="d?" desc="Scorpion wraiths are the elite warriors of the drow." />
+    <class level="1" book="Shining South page 40" name="Scourge Maiden {Prestige Class}" hd="d?" desc="Scourge maidens are warrior-priestesses of Loviatar dedicated to pain and anguish." />
+    <class level="1" book="Underdark page 42" name="Sea Mother Whip {Prestige Class}" hd="d?" desc="Devout worshipers of Blibdoolpoolp who seek closer communion with the Sea Mother often gain additional abilities in the Sea Mother whip prestige class." />
+    <class level="1" book="Stormwrack page 68" name="Sea Witch {Prestige Class}" hd="d?" desc="A sea witch is a terrible chaotic mage who wields the powers of water and calls on the living horrors of the deep." />
+    <class level="1" book="Complete Divine page 61" name="Seeker Of The Misty Isle {Prestige Class}" hd="d?" desc="Seekers search for the lost elves of Misty Isle." />
+    <class level="1" book="Complete Arcane page 56" name="Seeker Of The Song {Prestige Class}" hd="d?" desc="Seekers of the song wield the power of music in ways that amaze even the most skilled bards." />
+    <class level="1" book="Book of Exalted Deeds page 69" name="Sentinel Of Bharrai {Prestige Class}" hd="d?" desc="Respect for the power of nature, the desire to further the ends of good, and the resolve to destroy evil are the core beliefs of a sentinel of Bharrai." />
+    <class level="1" book="Serpent Kingdoms page 166" name="Serpent Slayer {Prestige Class}" hd="d?" desc="Some individuals devote their entire lives to thwarting the yuan-ti." />
+    <class level="1" book="Player's Guide to Faerun page 71" name="Shaaryan Hunter {Prestige Class}" hd="d?" desc="On the backs of their swift horses, Shaaryan hunters can run down even the fastest prey and either spear it with a lance or pelt it with arrows from horseback." />
+    <class level="1" book="Champions of Ruin page 58" name="Shade Hunter {Prestige Class}" hd="d?" desc="The shade hunter is a breed of adventurer who lives for the thrill of finding lost treasure, defeating ancient traps, and surviving deadly curses laid by the priests of dead gods." />
+    <class level="1" book="Forgotten Realms Campaign Setting page 52" name="Shadow Adept {Prestige Class}" hd="d?" desc="Shadow adepts hurl themselves into the abyss of the Shadow Weave, immediately acquiring all the gifts available to casual students and discovering secrets unavailable to all but the most dedicated." />
+    <class level="1" book="Oriental Adventures page 44" name="Shadow Scout {Prestige Class}" hd="d?" desc="The camouflage of a tiger, the stamina of a horse, the eyes of an eagle: these are the ingredients of the shadow scouts." />
+    <class level="1" book="Races of Destiny page 137" name="Shadow Sentinel {Prestige Class}" hd="d?" desc="Shadow sentinels are elite illumian warriors who protect their people from githyanki raiders, demonic invastions, and hordes of barely imaginable monsters from the Plane of Shadow." />
+    <class level="1" book="Tome of Battle: The Book of Nine Swords page 126" name="Shadow Sun Ninja {Prestige Class}" hd="d?" desc="A Shadow Sun ninja is a martial artist who studies the balance between good and evil, light and dark." />
+    <class level="1" book="Player's Guide to Faerun page 74" name="Shadow Thief Of Amn {Prestige Class}" hd="d?" desc="A shadow thief of Amn knows only her own minions, her coworkers, and her superior." />
+    <class level="1" book="Complete Adventurer page 68" name="Shadowbane Inquisitor {Prestige Class}" hd="d?" desc="Shadowbane inquisitors battle incessantly against evil in whatever form it takes." />
+    <class level="1" book="Complete Adventurer page 70" name="Shadowbane Stalker {Prestige Class}" hd="d?" desc="Shadowbane stalkers find evil hidden in civilized areas so that the martial arm of the order (the inquisitors) can spearhead the attack." />
+    <class level="1" book="Tome of Magic page 129" name="Shadowblade {Prestige Class}" hd="d?" desc="Shadowblades are martial combatants with an innate link to shadow." />
+    <class level="1" book="Races of Stone page 120" name="Shadowcraft Mage {Prestige Class}" hd="d?" desc="Some gnomes have an even greater affinity for illusions than the average representative of their race, resulting in the prestige class known as the shadowcraft mage." />
+    <class level="1" book="Underdark page 43" name="Shadowcrafter {Prestige Class}" hd="d?" desc="Shadowcrafters long ago mastered illusions and glamers." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 194" name="Shadowdancer {Prestige Class}" hd="d?" desc="Operating in the border between light and darkness, shadowdancers are nimble artists of deception." />
+    <class level="1" book="Complete Adventurer page 74" name="Shadowmind {Prestige Class}" hd="d?" desc="A shadowmind blends psionic powers and uncanny stealth into an effective whole." />
+    <class level="1" book="Tome of Magic page 132" name="Shadowsmith {Prestige Class}" hd="d?" desc="Shadowcasters draw power from darkness, and masters of shadow command it, but no one truly manipulates the darkness as does the shadowsmith." />
+    <class level="1" book="Oriental Adventures page 45" name="Shapeshifter {Prestige Class}" hd="d?" desc="Shapeshifters must already have some means of changing their form before learning to master that change." />
+    <class level="1" book="Sharn: City of Towers page 165" name="Sharn Skymage {Prestige Class}" hd="d?" desc="By studying the properties of the manifest zone in which Sharn is situated, learning its intricacies and methods for manipulating it, a spellcaster can improve her magical or natural ability to fly." />
+    <class level="1" book="Oriental Adventures page 222" name="Shiba Protector {Prestige Class}" hd="d?" desc="The warriors of the Shiba family are sworn to protect the Isawa family." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 68" name="Shifter {Prestige Class}" hd="d?" desc="The shifter has no form that she calls her own." />
+    <class level="1" book="Complete Divine page 63" name="Shining Blade Of Heironeous {Prestige Class}" hd="d?" desc="The shining blade of Heironeous is a member of an order of knights dedicated to prowess in melee combat." />
+    <class level="1" book="Oriental Adventures page 46" name="Shintao Monk {Prestige Class}" hd="d?" desc="Shintao monks are dedicated to following the teachings of Shinsei." />
+    <class level="1" book="Unapproachable East page 32" name="Shou Disciple {Prestige Class}" hd="d?" desc="Shou disciples are martial artists who have studied or observed the monks of Kara-Tur and seek to emulate their style." />
+    <class level="1" book="FN page 150" name="Silver Pyromancer {Prestige Class}" hd="d?" desc="The silver pyromancer is an arcane champion of the Church of the Silver Flame, taking his place alongside clerics, paladins, and exorcists in the Church's cause." />
+    <class level="1" book="Faiths &amp; Pantheons page 201" name="Silverstar {Prestige Class}" hd="d?" desc="Silverstars are dedicated advocates of freedom and tolerance, wanderers on the path of truth, and absolute foes of Shar." />
+    <class level="1" book="Races of the Dragon page 91" name="Singer Of Concordance {Prestige Class}" hd="d?" desc="The Singers of Concordance are a small order of wandering draconic spiritual guides who begin as servitors of Io, the Ninefold Dragon, creator of all dragonkind." />
+    <class level="1" book="Oriental Adventures page 48" name="Singh Rager {Prestige Class}" hd="d?" desc="Singh ragers draw their furious strength from the noble lion." />
+    <class level="1" book="Savage Species page 84" name="Siren {Prestige Class}" hd="d?" desc="A harpy siren is an artist who constantly seeks to expand and improve upon her innate sonic ability." />
+    <class level="1" book="Miniatures Handbook page 20" name="Skullclan Hunter {Prestige Class}" hd="d?" desc="The skullclan hunter is the acclaimed foe of unlife." />
+    <class level="1" book="Book of Exalted Deeds page 71" name="Skylord {Prestige Class}" hd="d?" desc="An elf crusader, the skylord uses his kinship with creatures of the sky and the power of the winds to fight evil." />
+    <class level="1" book="Races of the Wild page 126" name="Skypledged {Prestige Class}" hd="d?" desc="The skypledged represent a mystical tradition among the raptorans that hearkens back to an ancient pact with powerful lords of the Elemental Plane of Air." />
+    <class level="1" book="Savage Species page 87" name="Slaad Brooder {Prestige Class}" hd="d?" desc="The brooder's sole purpose is to implant as many eggs pellets as he can to produce the widest possible range of progeny." />
+    <class level="1" book="Book of Exalted Deeds page 73" name="Slayer Of Domiel {Prestige Class}" hd="d?" desc="Sometimes the skillset of an assassin is required for more noble pursuits." />
+    <class level="1" book="Player's Guide to Faerun page 186" name="Slime Lord {Prestige Class}" hd="d?" desc="Slime lords, the most favored of Ghaunadar's servants, are not clerics; they are spies and infiltrators who can change their shapes in order to move unnoticed among members of any race." />
+    <class level="1" book="Deities and Demigods page 208" name="Soldier Of Light {Prestige Class}" hd="d?" desc="The Soldiers of Light are a military order dedicated to open warfare against the minions of their church's enemies." />
+    <class level="1" book="Book of Vile Darkness page 66" name="Soul Eater {Prestige Class}" hd="d?" desc="The soul eater is a monstrous being that feeds on the very essence of life force." />
+    <class level="1" book="Complete Psionic page 36" name="Soulbow {Prestige Class}" hd="d?" desc="In the tradition of the soulknife, a soulbow realizes the direct capacity of her own mind to give shape to weapons of psionic perfection." />
+    <class level="1" book="Magic of Incarnum page 142" name="Soulcaster {Prestige Class}" hd="d?" desc="Soulcasters excel at incorporating soul energy into their magic." />
+    <class level="1" book="Faiths of Eberron page 32" name="Sovereign Speaker {Prestige Class}" hd="d?" desc="Although devotion to a single god enables some individuals to gain additional power, overriding" />
+    <class level="1" book="Unearthed Arcana page 167" name="Spell Scion {Prestige Class}" hd="d?" desc="This prestige class is for characters who wield legendary weapons designed for use by arcane spellcasters, such as wizards, sorcerers, and sometimes bards." />
+    <class level="1" book="Races of Eberron page 166" name="Spellcarved Soldier {Prestige Class}" hd="d?" desc="Spellcarved soldiers are warforged warriors who engrave magic runes into the plating of their inherently magic bodies, gaining remarkable defensive abilities." />
+    <class level="1" book="Magic of Faerun page 37" name="Spelldancer {Prestige Class}" hd="d?" desc="Spelldancers are an energetic sort of spellcaster who draw on the quasi-primal energy of song and dancing to power their magic." />
+    <class level="1" book="Magic of Faerun page 38" name="Spellfire Channeler {Prestige Class}" hd="d?" desc="Those who practice their spellfire can hone their talent into a tool with fantastic abilities that most dabblers can only dream of." />
+    <class level="1" book="Player's Guide to Faerun page 75" name="Spellguard Of Silverymoon {Prestige Class}" hd="d?" desc="The Spellguard, Silverymoon's elite cadre of battle-trained arcane spellcasters, protects the city against the threat of hostile magic and aids the Knights in Silver against more mundane threats." />
+    <class level="1" book="Races of Faerun page 185" name="Spellsinger {Prestige Class}" hd="d?" desc="Spellsingers are rare practitioners of an ancient elven bardic tradition." />
+    <class level="1" book="Complete Warrior page 79" name="Spellsword {Prestige Class}" hd="d?" desc="The dream of melding magic and weaponplay is fulfilled in the person of the spellsword." />
+    <class level="1" book="Magic of Incarnum page 147" name="Spinemeld Warrior {Prestige Class}" hd="d?" desc="When a spinemeld warrior trains, he is participating in a tradition that has long been venerated in skarn society." />
+    <class level="1" book="Lords of Darkness page 11" name="Spur Lord {Prestige Class}" hd="d?" desc="The Spur Lords are elite zealots of the church, wielding the dark power of Cyric and commanding the attention of even the most fanatical clerics." />
+    <class level="1" book="Complete Adventurer page 76" name="Spymaster {Prestige Class}" hd="d?" desc="Spymasters do their work quietly and in private, and they often have a cover identity." />
+    <class level="1" book="Book of Exalted Deeds page 75" name="Stalker Of Kharash {Prestige Class}" hd="d?" desc="The stalkers of Kharash are a loose-knit order of rangers, rogues, and other characters devoted to fighting evil under Kharash's patronage." />
+    <class level="1" book="Dragonlance Campaign Setting page 68" name="Steel Legionnaire {Prestige Class}" hd="d?" desc="Steel legionnaires are members of the Legion of Steel." />
+    <class level="1" book="Races of Stone page 122" name="Stoneblessed {Prestige Class}" hd="d?" desc="A stoneblessed bonds to the stone of the mountains, blending into a dwarf, gnome, or goliath community and making it her home." />
+    <class level="1" book="Races of Stone page 124" name="Stonedeath Assassin {Prestige Class}" hd="d?" desc="Most stonedeath assassins are hobgoblin rogues or rangers, but bugbears and even exceptional goblins have been known to undertake stonedeath training and learn the ways of infiltrating dwarf strongholds by disarming traps, weakening gates, and assassinating dwarf leaders." />
+    <class level="1" book="Complete Warrior page 81" name="Stonelord {Prestige Class}" hd="d?" desc="The earth whispers to special dwarves known as stonelords." />
+    <class level="1" book="Races of Stone page 127" name="Stonespeaker Guardian {Prestige Class}" hd="d?" desc="The stonespeaker guardian taps into the divine power of the earth itself to defend her fellow stonespeakers, as well as other goliaths and friendly races, from their enemies." />
+    <class level="1" book="Complete Psionic page 40" name="Storm Disciple {Prestige Class}" hd="d?" desc="A storm disciple is a character who decides that the best, most glorious way to serve his ideals is through the natural power, fury, and splendor of the storm." />
+    <class level="1" book="Stormwrack page 72" name="Stormcaster {Prestige Class}" hd="d?" desc="The stormcaster is one who seeks to tap into the power of a strange and terrifying phenomenon: the raging storm." />
+    <class level="1" book="Complete Divine page 65" name="Stormlord {Prestige Class}" hd="d?" desc="Stormlords often live as brigands, indulging their personal desires for wealth, food, luxury items, and wanton behavior as they crave random, spectacular acts of violence." />
+    <class level="1" book="Frostburn page 70" name="Stormsinger {Prestige Class}" hd="d?" desc="The stormsingers have learned the secret methods of harnessing the magic powers of music to influence and control the weather." />
+    <class level="1" book="Races of the Wild page 131" name="Stormtalon {Prestige Class}" hd="d?" desc="The stormtalons are consummate aerial warriors, using both their weapons and their razor-sharp foot talons to dive on their hapless foes." />
+    <class level="1" book="Complete Adventurer page 79" name="Streetfighter {Prestige Class}" hd="d?" desc="Streetfighters seek the challenges of the back alleys as a way of testing themselves and their experience in the wilder world." />
+    <class level="1" book="Faiths &amp; Pantheons page 204" name="Strifeleader {Prestige Class}" hd="d?" desc="Strifeleaders are the chief instruments of the Dark Sun, charged with spreading the One True Way of Cyric through force and deception." />
+    <class level="1" book="Complete Arcane page 60" name="Sublime Chord {Prestige Class}" hd="d?" desc="In return for abandoning her continuing study of bardic music, a sublime chord instead masters a number of spells more powerful than most bards can ever use." />
+    <class level="1" book="Complete Arcane page 63" name="Suel Arcanamach {Prestige Class}" hd="d?" desc="Arcanamach formerly served as elite guards and agents for powerful wizards." />
+    <class level="1" book="City of Splendors: Waterdeep page 88" name="Sun Soul Monk {Prestige Class}" hd="d?" desc="Monks of the Sun Soul Order believe that they each harbor a small fragment of the sun's divine essence." />
+    <class level="1" book="Lost Empires of Faerun page 25" name="Sunmaster {Prestige Class}" hd="d?" desc="The sunmasters are members of a sect within the church of Lathander who believe that the Morninglord is the living reincarnation of Amaunator." />
+    <class level="1" book="Savage Species page 89" name="Survivor {Prestige Class}" hd="d?" desc="Those who survive a program of frequent assaults and other dangers emerge a few weeks later -- tougher, faster, and less vulnerable to attacks." />
+    <class level="1" book="Book of Exalted Deeds page 76" name="Swanmay {Prestige Class}" hd="d?" desc="Swanmays are members of a secretive order sworn to protect wilderness areas from evil." />
+    <class level="1" book="Unearthed Arcana page 168" name="Swift Scion {Prestige Class}" hd="d?" desc="This prestige class is for those who wield legendary weapons that make use of or improve the wielder's stealth, speed, or dexterity (in the general sense)." />
+    <class level="1" book="Dragon Magic page 50" name="Swift Wing {Prestige Class}" hd="d?" desc="Swift wings are church servants who see themselves as the fast-moving, hard-hitting crusaders of their god's cadre of worshipers." />
+    <class level="1" book="Faiths &amp; Pantheons page 205" name="Sword Dancer {Prestige Class}" hd="d?" desc="Sword dancers are expected to lead the drow migration and work to promote harmony between drow and surface-dwelling races." />
+    <class level="1" book="Book of Exalted Deeds page 77" name="Sword Of Righteousness {Prestige Class}" hd="d?" desc="Pursuit of a commitment to righteousness and purity that exceeds the norm is a quality of a sword of righteousness." />
+    <class level="1" book="Savage Species page 90" name="Sybil {Prestige Class}" hd="d?" desc="Steeped in ancient lore, or maddened by divine inspiration, the sybil is a reclusive prophet." />
+    <class level="1" book="Miniatures Handbook page 22" name="Tactical Soldier {Prestige Class}" hd="d?" desc="The tactical soldier is the master of teamwork in melee." />
+    <class level="1" book="Heroes of Horror page 113" name="Tainted Scholar {Prestige Class}" hd="d?" desc="No secret is barred from the tainted scholar's grasp, and if such forbidden knowledge comes at the cost of his soul, he's willing to pay that price." />
+    <class level="1" book="Unearthed Arcana page 191" name="Tainted Sorcerer {Prestige Class}" hd="d?" desc="Tainted sorcerers find an easy path to tremendous magical power." />
+    <class level="1" book="Unearthed Arcana page 193" name="Tainted Warrior {Prestige Class}" hd="d?" desc="When a character's taint threatens to exceed the capacity of his body and soul to contain it, he may become possessed by its evil power and transformed into a creature of taint." />
+    <class level="1" book="Draconomicon page 134" name="Talon Of Tiamat {Prestige Class}" hd="d?" desc="The talon of Tiamat furthers the goals of evil dragonkind." />
+    <class level="1" book="Unapproachable East page 34" name="Talontar Blightlord {Prestige Class}" hd="d?" desc="Corrupt priests who revel in decay, the blightlords of Talona are feared and reviled throughout the Unapproachable East." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 70" name="Tamer Of Beasts {Prestige Class}" hd="d?" desc="Through magic and his overwhelming concern for his charges, the tamer of beasts can make them tougher and more intelligent." />
+    <class level="1" book="Complete Warrior page 82" name="Tattooed Monk {Prestige Class}" hd="d?" desc="Certain monastic orders bestow supernatural or spell-like powers on their members by inscribing magic tattoos on their skin." />
+    <class level="1" book="Faiths &amp; Pantheons page 206" name="Techsmith {Prestige Class}" hd="d?" desc="Techsmiths are devoted to the development of new inventions and the progression of achievement in the name of the Wonderbringer." />
+    <class level="1" book="Unapproachable East page 36" name="Telflammar Shadowlord {Prestige Class}" hd="d?" desc="Above all the criminals of the Shadowmasters of Telflamm stand the Telflammar shadowlords,the secret captains of iniquity who demand unquestioned obedience from their numerous minions." />
+    <class level="1" book="Complete Adventurer page 81" name="Tempest {Prestige Class}" hd="d?" desc="A tempest is the point of calm within a whirling barrier of deadly blades." />
+    <class level="1" book="Defenders of the Faith: A Guidebook to Clerics and Paladins page 72" name="Templar {Prestige Class}" hd="d?" desc="Sworn to the defense of a temple site, the templar is a holy warrior blessed by her deity with combat prowess and great endurance." />
+    <class level="1" book="Complete Divine page 67" name="Temple Raider Of Olidammara {Prestige Class}" hd="d?" desc="The temple raiders are an elite cadre of thiees who worship the Laughing Rogue and specialize in stealing valuables and secret lore from the temples of other deities." />
+    <class level="1" book="Tome of Magic page 63" name="Tenebrous Apostate {Prestige Class}" hd="d?" desc="The remnant of divinity once possessed by Orcus, Tenebrous is perhaps the only vestige still worshiped in some places as a god. Some followers, however, believe that Tenebrous is a separate deity, so these Tenebrous apostates revere him as such." />
+    <class level="1" book="Dungeon Master's Guide v.3.5 page 196" name="Thaumaturgist {Prestige Class}" hd="d?" desc="The thaumaturgist reaches out with divine power to other planes of existence, calling creatures there to do his bidding." />
+    <class level="1" book="Champions of Ruin page 63" name="Thayan Gladiator {Prestige Class}" hd="d?" desc="Popular and skillful gladiators fill the arenas of Faerun from Calimshan to the Dragon Coast, but the brutal Thayan gladiators are the best of the best." />
+    <class level="1" book="Complete Warrior page 85" name="Thayan Knight {Prestige Class}" hd="d?" desc="Thayan knights have mastered the art of swordplay, are familiar with magic, and are loyal to none but the tattooed mages." />
+    <class level="1" book="Unapproachable East page 37" name="Thayan Slaver {Prestige Class}" hd="d?" desc="Thayan slavers are cruel marauders who use their awful abilities to abduct creatures and then break their wills." />
+    <class level="1" book="Faiths of Eberron page 84" name="Thief Of Life {Prestige Class}" hd="d?" desc="When the prize is immortality, there is precious little a thief of life will not do to grasp it." />
+    <class level="1" book="Book of Vile Darkness page 67" name="Thrall Of Demogorgon {Prestige Class}" hd="d?" desc="A thrall of Demogorgon thrives on the chaotic nature of mutation and deformity." />
+    <class level="1" book="Book of Vile Darkness page 70" name="Thrall Of Juiblex {Prestige Class}" hd="d?" desc="A thrall of Juiblex oozes a horrible slime and is surrounded by a nauseating stench." />
+    <class level="1" book="Book of Vile Darkness page 71" name="Thrall Of Orcus {Prestige Class}" hd="d?" desc="A thrall of Orcus has devoted herself to the demon prince of undeath." />
+    <class level="1" book="Expanded Psionics Handbook page 153" name="Thrallherd {Prestige Class}" hd="d?" desc="Thrallherds manipulate the minds of others as if they were clay in the hands of a sculptor." />
+    <class level="1" book="Explorer's Handbook page 64" name="Thunder Guide {Prestige Class}" hd="d?" desc="Bodyguards to nobles on safari, shepherds to spelunking university professors, and the real-life heroes of chronicle serials across Khorvaire, thunder guides provide the strong blades, keen senses, and local knowledge necessary to survive a trip across the Thunder Sea." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 57" name="Tomb Warden {Prestige Class}" hd="d?" desc="Tomb wardens serve as selfless, undying protectors of the dead." />
+    <class level="1" book="Lords of Madness page 203" name="Topaz Guardian {Prestige Class}" hd="d?" desc="Resolute crusaders, the topaz guardians are the elite initiates of the Holy Order of the Supernal Topaz Defenders." />
+    <class level="1" book="Magic of Incarnum page 153" name="Totem Rager {Prestige Class}" hd="d?" desc="The totem rager embodies the wrath of nature in its most bestial form." />
+    <class level="1" book="CoV page 123" name="Triadic Knight {Prestige Class}" hd="d?" desc="Triadic knights are holy warriors who worship the Triad of Tyr, Torm, and Ilmater." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 35" name="Tribal Protector {Prestige Class}" hd="d?" desc="The tribal protector is the battlefield champion of a savage humanoid race." />
+    <class level="1" book="Book of Exalted Deeds page 78" name="Troubadour Of Stars {Prestige Class}" hd="d?" desc="Bards who channel their celestial music through their mortal voices and instruments are troubadours of stars." />
+    <class level="1" book="Libris Mortis: The Book of the Dead page 51" name="True Necromancer {Prestige Class}" hd="d?" desc="Those who seek to raise an unyielding obedience from the dead willingly tread the path of necromancy." />
+    <class level="1" book="Complete Mage page 77" name="Ultimate Magus {Prestige Class}" hd="d?" desc="A multiclass arcane preparation spellcaster and arcane spontaneous spellcaster." />
+    <class level="1" book="Magic of Incarnum page 158" name="Umbral Disciple {Prestige Class}" hd="d?" desc="The umbral disciple is a student of shadow in both a literal and a metaphysical sense." />
+    <class level="1" book="Draconomicon page 97" name="Unholy Ravager Of Tiamat {Prestige Class}" hd="d?" desc="Those who devote themselves to Tiamat's cause become unholy ravagers of Tiamat." />
+    <class level="1" book="Epic Level Handbook page 35" name="Union Sentinel {Prestige Class}" hd="d?" desc="A Union Sentinel is a member of an elite police force that guards the demiplane city of Union." />
+    <class level="1" book="Complete Mage page 81" name="Unseen Seer {Prestige Class}" hd="d?" desc="A stealthy character who dabbles in divination magic." />
+    <class level="1" book="Races of Destiny page 141" name="Urban Soul {Prestige Class}" hd="d?" desc="Urban souls are the chosen champions of the deity Urbanus, charged with protecting city denzens from external dangers and from subtler threats to the city." />
+    <class level="1" book="Book of Exalted Deeds page 80" name="Vassal Of Bahamut {Prestige Class}" hd="d?" desc="A vassal of Bahamut is a devout, nondraconic champion in the service of the Dragon King." />
+    <class level="1" book="Champions of Ruin page 67" name="Vengeance Knight {Prestige Class}" hd="d?" desc="Vengeance knights roam the Lands of Intrigue in search of those who have committed acts of treachery against their employers, the Knights of the Shield." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 73" name="Verdant Lord {Prestige Class}" hd="d?" desc="The verdant lord is the final defender of the forest." />
+    <class level="1" book="Underdark page 44" name="Vermin Keeper {Prestige Class}" hd="d?" desc="To a vermin keeper, insects are perfect killers." />
+    <class level="1" book="Book of Vile Darkness page 73" name="Vermin Lord {Prestige Class}" hd="d?" desc="The vermin lord offers itself as a host for all manner of parasitic organisms." />
+    <class level="1" book="Magic of Eberron page 85" name="Vigilant Sentinel Of Aerenal {Prestige Class}" hd="d?" desc="Part spy, part assassin, and completely loyal to the Sibling Kings and Aerenal's undying rulers, the sentinels roam across Eberron." />
+    <class level="1" book="Complete Adventurer page 85" name="Vigilante {Prestige Class}" hd="d?" desc="The vigilante combines magical and mundane investigative techniques to assess a crime scene." />
+    <class level="1" book="Complete Adventurer page 89" name="Virtuoso {Prestige Class}" hd="d?" desc="The typical virtuoso is outgoing, charismatic, and gregarious." />
+    <class level="1" book="Planar Handbook page 53" name="Visionary Seeker {Prestige Class}" hd="d?" desc="A visionary seeker knows how to navigate the mental plain stretching out ahead, finding landfall and truly discovering what it means to know." />
+    <class level="1" book="Complete Divine page 72" name="Void Disciple {Prestige Class}" hd="d?" desc="Void disciples understand that everything in the world contains all the basic elements, held together by the least tangible essence." />
+    <class level="1" book="Sandstorm page 89" name="Walker In The Waste {Prestige Class}" hd="d?" desc="A walker in the waste embodies the harsh, unforgiving nature of the desert." />
+    <class level="1" book="Complete Warrior page 87" name="War Chanter {Prestige Class}" hd="d?" desc="A war chanter's music flows across the battlefield like a raging torrent, catching friends and foes alike in its wake." />
+    <class level="1" book="Miniatures Handbook page 22" name="War Hulk {Prestige Class}" hd="d?" desc="The war hulk is a creature of great size and talent who is specifically trained to shock and awe opposing massed troops." />
+    <class level="1" book="Expanded Psionics Handbook page 155" name="War Mind {Prestige Class}" hd="d?" desc="War minds are expert fighters who claim to possess unequaled knowledge in the art of war." />
+    <class level="1" book="Heroes of Battle page 112" name="War Weaver {Prestige Class}" hd="d?" desc="By weaving together strands of pure arcane power, the war weaver becomes a force to be reckoned with on the battlefield." />
+    <class level="1" book="Magic of Faerun page 40" name="War Wizard Of Cormyr {Prestige Class}" hd="d?" desc="The Cormyrean war wizards are some of the most respected battle-mages in Faerun." />
+    <class level="1" book="Miniatures Handbook page 24" name="Warchief {Prestige Class}" hd="d?" desc="A warchief leads a primitive, aggressive tribe of humanoids, especially when they turn to marauding." />
+    <class level="1" book="Eberron Campaign Setting page 83" name="Warforged Juggernaut {Prestige Class}" hd="d?" desc="As a machine of war, the juggernaut is among the best at dealing damage and sustaining punishment." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 37" name="Warmaster {Prestige Class}" hd="d?" desc="Warmasters are trained at the College of War and can become a formidable presence on the battlefield." />
+    <class level="1" book="Complete Divine page 74" name="Warpriest {Prestige Class}" hd="d?" desc="Warpriests are fierce, earthy clerics who pray for peace but prepare for war." />
+    <class level="1" book="Book of Vile Darkness page 75" name="Warrior Of Darkness {Prestige Class}" hd="d?" desc="The warrior of darkness, sometimes called the dark knight, is a practitioner of black magic." />
+    <class level="1" book="Races of Faerun page 186" name="Warrior Skald {Prestige Class}" hd="d?" desc="Accompanying heroes of great renown, warrior skalds fight at their sides while composing the epics that will be told for centuries to come." />
+    <class level="1" book="Complete Warrior page 89" name="Warshaper {Prestige Class}" hd="d?" desc="The warshaper grows and evolves her own weapons and armor to suit the threat at hand." />
+    <class level="1" book="Races of Faerun page 188" name="Warsling Sniper {Prestige Class}" hd="d?" desc="The warsling sniper is an expert in the use of the weapon commonly associated with the halfling race." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 75" name="Watch Detective {Prestige Class}" hd="d?" desc="The watch detective specializes in solving mysteries." />
+    <class level="1" book="Stormwrack page 76" name="Wavekeeper {Prestige Class}" hd="d?" desc="Some druids feel the call of the primal deeps." />
+    <class level="1" book="Savage Species page 93" name="Waverider {Prestige Class}" hd="d?" desc="The waverider and her companion animal defend their city with a vigor that exceeds either's individual powers." />
+    <class level="1" book="Faiths &amp; Pantheons page 209" name="Waveservant {Prestige Class}" hd="d?" desc="Waveservants server the Bitch Queen as both tribute gatherers and enforcers." />
+    <class level="1" book="Complete Arcane page 65" name="Wayfarer Guide {Prestige Class}" hd="d?" desc="The wayfarer guide focues on honing her skill at instantaneous magical transportation." />
+    <class level="1" book="Sword and Fist: A Guidebook to Monks and Fighters page 38" name="Weapon Master {Prestige Class}" hd="d?" desc="For weapon masters, the perfection of kiis found in the mastery of a single melee weapon." />
+    <class level="1" book="Faiths &amp; Pantheons page 210" name="Wearer Of Purple {Prestige Class}" hd="d?" desc="Wearers of purple are members of the Cult of the Dragon who embrace the creation and veneration of the Sacred Ones, the great dracoliches of Faerun." />
+    <class level="1" book="Eberron Campaign Setting page 85" name="Weretouched Master {Prestige Class}" hd="d?" desc="Weretouched masters are shifters who learn to enhance their shifting ability to accentuate the power of their lycanthrope heritage." />
+    <class level="1" book="Races of the Wild page 135" name="Whisperknife {Prestige Class}" hd="d?" desc="The halfling whisperknife seeks to repay murder, theft, or humiliation in the same coin." />
+    <class level="1" book="Complete Arcane page 68" name="Wild Mage {Prestige Class}" hd="d?" desc="Wild mages aspire to cast spells without structure." />
+    <class level="1" book="Complete Adventurer page 92" name="Wild Plains Outrider {Prestige Class}" hd="d?" desc="Wild plains outriders work tirelessly to keep the plains as safe as such remote places can be." />
+    <class level="1" book="Silver Marches page 117" name="Wild Scout {Prestige Class}" hd="d?" desc="Wild scouts are the spies of the wilderness, traversing the open and wild country in search of valuable information." />
+    <class level="1" book="Complete Mage page 84" name="Wild Soul {Prestige Class}" hd="d?" desc="An arcanist who wields power from the realm of the fey." />
+    <class level="1" book="Races of the Wild page 139" name="Wildrunner {Prestige Class}" hd="d?" desc="Wildrunners give themselves almost wholly to nature, seeking to return to their untamed roots and eventually become fey creatures." />
+    <class level="1" book="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 77" name="Windrider {Prestige Class}" hd="d?" desc="The windrider is a specialist in mounted combat, but hers is no ordinary mount." />
+    <class level="1" book="Faiths &amp; Pantheons page 212" name="Windwalker {Prestige Class}" hd="d?" desc="Windwalkers learn to shape the winds with their hands and ride them to lands as yet unseen." />
+    <class level="1" book="Explorer's Handbook page 70" name="Windwright Captain {Prestige Class}" hd="d?" desc="The self-proclaimed masters of sky and sea, the windwright captains are the finest pilots of airships and wind galleons on Eberron." />
+    <class level="1" book="Frostburn page 72" name="Winterhaunt Of Iborighu {Prestige Class}" hd="d?" desc="As minions of the Frozen King, the winterhaunts of Iborighu lust for nothing less than eternal winter." />
+    <class level="1" book="Oriental Adventures page 54" name="Witch Hunter {Prestige Class}" hd="d?" desc="Witch hunters combine magical training with combat expertise to battle the spiritual forces of evil in the world." />
+    <class level="1" book="Tome of Magic page 67" name="Witch Slayer {Prestige Class}" hd="d?" desc="Witch slayers devote themselves to capturing and destroying those who share their souls with other entities." />
+    <class level="1" book="Magic of Incarnum page 162" name="Witchborn Binder {Prestige Class}" hd="d?" desc="Elite agents within the Vigilant Servants, a society whose members make it their business to frustrate the plans of the witchborn, witchborn binders are incarnum-wielding mage-hunters who can use the power of soul energy to create shields, traps, and shackles." />
+    <class level="1" book="Dragonlance Campaign Setting page 71" name="Wizard Of High Sorcery {Prestige Class}" hd="d?" desc="Once a wizard successfully completes (and survives) the Test of High Sorcery, his choices dictate his robe color and which deity of magic grants him power." />
+    <class level="1" book="Book of Exalted Deeds page 82" name="Wonderworker {Prestige Class}" hd="d?" desc="Wonderworkers sacrifice some of their spellcasting ability to grow closer to the ideal of goodness they revere." />
+    <class level="1" book="Dragon Magic page 55" name="Wyrm Wizard {Prestige Class}" hd="d?" desc="Wyrm wizards are spellcasters who learn new spells not through research and experimentation but rather by tapping into the vast wealth of arcane knowledge possessed by dragons." />
+    <class level="1" book="Oriental Adventures page 55" name="Yakuza {Prestige Class}" hd="d?" desc="Yakuza represent the shadowy underworld and provide protection for the helpless." />
+    <class level="1" book="Underdark page 46" name="Yathchol Webrider {Prestige Class}" hd="d?" desc="With their intimate understanding of webspinning and their familiarity with the Overweb, Yathchol webriders can move about the Underdark as they choose." />
+    <class level="1" book="Player's Guide to Faerun page 187" name="Yathrinshee {Prestige Class}" hd="d?" desc="Yathrinshees, the elite ranks of Kiaransalee's priests, are powerful masters of necromantic magic, both arcane and divine." />
+    <class level="1" book="Complete Psionic page 43" name="Zerth Cenobite {Prestige Class}" hd="d?" desc="The core of a zerth cenobite's studies involve strict meditation on the nature of time and the body's movements through it, culminating in a martial art known as zerthin." />
+    <class level="1" book="Lords of Darkness page 102" name="Zhentarim Skymage {Prestige Class}" hd="d?" desc="These powerful spellcasters ride strange flying beasts and serve the Zhentarim by performing acts of espionage and causing unrest on the frontiers of civilization." />
+    <class level="1" book="Player's Guide to Faerun page 77" name="Zhentarim Spy {Prestige Class}" hd="d?" desc="The Zhentarim spy is probably the one Faerunians encounter most often -- even if they never realize it." /></classes>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd35/dnd35feats.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2940 @@
+<feats>
+    <feat type="Arms and Equipment Guide page 73" name="[Creature Type] Trainer" desc="You are skilled at training a particular type  of creature." />
+    <feat type="Eberron Campaign Setting page 47" name="Aberrant Dragonmark" desc="Although you are not a recognized member of  one of the dragonmarked families, you have manifested a dragonmark." />
+    <feat type="Lords of Madness page 178" name="Aberration Banemagic" desc="You can cast spells that do additional damage  to aberrations." />
+    <feat type="Lords of Madness page 178" name="Aberration Blood" desc="One of your ancestors was an aberration and  has passed the taint of its aberrant physiology down through the generations  to you." />
+    <feat type="Lords of Madness page 178" name="Aberration Wild Shape" desc="Thanks to your heritage, you have learned to  channel your inhuman bloodline into your shapeshifting power." />
+    <feat type="Monster Manual v.3.5 page 303" name="Ability Focus" desc="Choose one of the creature's special attacks.  This attack becomes more potent than normal." />
+    <feat type="Monster Manual II page 18" name="Ability Focus" desc="One of the creature's special attacks is more  potent than normal." />
+    <feat type="Monster Manual III page 206" name="Ability Focus" desc="The special attack of a creature with this feat  is more potent than normal." />
+    <feat type="Savage Species page 30" name="Ability Focus" desc="Choose one of your spell-like abilities. This  attack becomes much more potent than normal." />
+    <feat type="Races of Destiny page 150" name="Able Learner" desc="You have great aptitude for learning." />
+    <feat type="Races of the Wild page 148" name="Able Sniper" desc="You are accomplished at remaining unseen when  you're sniping with a ranged weapon." />
+    <feat type="Unearthed Arcana page 92" name="Accurate Jaunt" desc="You have an instinctive sense of interplanar  travel." />
+    <feat type="Planar Handbook page 37" name="Acheron Flurry" desc="You master the secret technique developed by  Acheron-native special forces of limiting a foe's options in hand-to-hand  combat." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Acrobatic" desc="You have excellent body awareness and coordination." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Acrobatic" desc="You have excellent body awareness and coordination." />
+    <feat type="Eberron Campaign Setting page 47" name="Action Boost" desc="You have the ability to alter your luck drastically  in dire circumstances." />
+    <feat type="Eberron Campaign Setting page 50" name="Action Surge" desc="By spending 2 action points, you can perform  an additional action in a round." />
+    <feat type="Eberron Campaign Setting page 50" name="Adamantine Body" desc="At the cost of mobility, a warforged character's  body can be crafted with a layer of adamantine that provides formidable  protective armor and some damage reduction." />
+    <feat type="Monster Manual III page 192" name="Adamantine Body" desc="At the cost of mobility, a warforged character's  body can be crafted with a layer of adamantine that provides formidable  protective armor and some damage reduction." />
+    <feat type="Races of Eberron page 118" name="Adamantine Body" desc="At the cost of mobility, your warforged body  can be crafted with a layer of adamantine that provides formidable protective  armor and some damage reduction." />
+    <feat type="Epic Level Handbook page 50" name="Additional Magic Item Space" desc="You can wear more magic items." />
+    <feat type="Draconomicon page 67" name="Adroit Flyby Attack" desc="You can make flyby attacks and get out of reach  quickly." />
+    <feat type="Races of Eberron page 105" name="Aerenal Beastmaster" desc="As an elf of Aerenal, you consider baboons sacred  animals and they serve you obediently." />
+    <feat type="Races of the Wild page 148" name="Aerial Reflexes" desc="Your aerial agility allows you to avoid dangerous  effects while airborne." />
+    <feat type="Races of the Wild page 148" name="Aerial Superiority" desc="You can use your flying ability to gain an advantage  against landbound foes or airborne foes that you can outmaneuver." />
+    <feat type="Races of Faerun page 160" name="Aftersight" desc="You have a trace of the Sight in your blood,  which enables you to pick up echoes of the past, both wondrous and terrible." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Agile" desc="You are particularly flexible and poised." />
+    <feat type="Races of the Wild page 148" name="Agile Athlete" desc="You rely on your agility to perform athletic  feats, rather than brute strength." />
+    <feat type="Lords of Madness page 44" name="Agile Tyrant" desc="A creature with this feat develops longer, more  flexible eyestalks than its kin. This extra flexibility allows it to bring  additional eye rays to bear against its foes." />
+    <feat type="Ghostwalk page 28" name="Agony Touch" desc="Choose one physical ability score. When you  touch a creature, you can deal damage to this ability score." />
+    <feat type="Planar Handbook page 37" name="Air Heritage" desc="You are descended from creatures native to the  Elemental Plane of Air." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Alertness" desc="You have finely tuned senses." />
+    <feat type="Expanded Psionics Handbook page 41" name="Aligned Attack" desc="Your melee or ranged attack overcomes your opponent's  alignment-based damage reduction and deals additional damage." />
+    <feat type="Shining South page 19" name="Allied Defense" desc="You are good at protecting nearby allies." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Alluring" desc="Others have an inexplicable urge to believe  your every word." />
+    <feat type="Frostburn page 45" name="Altitude Adaptation" desc="Your body adapts quickly to changes in altitude,  preventing you from suffering as much from altitude sickness." />
+    <feat type="Planar Handbook page 38" name="Anarchic Heritage" desc="You are descended from creatures native to the  planes of chaos." />
+    <feat type="Races of Eberron page 105" name="Ancestral Guidance" desc="The spirit of your patron ancestor guides your  hands and thoughts in times of trouble." />
+    <feat type="Races of Stone page 136" name="Ancestral Knowledge" desc="You have a strong connection to the ancestors  of your clan, giving you understanding and knowledge beyond the mortal realms." />
+    <feat type="Book of Exalted Deeds page 39" name="Ancestral Relic" desc="You own an ancestral heirloom and can invest  it with increasing power." />
+    <feat type="Races of Faerun page 161" name="Ancestral Spirit" desc="You have ties to the long-dead spirit of one  of your clan's ancestors, who whispers ancient words of wisdom into your  mind in times of need." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Animal Affinity" desc="You are good with animals." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 20" name="Animal Control" desc="You can channel the power of nature to gain  mastery over animal creatures." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 20" name="Animal Defiance" desc="You can channel the power of nature to drive  off animals." />
+    <feat type="Book of Exalted Deeds page 41" name="Animal Friend" desc="Animals respond favorably to the aura of goodness  that exudes from you." />
+    <feat type="Races of Faerun page 161" name="Animal Friends" desc="Your ability to speak with animals has  allowed you to befriend an animal as a permanent ally." />
+    <feat type="Shining South page 19" name="Ankheg Tribe Ambush" desc="You have learned how to hide and spring to attack,  much like the ankhegs that roam the plains where you hunt." />
+    <feat type="Expanded Psionics Handbook page 41" name="Antipsionic Magic" desc="Your spells are more potent when used against  psionic characters and creatures." />
+    <feat type="Complete Warrior page 112" name="Anvil of Thunder" desc="You have mastered the style of fighting with  hammer and axes at the same time, and have learned to deal thunderous blows  with this unique pairing of weapons." />
+    <feat type="Complete Adventurer page 103" name="Appraise Magic Value" desc="Your ability to determine an item's worth and  your knowledge of magic allow you to determine the exact properties of a  magic item without the use of the identify spell or similar magic." />
+    <feat type="Dungeon Master's Guide II page 176" name="Apprentice" desc="A character with this feat has apprenticed himself  to a master in order to speed his learning and bolster his skills." />
+    <feat type="Stormwrack page 90" name="Aquatic Shot" desc="You have developed the technique of firing a  ranged weapon into or through the water with better accuracy than normal,  striking at just the right angle to allow it to slice through the obstruction  with precision." />
+    <feat type="Lords of Madness page 178" name="Aquatic Spellcasting" desc="You know how to cast spells that work equally  well in or out of water." />
+    <feat type="Races of Faerun page 161" name="Arachnid Rider" desc="You are trained in the art of employing spiders  as steeds." />
+    <feat type="Complete Arcane page 73" name="Arcane Defense" desc="Choose a school of magic, such as illusion.  You can resist spells from that school better than normal." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 38" name="Arcane Defense" desc="Choose a school of magic. You can resist spells  from that school better than normal." />
+    <feat type="Complete Divine page 79" name="Arcane Disciple" desc="Choose a deity, and then select a domain available  to clerics of that deity. You can learn to cast spells associated with that  domain as arcane spells." />
+    <feat type="Races of Destiny page 154" name="Arcane Insight" desc="By immersing yourself in the teachings of Boccob,  you have unearthed magical secrets and gained special insight into arcane  spellcasting." />
+    <feat type="Lost Empires of Faerun page 6" name="Arcane Manipulation" desc="You are learned in the arcane ways of Netheril,  where masters of magic once molded and shaped arcane energy to their own  will." />
+    <feat type="Complete Arcane page 73" name="Arcane Mastery" desc="You are quick and certain in your efforts to  defeat the arcane defenses and spells of others." />
+    <feat type="Complete Arcane page 73" name="Arcane Preparation" desc="You can prepare an arcane spell ahead of time,  just as a wizard does." />
+    <feat type="Forgotten Realms Campaign Setting page 33" name="Arcane Preparation" desc="You can prepare an arcane spell ahead of time,  just as a wizard does." />
+    <feat type="Player's Guide to Faerun page 32" name="Arcane Preparation" desc="You can prepare an arcane spell ahead of time,  just as a wizard does." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 38" name="Arcane Preparation" desc="You can prepare an arcane spell ahead of time,  just as a wizard does." />
+    <feat type="Forgotten Realms Campaign Setting page 33" name="Arcane Schooling" desc="In your homeland, all who show some skill at  the Art may receive training as a wielder of magic." />
+    <feat type="Player's Guide to Faerun page 33" name="Arcane Schooling" desc="In your homeland, all who show some skill at  the Art may receive training as arcane spellcasters." />
+    <feat type="Complete Warrior page 96" name="Arcane Strike" desc="You can channel arcane energy into your melee  attacks." />
+    <feat type="Lost Empires of Faerun page 6" name="Arcane Transfiguration" desc="Drawing upon forgotten lore, you broaden your  arcane studies and master a school of magic previously prohibited to you." />
+    <feat type="Races of Faerun page 161" name="Arctic Adaptation" desc="You have adapted to the snowbound environment  of the arctic reaches of Faerun." />
+    <feat type="Savage Species page 30" name="Area Attack" desc="You can wield improvised weapons to attack several  spaces at once." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Armor Proficiency (Heavy)" desc="You are proficient with heavy armor." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Armor Proficiency (Light)" desc="You are proficient with light armor." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Armor Proficiency (Medium)" desc="You are proficient with medium armor." />
+    <feat type="Complete Warrior page 151" name="Armor Skin" desc="Your skin becomes like armor." />
+    <feat type="Epic Level Handbook page 50" name="Armor Skin" desc="Your skin becomes like armor." />
+    <feat type="Oriental Adventures page 60" name="Art of Fascination" desc="You claim descent from Kakita Wayozu, whose  art was so great it is said that she helped create an alternate world." />
+    <feat type="Complete Warrior page 96" name="Arterial Strike" desc="Your sneak attacks target large blood vessels,  leaving wounds that cause massive blood loss." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Arterial Strike" desc="Your sneak attacks target large blood vessels,  leaving wounds that cause massive blood loss." />
+    <feat type="Frostburn page 46" name="Artic Priest" desc="You can swap out prepared spells to cast spell  to aid in exploring and surviving in frostfell areas." />
+    <feat type="Forgotten Realms Campaign Setting page 33" name="Artist" desc="You come from a culture in which the arts, philosophy,  and music have a prominent place in society." />
+    <feat type="Oriental Adventures page 61" name="Artist" desc="You claim descent from Doji, who was known as  a creator of culture and civilization." />
+    <feat type="Player's Guide to Faerun page 33" name="Artist" desc="Your people are renowned for their skill at  story and song." />
+    <feat type="Complete Adventurer page 105" name="Ascetic Hunter" desc="You have gone beyond the bounds of your monastic  training to incorporate new modes of bringing the unlawful to justice." />
+    <feat type="Complete Adventurer page 105" name="Ascetic Knight" desc="You belong to a special order of religious monks  that teaches its adherents that self-enlightenment and honorable service  grow from the same well of purity." />
+    <feat type="Complete Adventurer page 105" name="Ascetic Magic" desc="You practice an unusual martial art that mixes  self-taught spellcasting and melee attacks to great effect." />
+    <feat type="Complete Adventurer page 106" name="Ascetic Rogue" desc="You have gone beyond the bounds of your monastic  training to incorporate new modes of stealthy combat." />
+    <feat type="Eberron Campaign Setting page 50" name="Ashbound" desc="You have been trained in the druidic traditions  of the Ashbound, seeing yourself as one of nature's avengers." />
+    <feat type="Savage Species page 30" name="Assume Supernatural Ability" desc="You learn to use a supernatural ability of an  assumed form." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Athletic" desc="You have a knack for athletic endeavors." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Athletic" desc="You're physically fit and adept at outdoor sports." />
+    <feat type="Oriental Adventures page 61" name="Attention to Detail" desc="You are descended from Akodo's advisor Ikoma  -- a historian, judge, and storyteller." />
+    <feat type="Magic of Faerun page 21" name="Attune Gem" desc="You can magically imbue gems to hold a spell  until triggered." />
+    <feat type="Eberron Campaign Setting page 50" name="Attune Magic Weapon" desc="Through your study of magic weapons, you have  become adept at eking every advantage out of their enhanced qualities." />
+    <feat type="Complete Divine page 79" name="Augment Healing " desc="You can increase your healing ability." />
+    <feat type="Magic of Faerun page 21" name="Augment Summoning" desc="Your summoned creatures are better than normal." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Augment Summoning" desc="Your summoned creatures are more powerful than  normal." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 39" name="Augment Summoning" desc="Your summoned creatures are more powerful than  normal." />
+    <feat type="Complete Adventurer page 191" name="Augmented Alchemy" desc="You can create alchemical items and substances  that are much more powerful than normal." />
+    <feat type="Epic Level Handbook page 50" name="Augmented Alchemy" desc="You can create alchemical items and substances  that are much more powerful than normal." />
+    <feat type="Races of Stone page 136" name="Auspicious Marking" desc="Your [goliath] skin patterns indicate that fate  has marked you for greatness, and the patterns shift slowly to take new  forms." />
+    <feat type="Complete Arcane page 191" name="Automatic Quicken Spell" desc="You can cast any of your lesser spells with  a moment's thought." />
+    <feat type="Epic Level Handbook page 50" name="Automatic Quicken Spell" desc="You can cast any of your lesser spells with  a moment's thought." />
+    <feat type="Complete Arcane page 191" name="Automatic Silent Spell" desc="You can cast any of your lesser spells silently." />
+    <feat type="Epic Level Handbook page 51" name="Automatic Silent Spell" desc="You can cast any of your lesser spells silently." />
+    <feat type="Complete Arcane page 191" name="Automatic Still Spell" desc="You can cast any of your lesser spells without  gestures." />
+    <feat type="Epic Level Handbook page 51" name="Automatic Still Spell" desc="You can cast any of your lesser spells without  gestures." />
+    <feat type="Expanded Psionics Handbook page 41" name="Autonomous" desc="You have a knack for psionic self-sufficiency." />
+    <feat type="Draconomicon page 67" name="Awaken Frightful Presence" desc="You gain frightful presence." />
+    <feat type="Draconomicon page 67" name="Awaken Spell Resistance" desc="You gain spell resistance." />
+    <feat type="Monster Manual v.3.5 page 303" name="Awesome Blow" desc="The creature can choose to deliver blows that  send its smaller opponents flying like bowling pins." />
+    <feat type="Monster Manual III page 206" name="Awesome Blow" desc="A creature with this feat can choose to deliver  blows that send its smaller opponents flying like bowling pins." />
+    <feat type="Underdark page 24" name="Axeshield" desc="You know how to defend yourself with a battleaxe." />
+    <feat type="Races of Stone page 137" name="Axespike" desc="You have mastered the art of fighting in spiked  armor while wielding a greataxe. You blend greataxe blows and armor spike  attacks into one constant, deadly attack form." />
+    <feat type="Player's Guide to Faerun page 33" name="Axethrower" desc="You have learned how to hurl weapons to deadly  effect." />
+    <feat type="Planar Handbook page 38" name="Axiomatic Heritage" desc="You are descended from creatures native to the  planes of law." />
+    <feat type="Complete Warrior page 96" name="Axiomatic Strike" desc="You can turn your fist into an instrument of  law." />
+    <feat type="Player's Guide to Faerun page 135" name="Axiomatic Strike" desc="Your attacks deal incredible damage to chaotic  creatures." />
+    <feat type="Races of Faerun page 161" name="Azerblood" desc="You are descended from the shield dwarves of  Clan Azerkyn, who once ruled the Adamant Kingdom of Xothaerin beneath western  Amn. The blood of the azer runs thick in your veins." />
+    <feat type="Libris Mortis: The Book of the Dead page 24" name="Baleful Moan" desc="Your hollow cry strikes fear into the hearts  of the living." />
+    <feat type="Heroes of Battle page 96" name="Ballista Proficiency" desc="You have trained in ballista operation." />
+    <feat type="Epic Level Handbook page 51" name="Bane of Enemies" desc="Your attacks deal great damage to your favored  enemies." />
+    <feat type="Lords of Madness page 44" name="Bane of the Unclean" desc="A creature with this feat hates aberrant beholders  so strongly that it gains bonuses when fighting them." />
+    <feat type="Serpent Kingdoms page 144" name="Barbed Stinger" desc="Your stinger is unusually difficult to dislodge." />
+    <feat type="Races of Faerun page 161" name="Batrider" desc="You are highly skilled in the art of flying  dire bats, a common form of transportation among the shield dwarves of the  Far Hills." />
+    <feat type="Complete Arcane page 75" name="Battle Caster" desc="Building on your existing training allows you  to avoid the chance of arcane spell failure when you wear armor heavier  than normal." />
+    <feat type="Races of the Wild page 148" name="Battle Casting" desc="You have a knack for staying out of harm's way  when casting spells." />
+    <feat type="Races of Stone page 137" name="Battle Hardened" desc="Your extensive battle experience has left you  incredibly calm and composed, even in the heat of battle." />
+    <feat type="UE page 42" name="Battle Jump" desc="You know how to launch a devastating attack  from above by dropping onto your opponent." />
+    <feat type="Miniatures Handbook page 25" name="Battlefield Inspiration" desc="You inspire courage in your allies." />
+    <feat type="Races of Eberron page 116" name="Battleshifter Training" desc="Your shifter fighting instincts grant you a  sophisticated blend of defensive techniques and controlled attacks." />
+    <feat type="Complete Warrior page 112" name="Bear Fang" desc="You have mastered the fierce style of fighting  with axe and dagger at the same time." />
+    <feat type="Epic Level Handbook page 51" name="Beast Companion" desc="You can befriend a beast." />
+    <feat type="Eberron Campaign Setting page 50" name="Beast Shape" desc="You call upon the power of your beast totem  to physically change your form." />
+    <feat type="Eberron Campaign Setting page 51" name="Beast Totem" desc="In the druidic custom of your people, you have  claimed a kind of magical beast as your totem -- a patron, protector, and  source of strength." />
+    <feat type="Epic Level Handbook page 51" name="Beast Wild Shape" desc="You can wild shape into magical beast form." />
+    <feat type="Eberron Campaign Setting page 51" name="Beasthide Elite" desc="Your shifter trait improves." />
+    <feat type="Frostburn page 47" name="Beckon the Frozen" desc="Creatures you summon are infused with cold energy  and have the cold subtype." />
+    <feat type="Lords of Madness page 179" name="Bestial Hide" desc="Your skin is thicker, scalier, or furrier than  normal." />
+    <feat type="Eberron Campaign Setting page 51" name="Bind Elemental" desc="You can craft magic items that use bound elementals  for special effects, including weapons, armor, airships, and elemental galleons." />
+    <feat type="Complete Arcane page 75" name="Black Lore of Moil" desc="Your study of the sinister knowledge and spellcasting  techniques of the long-dead Nightlords of Moil makes your necromancy spells  especially potent." />
+    <feat type="Stormwrack page 91" name="Blackwater Invocation" desc="You can call upon negative energy to infuse  normal water around you, transforming it into the dark, cold water found  at the bottom of the deepest ocean trenches." />
+    <feat type="Races of Eberron page 107" name="Bladebearer of the Valenar" desc="Your extensive training makes you especially  adept with the curved blades of the Valenar." />
+    <feat type="Unearthed Arcana page 92" name="Bladeproof Skin" desc="Your skin has a degree of protection from even  the sharpest edge." />
+    <feat type="Sandstorm page 49" name="Blazing Berserker" desc="When you enter your rage, your body becomes  infused with fire." />
+    <feat type="Sandstorm page 49" name="Blessed by Tem-Et-Nu" desc="Tem-Et-Nu has marked you as having an important  destiny in her temple." />
+    <feat type="Player's Guide to Faerun page 176" name="Blessed of the Seven Sisters" desc="As a result of a personal connection to one  of the Seven Sisters, you have a taste of Mystra's special favor." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Blind-Fight" desc="You know how to fight in melee without being  able to see your foes." />
+    <feat type="Epic Level Handbook page 51" name="Blinding Speed" desc="You can trigger short bursts of great speed." />
+    <feat type="Complete Adventurer page 114" name="Blindsense" desc="You can sense creatures that you cannot see." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 21" name="Blindsight" desc="Your senses are as keen as the bat's." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 5" name="Blindsight, 5-Foot Radius" desc="You sense opponents in the darkness." />
+    <feat type="Deities and Demigods page 49" name="Blindsight, 5-Foot Radius" desc="The deity senses opponents in the darkness." />
+    <feat type="Heroes of Battle page 96" name="Block Arrow" desc="You can block incoming arrows with your shield." />
+    <feat type="Races of Faerun page 161" name="Blood of the Warlord" desc="You can influence a large number of orcs." />
+    <feat type="Oriental Adventures page 61" name="Blood Sorcerer" desc="You are descended from Yogo, the Scorpion shugenja  who was the first guardian of the Black Scrolls of Fu Leng." />
+    <feat type="Forgotten Realms Campaign Setting page 33" name="Blooded" desc="Enemies find it difficult to catch you off guard." />
+    <feat type="Player's Guide to Faerun page 35" name="Blooded" desc="You know what it means to fight for your life,  and you understand the value of quick wits and quicker reactions when blades  are bared and deadly spells are chanted." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Bloodline of Fire" desc="You are descended from the efreet who ruled  Calimshan for two millennia." />
+    <feat type="Player's Guide to Faerun page 35" name="Bloodline of Fire" desc="You are descended from the efreet who ruled  Calimshan long ago." />
+    <feat type="Champions of Ruin page 17" name="Bloodsoaked Intimidate" desc="Your bloody and vicious approach to combat makes  you a fearsome foe." />
+    <feat type="Savage Species page 31" name="Blowhard" desc="You can blow targets over with your breath." />
+    <feat type="Complete Divine page 79" name="Boar's Ferocity" desc="You can continue fighting even at the brink  of death." />
+    <feat type="Expanded Psionics Handbook page 41" name="Body Fuel" desc="You can expand your power point total at the  expense of your health." />
+    <feat type="Serpent Kingdoms page 144" name="Body Pouch" desc="You can open a cavity in your body without harm  to yourself and use it to carry or conceal items or creatures." />
+    <feat type="Libris Mortis: The Book of the Dead page 25" name="Bolster Resistance" desc="Undead you raise or create are more resistant  to turning than normal." />
+    <feat type="Savage Species page 31" name="Bonus Breath" desc="You can use your breath weapons one more time  per day than you normally could." />
+    <feat type="Complete Divine page 89" name="Bonus Domain" desc="You have access to one additional domain of  spells." />
+    <feat type="Epic Level Handbook page 51" name="Bonus Domain" desc="You have access to one additional domain of  spells." />
+    <feat type="Races of Eberron page 108" name="Boomerang Daze" desc="You can daze the targets of your boomerang attacks." />
+    <feat type="Races of Eberron page 108" name="Boomerang Ricochet" desc="You can strike up to two foes with a single  boomerang throw." />
+    <feat type="Expanded Psionics Handbook page 43" name="Boost Construct" desc="Your astral constructs have more abilities." />
+    <feat type="Book of Vile Darkness page 47" name="Boost Spell Resistance" desc="By making a deal with an evil power, the character  makes himself even more resistant to magic." />
+    <feat type="Book of Vile Darkness page 47" name="Boost Spell-Like Ability" desc="One of the creature's spell-like abilities is  harder to resist than it otherwise would be." />
+    <feat type="Oriental Adventures page 61" name="Born Duelist" desc="You claim descent from Mirumoto, one of the  first two samurai to join Togashi in his meditative retreat." />
+    <feat type="Races of the Wild page 148" name="Born Flyer" desc="You can fly as though born to do it." />
+    <feat type="Complete Arcane page 76" name="Born of the Three Thunders" desc="You have learned to marry the power of lightning  and thunder in your electricity and sonic spells." />
+    <feat type="Underdark page 24" name="Bowslinger" desc="You can ready ranged weapons surprisingly quickly." />
+    <feat type="Complete Adventurer page 106" name="Brachiation" desc="You can swing through trees like a monkey." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 21" name="Brachiation" desc="You move through trees like a monkey." />
+    <feat type="Unearthed Arcana page 92" name="Breadth of Knowledge" desc="Your time spent plumbing the depths of magic  knowledge has resulted in a treasure trove of obscure facts." />
+    <feat type="Races of Faerun page 161" name="Breathing Link" desc="You can allow a person adjacent to you to breathe  water." />
+    <feat type="Stormwrack page 92" name="Breathing Link" desc="You can allow a person adjacent to you to breathe  water." />
+    <feat type="Player's Handbook v.3.5 page 89" name="Brew Potion" desc="You can create potions, which carry spells within  themselves." />
+    <feat type="Races of Destiny page 150" name="Bright Sigil" desc="You have established a greater degree of control  over your sigils. When you concentrate, you can emit strong illumination  from the glowing symbols that surround your head." />
+    <feat type="Complete Adventurer page 106" name="Brutal Throw" desc="You have learned how to hurl weapons to deadly  effect." />
+    <feat type="Races of Eberron page 116" name="Brute Fighting" desc="Your extensive training with two-handed weapons  is revealed through brutally effective tactics." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Bullheaded" desc="The stubbornness and determination of your kind  is legendary." />
+    <feat type="Player's Guide to Faerun page 37" name="Bullheaded" desc="The stubbornness and determination of your kind  are legendary." />
+    <feat type="Epic Level Handbook page 51" name="Bulwark of Defense" desc="Your defensive stance bonuses increase." />
+    <feat type="Races of Stone page 137" name="Burrow Friend" desc="Your natural rapport with burrowing mammals  improves." />
+    <feat type="Expanded Psionics Handbook page 43" name="Burrowing Power" desc="Your powers sometimes bypass barriers." />
+    <feat type="Races of Faerun page 161" name="Calishite Elementalist" desc="You are a student of the Calishite tradition  of elemental magic and have mastered its mysterious lore. You may choose  to specialize in air magic or fire magic." />
+    <feat type="Races of Eberron page 108" name="Call of the Undying" desc="You call upon the power of the Undying Court  to instantly recall a previously cast spell." />
+    <feat type="Races of Faerun page 162" name="Caravanner" desc="You are skilled at leading caravans along established  trade routes." />
+    <feat type="Races of the Wild page 148" name="Catfolk Pounce" desc="You can rush unaware foes and deliver several  attacks before they have a chance to respond." />
+    <feat type="Underdark page 24" name="Caustic Adaptation" desc="Long have your ancestors hunted and been hunted  in the depths. Natural selection has given your blood an unpalatable, acidic  quality." />
+    <feat type="Complete Warrior page 108" name="Cavalry Charger" desc="Fighting from the back of a steed is second  nature to you." />
+    <feat type="Underdark page 24" name="Caver" desc="You are knowledgeable about the secrets of the  subterranean world and wise in its ways." />
+    <feat type="Races of Faerun page 162" name="Celestial Bloodline" desc="Some of your latent abilities have matured." />
+    <feat type="Book of Exalted Deeds page 41" name="Celestial Familiar" desc="As long as you are able to acquire a new familiar,  you may receive a celestial as a familiar." />
+    <feat type="Planar Handbook page 38" name="Celestial Heritage" desc="You are descended from creatures native to the  Upper Planes" />
+    <feat type="Book of Exalted Deeds page 42" name="Celestial Mount" desc="Your special mount is a true creature of the  heavens." />
+    <feat type="Planar Handbook page 38" name="Celestial Summoning Specialist" desc="You can select from a larger number of options  when summoning good creatures." />
+    <feat type="Races of the Wild page 148" name="Centaur Trample" desc="You have trained to use your large body and  unique physiology against your foes. You have learned how to knock down  opponents and ride over them in combat." />
+    <feat type="Expanded Psionics Handbook page 44" name="Chain Power" desc="You can manifest powers that arc to hit other  targets in addition to the primary target." />
+    <feat type="Complete Arcane page 76" name="Chain Spell" desc="You can cast spells that arc to other targets  in addition to your primary target." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 39" name="Chain Spell" desc="You can cast spells that arc to other targets  in addition to your primary target." />
+    <feat type="Champions of Ruin page 17" name="Chakram Ricochet" desc="You can hurl a chakram so that it strikes two  enemies instead of one." />
+    <feat type="Serpent Kingdoms page 144" name="Chameleon Hide" desc="You can alter the hue of your scales to match  the surrounding terrain." />
+    <feat type="Lost Empires of Faerun page 7" name="Channel Charge" desc="You can power a charged magic item with your  own magical ability." />
+    <feat type="WL page 13" name="Channel Legacy" desc="You can call upon the hidden strength within  your legacy item to empower yourself for a single spectacular effort." />
+    <feat type="Races of Destiny page 150" name="Channeled Rage" desc="You can focus your rage to counter charms and  compulsions." />
+    <feat type="Complete Adventurer page 113" name="Chant of Fortitude" desc="You can channel the power of your bardic music  to sustain your allies, allowing them to function even after receiving wounds  that would cause others to falter." />
+    <feat type="Expanded Psionics Handbook page 44" name="Chaotic Mind" desc="The turbulence of your thoughts prevents others  from gaining insight into your actions." />
+    <feat type="Epic Level Handbook page 51" name="Chaotic Rage" desc="Your rage is particularly damaging to lawful  creatures." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 78" name="Chariot Archery" desc="You are skilled at using ranged weapons from  a chariot." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 79" name="Chariot Charge" desc="You are skilled at charging with your chariot." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 78" name="Chariot Combat" desc="You are skilled in chariot combat." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 79" name="Chariot Sideswipe" desc="You are skilled at using your chariot's scythe  blades against foes." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 78" name="Chariot Trample" desc="You are trained in using your chariot to knock  down opponents." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Charlatan" desc="You're adept at fooling people. You know how  to tell them just what they want to hear." />
+    <feat type="Serpent Kingdoms page 145" name="Charm Immunity" desc="You are immune to charm effects." />
+    <feat type="Serpent Kingdoms page 145" name="Charm Resistance" desc="You can resist charm effects better than you  otherwise could." />
+    <feat type="Shining South page 19" name="Cheetah Tribe Sprint" desc="You have learned the secret of lightning-fast  running from the cheetah that roams the plains where you live." />
+    <feat type="Complete Divine page 79" name="Cheetah's Speed" desc="You can run with the speed of the cheetah." />
+    <feat type="Eberron Campaign Setting page 51" name="Child of Winter" desc="You are trained in the druidic traditions of  the Children of Winter, an Eldeen Reaches sect that embraces death and decay." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Chink in the Armor" desc="You are an expert at slipping a weapon between  armor plates or into seams." />
+    <feat type="Oriental Adventures page 61" name="Choke Hold" desc="You have learned the correct way to apply pressure  to render an opponent unconscious." />
+    <feat type="Races of Faerun page 162" name="Chondathan Missionary" desc="Your training has emphasized spells that help  you spread the word of your faith." />
+    <feat type="Frostburn page 47" name="Chosen of Iborighu" desc="You gain features that identify you as an ally  to the church of Iborighu and grant you supernatural qualities." />
+    <feat type="Player's Guide to Faerun page 135" name="Chosen Weapon Specialization" desc="You deal more damage than normal when wielding  your deity's chosen weapon." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 5" name="Circle Kick" desc="You kick multiple opponents with the same attack  action." />
+    <feat type="Ghostwalk page 29" name="Circle Magic" desc="You know how to use your connection to Galaedros  the Wood God to channel magical power to another spellcaster of your faith." />
+    <feat type="Races of Destiny page 150" name="City Slicker" desc="You are very familiar with city life and the  inner workings of your hometown." />
+    <feat type="Races of Stone page 137" name="Clan Prestige" desc="Your actions have brought you some measure of  fame and respect from your clan, whether from battle prowess or years of  service to the clan." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Cleave" desc="You can follow through with powerful blows." />
+    <feat type="Complete Warrior page 97" name="Clever Wrestling " desc="You have a better than normal chance to escape  or wriggle free from a big creature's grapple or pin." />
+    <feat type="Draconomicon page 103" name="Clever Wrestling" desc="You have a better than normal chance to escape  or wriggle free from a big creature's grapple or pin." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Clever Wrestling" desc="You have a better than normal chance to escape  or wriggle free from a big creature's grapple or pin." />
+    <feat type="Stormwrack page 92" name="Clever Wrestling" desc="You have a better than normal chance to escape  or wriggle free from a big creature's grapple or pin." />
+    <feat type="Eberron Campaign Setting page 52" name="Cliffwalk Elite" desc="Your shifter trait improves." />
+    <feat type="Races of Eberron page 113" name="Cliffwalk Elite" desc="Your cliffwalk shifter trait improves." />
+    <feat type="Complete Adventurer page 114" name="Climb Like an Ape" desc="You can improve your climbing ability." />
+    <feat type="Draconomicon page 67" name="Clinging Breath" desc="Your breath weapon clings to creatures and continues  to affect them in the round after you breath." />
+    <feat type="Expanded Psionics Handbook page 44" name="Cloak Dance" desc="You are skilled at using optical tricks to make  yourself seem to be where you are not." />
+    <feat type="Expanded Psionics Handbook page 44" name="Closed Mind" desc="Your mind is better able to resist psionics  than normal." />
+    <feat type="Races of Faerun page 162" name="Close-Quarters Fighting" desc="You are skilled at resisting grapple attacks  from creatures that usually grapple opponents." />
+    <feat type="Complete Warrior page 97" name="Close-Quarters Fighting" desc="You are skilled at fighting at close range and  resisting grapple attempts." />
+    <feat type="Draconomicon page 103" name="Close-Quarters Fighting" desc="You are skilled at fighting at close range and  resisting grapple attempts." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 5" name="Close-Quarters Fighting" desc="You are skilled at fighting at close range and  resisting grapple attacks." />
+    <feat type="Serpent Kingdoms page 145" name="Cobra Head" desc="You can extend the skin of your neck into a  cobra hood." />
+    <feat type="Frostburn page 47" name="Cold Endurance" desc="You can exist with ease in low-temperature environments." />
+    <feat type="Frostburn page 47" name="Cold Focus" desc="Your cold spells are more potent than normal." />
+    <feat type="Races of Eberron page 119" name="Cold Iron Tracery" desc="Cold-forged iron that runs through your body  allows you to overcome the supernatural defenses of certain creatures and  protecting against some magical attacks." />
+    <feat type="Frostburn page 47" name="Cold Spell Specialization" desc="You do additional damage with cold spells." />
+    <feat type="Complete Arcane page 181" name="Collegiate Wizard" desc="You have undergone extensive training in a formal  school for wizards." />
+    <feat type="Epic Level Handbook page 52" name="Colossal Wild Shape" desc="You can wild shape into animals of Colossal  size." />
+    <feat type="Complete Warrior page 151" name="Combat Archery" desc="You can fire a bow in melee safely." />
+    <feat type="Epic Level Handbook page 52" name="Combat Archery" desc="You can fire a bow in melee safely." />
+    <feat type="Complete Warrior page 110" name="Combat Brute" desc="You employ strength and leverage to great effect  in battle." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Combat Casting" desc="You are adept at casting spells in combat." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Combat Expertise" desc="You are trained at using your combat skill for  defense as well as offense." />
+    <feat type="Complete Warrior page 151" name="Combat Insight" desc="Your keen intellect allows you to place melee  attacks where they will deal the most damage." />
+    <feat type="Complete Adventurer page 106" name="Combat Intuition" desc="Your keen understanding of your opponent's moves  and your instinctive feel for the flow of combat enable you to shrewdly  assess your opponent's combat capabilities." />
+    <feat type="Expanded Psionics Handbook page 44" name="Combat Manifestation" desc="You are adept at manifesting powers in combat." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Combat Reflexes" desc="You can respond quickly and repeatedly to opponents  who let their defenses down." />
+    <feat type="Complete Arcane page 76" name="Communicator" desc="You possess a magical understanding of the essence  of language." />
+    <feat type="Races of Destiny page 150" name="Complementary Insight" desc="You get more out of having skills that work  well together." />
+    <feat type="Unearthed Arcana page 92" name="Conductivity" desc="You have crude control over electricity effects  near you." />
+    <feat type="Races of the Wild page 153" name="Confound the Big Folk" desc="You excel when battling foes bigger than you  are." />
+    <feat type="Book of Exalted Deeds page 42" name="Consecrate Spell" desc="You can imbue your spells with the raw energy  of good, by the grace of a celestial power." />
+    <feat type="Complete Divine page 79" name="Consecrate Spell" desc="You can imbue your spells with the raw energy  of good." />
+    <feat type="Book of Exalted Deeds page 42" name="Consecrate Spell Trigger" desc="You can channel holy power through a spell trigger  item, such as a wand or staff." />
+    <feat type="Book of Exalted Deeds page 42" name="Consecrate Spell-Like Ability" desc="You can channel holy power into your spell-like  abilities." />
+    <feat type="Races of Eberron page 119" name="Construct Lock" desc="Your knowledge of construct nature allows you  to deal extra damage to or even immobilize such foes." />
+    <feat type="Libris Mortis: The Book of the Dead page 25" name="Contagious Paralysis" desc="Your paralyzing attack is contagious." />
+    <feat type="Ghostwalk page 29" name="Control Visage" desc="Your ghost body is shaped as if you were alive  and unharmed, and you can control what your ghost body appears to wear." />
+    <feat type="Unearthed Arcana page 92" name="Controlled Immolation" desc="If you catch on fire, the flames don't hurt  you." />
+    <feat type="Savage Species page 31" name="Controlled Respiration" desc="You can stay out of water longer than you otherwise  could." />
+    <feat type="Oriental Adventures page 61" name="Cool Head" desc="You are descended from the great diplomat Ide,  who was chosen to be the voice of Shinjo in all dealings with strangers." />
+    <feat type="Complete Arcane page 76" name="Cooperative Spell" desc="You can cast spells to greater effect in conjunction  with the same spell cast by another individual." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 39" name="Cooperative Spell" desc="You can cast spells to greater effect in conjunction  with the same spell cast by another individual." />
+    <feat type="Heroes of Battle page 96" name="Coordinated Shot" desc="You are extraordinarily talented at making ranged  attacks past your allies." />
+    <feat type="Races of the Wild page 149" name="Coordinated Strike" desc="You and your animal companion or special mount  can coordinate your melee attacks to gain an advantage in combat." />
+    <feat type="Lost Empires of Faerun page 7" name="Cormanthyran Moon Magic" desc="You have mastered the ancient elven techniques  of drawing power from Sehanine Moonbow's light." />
+    <feat type="Dragonlance Campaign Setting page 85" name="Cornered Rat" desc="You can go from piteous groveling to a murderous  fury in the blink of an eye." />
+    <feat type="Ghostwalk page 29" name="Corpse Malevolence" desc="You can possess and animate dead bodies." />
+    <feat type="Libris Mortis: The Book of the Dead page 25" name="Corpsecrafter" desc="Undead you raise or create are tougher than  normal." />
+    <feat type="Ghostwalk page 29" name="Corrupt Arcane Studies" desc="You have dabbled in strange magic that has increased  your power but adversely affected your mind." />
+    <feat type="Book of Vile Darkness page 47" name="Corrupt Spell" desc="The character can transform one of her spells  into a thing of evil due to a deal she makes with an evil power." />
+    <feat type="Complete Divine page 79" name="Corrupt Spell" desc="You can transform one of your spells into an  evil version of itself." />
+    <feat type="Champions of Ruin page 17" name="Corrupt Spell" desc="You can transform one of your spells into a  thing of evil due to a deal you make with an evil power." />
+    <feat type="Book of Vile Darkness page 48" name="Corrupt Spell-Like Ability" desc="One of the creature's spell-like abilities is  powered by evil. A dark pact provides the creature with unholy energy." />
+    <feat type="Libris Mortis: The Book of the Dead page 25" name="Corrupted Wild Shape" desc="You have learned to use the necromantic energy  that powers your undead form to overcome the inability of undead creatures  to wild shape." />
+    <feat type="Ghostwalk page 29" name="Corrupting Touch" desc="Your touch can damage creatures." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Cosmopolitan" desc="Your exposure to the thousand forking paths  of the city has taught you things you ordinarily would never have uncovered." />
+    <feat type="Player's Guide to Faerun page 37" name="Cosmopolitan" desc="You've been lied to more times than you can  count." />
+    <feat type="Complete Adventurer page 114" name="Cougar's Vision" desc="You can see in the dark like a cat." />
+    <feat type="Heroes of Battle page 97" name="Courageous Rally" desc="You can rally demoralized foes with your bardic  music." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Courteous Magocracy" desc="You were raised in a land where mighty wizards  order affairs." />
+    <feat type="Shining South page 20" name="Cover Your Tracks" desc="You are good at masking your route, making it  difficult for others to track you." />
+    <feat type="Lords of Madness page 22" name="Craft Aboleth Glyph" desc="An aboleth with this feat can create magic glyphs  that store spells or have specialized effects of their own." />
+    <feat type="Unearthed Arcana page 99" name="Craft Alchemical Item" desc="You are capable of creating alchemical items  and substances." />
+    <feat type="Expanded Psionics Handbook page 44" name="Craft Cognizance Crystal" desc="You can create psionic cognizance crystals  that store power points." />
+    <feat type="Monster Manual v.3.5 page 303" name="Craft Construct" desc="The creature can create golems and other magic  automatons that obey its orders." />
+    <feat type="Monster Manual III page 206" name="Craft Construct" desc="A creature with this feat can create golems  and other magic automatons that obey its orders" />
+    <feat type="Complete Arcane page 77" name="Craft Contingent Spell" desc="You know how to attach semipermanent spells  to a creature and set them to activate under certain conditions." />
+    <feat type="UE page 42" name="Craft Contingent Spell" desc="You know how to create contingent spells, which  are semipermanent spells that can be 'worn' and activated under certain  conditions." />
+    <feat type="Oriental Adventures page 61" name="Craft Crystal Weapon" desc="You can create magic weapons from Kuni crystal,  which is deadly to creatures of the Shadowlands." />
+    <feat type="Expanded Psionics Handbook page 44" name="Craft Dorje" desc="You can create slender crystal wands called  dorjes that manifest powers when charges are expended." />
+    <feat type="Epic Level Handbook page 52" name="Craft Epic Magic Arms and Armor" desc="You can craft magic arms and armor of epic power." />
+    <feat type="Epic Level Handbook page 52" name="Craft Epic Rod" desc="You can craft magic rods of epic power." />
+    <feat type="Epic Level Handbook page 52" name="Craft Epic Staff" desc="You can craft magic staffs of epic power." />
+    <feat type="Epic Level Handbook page 52" name="Craft Epic Wondrous Item" desc="You can craft wondrous items of epic power." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Craft Magic Arms and Armor" desc="You can create magic weapons, armor, and shields." />
+    <feat type="Unearthed Arcana page 99" name="Craft Masterwork Armor" desc="You are trained in the creation of fine armor  and shields." />
+    <feat type="Unearthed Arcana page 99" name="Craft Masterwork Ranged Weapon" desc="You are trained in the creation of fine ranged  weapons and ammunition." />
+    <feat type="Unearthed Arcana page 99" name="Craft Masterwork Weapon" desc="You are trained in the creation of fine melee  and thrown weapons." />
+    <feat type="Expanded Psionics Handbook page 44" name="Craft Psicrown" desc="You can create psicrowns, which have multiple  psionic effects." />
+    <feat type="Expanded Psionics Handbook page 44" name="Craft Psionic Arms and Armor" desc="You can create psionic weapons, armor, and shields." />
+    <feat type="Expanded Psionics Handbook page 45" name="Craft Psionic Construct" desc="You can create golems and other psionic automatons  that obey your orders." />
+    <feat type="Lords of Madness page 69" name="Craft Psionic Seal" desc="A creature with this feat can create psionic  glyphs or symbols that hold spells or psionic powers until triggered." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Craft Rod" desc="You can create magic rods, which have varied  magical effects." />
+    <feat type="Races of Stone page 137" name="Craft Rune Circle" desc="You can create rune circles, stationary magic  items that hold a variety of spells and effects." />
+    <feat type="Lost Empires of Faerun page 8" name="Craft Scepter" desc="You know the ancient Netherese secret of creating  magic scepters." />
+    <feat type="Frostburn page 47" name="Craft Skull Talisman" desc="You can create skull talismans, which carry  spells within themselves." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Craft Staff" desc="You can create magic staffs, each of which has  multiple magical effects." />
+    <feat type="Oriental Adventures page 61" name="Craft Talisman" desc="You can create magic fetishes, single-use magic  items that hold spells until triggered." />
+    <feat type="Expanded Psionics Handbook page 45" name="Craft Universal Item" desc="You can create universal psionic items." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Craft Wand" desc="You can create wands, which hold spells." />
+    <feat type="Player's Handbook v.3.5 page 92" name="Craft Wondrous Item" desc="You can create a wide variety of magic items." />
+    <feat type="Champions of Ruin page 17" name="Craven" desc="Like most sly rogues, you are a dangerous coward.  However, your sneak attacks deal more damage than normal." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Create Infusion" desc="You store a divine spell within a specially  prepared herb." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Create Portal" desc="You have learned the ancient craft of creating  a portal." />
+    <feat type="Complete Warrior page 113" name="Crescent Moon" desc="You have mastered the style of fighting with  sword and dagger." />
+    <feat type="Races of Destiny page 156" name="Crowd Tactics" desc="You are adept at moving through and fighting  in crowds." />
+    <feat type="Savage Species page 31" name="Crush" desc="Like a dragon, you can hurl your body onto opponents  to deal tremendous damage." />
+    <feat type="Savage Species page 31" name="Cumbrous Dodge" desc="You have a chance to dodge attacks that hit  you, but at a cost." />
+    <feat type="Savage Species page 31" name="Cumbrous Fortitude" desc="You have a greater chance than normal to resist  attacks against your vitality, but at a cost." />
+    <feat type="Savage Species page 31" name="Cumbrous Reflexes" desc="You have a greater chance to resist attacks  against your agility, but at a cost." />
+    <feat type="Savage Species page 31" name="Cumbrous Will" desc="You have a greater chance to resist attacks  against your willpower, but at a cost." />
+    <feat type="Draconomicon page 103" name="Cunning Sidestep" desc="You have a better than normal chance to avoid  being bull rushed or tripped." />
+    <feat type="WL page 14" name="Curative Legacy" desc="Your item's legacy is so linked with your aura  that it restores your health each time it is activated." />
+    <feat type="Stormwrack page 92" name="Curling Wave Strike" desc="Mimicking the forceful power of the wave, you  can trip multiple foes as part of the same strike." />
+    <feat type="Races of the Wild page 149" name="Dallah Thaun's Luck" desc="You can rely on a good dose of luck to get you  through almost any scrape." />
+    <feat type="Complete Warrior page 151" name="Damage Reduction" desc="You can shrug off some damage from attacks." />
+    <feat type="Epic Level Handbook page 52" name="Damage Reduction" desc="You can shrug off some damage from attacks." />
+    <feat type="Ghostwalk page 30" name="Dancing Blade" desc="You have an energetic fighting style modeled  after traditional Salkirian dancing." />
+    <feat type="Races of Eberron page 117" name="Dancing with Shadows" desc="You have studied shesan talarash dasyannah,  the martial dance of the kalashtar." />
+    <feat type="Complete Adventurer page 106" name="Danger Sense" desc="You are one twitchy individual." />
+    <feat type="Miniatures Handbook page 25" name="Danger Sense" desc="You are one twitchy mother goose." />
+    <feat type="Races of Eberron page 108" name="Darguun Mauler" desc="The memory of your people's lost glory drives  your brutal mastery of the weapons of Darguun." />
+    <feat type="Book of Vile Darkness page 48" name="Dark Speech" desc="The character learns a smattering of the language  of truly dark power." />
+    <feat type="Lords of Madness page 179" name="Darkstalker" desc="You have learned how to stalk and surprise creatures  whose senses are very different from those of a humanoid." />
+    <feat type="Complete Warrior page 97" name="Dash" desc="You can move faster than normal." />
+    <feat type="Miniatures Handbook page 25" name="Dash" desc="You can move faster than normal." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Dash" desc="You move faster than normal for your race." />
+    <feat type="Libris Mortis: The Book of the Dead page 25" name="Daunting Presence" desc="You are skilled at inducing fear in your opponents." />
+    <feat type="Miniatures Handbook page 25" name="Daunting Presence" desc="You are skilled at inducing fear in your opponents." />
+    <feat type="Player's Guide to Faerun page 37" name="Dauntless" desc="You can stand up to greater punishment than  most and still keep going." />
+    <feat type="Player's Guide to Faerun page 37" name="Daylight Adaptation" desc="You have accustomed yourself to the painful  sunlight of the surface world." />
+    <feat type="Races of Eberron page 108" name="Daylight Adaptation" desc="You have grown accustomed to living in the surface  world, such that bright light no longer blinds or dazzles you." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Daylight Adaptation" desc="Through long exile from the shadowed homelands  of your kind, you have learned to endure the painful sunlight of the surface  world." />
+    <feat type="Libris Mortis: The Book of the Dead page 25" name="Deadly Chill" desc="Undead you raise or create deal more damage  than normal." />
+    <feat type="Serpent Kingdoms page 145" name="Deadly Poison" desc="Your poison attack deals more damage than normal." />
+    <feat type="Savage Species page 31" name="Deadly Poison" desc="Your poison attack deals more damage than normal." />
+    <feat type="Expanded Psionics Handbook page 45" name="Deadly Precision" desc="You empty your mind of all distracting emotion,  becoming an instrument of deadly precision." />
+    <feat type="Serpent Kingdoms page 145" name="Deadly Spittle" desc="You can use your spit attack against multiple  opponents." />
+    <feat type="Epic Level Handbook page 52" name="Deafening Song" desc="Your bardic music deafens those nearby." />
+    <feat type="Complete Adventurer page 106" name="Death Blow" desc="You waste no time in dealing with downed foes." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Death Blow" desc="You waste no time in dealing with downed foes." />
+    <feat type="Lords of Madness page 22" name="Death Frenzy" desc="When an aboleth takes this feat, its sense of  immortality rebels against the very concept of death." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Death Master" desc="Foes are especially afraid of your critical  hits." />
+    <feat type="Epic Level Handbook page 52" name="Death of Enemies" desc="You can instantly slay your favored enemies  with a single strike." />
+    <feat type="Player's Handbook v.3.5 page 93" name="Deceitful" desc="You have a knack for disguising the truth." />
+    <feat type="Savage Species page 32" name="Deep Denizen" desc="You are adapted to a subterranean environment." />
+    <feat type="Expanded Psionics Handbook page 45" name="Deep Impact" desc="You can strike your foe with a melee weapon  as if making a touch attack." />
+    <feat type="Races of Stone page 137" name="Deep Vision" desc="Your mental focus helps you see farther with  darkvision." />
+    <feat type="Races of Faerun page 162" name="Deepening Darkness" desc="Your inherent ability to create darkness is  more powerful than normal." />
+    <feat type="Lords of Madness page 179" name="Deepspawn" desc="Your body undergoes a shocking degeneration  into something that is strikingly inhuman." />
+    <feat type="Races of the Wild page 150" name="Defensive Archery" desc="You can avoid attacks of opportunity when making  ranged attacks while threatened." />
+    <feat type="Complete Warrior page 97" name="Defensive Strike" desc="You can turn a strong defense into a powerful  offense." />
+    <feat type="Oriental Adventures page 62" name="Defensive Strike" desc="You can turn a strong defense into a powerful  offense." />
+    <feat type="Complete Warrior page 97" name="Defensive Throw" desc="You can use your opponent's weight, strength,  and momentum against her, deflecting her attack and throwing her to the  ground." />
+    <feat type="Oriental Adventures page 62" name="Defensive Throw" desc="You can use your opponent's  weight, strength, and momentum against her, deflecting her attack and throwing  her to the ground." />
+    <feat type="Player's Handbook v.3.5 page 93" name="Deflect Arrows" desc="You can deflect incoming arrows, as well as  crossbow bolts, spears, and other projectile or thrown weapons." />
+    <feat type="Races of Stone page 137" name="Deflective Armor" desc="Your armor shields you from touch attacks as  well as regular blows." />
+    <feat type="Book of Vile Darkness page 48" name="Deformity (Clawed Hands)" desc="Because of intentional self-mutilation, the  character has deformed arms and hands ending in sharp claws." />
+    <feat type="Book of Vile Darkness page 48" name="Deformity (Eyes)" desc="The character has either drilled a hole in her  forehead trying to add a third eye, or she has supernaturally scarred one  of her regular eyes." />
+    <feat type="Book of Vile Darkness page 48" name="Deformity (Face)" desc="Because of intentional self-mutilation, the  character has a hideous face." />
+    <feat type="Book of Vile Darkness page 48" name="Deformity (Gaunt)" desc="Through intentional starvation and macabre operations,  the character is grossly underweight." />
+    <feat type="Book of Vile Darkness page 48" name="Deformity (Obese)" desc="Through intentional gorging and general gluttony,  the character is obese." />
+    <feat type="Player's Handbook v.3.5 page 93" name="Deft Hands" desc="You have exceptional manual dexterity." />
+    <feat type="Complete Adventurer page 106" name="Deft Opportunist" desc="You are prepared for the unexpected." />
+    <feat type="Miniatures Handbook page 25" name="Deft Opportunist" desc="You are prepared for the unexpected." />
+    <feat type="Complete Adventurer page 106" name="Deft Strike" desc="You can place attacks at weak points in your  opponent's defenses." />
+    <feat type="Draconomicon page 103" name="Deft Strike" desc="You can place attacks at weak points in your  opponent's defenses." />
+    <feat type="Expanded Psionics Handbook page 45" name="Delay Power" desc="You can manifest powers that go off up to 5  rounds later." />
+    <feat type="Complete Arcane page 77" name="Delay Spell" desc="You can cast spells that take effect after a  short delay of your choosing." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Delay Spell" desc="You can cast spells that take effect after a  short delay of your choosing." />
+    <feat type="Player's Guide to Faerun page 37" name="Delay Spell" desc="You can cast spells that take effect after a  short delay of your choosing." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 39" name="Delay Spell" desc="You can cast spells that take effect after a  short delay of your choosing." />
+    <feat type="Champions of Ruin page 22" name="Demonsworn Knight" desc="A scornful champion of the demon princes, you  detest and oppose devils and other creatures that refuse to heed the call  of chaos." />
+    <feat type="Savage Species page 32" name="Desert Dweller" desc="You are adapted to a desert environment." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Destruction Retribution" desc="Undead you raise or create harbor a retributive  curse that is unleashed if they are destroyed." />
+    <feat type="Complete Warrior page 97" name="Destructive Rage" desc="You can shatter barriers and objects when enraged." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Destructive Rage" desc="You shatter barriers and objects when enraged." />
+    <feat type="Savage Species page 32" name="Detach" desc="You can remove a part of your body and use it  as a ranged weapon." />
+    <feat type="Draconomicon page 68" name="Devastating Critical" desc="Choose one type of melee weapon, such as a claw  or bite. With that weapon, you are capable of killing any creature with  a single strike." />
+    <feat type="Epic Level Handbook page 53" name="Devastating Critical" desc="Choose one type of melee weapon, such as longsword  or greataxe. With that weapon, you are capable of killing any creature with  a single strike." />
+    <feat type="Complete Adventurer page 107" name="Devoted Inquisitor" desc="Your faithful service to your patron deity involves  training and methods that many paladins consider questionable." />
+    <feat type="Complete Adventurer page 107" name="Devoted Performer" desc="You have foregone the pursuit of frivolous musical  talents, instead entering religious training in service of honor and justice." />
+    <feat type="Complete Adventurer page 108" name="Devoted Tracker" desc="You have found a balance between your woodland  training and your devotion to religious training, blending these two aspects  into one seamless whole." />
+    <feat type="Epic Level Handbook page 53" name="Dexterous Fortitude" desc="You are able to resist physical attacks with  exceptional agility." />
+    <feat type="Epic Level Handbook page 53" name="Dexterous Will" desc="You are able to resist compelling effects with  exceptional agility." />
+    <feat type="Ghostwalk page 30" name="Diehard" desc="You can remain conscious after attacks that  would fell others." />
+    <feat type="Player's Handbook v.3.5 page 93" name="Diehard" desc="You can remain conscious after attacks that  would fell others." />
+    <feat type="Player's Handbook v.3.5 page 93" name="Diligent" desc="Your meticulousness allows you to analyze minute  details that others miss." />
+    <feat type="Epic Level Handbook page 53" name="Diminutive Wild Shape" desc="You can wild shape into animals of Diminutive  size." />
+    <feat type="Races of Eberron page 108" name="Dinosaur Hunter" desc="Your extraordinary knowledge of dinosaurs grants  you a special aptitude for tracking and hunting them." />
+    <feat type="Races of Eberron page 108" name="Dinosaur Wrangler" desc="You are attuned to dinosaurs and possess a special  bond with them." />
+    <feat type="Draconomicon page 68" name="Dire Charge" desc="You can make a full attack as part of a charge." />
+    <feat type="Epic Level Handbook page 53" name="Dire Charge" desc="You can make a full attack as part of a charge." />
+    <feat type="Champions of Ruin page 17" name="Dire Flail Smash" desc="You have mastered the style of fighting with  the dire flail and have learned to deal thunderous blows with the weapon." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Dirty Fighting" desc="You know the brutal and effective fighting tactics  of the streets and back alleys." />
+    <feat type="Champions of Ruin page 20" name="Dirty Rat" desc="You are quite adept at slipping under a foe's  guard while he's distracted." />
+    <feat type="Book of Vile Darkness page 49" name="Disciple of Darkness" desc="The character formally supplicates himself to  an archdevil." />
+    <feat type="Champions of Ruin page 23" name="Disciple of Darkness" desc="You formally supplicate yourself to an archdevil.  In return for this obedience, you gain a small measure of the archdevil's  power." />
+    <feat type="Complete Divine page 80" name="Disciple of the Sun" desc="You can destroy undead instead of merely turning  them." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Discipline" desc="Your people are admired for their single-minded  determination and clarity of purpose." />
+    <feat type="Oriental Adventures page 62" name="Discipline" desc="Your ancestor, Naka Kaeteru, was the first Grand  Master of all the elements, a master of meditation and contemplation." />
+    <feat type="Player's Guide to Faerun page 38" name="Discipline" desc="Your people are admired for their single-minded  determination and clarity of purpose." />
+    <feat type="Races of Faerun page 162" name="Disentangler" desc="Thanks to the teachings of Thard Harr, you have  practiced evading the attacks of jungle plants." />
+    <feat type="Complete Adventurer page 108" name="Disguise Spell" desc="You can cast spells without observers noticing." />
+    <feat type="Deities and Demigods page 50" name="Disguise Spell" desc="The deity can cast spells without observers  noticing it." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Disguise Spell" desc="You can cast spells without observers noticing." />
+    <feat type="Lords of Madness page 44" name="Disintegration Finesse" desc="A creature with this feat can use disintegrate  effects to affect smaller, more exacting areas." />
+    <feat type="Lords of Madness page 45" name="Disjunction Ray" desc="A beholder with this feat can narrow its antimagic  cone down to an eye ray that disjoins magic." />
+    <feat type="Epic Level Handbook page 53" name="Distant Shot" desc="You can target a thing you can see with a ranged  weapon." />
+    <feat type="Races of Eberron page 117" name="Disturbing Visage" desc="You can change your features to chilling effect." />
+    <feat type="Complete Adventurer page 108" name="Dive for Cover" desc="You can dive behind cover or drop to the ground  quickly enough to avoid many area effects." />
+    <feat type="Races of Destiny page 156" name="Diverse Background" desc="You have a wide and diverse background, giving  you a greater understanding of different occupations." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Divine Accuracy" desc="You can channel positive energy to give your  allies' melee attacks another chance to strike true against incorporeal  creatures." />
+    <feat type="Complete Warrior page 106" name="Divine Cleansing " desc="You can channel energy to improve your allies'  ability to resist attacks against their vitality and health." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 19" name="Divine Cleansing" desc="You can channel energy to improve you and your  allies' ability to resist poison and curses." />
+    <feat type="Races of Stone page 137" name="Divine Damage Reduction" desc="You can channel energy to give yourself a small  amount of protection from weapons." />
+    <feat type="Ghostwalk page 30" name="Divine Energy Focus" desc="You have a gift for channeling positive or negative  energy." />
+    <feat type="Complete Divine page 80" name="Divine Metamagic" desc="You can channel energy into some of your divine  spells to make them more powerful." />
+    <feat type="Complete Warrior page 106" name="Divine Might" desc="You can channel energy to increase the damage  you deal in combat." />
+    <feat type="Deities and Demigods page 50" name="Divine Might" desc="The deity can channel energy to increase its  damage in combat." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 19" name="Divine Might" desc="You can channel energy to increase the damage  you deal in combat." />
+    <feat type="Faiths &amp; Pantheons page 214" name="Divine Might" desc="You can channel energy to increase your damage  in combat." />
+    <feat type="Complete Warrior page 106" name="Divine Resistance" desc="You can channel energy to temporarily reduce  damage you and your allies take from some sources." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 19" name="Divine Resistance" desc="You can channel energy to temporarily reduce  damage you and your allies take from some sources." />
+    <feat type="Complete Warrior page 106" name="Divine Shield" desc="You can channel energy to make your shield more  effective for either offense or defense." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 19" name="Divine Shield" desc="You can channel energy to make your shield more  effective for either offense or defense." />
+    <feat type="Player's Guide to Faerun page 135" name="Divine Spell Penetration" desc="Choose one component of your alignment. Any  divine spells of that alignment that you cast are more capable of defeating  spell resistance than normal." />
+    <feat type="Complete Divine page 80" name="Divine Spell Power" desc="You can channel positive or negative energy  to enhance your divine spellcasting ability." />
+    <feat type="Races of Stone page 137" name="Divine Spellshield" desc="You can channel energy to help your allies resist  spells and spell-like effects." />
+    <feat type="Deities and Demigods page 50" name="Divine Vengeance" desc="The deity can channel energy to do additional  damage in combat against undead." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Divine Vengeance" desc="You can channel energy to deal additional damage  against undead in melee." />
+    <feat type="Faiths &amp; Pantheons page 214" name="Divine Vengeance" desc="You channel energy to do additional energy damage  in combat against undead." />
+    <feat type="Complete Warrior page 108" name="Divine Vigor" desc="You can channel energy to increase your speed  and durability." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Divine Vigor" desc="You can channel energy to increase your speed  and Constitution." />
+    <feat type="Races of the Wild page 150" name="Diving Charge" desc="You can dive down at a target to deal a devastating  strike." />
+    <feat type="Player's Handbook v.3.5 page 93" name="Dodge" desc="You are adept at dodging blows." />
+    <feat type="Complete Divine page 80" name="Domain Focus" desc="You have mastered the subtle intricacies of  the divine power you've devoted yourself to." />
+    <feat type="Complete Divine page 80" name="Domain Spontaneity " desc="You are so familiar with one of your domains  that you can convert other prepared spells into spells from that domain." />
+    <feat type="Champions of Ruin page 20" name="Doomspeak" desc="You can demoralize an enemy with horrible condemnations  and grim portents of impending doom." />
+    <feat type="Miniatures Handbook page 25" name="Double Hit" desc="You can react with your off hand to make an  additional attack along with an attack of opportunity." />
+    <feat type="Eberron Campaign Setting page 52" name="Double Steel Strike" desc="Through monastic weapon training, you have mastered  a fighting style that makes use of an unusual monk weapon: the two-bladed  sword." />
+    <feat type="Complete Arcane page 77" name="Double Wand Wielder" desc="You can activate two wands at the same time." />
+    <feat type="Dragonlance Campaign Setting page 85" name="Draconian Breath Weapon" desc="You have harnessed your draconic heritage and  can attack with a dragonlike breath weapon." />
+    <feat type="Complete Arcane page 77" name="Draconic Breath" desc="You can convert your arcane spells into a breath  weapon." />
+    <feat type="Complete Arcane page 77" name="Draconic Claw" desc="You develop the natural weapons of your draconic  ancestors." />
+    <feat type="Complete Arcane page 77" name="Draconic Flight" desc="The secret of draconic flight is revealed to  you, granting you the ability to fly occasionally." />
+    <feat type="Complete Arcane page 77" name="Draconic Heritage" desc="You have greater connection with your distant  draconic bloodline." />
+    <feat type="Draconomicon page 69" name="Draconic Knowledge" desc="You are attuned to nature and the elements and  can draw on deep wells of knowledge." />
+    <feat type="Complete Arcane page 78" name="Draconic Legacy" desc="You have realized greater arcane power through  your draconic heritage." />
+    <feat type="Complete Arcane page 78" name="Draconic Power" desc="You have greater power manipulating the energies  of your heritage." />
+    <feat type="Complete Arcane page 78" name="Draconic Presence" desc="When you use your magic, your mere presence  can terrify those around you." />
+    <feat type="Complete Arcane page 78" name="Draconic Resistance" desc="Your bloodline hardens your body against the  energy type of your progenitor." />
+    <feat type="Complete Arcane page 78" name="Draconic Skin" desc="Your skin takes on the sheen, luster, and hardness  of your draconic parentage." />
+    <feat type="Draconomicon page 104" name="Dragon Cohort" desc="You gain the service of a loyal dragon ally." />
+    <feat type="Draconomicon page 104" name="Dragon Familiar" desc="When you are able to acquire a new familiar,  you may select a wyrmling dragon as a familiar." />
+    <feat type="Draconomicon page 104" name="Dragon Hunter " desc="You have made a special study of dragons and  know how to defend against a dragon's attacks." />
+    <feat type="Draconomicon page 104" name="Dragon Hunter Bravery " desc="You resist dragons' frightful presence, and  your mere presence helps others resist as well." />
+    <feat type="Draconomicon page 104" name="Dragon Hunter Defense" desc="Your insight into the tactics and abilities  of dragons grants you awareness of how best to avoid their magical attacks." />
+    <feat type="Eberron Campaign Setting page 52" name="Dragon Rage" desc="You call upon the power of your dragon totem  to enhance your barbarian rage." />
+    <feat type="Draconomicon page 105" name="Dragon Steed" desc="You have earned the service of a loyal draconic  steed." />
+    <feat type="Eberron Campaign Setting page 52" name="Dragon Totem" desc="As a proud warrior of the barbarian tribes of  Argonnessen and Seren, you have claimed one of the true dragon types as  your totem -- a patron, protector, and source of strength." />
+    <feat type="Draconomicon page 105" name="Dragon Wild Shape" desc="You can take the form of a dragon." />
+    <feat type="Epic Level Handbook page 53" name="Dragon Wild Shape" desc="You can take the form of a dragon." />
+    <feat type="Draconomicon page 105" name="Dragonbane" desc="You have made a special study of dragons and  are adept at pulling off deliberate attacks that take advantage of a dragon's  weak spots." />
+    <feat type="Draconomicon page 105" name="Dragoncrafter" desc="You can make special weapons, armor, and other  items using parts of dragons as materials." />
+    <feat type="Draconomicon page 105" name="Dragondoom" desc="You have learned how to place blows against  a dragon that deal tremendous damage." />
+    <feat type="Draconomicon page 105" name="Dragonfoe" desc="You have learned how to attack dragons more  effectively than most other individuals." />
+    <feat type="Draconomicon page 105" name="Dragonfriend" desc="You are a known and respected ally of dragons." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Dragon's Toughness" desc="You are incredibly tough." />
+    <feat type="Draconomicon page 105" name="Dragonsong " desc="Your song or poetics echo the power of the dragonsong,  an ancient style of vocal performance created by dragons in the distant  past." />
+    <feat type="Draconomicon page 105" name="Dragonthrall" desc="You have pledged your life to the service of  evil dragonkind." />
+    <feat type="UE page 43" name="Draw from the Land" desc="You can draw strength and sustenance from the  land itself." />
+    <feat type="Races of Destiny page 154" name="Dread Tyranny" desc="A devoted student of Hextor's militant teachings,  you are skilled at intimidating and dominating weaker beings." />
+    <feat type="Player's Guide to Faerun page 38" name="Dreadful Wrath" desc="You are terrible to behold in battle, and few  foes have the heart to face you without quailing" />
+    <feat type="Races of Eberron page 113" name="Dreamsight Elite" desc="Your dreamsight shifter trait improves." />
+    <feat type="Sandstorm page 49" name="Drift Magic" desc="You can tap the power of drift magic." />
+    <feat type="Races of Faerun page 162" name="Drow Eyes" desc="You have trained your eyes to see in the dark  as well as your full drow ancestors." />
+    <feat type="Races of Eberron page 109" name="Drow Skirmisher" desc="Your experience with the guerrilla-style combat  of the deep jungle grants you mastery of the weapons of the drow." />
+    <feat type="Complete Adventurer page 108" name="Dual Strike" desc="You are an expert skirmisher skilled at fighting  with two weapons." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Dual Strike" desc="Your combat teamwork makes you a more dangerous  foe." />
+    <feat type="Races of Faerun page 162" name="Duergar Mindshaper" desc="You are accomplished at using the power of your  mind to overcome weaker personalities." />
+    <feat type="City of Splendors: Waterdeep page 144" name="Dungeoneer's Intuition" desc="You can sense when things don't feel right,  and you have a knack for avoiding deadly traps and ambushes." />
+    <feat type="Lords of Madness page 180" name="Durable Form" desc="You are much more resilient than the fragile  humanoids that do not share your aberrant heritage." />
+    <feat type="Savage Species page 33" name="Dust Cloud" desc="You can sweep dust into the air to hide from  opponents." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Dwarf's Toughness" desc="You are tougher than you were before." />
+    <feat type="Races of Stone page 138" name="Dwarven Armor Proficiency" desc="You are familiar with exotic armor of dwarven  manufacture and understand how to use it properly." />
+    <feat type="Complete Warrior page 97" name="Eagle Claw Attack" desc="Your superior insight allows you to strike objects  with impressive force." />
+    <feat type="Oriental Adventures page 62" name="Eagle Claw Attack" desc="Your unarmed attacks can shatter objects." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Eagle Claw Attack" desc="Your unarmed attacks shatter objects." />
+    <feat type="Shining South page 20" name="Eagle Tribe Vision" desc="You have keen eyesight reminiscent of the giant  eagles that fly over your tribal lands." />
+    <feat type="Sandstorm page 49" name="Eagle's Fury" desc="You know how to wield the eagle's claw with  deadly speed." />
+    <feat type="Complete Divine page 80" name="Eagle's Wings" desc="You can take wing and fly with the grace of  an eagle." />
+    <feat type="Races of Stone page 138" name="Earth Adept" desc="You are in tune with the ground at your feet,  making you more dangerous in the shifting conditions of combat." />
+    <feat type="Races of Stone page 138" name="Earth Fist" desc="Your bond with the earth and martial training  has imbued your fists with the qualities of cold iron." />
+    <feat type="Planar Handbook page 38" name="Earth Heritage" desc="You are descended from creatures native to the  Elemental Plane of Earth." />
+    <feat type="Races of Stone page 138" name="Earth Master" desc="You are in tune with the ground at your feet,  helping you anticipate your opponent's movements in combat." />
+    <feat type="Races of Stone page 138" name="Earth Power" desc="You draw psionic energy from raw stone." />
+    <feat type="Races of Stone page 138" name="Earth Sense" desc="You are in tune with the earth beneath you." />
+    <feat type="Races of Stone page 138" name="Earth Spell" desc="You draw magical power from the earth beneath  your feet." />
+    <feat type="Complete Warrior page 97" name="Earth's Embrace" desc="You can crush opponents when you grapple them." />
+    <feat type="Oriental Adventures page 62" name="Earth's Embrace" desc="You can crush opponents when you grapple them." />
+    <feat type="Races of Stone page 139" name="Earth's Warding" desc="You can channel energy to infuse your skin with  the strength of the earth." />
+    <feat type="Eberron Campaign Setting page 52" name="Ecclesiarch" desc="You command a degree of respect in your church's  hierarchy." />
+    <feat type="Ghostwalk page 30" name="Ectoplasm" desc="You can create ectoplasm, a gooey physical manifestation  of base supernatural spiritual energy." />
+    <feat type="Eberron Campaign Setting page 52" name="Education" desc="Some lands hold the pen in higher regard than  the sword." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Education" desc="In your youth you received the benefit of several  years of more or less formal schooling." />
+    <feat type="Ghostwalk page 31" name="Education" desc="In your youth you received the benefit of several  years of more or less formal schooling." />
+    <feat type="Player's Guide to Faerun page 38" name="Education" desc="You hail from a land where the pen is held in  higher regard than the sword." />
+    <feat type="Epic Level Handbook page 53" name="Efficient Item Creation" desc="Select an item creation feat. You can create  magic items using that feat much more quickly than normal." />
+    <feat type="Races of Faerun page 162" name="Eldritch Linguist" desc="You have a deep understanding of how words themselves  have their own kind of magic, and a mastery of the secret syntax of power." />
+    <feat type="Races of Faerun page 163" name="Elemental Bloodline" desc="You have taken on some of the aspects of the  type of element that infuses your flesh." />
+    <feat type="Complete Divine page 80" name="Elemental Healing" desc="You can channel elemental energy to heal creatures  of a specific elemental subtype." />
+    <feat type="Complete Divine page 81" name="Elemental Smiting" desc="You can channel elemental energy to deal extra  damage to creatures tied to a specific element." />
+    <feat type="Planar Handbook page 39" name="Elemental Spellcasting" desc="Choose an element. You cast spells with that  descriptor more effectively than normal." />
+    <feat type="Complete Divine page 81" name="Elephant's Hide" desc="You can thicken your skin to the toughness of  an elephant's." />
+    <feat type="Races of the Wild page 150" name="Elf Dilettante" desc="Throughout the long years of your life, you  have developed a talent for doing just about anything." />
+    <feat type="Underdark page 25" name="Elfhunter" desc="Because of your cultural hatred for elves, you  have had specific training in how best to fight them." />
+    <feat type="Complete Warrior page 110" name="Elusive Target " desc="Trying to land a blow against you can be a maddening  experience." />
+    <feat type="Draconomicon page 69" name="Embed Spell Focus" desc="You can embed focus components required for  your spells into your body." />
+    <feat type="WL page 14" name="Empower Legacy" desc="You can use one of your item's legacy abilities  to greater effect." />
+    <feat type="Expanded Psionics Handbook page 46" name="Empower Power" desc="You can manifest powers to greater effect." />
+    <feat type="Player's Handbook v.3.5 page 93" name="Empower Spell" desc="You can cast spells to greater effect." />
+    <feat type="Book of Vile Darkness page 49" name="Empower Spell-Like Ability" desc="The creature can use a spell-like ability with  greater effect." />
+    <feat type="Monster Manual v.3.5 page 303" name="Empower Spell-Like Ability" desc="The creature can use a spell-like ability with  greater effect than normal." />
+    <feat type="Monster Manual III page 206" name="Empower Spell-Like Ability" desc="A creature with this feat can use a spell-like  ability with greater effect than normal." />
+    <feat type="Savage Species page 33" name="Empower Spell-Like Ability" desc="You can use a spell-like ability with greater  effect than normal." />
+    <feat type="Complete Divine page 81" name="Empower Turning" desc="You can turn or rebuke more undead with a single  turning attempt." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Empower Turning" desc="You can turn or rebuke more undead with a single  turning attempt." />
+    <feat type="Faiths &amp; Pantheons page 214" name="Empower Turning" desc="You can turn or rebuke more undead with a single  turning attempt." />
+    <feat type="Ghostwalk page 31" name="Empower Turning" desc="You can turn or rebuke more undead with a single  turning attempt." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Empower Turning" desc="You can turn or rebuke greater numbers of undead  with a single turning attempt." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Empowered Ability Damage" desc="Your ability damage (or ability drain) special  attack is more potent than normal." />
+    <feat type="Oriental Adventures page 80" name="Empty Hand Mastery" desc="You have mastered the martial arts style of  'Empty Hand' -- a hard form emphasizing strikes with the hand." />
+    <feat type="Races of Stone page 139" name="Enchanting Song" desc="You can channel the power of your bardic music  to temporarily increase the power of your enchantment spells." />
+    <feat type="Player's Handbook v.3.5 page 93" name="Endurance" desc="You are capable of amazing feats of stamina." />
+    <feat type="Draconomicon page 70" name="Endure Blows" desc="You are adept at lessening the effects of blows." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Endure Sunlight" desc="Your vulnerability to sunlight is reduced." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Enduring Life" desc="You can ignore the effect of negative levels  for a short time." />
+    <feat type="Races of Stone page 139" name="Energize Armor" desc="You can charge your armor with psionic energy,  making it resistant to energy damage." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Energize Spell" desc="Your spells channel positive energy to deal  extra damage to undead creatures, but are less effective against other opponents." />
+    <feat type="Complete Arcane page 78" name="Energy Admixture" desc="You can modify a spell that uses one type of  energy to add an equal amount of another energy type." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 39" name="Energy Admixture" desc="You can modify a spell that uses one type of  energy to mix in an equal amount of another type of energy." />
+    <feat type="Miniatures Handbook page 25" name="Energy Affinity" desc="You can modify a spell that uses one type of  energy to use another type of energy." />
+    <feat type="Epic Level Handbook page 53" name="Energy Resistance" desc="You can resist the effects of a chosen type  of energy." />
+    <feat type="Complete Arcane page 79" name="Energy Substitution" desc="You can modify an energy-based spell to use  another type of energy instead." />
+    <feat type="Deities and Demigods page 50" name="Energy Substitution" desc="The deity can modify a spell that uses energy  to use another type of energy." />
+    <feat type="Magic of Faerun page 21" name="Energy Substitution" desc="You can modify a spell that uses one type of  energy to use another type of energy." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 40" name="Energy Substitution" desc="You can modify a spell that uses one type of  energy to use another type of energy." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Enervate Spell" desc="Your spells channel negative energy to deal  extra damage to undead creatures, but are less effective against unliving  opponents." />
+    <feat type="Ghostwalk page 31" name="Enervating Touch" desc="Your touch can bestow negative levels upon creatures." />
+    <feat type="Races of Faerun page 163" name="Enervative Healing" desc="You can use the life energy of an opponent to  heal yourself." />
+    <feat type="Player's Guide to Faerun page 135" name="Enhance Effect" desc="You can change the characteristics of a persistent  spell effect that is already in place." />
+    <feat type="Epic Level Handbook page 114" name="Enhance Item" desc="You can increase the minimum DC for saving throws  of magic items that you" />
+    <feat type="Complete Arcane page 191" name="Enhance Spell" desc="You can increase the power limit of your damage-dealing  spells." />
+    <feat type="Epic Level Handbook page 53" name="Enhance Spell" desc="You can increase the power limit of your damage-dealing  spells." />
+    <feat type="Underdark page 25" name="Enhanced Adhesive" desc="The natural adhesive you secrete becomes stickier." />
+    <feat type="Races of Destiny page 152" name="Enhanced Power Sigils" desc="Your illumian power sigils are more powerful  than normal." />
+    <feat type="Draconomicon page 70" name="Enlarge Breathe" desc="Your breath weapon is larger than normal." />
+    <feat type="Lords of Madness page 22" name="Enlarge Mucus Cloud" desc="An aboleth with this feat can extend its mucus  cloud into a wider area." />
+    <feat type="Expanded Psionics Handbook page 46" name="Enlarge Power" desc="You can manifest powers farther than normal." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Enlarge Spell" desc="You can cast spells farther than normal." />
+    <feat type="Champions of Ruin page 20" name="Entangling Spell" desc="Your spell releases residual eldritch power  that entangles your enemies." />
+    <feat type="Complete Warrior page 151" name="Epic Combat Expertise " desc="You have extraordinary talent at using your  combat skill for defense." />
+    <feat type="Player's Guide to Faerun page 135" name="Epic Counterspell" desc="You can counterspell any number of spells each  round." />
+    <feat type="Complete Divine page 89" name="Epic Devotion" desc="Choose an alignment component different from  your own alignment. You are particularly resistant to spells of that alignment." />
+    <feat type="Player's Guide to Faerun page 135" name="Epic Devotion" desc="Choose an alignment component that you do not  possess. You are particularly resistant to spells with that descriptor." />
+    <feat type="Complete Adventurer page 191" name="Epic Dodge" desc="You are able to evade attacks with exceptional  agility." />
+    <feat type="Epic Level Handbook page 54" name="Epic Dodge" desc="You are able to evade attacks with exceptional  agility." />
+    <feat type="Epic Level Handbook page 54" name="Epic Endurance" desc="You are capable of legendary feats of stamina." />
+    <feat type="Expanded Psionics Handbook page 34" name="Epic Expanded Knowledge" desc="You learn another power." />
+    <feat type="Draconomicon page 70" name="Epic Fortitude" desc="You have tremendously high fortitude." />
+    <feat type="Epic Level Handbook page 54" name="Epic Fortitude" desc="You have tremendously high fortitude." />
+    <feat type="Epic Level Handbook page 54" name="Epic Inspiration" desc="Your bardic music provides greater inspiration  than normally possible." />
+    <feat type="Epic Level Handbook page 54" name="Epic Leadership" desc="You attract more powerful cohorts and followers  than normally possible." />
+    <feat type="Complete Warrior page 151" name="Epic Prowess" desc="You have great skill in combat." />
+    <feat type="Epic Level Handbook page 54" name="Epic Prowess" desc="You gain great skill in combat." />
+    <feat type="Expanded Psionics Handbook page 34" name="Epic Psionic Focus" desc="You can expend your psionic focus to greater  effect." />
+    <feat type="Draconomicon page 70" name="Epic Reflexes" desc="You have tremendously fast reflexes." />
+    <feat type="Epic Level Handbook page 54" name="Epic Reflexes" desc="You have tremendously fast reflexes." />
+    <feat type="Complete Adventurer page 191" name="Epic Reputation" desc="Your reputation provides great bonuses on interactions  with others." />
+    <feat type="Epic Level Handbook page 54" name="Epic Reputation" desc="Your reputation provides great bonuses on interactions  with others." />
+    <feat type="Complete Adventurer page 191" name="Epic Skill Focus" desc="Choose a skill, such as Move Silently. You have  a legendary knack with that skill." />
+    <feat type="Epic Level Handbook page 54" name="Epic Skill Focus" desc="Choose a skill, such as Move Silently. You have  a legendary knack with that skill." />
+    <feat type="Epic Level Handbook page 54" name="Epic Speed" desc="You can move much more quickly than a normal  person." />
+    <feat type="Complete Arcane page 192" name="Epic Spell Focus " desc="Choose a school of magic, such as illusion.  Your spells of that school are for more potent than normal." />
+    <feat type="Epic Level Handbook page 54" name="Epic Spell Focus" desc="Choose a school of magic, such as illusion.  Your spells of that school are for more potent than normal." />
+    <feat type="Complete Arcane page 192" name="Epic Spell Penetration" desc="Your spells are tremendously potent, breaking  through spell resistance with ease." />
+    <feat type="Epic Level Handbook page 54" name="Epic Spell Penetration" desc="Your spells are tremendously potent, breaking  through spell resistance with ease." />
+    <feat type="Epic Level Handbook page 55" name="Epic Spellcasting" desc="You can create and cast spells that transcend  the most powerful existing spells." />
+    <feat type="Player's Guide to Faerun page 136" name="Epic Spellfire Wielder" desc="You can store more spellfire energy levels than  normal." />
+    <feat type="Complete Warrior page 151" name="Epic Sunder" desc="You are preternaturally tough." />
+    <feat type="Complete Warrior page 151" name="Epic Toughness" desc="You are specially good at using one chosen type  of weapon." />
+    <feat type="Epic Level Handbook page 55" name="Epic Toughness" desc="You are preternaturally tough." />
+    <feat type="Complete Warrior page 151" name="Epic Weapon Focus " desc="You deal extra damage when attacking objects." />
+    <feat type="Epic Level Handbook page 55" name="Epic Weapon Focus" desc="Choose one type of weapon, such as a greataxe.  You are especially good at using this weapon." />
+    <feat type="Epic Level Handbook page 55" name="Epic Weapon Specialization" desc="Choose one type of weapon, such as a greataxe.  You deal extraordinary damage wielding this weapon." />
+    <feat type="Draconomicon page 70" name="Epic Will" desc="You have tremendously strong willpower." />
+    <feat type="Epic Level Handbook page 55" name="Epic Will" desc="You have tremendously strong willpower." />
+    <feat type="Deities and Demigods page 50" name="Eschew Materials " desc="The deity can cast spells without material components." />
+    <feat type="Epic Level Handbook page 69" name="Eschew Materials" desc="You can cast spells without material components." />
+    <feat type="Faiths &amp; Pantheons page 214" name="Eschew Materials" desc="You can cast spells without material components." />
+    <feat type="Lords of Darkness page 189" name="Eschew Materials" desc="You can cast spells without material components." />
+    <feat type="Magic of Faerun page 22" name="Eschew Materials" desc="You can cast spells without material components." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Eschew Materials" desc="You can cast spells without relying on material  components." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 40" name="Eschew Materials" desc="You can cast spells without material components." />
+    <feat type="Races of Destiny page 155" name="Eternal Strength" desc="You have taken Kord's fighting ways to heart.  Throwing yourself into every brawl, you draw upon your mighty deity's strength." />
+    <feat type="Ghostwalk page 31" name="Ethereal Sidestep" desc="You can teleport yourself a short distance." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Ethran" desc="You have been initiated into the secrets of  the Witches of Rashemen as a member of the Ethran, the 'untried.'" />
+    <feat type="Player's Guide to Faerun page 38" name="Ethran" desc="You have been initiated into  the secrets of the Witches of Rashemen as a member of the Ethran, the 'untrained.'" />
+    <feat type="UE page 43" name="Ettercap Berserker" desc="The intense physical training required to join  your lodge has made you tougher." />
+    <feat type="Book of Vile Darkness page 49" name="Evil Brand" desc="The character is physically marked forever as  a servant of an evil power or as a villain." />
+    <feat type="Champions of Ruin page 23" name="Evil Brand" desc="You are physically marked forever as a servant  of an evil power or as a villain." />
+    <feat type="Champions of Ruin page 23" name="Evil Embraced" desc="You embrace the power of your fiendish patron  and call upon that power in moments of great need." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Eviscerator" desc="The allies of your foes are especially afraid  of your critical hits." />
+    <feat type="Book of Exalted Deeds page 42" name="Exalted Companion" desc="Instead of an animal companion, you have a magical  beast of good alignment." />
+    <feat type="Book of Exalted Deeds page 42" name="Exalted Smite" desc="Your smite ability is empowered with holy energy." />
+    <feat type="Book of Exalted Deeds page 42" name="Exalted Spell Resistance" desc="You are particularly resistant to evil spells." />
+    <feat type="Book of Exalted Deeds page 42" name="Exalted Turning" desc="You turn undead with such power that affected  undead take damage." />
+    <feat type="Book of Exalted Deeds page 42" name="Exalted Wild Shape" desc="You can use your wild shape ability to take  the form of a good-aligned magical beast." />
+    <feat type="Eberron Campaign Setting page 52" name="Exceptional Artisan" desc="You are an expert at creating magic items faster  than usual." />
+    <feat type="Epic Level Handbook page 55" name="Exceptional Deflection" desc="You can deflect any type of ranged attack." />
+    <feat type="Races of Stone page 139" name="Exotic Armor Proficiency" desc="Choose a type of exotic armor. You understand  how to wear that type of exotic armor properly." />
+    <feat type="Underdark page 25" name="Exotic Armor Proficiency" desc="Choose a type of exotic armor. You understand  how to wear that type of exotic armor properly." />
+    <feat type="Races of Stone page 139" name="Exotic Shield Proficiency" desc="Choose an exotic shield. You are proficient  with that type of exotic shield." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Exotic Weapon Proficiency" desc="Choose a type of exotic weapon. You understand  how to use that type of exotic weapon in combat." />
+    <feat type="Heroes of Battle page 97" name="Expanded Aura of Courage" desc="Your aura of courage protects more allies than  normal." />
+    <feat type="Expanded Psionics Handbook page 46" name="Expanded Knowledge" desc="You learn another power." />
+    <feat type="Ghostwalk page 31" name="Expanded Possession" desc="You can ride or possess an additional type of  creature." />
+    <feat type="Races of the Wild page 150" name="Expeditious Dodge" desc="You're good at avoiding attacks while moving  quickly." />
+    <feat type="Heroes of Battle page 97" name="Expert Siege Engineer" desc="You are particularly skilled at operating siege  weapons, such as catapults and battering rams." />
+    <feat type="Stormwrack page 92" name="Expert Swimmer" desc="You swim like a fish. You can stay underwater  far longer than others of your race, and you are at home in the water." />
+    <feat type="Complete Adventurer page 109" name="Expert Tactician" desc="Your tactical skills work to your advantage." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 38" name="Expert Tactician" desc="Your tactical skills work to your advantage." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Expert Tactician" desc="Your tactical skills work to your advantage." />
+    <feat type="Complete Arcane page 79" name="Explosive Spell" desc="You can cast spells that blast creatures off  their feet." />
+    <feat type="UE page 43" name="Explosive Spell" desc="You can cast spells that blast creatures off  their feet." />
+    <feat type="Expanded Psionics Handbook page 46" name="Extend Power" desc="You can manifest powers that last longer than  normal." />
+    <feat type="Complete Warrior page 97" name="Extend Rage" desc="You are able to maintain your rage longer than  most." />
+    <feat type="Eberron Campaign Setting page 52" name="Extend Rage" desc="You are able to maintain your rage longer than  most." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Extend Spell" desc="You can cast spells that last longer than normal." />
+    <feat type="Draconomicon page 70" name="Extend Spreading Breath" desc="You can convert your breath weapon into a spread  effect that can be used at range." />
+    <feat type="Epic Level Handbook page 56" name="Extended Life Span" desc="You are exceptionally long-lived." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Extended Rage" desc="Your rage lasts longer than it normally would." />
+    <feat type="UE page 43" name="Extended Rage" desc="Your rage lasts longer than it normally would." />
+    <feat type="Savage Species page 34" name="Extended Reach" desc="Your flexible body allows you to reach farther  than normal." />
+    <feat type="Miniatures Handbook page 26" name="Extra Domain Spell" desc="You have chosen to be more specialized in a  particular domain." />
+    <feat type="Complete Arcane page 79" name="Extra Edge" desc="Your ability to deal spell damage is particularly  striking." />
+    <feat type="Ghostwalk page 32" name="Extra Favored Enemy" desc="You select an additional favored enemy." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Extra Favored Enemy" desc="You select an additional favored enemy." />
+    <feat type="Heroes of Battle page 97" name="Extra Followers" desc="Your charismatic magnetism attracts even more  followers to your banner." />
+    <feat type="Complete Arcane page 79" name="Extra Invocation" desc="You learn an additional invocation." />
+    <feat type="Savage Species page 34" name="Extra Item Space" desc="You can wear more magic items than are normally  allowed." />
+    <feat type="Complete Adventurer page 109" name="Extra Music" desc="You can use your bardic music more often than  you otherwise could." />
+    <feat type="Deities and Demigods page 50" name="Extra Music" desc="The deity can use its bardic songs more often  than it otherwise could." />
+    <feat type="Eberron Campaign Setting page 52" name="Extra Music" desc="You can use your bardic music more often than  you otherwise could." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 39" name="Extra Music" desc="You can use your bardic music more often than  you otherwise could." />
+    <feat type="Complete Warrior page 98" name="Extra Rage" desc="You may rage more frequently than normal." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Extra Rage" desc="You rage more frequently than you normally would." />
+    <feat type="Eberron Campaign Setting page 53" name="Extra Rings" desc="Your familiarity with forging magic rings allows  you to make use of more rings than normal." />
+    <feat type="Eberron Campaign Setting page 53" name="Extra Shifter Trait" desc="You manifest a second shifter trait while shifting." />
+    <feat type="Monster Manual III page 150" name="Extra Shifter Trait" desc="You manifest a second shifter trait while shifting." />
+    <feat type="Races of Eberron page 114" name="Extra Shifter Trait" desc="You manifest a second shifter trait while shifting." />
+    <feat type="Races of Stone page 139" name="Extra Silence" desc="You can generate a field of silence more often  than other whisper gnomes can." />
+    <feat type="Complete Arcane page 79" name="Extra Slot" desc="You can cast an additional spell." />
+    <feat type="Ghostwalk page 32" name="Extra Slot" desc="You can cast an extra spell." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 40" name="Extra Slot" desc="You can cast an extra spell." />
+    <feat type="Complete Warrior page 98" name="Extra Smiting" desc="You can make more smite attacks." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Extra Smiting" desc="You can make more smite attacks." />
+    <feat type="Complete Arcane page 79" name="Extra Spell" desc="You learn an additional spell." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 40" name="Extra Spell" desc="You can learn one more spell." />
+    <feat type="Complete Arcane page 80" name="Extra Spell Secret" desc="You learn an additional spell secret." />
+    <feat type="Complete Warrior page 98" name="Extra Stunning" desc="You gain extra stunning attacks." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Extra Stunning Attacks" desc="You gain extra stunning attacks when fighting  unarmed." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Extra Turning" desc="You can turn or rebuke creatures more often  than normal." />
+    <feat type="Complete Divine page 81" name="Extra Wild Shape" desc="You can use wild shape more frequently than  you normally could." />
+    <feat type="Ghostwalk page 32" name="Extra Wild Shape" desc="You can use wild shape more frequently than  you normally could." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Extra Wild Shape" desc="You use wild shape more frequently than you  normally could." />
+    <feat type="Underdark page 25" name="Extra Wild Shape" desc="You can use wild shape more frequently than  you normally could." />
+    <feat type="Eberron Campaign Setting page 53" name="Extraordinary Artisan" desc="You are an expert at creating magic items at  a lower cost than usual." />
+    <feat type="Complete Adventurer page 109" name="Extraordinary Concentration" desc="Your mind is so focused that you can cast spells  even while concentrating on another spell." />
+    <feat type="Complete Adventurer page 109" name="Extraordinary Spell Aim" desc="You can shape a spell's area to exclude one  creature from its effects." />
+    <feat type="Complete Warrior page 98" name="Eyes in the Back of Your Head" desc="Your superior battle sense helps minimize the  threat of flanking." />
+    <feat type="Deities and Demigods page 50" name="Eyes in the Back of Your Head" desc="The deity's superior battle sense helps minimize  the threat of flanking attacks." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Eyes in the Back of Your Head" desc="Your superior battle sense helps minimize the  threat of flanking." />
+    <feat type="Races of Faerun page 163" name="Eyes of Light" desc="You can focus the holy power within you to create  a beam of destructive light energy." />
+    <feat type="Unearthed Arcana page 93" name="Eyes to the Sky" desc="You have an instinctive sense of when someone  is magically watching you." />
+    <feat type="Ghostwalk page 32" name="Fade" desc="You can make your ghost body more diaphanous  and difficult to detect." />
+    <feat type="Frostburn page 48" name="Faith in the Frost" desc="You channel frozen energies from your deity  when you turn or rebuke creatures." />
+    <feat type="Oriental Adventures page 62" name="Falling Far Strike" desc="You have mastered the art of striking a nerve  that blinds a humanoid opponent." />
+    <feat type="Unearthed Arcana page 93" name="False Pretenses" desc="Those who try to charm you get an unpleasant  surprise." />
+    <feat type="Lost Empires of Faerun page 8" name="Familiar Concentration" desc="In the tradition of Narfell's ancient summoners,  your familiar can concentrate to maintain spells for you." />
+    <feat type="Dungeon Master's Guide v.3.5 page 209" name="Familiar Spell" desc="Your familiar can cast a spell." />
+    <feat type="Epic Level Handbook page 56" name="Familiar Spell" desc="Your familiar can use one of your spells as  a spell-like ability." />
+    <feat type="Underdark page 25" name="Familiar Spell" desc="You are so well acquainted with the spells you  have mastered that you can store the prepared spells in the mind of your  familiar." />
+    <feat type="Races of Destiny page 155" name="Far Horizons" desc="By dedicating yourself to the philosophies of  Fharlanghn, you have become a more world-wise and capable traveler." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Far Shot" desc="You can get greater distance out of a ranged  weapon." />
+    <feat type="Draconomicon page 70" name="Fast Healing" desc="You heal your wounds very quickly." />
+    <feat type="Epic Level Handbook page 56" name="Fast Healing" desc="You heal your wounds very quickly." />
+    <feat type="Complete Divine page 81" name="Fast Wild Shape" desc="You assume your wild shape faster and more easily  than you otherwise could." />
+    <feat type="Ghostwalk page 32" name="Fast Wild Shape" desc="You can assume your wild shape faster and more  easily than you normally could." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Fast Wild Shape" desc="You assume your wild shape faster and more easily  than you otherwise could." />
+    <feat type="Complete Warrior page 98" name="Faster Healing" desc="You recover faster than normal." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 22" name="Faster Healing" desc="You recover faster than others do." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 23" name="Favored Critical" desc="You know how to hit your favored enemies where  it hurts." />
+    <feat type="Eberron Campaign Setting page 53" name="Favored in House" desc="You are a member of one of the dragonmarked  mercantile houses and wield some influence in that house." />
+    <feat type="Book of Exalted Deeds page 43" name="Favored of the Companions" desc="You swear allegiance to the Talisid or one of  the Five Companions, the paragons of the guardinals, and in exchange gain  power to act on their behalf." />
+    <feat type="Player's Guide to Faerun page 176" name="Favored of the Zulkirs" desc="Through your position of prestige among the  Red Wizards, you have gained access to secrets of evil magic known to few  outside the zulkirs themselves." />
+    <feat type="Complete Warrior page 98" name="Favored Power Attack" desc="You are able to deal more damage against your  favored enemies." />
+    <feat type="Player's Guide to Faerun page 38" name="Fearless" desc="You are a stranger to fear." />
+    <feat type="Races of Destiny page 152" name="Fearless Destiny" desc="Your grand destiny allows you to avoid death." />
+    <feat type="Oriental Adventures page 62" name="Fearsome and Fearless" desc="You claim descent from the first Akodo, the  paragon of samurai virtue." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Feign Weakness" desc="You capitalize on your foe's perceptions of  your unarmed status." />
+    <feat type="Libris Mortis: The Book of the Dead page 26" name="Fell Animate" desc="Living foes slain by your spell may rise as  zombies." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Fell Drain" desc="Living foes damaged by your spell also gain  a negative level." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Fell Frighten" desc="Living foes damaged by your spell are also shaken." />
+    <feat type="Expanded Psionics Handbook page 46" name="Fell Shot" desc="You can strike your foe with a ranged weapon  as if making a touch attack." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Fell Weaken" desc="Living foes damaged by your spell are also weakened." />
+    <feat type="Champions of Ruin page 20" name="Feral Animal Companion" desc="You can enslave a feral animal and adopt it  as your animal companion." />
+    <feat type="Races of Faerun page 163" name="Fiendish Bloodline" desc="Some of your latent abilities, inherited from  an unusually powerful fiendish ancestor, have matured." />
+    <feat type="Planar Handbook page 39" name="Fiendish Heritage" desc="You are descended from creatures native to the  Lower Planes." />
+    <feat type="Planar Handbook page 39" name="Fiendish Summoning Specialist" desc="You can select from a larger number of options  when summoning evil creatures." />
+    <feat type="Sandstorm page 49" name="Fiery Spell" desc="Your fire magic is bolstered, further scorching  your enemies." />
+    <feat type="Sharn: City of Towers page 157" name="Filth Eater" desc="You are highly resistant to the effects of disease  and can usually eat spoiled food without suffering ill effects." />
+    <feat type="Savage Species page 34" name="Final Strike" desc="Your death throes are destructive." />
+    <feat type="Epic Level Handbook page 56" name="Fine Wild Shape" desc="You can wild shape into animals of Fine size." />
+    <feat type="Planar Handbook page 39" name="Fire Heritage" desc="You are descended from creatures native to the  Elemental Plane of Fire." />
+    <feat type="Book of Exalted Deeds page 43" name="Fist of the Heavens" desc="Your stunning attack is empowered by celestial  might." />
+    <feat type="Complete Warrior page 99" name="Fists of Iron" desc="You have learned the secrets of imbuing your  unarmed attacks with extra force." />
+    <feat type="Oriental Adventures page 62" name="Fists of Iron" desc="You have learned the secrets of imbuing your  unarmed attacks with extra force." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 6" name="Fists of Iron" desc="You have learned the secrets of imbuing your  unarmed attacks with extra force." />
+    <feat type="Champions of Ruin page 20" name="Flay Foe" desc="You are skilled at flaying the flesh from your  enemy's bones." />
+    <feat type="Complete Warrior page 99" name="Fleet of Foot" desc="You run nimbly, able to turn corners without  losing momentum." />
+    <feat type="Deities and Demigods page 50" name="Fleet of Foot" desc="The deity runs so nimbly that it can turn corners  without losing momentum." />
+    <feat type="Player's Guide to Faerun page 38" name="Fleet of Foot" desc="You are extraordinarily swift." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 39" name="Fleet of Foot" desc="You run so nimbly that you can turn corners  without losing momentum." />
+    <feat type="Eberron Campaign Setting page 53" name="Flensing Strike" desc="You have studied a martial style practiced by  monks devoted to the Mockery, which has taught you to cut your opponent's  skin in a very painful way." />
+    <feat type="Complete Warrior page 99" name="Flick of the Wrist" desc="With a single motion, you can draw a light weapon  and make a devastating attack." />
+    <feat type="Races of the Wild page 150" name="Flick of the Wrist" desc="With a single motion, you can draw a light weapon  and make a devastating attack." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 39" name="Flick of the Wrist" desc="With a single motion, you can draw a light weapon  and make a devastating attack." />
+    <feat type="Races of Stone page 139" name="Fling Ally" desc="You can launch your comrades into the air as  if they were thrown weapons." />
+    <feat type="Races of Stone page 140" name="Fling Enemy" desc="When you're wrestling a foe, you can lift him  into the air and hurl him." />
+    <feat type="Savage Species page 34" name="Fling Enemy" desc="You can pick up an opponent and fling it." />
+    <feat type="Monster Manual v.3.5 page 303" name="Flyby Attack" desc="The creature can attack on the wing." />
+    <feat type="Monster Manual II page 18" name="Flyby Attack" desc="The creature can attack on the wing." />
+    <feat type="Monster Manual III page 206" name="Flyby Attack" desc="A creature with this feat can attack on the  wing." />
+    <feat type="Monster Compendium: Monsters of Faerun page 9" name="Flyby Attack" desc="The creature can attack on the wing." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 23" name="Flyby Attack" desc="You attack while on the wing." />
+    <feat type="Dragonlance Campaign Setting page 85" name="Flyby Breath" desc="You can employ your breath weapon in a high-speed  attack pass." />
+    <feat type="Stormwrack page 92" name="Flying Fish Leap" desc="You can hurl yourself out of the water with  ease." />
+    <feat type="Complete Warrior page 99" name="Flying Kick" desc="You literally leap into battle, dealing devastating  damage." />
+    <feat type="Oriental Adventures page 62" name="Flying Kick" desc="You literally leap into battle, dealing devastating  damage." />
+    <feat type="Lords of Madness page 45" name="Focused Antimagic" desc="A beholder with this feat can focus the antimagic  of its central eye to target a single person or object." />
+    <feat type="Races of the Wild page 151" name="Focused Mind" desc="When you have the opportunity to concentrate  on a task, you usually do very well at it." />
+    <feat type="Races of Stone page 140" name="Focused Shield" desc="Your mental focus makes you more adept at using  your shield." />
+    <feat type="Expanded Psionics Handbook page 46" name="Focused Sunder" desc="You can sense the stress points on others' weapons." />
+    <feat type="Forgotten Realms Campaign Setting page 34" name="Foe Hunter" desc="In lands threatened by evil nonhumans, many  warriors learn ways to fight effectively against these creatures." />
+    <feat type="Ghostwalk page 32" name="Foe Hunter" desc="You have been trained in the methods of fighting  various kinds of yuan-ti." />
+    <feat type="Player's Guide to Faerun page 38" name="Foe Hunter" desc="In a land threatened by fierce raiders, you  have learned to fight effectively against certain foes." />
+    <feat type="Miniatures Handbook page 26" name="Foe Specialist" desc="You are trained at how to damage a particular  type of foe." />
+    <feat type="Oriental Adventures page 80" name="Foot and Fist Mastery" desc="You have mastered the martial arts style of  'Foot and Fist' -- a hard form emphasizing strikes with the hands and feet." />
+    <feat type="Complete Adventurer page 109" name="Force of Personality" desc="You have cultivated an unshakable belief in  your self-worth." />
+    <feat type="Expanded Psionics Handbook page 46" name="Force of Will" desc="You are able to resist psionic attacks with  extreme force of will." />
+    <feat type="Ghostwalk page 32" name="Forceful Staff Style" desc="You can stun people with your quarterstaff and  push them around after you stun them." />
+    <feat type="Races of Faerun page 163" name="Forest Gnome Phantasist" desc="You can protect your forest home with a variety  of phantasms and patterns to befuddle your foes." />
+    <feat type="Forgotten Realms Campaign Setting page 35" name="Forester" desc="You are knowledgeable about the secrets of the  forest and wise in its ways." />
+    <feat type="Player's Guide to Faerun page 39" name="Forester" desc="You are one with Faerun's mighty forests." />
+    <feat type="Epic Level Handbook page 56" name="Forge Epic Ring" desc="You can craft magic rings of epic power." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Forge Ring" desc="You can create magic rings, which have varied  magical effects." />
+    <feat type="Player's Guide to Faerun page 39" name="Forgeheart" desc="Because you are inured to the hellish heat of  your homeland, you are resistant to blasts of fire that would damage other  creatures." />
+    <feat type="Serpent Kingdoms page 145" name="Forked Tongue" desc="You speak with a honeyed voice that bends listeners  to your will." />
+    <feat type="Complete Warrior page 110" name="Formation Expert" desc="You are trained at fighting in ranks and files." />
+    <feat type="Complete Arcane page 80" name="Fortify Spell" desc="You cast spells that more easily penetrate spell  resistance." />
+    <feat type="UE page 43" name="Fortify Spell" desc="You can cast spells that easily penetrate spell  resistance." />
+    <feat type="Complete Warrior page 99" name="Freezing the Lifeblood" desc="You can paralyze a humanoid opponent with an  unarmed attack." />
+    <feat type="Oriental Adventures page 62" name="Freezing the Lifeblood" desc="You can paralyze a humanoid opponent with an  unarmed attack." />
+    <feat type="Ghostwalk page 33" name="Freezing Touch" desc="Your touch is supernaturally cold." />
+    <feat type="Ghostwalk page 33" name="Frightful Moan" desc="You can unleash a moan that panics creatures  near you." />
+    <feat type="Draconomicon page 106" name="Frightful Presence" desc="Like a dragon, your mere presence can terrify  those around you." />
+    <feat type="Ghostwalk page 33" name="Frightful Presence" desc="Your very presence can cause others to be stricken  with fear." />
+    <feat type="Frostburn page 48" name="Frostfell Prodigy" desc="You gain additional bonus spells in cold regions." />
+    <feat type="Frostburn page 48" name="Frozen Berserker" desc="When you enter your barbarian rage, your body  becomes infused with cold energy." />
+    <feat type="Frostburn page 48" name="Frozen Magic" desc="Your cold spells are more powerful when you  cast them in a cold region." />
+    <feat type="Frostburn page 48" name="Frozen Wild Shape" desc="You can assume the form of magical beasts with  the cold subtype." />
+    <feat type="Ghostwalk page 33" name="Full Manifestation" desc="You can manifest fully when you would otherwise  be forced to be incorporeal." />
+    <feat type="Player's Guide to Faerun page 39" name="Furious Charge" desc="Your people are known for their love of battle,  and they rarely waste time in meeting a foe blade-to-blade." />
+    <feat type="Savage Species page 35" name="Gap of the Serpent" desc="You can swallow larger creatures than normal." />
+    <feat type="Serpent Kingdoms page 146" name="Gape of the Serpent" desc="Like a snake, you can stretch your mouth to  an outlandish extent to accommodate immense prey." />
+    <feat type="Epic Level Handbook page 56" name="Gargantuan Wild Shape" desc="You can wild shape into animals of Gargantuan  size." />
+    <feat type="Eberron Campaign Setting page 54" name="Gatekeeper Initiate" desc="You have been trained in the ancient druidic  tradition of the Gatekeepers, founded originally to ward off an extraplanar  assault by aberrations." />
+    <feat type="Races of Faerun page 163" name="Genie Lore" desc="You have studied centuries of Calishite lore  regarding geniekind." />
+    <feat type="Races of Eberron page 112" name="Gestalt Anchor" desc="Your strong bond to your quori spirit allows  you and your kalashtar allies to move and act as a fluid unit." />
+    <feat type="Expanded Psionics Handbook page 46" name="Ghost Attack" desc="Your deadly strikes against incorporeal foes  always hit their mark." />
+    <feat type="Ghostwalk page 33" name="Ghost Flight" desc="Your fully manifested ghost body can fly." />
+    <feat type="Ghostwalk page 33" name="Ghost Glide" desc="Your fully manifested ghost body can slowly  fly." />
+    <feat type="Ghostwalk page 33" name="Ghost Hand" desc="You can move small objects in a limited manner  when you are a ghost." />
+    <feat type="Ghostwalk page 33" name="Ghost Healing" desc="You can transfer some of your own ectoplasm  to another ghost to heal it." />
+    <feat type="Ghostwalk page 33" name="Ghost Ride" desc="You can hide within the physical body of a living  creature, perceiving the world through its senses, but without the ability  to control the host." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Ghost Scarred" desc="You are adept at fighting incorporeal undead." />
+    <feat type="Ghostwalk page 34" name="Ghost Smiting" desc="You can use your smite ability to smite ghosts." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Ghostly Grasp" desc="You can handle corporeal objects even while  corporeal." />
+    <feat type="Ghostwalk page 34" name="Ghost-Touch Spell" desc="You know how to tune your damaging spells to  affect ghosts without harming other creatures." />
+    <feat type="Complete Warrior page 111" name="Giantbane" desc="You are trained in fighting foes larger than  you are." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 23" name="Giant's Toughness" desc="You are amazingly tough." />
+    <feat type="Player's Guide to Faerun page 176" name="Gift of Discernment" desc="You can rely on your conscience to steer you  away from evil deeds." />
+    <feat type="Book of Exalted Deeds page 43" name="Gift of Faith" desc="You have an unusual capacity to trust in divine  providence working all things for the good." />
+    <feat type="Book of Exalted Deeds page 43" name="Gift of Grace" desc="You can improve the saving throws of your allies  by sharing some of your divine grace." />
+    <feat type="Ghostwalk page 34" name="Gift of Tongues" desc="You have an intuitive talent for learning languages." />
+    <feat type="Races of Faerun page 163" name="Gift of Tongues" desc="You have an intuitive talent for learning languages." />
+    <feat type="Oriental Adventures page 62" name="Gifted General" desc="Your ancestor Daidoji Yurei, an ancient daimyo  of the Daidoji family, was a gifted general -- the first in Rokugan to use  guerilla warfare." />
+    <feat type="Complete Divine page 82" name="Glorious Weapons" desc="You can channel positive or negative energy  to imbue your allies' weapons with an alignment." />
+    <feat type="Races of the Wild page 151" name="Gnoll Ferocity" desc="You embody the savage ferocity of your people.  When you fly into a berserk rage, you can bite opponents with your powerful  jaws." />
+    <feat type="Races of Stone page 140" name="Gnome Foe Killer" desc="Your battle techniques against your racial foes  improve." />
+    <feat type="Complete Adventurer page 109" name="Goad " desc="You are skilled at inducing opponents to attack  you." />
+    <feat type="Miniatures Handbook page 26" name="Goad" desc="You are skilled at inducing opponents to attack  you." />
+    <feat type="Races of Stone page 140" name="Goad" desc="You are skilled at inducing opponents to attack  you." />
+    <feat type="Lost Empires of Faerun page 8" name="Godsight" desc="You enjoy the special blessing of a deity of  the Mulhorandi pantheon, who has granted you unerring powers of perception." />
+    <feat type="Races of Faerun page 163" name="Gold Dwarf Dweomersmith" desc="You have learned the secrets of gold dwarf magic  that creates or enhances weapons." />
+    <feat type="Races of Eberron page 114" name="Gorebrute Elite" desc="Your mighty charge attack can knock down foes." />
+    <feat type="Fiend Folio page 207" name="Graft Flesh" desc="You can apply a certain type of graft to other  living creatures or to yourself." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Graft Flesh" desc="You can apply a certain type of graft to other  living creatures or to yourself." />
+    <feat type="Lords of Madness page 216" name="Graft Flesh" desc="You can apply a certain type of graft to other  living creatures or to yourself." />
+    <feat type="Underdark page 25" name="Graft Illithid Flesh" desc="You can apply illithid grafts to other living  creatures or to yourself." />
+    <feat type="Serpent Kingdoms page 146" name="Graft Yuan-Ti Flesh" desc="You can apply yuan-ti grafts to other living  creatures or to yourself." />
+    <feat type="Ghostwalk page 34" name="Grand Malevolence" desc="You can possess multiple creatures and control  their actions." />
+    <feat type="Oriental Adventures page 63" name="Grappling Block" desc="You can catch and pin an opponent's weapon with  your bare hands." />
+    <feat type="Savage Species page 35" name="Grass Trekker" desc="You are adapted to a plains environment." />
+    <feat type="Eberron Campaign Setting page 54" name="Great Bite" desc="You know how to hit where it hurts with your  fangs." />
+    <feat type="Stormwrack page 92" name="Great Captain" desc="You are a master pilot and battle leader; your  crew anticipates your every command and leaps to do your bidding." />
+    <feat type="Epic Level Handbook page 56" name="Great Charisma" desc="Your powers of persuasion and leadership are  greater than normal." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Great Cleave" desc="You can wield a melee weapon with such power  that you can strike multiple times when you fell your foes." />
+    <feat type="Epic Level Handbook page 56" name="Great Constitution" desc="Your health and endurance are greater than normal." />
+    <feat type="Oriental Adventures page 63" name="Great Crafter" desc="Your ancestor, Kaiu, was the first and greatest  blacksmith of the Crab clan." />
+    <feat type="Epic Level Handbook page 56" name="Great Dexterity" desc="Your agility and coordination are greater than  normal." />
+    <feat type="Oriental Adventures page 63" name="Great Diplomat" desc="You are descended from Asako, one of the companions  of the first Phoenix, a great healer, diplomat, and warrior." />
+    <feat type="Savage Species page 35" name="Great Flyby Attack" desc="You can make multiple flyby attacks in a round." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Great Fortitude" desc="You are tougher than normal." />
+    <feat type="Epic Level Handbook page 56" name="Great Intelligence" desc="Your powers of reason and learning are greater  than normal." />
+    <feat type="Eberron Campaign Setting page 54" name="Great Rend" desc="You know how to hit where it hurts with your  claws." />
+    <feat type="Dungeon Master's Guide v.3.5 page 209" name="Great Smiting" desc="Your smite attacks are much more powerful than  normal." />
+    <feat type="Epic Level Handbook page 56" name="Great Smiting" desc="Your smite attacks are much more powerful than  normal." />
+    <feat type="UE page 43" name="Great Stag Berserker" desc="Your fighting style employs aggressive charges  in the manner of your lodge's totem animal." />
+    <feat type="Oriental Adventures page 63" name="Great Stamina" desc="Your ancestor, Daidoji Masashigi, gave his life  defending the Kaiu Wall alongside the Crab at the Battle of the Landbridge." />
+    <feat type="Epic Level Handbook page 57" name="Great Strength" desc="Your muscle and physical power are greater than  normal." />
+    <feat type="Oriental Adventures page 63" name="Great Teamwork" desc="You are a descendant of Hida Banuken, the Crab  champion who oversaw the construction of the Kaiu Wall during the battle  of the Cresting Wave." />
+    <feat type="Epic Level Handbook page 57" name="Great Wisdom" desc="Your willpower and insight are greater than  normal." />
+    <feat type="Oriental Adventures page 63" name="Greater Ki Shout" desc="Your ki shout can panic your opponents." />
+    <feat type="Frostburn page 48" name="Greater Cold Focus" desc="Your cold spells are now even more potent than  before." />
+    <feat type="Eberron Campaign Setting page 54" name="Greater Dragonmark" desc="You have a greater dragonmark." />
+    <feat type="Races of Stone page 141" name="Greater Heavy Armor Optimization" desc="You have mastered the use of heavy armor, maximizing  its protective qualities while moving more easily in it." />
+    <feat type="Complete Warrior page 99" name="Greater Kiai Shout" desc="You kiai shout can panic your opponents." />
+    <feat type="WL page 14" name="Greater Legacy" desc="You awaken the most powerful abilities of a  specific item of legacy." />
+    <feat type="Expanded Psionics Handbook page 47" name="Greater Manyshot" desc="You are skilled at firing many arrows at once,  even at different opponents." />
+    <feat type="Savage Species page 35" name="Greater Mighty Roar" desc="You unsettle opponents with a dreadful roar  as you attack." />
+    <feat type="Serpent Kingdoms page 146" name="Greater Multigrab" desc="You can grapple enemies effortlessly with your  natural weapons." />
+    <feat type="Savage Species page 35" name="Greater Multigrab" desc="You can grapple enemies effortlessly with your  natural weapons." />
+    <feat type="Deities and Demigods page 50" name="Greater Multiweapon Fighting" desc="A deity with three or more hands can fight with  a weapon in each hand." />
+    <feat type="Epic Level Handbook page 69" name="Greater Multiweapon Fighting" desc="A creature with three or more hands can fight  with a weapon in each hand." />
+    <feat type="Savage Species page 35" name="Greater Multiweapon Fighting" desc="A creature with three or more arms can fight  with a weapon in each hand. The creature can make up to three attacks per  round with each extra weapon." />
+    <feat type="Expanded Psionics Handbook page 47" name="Greater Power Penetration" desc="Your powers are especially potent at breaking  through power resistance." />
+    <feat type="Expanded Psionics Handbook page 47" name="Greater Power Specialization" desc="You deal more damage with your powers." />
+    <feat type="Eberron Campaign Setting page 54" name="Greater Powerful Charge" desc="You can charge with extra force." />
+    <feat type="Miniatures Handbook page 27" name="Greater Powerful Charge" desc="You can charge with extra force." />
+    <feat type="Expanded Psionics Handbook page 47" name="Greater Psionic Endowment" desc="You can use meditation to focus your powers." />
+    <feat type="Expanded Psionics Handbook page 47" name="Greater Psionic Fist" desc="You can charge your unarmed strike or natural  weapon with additional damage potential." />
+    <feat type="Expanded Psionics Handbook page 47" name="Greater Psionic Shot" desc="You can charge your ranged attacks with additional  damage potential." />
+    <feat type="Expanded Psionics Handbook page 47" name="Greater Psionic Weapon" desc="You can charge your melee weapon with additional  damage potential." />
+    <feat type="Complete Warrior page 99" name="Greater Resiliency" desc="Your extraordinary resilience to damage increases." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 23" name="Greater Resiliency" desc="Your extraordinary resilience to damage increases." />
+    <feat type="Eberron Campaign Setting page 54" name="Greater Shifter Defense" desc="By delving deeper into your shifter heritage,  you develop the ability to ignore some damage from every attack." />
+    <feat type="Deities and Demigods page 50" name="Greater Spell Focus" desc="The deity chooses a school of magic to which  it already has applied the Spell Focus feat." />
+    <feat type="Epic Level Handbook page 69" name="Greater Spell Focus" desc="Choose a school of magic, such as illusion.  Your spells of that school are far more potent than normal." />
+    <feat type="Forgotten Realms Campaign Setting page 35" name="Greater Spell Focus" desc="Choose a school of magic to which you already  have applied the Spell Focus feat. Your spells of that school are even more  potent than normal." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Greater Spell Focus" desc="Choose a school of magic to which you already  have applied the Spell Focus feat. Your spells of that school are even more  potent than normal." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 40" name="Greater Spell Focus" desc="Choose a school of magic to which you already  have applied the Spell Focus feat. Your spells of that school are even more  potent than normal." />
+    <feat type="Deities and Demigods page 50" name="Greater Spell Penetration" desc="The deity's spells are especially potent, defeating  spell resistance more readily than normal." />
+    <feat type="Epic Level Handbook page 69" name="Greater Spell Penetration" desc="Your spells are especially potent, defeating  spell resistance more readily than normal." />
+    <feat type="Forgotten Realms Campaign Setting page 35" name="Greater Spell Penetration" desc="Your spells are especially potent, defeating  spell resistance more readily than normal." />
+    <feat type="Player's Handbook v.3.5 page 94" name="Greater Spell Penetration" desc="Your spells are remarkably potent, breaking  through spell resistance more readily than normal." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 40" name="Greater Spell Penetration" desc="Your spells are especially potent, breaking  through spell resistance more readily than normal." />
+    <feat type="Complete Warrior page 100" name="Greater Two-Weapon Defense" desc="When fighting with two weapons, your defenses  are extraordinarily strong." />
+    <feat type="Deities and Demigods page 50" name="Greater Two-Weapon Fighting" desc="The deity is a master at fighting two-handed." />
+    <feat type="Epic Level Handbook page 69" name="Greater Two-Weapon Fighting" desc="You are a master at fighting two-handed." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 23" name="Greater Two-Weapon Fighting" desc="You are a master at fighting two-handed." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Greater Two-Weapon Fighting" desc="You are a master at fighting two-handed." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Greater Weapon Focus" desc="Choose one type of weapon for which you have  already selected Weapon Focus. You can also choose unarmed strike or grapple  as your weapon for purposes of this feat. You are especially good at using  this weapon." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Greater Weapon Specialization" desc="Choose one type of weapon for which you have  already selected Weapon Specialization You can also choose unarmed strike  or grapple as your weapon for purposes of this feat. You deal extra damage  when using this weapon." />
+    <feat type="Ghostwalk page 34" name="Greater Witchlight" desc="Your witchlight can last longer, become hotter,  or give off more light." />
+    <feat type="Ghostwalk page 35" name="Green Bond" desc="You have an empathic bond with one of the spirit  trees around Manifest" />
+    <feat type="Complete Adventurer page 110" name="Green Ear" desc="Your bardic music can affect plant creatures." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 39" name="Green Ear" desc="Your bardic music and virtuoso performance affect  plants and plant creatures." />
+    <feat type="Lost Empires of Faerun page 8" name="Greenbound Summoning" desc="You are learned in a long-forgotten manner of  summoning once practiced by Eaerlanni elves of the High Forest." />
+    <feat type="Eberron Campaign Setting page 54" name="Greensinger Initiate" desc="You have embraced the druidic traditions of  the Greensingers, a chaotic Eldeen Reaches sect with close ties to the fey." />
+    <feat type="Lords of Madness page 114" name="Grell Alchemy" desc="A creature that has this feat has studied the  alien and disturbing arcane lore of the grell, and understands the magical  and physical laws by which their spells and devices function." />
+    <feat type="Races of Faerun page 163" name="Grim Visage" desc="Your eyes have seen a lot, and now they show  everyone that you aren't to be trifled with. Even glib people stammer in  your presence." />
+    <feat type="Complete Divine page 82" name="Grizzly's Claws" desc="You can grow claws as sharp as those of a bear." />
+    <feat type="Complete Adventurer page 192" name="Group Inspiration" desc="Your bardic powers can inspire more allies than  normal." />
+    <feat type="Epic Level Handbook page 57" name="Group Inspiration" desc="You can inspire competence or greatness in more  than one ally simultaneously." />
+    <feat type="Complete Arcane page 80" name="Guardian Spirit" desc="Your watchful spirit is more capable than normal." />
+    <feat type="Heroes of Battle page 97" name="Guerrilla Scout" desc="You know how to use your senses to greater effect." />
+    <feat type="Heroes of Battle page 97" name="Guerrilla Warrior" desc="You know how to move stealthily, even when armored." />
+    <feat type="Shining South page 20" name="Halruuan Adept" desc="You have studied the old cooperative spellcasting  traditions of Halruaa, and you are well-versed in the rites and arcana of  Halruuan magic." />
+    <feat type="Races of Faerun page 164" name="Hammer Fist" desc="You are trained in an unarmed fighting style  that emphasizes a two-handed strike." />
+    <feat type="Complete Warrior page 113" name="Hammer's Edge" desc="You are a master of the style of fighting with  a hammer and sword at the same time." />
+    <feat type="Complete Warrior page 100" name="Hamstring" desc="You can wound your opponents' legs, hampering  their movement." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 39" name="Hamstring" desc="You can wound an opponent's legs, hampering  his or her movement." />
+    <feat type="City of Splendors: Waterdeep page 145" name="Hand of Tyr" desc="You have sacrificed your right hand to Tyr,  the Maimed God, proving your resilience and strength of spirit." />
+    <feat type="Book of Exalted Deeds page 43" name="Hands of a Healer" desc="You can heal more damage than normal by laying  on hands." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Hardened Flesh" desc="Undead you raise or create can better handle  themselves in a fight." />
+    <feat type="Races of Faerun page 164" name="Harem Trained" desc="You have been trained to serve as a jhasin (if  male) or jhasina (if female) and are well versed in song, music, dance,  art, the recitation of great literature, the art of massage, and other duties  of the harem." />
+    <feat type="Ghostwalk page 35" name="Haunting Appearance" desc="You can make your ghost body assume a terrifying  appearance that can frighten observers." />
+    <feat type="Eberron Campaign Setting page 54" name="Haunting Melody" desc="You can use your music to inspire fear." />
+    <feat type="Ghostwalk page 35" name="Haunting Voice" desc="You can make your voice originate from another  location." />
+    <feat type="Complete Adventurer page 114" name="Hawk's Vision" desc="You can improve your visual acuity." />
+    <feat type="Races of Faerun page 164" name="Headlong Rush" desc="You charge your foes with immense force, heedless  of your own safety." />
+    <feat type="Eberron Campaign Setting page 55" name="Healing Factor" desc="When your current period of shifting ends, you  heal a limited amount of damage." />
+    <feat type="Monster Manual III page 150" name="Healing Factor" desc="When your current period of shifting ends, you  heal a limited amount of damage." />
+    <feat type="Races of Faerun page 164" name="Healing Flames" desc="You can draw energy from open flames to heal  yourself." />
+    <feat type="Complete Adventurer page 110" name="Hear the Unseen" desc="Your sense of hearing is so acute that you can  partially pinpoint an opponent's location by sound, allowing you to strike  even if the opponent is concealed or displaced." />
+    <feat type="Sandstorm page 50" name="Heat Endurance" desc="Either as a result of growing up in the waste,  or by training your body and mind to ignore the effects of searing heat,  you can exist with ease in high-temperature environments." />
+    <feat type="Shining South page 20" name="Heat Tolerance" desc="You are used to living in hot, humid conditions." />
+    <feat type="Races of Stone page 141" name="Heavy Armor Optimization" desc="You have trained extensively in heavy armor,  and you have learned to take advantage of the protection it offers." />
+    <feat type="Races of Stone page 141" name="Heavy Lithoderms" desc="You have stony growths on your skin that afford  you protection against attacks." />
+    <feat type="Draconomicon page 70" name="Heighten Breath" desc="Your breath weapon is even more deadly than  normal." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Heighten Spell" desc="You can cast a spell as if it were a higher-level  spell than it actually is." />
+    <feat type="Complete Arcane page 80" name="Heighten Spell-Like Ability" desc="You can use a spell-like ability as if it were  a higher spell-level equivalent than it actually is." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Heighten Turning" desc="You can affect more powerful undead with your  turning or rebuking attempts." />
+    <feat type="Faiths &amp; Pantheons page 214" name="Heighten Turning" desc="You can affect more powerful undead with your  turning or rebuking attempts." />
+    <feat type="Ghostwalk page 35" name="Heighten Turning" desc="You can affect more powerful undead with your  turning or rebuking attempts." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Heighten Turning" desc="You can affect more powerful undead with your  turning or rebuking attempts." />
+    <feat type="Champions of Ruin page 23" name="Hellbound Knight" desc="A devoted disciple of the Nine Hells, you have  sworn to strike down creatures that oppose law and threaten tyranny." />
+    <feat type="Races of Destiny page 152" name="Heroic Destiny" desc="You have a destiny to fulfill." />
+    <feat type="Races of Eberron page 109" name="Heroic Metamagic" desc="In times of great need, you can call upon a  heroic reserve of power to strengthen your spells." />
+    <feat type="Eberron Campaign Setting page 55" name="Heroic Spirit" desc="You have a larger reservoir of luck than the  average hero." />
+    <feat type="Complete Warrior page 113" name="High Sword Low Axe" desc="You have mastered the style of fighting with  a sword and axe at the same time." />
+    <feat type="Races of Faerun page 164" name="Highborn Drow" desc="You have learned how to tap into the advanced  magical abilities of your drow noble heritage." />
+    <feat type="Underdark page 25" name="Highborn Drow" desc="You have learned how to tap into the advanced  magical abilities available to you through your drow noble heritage." />
+    <feat type="Races of Faerun page 164" name="Hin Wandermage" desc="You have a natural affinity for spells that  take you from place to place." />
+    <feat type="Epic Level Handbook page 57" name="Hindering Song" desc="Your bardic music interferes with opposing spellcasters." />
+    <feat type="Complete Warrior page 100" name="Hold the Line" desc="You are trained in defensive techniques against  charging opponents." />
+    <feat type="Deities and Demigods page 51" name="Hold the Line" desc="The deity is trained in defensive techniques  against charging opponents." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 7" name="Hold the Line" desc="You are trained in defensive techniques against  charging opponents." />
+    <feat type="Shining South page 20" name="Hold the Line" desc="You are trained in defensive techniques against  charging opponents." />
+    <feat type="Book of Exalted Deeds page 44" name="Holy Ki Strike" desc="Your unarmed attacks deal extra damage to evil  creatures." />
+    <feat type="Book of Exalted Deeds page 44" name="Holy Radiance" desc="You can increase the intensity of the light  surrounding you to damage undead creatures." />
+    <feat type="Complete Divine page 89" name="Holy Strike" desc="Your attacks deal great damage to evil creatures." />
+    <feat type="Epic Level Handbook page 57" name="Holy Strike" desc="Your attacks deal great damage to evil creatures." />
+    <feat type="Book of Exalted Deeds page 44" name="Holy Subdual" desc="You can turn bonus damage into nonlethal damage." />
+    <feat type="Oriental Adventures page 63" name="Honest Merchant" desc="Your ancestor, Bayushi Tesaguri, was the son  of Bayushi Junzen, Scorpion Clan Champion." />
+    <feat type="Dragonlance Campaign Setting page 86" name="Honor-Bound" desc="Keeping your word and upholding your honor is  of great importance to you." />
+    <feat type="Ghostwalk page 35" name="Horrific Appearance" desc="You can blast creatures with your simple appearance." />
+    <feat type="Forgotten Realms Campaign Setting page 35" name="Horse Nomad" desc="You have been raised in a culture that relies  upon riding and shooting for survival." />
+    <feat type="Player's Guide to Faerun page 39" name="Horse Nomad" desc="You have been raised in a culture that relies  upon riding and shooting." />
+    <feat type="Expanded Psionics Handbook page 47" name="Hostile Mind" desc="Your mind recoils violently against those who  use psionics against you." />
+    <feat type="Monster Manual v.3.5 page 304" name="Hover" desc="The creature can come to a halt in midair." />
+    <feat type="Monster Manual II page 18" name="Hover" desc="The creature can halt its forward motion while  flying, regardless of maneuverability." />
+    <feat type="Dragonlance Campaign Setting page 86" name="Hulking Brute " desc="You possess a truly impressive stature." />
+    <feat type="Races of Destiny page 152" name="Human Heritage" desc="Your human heritage is more prominent than in  others of your kind." />
+    <feat type="Miniatures Handbook page 27" name="Hurling Charge" desc="You are trained in using thrown weapons as part  of a charge attack." />
+    <feat type="Shining South page 20" name="Hyena Tribe Hunter" desc="You have learned the secrets of hunting from  the hyena that roams the lands where your tribe wanders." />
+    <feat type="Oriental Adventures page 63" name="Iaijutsu Master" desc="You are not only descended from Kakita, the  greatest duelist ever to have lived, but you share a karmic tie to his spirit." />
+    <feat type="Frostburn page 48" name="Ice Harmonics" desc="Your summon spells work better in the frostfell  if you summon native creatures." />
+    <feat type="UE page 44" name="Ice Troll Berserker" desc="When raging, your skin becomes very thick and  tough like the ice trolls that plague parts of your homeland." />
+    <feat type="Frostburn page 48" name="Icy Calling" desc="You can use your voice to shatter ice." />
+    <feat type="Epic Level Handbook page 57" name="Ignore Material Components" desc="You need not use any material components in  casting spells." />
+    <feat type="Expanded Psionics Handbook page 47" name="Imprint Stone" desc="You can create power stones to store psionic  powers." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Improve Bull Rush" desc="You know how to push opponents back." />
+    <feat type="Oriental Adventures page 63" name="Improved Aid" desc="You are descended from Hida Tadaka, the great  Crab daimyo who gave his life to avert a war between his clan and the Lion." />
+    <feat type="Epic Level Handbook page 57" name="Improved Alignment-Based Casting" desc="Your spells of a particular alignment are more  powerful than normal." />
+    <feat type="Epic Level Handbook page 57" name="Improved Arrow of Death" desc="Your arrows of death are harder to resist." />
+    <feat type="Savage Species page 35" name="Improved Assume Supernatural Ability" desc="You gain skills using a supernatural ability  of an assumed form." />
+    <feat type="Epic Level Handbook page 57" name="Improved Aura of Courage" desc="Your aura of courage is stronger than normal." />
+    <feat type="Epic Level Handbook page 57" name="Improved Aura of Despair" desc="Your aura of despair is wider than normal." />
+    <feat type="Complete Warrior page 100" name="Improved Buckler Defense" desc="You can attack with an off-hand weapon while  retaining a buckler's shield bonus to your Armor Class." />
+    <feat type="Heroes of Battle page 98" name="Improved Cohort" desc="You attract a more powerful cohort than you  normally would." />
+    <feat type="Frostburn page 48" name="Improved Cold Endurance" desc="Your training and natural hardiness have improved  your natural resistance to cold temperatures." />
+    <feat type="Complete Arcane page 192" name="Improved Combat Casting" desc="You heighten your ability to cast spells while  threatened without fear of being attacked." />
+    <feat type="Epic Level Handbook page 57" name="Improved Combat Casting" desc="You can cast spells while threatened without  fear of being attacked." />
+    <feat type="Complete Warrior page 100" name="Improved Combat Expertise" desc="You have mastered the art of defense in combat." />
+    <feat type="Epic Level Handbook page 57" name="Improved Combat Reflexes" desc="You can respond to any number of opponents who  let their defenses down." />
+    <feat type="Ghostwalk page 35" name="Improved Control Visage" desc="You can change your ghost form's appearance." />
+    <feat type="Player's Guide to Faerun page 136" name="Improved Cooperative Metamagic" desc="Your ability to enhance an ally's spell during  casting is expanded." />
+    <feat type="Epic Level Handbook page 70" name="Improved Counterspell" desc="You understand the nuances of magic to such  an extent that you can counter your opponent's spells with great efficiency." />
+    <feat type="Forgotten Realms Campaign Setting page 35" name="Improved Counterspell" desc="You understand the nuances of magic to such  an extent that you can counter your opponent's spells with great efficiency." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Improved Counterspell" desc="You understand the nuances of magic to such  an extent that you can counter your opponent's spells with great efficiency." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Improved Critical" desc="Choose one type of weapon. With that weapon,  you know how to hit where it hurts." />
+    <feat type="Eberron Campaign Setting page 55" name="Improved Damage Reduction" desc="You gain damage reduction or improve your existing  damage reduction." />
+    <feat type="Epic Level Handbook page 58" name="Improved Darkvision" desc="Your ability to see in the dark is greater than  normal." />
+    <feat type="Epic Level Handbook page 58" name="Improved Death Attack" desc="Your death attack is harder to overcome." />
+    <feat type="Ghostwalk page 35" name="Improved Deflection" desc="You are adept at deflecting things before they  strike you." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Improved Disarm" desc="You know how to disarm opponents in melee combat." />
+    <feat type="Complete Adventurer page 110" name="Improved Diversion" desc="You can create a diversion to hide quickly and  with less effort." />
+    <feat type="Dragonlance Campaign Setting page 86" name="Improved Draconian Breath Weapon" desc="You have mastered your draconic heritage and  improved on your innate breath weapon." />
+    <feat type="Planar Handbook page 40" name="Improved Elemental Heritage" desc="You have manifested an even stronger tie to  your elemental ancestor, resulting in a minor resistance to elemental effects." />
+    <feat type="Dungeon Master's Guide v.3.5 page 209" name="Improved Elemental Wild Shape " desc="You can take the form of a larger variety of  elementals than normal." />
+    <feat type="Epic Level Handbook page 58" name="Improved Elemental Wild Shape" desc="You can take the form of a greater variety of  elementals than normal." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Improved Energy Drain" desc="You draw extra power from your energy-drained  victims." />
+    <feat type="Races of Faerun page 164" name="Improved Energy Resistance" desc="Choose one form of energy to which you have  a natural (not spell- or item-generated) resistance. Your inherent resistance  to this kind of energy is more effective than normal." />
+    <feat type="Complete Warrior page 100" name="Improved Familiar" desc="This feat allows spellcasters to acquire a new  familiar from a nonstandard list, but only when they could normally acquire  a new familiar." />
+    <feat type="Forgotten Realms Campaign Setting page 35" name="Improved Familiar" desc="So long as you are able to acquire a new familiar,  you may choose your new familiar from a nonstandard list." />
+    <feat type="Player's Guide to Faerun page 39" name="Improved Familiar" desc="Refer to the Improved Familiar feat description  on page 200 of the Dungeon Master's Guide." />
+    <feat type="Races of Faerun page 165" name="Improved Familiar" desc="See the discussion of the Improved Familiar  feat in Chapter 1 of the Forgotten Realms Campaign Setting. Table A-5 shows  additional improved familiars from this book that are available with this  feat." />
+    <feat type="Serpent Kingdoms page 146" name="Improved Familiar" desc="Refer to the Improved Familiar feat description  in the Dungeon Master's Guide." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 40" name="Improved Familiar" desc="As long as you are able to acquire a new familiar,  you may choose your new familiar from a nonstandard list." />
+    <feat type="Complete Warrior page 101" name="Improved Favored Enemy" desc="You know how to hit your favored enemies where  it hurts." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Improved Favored Enemy" desc="Gain bonuses against favored enemies." />
+    <feat type="Epic Level Handbook page 58" name="Improved Favored Enemy" desc="You gain bonuses against favored enemies." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Improved Feint" desc="You are skilled at misdirecting your opponent's  attention in combat." />
+    <feat type="Champions of Ruin page 20" name="Improved Fiendish Servant" desc="You gain the service of a powerful fiendish  animal servitor." />
+    <feat type="Complete Adventurer page 110" name="Improved Flight" desc="You gain greater maneuverability when flying  than you would normally have." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 23" name="Improved Flight" desc="You gain greater maneuverability when flying  than you would normally have." />
+    <feat type="Races of Faerun page 165" name="Improved Flight" desc="You gain greater maneuverability when flying  than you would normally have." />
+    <feat type="Races of the Wild page 151" name="Improved Flight" desc="You have gained greater maneuverability when  flying than you would normally have." />
+    <feat type="Sharn: City of Towers page 157" name="Improved Flight Item (Item Creation)" desc="You have learned to make use of the manifest  zone in Sharn to craft magic items that grant superior flight." />
+    <feat type="Epic Level Handbook page 70" name="Improved Flyby Attack" desc="The creature can attack on the wing with increased  mobility." />
+    <feat type="Savage Species page 36" name="Improved Flyby Attack" desc="You can attack on the wing with increased mobility." />
+    <feat type="Eberron Campaign Setting page 55" name="Improved Fortification" desc="You improve your warforged fortification, gaining  immunity to sneak attacks and extra damage from critical hits." />
+    <feat type="Monster Manual III page 192" name="Improved Fortification" desc="You improve your warforged fortification, gaining  immunity to sneak attacks and extra damage from critical hits." />
+    <feat type="Frostburn page 49" name="Improved Frosty Touch" desc="Your frosty touch causes more cold damage." />
+    <feat type="Ghostwalk page 35" name="Improved Ghost Flight" desc="Your ghost body can fly rapidly." />
+    <feat type="Deities and Demigods page 51" name="Improved Grapple " desc="The deity is skilled in martial arts that emphasize  holds and throws." />
+    <feat type="Oriental Adventures page 63" name="Improved Grapple" desc="You are skilled in martial arts that emphasize  holds and throws." />
+    <feat type="Player's Handbook v.3.5 page 95" name="Improved Grapple" desc="You are skilled at grappling opponents." />
+    <feat type="UE page 44" name="Improved Grapple" desc="You are skilled in martial arts that emphasize  holds and throws." />
+    <feat type="Sandstorm page 50" name="Improved Heat Endurance" desc="You can survive even in the most extreme natural  heat conditions." />
+    <feat type="Epic Level Handbook page 58" name="Improved Heighten Spell" desc="You can cast a spell at any level above its  own." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Initiative" desc="You can react more quickly than normal in a  fight." />
+    <feat type="Epic Level Handbook page 58" name="Improved Ki Strike" desc="You can strike opponents with great damage reduction." />
+    <feat type="Races of Faerun page 165" name="Improved Levitation" desc="You have learned to use part of your levitate  spell-like ability at a time, allowing multiple uses with a shorter duration." />
+    <feat type="Underdark page 25" name="Improved Levitation" desc="You have learned to use only part of your levitate  spell-like ability at a time, allowing multiple uses with shorter duration." />
+    <feat type="Races of Faerun page 165" name="Improved Low Blow" desc="You are especially good at using the Low Blow  feat." />
+    <feat type="Epic Level Handbook page 58" name="Improved Low-Light Vision" desc="The range of your low-light vision is greater  than normal." />
+    <feat type="Draconomicon page 70" name="Improved Maneuverability" desc="Your maneuverability in flight improves." />
+    <feat type="Epic Level Handbook page 58" name="Improved Manifestation" desc="You can manifest psionic powers more powerful  than the normal limits of manifestation." />
+    <feat type="Expanded Psionics Handbook page 34" name="Improved Manifestation" desc="You increase your power point reserve." />
+    <feat type="Epic Level Handbook page 58" name="Improved Manyshot" desc="You can fire even more arrows as a single attack  against a nearby target." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Improved Metamagic" desc="You can cast spells using metamagic feats more  easily than normal." />
+    <feat type="Epic Level Handbook page 59" name="Improved Metamagic" desc="You can cast spells using metamagic feats more  easily than normal." />
+    <feat type="Expanded Psionics Handbook page 34" name="Improved Metapsionics" desc="You can manifest powers using metapsionic feats  more often than normal." />
+    <feat type="Complete Warrior page 101" name="Improved Mounted Archery" desc="You can make ranged attacks from a mount almost  as well as you can from the ground." />
+    <feat type="Draconomicon page 70" name="Improved Multiattack" desc="You are particularly adept at using all your  natural weapons at once." />
+    <feat type="Epic Level Handbook page 70" name="Improved Multiattack" desc="The creature is particularly adept at using  all its natural weapons at once." />
+    <feat type="Savage Species page 36" name="Improved Multiattack" desc="You are particularly adept at using all your  natural weapons at once." />
+    <feat type="Deities and Demigods page 51" name="Improved Multiweapon Fighting" desc="A deity with three or more hands can fight with  a weapon in each hand." />
+    <feat type="Epic Level Handbook page 70" name="Improved Multiweapon Fighting" desc="A creature with three or more hands can fight  with a weapon in each hand." />
+    <feat type="Savage Species page 36" name="Improved Multiweapon Fighting" desc="You are expert at fighting with a weapon in  each of your three or more hands. You can make up to two attacks per round  with each off-hand weapon." />
+    <feat type="Monster Manual v.3.5 page 304" name="Improved Natural Armor" desc="The creature's natural armor is thicker and  harder than that of other of its kind." />
+    <feat type="Monster Manual III page 206" name="Improved Natural Armor" desc="The natural armor of a creature with this feat  is thicker and harder than normal for its kind." />
+    <feat type="Races of Faerun page 165" name="Improved Natural Armor" desc="Your skin is even tougher than that of most  of your kind." />
+    <feat type="Eberron Campaign Setting page 55" name="Improved Natural Attack" desc="One of a creature's natural attacks is more  dangerous than its type and size would otherwise indicate." />
+    <feat type="Monster Manual v.3.5 page 304" name="Improved Natural Attack" desc="The creature's natural attacks are more dangerous  than its size and type would otherwise dictate." />
+    <feat type="Monster Manual III page 206" name="Improved Natural Attack" desc="The natural attacks of a creature with this  feat are more dangerous than its size and type would otherwise dictate." />
+    <feat type="Planar Handbook page 40" name="Improved Outer Planar Heritage" desc="Your ancestral tie to the Outer Planes manifests  as an ability to deal damage with your natural attacks as if they matched  the alignment of your ancestors." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Overrun" desc="You are skilled at knocking down opponents." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 7" name="Improved Overrun" desc="You are trained in knocking over opponents that  are smaller than you." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Improved Paralysis" desc="You are better at paralyzing your victims." />
+    <feat type="Ghostwalk page 36" name="Improved Poltergeist Hand" desc="You can move a large object at a distance when  you are a ghost." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Precise Shot" desc="Your ranged attacks can ignore the effects of  cover or concealment." />
+    <feat type="Expanded Psionics Handbook page 47" name="Improved Psicrystal" desc="You can upgrade your psicrystal." />
+    <feat type="Complete Warrior page 101" name="Improved Rapid Shot" desc="You are an expert at firing weapons with exceptional  speed." />
+    <feat type="Draconomicon page 70" name="Improved Rapidstrike" desc="You can make multiple attacks with a natural  weapon." />
+    <feat type="Races of Eberron page 119" name="Improved Resiliency" desc="You gain a construct's resistance to nonlethal  damage." />
+    <feat type="Dragonlance Campaign Setting page 86" name="Improved Resist Dragonfear" desc="You are able to demonstrate great courage in  the presence of dragons." />
+    <feat type="Races of Stone page 141" name="Improved Rock Hurling" desc="Your accuracy and effectiveness with thrown  rocks improves." />
+    <feat type="Savage Species page 36" name="Improved Scent" desc="You can detect and track creatures by smell  at greater distances than normal." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Improved Shield Bash" desc="You can push opponents back by bashing them  with your shield." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Shield Bash" desc="You can bash with a shield while retaining its  shield bonus to your Armor Class." />
+    <feat type="Miniatures Handbook page 27" name="Improved Shieldmate" desc="You have an outstanding ability to protect those  near you with your shield." />
+    <feat type="Races of Destiny page 152" name="Improved Sigil (Aesh)" desc="You tap into your aesh power sigil to  gain enhanced accuracy with your favored melee weapons." />
+    <feat type="Races of Destiny page 152" name="Improved Sigil (Hoon)" desc="You tap into your hoon power sigil to  help survive deadly conditions." />
+    <feat type="Races of Destiny page 153" name="Improved Sigil (Krau)" desc="You tap into your krau power sigil to  augment the energy of your magical utterances." />
+    <feat type="Races of Destiny page 153" name="Improved Sigil (Naen)" desc="You tap into your naen power sigil to  see through illusions and resist language-based effects." />
+    <feat type="Races of Destiny page 153" name="Improved Sigil (Uur)" desc="You tap into your uur power sigil to  gain enhanced accuracy with ranged weapons." />
+    <feat type="Races of Destiny page 153" name="Improved Sigil (Vaul)" desc="You tap into your vaul power sigil to  resist mental effects." />
+    <feat type="Complete Adventurer page 192" name="Improved Skirmish" desc="Your combat mobility improves." />
+    <feat type="Complete Divine page 82" name="Improved Smiting" desc="Your smite attacks deal more damage to specific  foes, and can damage creature with alignment-based damage reduction." />
+    <feat type="Draconomicon page 71" name="Improved Snatch" desc="You can make snatch attacks against bigger opponents  than other creatures can." />
+    <feat type="Player's Guide to Faerun page 136" name="Improved Snatch Spell" desc="When you take over a spell from another spellcaster,  you gain more control over its effect." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Improved Sneak Attack" desc="Your sneak attacks are more deadly than normal." />
+    <feat type="Epic Level Handbook page 59" name="Improved Sneak Attack" desc="Your sneak attacks are more deadly than normal." />
+    <feat type="Draconomicon page 71" name="Improved Speed " desc="You are faster than others of your kind." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Improved Spell Capacity" desc="You can prepare spells that exceed the normal  limits of spellcasting." />
+    <feat type="Draconomicon page 71" name="Improved Spell Capacity" desc="You can prepare spells that exceed the normal  limits of spellcasting." />
+    <feat type="Epic Level Handbook page 59" name="Improved Spell Capacity" desc="You can prepare spells that exceed the normal  limits of spellcasting." />
+    <feat type="Epic Level Handbook page 60" name="Improved Spell Resistance" desc="Your innate resistance to magical effects increases." />
+    <feat type="Player's Guide to Faerun page 136" name="Improved Spellpool Access" desc="You can use your spellpool access to call spells  of greater than normal power." />
+    <feat type="Serpent Kingdoms page 146" name="Improved Spit" desc="You can spit farther than normal." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Improved Stunning Fist" desc="Your stunning attack is more powerful." />
+    <feat type="Epic Level Handbook page 60" name="Improved Stunning Fist" desc="Your stunning attack is more powerful." />
+    <feat type="Complete Adventurer page 192" name="Improved Sudden Strike" desc="Your ability to strike unaware foes improves." />
+    <feat type="Deities and Demigods page 51" name="Improved Sunder" desc="The deity is adept at placing its attacks precisely  where it wants them to land." />
+    <feat type="Enemies and Allies page 42" name="Improved Sunder" desc="You are adept at placing your attacks precisely  where you want them to land." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Sunder" desc="You are skilled at attacking your opponents'  weapons and shields, as well as other objects." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 7" name="Improved Sunder" desc="You are adept at placing your attacks precisely  where you want them to land." />
+    <feat type="Complete Adventurer page 110" name="Improved Swimming" desc="You can swim faster than you normally could." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 23" name="Improved Swimming" desc="You swim faster than you normally could." />
+    <feat type="Complete Warrior page 101" name="Improved Toughness" desc="You are significantly tougher than normal." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Improved Toughness" desc="You are significantly tougher than normal." />
+    <feat type="Monster Manual III page 207" name="Improved Toughness" desc="A creature with this feat is significantly tougher  than normal." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Trip" desc="You are trained not only in tripping opponents  safely but also following through with an attack." />
+    <feat type="Ghostwalk page 36" name="Improved Turn Resistance" desc="You are better able to resist the channeling  of positive or negative energy by clerics and similar classes." />
+    <feat type="Libris Mortis: The Book of the Dead page 27" name="Improved Turn Resistance" desc="You have a better than normal chance to resist  turning." />
+    <feat type="Savage Species page 36" name="Improved Turn Resistance" desc="You have a better than normal chance to resist  turning." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Turning" desc="Your turning or rebuking attempts are more powerful  than normal." />
+    <feat type="Complete Warrior page 101" name="Improved Two-Weapon Defense" desc="You gain a significant defensive advantage while  fighting with two weapons." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Two-Weapon Fighting" desc="You are an expert in fighting two-handed." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Improved Unarmed Strike" desc="You are skilled at fighting while unarmed." />
+    <feat type="Complete Warrior page 101" name="Improved Weapon Familiarity" desc="You are familiar with all exotic weapons common  to your people." />
+    <feat type="Races of Stone page 141" name="Improved Weapon Familiarity" desc="You are familiar with all exotic weapons common  to your people." />
+    <feat type="Savage Species page 36" name="Improved Web" desc="You gain additional utility from your webs." />
+    <feat type="Epic Level Handbook page 60" name="Improved Whirlwind Attack" desc="You become a blurry whirlwind of attacks, striking  out at all enemies near your position." />
+    <feat type="Epic Level Handbook page 60" name="Incite Rage" desc="You can incite allies into a rage." />
+    <feat type="Ghostwalk page 36" name="Incorporeal Form" desc="You can become incorporeal even when you would  otherwise be forced to manifest fully." />
+    <feat type="Ghostwalk page 36" name="Incorporeal Spell Targeting" desc="You know how to cast your spells so they're  more likely to affect incorporeal creatures." />
+    <feat type="Ghostwalk page 36" name="Incorporeal Target Fighting" desc="You know how to fight incorporeal creatures  in melee." />
+    <feat type="Unearthed Arcana page 93" name="Ineluctable Echo" desc="Those who use words of power around you hear  the sound of their own voices." />
+    <feat type="Races of Faerun page 165" name="Infernal Bargainer" desc="You are comfortable making deals with powerful  entities from the Lower Planes." />
+    <feat type="Epic Level Handbook page 61" name="Infinite Deflection" desc="You can deflect an infinite number of projectiles." />
+    <feat type="Lords of Madness page 180" name="Inhuman Reach" desc="Your arms elongate, allowing you to touch the  floor with your hands." />
+    <feat type="Lords of Madness page 180" name="Inhuman Vision" desc="You possess the inhuman eyes of some strange  creature." />
+    <feat type="Champions of Ruin page 23" name="Initiate of Ghaunadaur" desc="You have learned the dread secrets of the god  of oozes, slimes, jellies, and outcasts." />
+    <feat type="Champions of Ruin page 24" name="Initiate of Gruumsh" desc="The singular eye of the great orc god Gruumsh  watches over you." />
+    <feat type="Champions of Ruin page 24" name="Initiate of Kossuth" desc="You have faced the fierce elemental flame and  unlocked some of the secrets of Kossuth's church." />
+    <feat type="Champions of Ruin page 24" name="Initiate of Loviatar" desc="With great pain comes great power. This and  other secrets you have learned from the church of Loviatar." />
+    <feat type="Shining South page 20" name="Initiate of Loviatar" desc="You have been initiated into the greatest secrets  of Loviatar's church." />
+    <feat type="Champions of Ruin page 24" name="Initiate of Shar" desc="You have been initiated into the greatest secrets  of Shar's church." />
+    <feat type="City of Splendors: Waterdeep page 145" name="Initiate of Shar" desc="You have been initiated into the greatest secrets  of Shar's church." />
+    <feat type="Champions of Ruin page 25" name="Initiate of Varae" desc="You fervently worship Varae, the serpentine  goddess, and guard well the secrets of your faith." />
+    <feat type="Complete Arcane page 80" name="Innate Spell" desc="You have mastered a spell so thoroughly that  you can now use it as a spell-like ability." />
+    <feat type="Forgotten Realms Campaign Setting page 36" name="Innate Spell" desc="You have mastered a spell so thoroughly that  you can now use it as a spell-like ability." />
+    <feat type="Player's Guide to Faerun page 39" name="Innate Spell" desc="You have mastered a spell so thoroughly that  you can now use it as a spell-like ability." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 41" name="Innate Spell" desc="You have mastered a spell so thoroughly that  you can now use it as a spell-like ability." />
+    <feat type="Expanded Psionics Handbook page 48" name="Inquisitor" desc="You know when others lie." />
+    <feat type="Player's Guide to Faerun page 136" name="Inscribe Epic Runes" desc="You can inscribe runes of epic power." />
+    <feat type="Forgotten Realms Campaign Setting page 36" name="Inscribe Rune" desc="You can create magic runes that hold spells  until triggered." />
+    <feat type="Player's Guide to Faerun page 40" name="Inscribe Rune" desc="You can create magic runes that hold spells  until triggered." />
+    <feat type="Races of Destiny page 153" name="Inside Connection" desc="Choose a specific organization. You have strong  personal connections within that organization, as well as insight into its  membership." />
+    <feat type="Forgotten Realms Campaign Setting page 36" name="Insidious Magic" desc="You can use the Shadow Weave to make your spells  harder for Weave users to detect." />
+    <feat type="Player's Guide to Faerun page 40" name="Insidious Magic" desc="You can use the Shadow Weave to make your spells  harder for Weave users to dispel." />
+    <feat type="Complete Arcane page 80" name="Insightful" desc="You possess a magical understanding of the workings  of arcane detection." />
+    <feat type="Complete Adventurer page 110" name="Insightful Reflexes" desc="Your keen intellect allows you an uncanny knack  for evading dangerous effects." />
+    <feat type="Heroes of Battle page 98" name="Inspirational Leadership" desc="Your cohort and followers are exceptionally  faithful to your cause." />
+    <feat type="Epic Level Handbook page 61" name="Inspire Excellence" desc="You can improve the abilities of your comrades  through your performance." />
+    <feat type="Races of Stone page 141" name="Inspire Spellpower" desc="You can use your bardic music to increase the  power of your allies' spells." />
+    <feat type="Epic Level Handbook page 61" name="Instant Reload" desc="Choose one type of crossbow, such as heavy crossbow.  You can fire that type of crossbow as fast as a bow." />
+    <feat type="Complete Warrior page 102" name="Instantaneous Rage" desc="You activate your rage instantly." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 23" name="Instantaneous Rage" desc="You activate your rage instantly." />
+    <feat type="Epic Level Handbook page 61" name="Intensify Spell" desc="You can cast spells with exceptionally great  effect." />
+    <feat type="Complete Warrior page 102" name="Intimidating Rage" desc="Your rage engenders fear in your opponents." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Intimidating Rage" desc="Your rage engenders fear in your opponents." />
+    <feat type="Book of Exalted Deeds page 44" name="Intuitive Attack" desc="You fight by faith more than brute strength." />
+    <feat type="Savage Species page 36" name="Inured to Energy" desc="You can resist energy attacks more efficiently  than normal." />
+    <feat type="Races of Stone page 141" name="Invest Armor" desc="You can charge your armor with additional protective  qualities." />
+    <feat type="Eberron Campaign Setting page 55" name="Investigate" desc="You can use the Search skill to find and analyze  clues at the scene of a crime or a mystery." />
+    <feat type="Player's Handbook v.3.5 page 96" name="Investigator" desc="You have a knack for finding information." />
+    <feat type="Savage Species page 36" name="Involuntary Rage" desc="Extreme pain drives you berserk." />
+    <feat type="Races of Faerun page 165" name="Iron Mind" desc="You are descended from duergar who escaped enslavement  by the illithids. The blood of these psionic-resistant former thralls runs  thick in your veins." />
+    <feat type="Player's Handbook v.3.5 page 97" name="Iron Will" desc="You have a stronger will than normal." />
+    <feat type="Complete Adventurer page 113" name="Ironskin Chant" desc="You can channel the power of your bardic music  to enable yourself to ignore minor injuries." />
+    <feat type="Races of Eberron page 119" name="Ironwood Body" desc="Your body is crafted with a layer of hard ironwood  that cushions blows." />
+    <feat type="Serpent Kingdoms page 146" name="Irresistible Gaze" desc="Your gaze attack is more potent than normal." />
+    <feat type="Savage Species page 37" name="Irresistible Gaze" desc="Your gaze attack is more potent than normal." />
+    <feat type="Lost Empires of Faerun page 8" name="Item Reprieve" desc="You learn how to use items from a school of  magic previously prohibited to you." />
+    <feat type="Complete Adventurer page 110" name="Jack of All Trades" desc="You have picked up a smattering of even the  most obscure skills." />
+    <feat type="Deities and Demigods page 51" name="Jack of All Trades" desc="The deity has picked up a smattering of even  the most obscure skills." />
+    <feat type="Faiths &amp; Pantheons page 214" name="Jack of All Trades" desc="You've picked up a smattering of even the most  obscure skills." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Jack of All Trades" desc="You've picked up a smattering of even the most  obscure skills." />
+    <feat type="Races of Eberron page 119" name="Jaws of Death" desc="Gnashing teeth and a powerful set of jaws allow  you to bite foes." />
+    <feat type="Lost Empires of Faerun page 8" name="Jergal's Pact" desc="You have made a bargain with Jergal, seneschal  to the god of death." />
+    <feat type="City of Splendors: Waterdeep page 145" name="Jester's Magic" desc="You are a skilled master of magical jests, capable  of inciting audiences to laughter or lulling them to sleep." />
+    <feat type="Races of Faerun page 166" name="Jotunbrud" desc="You are descended from the giants who ruled  the mountain-spanning empire of Ostoria in ages past, and possess a truly  impressive stature." />
+    <feat type="Sandstorm page 50" name="Judged by Aurifar" desc="Aurifar, the Caliph of the Sky, has judged you,  and he shows you special favor." />
+    <feat type="Races of Faerun page 166" name="Jungle Stamina" desc="You are acclimated to the disease-ridden jungles  of southwestern Faerun." />
+    <feat type="Races of Eberron page 118" name="Kalashtar Thoughtshifter" desc="You have learned to control your mind blade  for maximum impact in battle." />
+    <feat type="Oriental Adventures page 63" name="Kami's Intuition" desc="You are descended from Shinjo, the first Unicorn,  the kindest and most compassionate of the kami." />
+    <feat type="Complete Warrior page 102" name="Karmic Strike" desc="You have learned to strike when your opponent  is more vulnerable -- the same instant your opponent strikes you." />
+    <feat type="Oriental Adventures page 63" name="Karmic Strike" desc="You have learned to strike when your opponent  is more vulnerable -- the same instant your opponent strikes you." />
+    <feat type="Oriental Adventures page 64" name="Karmic Twin" desc="You are descended from Bayushi, the first Scorpion,  whos love for his daughter proved his final downfall." />
+    <feat type="Oriental Adventures page 64" name="Keen Intellect" desc="You are descended from Agasha, the founder of  the original Dragon shugenja school, a shugenja known for her keen intellect  and powers of observation." />
+    <feat type="Epic Level Handbook page 61" name="Keen Strike" desc="Your unarmed strikes become as sharp as blades." />
+    <feat type="Oriental Adventures page 64" name="Ki Shout" desc="You can bellow forth a ki-empowered shout  that strikes terror into your enemies." />
+    <feat type="Complete Warrior page 102" name="Kiai Shout" desc="You can bellow forth a shout that strikes terror  into your enemies." />
+    <feat type="Ghostwalk page 36" name="Kihu-Sherem Guardian" desc="You are one of the Kihu-Sherem, magically altered  in the womb to allow you to better protect the sorcerers of your homeland." />
+    <feat type="Races of the Wild page 151" name="Killoren Ancient" desc="You favor the killoren aspect of the ancient." />
+    <feat type="Races of the Wild page 151" name="Killoren Destroyer" desc="You favor the killoren aspect of the destroyer." />
+    <feat type="Races of the Wild page 151" name="Killoren Hunter" desc="You favor the killoren aspect of the hunter" />
+    <feat type="Player's Guide to Faerun page 40" name="Knifefighter" desc="You're an expert at using weapons in a grapple." />
+    <feat type="Book of Exalted Deeds page 44" name="Knight of Stars " desc="You swear allegiance to the Court of Stars,  the paragons of the eladrin, and in exchange gain power to act on their  behalf." />
+    <feat type="Eberron Campaign Setting page 56" name="Knight Training" desc="You are part of a knightly order that combines  the divine calling of the paladin class with another form of training." />
+    <feat type="Races of Stone page 142" name="Knockback" desc="By putting your bulk behind a blow, you can  push your enemy backward." />
+    <feat type="Deities and Demigods page 51" name="Knock-Down" desc="The deity's mighty blows can knock foes off  their feet." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 7" name="Knock-Down" desc="Your mighty blows can knock foes off their feet." />
+    <feat type="Stronghold Builder's Guidebook page 10" name="Landlord" desc="By knowing the right nobles, making contacts  with masons and artisans, or performing great deeds for a liege-lord, you  have resources that help you build and expand your stronghold." />
+    <feat type="Races of Faerun page 166" name="Landwalker" desc="You can survive out of water for a longer period  of time than most of your kind." />
+    <feat type="Stormwrack page 92" name="Landwalker" desc="You can survive out of water for a longer period  of time than most of your kind." />
+    <feat type="Draconomicon page 71" name="Large and In Charge" desc="You can prevent opponents from closing inside  your reach." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 61" name="Large and in Charge" desc="You can prevent opponents from closing inside  your reach." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Lasting Inspiration" desc="Your songs continue to inspire allies long after  your words have faded." />
+    <feat type="Epic Level Handbook page 61" name="Lasting Inspiration" desc="Your songs continue to inspire allies long after  your words have faded." />
+    <feat type="Libris Mortis: The Book of the Dead page 28" name="Lasting Life" desc="You can shed negative levels with an act of  will." />
+    <feat type="Races of Destiny page 155" name="Law Inviolate" desc="Your unshakable faith in St. Cuthbert allows  you to better apprehend fugitives or overcome villains who transgress the  law." />
+    <feat type="Player's Handbook v.3.5 page 97" name="Leadership" desc="You are the sort of person others want to follow,  and you have done some work attempting to recruit cohorts and followers." />
+    <feat type="Complete Adventurer page 110" name="Leap Attack" desc="You can combine a powerful charge and a mighty  leap into one devastating attack." />
+    <feat type="Eberron Campaign Setting page 56" name="Least Dragonmark" desc="You have a least dragonmark." />
+    <feat type="WL page 14" name="Least Legacy" desc="You awaken the basic abilities of a specific  item of legacy." />
+    <feat type="WL page 15" name="Legacy Focus" desc="Your item's legacy abilities are more potent  than normal." />
+    <feat type="Complete Adventurer page 192" name="Legendary Acrobat" desc="You can balance and tumble much more easily  than a normal person." />
+    <feat type="Eberron Campaign Setting page 56" name="Legendary Artisan" desc="You have mastered the method of creating magic  items." />
+    <feat type="Complete Adventurer page 192" name="Legendary Climber" desc="You can climb rapidly much more easily than  a normal person." />
+    <feat type="Epic Level Handbook page 61" name="Legendary Climber" desc="You can climb rapidly much more easily than  a normal person." />
+    <feat type="Epic Level Handbook page 62" name="Legendary Commander" desc="You attract and lead great armies of followers  through sheer force of personality." />
+    <feat type="Complete Adventurer page 192" name="Legendary Leaper" desc="You can cover great distances with only a brief  start." />
+    <feat type="Epic Level Handbook page 62" name="Legendary Leaper" desc="You can jump much farther than normal for your  size." />
+    <feat type="Complete Warrior page 152" name="Legendary Rider" desc="You can ride a mount in combat with ease, even  bareback." />
+    <feat type="Epic Level Handbook page 62" name="Legendary Rider" desc="You can ride any mount without penalty (even  bareback) and can control any mount in combat." />
+    <feat type="Complete Adventurer page 192" name="Legendary Tracker" desc="You can track prey across or through the water,  or even through the air." />
+    <feat type="Epic Level Handbook page 62" name="Legendary Tracker" desc="You can track prey across or through the water,  or even through the air." />
+    <feat type="Epic Level Handbook page 62" name="Legendary Wrestler" desc="You are exceptionally proficient at grappling." />
+    <feat type="Eberron Campaign Setting page 56" name="Lesser Dragonmark" desc="You have a lesser dragonmark." />
+    <feat type="WL page 15" name="Lesser Legacy" desc="You awaken more powerful abilities of a specific  item of legacy." />
+    <feat type="Book of Vile Darkness page 49" name="Lichloved" desc="By repeatedly committing perverted sex acts  with the undead, the character gains dread powers." />
+    <feat type="Libris Mortis: The Book of the Dead page 28" name="Life Drain" desc="You drain additional life energy from your foes." />
+    <feat type="Unearthed Arcana page 93" name="Life Leech" desc="You automatically try to steal the last bit  of life energy from anyone nearby." />
+    <feat type="Libris Mortis: The Book of the Dead page 28" name="Lifebond" desc="Select a specific living creature that is friendly  to you. You create a special bond with that creature." />
+    <feat type="Libris Mortis: The Book of the Dead page 28" name="Lifesense" desc="You see the light that all living creatures  emit." />
+    <feat type="Sandstorm page 51" name="Light of Aurifar" desc="Undead that you turn or rebuke immolate." />
+    <feat type="Races of Faerun page 166" name="Light to Daylight" desc="Your inherent ability to create light is more  powerful than normal." />
+    <feat type="Races of Faerun page 166" name="Lightbringer" desc="You can channel positive energy into your spells  so that they glow with holy power." />
+    <feat type="Races of the Wild page 151" name="Lightfeet" desc="You have an incredibly soft step, making it  difficult to track or hear you." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 7" name="Lightning Fists" desc="Your skill and agility allow you to attempt  a series of blindingly fast blows." />
+    <feat type="Complete Warrior page 113" name="Lightning Mace" desc="You are a master of fighting with two maces  at the same time." />
+    <feat type="Player's Handbook v.3.5 page 97" name="Lightning Reflexes" desc="You have faster than normal reflexes." />
+    <feat type="Draconomicon page 71" name="Lingering Breath" desc="Your breath weapon forms a lingering cloud." />
+    <feat type="Epic Level Handbook page 62" name="Lingering Damage" desc="Your sneak attacks continue to deal damage even  after you strike." />
+    <feat type="Complete Adventurer page 111" name="Lingering Song" desc="Your inspirational bardic music stays with the  listeners long after the last note has died away." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Lingering Song" desc="Your bardic music stays with the listeners long  after the last note has died away." />
+    <feat type="Champions of Ruin page 20" name="Lingering Spell" desc="Residual eldritch energy from your spell continues  to harm your enemies after the spell's main effect has expired." />
+    <feat type="Oriental Adventures page 64" name="Lion Spy" desc="Your ancestor, Akodo Shinju, was the greatest  spy of the Lion clan." />
+    <feat type="Shining South page 20" name="Lion Tribe Warrior" desc="You have learned how to pounce on your foes,  like the lion that roams your lands." />
+    <feat type="Complete Divine page 82" name="Lion's Pounce" desc="You can deliver a terrible attack at the end  of a charge." />
+    <feat type="Unearthed Arcana page 94" name="Live My Nightmare" desc="Those who magically pry into your mind become  privy to your most frightening dreams." />
+    <feat type="Player's Guide to Faerun page 176" name="Lliira's Blessing" desc="Thanks to the favor of the goddess of freedom,  you are difficult to restrain." />
+    <feat type="Races of Faerun page 166" name="Lolth's Blessing" desc="The Spider Queen has blessed you with additional  magical abilities." />
+    <feat type="Underdark page 26" name="Lolth's Meat" desc="Like all drow raised in cities that are ruled  by Lolth's priestesses, you know that you exist only to provide your goddess  with food and pleasure. This knowledge lends you a certain bloodthirsty  readiness." />
+    <feat type="UE page 44" name="Long Reach" desc="You know how to use your great stature to reach  an opponent more than 5 feet away with a spearlike weapon." />
+    <feat type="Eberron Campaign Setting page 57" name="Longstride Elite" desc="Your shifter trait improves." />
+    <feat type="Races of Eberron page 114" name="Longstride Elite" desc="Your longstride shifter trait improves." />
+    <feat type="Races of Eberron page 114" name="Longtooth Elite" desc="Your longtooth shifter trait improves." />
+    <feat type="Complete Arcane page 80" name="Lord of the Uttercold" desc="Through careful study of the Elemental Planes  and their interactions with the Negative Energy Plane, you have learned  to wield the uttercold." />
+    <feat type="Races of Faerun page 166" name="Low Blow" desc="You can get underfoot and attack creatures larger  than you." />
+    <feat type="Unearthed Arcana page 182" name="Low Profile" desc="You are less famous than others of your class  and level, or you wish to maintain a less visible presence than others of  your station." />
+    <feat type="Forgotten Realms Campaign Setting page 36" name="Luck of Heroes" desc="Your land is known for producing heroes; you  receive a luck bonus on all saving throws." />
+    <feat type="Oriental Adventures page 64" name="Luck of Heroes" desc="You are descended from the quick-footed and  quick-witted Hiruma, the archetypal hunter and scout." />
+    <feat type="Player's Guide to Faerun page 40" name="Luck of Heroes" desc="Your land is known for producing heroes." />
+    <feat type="City of Splendors: Waterdeep page 146" name="Lunar Magic" desc="Your spells and spell-like abilities are tied  to the phase of the moon, rising and falling with the strength of Selune." />
+    <feat type="Faiths &amp; Pantheons page 214" name="Lycanthropic Spell" desc="You cast spells while in your lycanthropic animal  form." />
+    <feat type="Complete Adventurer page 113" name="Lyric Spell" desc="You can channel the power of your bardic music  into your magic, allowing you to expend uses of your bardic music ability  to cast spells." />
+    <feat type="Complete Arcane page 81" name="Mage Slayer" desc="You have studied the ways and weaknesses of  spellcasters and can time your attacks and defenses against them expertly." />
+    <feat type="Miniatures Handbook page 27" name="Mage Slayer" desc="You have studied the ways and weaknesses of  spellcasters and can time your attacks and defenses against them expertly." />
+    <feat type="Oriental Adventures page 64" name="Magic in the Blood" desc="You claim a karmic link with Iuchi, one of the  most resourceful shugenjas in early Rokugan." />
+    <feat type="Player's Guide to Faerun page 40" name="Magic in the Blood" desc="You have a knack for getting the most out of  your innate magic abilities." />
+    <feat type="Races of the Wild page 152" name="Magic of the Land" desc="Your intimate understanding of the natural world  allows you to imbue your spells with life-giving magical power from the  land itself." />
+    <feat type="Player's Handbook v.3.5 page 97" name="Magical Aptitude" desc="You have a knack for magical endeavors." />
+    <feat type="Forgotten Realms Campaign Setting page 36" name="Magical Artisan" desc="You have mastered the method of creating certain  magic items." />
+    <feat type="Oriental Adventures page 64" name="Magical Artisan" desc="You are descended from Asahina Yajinden, a shugenja  of Crane clan who became the greatest lieutenant of the dread sorcerer Iuchiban." />
+    <feat type="Player's Guide to Faerun page 41" name="Magical Artisan" desc="You have mastered the method of creating a certain  kind of magic item." />
+    <feat type="Complete Divine page 90" name="Magical Beast Wild Shape" desc="You can wild shape into magical beast form." />
+    <feat type="Epic Level Handbook page 62" name="Magical Beast Wild Shape" desc="You can wild shape into magical beast form." />
+    <feat type="Forgotten Realms Campaign Setting page 36" name="Magical Training" desc="Every crafter and laborer knows a cantrip or  two to ease her work." />
+    <feat type="Player's Guide to Faerun page 41" name="Magical Training" desc="You come from a land where cantrips are taught  to all who have the aptitude to learn magic." />
+    <feat type="Oriental Adventures page 64" name="Magistrate's Mind" desc="You claim descent from Soshi Saibankan, a great  Scorpion judge who helped establish the Empire's institution of Emerald  magistrates." />
+    <feat type="Ghostwalk page 36" name="Malevolence" desc="You can possess a creature and control its actions." />
+    <feat type="Book of Vile Darkness page 49" name="Malign Spell Focus" desc="The character's spells that have the evil descriptor  are more potent than normal due to a deal she makes with an evil power." />
+    <feat type="Champions of Ruin page 20" name="Malign Spell Focus" desc="Your evil spells are more potent than normal  due to a deal forged with an evil power." />
+    <feat type="Sharn: City of Towers page 157" name="Manifest Flight" desc="You have learned to make use of the manifest  zone in Sharn to improve your natural ability to fly." />
+    <feat type="Sharn: City of Towers page 157" name="Manifest Leap" desc="You have learned to make use of the manifest  zone in Sharn to increase your ability to jump and reduce the damage you  take when you fall." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 7" name="Mantis Leap" desc="You deliver a powerful attack after making a  jump." />
+    <feat type="Oriental Adventures page 64" name="Many Masks" desc="You are descended from Shosuro Furuyari, an  important Scorpion playwright." />
+    <feat type="Epic Level Handbook page 70" name="Manyshot" desc="You can fire multiple arrows as a single attack  against a nearby target." />
+    <feat type="Player's Handbook v.3.5 page 97" name="Manyshot" desc="You can fire multiple arrows simultaneously  against a nearby target." />
+    <feat type="Frostburn page 49" name="Mark of Hleid" desc="You bear a mark that identifies you as an ally  of the church of Hleid and grants you supernatural qualities." />
+    <feat type="Races of Stone page 142" name="Markings of the Blessed" desc="Your skin markings shift into a pattern that  resists a wide array of harmful effects in times of trouble." />
+    <feat type="Races of Stone page 142" name="Markings of the Hunter" desc="Your skin markings shift into a pattern that  makes you hard to get the drop on." />
+    <feat type="Races of Stone page 142" name="Markings of the Magi" desc="Your skin markings shift into a pattern that  denotes you as having strong magical talent." />
+    <feat type="Races of Stone page 142" name="Markings of the Maker" desc="Your skin markings shift into a pattern that  gives you fate's edge when using skills." />
+    <feat type="Races of Stone page 142" name="Markings of the Warrior" desc="Your skin markings have shifted over time into  a pattern that gives you fate's deathly accuracy in times of trouble." />
+    <feat type="Miniatures Handbook page 27" name="Martial Throw" desc="You can switch positions with an opponent you  hit in melee by throwing that opponent." />
+    <feat type="Player's Handbook v.3.5 page 97" name="Martial Weapon Proficiency" desc="Choose a type of martial weapon. You understand  how to use that type of martial weapon in combat." />
+    <feat type="WL page 15" name="Master Legacy" desc="You temporarily gain access to legacy abilities  beyond your normal reach." />
+    <feat type="Races of Eberron page 109" name="Master Linguist" desc="You have a broad knowledge of language." />
+    <feat type="Complete Arcane page 192" name="Master Staff" desc="You can activate a staff without using a charge." />
+    <feat type="Epic Level Handbook page 62" name="Master Staff" desc="You can activate a staff without using a charge." />
+    <feat type="Complete Arcane page 192" name="Master Wand" desc="You can activate a wand without using a charge." />
+    <feat type="Epic Level Handbook page 62" name="Master Wand" desc="You can activate a wand without using a charge." />
+    <feat type="Draconomicon page 71" name="Maximize Breath" desc="You can take a full-round action to use your  breath weapon to maximum effect." />
+    <feat type="Expanded Psionics Handbook page 48" name="Maximize Power" desc="You can manifest powers to maximum effect." />
+    <feat type="Player's Handbook v.3.5 page 97" name="Maximize Spell" desc="You can cast spells to maximum effect." />
+    <feat type="Complete Arcane page 81" name="Maximize Spell-Like Ability" desc="You can use a spell-like ability at its maximum  effect." />
+    <feat type="Oriental Adventures page 81" name="Meditation of War Mastery" desc="You have mastered the martial arts style of  'Meditation of War' -- a hard/soft form emphasizing weapon use and strikes  to pressure points." />
+    <feat type="Lords of Madness page 22" name="Memory Eater" desc="An aboleth with this feat is particularly adept  at extracting memories and knowledge from the bodies of those it consumes." />
+    <feat type="Races of Destiny page 155" name="Menacing Demeanor" desc="You can tap into your savage heritage to improve  your intimidation techniques." />
+    <feat type="Expanded Psionics Handbook page 48" name="Mental Leap" desc="You can make amazing jumps." />
+    <feat type="Expanded Psionics Handbook page 48" name="Mental Resistance" desc="Your mind is armored against mental intrusion." />
+    <feat type="Dungeon Master's Guide II page 176" name="Mentor" desc="A character who takes this feat has offered  his knowledge and skill to a lower-level NPC and takes that NPC on as an  apprentice." />
+    <feat type="Forgotten Realms Campaign Setting page 36" name="Mercantile Background" desc="You come from a family that excels at a particular  trade and knows well the value of any kind of trade good or commodity." />
+    <feat type="Player's Guide to Faerun page 41" name="Mercantile Background" desc="You come from a wealthy family with numerous  contacts in the trading costers and craft guilds of Faerun's bustling cities." />
+    <feat type="Races of Faerun page 166" name="Metallurgy" desc="You are skilled in the act of metallurgy, creating  metal alloys both for their appearance and their properties." />
+    <feat type="Races of Stone page 142" name="Metamagic Song" desc="You can channel the power of your bardic music  into your magic, allowing you to pay the cost of metamagic feats by spending  uses of your bardic music ability." />
+    <feat type="Expanded Psionics Handbook page 48" name="Metamorphic Transfer" desc="You can gain a supernatural ability of a metamorphed  form." />
+    <feat type="Champions of Ruin page 25" name="Metanode Spell" desc="You cast metamagic spells to greater effect  in nodes to which you are attuned than elsewhere." />
+    <feat type="Underdark page 26" name="Metanode Spell" desc="You cast metamagic spells to greater effect  in earth nodes than elsewhere." />
+    <feat type="Lords of Madness page 45" name="Metaray" desc="A beholder with this feat can apply the effects  of metamagic feats to its eye rays." />
+    <feat type="Races of Faerun page 166" name="Might Makes Right" desc="Your great strength draws more followers." />
+    <feat type="Savage Species page 37" name="Mighty Leaping" desc="You have developed your leg muscles and trained  yourself to make mighty leaps." />
+    <feat type="Epic Level Handbook page 63" name="Mighty Rage" desc="Your rage becomes even more powerful than normal." />
+    <feat type="Savage Species page 37" name="Mighty Roar" desc="You unsettle opponents with a dreadful roar  as you attack." />
+    <feat type="Oriental Adventures page 80" name="Mighty Works Mastery I" desc="You have mastered the initial secrets of the  'Mighty Works' martial arts style -- a hard/soft form emphasizing locks  and hand strikes." />
+    <feat type="Oriental Adventures page 80" name="Mighty Works Mastery II" desc="You have mastered the deeper secrets of the  'Mighty Works' martial arts style." />
+    <feat type="Forgotten Realms Campaign Setting page 36" name="Militia" desc="You served in a local militia, training with  weapons suitable for use on the battlefield." />
+    <feat type="Ghostwalk page 37" name="Militia" desc="You served in a local militia, training with  weapons suitable for use on the battlefield." />
+    <feat type="Player's Guide to Faerun page 41" name="Militia" desc="Your people rely on a well-trained and well-armed  militia to defend their land." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Mind Over Body" desc="The arcane spellcasters of some lands have learned  to overcome the frailties of the body with the unyielding power of the mind." />
+    <feat type="Player's Guide to Faerun page 41" name="Mind Over Body" desc="The aesthetics and mystics of your homeland  have learned to overcome the frailties of the body with the unyielding power  of the mind." />
+    <feat type="Expanded Psionics Handbook page 48" name="Mind Over Body" desc="Your ability damage heals more rapidly." />
+    <feat type="Lords of Madness page 126" name="Mindsight" desc="A creature that has this feat possesses innate  telepathic ability that allows it to precisely pinpoint other thinking beings  within range of its telepathy." />
+    <feat type="Ghostwalk page 37" name="Minor Malevolence" desc="You can possess a creature for a short while  and control its actions." />
+    <feat type="Races of Stone page 142" name="Misleading Song" desc="You can channel the power of your bardic music  to temporarily increase the power of your illusion spells." />
+    <feat type="Eberron Campaign Setting page 57" name="Mithral Body" desc="A warforged character's body can be crafted  with a layer of mithral that provides some protection without hindering  speed or gracefulness." />
+    <feat type="Monster Manual III page 192" name="Mithral Body" desc="A warforged character's body can be crafted  with a layer of mithral that provides some protection without hindering  speed or gracefulness." />
+    <feat type="Races of Eberron page 119" name="Mithral Body" desc="Your warforged body can be crafted with a layer  of mithral that provides some protection without hindering speed or gracefulness." />
+    <feat type="Eberron Campaign Setting page 57" name="Mithral Fluidity" desc="Your movements are smoother and more fluid than  those of other warforged." />
+    <feat type="Monster Manual III page 192" name="Mithral Fluidity" desc="Your movements are smoother and more fluid than  those of other warforged." />
+    <feat type="Epic Level Handbook page 63" name="Mobile Defense" desc="You can adjust your position while maintaining  a defensive stance." />
+    <feat type="Complete Adventurer page 111" name="Mobile Spell-Casting" desc="Your focused concentration allows you to move  while casting a spell." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Mobility" desc="You are skilled at dodging past opponents and  avoiding blows." />
+    <feat type="Unearthed Arcana page 94" name="Momentary Alteration" desc="You can briefly transform yourself into a second  form, acquiring its physical qualities." />
+    <feat type="Eberron Campaign Setting page 57" name="Monastic Training" desc="You are part of an order that combines the monastic  discipline of the monk class with another form of training." />
+    <feat type="Complete Warrior page 103" name="Monkey Grip" desc="You are able to use a larger weapon than other  people your size." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 7" name="Monkey Grip" desc="You use a wider variety of sizes of weapons." />
+    <feat type="Races of Stone page 142" name="Moradin's Smile" desc="Through the favor of Moradin, you are skilled  at interacting with others." />
+    <feat type="Book of Vile Darkness page 49" name="Mortalbane" desc="The creature can make a spell-like ability particularly  deadly to mortals." />
+    <feat type="Champions of Ruin page 20" name="Mortifying Attack" desc="Those who witness your brutal death attack are  unnerved and jarred by the experience." />
+    <feat type="Libris Mortis: The Book of the Dead page 28" name="Mother Cyst" desc="You gain the ability to cast necrotic cyst spells  by growing a cyst of your own." />
+    <feat type="Races of Stone page 142" name="Mountain Warrior" desc="You are adept at fighting on the uneven ground  of mountainous terrain." />
+    <feat type="Frostburn page 49" name="Mountaineer" desc="You are a particularly gifted explorer and mountain  climber." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Mounted Archery" desc="You are skilled at using ranged weapons while  mounted." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Mounted Combat" desc="You are skilled in mounted combat." />
+    <feat type="Heroes of Battle page 98" name="Mounted Mobility" desc="You are skilled at dodging past opponents while  mounted." />
+    <feat type="Miniatures Handbook page 27" name="Mounting Casting" desc="You are skilled at casting spells while riding  a mount." />
+    <feat type="Races of Eberron page 109" name="Mror Stalwart" desc="You have been trained to make devastating strikes  with the weapons of the dwarves of the Mror Holds." />
+    <feat type="Monster Manual v.3.5 page 304" name="Multiattack" desc="The creature is adept at using all its natural  weapons at once." />
+    <feat type="Monster Manual II page 18" name="Multiattack" desc="The creature is adept at using all its natural  weapons at once." />
+    <feat type="Monster Manual III page 207" name="Multiattack" desc="A creature with this feat is adept at using  all its natural weapons at once." />
+    <feat type="Monster Compendium: Monsters of Faerun page 9" name="Multiattack" desc="The creature is adept at using all its natural  weapons at once." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Multiattack" desc="You are adept at using all your natural weapons  at once." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Multicultural" desc="You blend in well with members of another race." />
+    <feat type="Monster Manual II page 18" name="Multidexterity" desc="The creature is adept at using all its hands  in combat." />
+    <feat type="Monster Compendium: Monsters of Faerun page 9" name="Multidexterity" desc="The creature is adept at using all its hands  in combat." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Multidexterity" desc="You are skilled at utilizing all your hands  in combat." />
+    <feat type="Serpent Kingdoms page 146" name="Multigrab" desc="You can grapple enemies more firmly than normal  with your natural attacks." />
+    <feat type="Savage Species page 37" name="Multigrab" desc="You can grapple enemies more firmly than normal  with your natural attacks." />
+    <feat type="Lost Empires of Faerun page 8" name="Multilingual" desc="You have an uncanny knack for languages." />
+    <feat type="Draconomicon page 72" name="Multisnatch" desc="You can grapple enemies more firmly with only  one of your natural attacks." />
+    <feat type="Epic Level Handbook page 63" name="Multispell" desc="You can cast an additional quickened spell in  a round." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 62" name="Multitasking" desc="You can perform different tasks with different  limbs." />
+    <feat type="Savage Species page 37" name="Multitasking" desc="You can perform different tasks with different  limbs." />
+    <feat type="Savage Species page 37" name="Multivoice" desc="If you have two or more heads, you can cast  more spells than usual in a round." />
+    <feat type="Monster Manual v.3.5 page 304" name="Multiweapon Fighting" desc="A creature with three or more hands can fight  with a weapon in each hand." />
+    <feat type="Monster Manual II page 18" name="Multiweapon Fighting" desc="A creature with three or more hands can fight  with a weapon in each hand." />
+    <feat type="Monster Compendium: Monsters of Faerun page 9" name="Multiweapon Fighting" desc="A creature with three or more hands can fight  with a weapon in each hand." />
+    <feat type="Epic Level Handbook page 63" name="Multiweapon Rend" desc="You can rend opponents when fighting with more  than two limbs." />
+    <feat type="Eberron Campaign Setting page 57" name="Music of Growth" desc="Your music can enhance the power of animals  and plant creatures." />
+    <feat type="Eberron Campaign Setting page 57" name="Music of Making" desc="Echoing the music of creation, your own performance  enhances any process of creation." />
+    <feat type="Epic Level Handbook page 63" name="Music of the Gods" desc="You can use your bardic music to influence creatures  immune to mind-affecting effects." />
+    <feat type="Lords of Madness page 181" name="Music of the Outer Spheres" desc="You can use your bardic music to create discordant,  insane sounds." />
+    <feat type="Races of Eberron page 110" name="Mutable Body" desc="Your enhanced control over your shapechanging  ability grants you extra power from transmutation spells." />
+    <feat type="Champions of Ruin page 20" name="Mutilator" desc="After striking down your enemy in battle, you  can skillfully mutilate the corpse to prevent others from raising it from  the dead." />
+    <feat type="Expanded Psionics Handbook page 48" name="Narrow Mind" desc="Your ability to concentrate is as keen as an  arrowhead, allowing you to gain your psionic focus even in the most turbulent  situations." />
+    <feat type="Serpent Kingdoms page 146" name="Narrowed Gaze" desc="Your gaze attack has a limited field of effect." />
+    <feat type="Savage Species page 37" name="Narrowed Gaze" desc="Your gaze attack has a limited field of effect." />
+    <feat type="Complete Adventurer page 111" name="Natural Bond" desc="Your bond with your animal companion is exceptionally  strong." />
+    <feat type="Champions of Ruin page 21" name="Natural Bully" desc="You easily terrify weaker adversaries." />
+    <feat type="Planar Handbook page 40" name="Natural Heavyweight" desc="You are descended from creatures native to a  plane of heavy gravity." />
+    <feat type="Heroes of Battle page 98" name="Natural Leader" desc="You have a natural commanding presence." />
+    <feat type="Shining South page 21" name="Natural Scavenger" desc="You are particularly adept at finding food while  on the move." />
+    <feat type="Ghostwalk page 37" name="Natural Spell" desc="You can cast spells while in wild shape or shifted  form." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Natural Spell" desc="You cast spells while in a wild shape." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Natural Spell" desc="You can cast spells while in a wild shape." />
+    <feat type="Races of Stone page 143" name="Natural Trickster" desc="You have greater natural access to your race's  powers of illusion." />
+    <feat type="Unearthed Arcana page 94" name="Naturalized Denizen" desc="You are unusually anchored to your location." />
+    <feat type="Ghostwalk page 37" name="Nauseating Touch" desc="When you touch a living creature, you can make  it nauseated." />
+    <feat type="Libris Mortis: The Book of the Dead page 28" name="Necromantic Might" desc="Undead you control gain benefits when they are  near you." />
+    <feat type="Libris Mortis: The Book of the Dead page 28" name="Necromantic Presence" desc="Undead you control are harder to turn when they  are near you." />
+    <feat type="Complete Arcane page 81" name="Necropolis Born" desc="You possess a magical understanding of the essence  of mortal dread." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Necropotent" desc="Your special melee or ranged attack with one  type of weapon is especially effective against undead." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Necrotic Reserve" desc="You are not immediately destroyed when your  hit points fall to 0 or lower." />
+    <feat type="Complete Divine page 90" name="Negative Energy Burst" desc="You can use your rebuke/command undead ability  to unleash a burst of negative energy." />
+    <feat type="Epic Level Handbook page 63" name="Negative Energy Burst" desc="You can use your rebuke/command undead ability  to unleash a burst of negative energy." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Negotiator" desc="You are good at gauging and swaying attitudes." />
+    <feat type="Book of Exalted Deeds page 44" name="Nemesis" desc="You are the holy bane of creatures of a particular  type." />
+    <feat type="Planar Handbook page 40" name="Neraph Charge" desc="You master the Limbo-native neraph martial art  of motion camouflage when you charge your foe." />
+    <feat type="Planar Handbook page 40" name="Neraph Throw" desc="You master the Limbo-native neraph martial art  of motion camouflage for your thrown weapons." />
+    <feat type="Complete Warrior page 114" name="Net and Trident" desc="You are a master of fighting with the net and  the trident." />
+    <feat type="Lost Empires of Faerun page 8" name="Netherese Battle Curse" desc="You can channel your own arcane energy into  a powerful curse upon those who dare to face you in battle." />
+    <feat type="Complete Arcane page 81" name="Night Haunt" desc="You possess a magical understanding of the workings  of the unseen." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Nimble Bones" desc="Undead you raise or create are faster and more  nimble than normal." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Nimble Fingers" desc="You are adept at manipulating small, delicate  objects." />
+    <feat type="Book of Exalted Deeds page 44" name="Nimbus of Light" desc="You are cloaked in the radiant light that marks  you as a servant of the purest ideals." />
+    <feat type="Races of Faerun page 166" name="Nobody's Fool" desc="You have an uncommon streak of skepticism and  common sense, and have a knack for discerning falsehood from truth." />
+    <feat type="Champions of Ruin page 25" name="Node Defense" desc="You can use the magical power of a node to defend  yourself from harm." />
+    <feat type="Underdark page 26" name="Node Defense" desc="You can use the magical power of a node to defend  yourself from harm." />
+    <feat type="Champions of Ruin page 25" name="Node Sensitive" desc="You can perceive a node just by passing near  it." />
+    <feat type="Underdark page 26" name="Node Sensitive" desc="You can perceive an earth node just by passing  near it." />
+    <feat type="Champions of Ruin page 25" name="Node Spellcasting" desc="You have discovered the secret of the magic  of a particular type of node." />
+    <feat type="Underdark page 26" name="Node Spellcasting" desc="You have discovered the secret of node magic." />
+    <feat type="Champions of Ruin page 26" name="Node Store" desc="You can store a prepared spell in a node to  which you are attuned." />
+    <feat type="Underdark page 26" name="Node Store" desc="You can store a prepared spell in an earth node." />
+    <feat type="Shining South page 21" name="Nomadic Trekker" desc="You are particularly efficient at overland movement  across the great grasslands." />
+    <feat type="Book of Exalted Deeds page 44" name="Nonlethal Substitution" desc="You can modify a spell that uses energy to deal  damage to deal nonlethal damage instead." />
+    <feat type="Complete Arcane page 81" name="Nonlethal Substitution" desc="You can modify an energy spell to deal nonlethal  damage." />
+    <feat type="Planar Handbook page 40" name="Nonverbal Spell" desc="You can cast spells that have verbal components  without actually verbalizing the words." />
+    <feat type="Book of Exalted Deeds page 44" name="Nymph's Kiss" desc="By maintaining an intimate relationship with  a good-aligned fey, you gain some of the characteristics of fey." />
+    <feat type="Complete Divine page 82" name="Oaken Resilience" desc="You can take on the sturdiness of the mighty  oak." />
+    <feat type="Complete Adventurer page 111" name="Obscure Lore" desc="You are a treasure trove of little-known information." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Obscure Lore" desc="You are a treasure trove of little-known information." />
+    <feat type="Complete Arcane page 81" name="Obtain Familiar" desc="You gain a familiar." />
+    <feat type="Lords of Madness page 181" name="Ocular Spell" desc="Your study of the terrible powers of the beholder  has given you insight into new ways to prepare and cast spells." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Off-Hand Parry" desc="You use your off-hand weapon to defend against  melee attacks." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 7" name="Off-Hand Parry" desc="You use your off-hand weapon to defend against  melee attacks." />
+    <feat type="Stormwrack page 93" name="Old Salt" desc="You are an old hand at shipboard life, having  mastered the myriad skills that are required of the experience sailor. Additionally,  you have an eye for the weather." />
+    <feat type="Unearthed Arcana page 94" name="Omniscient Whispers" desc="A constant, barely audible muttering echoes  in your ears, usually beyond your comprehension. But if you focus all your  energy on listening, you sometimes catch a sentence or two that bears directly  on your current situation." />
+    <feat type="Oriental Adventures page 64" name="Oni's Bane" desc="Your ancestor, Isawa Akuma, was a Phoenix shugenja  who sought to understand the mystery of identity." />
+    <feat type="Expanded Psionics Handbook page 48" name="Open Minded" desc="You are naturally able to reroute your memory,  mind, and skill expertise." />
+    <feat type="Complete Adventurer page 111" name="Open Minded" desc="You are naturally able to reroute your memory  and skill expertise." />
+    <feat type="Expanded Psionics Handbook page 48" name="Opportunity Power" desc="You can make power-enhanced attacks of opportunity." />
+    <feat type="Races of Faerun page 167" name="Oral History" desc="You are well versed in the art of storytelling  and the oral history of your culture." />
+    <feat type="Player's Guide to Faerun page 41" name="Otherworldly" desc="Your folk are known for their mystic power and  seem to transcend their mortal forms." />
+    <feat type="Races of Faerun page 167" name="Outsider Wings" desc="You have sprouted wings appropriate to your  heritage, revealing the power of your supernatural bloodline." />
+    <feat type="Expanded Psionics Handbook page 49" name="Overchannel" desc="You burn your life force to strengthen your  powers." />
+    <feat type="Draconomicon page 72" name="Overcome Weakness" desc="You can overcome an innate vulnerability through  sheer willpower." />
+    <feat type="Draconomicon page 106" name="Overhead Thrust" desc="You can deal a nasty attack to anything that  tries to crush or run over you." />
+    <feat type="Complete Adventurer page 111" name="Oversized Two-Weapon Fighting" desc="You are adept at wielding larger than normal  weapons in your off hand." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Overwhelming Critical" desc="Choose one type of melee weapon, such as longsword  or greataxe. With that weapon, you do more damage on a critical hit." />
+    <feat type="Draconomicon page 72" name="Overwhelming Critical" desc="Choose one type of melee weapon, such as a claw  or bite. With that weapon, you deal more damage on a critical hit." />
+    <feat type="Epic Level Handbook page 63" name="Overwhelming Critical" desc="Choose one type of melee weapon, such as longsword  or greataxe. With that weapon, you do more damage on a critical hit." />
+    <feat type="UE page 44" name="Owlbear Berserker" desc="Your fighting style emulates the owlbear, the  totem beast of your berserker lodge." />
+    <feat type="Savage Species page 37" name="Pain Mastery" desc="Injuries send you into a fury, increasing your  physical power." />
+    <feat type="Complete Warrior page 103" name="Pain Touch" desc="You cause intense pain in an opponent with a  successful stunning attack." />
+    <feat type="Oriental Adventures page 64" name="Pain Touch" desc="You cause intense pain in an opponent with a  successful stunning attack." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 8" name="Pain Touch" desc="You cause intense pain in an opponent with a  successful stunning attack." />
+    <feat type="Lords of Madness page 181" name="Parrying Shield" desc="You have studied advanced techniques for battling  foes whose attacks normally bypass armor." />
+    <feat type="Races of Eberron page 110" name="Path of Shadows" desc="You can use dancelike maneuvers to aid your  defense." />
+    <feat type="Savage Species page 37" name="Peak Hopper" desc="You are adapted to a hilly or mountainous environment." />
+    <feat type="Epic Level Handbook page 63" name="Penetrating Damage Reduction" desc="You can bypass a creature's damage reduction." />
+    <feat type="Epic Level Handbook page 63" name="Perfect Health" desc="You are immune to normal diseases and common  poisons." />
+    <feat type="Epic Level Handbook page 63" name="Perfect Multiweapon Fighting" desc="A creature with three or more hands can fight  with a weapon in each hand." />
+    <feat type="Complete Warrior page 152" name="Perfect Two-Weapon Fighting" desc="You can attack with your off-hand weapon as  frequently as with your primary weapon." />
+    <feat type="Epic Level Handbook page 64" name="Perfect Two-Weapon Fighting" desc="You can attack with your off-hand weapon as  frequently as with your primary weapon." />
+    <feat type="Epic Level Handbook page 64" name="Permanent Emanation" desc="One of your personal emanation spells becomes  permanent." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Pernicious Magic" desc="You can use the Shadow Weave to make your spells  harder for Weave users to counter." />
+    <feat type="Player's Guide to Faerun page 42" name="Pernicious Magic" desc="You can use the Shadow Weave to make your spells  harder for Weave users to dispel." />
+    <feat type="Complete Arcane page 81" name="Persistent Spell" desc="You can make a spell last all day." />
+    <feat type="Deities and Demigods page 51" name="Persistent Spell" desc="The deity makes one of its spells last all day." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Persistent Spell" desc="You make one of your spells last all day." />
+    <feat type="Player's Guide to Faerun page 42" name="Persistent Spell" desc="You can make a spell last all day." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 41" name="Persistent Spell" desc="You make one of your spells last all day." />
+    <feat type="Races of Eberron page 110" name="Persona Immersion" desc="Your assumption of another's physical identity  grants you defenses against mental intrusion." />
+    <feat type="Planar Handbook page 41" name="Personal Touchstone" desc="You draw more power form one of the planar touchstone  locations to which you have forged a link." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Persuasive" desc="You have a way with words and body language." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Persuasive" desc="You could sell a tindertwig hat to a troll." />
+    <feat type="Serpent Kingdoms page 146" name="Pervasive Gaze" desc="Your gaze attack is more effective than normal." />
+    <feat type="Savage Species page 37" name="Pervasive Gaze" desc="Your gaze attack is more effective than normal." />
+    <feat type="Serpent Kingdoms page 147" name="Petrification Immunity" desc="You are immune to petrification effects." />
+    <feat type="Serpent Kingdoms page 147" name="Petrification Resistance" desc="You can resist petrification effects better  than you otherwise could." />
+    <feat type="Complete Warrior page 103" name="Phalanx Fighting" desc="You are trained in fighting in close formation  with your allies." />
+    <feat type="Lords of Darkness page 189" name="Phalanx Fighting" desc="You are trained in fighting in close formation  with your allies." />
+    <feat type="Sandstorm page 51" name="Pharaoh's Fist" desc="Your unarmed strikes echo with thunder, stunning  your foe and those nearby." />
+    <feat type="Unearthed Arcana page 94" name="Photosynthetic Skin" desc="Your skin toughens when it draws energy from  the sun." />
+    <feat type="Complete Arcane page 81" name="Pierce Magical Concealment" desc="You ignore the miss chance provided by certain  magical effects." />
+    <feat type="Complete Arcane page 82" name="Pierce Magical Protection" desc="You can overcome the magical protections of  your enemies." />
+    <feat type="Races of Stone page 143" name="Pierce the Darkness" desc="You can channel positive energy to temporarily  increase the range of your darkvision." />
+    <feat type="Frostburn page 49" name="Piercing Cold" desc="Your cold spells are so cold that they can damage  creatures normally resistant or immune to cold." />
+    <feat type="Serpent Kingdoms page 147" name="Piercing Gaze" desc="Your gaze attack has a greater range than normal." />
+    <feat type="Savage Species page 38" name="Piercing Gaze" desc="Your gaze attack has a greater range than normal." />
+    <feat type="Races of Stone page 143" name="Piercing Sight" desc="Your fundamental familiarity with illusion allows  you to better recognize them." />
+    <feat type="Complete Warrior page 103" name="Pin Shield" desc="You know how to get inside your opponent's guard  by pinning his shield out of the way." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 8" name="Pin Shield" desc="You know how to get inside your opponent's guard  by pinning his shield out of the way." />
+    <feat type="Complete Divine page 86" name="Pious Defense" desc="Your connection to a greater power sometimes  gives you flashes of insight that keep you safe." />
+    <feat type="Complete Divine page 86" name="Pious Soul" desc="By adhering to the precepts of your religion  or philosophy, you gain an extra edge when you need it most." />
+    <feat type="Complete Divine page 87" name="Pious Spellsurge" desc="You can use the strength of your faith to augment  a spell cast at a critical juncture." />
+    <feat type="Races of Faerun page 167" name="Plague Resistant" desc="You are descended from the handful of combatants  who fought on the Fields of Nun and survived Chondath's Rotting War in 902  DR." />
+    <feat type="Planar Handbook page 41" name="Planar Familiar" desc="When you are ready and able to acquire a new  familiar, you may choose one of several nonstandard familiars." />
+    <feat type="Planar Handbook page 41" name="Planar Touchstone" desc="Forge a link between you and power-rich planar  locations, referred to as planar touchstones." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Planar Turning" desc="You can turn or rebuke outsiders." />
+    <feat type="Epic Level Handbook page 64" name="Planer Turning" desc="You can turn or rebuke outsiders." />
+    <feat type="Races of Faerun page 167" name="Planetouched Animal Affinity" desc="You have a special affinity for a kind of animal  associated with your deity ancestor." />
+    <feat type="Deities and Demigods page 51" name="Plant Control" desc="The deity can channel the power of nature to  gain mastery over plant creatures." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Plant Control" desc="You channel the power of nature to gain mastery  over plant creatures." />
+    <feat type="Deities and Demigods page 51" name="Plant Defiance" desc="The deity can channel the power of nature to  drive off or stop plant creatures." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Plant Defiance" desc="You channel the power of nature to drive off  plant creatures." />
+    <feat type="Epic Level Handbook page 65" name="Plant Wild Shape" desc="You can wild shape into plant form." />
+    <feat type="Heroes of Battle page 99" name="Plunging Shot" desc="You can use the force of gravity to make your  ranged attacks deal extra damage if your target is below you." />
+    <feat type="Races of the Wild page 152" name="Plunging Shot" desc="You can use the force of gravity to make your  ranged attacks deal extra damage if your target is below you." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Point Blank Shot" desc="You are skilled at making well-placed shots  with ranged weapons at close range." />
+    <feat type="Book of Vile Darkness page 49" name="Poison Immunity" desc="After prolonged exposure to a poison or toxin,  the character has rendered himself immune to it." />
+    <feat type="Champions of Ruin page 21" name="Poison Immunity" desc="After prolonged exposure to a poison or toxin,  you have rendered yourself immune to it." />
+    <feat type="Serpent Kingdoms page 147" name="Poison Immunity" desc="You can ignore the effects of poison." />
+    <feat type="Savage Species page 38" name="Poison Immunity" desc="You can ignore the effects of poison." />
+    <feat type="Serpent Kingdoms page 147" name="Poison Resistance" desc="You can resist poison better than you otherwise  could." />
+    <feat type="Savage Species page 38" name="Poison Resistance" desc="You can resist poison better than you otherwise  could." />
+    <feat type="Unearthed Arcana page 94" name="Polar Chill" desc="You can call forth the cold of the arctic regions,  making movement and fighting difficult for the unprepared." />
+    <feat type="Ghostwalk page 37" name="Poltergeist Hand" desc="You can move small objects in a limited manner  at a distance when you are a ghost." />
+    <feat type="Ghostwalk page 37" name="Poltergeist Rage" desc="You can throw heavy objects with the power of  your mind." />
+    <feat type="Complete Adventurer page 192" name="Polyglot" desc="You can speak, read, and write all languages." />
+    <feat type="Epic Level Handbook page 65" name="Polyglot" desc="You can speak, read, and write all languages." />
+    <feat type="Player's Guide to Faerun page 42" name="Portal Master" desc="You are especially proficient at creating portals." />
+    <feat type="Underdark page 27" name="Portal Sensitive" desc="You can perceive a portal just by passing  near it." />
+    <feat type="Complete Divine page 90" name="Positive Energy Aura" desc="You automatically turn (or even destroy) lesser  undead." />
+    <feat type="Epic Level Handbook page 65" name="Positive Energy Aura" desc="You automatically turn (or even destroy) lesser  undead." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Positive Energy Resistance" desc="You are resistant to the damage dealt by positive  energy effects." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Power Attack" desc="You can make exceptionally powerful melee attacks." />
+    <feat type="Oriental Adventures page 64" name="Power Attack - Iaijutsu" desc="Your ancestor, Kakita Rensei, was a renowned  duelist whose strength was legendary." />
+    <feat type="Oriental Adventures page 65" name="Power Attack - Shadowlands" desc="You are descended from Kaiu Gineza, the engineer  who not only helped construct the tomb of Iuchiban, but also remained in  the tomb to set the last trap." />
+    <feat type="Draconomicon page 72" name="Power Climb" desc="If you fly in a straight line, you can gain  altitude in flight more easily than others." />
+    <feat type="Complete Warrior page 103" name="Power Critical" desc="Choose one weapon, such as a longsword or a  greataxe. With that weapon, you know how to hit where it hurts." />
+    <feat type="Deities and Demigods page 51" name="Power Critical" desc="The deity chooses one kind of weapon, such as  a longsword or greataxe. With this weapon, the deity knows how to hit where  it hurts when it counts." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Power Critical" desc="Choose one weapon, such as a longsword or a  greataxe. With that weapon, you know how to hit where it hurts." />
+    <feat type="Draconomicon page 72" name="Power Dive" desc="You can fall upon an opponent from the sky." />
+    <feat type="Savage Species page 38" name="Power Dive" desc="You can fall upon an opponent from the sky." />
+    <feat type="Expanded Psionics Handbook page 34" name="Power Knowledge" desc="You add two additional powers to your list of  powers known." />
+    <feat type="Enemies and Allies page 50" name="Power Lunge" desc="Your ferocious attack may catch an opponent  unprepared." />
+    <feat type="Ghostwalk page 37" name="Power Lunge" desc="Your ferocious attack may catch an opponent  unprepared." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 8" name="Power Lunge" desc="Your ferocious attack may catch an opponent  unprepared." />
+    <feat type="Expanded Psionics Handbook page 49" name="Power Penetration" desc="Your powers are especially potent at breaking  through power resistance." />
+    <feat type="Expanded Psionics Handbook page 49" name="Power Specialization" desc="You deal more damage with your powers." />
+    <feat type="Complete Adventurer page 111" name="Power Throw" desc="You have learned how to hurl weapons to deadly  effect." />
+    <feat type="Lords of Madness page 23" name="Powerful Bite" desc="An aboleth with this feat develops jaws that  are much more muscular than normal, allowing it to bite more efficiently." />
+    <feat type="Eberron Campaign Setting page 57" name="Powerful Charge" desc="You can charge with extra force." />
+    <feat type="Miniatures Handbook page 27" name="Powerful Charge" desc="You can charge with extra force." />
+    <feat type="Monster Manual III page 207" name="Powerful Charge" desc="A creature with this feat can charge with extra  force." />
+    <feat type="Oriental Adventures page 65" name="Powerful Voice" desc="You are karmically linked to Utaku, Shinjo's  most trusted lieutenant and devoted bodyguard." />
+    <feat type="Races of Stone page 143" name="Powerful Wild Shape" desc="You retain your powerful build while in wild  shape form." />
+    <feat type="Heroes of Battle page 99" name="Practiced Cohort" desc="Your cohort works well as part of your team." />
+    <feat type="Complete Arcane page 82" name="Practiced Spellcaster" desc="Choose a spellcasting class that you possess.  Your spells cast from that class are more powerful." />
+    <feat type="Complete Divine page 82" name="Practiced Spellcaster" desc="Choose a spellcasting class that you possess.  Your spells cast from that class are more powerful." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Precise Shot" desc="You are skilled at timing and aiming ranged  attacks." />
+    <feat type="Eberron Campaign Setting page 58" name="Precise Swing" desc="You can ignore most obstacles when making a  melee attack against an opponent." />
+    <feat type="Complete Arcane page 181" name="Precocious Apprentice" desc="Your master has shown you the basics of a spell  beyond the normal limits of your experience and training." />
+    <feat type="Serpent Kingdoms page 147" name="Prehensile Tail" desc="You can use your tail to manipulate objects." />
+    <feat type="Savage Species page 38" name="Prehensile Tail" desc="You can use your tail to manipulate objects." />
+    <feat type="Sandstorm page 51" name="Priest of the Waste" desc="You can swap out prepared spells for others  that aid in exploring and surviving in wastelands." />
+    <feat type="Frostburn page 49" name="Primeval Wild Shape" desc="Your wild shape forms are stronger than normal." />
+    <feat type="Frostburn page 49" name="Primitive Caster" desc="You use screeches, wild gesticulations, and  extra material components to give your spells additional power." />
+    <feat type="Races of Faerun page 167" name="Primitive Caster" desc="You use screeches, wild gesticulations, and  extra material components to give your spells additional powers." />
+    <feat type="Complete Divine page 84" name="Profane Boost" desc="You can channel negative energy to increase  the power of inflict wounds spells cast near you." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Profane Lifeleech" desc="You can channel negative energy to draw the  life force from nearby living creatures." />
+    <feat type="Champions of Ruin page 21" name="Profane Outburst" desc="With a horrendous release of divine energy,  you steel your undead allies and minions against harm." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Profane Vigor" desc="You can channel negative energy to heal nearby  undead allies of physical damage." />
+    <feat type="Complete Warrior page 103" name="Prone Attack" desc="You can attack from a prone position without  penalty." />
+    <feat type="Oriental Adventures page 65" name="Prone Attack" desc="You attack from a prone position without penalty." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 8" name="Prone Attack" desc="You attack from a prone position without penalty." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 24" name="Proportionate Wild Shape" desc="You use wild shape to become animals of your  own size even if your wild shape ability would normally exclude that size  category." />
+    <feat type="Races of Destiny page 153" name="Protected Destiny" desc="Your heroic destiny is guarded against the whims  of misfortune." />
+    <feat type="Expanded Psionics Handbook page 49" name="Psicrystal Affinity" desc="You have created a psicrystal." />
+    <feat type="Expanded Psionics Handbook page 49" name="Psicrystal Containment" desc="Your psicrystal has advanced enough that it  can hold a psionic focus that you store within it." />
+    <feat type="Expanded Psionics Handbook page 34" name="Psicrystal Power" desc="Your psicrystal can manifest a power." />
+    <feat type="Expanded Psionics Handbook page 49" name="Psionic Affinity" desc="You have a knack for psionic endeavors." />
+    <feat type="Expanded Psionics Handbook page 49" name="Psionic Body" desc="Your mind reinforces your body." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Charge" desc="You can charge in crooked line." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Dodge" desc="You are proficient at dodging blows." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Endowment" desc="You can endow your manifestations with more  concentrated focus." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Fist" desc="You can charge your unarmed strike or natural  weapon with additional damage potential." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Hole" desc="You are anathema to psionic creatures and characters." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Meditation" desc="You can focus your mind faster than normal,  even under duress." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Shot" desc="You can charge your ranged attacks with additional  damage potential." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Talent" desc="You gain additional power points to supplement  those you already had." />
+    <feat type="Expanded Psionics Handbook page 50" name="Psionic Weapon" desc="You can charge your melee weapon with additional  damage potential." />
+    <feat type="Serpent Kingdoms page 147" name="Puff Torso" desc="You can puff out your skin to appear larger  and more threatening." />
+    <feat type="Champions of Ruin page 21" name="Pulverize Foe" desc="You enjoy smashing your opponents into submission." />
+    <feat type="Book of Exalted Deeds page 44" name="Purify Spell" desc="You can charge your damaging spells with celestial  energy that leaves good creatures unharmed." />
+    <feat type="Book of Exalted Deeds page 45" name="Purify Spell Trigger" desc="You can channel holy power through a spell trigger  item, such as a wand or staff." />
+    <feat type="Book of Exalted Deeds page 45" name="Purify Spell-Like Ability" desc="You can charge your damaging spell-like abilities  with celestial energy that leaves good creatures unharmed." />
+    <feat type="Eberron Campaign Setting page 58" name="Pursue" desc="You have the ability to follow in an opponent's  wake." />
+    <feat type="Miniatures Handbook page 27" name="Pushback" desc="You can knock opponents back when you hit them  in melee." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Pyro" desc="You're good at lighting objects and opponents  on fire." />
+    <feat type="Book of Exalted Deeds page 45" name="Quell the Profane" desc="Your mightiest attacks weaken evil foes." />
+    <feat type="Races of Eberron page 110" name="Quick Change" desc="You can quickly alter your features and physiology." />
+    <feat type="Savage Species page 38" name="Quick Change" desc="You can shift to an alternate form faster and  more easily than you otherwise could." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Quick Draw" desc="You can draw weapons with startling speed." />
+    <feat type="Complete Adventurer page 112" name="Quick Reconnoiter" desc="You can learn a lot of information from just  a quick scan of an area or object." />
+    <feat type="Lords of Madness page 181" name="Quick Recovery" desc="It's hard to keep you down for long. You have  a talent for shaking off effects that leave others unable to act." />
+    <feat type="Complete Warrior page 114" name="Quick Staff" desc="You have mastered the style of fighting with  a quarterstaff." />
+    <feat type="Draconomicon page 73" name="Quicken Breath" desc="You can loose your breath weapon with but a  thought." />
+    <feat type="WL page 15" name="Quicken Legacy" desc="You can activate one of your item's legacy abilities  with a moment's thought." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Quicken Manifestation" desc="You can manifest from the Ethereal Plane with  a moment's thought." />
+    <feat type="Expanded Psionics Handbook page 50" name="Quicken Power" desc="You can manifest a power with a moment's thought." />
+    <feat type="Player's Handbook v.3.5 page 98" name="Quicken Spell" desc="You can cast a spell with a moment's thought." />
+    <feat type="Book of Vile Darkness page 49" name="Quicken Spell-Like Ability" desc="The creature can use a spell-like ability with  a moment's thought." />
+    <feat type="Monster Manual v.3.5 page 304" name="Quicken Spell-Like Ability" desc="The creature can employ a spell-like ability  with a moment's thought." />
+    <feat type="Monster Manual II page 18" name="Quicken Spell-like Ability" desc="The creature can use a spell-like ability with  a moment's thought." />
+    <feat type="Monster Manual III page 207" name="Quicken Spell-Like Ability" desc="A creature with this feat can employ a spell-like  ability with a moment's thought." />
+    <feat type="Savage Species page 38" name="Quicken Spell-Like Ability" desc="You can use a spell-like ability with a moment's  thought." />
+    <feat type="Complete Divine page 84" name="Quicken Turning" desc="You can turn or rebuke undead with a moment's  thought." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Quicken Turning" desc="You can turn or rebuke undead with a moment's  thought." />
+    <feat type="Faiths &amp; Pantheons page 215" name="Quicken Turning" desc="You can turn or rebuke undead with a moment's  thought." />
+    <feat type="Ghostwalk page 37" name="Quicken Turning" desc="You can turn or rebuke undead with a moment's  thought." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Quicken Turning" desc="You can turn or rebuke undead with a moment's  thought." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Quicker Than the Eye" desc="Your hands can move so quickly that observers  don't see what you've done." />
+    <feat type="Lords of Madness page 23" name="Quickslime" desc="The slime attack of an aboleth with this feat  is particularly fast and difficult to resist." />
+    <feat type="Races of Eberron page 110" name="Racial Emulation" desc="You can emulate a humanoid more closely with  your minor change shape ability." />
+    <feat type="Races of Destiny page 155" name="Radiant Fire" desc="Pelor has ignited your faith and conviction,  making you better able to fight the creatures of darkness." />
+    <feat type="Races of Eberron page 118" name="Ragewild Fighting" desc="You have mastered a merciless form of combat  that emphasizes using brute strength to shatter your foes." />
+    <feat type="Eberron Campaign Setting page 58" name="Raging Luck" desc="When raging, you have a greater ability to alter  your luck than most others do." />
+    <feat type="Races of Stone page 143" name="Rampaging Bull Rush" desc="You can use brute force to slam into and knock  down your enemies." />
+    <feat type="Complete Warrior page 103" name="Ranged Disarm" desc="You can disarm a foe from a distance." />
+    <feat type="Epic Level Handbook page 65" name="Ranged Inspiration" desc="You can use your bardic music at a greater range  than normal." />
+    <feat type="Complete Warrior page 104" name="Ranged Pin" desc="You can perform a ranged grapple attempt against  an opponent not adjacent to you." />
+    <feat type="Book of Exalted Deeds page 45" name="Ranged Smite Evil" desc="You smite ability can be channeled through your  ranged weapon." />
+    <feat type="Complete Arcane page 82" name="Ranged Spell Specialization" desc="You deal more damage with ranged touch attack  spells." />
+    <feat type="Complete Warrior page 104" name="Ranged Sunder" desc="You can attack an opponent's weapon from a distance." />
+    <feat type="Savage Species page 39" name="Rapid Breath" desc="You do not have to wait as long to reuse your  breath weapons as you normally would." />
+    <feat type="Epic Level Handbook page 66" name="Rapid Inspiration" desc="You can inspire your allies with bardic music  more quickly than normal." />
+    <feat type="Expanded Psionics Handbook page 50" name="Rapid Metabolism" desc="Your wounds heal rapidly." />
+    <feat type="Epic Level Handbook page 70" name="Rapid Reload" desc="You reload a crossbow more quickly than normal." />
+    <feat type="Player's Handbook v.3.5 page 99" name="Rapid Reload" desc="Choose a type of crossbow. You can reload a  crossbow of that type more quickly than normal." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 9" name="Rapid Reload" desc="You reload a crossbow more quickly than normal." />
+    <feat type="Player's Handbook v.3.5 page 99" name="Rapid Shot" desc="You can use ranged weapons with exceptional  speed." />
+    <feat type="Complete Divine page 84" name="Rapid Spell" desc="You can cast spells with long casting times  more quickly." />
+    <feat type="Complete Warrior page 104" name="Rapid Stunning" desc="You can use your stunning attacks in rapid succession." />
+    <feat type="Races of Faerun page 167" name="Rapid Swimming" desc="You are one with the water." />
+    <feat type="Stormwrack page 93" name="Rapid Swimming" desc="You are one with the water." />
+    <feat type="Draconomicon page 73" name="Rapidstrike" desc="You can attack more than once with a natural  weapon." />
+    <feat type="Complete Warrior page 111" name="Raptor School" desc="You know martial arts techniques inspired by  hunting birds." />
+    <feat type="UE page 45" name="Rashemi Elemental Summoning" desc="You may summon Rashemen's native elementals  in any situation where you could summon an air or earth elemental." />
+    <feat type="Sandstorm page 51" name="Rattlesnake Strike" desc="Having observed the ways of a desert viper,  you have learned to use ki in a fashion similar to poison." />
+    <feat type="Complete Adventurer page 112" name="Razing Strike" desc="You have mastered the art of delivering precise  strikes against nonliving creatures while channeling spell energy through  your melee attacks." />
+    <feat type="Races of Eberron page 114" name="Razorclaw Elite" desc="Your razorclaw shifter trait improves." />
+    <feat type="Lords of Madness page 23" name="Reach Bite" desc="An aboleth with this feat can extend its jaws  and esophagus out from its body to make attacks beyond its normal reach." />
+    <feat type="Complete Divine page 84" name="Reach Spell" desc="You can cast touch spells without touching the  spell recipient." />
+    <feat type="Deities and Demigods page 51" name="Reach Spell" desc="The deity can cast touch spells without touching  the spell recipient." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Reach Spell" desc="You can cast touch spells without touching the  spell recipient." />
+    <feat type="Faiths &amp; Pantheons page 215" name="Reach Spell" desc="You can cast touch spells without touching the  spell recipient." />
+    <feat type="Epic Level Handbook page 66" name="Reactive Countersong" desc="You can use countersong as a reaction to a sonic  or language-dependent magical attack." />
+    <feat type="Magic of Faerun page 22" name="Reactive Counterspell" desc="You can react quickly to counterspells cast  by opponents." />
+    <feat type="Player's Guide to Faerun page 42" name="Reactive Counterspell" desc="You can react quickly to counter  spells cast by opponents." />
+    <feat type="Races of Eberron page 115" name="Reactive Shifting" desc="You can shift with a mere thought." />
+    <feat type="Heroes of Battle page 99" name="Ready Shot" desc="You can make devastating attacks with ranged  weapons against charging opponents." />
+    <feat type="Champions of Ruin page 21" name="Reaping Spell" desc="The dark energy of your spell devours the soul  of any creature killed by it." />
+    <feat type="Miniatures Handbook page 27" name="Reckless Charge" desc="You can charge with wild abandon." />
+    <feat type="Expanded Psionics Handbook page 51" name="Reckless Offense" desc="You can shift your focus from defense to offense." />
+    <feat type="Enemies and Allies page 41" name="Reckless Offensive" desc="You lower your guard in order to make a telling  attack." />
+    <feat type="Races of Faerun page 167" name="Reckless Offensive" desc="You lower your guard in order to make a telling  attack." />
+    <feat type="Races of Stone page 143" name="Reckless Rage" desc="You are considered extreme even among other  barbaric warriors, and you enter a deeper state of rage than others." />
+    <feat type="Complete Arcane page 82" name="Reckless Wand Wielder" desc="You can increase the effectiveness of spells  cast from a wand." />
+    <feat type="Eberron Campaign Setting page 58" name="Recognize Impostor" desc="You are extremely skilled at spotting imposters." />
+    <feat type="Draconomicon page 73" name="Recover Breath" desc="You wait less time before being able to use  your breath weapon again." />
+    <feat type="Epic Level Handbook page 66" name="Reflect Arrows" desc="You reflect ranged attacks back upon the attacker." />
+    <feat type="Races of Eberron page 111" name="Relic Hunter" desc="You possess great knowledge of the relics and  crafts of the ancient cultures of Eberron." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Remain Conscious" desc="You have the tenacity of will that supports  you even when things look bleak." />
+    <feat type="Oriental Adventures page 65" name="Remain Conscious" desc="You have a tenacity of will that supports you  even when you are disabled or dying." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 9" name="Remain Conscious" desc="You have a tenacity of will that supports you  even when things look bleak." />
+    <feat type="Draconomicon page 73" name="Rend" desc="You can rend opponents with your claws." />
+    <feat type="Ghostwalk page 37" name="Rend Ghost" desc="Your touch can maul the ectoplasm of another  ghost." />
+    <feat type="Serpent Kingdoms page 147" name="Rending Constriction" desc="You can pull grappled enemies apart." />
+    <feat type="Savage Species page 39" name="Rending Constriction" desc="You can pull grappled enemies apart." />
+    <feat type="Unearthed Arcana page 182" name="Renown" desc="You have a better chance of being recognized." />
+    <feat type="Complete Arcane page 82" name="Repeat Spell" desc="You can cast a spell that repeats on the following  round." />
+    <feat type="Deities and Demigods page 51" name="Repeat Spell" desc="The deity can cast a spell that repeats the  following round." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 41" name="Repeat Spell" desc="You can cast a spell that repeats on the following  round." />
+    <feat type="Eberron Campaign Setting page 58" name="Repel Aberration" desc="Your Gatekeeper training allows you to keep  aberrations at bay." />
+    <feat type="Libris Mortis: The Book of the Dead page 29" name="Requiem" desc="Your bardic music affects undead creatures." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Requiem" desc="Your bardic music affects undead creatures." />
+    <feat type="Eberron Campaign Setting page 59" name="Research" desc="You can use your Knowledge skills to extract  information from books, scrolls, and other repositories of facts and figures." />
+    <feat type="Dragonlance Campaign Setting page 86" name="Reserves of Strength" desc="When you cast a spell, you can choose to increase  its effective caster level at the cost of exhausting yourself." />
+    <feat type="Unearthed Arcana page 94" name="Residual Rebound" desc="Sometimes spells cast at you rebound on the  caster instead." />
+    <feat type="Epic Level Handbook page 111" name="Resist Death" desc="You are capable of withstanding tremendous amounts  of damage without risk of instant death." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Resist Disease" desc="You have developed a natural resistance to diseases." />
+    <feat type="Shining South page 21" name="Resist Disease" desc="You have developed a natural resistance to diseases." />
+    <feat type="Dragonlance Campaign Setting page 86" name="Resist Dragonfear" desc="You are able to show courage in the presence  of dragons." />
+    <feat type="Ghostwalk page 37" name="Resist Ghost" desc="You are resistant to the effects of ghost powers." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Resist Poison" desc="Over years, some among your people carefully  expose themselves to poisons in controlled dosages in order to build up  immunity to their effects." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Resist Poison" desc="You have built up an immunity to the effects  of poisons by exposing yourself to controlled doses of them." />
+    <feat type="Oriental Adventures page 65" name="Resist Poison" desc="Your ancestor, Agasha Kitsuki, founded the fourth  family of the Dragon clan and a school for magistrates renowned for teaching  skills of investigation and deduction." />
+    <feat type="Player's Guide to Faerun page 43" name="Resist Poison" desc="Your people have become inured to many deadly  substances through controlled exposure or the simple hostility of your home  environment." />
+    <feat type="Oriental Adventures page 65" name="Resist Taint" desc="You are descended from Kuni, the founder of  the Kuni family, a scholar of -- and mighty warrior against -- the Shadowlands." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Resistance to Energy" desc="You channel the power of nature to resist a  particular energy type." />
+    <feat type="Book of Exalted Deeds page 45" name="Resounding Blow" desc="Your mightiest attacks cause your foes to tremble  before you." />
+    <feat type="Races of Destiny page 153" name="Resourceful Buyer" desc="You know where to look in a community for anything  you need." />
+    <feat type="Expanded Psionics Handbook page 51" name="Return Shot" desc="You can return incoming arrows, as well as crossbow  bolts, spears, and other projectile thrown weapons." />
+    <feat type="Savage Species page 39" name="Reverberation" desc="Your sonic attack is more potent than normal." />
+    <feat type="Shining South page 21" name="Rhinoceros Tribe Charge" desc="You use the power of the rhinoceros's charge  in battle." />
+    <feat type="Player's Handbook v.3.5 page 99" name="Ride-By Attack" desc="You are skilled at making fast attacks from  your mount." />
+    <feat type="Eberron Campaign Setting page 59" name="Right of Counsel" desc="You have the legal and sacral right to seek  advice from one of your ancestors, a deathless elf in Aerenal's City of  the Dead." />
+    <feat type="Epic Level Handbook page 66" name="Righteous Strike" desc="Your unarmed strikes are particularly damaging  to chaotic creatures." />
+    <feat type="Book of Exalted Deeds page 45" name="Righteous Wrath" desc="Your rage is empowered with divine fury." />
+    <feat type="Races of Faerun page 167" name="Rock Gnome Trickster" desc="Your glamers are particularly likely to fool  the senses of your target." />
+    <feat type="Races of Stone page 143" name="Rock Hurling" desc="You can throw rocks like a giant can." />
+    <feat type="Savage Species page 39" name="Roll With It" desc="You are adept at lessening the effects of blows." />
+    <feat type="Races of Destiny page 156" name="Roofwalker" desc="You are adept at moving and fighting on rooftops  and ledges." />
+    <feat type="Races of Stone page 143" name="Roots of the Mountain" desc="You can channel energy to make yourself immovable." />
+    <feat type="Complete Warrior page 105" name="Roundabout Kick" desc="You can follow up on a particularly powerful  unarmed attack with a mighty kick, spinning in a complete circle before  landing the kick." />
+    <feat type="Oriental Adventures page 65" name="Roundabout Kick" desc="You can follow up on a particularly powerful  unarmed attack with a mighty kick, spinning in a complete circle before  landing the kick." />
+    <feat type="Epic Level Handbook page 66" name="Ruinous Rage" desc="While in a rage, you can deal tremendous damage  to objects." />
+    <feat type="Player's Handbook v.3.5 page 99" name="Run" desc="You are fleet of foot." />
+    <feat type="Races of Faerun page 167" name="Runesmith" desc="You can fashion runes that take the place of  material components for your spells." />
+    <feat type="Complete Divine page 84" name="Sacred Boost" desc="You can channel positive energy to increase  the power of cure wounds spells cast near you." />
+    <feat type="Complete Divine page 84" name="Sacred Healing" desc="You can channel positive energy to grant nearby  living creatures the ability to recover form their wounds quickly." />
+    <feat type="Deities and Demigods page 51" name="Sacred Spell" desc="The deity's damaging spells are imbued with  divine power." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Sacred Spell" desc="Your damaging spells are imbued with divine  power." />
+    <feat type="Faiths &amp; Pantheons page 215" name="Sacred Spell" desc="Your damaging spells are imbued with divine  power." />
+    <feat type="Book of Exalted Deeds page 45" name="Sacred Strike" desc="Your sneak attack is enhanced by your unshakable  faith in a good-aligned deity." />
+    <feat type="Races of Faerun page 168" name="Sacred Tattoo" desc="You have been spiritually touched by one of  the god-kings of the Old Empires and bear his or her symbol in the form  of a tattoo in the shape of a holy symbol." />
+    <feat type="Complete Warrior page 108" name="Sacred Vengeance" desc="You can channel energy to deal extra damage  against undead in melee." />
+    <feat type="Libris Mortis: The Book of the Dead page 30" name="Sacred Vengeance" desc="You can channel energy to deal extra damage  against undead in melee." />
+    <feat type="Libris Mortis: The Book of the Dead page 30" name="Sacred Vitality" desc="You can channel positive energy to gain protection  from damage to your abilities or your life force." />
+    <feat type="Book of Exalted Deeds page 45" name="Sacred Vow" desc="You have willingly given yourself to the service  of a good deity or cause, denying yourself an ordinary life to better serve  you highest ideals." />
+    <feat type="Book of Vile Darkness page 50" name="Sacrificial Mastery" desc="The character is skilled at offering living  sacrifices to evil gods or fiends." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Saddleback" desc="Your people are as comfortable riding as walking." />
+    <feat type="Ghostwalk page 38" name="Saddleback" desc="You were raised among people who are as comfortable  riding as walking." />
+    <feat type="Oriental Adventures page 65" name="Saddleback" desc="You have a unique karmic tie to Moto Chai, one  of the greatest riders ever to live, even by Unicorn standards." />
+    <feat type="Player's Guide to Faerun page 43" name="Saddleback" desc="You've spent endless hours learning how to handle  a mount in a fight." />
+    <feat type="Stormwrack page 93" name="Sahuagin Flip" desc="You can safely attack and withdraw underwater." />
+    <feat type="Stormwrack page 93" name="Sailor's Balance" desc="You are experienced with the rolling decks of  the ship and maintain strong footing, even in a terrible storm." />
+    <feat type="Book of Exalted Deeds page 46" name="Sanctify Ki Strike" desc="Sacred power suffuses your unarmed strikes." />
+    <feat type="Book of Exalted Deeds page 46" name="Sanctify Martial Strike" desc="Sacred power suffuses your attacks with a certain  kind of weapon." />
+    <feat type="Book of Exalted Deeds page 46" name="Sanctify Natural Attack" desc="You can focus holy power into your natural attacks." />
+    <feat type="Complete Divine page 84" name="Sanctify Relic" desc="You can create magic items that are imbued with  a connection to your deity." />
+    <feat type="Stormwrack page 93" name="Sanctify Water" desc="You can call upon positive energy to momentarily  transform normal water around you into holy water." />
+    <feat type="Book of Exalted Deeds page 46" name="Sanctify Weapon" desc="You can focus holy power into your weapon." />
+    <feat type="Complete Arcane page 82" name="Sanctum Spell" desc="Your spells are especially potent on home ground." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 41" name="Sanctum Spell" desc="Your spells have a home ground advantage." />
+    <feat type="Sandstorm page 51" name="Sand Camouflage" desc="You can hide yourself in sand with a moment's  notice." />
+    <feat type="Sandstorm page 52" name="Sand Dancer" desc="While making another attack, you attempt to  blind a foe with thrown sand." />
+    <feat type="Sandstorm page 52" name="Sand Snare" desc="When you knock your foes into the sand, they  have a hard time regaining their feet." />
+    <feat type="Sandstorm page 52" name="Sand Spinner" desc="You spray sand with your acrobatic maneuvers." />
+    <feat type="Sandstorm page 52" name="Sandskimmer" desc="You are particularly adept at moving over sand." />
+    <feat type="Complete Adventurer page 114" name="Savage Grapple" desc="While transformed into the shape of a wild animal,  you can savagely tear at any creature that you manage to grapple." />
+    <feat type="Lords of Madness page 181" name="Scavenging Gullet" desc="The taint of the aberration in your blood has  gifted you with the ability to gain nourishment from things that others  would never consider as food." />
+    <feat type="Complete Adventurer page 114" name="Scent" desc="You can sharpen your sense of smell." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Scent" desc="Your olfactory senses are as sharp as the wolf's." />
+    <feat type="Oriental Adventures page 65" name="Scholar of Nature" desc="You are descended from Asako Hanasku, a great  scholar who threw himself into the study of medicine, herbs, and poison." />
+    <feat type="Champions of Ruin page 23" name="Scion of Sorrow" desc="You formally supplicate yourself to a powerful  yugoloth lord." />
+    <feat type="Sandstorm page 52" name="Scorpion's Grasp" desc="Like the scorpion, you can grab and hold your  prey." />
+    <feat type="Sandstorm page 53" name="Scorpion's Instincts" desc="You are hard to find in the waste." />
+    <feat type="Sandstorm page 53" name="Scorpion's Resolve" desc="Like the scorpion, you are not easily distracted." />
+    <feat type="Sandstorm page 53" name="Scorpion's Sense" desc="Like the scorpion, you sense other creatures  simply by perceiving their contact with the sand." />
+    <feat type="Stormwrack page 93" name="Scourge of the Seas" desc="You have a sinister reputation as a pirate and  can intimidate enemy captains by your mere presence." />
+    <feat type="Savage Species page 39" name="Scramble" desc="Your slippery ways allow you to evade a damaging  blow." />
+    <feat type="Epic Level Handbook page 66" name="Scribe Epic Scroll" desc="You can scribe scrolls of epic power." />
+    <feat type="Player's Handbook v.3.5 page 99" name="Scribe Scroll" desc="You can create scrolls, from which you or another  spellcaster can cast the scribed spells." />
+    <feat type="Expanded Psionics Handbook page 51" name="Scribe Tattoo" desc="You can create psionic tattoos, which store  powers within their designs." />
+    <feat type="Ghostwalk page 38" name="Sculpt Ghost Body" desc="You can reshape your ghost body's ectoplasm  to enhance one physical ability score at the expense of another." />
+    <feat type="Complete Arcane page 83" name="Sculpt Spell" desc="You can alter the area of your spells." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 42" name="Sculpt Spell" desc="You can alter the shape of a spell's area." />
+    <feat type="Frostburn page 49" name="Sea Legs" desc="You are accustomed to the rolling motion on  board a ship, and can use this motion to your advantage." />
+    <feat type="Oriental Adventures page 65" name="Sea Legs" desc="You are descended from Yasuki Fumoki, a notorious  pirate who preyed on Crane merchant ships off the coast." />
+    <feat type="Stormwrack page 93" name="Sea Legs" desc="You are accustomed to the rolling motion on  board a ship, and can use this motion to your advantage." />
+    <feat type="Sandstorm page 53" name="Searing Spell" desc="Your fire spells are so hot that they can damage  creatures that normally have resistance or immunity to fire." />
+    <feat type="Races of Eberron page 120" name="Second Slam" desc="You have learned to use your form to the utmost  and can make two slam attacks." />
+    <feat type="Miniatures Handbook page 28" name="Second Wind" desc="You can shrug off minor wounds with ease." />
+    <feat type="Shining South page 21" name="Selective Spell" desc="You can screen allies from the effects of your  area spells." />
+    <feat type="Epic Level Handbook page 66" name="Self-Concealment" desc="When in combat, your form becomes blurry and  indistinct, making it difficult to land a blow against you." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Self-Sufficient" desc="You can take care of yourself in harsh environments  and situations." />
+    <feat type="Draconomicon page 106" name="Sense Weakness" desc="You can take advantage of subtle weaknesses  in your opponents' defenses." />
+    <feat type="Sandstorm page 53" name="Serpent Fang" desc="You are able to project your ki to strike  foes as though you had extended reach." />
+    <feat type="Eberron Campaign Setting page 60" name="Serpent Strike" desc="Through monastic weapon training, you have mastered  a fighting style that makes use of an unusual monk weapon: the longspear." />
+    <feat type="Complete Divine page 84" name="Serpent's Venom" desc="You can deliver a toxic bite attack reminiscent  of the viper." />
+    <feat type="Lost Empires of Faerun page 9" name="Servant of the Fallen" desc="You keep alive the worship of a deity who has  died or vanished." />
+    <feat type="Book of Exalted Deeds page 46" name="Servant of the Heavens" desc="You swear allegiance to one of the Tome Archons  who rules the Seven Heavens, and in exchange gain power to act on their  behalf." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Shadow" desc="You have a better chance than most to trail  someone unnoticed." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Shadow" desc="You are good at following someone surreptitiously." />
+    <feat type="Planar Handbook page 42" name="Shadow Heritage" desc="You are descended from creatures native to the  Plane of Shadow." />
+    <feat type="Races of Eberron page 111" name="Shadow Marches Warmonger" desc="The ancient martial pride of your people grants  you mastery of their style of battle." />
+    <feat type="Races of Faerun page 168" name="Shadow Shield" desc="Your ancestors long battled the insidious influence  of shadow magic, and some of their descendants (including you) have a greater  resistance to its effects." />
+    <feat type="Races of Faerun page 168" name="Shadow Song" desc="A dark legacy of the Shadowking's ambitions  is the shadow of sorrow that cloaks many Tethyrian songs and ballads. Some  bards have learned to infuse their performances with the sense of loss and  suffering that suffuses the Shadow Weave." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Shadow Weave Magic" desc="You have discovered the dark and dangerous secret  of the Shadow Weave." />
+    <feat type="Player's Guide to Faerun page 43" name="Shadow Weave Magic" desc="You have discovered the dangerous secret of  the Shadow Weave." />
+    <feat type="Champions of Ruin page 22" name="Shadowform Familiar" desc="You can summon a familiar from the Plane of  Shadow." />
+    <feat type="Champions of Ruin page 22" name="Shadowstrike" desc="Due to your ties to the Plane of Shadow, you  strike more effectively in areas of dim illumination." />
+    <feat type="Draconomicon page 73" name="Shape Breath" desc="You can make the area of your breath weapon  a cone or a line, as you see fit." />
+    <feat type="Ghostwalk page 38" name="Shape Ectoplasm" desc="You can make equipment out of ectoplasm." />
+    <feat type="Races of Eberron page 111" name="Shaped Splash" desc="Your expertise with thrown weapons enables you  to use splash weapons more effectively." />
+    <feat type="Races of the Wild page 152" name="Shared Fury" desc="Your fearsome rage spurs your animal companion  to greater heights." />
+    <feat type="Complete Warrior page 105" name="Sharp-Shooting" desc="Your skill with ranged weapons lets you score  hits others would miss due to an opponent's cover." />
+    <feat type="Deities and Demigods page 52" name="Sharp-Shooting" desc="The deity's skill with ranged weapons lets it  score hits others would miss due to an opponent's cover." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 9" name="Sharp-Shooting" desc="Your skill with ranged weapons lets you score  hits others would miss due to an opponent's cover." />
+    <feat type="Epic Level Handbook page 66" name="Shattering Strike" desc="You can shatter objects with your unarmed strike." />
+    <feat type="Ghostwalk page 38" name="Sherem-Lar Sorcery" desc="You are one of the Sherem-Lar, magically altered  in the womb to enhance your potential as a sorcerer." />
+    <feat type="Ghostwalk page 38" name="Sherezem-Lar Sorcery" desc="You are one of the Sherezem-Lar, an elite group  within the Sherem-Lar, head and shoulders above the others in power." />
+    <feat type="Complete Warrior page 105" name="Shield Charge" desc="You deal extra damage if you use your shield  as a weapon when charging." />
+    <feat type="Defenders of the Faith: A Guidebook to Clerics and Paladins page 20" name="Shield Charge" desc="You deal extra damage if you use your shield  as a weapon when charging." />
+    <feat type="Races of Faerun page 168" name="Shield Dwarf Warder" desc="You are a student of the protective magics of  the shield dwarves, learned at great cost during centuries of warfare and  wandering." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 9" name="Shield Expert" desc="You use a shield as an off-hand weapon while  retaining its armor bonus." />
+    <feat type="Races of Eberron page 113" name="Shield of Thought" desc="You wield your spirit as both weapon and shield." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Shield Proficiency" desc="You are proficient with bucklers, small shields,  and large shields." />
+    <feat type="Complete Warrior page 105" name="Shield Slam" desc="You can use your shield to daze your opponent." />
+    <feat type="Heroes of Battle page 99" name="Shield Wall" desc="You are skilled in using shields when in formation  with other shield-bearers." />
+    <feat type="Races of Stone page 144" name="Shielded Axe" desc="You have mastered the style of fighting with  a dwarven waraxe and a handaxe while keeping a buckler strapped to your  offhand, and you have learned to use this unusual combination of weapons  and buckler to protect yourself while wielding both axes effectively." />
+    <feat type="Races of Stone page 144" name="Shielded Casting" desc="You are skilled at covering yourself with your  shield when casting spells in combat." />
+    <feat type="Races of Stone page 144" name="Shielded Manifesting" desc="You are skilled at covering yourself with your  shield when manifesting psionic powers in combat." />
+    <feat type="Miniatures Handbook page 28" name="Shieldmate" desc="You can protect those near you with your shield." />
+    <feat type="Races of Eberron page 115" name="Shifter Agility" desc="Your heritage of speed and ferocity has honed  your reflexes, allowing you to avoid attacks." />
+    <feat type="Eberron Campaign Setting page 60" name="Shifter Defense" desc="By delving deeper into your shifter heritage,  you have developed the ability to ignore a little damage from every attack." />
+    <feat type="Monster Manual III page 150" name="Shifter Defense" desc="By delving into your shifter heritage, you have  developed the ability to ignore a little damage from every attack." />
+    <feat type="Eberron Campaign Setting page 60" name="Shifter Ferocity" desc="You are a tenacious combatant, continuing to  fight when others would succumb to pain and injury." />
+    <feat type="Races of Eberron page 115" name="Shifter Ferocity" desc="You are a tenacious combatant, continuing to  fight when others would succumb to pain and injury." />
+    <feat type="Monster Manual III page 150" name="Shifter Instincts" desc="Your heritage has given you sharp senses and  quick reflexes, and you have learned to trust your equally sharp instincts." />
+    <feat type="Races of Eberron page 115" name="Shifter Instincts" desc="Your heritage has given you sharp senses and  quick reflexes, and you have learned to trust your equally sharp instincts." />
+    <feat type="Eberron Campaign Setting page 60" name="Shifter Multiattack" desc="You are adept at using your natural attack in  conjunction with another weapon." />
+    <feat type="Races of Eberron page 115" name="Shifter Savagery" desc="The bestial fury of your lycanthrope ancestors  allows you to deal devastating strikes with your natural weapons." />
+    <feat type="Races of Eberron page 115" name="Shifter Stamina" desc="Yours is a heritage of endurance and tenacity,  and you can shrug off bruises and fatigue." />
+    <feat type="Races of Eberron page 112" name="Ship Savvy" desc="Your heritage among the sailors and shipwrights  of Zilargo gives you an edge in shipboard combat." />
+    <feat type="Stormwrack page 93" name="Ship's Mage" desc="You form a potent supernatural bond with a ship.  Your spells have a more potent effect when cast aboard this ship." />
+    <feat type="Complete Warrior page 112" name="Shock Trooper" desc="You are adept at breaking up formations of soldiers  when you rush into battle." />
+    <feat type="Draconomicon page 73" name="Shock Wave" desc="You can strike the ground with your tail so  hard it knocks other creatures down." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Shot on the Run" desc="You are highly trained in skirmish ranged weapons  tactics." />
+    <feat type="Ghostwalk page 38" name="Shriveling Touch" desc="Choose one physical ability score. When you  touch a creature, you can cause permanent drain to this ability score." />
+    <feat type="Miniatures Handbook page 28" name="Sidestep" desc="You can move nimbly around the battlefield." />
+    <feat type="Expanded Psionics Handbook page 51" name="Sidestep Charge" desc="You are skilled at dodging past charging opponents  and taking advantage when they miss." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Signature Spell" desc="You are so familiar with a mastered spell that  you can convert other prepared spells into that spell." />
+    <feat type="Player's Guide to Faerun page 43" name="Signature Spell" desc="You are so familiar with a mastered spell that  you can convert other prepared spells into that spell." />
+    <feat type="Races of Stone page 144" name="Silencing Strike" desc="You can infuse your sneak attacks with the magical  essence of silence." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Silent Spell" desc="You can cast spells silently." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Silver Palm" desc="Your culture is based on haggling and the art  of the deal." />
+    <feat type="Player's Guide to Faerun page 43" name="Silver Palm" desc="Your culture is based on haggling and the art  of the deal." />
+    <feat type="Eberron Campaign Setting page 60" name="Silver Smite" desc="You wield the power of the Silver Flame to smite  evil." />
+    <feat type="Oriental Adventures page 65" name="Silver Tongue" desc="Your ancestor, Mirumoto Kaijuko, was the first  woman to become daimyo of the Mirumoto family." />
+    <feat type="Races of Eberron page 120" name="Silver Tracery" desc="Alchemical silver tracery covers your body,  allowing you to overcome the supernatural defenses of certain creatures  and protecting against some magical attacks." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Simple Weapon Proficiency" desc="You understand how to use all types of simple  weapons in combat." />
+    <feat type="Champions of Ruin page 22" name="Skewer Foe" desc="A ruthless combatant, you like to impale enemies  on spears and similar piercing weapons." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Skill Focus" desc="Choose a skill. You have a special knack with  that skill." />
+    <feat type="Lords of Madness page 45" name="Skilled Telekinetic" desc="A creature with this feat becomes so skilled  with its telekinesis ability that it can manipulate and use magic  items via telekinesis." />
+    <feat type="Races of Faerun page 168" name="Skyrider" desc="You have trained and served with the hippogriff  cavalry that guards the Great Rift." />
+    <feat type="Races of Destiny page 153" name="Smatterings" desc="You have a talent for acquiring languages --  at least enough of each one to get by." />
+    <feat type="Frostburn page 50" name="Smite Fiery Foe" desc="You can smite creatures with the fire subtype." />
+    <feat type="Forgotten Realms Campaign Setting page 37" name="Smooth Talk" desc="Your people are accustomed to dealing with strangers  and foreigners without needing to draw weapons to make their point." />
+    <feat type="Oriental Adventures page 66" name="Smooth Talk" desc="You are descended from Doji Taehime, a Crane  ambassador to the Scorpion court -- a courtier skilled at discovering falsehoods  and uncovering plots." />
+    <feat type="Player's Guide to Faerun page 43" name="Smooth Talk" desc="Your people rarely have to draw their weapons  to deal with potential adversaries." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Snake Blood" desc="The taint of the yuan-ti runs in your veins." />
+    <feat type="Player's Guide to Faerun page 43" name="Snake Blood" desc="The taint of the yuan-ti runs in your veins." />
+    <feat type="Monster Manual II page 18" name="Snatch" desc="The creature can grapple more easily with its  claws or bite." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Snatch" desc="You can grapple more easily with your claws  or bite." />
+    <feat type="Draconomicon page 73" name="Snatch and Swallow" desc="You can swallow creatures you have grabbed with  your bite attack." />
+    <feat type="Monster Manual v.3.5 page 304" name="Snatch Arrows" desc="The creature can grab opponents much smaller  than itself and hold them in its mouth or claw." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Snatch Arrows" desc="You are adept at grabbing incoming arrows, as  well as crossbow bolts, spears, and other projectile or thrown weapons." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 9" name="Snatch Arrows" desc="You are adept at grabbing incoming arrows, as  well as crossbow bolts, spears, and other projectile or thrown weapons." />
+    <feat type="Champions of Ruin page 22" name="Snatch Trophy" desc="You can quickly and skillfully collect a trophy  of your victory over a fallen foe." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Snatch Weapon" desc="You can disarm an opponent, then pluck the weapon  from midair." />
+    <feat type="Epic Level Handbook page 66" name="Sneak Attack of Opportunity" desc="Whenever your opponent lets his guard down,  you can make a sneak attack." />
+    <feat type="UE page 45" name="Snow Tiger Berserker" desc="You have learned how to pounce on your foes,  much like your totem spirit." />
+    <feat type="Frostburn page 50" name="Snowcasting" desc="You add ice or snow to your spell's components  to make them more powerful." />
+    <feat type="Frostburn page 50" name="Snowflake Wardance" desc="You are particularly adept at moving through  snow and over ice." />
+    <feat type="Frostburn page 50" name="Snowrunner" desc="You have mastered the snowflake wardance, a  mystical style of fighting with slashing weapons that allows you to leap  and almost seem to float haphazardly across the battlefield like a whirling,  razor-edged snowflake." />
+    <feat type="Races of Destiny page 153" name="Sociable Personality" desc="You are adroit at avoiding social gaffes." />
+    <feat type="Ghostwalk page 39" name="Solid Visage" desc="Your ghost body appears solid and alive." />
+    <feat type="Eberron Campaign Setting page 60" name="Song of the Heart" desc="Your bardic music reaches the depths of its  listeners' hearts." />
+    <feat type="Eberron Campaign Setting page 60" name="Soothe the Beast" desc="Echoing the music of creation, your music has  powers to calm animals." />
+    <feat type="Oriental Adventures page 66" name="Soul of Honor" desc="Your ancestor Shinjo Martera, the firstborn  son of Shinjo, was the living incarnation of bushido for the Unicorn,  utterly without fault or failing." />
+    <feat type="Oriental Adventures page 66" name="Soul of Loyalty" desc="Your ancestor, Mirumoto Tokeru, was renowned  for his loyalty to his twin brother, Ryudumu." />
+    <feat type="Oriental Adventures page 66" name="Soul of Sincerity" desc="You are descended from the famous Scorpion daimyo  Bayushi Tangen, author of Lies and Little Truths." />
+    <feat type="Complete Arcane page 83" name="Soul of the North" desc="You possess a magical understanding of the nature  of cold." />
+    <feat type="Races of Eberron page 120" name="Soulblade Warrior" desc="The spirit of a quori warrior grants you deadly  speed and combat prowess with your mind blade." />
+    <feat type="Races of Faerun page 168" name="Southern Magician" desc="Your magical studies in Mulan lands have taught  you spellcasting techniques unknown in the north that blur the line between  arcane and divine magic." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Speaking Wild Shape" desc="While in wild shape, you can communicate with  animals or elementals of the same kind as your current form." />
+    <feat type="Dragonlance Campaign Setting page 87" name="Spear of Doom" desc="Few can avoid death on your spearpoint when  you brace yourself for their attack." />
+    <feat type="Dragonlance Campaign Setting page 87" name="Spectacular Death Throes" desc="Your body seethes with unchecked power, promising  dire consequences to your killer." />
+    <feat type="Complete Divine page 90" name="Spectral Strike" desc="You can strike incorporeal creatures as if they  were solid." />
+    <feat type="Epic Level Handbook page 66" name="Spectral Strike" desc="You can strike incorporeal creatures as if they  were solid." />
+    <feat type="Expanded Psionics Handbook page 51" name="Speed of Thought" desc="The energy of your mind energizes the alacrity  of your body." />
+    <feat type="Libris Mortis: The Book of the Dead page 30" name="Spell Drain" desc="You can cast any spell that you drain from a  creature's mind." />
+    <feat type="Complete Divine page 84" name="Spell Focus" desc="Your spells with an alignment descriptor are  more potent than normal." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Spell Focus" desc="Choose a school of magic. Your spells of that  school are more potent than normal." />
+    <feat type="Book of Exalted Deeds page 46" name="Spell Focus (Good)" desc="Your spells with the good descriptor are more  potent than normal due to your relationship with the powers of good." />
+    <feat type="Magic of Faerun page 22" name="Spell Girding" desc="Your spells are particularly hardy, resisting  dispel checks more readily than normal." />
+    <feat type="Complete Arcane page 83" name="Spell Hand" desc="You possess a magical understanding of the manipulation  of force." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Spell Knowledge" desc="You add two additional arcane spells to your  repertoire." />
+    <feat type="Epic Level Handbook page 67" name="Spell Knowledge" desc="You add two additional arcane spells to your  repertoire." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Spell Mastery" desc="You are so intimately familiar with certain  spells that you don't need a spellbook to prepare them anymore." />
+    <feat type="Epic Level Handbook page 67" name="Spell Opportunity" desc="You can cast a touch spell as an attack of opportunity." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Spell Penetration" desc="Your spells are especially potent, breaking  through spell resistance more readily than normal." />
+    <feat type="Oriental Adventures page 66" name="Spell Power" desc="Your lineage traces back to the young shugenja  Kuni Osaku, who single-handedly held off a massive army of oni at the Battle  of the Cresting Wave." />
+    <feat type="Lost Empires of Faerun page 9" name="Spell Reprieve" desc="Your studies of the less restrictive arcane  traditions of old allow you to cast one spell from a prohibited school." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 42" name="Spell Specialization" desc="You deal more damage with ray or energy missile  spells." />
+    <feat type="Epic Level Handbook page 67" name="Spell Stowaway" desc="Choose a spell-like ability you possess or a  spell you can cast. You gain the benefits of this magic whenever it is used  near you." />
+    <feat type="Magic of Faerun page 22" name="Spell Thematics" desc="Your spells have a distinct visual or auditory  effect in their manifestation." />
+    <feat type="Player's Guide to Faerun page 44" name="Spell Thematics" desc="Your spells manifest with a distinct theme or  appearance." />
+    <feat type="Oriental Adventures page 66" name="Spellcaster Support" desc="Your ancestor, Shiba Kaigen, was a samurai who  used his knowledge of spellcraft to help defend a mountain pass from a Lion  invasion." />
+    <feat type="Draconomicon page 74" name="Spellcasting Harrier" desc="Spellcasters you threaten find it difficult  to cast defensively." />
+    <feat type="Epic Level Handbook page 67" name="Spellcasting Harrier" desc="Spellcasters you threaten find it difficult  to cast defensively." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Spellcasting Prodigy" desc="You have an exceptional gift for magic." />
+    <feat type="Player's Guide to Faerun page 44" name="Spellcasting Prodigy" desc="You have an exceptional gift for magic." />
+    <feat type="Magic of Faerun page 23" name="Spellfire Wielder" desc="You are one of the rare people who have the  innate talent to control raw magic in the form of spellfire." />
+    <feat type="Races of Faerun page 168" name="Spell-Like Ability Focus" desc="Choose one of your spell-like abilities. This  attack becomes much more potent than normal." />
+    <feat type="Races of Stone page 144" name="Spellrazor" desc="You have mastered the style of combining a gnome  quickrazor with spellcasting." />
+    <feat type="Player's Guide to Faerun page 44" name="Spellwise" desc="You were raised in a land where mighty wizards  are common." />
+    <feat type="Player's Guide to Faerun page 176" name="Spider Bite" desc="You gain a poisonous bite like that of a spider." />
+    <feat type="Races of Eberron page 120" name="Spiked Body" desc="Your body is overlaid with hundreds of protruding  spikes that can deal great damage to foes." />
+    <feat type="Complete Warrior page 114" name="Spinning Halberd" desc="You have mastered the style of fighting with  a halberd." />
+    <feat type="Races of Faerun page 168" name="Spire Walking" desc="Iriaebor is justly known as the City of a Thousand  Spires, for fantastically bizarre, many-storied towers rise from all quarters  of the city and are tightly packed together. As a result, it is possible  to navigate Iriaebor via a network of arches, bridges, stairs, and leapable  gulfs far above the city streets. You are well versed in the skill of navigating  the skyroads of Iriaebor." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Spirited Charge" desc="You are trained at making a devastating mounted  charge." />
+    <feat type="Races of Eberron page 113" name="Spiritual Force" desc="Your forceful inner spirit allows you to deal  more damage with your mind blade." />
+    <feat type="Lords of Madness page 94" name="Spit Poison" desc="A creature with this feat can spit its poison  as a ranged touch attack." />
+    <feat type="Serpent Kingdoms page 147" name="Spit Venom" desc="You can spit venom in the manner of a spitting  cobra." />
+    <feat type="Draconomicon page 74" name="Split Breath" desc="You can split your breath weapon into a pair  of weaker effects." />
+    <feat type="Expanded Psionics Handbook page 51" name="Split Psionic Ray" desc="You can affect two targets with a single ray." />
+    <feat type="Complete Arcane page 83" name="Split Ray" desc="Your ray spells can affect an additional target." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 42" name="Split Ray" desc="You can affect two targets with a single ray." />
+    <feat type="Eberron Campaign Setting page 61" name="Spontaneous Casting" desc="You can swap a prepared spell on the fly." />
+    <feat type="Epic Level Handbook page 67" name="Spontaneous Domain Access" desc="Select a domain of spells you have access to.  You can spontaneously convert spells into spells of this domain." />
+    <feat type="Complete Divine page 84" name="Spontaneous Healer" desc="You can use your spellcasting ability to spontaneously  cast cure spells." />
+    <feat type="Epic Level Handbook page 67" name="Spontaneous Spell" desc="Select a spell you can cast. You can spontaneously  convert spells of that spell's level into that spell." />
+    <feat type="Complete Divine page 85" name="Spontaneous Summoner" desc="You can spontaneously cast summon nature's  ally spells." />
+    <feat type="Complete Divine page 85" name="Spontaneous Wounder" desc="You can use your spellcasting ability to spontaneously  cast inflict spells." />
+    <feat type="Draconomicon page 74" name="Spreading Breath" desc="You can convert your breath weapon into a spread  effect." />
+    <feat type="Player's Handbook v.3.5 page 100" name="Spring Attack" desc="You are trained in fast melee attacks and fancy  footwork." />
+    <feat type="Libris Mortis: The Book of the Dead page 30" name="Spurn Death's Touch" desc="You can channel divine energy to remove some  of the harmful effects of attacks made by undead creatures." />
+    <feat type="Races of Eberron page 112" name="Stable Footing" desc="Because of your training and wariness, you are  skilled at keeping your feet in combat and able to move over difficult terrain  with ease." />
+    <feat type="Complete Adventurer page 112" name="Staggering Strike" desc="You can deliver a wound that hampers an opponent's  movement." />
+    <feat type="Races of Faerun page 169" name="Staggering Strike" desc="You are particularly adept at making cruel and  demoralizing sneak attacks." />
+    <feat type="Planar Handbook page 42" name="Stalwart Planar Ally" desc="The allies you summon from a specific plane  are tougher than normal." />
+    <feat type="Savage Species page 39" name="Stamp" desc="You can stamp the ground to crush and disrupt  opponents." />
+    <feat type="Expanded Psionics Handbook page 51" name="Stand Still" desc="You can prevent foes from fleeing or closing." />
+    <feat type="Lords of Madness page 181" name="Starspawn" desc="Your abnormal body and heritage has become more  pronounced. You grow membranous wings and are comfortable in extreme elevations." />
+    <feat type="Races of Stone page 144" name="Steady Concentration" desc="You are an expert at avoiding distractions and  focusing your mind, and you can concentrate clearly even in the most stressful  conditions." />
+    <feat type="Races of Stone page 144" name="Steady Mountaineer" desc="You are so good at climbing cliffs and leaping  across crevasses that distractions don't affect you." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Stealthy" desc="Your people are known for their stealthiness." />
+    <feat type="Player's Handbook v.3.5 page 101" name="Stealthy" desc="You are particularly good at avoiding notice." />
+    <feat type="Stormwrack page 93" name="Steam Magic" desc="You are skilled at casting fiery spells into  the water, causing terrible gouts of scalding steam." />
+    <feat type="Unearthed Arcana page 94" name="Stench of the Dead" desc="The odor of decay hangs heavy on you, causing  others to gasp and retch." />
+    <feat type="Book of Exalted Deeds page 46" name="Stigmata" desc="You bear the marks of wounds on your body, as  sort of a living martyrdom." />
+    <feat type="Player's Handbook v.3.5 page 101" name="Still Spell" desc="You can cast spells without gestures." />
+    <feat type="Libris Mortis: The Book of the Dead page 30" name="Stitched Flesh Familiar" desc="When you are ready and able to acquire a new  familiar, you may choose to gain a stitched flesh familiar." />
+    <feat type="Races of Faerun page 169" name="Stone Colossus" desc="You can focus a part of your power to increase  the toughness of your skin." />
+    <feat type="Races of Stone page 144" name="Stone Form" desc="You can use wild shape to assume a rocklike  form." />
+    <feat type="Races of Stone page 144" name="Stone Rage" desc="Your bond with the earth and tough hide makes  it easier for you to shrug off blows while you are raging." />
+    <feat type="Races of Faerun page 169" name="Stone Slide" desc="You have attuned yourself to stone to such an  extent that you can merge with it for a short time." />
+    <feat type="Underdark page 27" name="Stone Soul" desc="You were born with a dwarflike, innate sense  about rock, stone, and construction." />
+    <feat type="Races of Stone page 144" name="Stoneback" desc="You have studied the techniques of fighting  underground, and you can protect yourself from the dangers of multiple attackers  whenever you can put your back to a solid wall." />
+    <feat type="Races of Faerun page 169" name="Stoneblood" desc="Your blood is thick like cooling lava, making  it difficult for you to die after falling from injuries." />
+    <feat type="Races of Faerun page 169" name="Stoneshaper" desc="You have a deep and abiding tie to earth and  stone." />
+    <feat type="Races of Faerun page 169" name="Stonewalker Fist" desc="You are trained in an unarmed fighting style  that draws on your ability to pass through minerals as if they were air." />
+    <feat type="Frostburn page 50" name="Storm Magic" desc="You gain a boost in spellcasting power during  storms." />
+    <feat type="Stormwrack page 94" name="Storm Magic" desc="You gain a boost in spellcasting power during  storms." />
+    <feat type="Epic Level Handbook page 67" name="Storm of Throws" desc="You become a flurry of thrown weapons, targeting  all nearby opponents." />
+    <feat type="Player's Guide to Faerun page 44" name="Stormheart" desc="The sea is in your blood." />
+    <feat type="Dragonlance Campaign Setting page 87" name="Strafing Breath" desc="You can sustain your breath weapon when you  use it on the wing, covering a larger ground area in its effect." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Street Smart" desc="You have learned how to keep informed, ask questions,  and interact with the underworld without raising suspicion." />
+    <feat type="Player's Guide to Faerun page 44" name="Street Smart" desc="You know how to keep informed, ask questions,  and interact with the underworld without raising suspicions." />
+    <feat type="Oriental Adventures page 66" name="Strength of the Charger" desc="You share the spirit of Utaku Shiko, the founder  of the Utaku Battle Maiden tradition." />
+    <feat type="Oriental Adventures page 87" name="Strength of the Crab" desc="You claim descent from Hida, the first Crab." />
+    <feat type="Races of Eberron page 113" name="Strength of Two" desc="Your quori spirit gives you unmatched willpower." />
+    <feat type="Eberron Campaign Setting page 61" name="Strong Mind" desc="You are unusually hard to affect with psionic  powers and mind attacks." />
+    <feat type="Underdark page 27" name="Strong Mind" desc="You are unusually difficult to affect with psionic  powers and mind attacks." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Strong Soul" desc="The souls of your people are hard to separate  from their bodies." />
+    <feat type="Oriental Adventures page 66" name="Strong Soul" desc="You claim descent from Moto Soro, the simple  peasant who earned his place among samurai and founded the Moto family." />
+    <feat type="Player's Guide to Faerun page 44" name="Strong Soul" desc="You possess an innate resistance to fell magic  and supernatural attacks." />
+    <feat type="Player's Handbook v.3.5 page 101" name="Stunning Fist" desc="You know how to strike opponents in vulnerable  areas." />
+    <feat type="Deities and Demigods page 52" name="Subdual Substitution" desc="The deity can modify a spell that uses energy  to deal damage to deal subdual damage instead." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 42" name="Subdual Substitution" desc="You can modify a spell that uses energy to deal  damage to deal nonlethal damage instead." />
+    <feat type="Book of Exalted Deeds page 46" name="Subduing Strike" desc="You are adept at striking to deal nonlethal  damage even with normal weapons." />
+    <feat type="Complete Adventurer page 112" name="Subsonics" desc="Your music can affect even those who do not  consciously hear it." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Subsonics" desc="Your music can affect even those who do not  consciously hear it." />
+    <feat type="Races of Destiny page 154" name="Subtle Sigil" desc="You are able to fade your sigils into invisibility,  but still tap into their magical energy." />
+    <feat type="Complete Arcane page 83" name="Sudden Empower" desc="You can cast a spell to greater effect without  special preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Empower" desc="You can cast one spell per day to greater effect  without special preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Energy Affinity" desc="You can modify a spell's energy type once per  day without special preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Enlarge" desc="You may cast one spell per day with a greater  range than normal without special preparation." />
+    <feat type="Complete Arcane page 83" name="Sudden Extend" desc="You can make a spell last longer than normal  without special preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Extend" desc="You can cast one spell per day with a longer  duration than normal without special preparation." />
+    <feat type="Complete Arcane page 83" name="Sudden Maximize" desc="You can cast a spell to maximum effect without  special preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Maximize" desc="Once per day you can cast a spell to maximum  effect without special preparation." />
+    <feat type="Complete Arcane page 83" name="Sudden Quicken" desc="You can cast a spell with a moment's thought  without special preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Quicken" desc="Once per day you can cast a spell with a moment's  thought without special preparation." />
+    <feat type="Complete Arcane page 83" name="Sudden Silent" desc="You can cast a spell silently without special  preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Silent" desc="Once per day you can cast a spell silently without  special preparation." />
+    <feat type="Complete Arcane page 83" name="Sudden Still" desc="You can cast a spell without gestures or special  preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Still" desc="Once per day you can cast a spell without gestures  without special preparation." />
+    <feat type="Complete Arcane page 83" name="Sudden Widen" desc="You can increase a spell's area without special  preparation." />
+    <feat type="Miniatures Handbook page 28" name="Sudden Widen" desc="Once per day you can increase the area of a  spell without special preparation." />
+    <feat type="Frostburn page 50" name="Sugliin Mastery" desc="You are a master at fighting with the massive  sugliin." />
+    <feat type="Races of Faerun page 169" name="Summon Earth Elemental" desc="Like many experienced deep gnomes, you have  developed the ability to summon earth elementals to help you with tasks." />
+    <feat type="Complete Warrior page 112" name="Sun School" desc="You have learned a number of esoteric martial  arts techniques inspired by the sun." />
+    <feat type="Stormwrack page 94" name="Sunken Song" desc="You can project your voice underwater." />
+    <feat type="Deities and Demigods page 52" name="Superior Expertise" desc="The deity has mastered the art of defense in  combat." />
+    <feat type="Faiths &amp; Pantheons page 215" name="Superior Expertise" desc="You have mastered the art of defense in combat." />
+    <feat type="Oriental Adventures page 66" name="Superior Expertise" desc="You have mastered the art of defense in combat." />
+    <feat type="Epic Level Handbook page 67" name="Superior Initiative" desc="You can react even more quickly than normal  in a fight." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Supernatural Blow" desc="Choose one favored enemy that is immune to critical  hits. You know how to place blows against this opponent for best effect." />
+    <feat type="Savage Species page 39" name="Supernatural Transformation" desc="You convert a spell-like ability to a supernatural  ability." />
+    <feat type="Draconomicon page 74" name="Suppress Weakness " desc="Your vulnerability to an energy type is reduced." />
+    <feat type="Player's Guide to Faerun page 45" name="Surefooted" desc="You are used to fighting on steep slopes and  treacherous surfaces." />
+    <feat type="Savage Species page 39" name="Surrogate Spellcasting" desc="You use substitute verbal and somatic components  when casting spells." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Survivor" desc="Your people thrive in regions that others find  uninhabitable, and excel at uncovering the secrets of the wilderness and  surviving to tell the tale." />
+    <feat type="Ghostwalk page 39" name="Survivor" desc="Your people thrive in a region that others find  uninhabitable, and you excel at uncovering the secrets of the wilderness  and surviving to tell the tale." />
+    <feat type="Player's Guide to Faerun page 45" name="Survivor" desc="Your people thrive in places that others find  almost uninhabitable, and you know many of the secrets of the wilderness." />
+    <feat type="Races of Faerun page 169" name="Svirfneblin Figment" desc="Your time underground has made you acutely aware  of even slight differences in sound and vision in caves that have never  seen the sun. Accordingly, your illusions are finely tuned and ultra-realistic." />
+    <feat type="Savage Species page 40" name="Swamp Stalker" desc="You are adapted to a marshy environment." />
+    <feat type="Epic Level Handbook page 67" name="Swarm of Arrows" desc="You can fire a veritable storm of arrows at  nearby opponents." />
+    <feat type="Complete Warrior page 105" name="Swarmfighting" desc="You and allies with this feat can coordinate  melee attacks against a single target and are adept at fighting side by  side in close quarters." />
+    <feat type="Races of Faerun page 169" name="Swarmfighting" desc="You and allies with this feat can coordinate  melee attacks against a single target and are adept at fighting side by  side in close quarters." />
+    <feat type="Sharn: City of Towers page 158" name="Swarm's Embrace" desc="You have a natural affinity for swarms and can  stand in the midst of a swarm with few harmful effects." />
+    <feat type="Player's Guide to Faerun page 45" name="Swift and Silent" desc="The shadows are your friends, and your footfalls  are whispers of death." />
+    <feat type="Races of Eberron page 116" name="Swiftwing Elite" desc="Your swiftwing shifter trait improves." />
+    <feat type="Complete Divine page 85" name="Swim Like a Fish" desc="You can breathe and swim underwater with grace." />
+    <feat type="Stormwrack page 94" name="Swim-By Attack" desc="You can attack in the middle of a fast pass  by your opponent." />
+    <feat type="Complete Adventurer page 112" name="Tactile Trapsmith" desc="You can rely on your rapid reflexes and nimble  fingers instead of your intellect when searching a room or when disabling  a trap." />
+    <feat type="Draconomicon page 74" name="Tail Constrict" desc="You can make constriction attacks with your  tail." />
+    <feat type="Serpent Kingdoms page 147" name="Tail Rattle" desc="Your tail gains a rattle like that of a serpent." />
+    <feat type="Draconomicon page 74" name="Tail Sweep Knockdown" desc="Your tail sweep attack knocks opponents prone." />
+    <feat type="Races of Eberron page 112" name="Talenta Warrior" desc="You have trained with the ancestral weapons  of the Talenta halflings and are particularly adept at striking from the  back of a dinosaur mount." />
+    <feat type="Expanded Psionics Handbook page 51" name="Talented" desc="You can overchannel powers with less cost to  yourself." />
+    <feat type="Races of Faerun page 170" name="Talfirian Song" desc="You can use the power of your bardic music to  enhance your illusion spells." />
+    <feat type="Shining South page 21" name="Tall Mouther Hunter" desc="Because of your cultural hatred for tall mouthers,  you have had specific training in how best to fight them." />
+    <feat type="Dungeon Master's Guide v.3.5 page 194" name="Tattoo Focus" desc="You bear the powerful magical tattoos of a Red  Wizard of Thay." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Tattoo Focus" desc="You bear the powerful magic tattoos of a Red  Wizard of Thay." />
+    <feat type="Player's Guide to Faerun page 45" name="Tattoo Focus" desc="You bear the powerful magical tattoos of a Red  Wizard of Thay." />
+    <feat type="Lords of Darkness page 189" name="Tattoo Magic" desc="You can create tattoos that store spells." />
+    <feat type="Races of Faerun page 170" name="Tattoo Magic" desc="You can create tattoos that store spells." />
+    <feat type="Ghostwalk page 39" name="Temper Ectoplasm" desc="You can make durable equipment out of ectoplasm." />
+    <feat type="Draconomicon page 74" name="Tempest Breath" desc="You can make your breath weapon strike with  the force of a windstorm." />
+    <feat type="Epic Level Handbook page 68" name="Tenacious Magic" desc="Choose one of your spells or spell-like abilities.  That magic cannot be dispelled, only suppressed." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Tenacious Magic" desc="You can use the Shadow Weave to make your spells  harder for Weave users to dispel." />
+    <feat type="Player's Guide to Faerun page 45" name="Tenacious Magic" desc="You can use the Shadow Weave to make your spells  harder for Weave users to dispel." />
+    <feat type="Dungeon Master's Guide v.3.5 page 210" name="Terrifying Rage" desc="While in a rage, you panic your opponents." />
+    <feat type="Epic Level Handbook page 68" name="Terrifying Rage" desc="While in a rage, you panic your opponents." />
+    <feat type="Oriental Adventures page 81" name="The Gentle Way Mastery" desc="You have mastered the martial arts style of  'The Gentle Way' -- a soft form emphasizing throws and movement." />
+    <feat type="Races of Faerun page 170" name="Theocrat" desc="You have the delicate touch needed to maintain  the favor of your patron deity and the political skills needed to survive  in the trenches of bureaucratic warfare common in the lands ruled by agents  of the Mulhorandi pantheon." />
+    <feat type="Lords of Madness page 23" name="Thicken Mucus" desc="An aboleth with this feat can produce mucus  that is thicker than normal, and other creatures find it difficult to swim  through." />
+    <feat type="Savage Species page 40" name="Thick-Skinned" desc="Your tough hide grants improved damage reduction." />
+    <feat type="Lords of Madness page 182" name="Thrall Bred" desc="Spawned in the breeding pits of the mind flayers  or the beholders, you have unusual strength and hardiness, as well as loyalty." />
+    <feat type="Book of Vile Darkness page 50" name="Thrall to Demon" desc="The character formally supplicates himself to  a demon prince." />
+    <feat type="Champions of Ruin page 23" name="Thrall to Demon" desc="You formally supplicate yourself to a demon  prince." />
+    <feat type="Complete Warrior page 114" name="Three Mountains" desc="You are a master of fighting with powerful bludgeoning  weapons." />
+    <feat type="Complete Warrior page 105" name="Throw Anything" desc="In your hands, any weapon becomes a deadly ranged  weapon." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 9" name="Throw Anything" desc="In your hands, any weapon becomes a deadly ranged  weapon." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Thug" desc="Your people know how to get the jump on the  competition and push other people around." />
+    <feat type="Player's Guide to Faerun page 45" name="Thug" desc="You have a knack for getting the jump on the  competition and pushing other people around." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Thunder Twin" desc="You are one of the dwarven generation of twins  born after Moradin's Thunder Blessing in the Year of Thunder (1306 DC)." />
+    <feat type="Player's Guide to Faerun page 46" name="Thunder Twin" desc="You are one of the generation of dwarf twins  born after Moradin's Thunder Blessing in the Year of Thunder." />
+    <feat type="Savage Species page 40" name="Thunderclap" desc="You create a cone of deafening sound by clapping  two limbs together." />
+    <feat type="Epic Level Handbook page 68" name="Thundering Rage" desc="Your rage attacks can cause thunderous roars  that can deafen opponents." />
+    <feat type="Player's Guide to Faerun page 46" name="Tireless" desc="You don't know the meaning of the word 'quit.'" />
+    <feat type="Races of Stone page 145" name="Titan Fighting" desc="You have been trained to fight larger creatures,  and you are adept at dodging their attacks." />
+    <feat type="Libris Mortis: The Book of the Dead page 30" name="Tomb-Born Fortitude" desc="The power of undeath taints you, body and soul.  Its power has hardened your flesh and given it the foul look of the grave." />
+    <feat type="Libris Mortis: The Book of the Dead page 30" name="Tomb-Born Resilience" desc="The power of undeath taints you, deadening your  mind and body to the effects of mind-controlling magic, poison, and disease." />
+    <feat type="Libris Mortis: The Book of the Dead page 31" name="Tomb-Born Vitality" desc="The power of undeath taints you, body and soul.  Its power has removed your need to sleep and eat." />
+    <feat type="Libris Mortis: The Book of the Dead page 31" name="Tomb-Tainted Soul" desc="Your soul is tainted by the foul touch of undeath." />
+    <feat type="Stormwrack page 94" name="Toothed Blow" desc="You are able to hammer your foes more effectively  underwater." />
+    <feat type="Champions of Ruin page 23" name="Tormented Knight" desc="You are inexorably bound to the loathsome yugoloths  that lurk in the Barrens of Doom and Despair, and you strive to bring misery  and pain to all creatures that oppose them." />
+    <feat type="Eberron Campaign Setting page 61" name="Totem Companion" desc="Instead of an animal companion, you have your  totem magical beast as a companion." />
+    <feat type="Ghostwalk page 39" name="Touch Attack Specialization" desc="Choose one of your ghost touch attacks that  deals hit point damage, ability damage, or ability drain, such as Corrupting  Touch. You are especially good at using this touch attack." />
+    <feat type="Champions of Ruin page 22" name="Touch of Benevolence" desc="Despite your evil alignment, you are prone to  moments of benevolence and mercy." />
+    <feat type="Book of Exalted Deeds page 47" name="Touch of Golden Ice" desc="Your touch is poisonous to evil creatures." />
+    <feat type="Player's Guide to Faerun page 177" name="Touch of Hate" desc="Because you are favored by Bane, you can transform  animals into evil minions." />
+    <feat type="Complete Arcane page 83" name="Touch Spell Specialization" desc="You deal extra damage with touch spells." />
+    <feat type="Sandstorm page 53" name="Touchstone" desc="You forge a link with a power-rich location,  referred to as a touchstone site." />
+    <feat type="Player's Handbook v.3.5 page 101" name="Toughness" desc="You are tougher than normal." />
+    <feat type="Player's Handbook v.3.5 page 101" name="Tower Shield Proficiency" desc="You are proficient with tower shields." />
+    <feat type="Lords of Madness page 23" name="Toxic Mucus" desc="An aboleth with this feat can produce mucus  that is poisonous to other creatures." />
+    <feat type="Player's Handbook v.3.5 page 101" name="Track" desc="You can follow the trails of creatures and characters  across most types of terrain." />
+    <feat type="Player's Handbook v.3.5 page 101" name="Trample" desc="You are trained in using your mount to knock  down opponents." />
+    <feat type="Complete Arcane page 84" name="Transdimensional Spell" desc="You can cast spells that affect targets lurking  in coexistent planes and extradimensional spaces whose entrances fall within  the spell's area." />
+    <feat type="Complete Divine page 85" name="Transdimensional Spell" desc="You can cast spells that affect targets lurking  in coexistent planes and extradimensional spaces whose entrances fall within  the spell's area." />
+    <feat type="UE page 45" name="Transdimensional Spell" desc="You can cast spells that affect targets lurking  in coexistent planes and extradimensional spaces whose entrances fall within  the spell's area." />
+    <feat type="WL page 16" name="Transfer Legacy" desc="You can temporarily transfer one of your legacy  item's abilities to another magic item." />
+    <feat type="Epic Level Handbook page 68" name="Trap Sense" desc="You can sense nearby traps even if not actively  searching for them." />
+    <feat type="Lost Empires of Faerun page 9" name="Trapmaster" desc="You have studied the funereal architecture and  lethal traps of a dozen long-dead cultures, which gives you an uncanny knack  for avoiding traps." />
+    <feat type="Savage Species page 40" name="Treefriend" desc="You are adapted to a forest environment." />
+    <feat type="Forgotten Realms Campaign Setting page 38" name="Treetopper" desc="Your people are at home in the trees and high  places, daring falls that paralyze most other folk in abject terror." />
+    <feat type="Player's Guide to Faerun page 46" name="Treetopper" desc="Your people are at home in the trees and high  places." />
+    <feat type="Dragonlance Campaign Setting page 87" name="Tremendous Charge" desc="You know how to use your mount's power to make  your lance attacks even more deadly." />
+    <feat type="Races of Stone page 145" name="Trivial Knowledge" desc="You have the ability to dredge up obscure knowledge  in appropriate situations." />
+    <feat type="Complete Divine page 86" name="True Believer" desc="Your deity rewards your unquestioning faith  and dedication." />
+    <feat type="Dungeon Master's Guide II page 232" name="Truebond" desc="Your bond to your chosen item becomes stronger." />
+    <feat type="Races of Eberron page 116" name="Truedive Elite" desc="Your truedive shifter trait improves." />
+    <feat type="Song and Silence: A Guidebook to Bards and Rogues page 40" name="Trustworthy" desc="Others feel comfortable telling you their secrets." />
+    <feat type="Races of Stone page 145" name="Tunnel Fighting" desc="You are adept at maneuvering and fighting in  tight spaces and underground passages." />
+    <feat type="Races of Stone page 145" name="Tunnel Riding" desc="You are particularly adept at maneuvering mounts  through tight spaces and underground passages." />
+    <feat type="Underdark page 27" name="Tunnelfighter" desc="You can fight more naturally in the cramped  and close quarters of caves and tunnels than usual." />
+    <feat type="Underdark page 27" name="Tunnelrunner" desc="You can move naturally in the cramped quarters  of caves and tunnels." />
+    <feat type="Races of Stone page 145" name="Turtle Dart" desc="You have mastered the style of fighting with  a short sword while wearing extremely heavy armor and carrying a large shield." />
+    <feat type="Expanded Psionics Handbook page 51" name="Twin Power" desc="You can manifest a power simultaneously with  another power just like it." />
+    <feat type="Complete Arcane page 84" name="Twin Spell" desc="You can simultaneously cast a single spell twice." />
+    <feat type="Forgotten Realms Campaign Setting page 39" name="Twin Spell" desc="You can cast a spell simultaneously with another  spell just like it." />
+    <feat type="Player's Guide to Faerun page 46" name="Twin Spell" desc="You can cast a spell simultaneously with another  spell just like it." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 42" name="Twin Spell" desc="You can cast a spell simultaneously with another  spell just like it." />
+    <feat type="Forgotten Realms Campaign Setting page 39" name="Twin Sword Style" desc="You have mastered a style of defense that others  find frustrating." />
+    <feat type="Ghostwalk page 39" name="Twin Sword Style" desc="You have mastered a style of defense that others  find frustrating." />
+    <feat type="Player's Guide to Faerun page 46" name="Twin Sword Style" desc="You have mastered a defensive style based on  wielding a blade in each hand." />
+    <feat type="Player's Handbook v.3.5 page 102" name="Two-Weapon Defense" desc="Your two-weapon fighting style bolsters your  defense as well as your offense." />
+    <feat type="Player's Handbook v.3.5 page 102" name="Two-Weapon Fighting" desc="You can fight with a weapon in each hand. You  can make one extra attack each round with the second weapon." />
+    <feat type="Epic Level Handbook page 68" name="Two-Weapon Rend" desc="You can rend opponents when fighting with two  weapons." />
+    <feat type="Races of Eberron page 120" name="Unarmored Body" desc="Your body is crafted without its normal layer  of armor, trading off physical strength for magical potential." />
+    <feat type="Expanded Psionics Handbook page 52" name="Unavoidable Strike" desc="You can make an unarmed strike or use a natural  weapon against your foe as if delivering a touch attack." />
+    <feat type="Oriental Adventures page 66" name="Unbalancing Strike" desc="You can strike a humanoid opponent's joints  to knock him off balance." />
+    <feat type="Epic Level Handbook page 68" name="Uncanny Accuracy" desc="You can ignore anything less than total cover  or total concealment when using ranged weapons." />
+    <feat type="Savage Species page 40" name="Uncanny Scent" desc="You can pinpoint scents at a greater distance." />
+    <feat type="Expanded Psionics Handbook page 52" name="Unconditional Power" desc="Disabling conditions do not hold you back." />
+    <feat type="Eberron Campaign Setting page 61" name="Undead Empathy" desc="You are adept at communicating with and influencing  the undead." />
+    <feat type="Libris Mortis: The Book of the Dead page 31" name="Undead Leadership" desc="You gain the service of loyal undead followers." />
+    <feat type="Complete Divine page 90" name="Undead Mastery" desc="You can command a greater number of undead than  normal." />
+    <feat type="Epic Level Handbook page 68" name="Undead Mastery" desc="You can command a greater number of undead than  normal." />
+    <feat type="Races of the Wild page 152" name="Underfoot Combat" desc="You can enter the space that a foe at least  two size categories bigger than you occupies." />
+    <feat type="Races of Destiny page 155" name="Undying Fate" desc="You have pledged your unswerving obedience to  Wee Jas, and she in turn has granted you special insight into life and death." />
+    <feat type="Complete Divine page 90" name="Unholy Strike" desc="Your attacks deal great damage to good creatures." />
+    <feat type="Epic Level Handbook page 68" name="Unholy Strike" desc="Your attacks deal great damage to good creatures." />
+    <feat type="Libris Mortis: The Book of the Dead page 31" name="Unquenchable Flame of Life" desc="You are hardened to the attacks of the undead." />
+    <feat type="Expanded Psionics Handbook page 52" name="Up the Walls" desc="You can run on walls for brief distances." />
+    <feat type="Races of Destiny page 154" name="Urban Stealth" desc="You are particularly adept at moving quietly  and unnoticed through the city." />
+    <feat type="Eberron Campaign Setting page 61" name="Urban Tracking" desc="You can track the location of missing persons  or wanted individuals within communities." />
+    <feat type="Races of Destiny page 154" name="Urban Tracking" desc="You can track down the location of missing persons  or wanted individuals within communities." />
+    <feat type="Libris Mortis: The Book of the Dead page 31" name="Vampire Hunter" desc="Your knowledge of vampires has given you the  extraordinary ability to detect subtle signs of their presence and to resist  their dominating gaze ability." />
+    <feat type="City of Splendors: Waterdeep page 146" name="Veil of Cyric" desc="You have reconciled yourself to the unfortunate  truth that hard decisions and regrettable actions are necessary in the service  of your deity." />
+    <feat type="Eberron Campaign Setting page 62" name="Vermin Companion" desc="Instead of an animal companion, you have a vermin  creature as a companion." />
+    <feat type="Eberron Campaign Setting page 62" name="Vermin Shape" desc="You can use your wild shape ability to assume  vermin forms instead of animal forms." />
+    <feat type="Epic Level Handbook page 68" name="Vermin Wild Shape" desc="You can wild shape into vermin form." />
+    <feat type="Book of Vile Darkness page 50" name="Verminfriend" desc="Vermin regard the character better than they  would normally." />
+    <feat type="Complete Adventurer page 112" name="Versatile Performer" desc="You are skilled at many kinds of performances." />
+    <feat type="Heroes of Battle page 99" name="Veteran Knowledge" desc="You are capable of seeing potential battlefield  advantages where others cannot." />
+    <feat type="Champions of Ruin page 22" name="Via Negativa" desc="You can channel greater amounts of negative  energy into your inflict spells." />
+    <feat type="Savage Species page 40" name="Vicious Wound" desc="Damage you deal causes wounds that bleed excessively." />
+    <feat type="Book of Vile Darkness page 50" name="Vile Ki Strike" desc="The character can focus evil power into his  unarmed strike." />
+    <feat type="Book of Vile Darkness page 50" name="Vile Martial Strike" desc="The character can focus evil power in her weapon  blows." />
+    <feat type="Book of Vile Darkness page 50" name="Vile Natural Attack" desc="The character can focus evil power into his  natural attacks." />
+    <feat type="Book of Vile Darkness page 50" name="Violate Spell" desc="The character can transform one of his spells  into an evil spell, and the wounds the spell inflicts are tainted with the  foulest evil." />
+    <feat type="Book of Vile Darkness page 50" name="Violate Spell-Like Ability" desc="The creature's spell-like abilities are particularly  tainted with evil." />
+    <feat type="Savage Species page 40" name="Virulent Poison" desc="Your poison attack is more effective." />
+    <feat type="Epic Level Handbook page 68" name="Vorpal Strike" desc="Your unarmed strikes can behead your opponents." />
+    <feat type="Book of Exalted Deeds page 47" name="Vow of Abstinence" desc="You have taken a sacred vow to abstain from  alcoholic beverages, drugs, stimulants such as caffeine, and intoxication." />
+    <feat type="Book of Exalted Deeds page 47" name="Vow of Chastity" desc="You have taken a sacred vow to refrain from  marriage and sexual intercourse." />
+    <feat type="Book of Exalted Deeds page 47" name="Vow of Nonviolence" desc="You have taken a sacred vow to avoid violence  against humanoids." />
+    <feat type="Book of Exalted Deeds page 48" name="Vow of Obedience" desc="You have taken a sacred vow to live according  to the dictates of another, generally your superior in a religious order  or similar organization." />
+    <feat type="Book of Exalted Deeds page 48" name="Vow of Peace" desc="You have taken a sacred vow to abstain from  harming any living creature." />
+    <feat type="Book of Exalted Deeds page 48" name="Vow of Poverty" desc="You have taken a sacred vow to forswear material  possessions." />
+    <feat type="Book of Exalted Deeds page 48" name="Vow of Purity" desc="You have taken a sacred vow to avoid contact  with dead flesh." />
+    <feat type="UE page 45" name="Vremyonni Training" desc="You have had more than the typical amount of  training with the vremyonni, the Old Ones who research spells and  craft magic items for the Witches of Rashemen." />
+    <feat type="Eberron Campaign Setting page 62" name="Wand Mastery" desc="Wands are far more potent in your hands." />
+    <feat type="Complete Arcane page 84" name="Wandstrike" desc="You can channel the magical energy of a wand  through your melee attacks." />
+    <feat type="Eberron Campaign Setting page 62" name="Warden Initiate" desc="You have been trained in the ancient druidic  tradition of the Wardens of the Wood, a sect dedicated to protecting the  eastern plain and the great woods of the Eldeen Reaches." />
+    <feat type="Lords of Madness page 182" name="Warped Mind" desc="Your tainted form has altered the physical nature  of your brain, making you resistant to mental effects and more capable of  unleashing the power of your mind on others." />
+    <feat type="Oriental Adventures page 66" name="Warrior Instinct" desc="Your ancestor, Matsu Hitomi, was the most famous  female samurai of the early Empire." />
+    <feat type="Oriental Adventures page 66" name="Warrior Shugenja" desc="Your ancestor, Agasha Nodotai, was a shugenja  well versed in the code of bushido and the way of war." />
+    <feat type="Races of Faerun page 170" name="Water Adaptation" desc="You favor your aquatic elven parent and have  developed the ability to breathe and move about in water easily." />
+    <feat type="Stormwrack page 94" name="Water Adaptation" desc="You favor your aquatic elf parent and have developed  the ability to breathe and move about in water easily." />
+    <feat type="Planar Handbook page 42" name="Water Heritage" desc="You are descended from creatures native to the  Plane of Water." />
+    <feat type="Lords of Madness page 182" name="Waterspawn" desc="Your abnormal body and heritage has become more  pronounced. You have prominent fins and are supremely well adapted to the  icy deeps." />
+    <feat type="Complete Warrior page 106" name="Weakening Touch" desc="You can temporarily weaken an opponent with  your unarmed strike." />
+    <feat type="Player's Handbook v.3.5 page 102" name="Weapon Finesse" desc="You are especially skilled at using weapons  that can benefit as much from Dexterity as from Strength." />
+    <feat type="Player's Handbook v.3.5 page 102" name="Weapon Focus" desc="Choose one type of weapon. You can also choose  unarmed strike or grapple (or ray, if you are a spellcaster) as a weapon  for purposes of this feat. You are especially good with this weapon." />
+    <feat type="Unearthed Arcana page 95" name="Weapon Group (Axes)" desc="You understand how to use axes and axelike weapons." />
+    <feat type="Unearthed Arcana page 95" name="Weapon Group (Basic Weapons)" desc="You understand how to use a few basic weapons." />
+    <feat type="Unearthed Arcana page 95" name="Weapon Group (Bows)" desc="You understand how to use bows." />
+    <feat type="Unearthed Arcana page 95" name="Weapon Group (Claw Weapons)" desc="You understand how to use weapons strapped to  the hands." />
+    <feat type="Unearthed Arcana page 95" name="Weapon Group (Crossbows)" desc="You understand how to use crossbows." />
+    <feat type="Unearthed Arcana page 95" name="Weapon Group (Druid Weapons)" desc="You understand how to use weapons favored by  druids." />
+    <feat type="Unearthed Arcana page 95" name="Weapon Group (Exotic Double Weapon)" desc="You understand how to use the exotic double  weapons associated with the weapon groups that you have mastered." />
+    <feat type="Unearthed Arcana page 96" name="Weapon Group (Exotic Weapons)" desc="You understand how to use the exotic weapons  associated with the weapon groups that you have mastered." />
+    <feat type="Unearthed Arcana page 96" name="Weapon Group (Flails and Chains)" desc="You understand how to use flails and chain weapons." />
+    <feat type="Unearthed Arcana page 96" name="Weapon Group (Heavy Blades)" desc="You understand how to use large bladed weapons." />
+    <feat type="Unearthed Arcana page 96" name="Weapon Group (Light Blades)" desc="You understand how to use light bladed weapons." />
+    <feat type="Unearthed Arcana page 96" name="Weapon Group (Maces and Clubs)" desc="You understand how to use maces and clubs." />
+    <feat type="Unearthed Arcana page 97" name="Weapon Group (Monk Weapons)" desc="You understand how to use weapons normally favored  by monks." />
+    <feat type="Unearthed Arcana page 97" name="Weapon Group (Picks and Hammers)" desc="You understand how to use picks and hammers." />
+    <feat type="Unearthed Arcana page 97" name="Weapon Group (Polearms)" desc="You understand how to use polearms." />
+    <feat type="Unearthed Arcana page 97" name="Weapon Group (Slings and Thrown Weapons)" desc="You understand how to use slings and handheld  thrown weapons." />
+    <feat type="Unearthed Arcana page 97" name="Weapon Group (Spears and Lances)" desc="You understand how to use spears and javelins." />
+    <feat type="Player's Handbook v.3.5 page 102" name="Weapon Specialization" desc="Choose one type of weapon for which you have  already selected the Weapon Focus feat. You can also choose unarmed strike  or grapple as your weapon for purposes of this feat. You deal extra damage  when using this weapon." />
+    <feat type="Eberron Campaign Setting page 62" name="Whirling Steel Strike" desc="Through monastic weapon training, you have mastered  a fighting style that makes use of an unusual monk weapon: the longsword." />
+    <feat type="Player's Handbook v.3.5 page 102" name="Whirlwind Attack" desc="You can strike nearby opponents in an amazing,  spinning attack" />
+    <feat type="Draconomicon page 75" name="Whirlwind Tail Sweep" desc="You can sweep your tail in a circular arc." />
+    <feat type="Races of Destiny page 155" name="Whispered Secrets" desc="You revere the Maimed Lord and have devoted  your miserable, worthless life to learning but a few of the Whispered One's  secrets." />
+    <feat type="Races of Eberron page 112" name="White Scorpion Strike" desc="Your fists and feet sting like the dread white  scorpion and are particularly effective against undead." />
+    <feat type="Epic Level Handbook page 69" name="Widen Aura of Courage" desc="Your aura of courage is wider than normal." />
+    <feat type="Epic Level Handbook page 69" name="Widen Aura of Despair" desc="Your aura of despair is wider than normal." />
+    <feat type="Expanded Psionics Handbook page 52" name="Widen Power" desc="You can increase the area of your powers." />
+    <feat type="Deities and Demigods page 52" name="Widen Spell " desc="The deity can increase the area of its spells." />
+    <feat type="Magic of Faerun page 23" name="Widen Spell" desc="You can increase the area of your spells." />
+    <feat type="Player's Handbook v.3.5 page 102" name="Widen Spell" desc="You can increase the area of your spells." />
+    <feat type="Tome and Blood: A Guidebook to Wizards and Sorcerers page 42" name="Widen Spell" desc="You can increase the area of your spells." />
+    <feat type="Complete Warrior page 153" name="Wield Oversized Weapon" desc="You can use larger than normal weapons with  ease." />
+    <feat type="Lords of Madness page 182" name="Wild Talent" desc="Your mind wakes to a previously unrealized talent  for psionics." />
+    <feat type="Expanded Psionics Handbook page 52" name="Wild Talent" desc="Your mind wakes to a previously unrealized talent  for psionics." />
+    <feat type="Races of Eberron page 116" name="Wildhunt Elite" desc="Your shifter-enhanced instincts and senses allow  you to detect concealed and invisible creatures." />
+    <feat type="Book of Vile Darkness page 50" name="Willing Deformity" desc="Through scarification, self-mutilation, and  supplication to dark power, the character intentionally mars her own body." />
+    <feat type="Races of the Wild page 153" name="Winged Warrior" desc="You use your wings for more than just flying." />
+    <feat type="Monster Manual v.3.5 page 304" name="Wingover" desc="The creature can change direction quickly while  flying." />
+    <feat type="Monster Manual II page 18" name="Wingover" desc="The creature can change direction quickly while  flying." />
+    <feat type="Masters of the Wild: A Guidebook to Barbarians, Druids, and Rangers page 25" name="Wingover" desc="You change direction quickly once per round  while airborne." />
+    <feat type="Stormwrack page 94" name="Wingsinger" desc="You can use song or a wind instrument to compel  the winds to obey you." />
+    <feat type="Draconomicon page 75" name="Wingstorm" desc="You can flatten targets with blasts of air from  your wings." />
+    <feat type="Savage Species page 40" name="Wingstorm" desc="You can flatten targets with blasts of air from  your wings." />
+    <feat type="Frostburn page 50" name="Winter's Champion" desc="Your paladin spell list is enhanced." />
+    <feat type="Savage Species page 40" name="Winter's Child" desc="You are adapted to a cold environment." />
+    <feat type="Frostburn page 50" name="Winter's Mount" desc="Your special mount is native to the frostfell." />
+    <feat type="Underdark page 27" name="Wisdom Breeds Caution" desc="Not getting into a dangerous situation is generally  the wisest course, but if danger is unavoidable, you're prepared. You rely  more on caution and forethought than you do on physical prowess." />
+    <feat type="Ghostwalk page 39" name="Wise to Your Ways" desc="You are particularly resistant to the unusual  attacks of your favored enemy." />
+    <feat type="Ghostwalk page 39" name="Witchlight" desc="You can create witchlight, a harmless faint  light, on yourself or an object." />
+    <feat type="UE page 45" name="Wolf Berserker" desc="You have studied the fighting style of the wolf  and employ its tactics in combat." />
+    <feat type="Races of the Wild page 153" name="Wolfpack" desc="You can gain an extra advantage when you and  your allies can gang up on a foe." />
+    <feat type="Complete Divine page 86" name="Wolverine's Rage" desc="You can fly into a berserk rage when injured." />
+    <feat type="Races of the Wild page 154" name="Woodland Archer" desc="You have honed your archery ability in the wilds  of the forest." />
+    <feat type="Shining South page 21" name="Woodwise" desc="You are trained in fighting in woodlands and  know how to use the terrain to best advantage." />
+    <feat type="UE page 45" name="Woodwise" desc="You are trained in fighting in woodlands and  know how to use the terrain to best advantage." />
+    <feat type="Book of Exalted Deeds page 48" name="Words of Creation" desc="You have learned a few of the words that were  spoken to create the world." />
+    <feat type="Expanded Psionics Handbook page 52" name="Wounding Attack" desc="Your vicious attacks wound your foe." />
+    <feat type="Lost Empires of Faerun page 9" name="Wounding Spell" desc="Because you have studied the cruel arts of the  Athalantan magelords of old, you know how to cast spells that cause terrible,  bleeding wounds." />
+    <feat type="Races of the Wild page 152" name="Yondalla's Sense" desc="You display a shrewd perception of danger. Other  halflings say the blessing of Yondalla is upon you." />
+    <feat type="Complete Warrior page 106" name="Zen Archery" desc="Your intuition guides your hand when you use  a ranged weapon." />
+    <feat type="Sword and Fist: A Guidebook to Monks and Fighters page 9" name="Zen Archery" desc="Your intuition guides your hand when you use  a ranged weapon." />
+    <feat type="Complete Divine page 90" name="Zone of Animation" desc="You can channel negative energy to animate  undead." />
+    <feat type="Epic Level Handbook page 69" name="Zone of Animation" desc="You can channel negative energy to animate  undead." />
+    <feat type="Champions of Valor page 28" name="Broken One's Sacrifice" desc="Your dedication to Ilmater's philosophy has given you the power to take attacks directed at others." />
+    <feat type="Champions of Valor page 28" name="Carmendine Monk" desc="You have learned that study is just as important as insight to finding enlightenment." />
+    <feat type="Champions of Valor page 28" name="Defender of the Homeland" desc="You have sworn a sacred oath to protect your country from evil." />
+    <feat type="Champions of Valor page 28" name="Detect Shadow Weave User" desc="You can determine if a magic item or spellcaster is using the Weave or the Shadow Weave." />
+    <feat type="Champions of Valor page 29" name="Druuth Slayer" desc="You have studied the lore of the druuth (a cabal of doppelgangers led by a mind flayer) and know how to recognize and resist their powers." />
+    <feat type="Champions of Valor page 29" name="Duerran Metaform Training" desc="Your studies have shown you the way to link your psionics and your enlarge person spell-like ability." />
+    <feat type="Champions of Valor page 29" name="Duerran Stealth Training" desc="Your studies have shown you the way to link your psionics and your invisibility spell-like ability." />
+    <feat type="Champions of Valor page 29" name="From Smite to Song" desc="You can channel your destructive holy energy into powerful song magic for the glory of Milil." />
+    <feat type="Champions of Valor page 30" name="Initiate of Anhur" desc="You have been initiated into the greatest secrets of Anhur's church." />
+    <feat type="Champions of Valor page 30" name="Initiate of Arvoreen" desc="You have been initiated into the greatest secrets of Arvoreen's church." />
+    <feat type="Champions of Valor page 30" name="Initiate of Baravar Cloakshadow" desc="You have been initiated into the greatest secrets of Baravar Cloakshadow's church." />
+    <feat type="Champions of Valor page 30" name="Initiate of Eilistraee" desc="You have been initiated into the greatest secrets of Eilistraee's church." />
+    <feat type="Champions of Valor page 30" name="Initiate of the Holy Realm" desc="You have been initiated into the greatest secrets of one of the faiths of the Holy Realm (Chauntea, Helm, Lathander, Selune, or Sune)." />
+    <feat type="Champions of Valor page 30" name="Initiate of Horus-Re" desc="You have been initiated into the greatest secrets of Horus-Re's church." />
+    <feat type="Champions of Valor page 31" name="Initiate of Milil" desc="You have been initiated into the greatest secrets of Milil's church." />
+    <feat type="Champions of Valor page 31" name="Initiate of Nobanion" desc="You have been initiated into the greatest secrets of Nobanion's church." />
+    <feat type="Champions of Valor page 31" name="Initiate of Torm" desc="You have been initiated into the greatest secrets of Torm's church." />
+    <feat type="Champions of Valor page 32" name="Initiate of Tymora" desc="You have been initiated into the greatest secrets of Tymora's church." />
+    <feat type="Champions of Valor page 32" name="Knight of the Red Falcon" desc="Your military order has a legendary ability to survive against overwhelming odds." />
+    <feat type="Champions of Valor page 32" name="Knight of the Risen Scepter" desc="Your military order is dedicated to fighting Set and his minions, and even death cannot stop you from this task." />
+    <feat type="Champions of Valor page 32" name="Knight of Tyr's Holy Judgment" desc="You can draw upon the power of Tyr to sense and understand the law and to locate devils." />
+    <feat type="Champions of Valor page 33" name="Knight ot Tyr's Merciful Sword" desc="You can draw upon the power of Tyr to sense where you are needed." />
+    <feat type="Champions of Valor page 33" name="Mark of the Triad" desc="You have been initiated into the greatest secrets of the Triad, the godly triumvirate of Tyr, Torm, and Ilmater." />
+    <feat type="Champions of Valor page 33" name="Overcome Shadow Weave" desc="You understand the strengths and weaknesses of the Shadow Weave and are more resistant to its tricks." />
+    <feat type="Champions of Valor page 33" name="Paladin of the Noble Heart" desc="You are tasked by Ilmater to eliminate cruelty from the world, particularly that of Loviatar." />
+    <feat type="Champions of Valor page 33" name="Silver Blood" desc="You have magically or alchemically imbued your flesh and blood with silver, making you resistant to lycanthrope attacks." />
+    <feat type="Champions of Valor page 33" name="Silver Fang" desc="By following a ritual taught by the Fangshields, your natural attacks are suffused with the power of silver and are fully effective against lycanthropes." />
+    <feat type="Champions of Valor page 33" name="Smiting Power" desc="You use your smite ability to augment other combat maneuvers." />
+    <feat type="Champions of Valor page 34" name="Sword of the Arcane Order" desc="Members of your military order have a special connection with arcane magic." />
+    <feat type="Champions of Valor page 34" name="Sun Soul Monk" desc="Your training with this monk order gives you special powers depending on which sect you follow." />
+    <feat type="Heroes of Horror page 119" name="Archivist of Nature" desc="In addition to your studies of the darkness, you have spent time studying giants and fey." />
+    <feat type="Heroes of Horror page 119" name="Bane Magic" desc="Your spells deal extra damage to a particular type of creature." />
+    <feat type="Heroes of Horror page 120" name="Blood Calls to Blood" desc="Exploring the latent potential in your blood due to your fiendish descent, you learn how to better adapt to the mystical attacks of your forebears." />
+    <feat type="Heroes of Horror page 120" name="Corrupt Arcana" desc="You can prepare and cast corrupt spells." />
+    <feat type="Heroes of Horror page 120" name="Corrupt Spell Focus" desc="All spells you cast that have a corrupt component (such as call forth the beast, master's lament, or chain of sorrow) are more potent than normal." />
+    <feat type="Heroes of Horror page 120" name="Debilitating Spell" desc="By calling upon the taint within, you add a malign power to your offensive spells." />
+    <feat type="Heroes of Horror page 120" name="Debilitating Strike" desc="By calling upon the taint within, you add a malign power to your melee attacks." />
+    <feat type="Heroes of Horror page 121" name="Deformity (Skin)" desc="Due to a regimen of deliberate abuse, you have roughened your skin until it has grown as coarse and tough as rhino hide." />
+    <feat type="Heroes of Horror page 121" name="Deformity (Tall)" desc="Through long and painful stints on the rack, bolstered by the surgical implantation of various splints and struts, you have stretched yourself to well over 7 feet in height." />
+    <feat type="Heroes of Horror page 121" name="Deformity (Teeth)" desc="By filing your teeth to points and brutalizing your gums, you gain a hideous smile full of razor-sharp teeth that enable you to make a grisly bite attack." />
+    <feat type="Heroes of Horror page 121" name="Deformity (Tongue)" desc="Through protracted self-mutilation that involves frequently piercing your tongue and dipping it in acid, your tongue becomes hideous to behold but oddly sensitive to the environment." />
+    <feat type="Heroes of Horror page 121" name="Disease Immunity" desc="Whether due to prolonged exposure or natural hardiness, you have grown immune to some diseases and resistant to all others." />
+    <feat type="Heroes of Horror page 122" name="Draconic Archivist" desc="In addition to your studies of the darkness, you have spent time studying dragons and constructs." />
+    <feat type="Heroes of Horror page 122" name="Dreamtelling" desc="You can use your Knowledge (the planes) skill to interpret your dreams or the dreams of others, thus gleaning useful information and insights." />
+    <feat type="Heroes of Horror page 122" name="Eldritch Corruption" desc="You can add power to your spells or spell-like abilities at the expense of your companions' health." />
+    <feat type="Heroes of Horror page 122" name="Font of Life" desc="Your life-force is strong enough to make you highly resistant to all forms of energy drain and level loss." />
+    <feat type="Heroes of Horror page 123" name="Forbidden Lore" desc="You gain hideous insights into subjects not meant to be understood by mortal minds." />
+    <feat type="Heroes of Horror page 123" name="Greater Corrupt Spell Focus" desc="Your corrupt spells are now even more potent than they were before." />
+    <feat type="Heroes of Horror page 123" name="Haunting Melody" desc="You can use your music to inspire fear." />
+    <feat type="Heroes of Horror page 123" name="Improved Oneiromancy" desc="With the Improved Oneiromancy feat, you gain additional dream-related spellcasting abilities." />
+    <feat type="Heroes of Horror page 123" name="Lunatic Insight" desc="Your madness grants you insight and knowledge." />
+    <feat type="Heroes of Horror page 123" name="Mad Faith" desc="Your depravity has twisted the connection between you and your patron deity. You suffer flashes of insight interrupted by flashes of madness." />
+    <feat type="Heroes of Horror page 123" name="Master of Knowledge" desc="You have spent most of your life in study, and it comes naturally to you now." />
+    <feat type="Heroes of Horror page 123" name="Oneiromancy" desc="You gain a number of abilities and advantages related to dreams and magic." />
+    <feat type="Heroes of Horror page 124" name="Pure Soul" desc="Your faith or purity of mind overrides the evils within you. You are immune to taint." />
+    <feat type="Heroes of Horror page 124" name="Spirit Sense" desc="You can see and communicate with the souls of the recently departed." />
+    <feat type="Heroes of Horror page 124" name="Surge of Malevolence" desc="You empower yourself by drawing on the taint within." />
+    <feat type="Heroes of Horror page 124" name="Tainted Fury" desc="You can channel your physical corruption into a state of fury." />
+    <feat type="Heroes of Horror page 124" name="Touch of Taint" desc="One of your attack forms that normally deals ability damage, ability drain, or energy drain can also deal corruption or depravity." />
+    <feat type="Heroes of Horror page 124" name="Unnatural Will" desc="You have learned to focus your force of personality and inner strength to stand against fearful circumstances." />
+    <feat type="Heroes of Horror page 125" name="Willing Deformity" desc="Through scarification, self-mutilation, or supplication to dark powers, you intentionally mar your own body." />
+    <feat type="Magic of Eberron page 46" name="Augment Elemental" desc="Your knowledge of planar magic allows you to imbue your summoned elementals with extraordinary combat prowess and durability." />
+    <feat type="Magic of Eberron page 46" name="Cull Wand Essence" desc="You can focus the raw magical energy of a wand or staff into a beam of energy." />
+    <feat type="Magic of Eberron page 46" name="Deathless Fleshgrafter" desc="You can grow and graft the tissues and body parts of deathless creatures onto others, granting the recipients of your grafts new, potent abilities." />
+    <feat type="Magic of Eberron page 46" name="Dorje Mastery" desc="Psionic dorjes are more potent in your hands." />
+    <feat type="Magic of Eberron page 46" name="Dragon Prophesier" desc="Like the dragons, you seek to untangle and perceive the record of everything that has been, and more important, what will be." />
+    <feat type="Magic of Eberron page 46" name="Dragon Totem Focus" desc="Your focus allows you to enjoy the benefit of a dragon totem ritual longer than normal." />
+    <feat type="Magic of Eberron page 47" name="Dragon Totem Lorekeeper" desc="You have been instructed in how to perform the rituals of dragon totem magic." />
+    <feat type="Magic of Eberron page 47" name="Dragon Totem Scion" desc="You are naturally attuned to the magic of the dragon totem ritual." />
+    <feat type="Magic of Eberron page 47" name="Eldeen Plantgrafter" desc="You can create and apply plant grafts onto others, granting the recipients of your grafts new, potent abilities." />
+    <feat type="Magic of Eberron page 47" name="Elemental Grafter" desc="You can create and apply elemental grafts onto others, granting the recipients of your grafts new, potent abilities." />
+    <feat type="Magic of Eberron page 47" name="Elemental Helmsman" desc="You are more capable of piloting an elemental vessel." />
+    <feat type="Magic of Eberron page 47" name="Elemental Smite" desc="You can channel the energy associated with one of your elemental grafts into your melee attacks." />
+    <feat type="Magic of Eberron page 47" name="Etch Schema" desc="You can create a minor schema." />
+    <feat type="Magic of Eberron page 48" name="Heroic Companion" desc="Your luck extends to your companion creature." />
+    <feat type="Magic of Eberron page 48" name="Heroic Focus" desc="Despite the dangers all around, you can quickly regain your psionic focus." />
+    <feat type="Magic of Eberron page 49" name="Improved Homunculus" desc="You are adept at improving and modifying your homunculus. Whenever you advance your homunculus's Hit Dice, you can also imbue it with special supernatural abilities." />
+    <feat type="Magic of Eberron page 50" name="Prophecy's Artifex" desc="Your perception of the draconic Prophecy gives you insights that allow you to transcend the normal limits of magic item use." />
+    <feat type="Magic of Eberron page 50" name="Prophecy's Explorer" desc="Your perception of the draconic Prophecy imbues you with a preternatural sense of your surroundings, enabling you to move easily and quickly through dangerous areas." />
+    <feat type="Magic of Eberron page 50" name="Prophecy's Hero" desc="Your perception of the draconic Prophecy charges you with the will to prevail, providing you with the opportunity to see a way to victory even when the odds are stacked against you." />
+    <feat type="Magic of Eberron page 50" name="Prophecy's Mind" desc="You meld your perception of the draconic Prophecy with a mental focus that provides you with momentary warning when death is at hand." />
+    <feat type="Magic of Eberron page 50" name="Prophecy's Shaper" desc="Your perception of the draconic Prophecy is such that you can disrupt reality and make your spells more powerful than reality would normally allow." />
+    <feat type="Magic of Eberron page 50" name="Prophecy's Shepherd" desc="Your perception of the draconic Prophecy is such that you can alter the natural flow of the world by connecting your knowledge of life-force with the world around you." />
+    <feat type="Magic of Eberron page 51" name="Prophecy's Slayer" desc="Your perception of the draconic Prophecy includes a keen appreciation of life. You recognize how fragile and tenuous life truly is when balanced against your lethal foreknowledge." />
+    <feat type="Magic of Eberron page 51" name="Psiforged Body" desc="As a warforged, your body can be crafted using trace amounts of psionically resonant deep crystal, providing you with increased psionic power and the ability to store psionic energy in your body. If you take this feat, you will often be referred to as a psiforged." />
+    <feat type="Magic of Eberron page 51" name="Psionic Luck" desc="Your psionic focus improves your luck." />
+    <feat type="Magic of Eberron page 51" name="Psychic Rush" desc="You can occasionally manifest a psionic power with less effort." />
+    <feat type="Magic of Eberron page 51" name="Quicken Dragonmark" desc="You can use your dragonmark abilities more quickly." />
+    <feat type="Magic of Eberron page 51" name="Rapid Infusion" desc="You can imbue an item with an infusion more quickly than normal." />
+    <feat type="Magic of Eberron page 51" name="Symbiont Mastery" desc="You have stronger control over an attached symbiont than regular creatures, and you gain vitality for each symbiont attached to you." />
+    <feat type="Magic of Eberron page 51" name="Wand Surge" desc="You can squeeze more magic out of charged items." />
+    <feat type="Magic of Incarnum page 34" name="Azure Enmity" desc="You can channel incarnum to enhance your ability to deal damage to your favored enemies." />
+    <feat type="Magic of Incarnum page 34" name="Azure Talent" desc="The soul energy of incarnum increases your mental capacity." />
+    <feat type="Magic of Incarnum page 34" name="Azure Touch" desc="You can channel incarnum to enhance your abilit to heal." />
+    <feat type="Magic of Incarnum page 35" name="Azure Toughness" desc="You can use incarnumto boost your physical vigor." />
+    <feat type="Magic of Incarnum page 35" name="Azure Turning" desc="You can blast ndead with incarnum-purified positive energy." />
+    <feat type="Magic of Incarnum page 35" name="Azure Wild Shape" desc="You can channel incarnum to enhance your combat prowess while wild shaped." />
+    <feat type="Magic of Incarnum page 35" name="Bonus Essentia" desc="You are better able to harness your personal store of incarnum." />
+    <feat type="Magic of Incarnum page 35" name="Cerulean Fortitude" desc="You can use incarnum to boost your ability to resist effects that would adversely affect your health." />
+    <feat type="Magic of Incarnum page 35" name="Cerulean Reflexes" desc="You can use incarnum to boost your ability to avoid harm." />
+    <feat type="Magic of Incarnum page 35" name="Cerulean Will" desc="You can use incarnum to boost your willpower." />
+    <feat type="Magic of Incarnum page 35" name="Cobalt Charge" desc="You can channel incarnum to deal devastating strikes when charging." />
+    <feat type="Magic of Incarnum page 35" name="Cobalt Critical" desc="You can focus your spirit into your melee weapon attacks, dealing more damage with successful critical strikes." />
+    <feat type="Magic of Incarnum page 35" name="Cobalt Expertise" desc="By channeling the soul energy of weapon masters past, present, and future, you become more adept at maneuvers of skill and expertise." />
+    <feat type="Magic of Incarnum page 37" name="Cobalt Power" desc="By channeling the soul energy of brutal warriors past, present, and future, you become more capable of overcoming your enemies through sheer strength." />
+    <feat type="Magic of Incarnum page 37" name="Cobalt Precision" desc="You can focus your soul energy into your ranged attacks, dealing more damage with successful critical hits." />
+    <feat type="Magic of Incarnum page 37" name="Cobalt Rage" desc="You can channel incarnum to enhance your rage. When you do so, your eyes turn deep blue in color." />
+    <feat type="Magic of Incarnum page 37" name="Divine Soultouch" desc="You can channel positive or negative energy to imbue yourself with incarnum." />
+    <feat type="Magic of Incarnum page 38" name="Double Chakra" desc="One of your chakras becomes capable of holding more incarnum than it is normally capable of containing." />
+    <feat type="Magic of Incarnum page 38" name="Expanded Soulmeld Capacity" desc="Your soul's tie to incarnum allows you to maintain more essentia in a single soulmeld." />
+    <feat type="Magic of Incarnum page 38" name="Healing Soul" desc="You can draw upon the soul energy of incarnum to heal your wounds." />
+    <feat type="Magic of Incarnum page 38" name="Heart of Incarnum" desc="You tap into the power of your heart chakra to gain resilience." />
+    <feat type="Magic of Incarnum page 38" name="Improved Essentia Capacity" desc="Your capability of investing essentia improves." />
+    <feat type="Magic of Incarnum page 38" name="Incarnum-Fortified Body" desc="The incarnum within you strengthens your body's toughness, enabling you to withstand greater injury." />
+    <feat type="Magic of Incarnum page 38" name="Incarnum Resistance" desc="Your body, untainted by incarnum, is not easily affected by the power of soul energy." />
+    <feat type="Magic of Incarnum page 38" name="Incarnum Spellshaping" desc="You gain the ability to invest incarnum into your spellcasting." />
+    <feat type="Magic of Incarnum page 38" name="Indigo Strike" desc="You can channel incarnum to enhance your ability to deal damage with your skirmish attack, sneak attack or sudden strike." />
+    <feat type="Magic of Incarnum page 38" name="Midnight Augmentation" desc="You can augment a psionic power with your personal soul energy rather than mental energy." />
+    <feat type="Magic of Incarnum page 39" name="Midnight Dodge" desc="You can channel incarnum to enhance your ability to avoid attacks against you." />
+    <feat type="Magic of Incarnum page 39" name="Midnight Metamagic" desc="You can channel incarnum to alter your prepared spells." />
+    <feat type="Magic of Incarnum page 39" name="Necrocarnum Acolyte" desc="You have experienced the power of necrocarnum, a dark and twisted form of incarnum." />
+    <feat type="Magic of Incarnum page 39" name="Open Greater Chakra" desc="You open up one of your body's centers of power, allowing you to bind a soulmeld or a magic item to that chakra." />
+    <feat type="Magic of Incarnum page 39" name="Open Least Chakra" desc="You open up one of your body's centers of power, allowing you to bind a soulmeld or a magic item to that chakra." />
+    <feat type="Magic of Incarnum page 40" name="Open Lesser Chakra" desc="You open up one of your body's centers of power, allowing you to bind a soulmeld or a magic item to that chakra." />
+    <feat type="Magic of Incarnum page 40" name="Psycarnum Blade" desc="You can forge your mind blade from a mixture of mental and soul energy, enabling you to deal devastating strikes with the weapon." />
+    <feat type="Magic of Incarnum page 40" name="Psycarnum Crystal" desc="Your psycrystal taps into the natural ebb and flow of incarnum, turning it into a small reservoir of soul energy." />
+    <feat type="Magic of Incarnum page 40" name="Psycarnum Infusion" desc="You transform your mental focus into a brief burst of soul energy." />
+    <feat type="Magic of Incarnum page 40" name="Sapphire Fist" desc="You can channel incarnum to enhance your ability to deliver stunning attacks." />
+    <feat type="Magic of Incarnum page 40" name="Sapphire Smite" desc="You can channel incarnum to enhance your ability to deal mighty blows." />
+    <feat type="Magic of Incarnum page 40" name="Sapphire Sprint" desc="Drawing on the soul energy of great runners of history, you infuse your body with incarnum to speed your steps." />
+    <feat type="Magic of Incarnum page 40" name="Shape Soulmeld" desc="You gain the ability to shape a single soulmeld." />
+    <feat type="Magic of Incarnum page 41" name="Share Soulmeld" desc="You can share a soulmeld with an ally with which you have a special bond." />
+    <feat type="Magic of Incarnum page 41" name="Soulsight" desc="You can attune your soul to sense living creatures near you." />
+    <feat type="Magic of Incarnum page 41" name="Soultouched Spellcasting" desc="By fusing your spells with incarnum, they become more capable of overcoming enemy magic and spell resistance." />
+    <feat type="Magic of Incarnum page 41" name="Split Chakra" desc="One of your chakras becomes capable of holding both a bound soulmeld and a magic item." />
+    <feat type="Magic of Incarnum page 41" name="Undead Meldshaper" desc="Despite having no soul of your own, you maintain the ability to channel incarnum through force of will alone." />
+    <feat type="Player's Guide to Eberron page 20" name="Aereni Focus" desc="From childhood you have studied one particular path, and these decades of devotion result in remarkable skill." />
+    <feat type="Player's Guide to Eberron page 20" name="Aerenal Arcanist" desc="Your family has studied wizardry for thousands of years." />
+    <feat type="Player's Guide to Eberron page 20" name="Aerenal Half-Life" desc="The Priests of Transition have guided you through strange rituals that left you poised between the world of the living and the dead." />
+    <feat type="Player's Guide to Eberron page 25" name="Perfect Reflection" desc="You are particularly skilled at mimicking the forms and mannerisms of others." />
+    <feat type="Player's Guide to Eberron page 35" name="Touch of Captivation" desc="You are sakah, and your fiendish gift allows you to captivate people around you." />
+    <feat type="Player's Guide to Eberron page 35" name="Touch of Deception" desc="You are sakah, and your fiend gift allows you to alter your appearance and trick others." />
+    <feat type="Player's Guide to Eberron page 35" name="Touch of Summoning" desc="You are sakah, and your fiendish gift allows you to summon fell creatures to do your bidding." />
+    <feat type="Player's Guide to Eberron page 36" name="Binding Brand" desc="You carry the brand of the binding flame, marking you as a warrior of the Ghaash'kala clans." />
+    <feat type="Player's Guide to Eberron page 48" name="Dragon's Insight" desc="You can call on the power of your dragonmark to enhance your natural abilities." />
+    <feat type="Player's Guide to Eberron page 48" name="Shield of Deneith" desc="You can channel the power of your Deneith dragonmark to defend yourself in battle." />
+    <feat type="Player's Guide to Eberron page 48" name="Swiftness of Orien" desc="You can draw on the power of your Mark of Passage to temporarily enhance your speed or the speed of your mount." />
+    <feat type="Player's Guide to Eberron page 49" name="Aberrant Dragonmark Gift" desc="Your aberrant dragonmark is especially potent." />
+    <feat type="Player's Guide to Eberron page 49" name="Aberrant Dragonmark Mystery" desc="You can use the power of your aberrant mark to enhance your magical abilities." />
+    <feat type="Player's Guide to Eberron page 49" name="Aberrant Dragonmark Vigor" desc="You can channel the energy of your aberrant mark to enhance your health." />
+    <feat type="Player's Guide to Eberron page 60" name="Ritual of Arcane Opposition" desc="You have been inured against the effects of arcane magic by a ritual of the Ashbound set." />
+    <feat type="Player's Guide to Eberron page 60" name="Ritual of Blight's Embrace" desc="You have been warded from the effects of poison and disease by a ritual of the Children of Winter, solidifying your bond with vermin." />
+    <feat type="Player's Guide to Eberron page 60" name="Ritual of the Timeless Soul" desc="You have been blessed by the faerie lords of Thelanis in a ritual of the Greensinger sect, and you temporarily slip from time's grasp." />
+    <feat type="Player's Guide to Eberron page 60" name="Ritual of the Woodland Bond" desc="You have formed a bond with the growth of the woods through a ritual of the Wardens of the Wood." />
+    <feat type="Player's Guide to Eberron page 75" name="Friends of the Tribes" desc="You are deeply familiar with the tribes of the Talenta Plains." />
+    <feat type="Player's Guide to Eberron page 75" name="Talenta Dinosaur Bond" desc="You have undergone grueling training on the dinosaur back and are skilled in the halfling techniques of fighting while mounted." />
+    <feat type="Player's Guide to Eberron page 75" name="Talenta Drifter" desc="Your extensive travels on the Talenta Plains give you an advantage while in that region." />
+    <feat type="Player's Guide to Eberron page 77" name="Galifaran Scholar" desc="You have made an exhaustive study of the history of Galifar, from the earliest roots of the Five Nations, through the formation of the united Kingdom of Galifar, and on to the Last War and the dissolution of the kingdom." />
+    <feat type="Player's Guide to Eberron page 83" name="Du'ulora Ancestor" desc="The tsucora are the most common of the quori, but they are not the only spirits in Dal Quor." />
+    <feat type="Player's Guide to Eberron page 83" name="Hashalaq Ancestor" desc="The hashalaq quori essence within you allows you to sense the emotions of others." />
+    <feat type="Player's Guide to Eberron page 86" name="Aberration Banemagic" desc="You can cast spells that do extra damage to aberrations." />
+    <feat type="Player's Guide to Eberron page 86" name="Indomitable Discipline" desc="Your strict mental discipline allows you to resist attempts to manipulate your thoughts." />
+    <feat type="Player's Guide to Eberron page 86" name="Unnatural Enemy" desc="You have been trained in the ways of aberrations, and you know how to recognize them and spot their weaknesses." />
+    <feat type="Player's Guide to Eberron page 109" name="Sudden Willow Strike" desc="Your monastic training allows great precision with your quarterstaff." />
+    <feat type="Player's Guide to Eberron page 119" name="Child of the Swamps" desc="You can find food and shelter in the deep swamps, and you can move more freely through the difficult terrain." />
+    <feat type="Player's Guide to Eberron page 122" name="Battlebred" desc="Due to traumatic experiences in past battles, the plane of Shavarath with its endless war seems never far from you." />
+    <feat type="Player's Guide to Eberron page 122" name="Chosen of the Deathless" desc="You carry with you an intimate familiarity with the positive energy that suffuses the City of the Dead." />
+    <feat type="Player's Guide to Eberron page 122" name="Manifest Druid" desc="You have a familiarity with the three manifest zones of the Eldeen Reaches and the powers of the planes to which they are linked." />
+    <feat type="Player's Guide to Eberron page 125" name="Mastery of the Azure Sky" desc="You have learned to calculate the precise location of Syrania at any given time, and to use that knowledge to enhance spells you cast to grant flight." />
+    <feat type="Player's Guide to Eberron page 125" name="Mastery of the Battleground" desc="You have learned to calculate the precise location of Shavarath at any given time, and to use that knowledge to enhance spells of battle that you cast." />
+    <feat type="Player's Guide to Eberron page 125" name="Mastery of Chaos and Order" desc="You have learned to calculate the precise locations of Daanvi and Kythri at any given time, and to use that knowledge to imbue your spells with unusual regularity or strinking unpredictability -- or both." />
+    <feat type="Player's Guide to Eberron page 125" name="Mastery of Day and Night" desc="You have learned to calculate the precise locations of Irian and Mabar at any given time, and to use that knowledge to enhance your manipulation of positive and negative energy." />
+    <feat type="Player's Guide to Eberron page 125" name="Mastery of the Dead" desc="You have learned to calculate the precise location of Dolurrh at any given time, and to use that knowledge to capture the souls of creatures slain with your death spells." />
+    <feat type="Player's Guide to Eberron page 125" name="Mastery of Dreams" desc="By physically exploring the realm of Dal Quor, you have learned to instill your spells with the stuff of dreams . . . and nightmares." />
+    <feat type="Player's Guide to Eberron page 125" name="Mastery of Faerie Enchantment" desc="You have learned to calculate the precise location of Thelanis at any given time, and to use that knowledge to improve your ability to control the minds of other creatures." />
+    <feat type="Player's Guide to Eberron page 126" name="Mastery of Ice and Fire" desc="You have learned to caclulate the precies locations of Fernia and Risia at any given time, and to use that knowledge to enhance cold and fire spells that you use." />
+    <feat type="Player's Guide to Eberron page 126" name="Mastery of Madness" desc="You have learned to reach magically to the ever-distant plane of Xoriat and draw some element of its madness into the world -- but these techniques come with some risk." />
+    <feat type="Player's Guide to Eberron page 126" name="Mastery of the Mists" desc="By learning of the intricate relationship between the Ethereal Plane and the Material Plane, you gain the ability to see and sometimes reach through the barrier between these two planes." />
+    <feat type="Player's Guide to Eberron page 126" name="Mastery of the Silver Void" desc="You have gained a deeper understanding of the Astral Plane and its relationship to the other planes of the cosmos. You can use that knowledge to more quickly access that plane." />
+    <feat type="Player's Guide to Eberron page 126" name="Mastery of Twilight Denizens" desc="You have learned to calculate the precise location of Lamannia at any given time, and to use that knowledge to summon more powerful creatures from that plane." />
+    <feat type="Player's Guide to Eberron page 126" name="Mastery of Twisted Shadow" desc="You gain the ability to reach into the Plane of Shadow when casting an illusion, concealing yourself in the raw shadowstuff drawn forth." />
+    <feat type="Player's Guide to Eberron page 135" name="Shifter Acrobatics" desc="Your heritage makes you agile and light-footed." />
+    <feat type="Player's Guide to Eberron page 135" name="Shifter Magnetism" desc="Your heritage gives you a strong animal presence." />
+    <feat type="Player's Guide to Eberron page 135" name="Shifter Stealth" desc="You can call upon your bestial heritage to increase your stealth." />
+    <feat type="Player's Guide to Eberron page 141" name="Bladebearer of the Valenar" desc="You have trained extensively with scimitars, including the Valenar double scimitar. You are adept at striking from horseback with the curved blades of the Valenar." />
+    <feat type="Player's Guide to Eberron page 141" name="Shield of Blades" desc="As a master of the double scimitar, you can weave a web of steel to protect yourself from attack." />
+    <feat type="Player's Guide to Eberron page 141" name="Spirit of the Stallion" desc="Your patron ancestor was a legendary cavalry soldier, and her spirit guides you and your mount." />
+    <feat type="Player's Guide to Eberron page 141" name="Valenar Trample" desc="You are trained in Valenar cavalry techniques emphasizing trampling your opponents into the ground." />
+    <feat type="Player's Guide to Eberron page 151" name="Shocking Fist" desc="Your slam attack can deal a shock." />
+    <feat type="Player's Guide to Eberron page 151" name="Overload Metabolism" desc="You can heal damage at a cost to your other physical attributes." />
+    <feat type="Power of Faerun page 46" name="Heretic of the Faith" desc="You stray significantly from the teachings of your faith." />
+    <feat type="Power of Faerun page 49" name="Prophet of the Divine" desc="Your communications with the divine manifest in a public fashion." />
+    <feat type="Power of Faerun page 53" name="Bane of Infidels" desc="In a church locked in eternal conflict with followers of another faith, you have learned to fight effectively against the infidels. You know their ways and how to beat them." />
+    <feat type="Power of Faerun page 58" name="Initiate of Amaunator" desc="You have been initiated into the greatest secrets of Amaunator's faith." />
+    <feat type="Power of Faerun page 158" name="Rulership" desc="You are a ruler of an economic, frontier, governmental, military, religious, transport, or other community." />
+    <feat type="Races of the Dragon page 98" name="Accelerate Metamagic" desc="You can apply a selected metamagic feat to your spells more quickly than normal." />
+    <feat type="Races of the Dragon page 98" name="Dragon Breath" desc="You can use your breath weapon as often as a normal dragon." />
+    <feat type="Races of the Dragon page 98" name="Dragon Tail" desc="Your draconic ancestry manifests as a muscular tail you can use in combat." />
+    <feat type="Races of the Dragon page 98" name="Dragon Trainer" desc="Your draconic nature gives you special insight into training dragons and draconic creatures." />
+    <feat type="Races of the Dragon page 100" name="Dragon Wings" desc="Your draconic ancestry manifests as a pair of wings that aid your jumps and allow you to glide." />
+    <feat type="Races of the Dragon page 100" name="Dragonwrought" desc="You were born a dragonwrought kobold, proof of your race's innate connection to dragons." />
+    <feat type="Races of the Dragon page 100" name="Extraordinary Trapsmith" desc="You are an expert at constructing mechanical traps." />
+    <feat type="Races of the Dragon page 100" name="Heavyweight Wings" desc="Your superior strength allows you to fly while heavily burdened." />
+    <feat type="Races of the Dragon page 100" name="Improved Dragon Wings" desc="Your draconic wings now grant you flight." />
+    <feat type="Races of the Dragon page 101" name="Kobold Endurance" desc="Thanks to your race's determination, you are capable of amazing feats of strength and stamina." />
+    <feat type="Races of the Dragon page 101" name="Kobold Foe Strike" desc="You are more effective in combat against your racial enemies." />
+    <feat type="Races of the Dragon page 101" name="Practical Metamagic" desc="You can apply a selected metamagic feat to your spells more easily." />
+    <feat type="Races of the Dragon page 101" name="Reinforced Wings" desc="You have strengthened the muscles of your wings." />
+    <feat type="Races of the Dragon page 101" name="Versatile Spellcaster" desc="You can use two lower-level spell slots to cast a spell one level higher." />
+    <feat type="Races of the Dragon page 101" name="Wyrmgrafter" desc="You can apply draconic grafts to other living creatures or to yourself." />
+    <feat type="Races of the Dragon page 101" name="Entangling Exhalation" desc="You can use your breath weapon to create an entangling mesh of energy." />
+    <feat type="Races of the Dragon page 101" name="Exhaled Barrier" desc="You can use your breath weapon to create a wall of energy." />
+    <feat type="Races of the Dragon page 102" name="Exhaled Immunity" desc="You can use your breath weapon to grant a willing creature immunity to energy." />
+    <feat type="Races of the Dragon page 102" name="Extra Exhalation" desc="You can use your breath weapon one more time per day than normal." />
+    <feat type="Races of the Dragon page 102" name="Furious Inhalation" desc="While raging, you can use your breath weapon to deal energy damage with your bite attacks." />
+    <feat type="Races of the Dragon page 102" name="Draconic Arcane Grace" desc="You can convert some of your arcane spell energy into a saving throw bonus." />
+    <feat type="Races of the Dragon page 102" name="Draconic Breath" desc="You can convert some of your arcane spell energy into a breath weapon." />
+    <feat type="Races of the Dragon page 102" name="Draconic Claw" desc="You develop natural weapons like those of your draconic ancestors." />
+    <feat type="Races of the Dragon page 102" name="Draconic Flight" desc="The secret of draconic flight has been revealed to you, granting you the ability to fly occasionally." />
+    <feat type="Races of the Dragon page 102" name="Draconic Heritage" desc="You have a greater connection with your draconic bloodline than others of your kind." />
+    <feat type="Races of the Dragon page 104" name="Draconic Legacy" desc="You have realized greater arcane power through your draconic heritage." />
+    <feat type="Races of the Dragon page 104" name="Draconic Persuasion" desc="Your arcane talents lend you a great deal of allure." />
+    <feat type="Races of the Dragon page 104" name="Draconic Power" desc="You have greater power when manipulating the energies of your heritage." />
+    <feat type="Races of the Dragon page 104" name="Draconic Presence" desc="When you use your magic, your mere presence can terrify those around you." />
+    <feat type="Races of the Dragon page 105" name="Draconic Resistance" desc="Your bloodline hardens your body against effects related to the nature of your progenitor." />
+    <feat type="Races of the Dragon page 105" name="Draconic Skin" desc="Your skin takes on a sheen, luster, and hardness related to your draconic ancestor." />
+    <feat type="Races of the Dragon page 105" name="Draconic Toughness" desc="Your draconic nature reinforces your body as you embrace your heritage." />
+    <feat type="Races of the Dragon page 105" name="Spell Rehearsal" desc="Casting the same spell several times in a row or at the same target enables you to perfect it." />
+    <feat type="Races of the Dragon page 105" name="Wing Expert" desc="You can use your wings to create a variety of effects." />
+    <feat type="Red Hand of Doom page 126" name="Divine Vigor" desc="You can channel energy to increase your speed and durability." />
+    <feat type="Red Hand of Doom page 126" name="Dragonthrall" desc="You have pledged your life to the service of evil dragonkind." />
+    <feat type="Tome of Magic page 72" name="Bind Vestige" desc="You know how to make pacts with otherworldly spirits called vestiges." />
+    <feat type="Tome of Magic page 73" name="Bind Vestige, Improved" desc="You can bind a wider range of vestiges." />
+    <feat type="Tome of Magic page 73" name="Defense against the Supernatural" desc="Your in-depth knowledge of supernatural forces grants you greater ability to resist their effects." />
+    <feat type="Tome of Magic page 73" name="Empower Supernatural Ability" desc="You can use a supernatural ability with greater effect than normal." />
+    <feat type="Tome of Magic page 73" name="Enlarge Supenatural Ability" desc="You can increase the range of a supernatural attack." />
+    <feat type="Tome of Magic page 73" name="Expel Vestige" desc="You can expel a vestige to which you are bound before the duration of its pact with you has expired." />
+    <feat type="Tome of Magic page 73" name="Extend Supernatural Ability" desc="You can cause a supernatural ability with a duration to last longer than normal." />
+    <feat type="Tome of Magic page 74" name="Favored Vestige" desc="Choose one vestige to which you have access. You establish a close, mystical affinity with that spirit." />
+    <feat type="Tome of Magic page 74" name="Favored Vestige Focus" desc="The supernatural abilities of your favored vestige are more potent than normal." />
+    <feat type="Tome of Magic page 74" name="Ignore Special Requirements" desc="The strange constraints that vestiges place on their summoning are meaningless to you." />
+    <feat type="Tome of Magic page 74" name="Improved Binding" desc="You are so adept at binding vestiges that you can contact powerful ones more easily than other soul binders can." />
+    <feat type="Tome of Magic page 74" name="Practiced Binder" desc="When you bind a vestige, you gain an additional power associated with it." />
+    <feat type="Tome of Magic page 74" name="Rapid Pact Making" desc="Your skill with pact magic lets you bind a vestige extremely quickly, even in the heat of combat." />
+    <feat type="Tome of Magic page 74" name="Rapid Recovery" desc="You can use the abilities of your favored vestige more frequently." />
+    <feat type="Tome of Magic page 74" name="Skilled Pact Making" desc="Your strong will serves you well when making pacts with vestiges." />
+    <feat type="Tome of Magic page 74" name="Sudden Ability Focus" desc="One of your special attacks becomes more potent than usual." />
+    <feat type="Tome of Magic page 75" name="Supernatural Crusader" desc="You are adept at fighting supernatural creatures." />
+    <feat type="Tome of Magic page 75" name="Supernatural Opportunist" desc="You are adept at exploiting a creature's momentary distraction while it activates its supernatural abilities." />
+    <feat type="Tome of Magic page 75" name="Widen Supernatural Ability" desc="You can increase the area of your supernatural abilities." />
+    <feat type="Tome of Magic page 136" name="Empower Mystery" desc="You can cast mysteries to greater effect." />
+    <feat type="Tome of Magic page 136" name="Enlarge Mystery" desc="You can cast mysteries farther than normal." />
+    <feat type="Tome of Magic page 136" name="Extend Mystery" desc="You can cast mysteries that last longer than normal." />
+    <feat type="Tome of Magic page 136" name="Favored Mystery" desc="The mystery you choose becomes easier to cast." />
+    <feat type="Tome of Magic page 136" name="Greater Path Focus" desc="Choose a path of shadow magic to which you have already applied the Path Focus feat. Your mysteries of that path are now even more potent." />
+    <feat type="Tome of Magic page 136" name="Line of Shadow" desc="You can cast a mystery without line of sight or line of effect to the target." />
+    <feat type="Tome of Magic page 136" name="Maximize Mystery" desc="You can cast mysteries to maximum effect." />
+    <feat type="Tome of Magic page 137" name="Nocturnal Caster" desc="You are empowered by darkness, making your abilities stronger at night." />
+    <feat type="Tome of Magic page 137" name="Path Focus" desc="Choose a path of shadow magic, such as Touch of Twilight. Your mysteries of that path are more potent than normal." />
+    <feat type="Tome of Magic page 137" name="Quicken Mystery" desc="You can cast a mystery with a moment's thought." />
+    <feat type="Tome of Magic page 137" name="Reach Mystery" desc="You can cast touch-range mysteries without touching the target." />
+    <feat type="Tome of Magic page 137" name="Shadow Cast" desc="Your shadow shimmers as you cast a spell and you seem to cast your mysteries from elsewhere." />
+    <feat type="Tome of Magic page 138" name="Shadow Familiar" desc="Noctumancers developed this feat in order to gain a mystical companion." />
+    <feat type="Tome of Magic page 138" name="Shadow Reflection" desc="Your shadow flickers and moves in an aggressive, independent manner, enabling you to avoid some attacks of opportunity." />
+    <feat type="Tome of Magic page 138" name="Shadow Vision" desc="Your senses grow so attuned with shadow that you gain a limited ability to see in natural and magical darkness." />
+    <feat type="Tome of Magic page 138" name="Still Mystery" desc="You can cast mysteries without gestures." />
+    <feat type="Tome of Magic page 138" name="Unseen Arrow" desc="Developed by shadowblades, this feat allows a member of that class to apply his unseen weapon abilities to thrown or projectile weapons." />
+    <feat type="Tome of Magic page 228" name="Empower Utterance" desc="Your utterances have more powerful effects." />
+    <feat type="Tome of Magic page 229" name="Enlarge Utterance" desc="You can project the power of an utterance to a greater distance." />
+    <feat type="Tome of Magic page 229" name="Extend Utterance" desc="Your utterances have a more lasting effect on the universe." />
+    <feat type="Tome of Magic page 229" name="Focused Lexicon" desc="Your utterances have greater effect against a certain type of creature." />
+    <feat type="Tome of Magic page 229" name="Minor Utterance of the Evolving Mind" desc="Your mastery of Truespeech has led you to the understanding necessary to perform a simple utterance from the Lexicon of the Evolving Mind." />
+    <feat type="Tome of Magic page 229" name="Obscure Personal Truename" desc="Truenames are notoriously difficult to pronounce, but yours is harder than most." />
+    <feat type="Tome of Magic page 229" name="Personal Truename Backlash" desc="Your personal truename is so charged with magic power that those who fail to speak it properly are warped by reality run amok." />
+    <feat type="Tome of Magic page 229" name="Truename Rebuttal" desc="You are particularly good at negating other truenamers' power with well-chosen truenames." />
+    <feat type="Tome of Magic page 229" name="Truename Research" desc="You have a knack for uncovering the personal truenames of friends and foes alike through study and investigation." />
+    <feat type="Tome of Magic page 229" name="Truename Training" desc="Unlike most of your peers, you have discovered the secret power of truenames." />
+    <feat type="Tome of Magic page 230" name="Utterance of the Evolving Mind" desc="Your further mastery of Truespeech allows you to wield its power more effectively against creatures." />
+    <feat type="Tome of Magic page 230" name="Utterance of the Crafted Tool" desc="As you strive for ever more mastery of Truespeech, you gain more power over the universe around you. You can now use the power of Truespeech to affect objects." />
+    <feat type="Tome of Magic page 230" name="Utterance of the Perfected Map" desc="The power of the Truespeech can alter the state of reality itself. Reaching toward this great power, you have mastered an utterance from the Lexicon of the Perfected Map." />
+    <feat type="Tome of Magic page 230" name="Utterance Focus" desc="You have a particular utterance you favor above others, and your enemies are less able to resist the power of your words." />
+    <feat type="Tome of Magic page 231" name="Quicken Utterance" desc="You can speak an utterance with just a moment's thought." />
+    <feat type="Tome of Magic page 231" name="Recitation of the Fortified State" desc="This recitation allows you to stand unyielding against the blows of your enemies." />
+    <feat type="Tome of Magic page 232" name="Recitation of the Meditative State" desc="This recitation gives you an unparalleled sense of serene calm." />
+    <feat type="Tome of Magic page 232" name="Recitation of Mindful State" desc="This recitation narrows and focuses your perception so you can concentrate on a delicate task at hand." />
+    <feat type="Tome of Magic page 232" name="Recitation of the Sanguine State" desc="This recitation purges all poisons from your body." />
+    <feat type="Tome of Magic page 232" name="Recitation of Vital State" desc="This recitation frees your body of disease and sickness." />
+    <feat type="Complete Psionic page 49" name="Dazzling Energy" desc="Your facility with energy is such that enemies are shaken by your prowess." />
+    <feat type="Complete Psionic page 49" name="Deep Vision" desc="Your mental focus helps you see farther with darkvision" />
+    <feat type="Complete Psionic page 49" name="Dire Flail Mind Blade" desc="When you reshape your mind blade, you can change it into an exotic weapon: a dire flail." />
+    <feat type="Complete Psionic page 49" name="Dire Stun" desc="When you choose to stun your foe with your lurk augment ability, your foe might be stunned for a long time." />
+    <feat type="Complete Psionic page 49" name="Don  Mantle" desc="You gain the granted ability of a mantle you have tapped." />
+    <feat type="Complete Psionic page 49" name="Dromite Barrier" desc="You can convert uses of yourenergy ray psi-like ability into walls of energy." />
+    <feat type="Complete Psionic page 49" name="Dromite Ray" desc="You an use yourenergy ray psi-like ability more often." />
+    <feat type="Complete Psionic page 49" name="Duergar Expansion" desc="You can use yourexpansion psi-like ability more often." />
+    <feat type="Complete Psionic page 49" name="Duergar Invisibility" desc="You can use yourinvisibility psi-like ability more often." />
+    <feat type="Complete Psionic page 49" name="Dwarven Urgrosh Mind Blade" desc="When you reshape your mind blade, you can change it into an exotic weapon: a dwarven urgrosh." />
+    <feat type="Complete Psionic page 50" name="Ectopic Form" desc="This feat allows you to create astral constructs with distinct appearances and specialties." />
+    <feat type="Complete Psionic page 52" name="Elan Repletion" desc="As an elan, you can sustain yourself with repletion longer than other members of your race." />
+    <feat type="Complete Psionic page 52" name="Elan Resilience" desc="As an elan, you can prevent greater amounts of damage than other members of your race." />
+    <feat type="Complete Psionic page 52" name="Elan Resistance, Enhanced" desc="As an elan, you can resist harmful effects more readily than other members of your race." />
+    <feat type="Complete Psionic page 52" name="Elan Retainment" desc="You can use your psionic metabolism to aid your ability to retain your psionic focus when you would otherwise expend it." />
+    <feat type="Complete Psionic page 52" name="Elemental Envoy" desc="This feat allows you to acquire an elemental steward." />
+    <feat type="Complete Psionic page 53" name="Energize Armor" desc="You can charge your armor with psionic energy, making it resistant to energy damage." />
+    <feat type="Complete Psionic page 53" name="Enervation Endurance" desc="When facing the aftermath of a wild surge, enervation doesn't sap your power points." />
+    <feat type="Complete Psionic page 53" name="Enhanced Beneficence" desc="Your psychic aura is larger than normal, reflecting your devotion to your deity." />
+    <feat type="Complete Psionic page 53" name="Envoy Cognizance" desc="When your elemental envoy is nearby, its associated energy enhances your ability to manifest energy powers." />
+    <feat type="Complete Psionic page 53" name="Euphoric Reduction" desc="Channel your euphoric surge into a boost for one of your skills." />
+    <feat type="Complete Psionic page 54" name="Extra Aura" desc="You gain the aura ability of a mantle you have donned." />
+    <feat type="Complete Psionic page 54" name="Focused Perception" desc="When you concentrate your faculties, your power of sight pierces the darkness." />
+    <feat type="Complete Psionic page 54" name="Focused Shield" desc="Your mental focus makes you more adept at using your shield." />
+    <feat type="Complete Psionic page 54" name="Focused Skill User" desc="You can take advantage of your psionic focus in new ways." />
+    <feat type="Complete Psionic page 54" name="Gestalt Anchor" desc="You have a strong bond to the psionic entity you host." />
+    <feat type="Complete Psionic page 54" name="Githyanki Charm" desc="You can leverage yourpsionic daze psi-like ability to gain greater control over subjects." />
+    <feat type="Complete Psionic page 54" name="Githyanki Control" desc="You can leverage yourfar hand psi-like ability to gain greater control over objects." />
+    <feat type="Complete Psionic page 54" name="Githyanki Dismissal" desc="You can leverage yourdimension door psi-like ability to gain greater control over other creatures' locations." />
+    <feat type="Complete Psionic page 54" name="Githyanki Ectoform" desc="You can leverage yourconcealing amorpha psi-like ability to gain greater control over your own body." />
+    <feat type="Complete Psionic page 54" name="Githzerai Burst" desc="You can leverage yourcat fall psi-like ability to gain greater control over yourself in your environment." />
+    <feat type="Complete Psionic page 54" name="Githzerai Feedback" desc="You can leverage yourinertial armor psi-like ability to further insulate yourself from harm." />
+    <feat type="Complete Psionic page 54" name="Githzerai Knock" desc="You can leverage yourconcussion blast psi-like ability to gain such fine control over manipulating force that you can open locks or sealed doors." />
+    <feat type="Complete Psionic page 55" name="Githzerai Link" desc="You can leverage yourpsionic daze psi-like ability to forge direct mental contact with another creature." />
+    <feat type="Complete Psionic page 55" name="Half-Giant Stomp" desc="You can use yourstomp psi-like ability more often." />
+    <feat type="Complete Psionic page 55" name="Half-Giant Thunderer" desc="You can use yourstomp psi-like ability to far greater effect." />
+    <feat type="Complete Psionic page 55" name="Instinctive Consummator" desc="You always make good on your threats." />
+    <feat type="Complete Psionic page 55" name="Invest Armor" desc="You can charge your armor with additional protective qualities." />
+    <feat type="Complete Psionic page 55" name="Lurk Augment, Extra" desc="You can use your lurk augment more often than normal." />
+    <feat type="Complete Psionic page 55" name="Lurk Augment, Ranged" desc="You can use some of your lurk augments in conjunction with a ranged attack." />
+    <feat type="Complete Psionic page 55" name="Lurk Master" desc="You are more skilled in augmenting your attack than your training would indicate." />
+    <feat type="Complete Psionic page 55" name="Maenad Fury" desc="You can use your outburst racial trait more often." />
+    <feat type="Complete Psionic page 55" name="Maenad Scream" desc="You can use yourenergy ray (sonic) psi-like ability more often." />
+    <feat type="Complete Psionic page 55" name="Maenad Deafening Scream" desc="You can use yourenergy ray (sonic) psi-like ability to better effect." />
+    <feat type="Complete Psionic page 55" name="Mantle Focus" desc="The powers from one of your mantles become more potent." />
+    <feat type="Complete Psionic page 56" name="Mental Juggernaut" desc="You are adroit at avoiding the mind blasting effects of certain psionic abilities and powers." />
+    <feat type="Complete Psionic page 56" name="Mind Cleave" desc="When you lay low a foe, you drain off a portion of its excess mental energy into the conduit of your mind blade." />
+    <feat type="Complete Psionic page 56" name="Mind Empowerment" desc="When you lay low a foe, you drain off a portion of its excess mental energy into the conduit of your mind blade." />
+    <feat type="Complete Psionic page 56" name="Mind Strike" desc="When you use your psychic strike ability, you deal more damage." />
+    <feat type="Complete Psionic page 57" name="Mind Strike, Swift" desc="You possess a deadly speed when charging your mind blade with psychic energy." />
+    <feat type="Complete Psionic page 57" name="Orc Double Axe Mind Blade" desc="When you reshape your mind blade, you can change it into an exotic weapon: an orc double axe." />
+    <feat type="Complete Psionic page 57" name="Postpone Enervation" desc="You can postpone the onset of your psychic enervation." />
+    <feat type="Complete Psionic page 57" name="Practiced Manifester" desc="Choose a manifesting class that you possess. The powers you manifest from that class are more powerful." />
+    <feat type="Complete Psionic page 57" name="Privileged Energy" desc="You favor one specific energy type over all others." />
+    <feat type="Complete Psionic page 57" name="Psymbiot" desc="You gain benefits when you are near other psionic characters or creatures." />
+    <feat type="Complete Psionic page 57" name="Skin of the Construct" desc="You can wear an astral construct as if it were a second skin." />
+    <feat type="Complete Psionic page 57" name="Stygian Archon" desc="You sear the synapses of your mind with a scar of void and emptiness." />
+    <feat type="Complete Psionic page 58" name="Synad Multitask, Enhanced" desc="As a synad, your threefold mind grants you an additional opportunity to multitask." />
+    <feat type="Complete Psionic page 58" name="Tap Mantle" desc="You gain the ability to access the powers in a new mantle." />
+    <feat type="Complete Psionic page 58" name="Thri-Kreen Carapace" desc="Your carapace is harder than average." />
+    <feat type="Complete Psionic page 58" name="Thri-Kreen Claw" desc="You can use yourmetaphysical claw psi-like ability more often." />
+    <feat type="Complete Psionic page 58" name="Thri-Kreen Displacement" desc="You can use yourpsionic displacement psi-like ability more often." />
+    <feat type="Complete Psionic page 58" name="Thri-Kreen Poison" desc="You can use your poison bite more often." />
+    <feat type="Complete Psionic page 58" name="Two-Bladed Mind Blade" desc="When you reshape your mind blade, you can change it into an exotic weapon: a two-bladed sword." />
+    <feat type="Complete Psionic page 58" name="Volatile Escalation" desc="When you are attacked with a telepathic power, your innate wildness forces a higher mental price on your attacker." />
+    <feat type="Complete Psionic page 58" name="Volatile Leech" desc="You gain the power points your attacker wastes attacking you with a telepathic power." />
+    <feat type="Complete Psionic page 59" name="Xeph Burst, Extra" desc="You can use your burst racial trait more often." />
+    <feat type="Complete Psionic page 59" name="Xeph Celerity" desc="You can use your burst racial trait to gain an extra attack." />
+    <feat type="Complete Psionic page 59" name="Dorje Mastery" desc="Psionic dorjes are more potent in your hands." />
+    <feat type="Complete Psionic page 59" name="Dual Dorje" desc="You can fight with two dorjes at the same time." />
+    <feat type="Complete Psionic page 59" name="Hostile Mind, Improved" desc="You have mental defenses erected  against telepathic attacks." />
+    <feat type="Complete Psionic page 59" name="Psionic Mastery" desc="You are quick and certain in your efforts to defeat the psionic defenses and powers of others." />
+    <feat type="Complete Psionic page 60" name="Aggressive Mind" desc="The psionic entity you host gives you access to psi-like abilities capable of disrupting the mind of your enemy." />
+    <feat type="Complete Psionic page 60" name="Antagonist" desc="The psionic entity you host seeks to cause damage and mayhem, and you have powers to further that end." />
+    <feat type="Complete Psionic page 60" name="Defensive Shell" desc="The psionic entity living in your mind enables you to better resist attacks." />
+    <feat type="Complete Psionic page 60" name="Host Focus" desc="You can use a psi-like ability granted by a host feat an extra time each day." />
+    <feat type="Complete Psionic page 60" name="Pacifist" desc="You host a psionic entity that dislikes combat and provides you psi-like abilities to help you avoid a fight." />
+    <feat type="Complete Psionic page 60" name="Spiritual Force" desc="Your mind blade is an expression of your inner spirit." />
+    <feat type="Complete Psionic page 60" name="Strength of Two" desc="As the host of a formless psionic entity, you possess immense willpower." />
+    <feat type="Complete Psionic page 60" name="Telepathic Affinity" desc="The entity you host gives you the ability to better communicate with other creatures." />
+    <feat type="Complete Psionic page 61" name="Illithid Blast" desc="You can convert your pisonic energy into amind blast." />
+    <feat type="Complete Psionic page 61" name="Illithid Compulsion" desc="You can call upon your heritage and enhance your ability to manipulate the minds of other creatures." />
+    <feat type="Complete Psionic page 61" name="Illithid Enthusiast" desc="When you manipulate the minds of other creatures, you are heartened and emboldened by your success." />
+    <feat type="Complete Psionic page 61" name="Illithid Extraction" desc="Your acceptance of your illithid heritage is so encompassing that you have learned how to extract the brain of a helpless victim." />
+    <feat type="Complete Psionic page 61" name="Illithid Grapple" desc="You embrace more of your illithid heritage, and grow at least one long purplish tentacle that you can reveal and unfurl when you open your mouth." />
+    <feat type="Complete Psionic page 62" name="Illithid Heritage" desc="Somewhere in the deeps of time, your bloodline was polluted with illithid influence." />
+    <feat type="Complete Psionic page 62" name="Illithid Legacy" desc="You have realized greater psionic power through your illithid heritage." />
+    <feat type="Complete Psionic page 62" name="Illithid Legacy, Greater" desc="Your knowledge of psionic power has grown even further due to your illithid heritage." />
+    <feat type="Complete Psionic page 62" name="Illithid Skin" desc="Your skin takes on the glistening, rubbery, green-mauve consistency of your illithid parentage." />
+    <feat type="Complete Psionic page 62" name="Knockdown Power" desc="You can manifest powers that knock creatures off their feet." />
+    <feat type="Complete Psionic page 62" name="Linked Power" desc="You can link a power to the power you manifest in this round so that it goes off next round." />
+    <feat type="Complete Psionic page 63" name="Metapower" desc="You can permanently modify a psionic power you know with a metapsionic feat." />
+    <feat type="Complete Psionic page 63" name="Paraelemental Power" desc="When using a power that allows you to choose a type of energy, you have a wider range of possible choices owing to your ability to mix energy with matter." />
+    <feat type="Complete Psionic page 63" name="Phrenic Leech" desc="Psionic foes damaged by your power are also mentally drained." />
+    <feat type="Complete Psionic page 64" name="Stygian Power" desc="Psionic powers you manifest that utilize negative energy are branded with an imprint of fear." />
+    <feat type="Complete Psionic page 64" name="Transdimensional Power" desc="You can manifest powers that affect targets lurking in coexistent planes and extradimensional spaces whose entrances fall within the power's area." />
+    <feat type="Player's Handbook II page 71" name="Acrobatic Strike" desc="Your dexterous maneuvers and skilled acrobatics allow you to slip past a foe's defenses and deliver an accurate strike against him." />
+    <feat type="Player's Handbook II page 71" name="Active Shield Defense" desc="Your expert use of your shield allows you to strike at vulnerable foes even when you forgo your own attacks in favor of defense." />
+    <feat type="Player's Handbook II page 71" name="Adaptable Flanker" desc="When you and an ally team up against a foe, you know how to maximize the threat your ally poses to ruin your target's defenses." />
+    <feat type="Player's Handbook II page 74" name="Agile Shield Fighter" desc="You are skilled in combining your shield bash attack with an armed strike. When you use your shield in unison with a weapon, your training allows you to score telling blows with both." />
+    <feat type="Player's Handbook II page 74" name="Arcane Accompaniment" desc="You infuse your performance with magical energy, allowing its effects to continue even as you attend to other tasks." />
+    <feat type="Player's Handbook II page 74" name="Arcane Consumption" desc="You can sacrifice your physical health to strengthen a spell. This process leaves you wracked with pain, but the enhanced energy you draw from the spell might provide the margin between victory and defeat." />
+    <feat type="Player's Handbook II page 74" name="Arcane Flourish" desc="You use your magical abilities to improve your performance talents." />
+    <feat type="Player's Handbook II page 74" name="Arcane Thesis" desc="You have studied a single spell in-depth." />
+    <feat type="Player's Handbook II page 75" name="Arcane Toughness" desc="You draw upon the power of your magic to sustain yourself, allowing you to continue fighting long after your physical body has failed you." />
+    <feat type="Player's Handbook II page 75" name="Armor Specialization" desc="Through long wear and hours of combat, you have trained your body to believe in its armor." />
+    <feat type="Player's Handbook II page 75" name="Battle Dancer" desc="You strike at your foes in time with the music you sing or in cadence with an oration you deliver." />
+    <feat type="Player's Handbook II page 75" name="Bonded Familiar" desc="You enjoy a stronger than normal magical bond with your familiar, granting you access to two special abilities." />
+    <feat type="Player's Handbook II page 75" name="Bounding Assault" desc="You can move and attack with superior speed and power." />
+    <feat type="Player's Handbook II page 76" name="Brutal Strike" desc="You can batter foes senseless with your mace, morningstar, quarterstaff, or flail." />
+    <feat type="Player's Handbook II page 76" name="Combat Acrobat" desc="Your acrobatics and agility in combat allow you to maneuver across the battlefield with ease." />
+    <feat type="Player's Handbook II page 76" name="Combat Familiar" desc="Your familiar is skilled in delivering attack spells againstyour foes." />
+    <feat type="Player's Handbook II page 77" name="Combat Tactician" desc="You excel at approaching an opponent from an unexpected direction to deliver deadly attacks." />
+    <feat type="Player's Handbook II page 77" name="Cometary Collision" desc="You are a thunderbolt of destruction on the battlefield." />
+    <feat type="Player's Handbook II page 77" name="Companion Spellbond" desc="You form a special magical link with your animal companion, allowing you to share spells with it over a greater distance." />
+    <feat type="Player's Handbook II page 77" name="Crossbow Sniper" desc="You are skilled in lining up accurate, deadly shots with your crossbow." />
+    <feat type="Player's Handbook II page 78" name="Crushing Strike" desc="You wield a bludgeoning weapon with superior power, allowing you to batter aside an opponent's defenses." />
+    <feat type="Player's Handbook II page 78" name="Cunning Evasion" desc="When an area attack detonates around you, you use the chaos and flash of energy to duck out of sight." />
+    <feat type="Player's Handbook II page 78" name="Dampen Spell" desc="From the lowliest prestidigitator to the most august hierophant, spellcasters both arcane and divine recognize the power of counterspelling." />
+    <feat type="Player's Handbook II page 78" name="Deadeye Shot" desc="You carefully line up a ranged attack, timing it precisely so that you hit your opponent when his guard is down." />
+    <feat type="Player's Handbook II page 78" name="Defensive Sweep" desc="You sweep your weapon through the area you threaten, warding away opponents and forcing them to move away or suffer a fearsome blow." />
+    <feat type="Player's Handbook II page 78" name="Driving Attack" desc="When you strike an opponent with a piercing weapon, the brutal impact of your strike sends him sprawling." />
+    <feat type="Player's Handbook II page 78" name="Elven Spell Lore" desc="You have studied the mighty arcane traditions of the elven, granting you insight into the intricate workings of magic and the theoretical structures behind spells." />
+    <feat type="Player's Handbook II page 79" name="Fade into Violence" desc="While the chaos of battle swirls around you, you rely on your ability to slip into the background to avoid your enemy's notice." />
+    <feat type="Player's Handbook II page 79" name="Fiery Fist" desc="By channeling your kienergy, you sheathe your limbs in magical fire." />
+    <feat type="Player's Handbook II page 79" name="Fiery KiDefense" desc="You channel your kienergy into a cloak of flame that injures any who attempt to strike you." />
+    <feat type="Player's Handbook II page 79" name="Flay" desc="When fighting unarmored opponents, you excel at twisting your weapon just before impact." />
+    <feat type="Player's Handbook II page 79" name="Grenadier" desc="You are skilled in using grenadelike weapons." />
+    <feat type="Player's Handbook II page 79" name="Hindering Opportunist" desc="When you have a chance to strike a distracted foe, you instead use that opportunity to aid or protect an ally against him." />
+    <feat type="Player's Handbook II page 79" name="Intimidating Strike" desc="You make a display of your combat prowess designed to strike terror in your foe." />
+    <feat type="Player's Handbook II page 80" name="Indomitable Soul" desc="Your physical toughness translates into greater mental resiliency." />
+    <feat type="Player's Handbook II page 80" name="Keen-Eared Scout" desc="Your sharp sense of hearing allows you to determine much more about your surroundings." />
+    <feat type="Player's Handbook II page 80" name="KiBlast" desc="You focus your kiinto a ball of energy that you can hurl at an opponent." />
+    <feat type="Player's Handbook II page 80" name="Leap of the Heavens" desc="Your excellent athletic ability and superior conditioning allow you to make near-superhuman leaps." />
+    <feat type="Player's Handbook II page 80" name="Lunging Strike" desc="You make a single attack against a foe who stands just beyond your reach." />
+    <feat type="Player's Handbook II page 80" name="Lurking Familiar" desc="Your familiar hides within the folds of your robe or takes cover behind you as your opponents close in." />
+    <feat type="Player's Handbook II page 80" name="Mad Foam Rager" desc="You fight with the rage that only a rabid badger or a beer-addled dwarf can bring to bear." />
+    <feat type="Player's Handbook II page 80" name="Master Manipulator" desc="Your words are your weapons." />
+    <feat type="Player's Handbook II page 81" name="Melee Evasion" desc="Your speed, agility, and talent for intelligent fighting allow you to avoid your opponent's blows." />
+    <feat type="Player's Handbook II page 81" name="Melee Weapon Mastery" desc="You have mastered a wide range of weapons." />
+    <feat type="Player's Handbook II page 81" name="Overwhelming Assault" desc="If you attack a foe who does nothing to turn aside your attack, you press forward with an indomitable strike." />
+    <feat type="Player's Handbook II page 81" name="Penetrating Shot" desc="You send a powerful shot cleaving through your enemies." />
+    <feat type="Player's Handbook II page 82" name="Ranged Weapon Mastery" desc="You have mastered a wide range of weapons." />
+    <feat type="Player's Handbook II page 82" name="Rapid Blitz" desc="You charge across the battlefield, combining your speed and fighting ability to move and attack with unmatched skill." />
+    <feat type="Player's Handbook II page 82" name="Robilar's Gambit" desc="By offering Robilar's Gambit, you absorb damage to place yourself in an advantageous position." />
+    <feat type="Player's Handbook II page 82" name="Shield Sling" desc="You can hurl your shield as a deadly missile, turning it from a defensive item to a crushing, thrown weapon." />
+    <feat type="Player's Handbook II page 82" name="Shield Specialization" desc="You are skilled in using a shield, allowing you to gain greater defensive benefits from it." />
+    <feat type="Player's Handbook II page 82" name="Shield Ward" desc="You use your shield like a wall of steel and wood." />
+    <feat type="Player's Handbook II page 82" name="Short Haft" desc="You have trained in polearm fighting alongside your comrades in arms, sometimes reaching past them while they shield you, and sometimes shielding them while they attack from behind you." />
+    <feat type="Player's Handbook II page 82" name="Slashing Flurry" desc="You swing your weapon with uncanny speed, slicing apart a foe in the blink of an eye." />
+    <feat type="Player's Handbook II page 83" name="Spectral Skirmisher" desc="You have trained extensively in the use of magic that renders you invisible." />
+    <feat type="Player's Handbook II page 83" name="Spell-Linked Familiar" desc="You and your familiar can share spell energy, allowing your familir to cast a limited number of spells each day." />
+    <feat type="Player's Handbook II page 83" name="Stalwart Defense" desc="You excel at aiding your allies in battle. When an opponent attempts to strike one of them, you make a quick, distracting motion to ruin the foe's efforts." />
+    <feat type="Player's Handbook II page 83" name="Steadfast Determination" desc="Your physical durability allows you to shrug off attacks that would cripple a lesser person." />
+    <feat type="Player's Handbook II page 83" name="Telling Blow" desc="When you strike an opponent's vital areas, you draw on your ability to land crippling blows to make the most of your attack." />
+    <feat type="Player's Handbook II page 83" name="Trophy Collector" desc="A belt of minotaur fur, a hood of cloaker wing-skin, and an amulet fashioned from a petrified dragon's eye -- these are the intimidating symbols of your trade." />
+    <feat type="Player's Handbook II page 84" name="Tumbling Feint" desc="When you move near an opponent, your acrobatic maneuvers leave him confused and unable to properly defend himself." />
+    <feat type="Player's Handbook II page 84" name="Two-Weapon Pounce" desc="When you charge an opponent while wielding two weapons, you can make two quick attacks." />
+    <feat type="Player's Handbook II page 84" name="Two-Weapon Rend" desc="You wield two weapons with an artisan's precision." />
+    <feat type="Player's Handbook II page 85" name="Vatic Gaze" desc="Your arcane studies have brought forth your nascent talent to sense magical auras and the power that others are capable of wielding." />
+    <feat type="Player's Handbook II page 85" name="Versatile Unarmed Strike" desc="You employ a variety of unarmed fighting styles, allowing you to alter the type of damage your attacks deal." />
+    <feat type="Player's Handbook II page 85" name="Vexing Flanker" desc="You excel at picking apart an opponent's defenses when your allies also threaten them." />
+    <feat type="Player's Handbook II page 85" name="Wanderer's Diplomacy" desc="Many halflings journey far and wide across the world, spending no more than a few months in one place." />
+    <feat type="Player's Handbook II page 85" name="Water Splitting Stone" desc="You channel your kienergy to splinter the defenses of creatures whose tough hides or magical natures normally allow them to shrug off your blows." />
+    <feat type="Player's Handbook II page 85" name="Weapon Supremacy" desc="You are a grandmaster in the use of your chosen weapon." />
+    <feat type="Player's Handbook II page 86" name="Ritual Blessing" desc="You call upon the powers of goodness and light to bless your allies." />
+    <feat type="Player's Handbook II page 86" name="Ritual Blood Bonds" desc="You invest your allies with the mighty power of your toten, god, or similar divine entity." />
+    <feat type="Player's Handbook II page 86" name="Combat Awareness" desc="When you maintain your combat focus, you have an uncanny ability to sense the ebb and flow of your opponents' vitality." />
+    <feat type="Player's Handbook II page 87" name="Combat Defense" desc="The state of keen focus and mental discipline you attain in combat allows you to shift the focus of yoru defense from one opponent to another with careful, precise maneuvers." />
+    <feat type="Player's Handbook II page 87" name="Combat Focus" desc="The way of the warrior requires more than simple, brute strength." />
+    <feat type="Player's Handbook II page 87" name="Combat Stability" desc="When you maintain your combat focus, you become difficult to dislodge." />
+    <feat type="Player's Handbook II page 87" name="Combat Strike" desc="Your intense, focused state allows you to see the one critical moment in a battle when you hang suspended between victory and defeat." />
+    <feat type="Player's Handbook II page 88" name="Combat Vigor" desc="When you maintain your combat focus, your clarity of purpose and relentless drive allow you to overcome your body's frailties." />
+    <feat type="Player's Handbook II page 88" name="Divine Armor" desc="You call upon your deity to protect you in your hour of need by wreathing you in divine power that wards off your enemies' attacks." />
+    <feat type="Player's Handbook II page 88" name="Divine Fortune" desc="With a quick prayer, you channel divine energy to help resist a spell, poison, or other deadly effect." />
+    <feat type="Player's Handbook II page 88" name="Divine Justice" desc="You can channel divine energy to turn your foe's strength against him, striking him with the same force that he used against you." />
+    <feat type="Player's Handbook II page 88" name="Divine Ward" desc="You create a channel of divine energy between yourself and a willing ally." />
+    <feat type="Player's Handbook II page 89" name="Profane Aura" desc="You call upon the dark powers you worship to fill the area around you with a dreadful mist that obscures sight." />
+    <feat type="Player's Handbook II page 89" name="Sacred Healing" desc="You can channel divine energy to aid in your efforts to tend to a comrade's injuries, sickness, or other conditions." />
+    <feat type="Player's Handbook II page 89" name="Sacred Purification" desc="You serve as a conduit of divine energy, filling the area around you with power that aids the living and saps the undead." />
+    <feat type="Player's Handbook II page 89" name="Sacred Radiance" desc="You channel divine energy to fill the area around you with a soothing, gentle radiance." />
+    <feat type="Player's Handbook II page 90" name="Celestial Sorcerer Aura" desc="The power of your sorcerous heritage shines through, allowing you to infuse the area around you with a menacing aura." />
+    <feat type="Player's Handbook II page 90" name="Celestial Sorcerer Heritage" desc="Your ancestry manifests in the form of several special abilities." />
+    <feat type="Player's Handbook II page 90" name="Celestial Sorcerer Lance" desc="You can channel your arcane energy into a bolt of power that is baneful to evil creatures." />
+    <feat type="Player's Handbook II page 90" name="Celestial Sorcerer Lore" desc="The power of your ancestry grants you access to a variety of new spells." />
+    <feat type="Player's Handbook II page 91" name="Celestial Sorcerer Wings" desc="You channel your inborn magical abilities to spawn a pair of spectral, magical wings that glow with majestic power." />
+    <feat type="Player's Handbook II page 91" name="Infernal Sorcerer Eyes" desc="Your eyes glow with infernal fire, allowing you to see through magical darkness." />
+    <feat type="Player's Handbook II page 91" name="Infernal Sorcerer Heritage" desc="Your innate magic derives from infernal ancestors." />
+    <feat type="Player's Handbook II page 91" name="Infernal Sorcerer Howl" desc="You channel the fury of your infernal ancesters into a thunderous roar that blasts your enemies with sonic power." />
+    <feat type="Player's Handbook II page 91" name="Infernal Sorcerer Resistance" desc="You are as tough and resilient as an infernal monstrosity, allowing you to shrug off acid and cold damage." />
+    <feat type="Player's Handbook II page 91" name="Blistering Spell" desc="Your fire spells sear the flesh from your enemies' bones, leaving them wracked with pain." />
+    <feat type="Player's Handbook II page 91" name="Earthbound Spell" desc="You bind a spell into the rock and soil, leaving it there until an opponent stumbles across it." />
+    <feat type="Player's Handbook II page 91" name="Flash Frost Spell" desc="Your spells that use cold and ice to damage your foes leave behind a thin layer of slippery frost." />
+    <feat type="Player's Handbook II page 92" name="Imbued Summoning" desc="Your summoning spells gain an element of surprise." />
+    <feat type="Player's Handbook II page 92" name="Smiting Spell" desc="You can channel the energy of a touch spell into a weapon, causing the spell to discharge when you strike an opponent." />
+    <feat type="Player's Handbook II page 92" name="Blood-Spiked Charger" desc="You throw yourself into the fray, using your spiked armor and spiked shield to tear your opponents to pieces." />
+    <feat type="Player's Handbook II page 93" name="Combat Cloak Expert" desc="You are adept at turning your cloak into a vital part of your combat repertoire." />
+    <feat type="Player's Handbook II page 93" name="Combat Panache" desc="Your glowing personality and sharp performance abilities allow you to navigate the battlefield on sheer chutzpah alone." />
+    <feat type="Player's Handbook II page 94" name="Einhander" desc="You excel at wielding a one-handed weapon while carrying nothing in your off hand." />
+    <feat type="Player's Handbook II page 94" name="Mad Alchemist" desc="You are an expert at using alchemical items." />
+    <feat type="Player's Handbook II page 94" name="Shadow Striker" desc="You melt into the shadows, hiding from your enemies until the time is right." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 83" name="Abyss-Bound Soul [Vile]" desc="You have pledged your immortal soul to a particular demon lord in return for a gift that aids your evil works in life." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 83" name="Blood War Conscript [Vile]" desc="Your evil brand indicates your rank in the armies of the Blood War and infuses you with fury." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 84" name="Chaotic Spell Recall [Abyssal Heritor]" desc="A few choice spells never stray far from your mind." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 84" name="Claws of the Beast [Abyssal Heritor]" desc="Your hands are twisted like claws. This deformity allows you to deal more damage than usual with your unarmed strikes and sneak attacks." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 85" name="Cloak of the Obyrith [Abyssal Heritor]" desc="The chaos of the Abyss suffuses your being, as it does the ancient obyriths." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 85" name="Dark Speech [Vile]" desc="You learn a smattering of the language of truly dark power." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 85" name="Demonic Conduit [Vile]" desc="Your evil brand incorporates blasphemous runes and sigils that augment magical attacks you make against lawful and/or good targets." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 85" name="Demonic Skin [Abyssal Heritor]" desc="Your skin has rough, scaly patches that enhance your natural armor." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 85" name="Demonic Sneak Attack [Abyssal Heritor]" desc="You know exactly how to twist the blade to get the most out of your sneak attacks." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 85" name="Demon Mastery" desc="You are particularly skilled at summoning demons and convincing them to serve you." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 85" name="Evil Brand [Vile]" desc="You are physically marked forever as the servant of an evil power greater than yourself -- in this case, a demon lord. The symbols is unquestionable in its perversity, depicting a depravity so unthinkable that all who see it know beyond a doubt that you serve the lords of the Abyss." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 86" name="Extract Demonic Essence" desc="You can draw upon the living essence of a willing or captured demon to fuel the creation of items or the casting of potent spells." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 86" name="Eyes of the Abyss [Abyssal Heritor]" desc="Your eyes glow with an inner fire of some unusual color. This glow increases your perception and allows you to see in the dark." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 86" name="Heart of the Nabassu" desc="Your ancestry traces back to a place where the Abyss meets the Negative Energy Plane." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 86" name="Keeper of Forbidden Lore [Abyssal Heritor]" desc="A shred of demonic racial memory grants you knowledge of numerous ancient magical secrets." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 86" name="Ordered Chaos" desc="You are an unusually lawful Abyssal heritor." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 87" name="Otherworldly Countenance [Abyssal Heritor]" desc="You are either stunningly beautiful or wretchedly hideous. Either way, your appearance can be terribly unsettling to others upon whom you focus your attentions." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 87" name="Poison Healer" desc="Poison isn't always bad for you." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 87" name="Poison Talons [Abyssal Heritor]" desc="Your claws drip with poison." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 87" name="Precognitive Visions [Abyssal Heritor]" desc="You periodically experience visions from the near future." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 87" name="Primordial Scion [Abyssal Heritor]" desc="The Abyss beckons. . . ." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 87" name="Thrall to Demon [Vile]" desc="You formally become a supplicant to a demon lord. In return for your obedience, you gain a small measure of that demon lord's power." />
+    <feat type="Fiendish Codex I: Hordes of the Abyss page 87" name="Vestigial Wings [Abyssal Heritor]" desc="A pair of vestigial wings sprouts from your shoulders." />
+    <feat type="Monster Manual IV page 202" name="Ability Focus" desc="A particular special ability of a creature with this feat is more potent than normal." />
+    <feat type="Monster Manual IV page 202" name="Awesome Blow" desc="A creature with this feat can choose to deliver blows that send its smaller opponents flying like bowling pins." />
+    <feat type="Monster Manual IV page 202" name="Clinging Breath" desc="This feat enables a creature's breath weapon to cling to creatures and continue to affect them after it has breathed." />
+    <feat type="Monster Manual IV page 202" name="Craft Construct [Item Creation]" desc="A creature with this feat can create golems and other magic automatons that obey its orders." />
+    <feat type="Monster Manual IV page 202" name="Flyby Attack" desc="A creature with this feat can attack on the wing." />
+    <feat type="Monster Manual IV page 202" name="Githyanki Battlecaster" desc="A creature with this feat ignores arcane spell failure chances when wearing light armor." />
+    <feat type="Monster Manual IV page 202" name="Githyanki Dragonrider [Racial]" desc="A creature with this feat has a knack for getting along with red dragons." />
+    <feat type="Monster Manual IV page 203" name="Improved Natural Attack" desc="The natural attacks of a creature with this feat are more dangerous than its size and type would otherwise dictate." />
+    <feat type="Monster Manual IV page 203" name="Improved Toughness" desc="A creature with this feat is significantly tougher than normal." />
+    <feat type="Monster Manual IV page 203" name="Lingering Breath" desc="The breath weapon of a creature with this feat forms a lingering cloud." />
+    <feat type="Monster Manual IV page 203" name="Multiattack" desc="A creature with this feat is adept at using all its natural weapons at once." />
+    <feat type="Monster Manual IV page 203" name="Powerful Charge" desc="A creature with this feat can charge with extra force." />
+    <feat type="Monster Manual IV page 203" name="Quicken Spell-Like Ability" desc="A creature with this feat can employ a spell-like ability with a moment's thought." />
+    <feat type="Secrets of Xen'drik page 134" name="Blessed of Vulkoor [Racial]" desc="A scorpion-shaped birthmark denotes you as one of the chosen of Vulkoor." />
+    <feat type="Secrets of Xen'drik page 134" name="Drow Scorpion Warrior [Racial, Tactical]" desc="Your study of the ways of the scorpion grants you special tactics." />
+    <feat type="Secrets of Xen'drik page 134" name="Drow Skirmisher [Racial]" desc="Your experience with the guerrilla-style combat of the deep jungle grants you mastery of the weapons of the drow." />
+    <feat type="Secrets of Xen'drik page 134" name="Earthquake Stomp" desc="Your thunderous steps allow you to knock smaller enemies off their feet." />
+    <feat type="Secrets of Xen'drik page 134" name="Echoing Spell [Metamagic]" desc="Your spells return after you cast them, although with lessened effects." />
+    <feat type="Secrets of Xen'drik page 135" name="Elder Giant Magic" desc="You have learned a technique developed by ancient giant spellcasters, allowing you to channel additional power in your spells." />
+    <feat type="Secrets of Xen'drik page 135" name="Giant Banemagic" desc="You can cast spells that deal additional damage to giants." />
+    <feat type="Secrets of Xen'drik page 135" name="Jungle Veteran" desc="You have a knack for surviving in harsh environments and avoiding the deadly ambushes of natives." />
+    <feat type="Secrets of Xen'drik page 135" name="Mysterious Magic" desc="Your study of unconventional magic gives your spells an odd appearance and makes them difficult to identify." />
+    <feat type="Secrets of Xen'drik page 135" name="Rending Claws" desc="Your expertise with scorpion claw gauntlets allows you to tear apart your opponents with deadly precision." />
+    <feat type="Dragons of Faerun page 47" name="Breath of Unlife [Metabreath]" desc="Your breath weapon contains the chill of undeath." />
+    <feat type="Dragons of Faerun page 50" name="Transdimensional Breath [Psionic]" desc="Your breath weapon affects bordering planes." />
+    <feat type="Dragons of Faerun page 57" name="Follower of the Scaly Way" desc="You are an adherent of Sammaster's teachings." />
+    <feat type="Dragons of Faerun page 92" name="Servant of a Dragon Ascendant" desc="You formally supplicate yourself to an immortal dragon quasi-deity." />
+    <feat type="Dragons of Faerun page 92" name="Initiate of Tchazzar" desc="You have been initiated into the greatest mysteries of Tchazzar's church." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 28" name="Adaptive Style" desc="With just a short period of meditation, you can change your maneuvers and tactics to meet the threat you currently face." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 28" name="Avenging Strike" desc="Your strength of will and strong sense of justice allow you to smite your foes." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 28" name="Blade Meditation" desc="You have learned a meditation that grants you insight into the martial disciplines you have studied." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 29" name="Desert Fire" desc="The power of the Desert Wind surges through you, and you find power in the motion of the hot winds and shifting sands that you can channel into your Desert Wind strikes." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 29" name="Desert Wind Dodge" desc="Your training in the Desert Wind discipline allows you to dance across the battlefield like a blistering sirocco." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 29" name="Devoted Bulwark" desc="Because of your staunch devotion to your cause and your Devoted Spirit training, you can stand your ground even in the face of an enemy's resounding attack." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 29" name="Divine Spirit [Divine]" desc="The fervor and dedication of the Devoted Spirit discipline, combined with your fanatical adherence to a divine power, turns you into a font of spiritual energy." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 30" name="Evasive Reflexes" desc="When an opponent gives you an opening in combat, you know exactly what to do: slip away." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 30" name="Extra Granted Maneuver" desc="You are especially devout or insightful, and you have more control over which of your martial maneuvers are currently granted than other crusaders." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 30" name="Extra Readied Maneuver" desc="You are an unusually perspicacious student of the Sublime Way, and you find it easy to keep a large number of maneuvers ready for use." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 31" name="Falling Sun Attack" desc="The discipline of the Setting Sun teaches you how to turn an opponent's strengths into weaknesses." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 31" name="Instant Clarity [ Psionic]" desc="You have sharpened your concentration to the point that you can focus your psionic abilities with just an instant's thought." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 31" name="Ironheart Aura" desc="Your strength of spirit and martial training inspires those around you." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 31" name="Martial Stance" desc="You have mastered the fundamentals of a martial discipline, and you are now able to master one of its stances." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 31" name="Martial Study" desc="By studying the basics of a martial discipline, you learn to focus you kiand perfect the form needed to use a maneuver." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 32" name="Psychic Renewal [Psionic]" desc="Your mental strength and psionic abilities allow you to focus your mind on combat and use your most devastating maneuvers more frequently." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 32" name="Rapid Assault" desc="Your fighting style emphasizes taking foes down with quick, powerful blows." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 32" name="Scribe Martial Script [Item Creation]" desc="You know the secret of creating martial scripts -- small slips of paper into which you infuse your own martial power and skill." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 32" name="Shadow Blade" desc="In the course of your training in the Shadow Hand discipline, you learn to use your natural agility and speed to augment your attacks with certain weapons." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 32" name="Shadow Trickster" desc="Your mastery of the Shadow Hand discipline lets you augment your illusion spells with the stuff of shadow." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 32" name="Song of the White Raven" desc="The White Raven discipline shows you how to rouse dedication and fervor within your allies' hearts." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 32" name="Snap Kick" desc="You have continued to hone your unarmed combat skills, and you deal more damage with your unarmed strikes." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 32" name="Stone Power" desc="The principles of the Stone Dragon discipline teach you how to gather and focus your raw, physical strength into an attack." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 33" name="Sudden Recovery" desc="You can instantly recover your focus, balance, and personal energy after using a martial maneuver." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 33" name="Superior Unarmed Strike" desc="Your unarmed strikes have become increasingly deadly, enabling you to strike your foes in their most vulnerable areas." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 33" name="Tiger Blooded" desc="The Tiger Claw discipline teaches students to mimic the rampant, feral qualities of a wild animal." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 33" name="Unnerving Calm" desc="You know that the secret to defeating your enemies lies within the still center of your own mind." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 33" name="Vital Recovery" desc="Preparing yourself to execute more of your maneuvers gives you the chance to catch a quick second wind and recover from damage you have sustained in the fight." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 33" name="White Raven Defense" desc="The White Raven discipline has taught you to shine as a gleaming beacon of hope and endurance amid the chaos of battle." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 34" name="Clarion Commander" desc="On the battlefield, you are a natural leader." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 34" name="Distant Horizon" desc="An initiate of the Setting Sun sometimes learns a set of combat maneuvers to create the Distant Horizon fighting form." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 34" name="Faith Unswerving" desc="The initiate of the Devoted Spirit knows that his fanaticism and devotion to a cause are enough to carry him through almost anything." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 35" name="Gloom Razor" desc="The teachings of the Shadow Hand discipline allow you to confuse your enemies." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 35" name="Perfect Clarity of Mind and Body" desc="Your mastery of the Diamond Mind discipline allows you to tap into reserves of spiritual and physical strength that other warriors cannot imagine using." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 35" name="Reaping Talons" desc="When fighting with the Tiger Claw discipline's preferred weapons, you can use a variety of combat options that maximize the benefits of wielding two weapons." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 35" name="Scorching Sirocco" desc="As a student of the Desert Wind, the burning fury of the desert sirocco is at your command." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 36" name="Shards of Granite" desc="Like the great Stone Dragon, you hammer through your opponents' defenses using raw, brutal strength." />
+    <feat type="Tome of Battle: The Book of Nine Swords page 36" name="Stormguard Warrior" desc="The Stormguard Warrior feat encompasses a number of the more advanced tactics and techniques you would use as a student of the Iron Heart school." />
+    <feat type="Dragon Magic page 15" name="Armor of Scales [Ceremony]" desc="You imbue a target with the protection of a dragon's blade." />
+    <feat type="Dragon Magic page 15" name="Black Dragon Lineage [Draconic]" desc="You have attuned yourself to your black dragon ancestry and can poison foes with your touch." />
+    <feat type="Dragon Magic page 15" name="Blue Dragon Lineage [Draconic]" desc="You have learned to harness the power of your blue dragon ancestry and can hurl orbs of lightning." />
+    <feat type="Dragon Magic page 16" name="Brass Dragon Lineage [Draconic]" desc="You have unlocked the power of your brass dragon ancestry and can put foes to sleep with ease." />
+    <feat type="Dragon Magic page 16" name="Bronze Dragon Lineage [Draconic]" desc="You have tapped into your bronze dragon blood and can channel arcane energy to repel foes." />
+    <feat type="Dragon Magic page 16" name="Copper Dragon Lineage [Draconic]" desc="You have learned to channel the powers of your copper dragon ancestry to hinder your enemies' mobility." />
+    <feat type="Dragon Magic page 16" name="Double Draconic Aura" desc="You can project two draconic auras simultaneously." />
+    <feat type="Dragon Magic page 16" name="Draconic Armor [Draconic]" desc="You learn to block damage from successful attacks, lessening the blows with spell energy." />
+    <feat type="Dragon Magic page 16" name="Draconic Aura" desc="You can tap into the raw power of dragons to create a variety of potent auras around you." />
+    <feat type="Dragon Magic page 17" name="Draconic Heritage [Draconic]" desc="You have a greater connection with your draconic bloodline than others of your kind." />
+    <feat type="Dragon Magic page 17" name="Draconic Knowledge [Draconic]" desc="Your draconic blood lets you access ancient draconic knowledge." />
+    <feat type="Dragon Magic page 17" name="Draconic Senses [Draconic]" desc="Your draconic blood grants you great sensory powers." />
+    <feat type="Dragon Magic page 17" name="Draconic Vigor [Draconic]" desc="You gain some of the vitality of your draconic ancestry when casting spells." />
+    <feat type="Dragon Magic page 17" name="Dragonfire Assault [Draconic]" desc="You can augment your most powerful melee attacks with draconic power." />
+    <feat type="Dragon Magic page 17" name="Dragonfire Channeling [Draconic]" desc="You channel draconic fire through your holy symbol." />
+    <feat type="Dragon Magic page 17" name="Dragonfire Inspiration [Draconic]" desc="You can channel the power of your draconic ancestry into the attacks of your allies." />
+    <feat type="Dragon Magic page 18" name="Dragonfire Strike [Draconic]" desc="You can call upon your innate draconic powers to augment certain weapon attacks." />
+    <feat type="Dragon Magic page 18" name="Dragontouched [Draconic]" desc="You have a trace of draconic power, a result of dragons in your ancestry or a spiritual connection between you and the forces of dragonkind." />
+    <feat type="Dragon Magic page 19" name="Gold Dragon Lineage [Draconic]" desc="You can harness the legacy of your gold dragon ancestry to protect your allies." />
+    <feat type="Dragon Magic page 19" name="Heart of Dragons [Ceremony]" desc="You imbue your allies with draconic power." />
+    <feat type="Dragon Magic page 20" name="Initiate of Aasterinian [Initiate]" desc="You live for the moment, reveling in new experiences without fear of consequence." />
+    <feat type="Dragon Magic page 20" name="Initiate of Astilabor [Initiate]" desc="You share your deity's desire to acquire and protect treasure, and she has recognized this by granting you an edge in achieving these goals." />
+    <feat type="Dragon Magic page 20" name="Initiate of Bahamut [Initiate]" desc="The Platinum Dragon has entrusted you with great power in the battle against evil." />
+    <feat type="Dragon Magic page 20" name="Initiate of Falazure [Initiate]" desc="Your celebration of death and decay has opened up new magical secrets involving the living and undead." />
+    <feat type="Dragon Magic page 20" name="Initiate of Garyx [Initiate]" desc="You channel the cleansing fire of destruction, as wielded by your deity." />
+    <feat type="Dragon Magic page 21" name="Initiate of Hlal [Initiate]" desc="Fueled by faith in your deity, your audacity and bravery truly know no bounds." />
+    <feat type="Dragon Magic page 21" name="Initiate of Io [Initiate]" desc="Your deity has entrusted you with the responsibility of tending to dragonkind." />
+    <feat type="Dragon Magic page 21" name="Initiate of Lendys [Initiate]" desc="Your dedication to justice grants you the ability to ferret out and punish wrongdoers." />
+    <feat type="Dragon Magic page 21" name="Initiate of Tamara [Initiate]" desc="You wield the twin powers of mercy and death in service to your draconic patron." />
+    <feat type="Dragon Magic page 21" name="Initiate of Tiamat [Initiate]" desc="Your homage to the creator of evil dragonkind has been rewarded with physical and mental power." />
+    <feat type="Dragon Magic page 21" name="Red Dragon Lineage [Draconic]" desc="The fiery blood of red dragons runs within your veins, allowing you to produce flames from thin air." />
+    <feat type="Dragon Magic page 22" name="Silver Dragon Lineage [Draconic]" desc="You are the descendant of silver dragons and can harness your ancestors' power to paralyze your opponents." />
+    <feat type="Dragon Magic page 22" name="Slayer of Dragons [Ceremony]" desc="You protect your allies from the ravages they are sure to face while hunting dragons." />
+    <feat type="Dragon Magic page 22" name="White Dragon Lineage [Draconic]" desc="Your veins run with the savage blood of white dragons, allowing you to whyp yourself into a ragelike state." />
+    <feat type="Dragon Magic page 22" name="Words of Draconic Power [Ceremony]" desc="You tap into the great tradition of draconic magic to enhance the words of your allies." />
+    <feat type="Faiths of Eberron page 145" name="Action Healing" desc="You can spend an action point to enhance your healing power." />
+    <feat type="Faiths of Eberron page 145" name="Ancestral Whispers" desc="Through intense focus and divine energies, you can hear the advice of past ancestors." />
+    <feat type="Faiths of Eberron page 145" name="Ceremonial Empowerment" desc="Your divine might increases on your patron's holy days." />
+    <feat type="Faiths of Eberron page 145" name="Construct Grafter [Item Creation]" desc="You can apply construct grafts to other living creatures or to yourself." />
+    <feat type="Faiths of Eberron page 145" name="Divine Alacrity [Divine]" desc="You can channel divine energies into your own body, increasing your speed." />
+    <feat type="Faiths of Eberron page 146" name="Divine Countermagic [Divine]" desc="You channel divine energies to counter spells." />
+    <feat type="Faiths of Eberron page 146" name="Divine Warrior [Divine]" desc="Through divine power, you wield your deity's favored weapon to devastating effect." />
+    <feat type="Faiths of Eberron page 147" name="Domain Spontaneity [Divine]" desc="You are so familiar with one of your domains that you can convert other prepared spells into spells from that domain." />
+    <feat type="Faiths of Eberron page 147" name="Frantic Rage" desc="Your divine madness allows you to channel your fury into frenetic agility rather than might." />
+    <feat type="Faiths of Eberron page 147" name="Heroic Channeling [Divine]" desc="You can call on your personal strength of will to channel positive or negative energy into divine feats." />
+    <feat type="Faiths of Eberron page 147" name="Heroic Devotion [Divine]" desc="Your devotion to your faith allows you to manipulate fate at the expense of some spellcasting ability." />
+    <feat type="Faiths of Eberron page 147" name="Lucid Channeling" desc="When you invite a celestial into your body, you open your mind completely to the divine spirit." />
+    <feat type="Faiths of Eberron page 147" name="Nightbringer Initiate" desc="You have been trained in the ways of the Nightbringers, a new offshoot of the Children of Winter." />
+    <feat type="Faiths of Eberron page 147" name="Sacred Resilience" desc="You can channel divine energies to protect your allies from harm." />
+    <feat type="Faiths of Eberron page 148" name="Touch of Silver" desc="Your devotion to the Silver Flame allows you to burn the Church's foes with holy energies." />
+    <feat type="Faiths of Eberron page 148" name="Unquenchable Flame of Life" desc="You are hardened to the attacks of the undead." />
+    <feat type="Faiths of Eberron page 148" name="Unyielding Bond of Soul" desc="You are hardened to the attacks of the beings of other worlds." />
+    <feat type="Faiths of Eberron page 148" name="Worldly Focus" desc="Your belief in the omnipresence of the gods is so strong, you can channel your spells through the environment rather than a holy symbol." />
+    <feat type="Faiths of Eberron page 148" name="Wrest Possession" desc="If you resist control by a possessing fiend, you can attempt to seize control of its abilities." />
+    <feat type="Expedition to Castle Ravenloft page 200" name="Enduring Life" desc="You can ignore the effect of negative levels for a short time." />
+    <feat type="Expedition to Castle Ravenloft page 200" name="Lasting Life" desc="You can shed negative levels with an act of will." />
+    <feat type="Expedition to Castle Ravenloft page 205" name="Favored in Guild" desc="You are an active and valued member of your guild." />
+    <feat type="Complete Mage page 37" name="Acidic Splatter" desc="You can channel magical energy into orbs of acid." />
+    <feat type="Complete Mage page 37" name="Alacritous Cogitation" desc="You can leave a prepared spell slot open to spontaneously cast a spell." />
+    <feat type="Complete Mage page 39" name="Aquatic Breath [Reserve]" desc="Your reservoir of magic allows you to breathe normally even underwater." />
+    <feat type="Complete Mage page 39" name="Battlecaster Defense [Tactical]" desc="You have mastered techniques for taking full advantage of spells in melee while remaining unharmed." />
+    <feat type="Complete Mage page 40" name="Battlecaster Offense [Tactical]" desc="You cunningly mix melee combat and spellcasting to increase the potency of both." />
+    <feat type="Complete Mage page 40" name="Blade of Force [Reserve]" desc="You can surround a weapon with a short-lived aura of force." />
+    <feat type="Complete Mage page 40" name="Borne Aloft [Reserve]" desc="You can channel the magic of the winds to briefly grant you flight." />
+    <feat type="Complete Mage page 40" name="Captivating Melody" desc="You can expend some of your musical abilities to increase the potency of your enchantment or illusion spells." />
+    <feat type="Complete Mage page 40" name="Clap of Thunder [Reserve]" desc="You can deliver a thunderous roar with a touch." />
+    <feat type="Complete Mage page 40" name="Cloudy Conjuration" desc="Your conjured creations and summoned beings appear in a puff of sickening black smoke, and you vanish in a cloud of the same when you teleport." />
+    <feat type="Complete Mage page 40" name="Clutch of Earth [Reserve]" desc="You briefly increase the earth's pull on the target creature." />
+    <feat type="Complete Mage page 41" name="Dazzling Illusion" desc="Casting illusions causes the air about you to be filled with flashing colors that dazzle your foes." />
+    <feat type="Complete Mage page 41" name="Defending Spirit" desc="Your watchful spirit helps keep you safe in combat." />
+    <feat type="Complete Mage page 41" name="Delay Potion" desc="You can drink a potion and postpone its effects." />
+    <feat type="Complete Mage page 41" name="Dimensional Jaunt" desc="With a single step, you can cross an entire room." />
+    <feat type="Complete Mage page 41" name="Dimensional Reach [Reserve]" desc="You can transport small objects to you with an act of will." />
+    <feat type="Complete Mage page 41" name="Drowning Glance [Reserve]" desc="With a look, you create a small but incapacitating amount of water in the subject's lungs." />
+    <feat type="Complete Mage page 42" name="Elemental Adept" desc="You can spontaneously cast a spell of the element you have mastered." />
+    <feat type="Complete Mage page 42" name="Energy Abjuration" desc="Casting an abjuration spell grants you protection from energy damage." />
+    <feat type="Complete Mage page 42" name="Energy Gestalt [Tactical]" desc="You have learned to combine multiple energy effects to great advantage." />
+    <feat type="Complete Mage page 42" name="Face-Changer [Reserve]" desc="Your mastery of illusions allows you to subtly alter your appearance at whim." />
+    <feat type="Complete Mage page 42" name="Favored Magic Foe" desc="Through study, you have learned how best to defend yourself against your favored enemies' spells and how to best affect them with your own." />
+    <feat type="Complete Mage page 42" name="Fearsome Necromancy" desc="Creatures subjected to your necromantic spells feel the chill of fear." />
+    <feat type="Complete Mage page 43" name="Fey Heritage [Heritage]" desc="You are descended from creatures native to the fey realms. You are naturally resistant to the most common effects produced by" />
+    <feat type="Complete Mage page 43" name="Fey Legacy [Heritage]" desc="The magical powers of your ancestors manifest in you." />
+    <feat type="Complete Mage page 43" name="Fey Power [Heritage]" desc="Your fey heritage augments the power of certain types of magic." />
+    <feat type="Complete Mage page 43" name="Fey Presence [Heritage]" desc="You share your ancestor's knack for playing tricks on the minds of others." />
+    <feat type="Complete Mage page 43" name="Fey Skin [Heritage]" desc="Your fey heritage guards you against all weapons except those crafted from the dreaded cold iron." />
+    <feat type="Complete Mage page 43" name="Fiendish Heritage [Heritage]" desc="You are descended from creatures native to the Lower Planes." />
+    <feat type="Complete Mage page 43" name="Fiendish Legacy [Heritage]" desc="The magical powers of your ancestors manifest in you." />
+    <feat type="Complete Mage page 43" name="Fiendish Power [Heritage]" desc="Your fiendish heritage augments the power of certain types of magic." />
+    <feat type="Complete Mage page 43" name="Fiendish Presence [Heritage]" desc="You share your ancestors' ability to tamper with the minds of weak-minded fools." />
+    <feat type="Complete Mage page 43" name="Fiendish Resistance [Heritage]" desc="Your bloodline inures you against corrosion and fire." />
+    <feat type="Complete Mage page 43" name="Fiery Burst [Reserve]" desc="You channel your magical talent into a blast of fire." />
+    <feat type="Complete Mage page 44" name="Hasty Spirit" desc="Your watchful spirit lends you a burst of speed in times of great need." />
+    <feat type="Complete Mage page 44" name="Hurricane Breath [Reserve]" desc="The power of elemental air you hold in your mind allows you to exhale the wind." />
+    <feat type="Complete Mage page 44" name="Insightful Divination" desc="Casting a divination spell grants you an uncanny insight into danger." />
+    <feat type="Complete Mage page 44" name="Invisible Needle [Reserve]" desc="You can create tiny darts of force." />
+    <feat type="Complete Mage page 44" name="Magic Device Attunement" desc="You have a knack for activating familiar magic items." />
+    <feat type="Complete Mage page 44" name="Magic Disruption [Reserve]" desc="You can use your powers of abjuration to interfere with other casters' spells." />
+    <feat type="Complete Mage page 44" name="Magic Sensitive [Reserve]" desc="You literally see the emanations of magic around you." />
+    <feat type="Complete Mage page 44" name="Master of Undeath" desc="You can control an undead that you create . . . for a time." />
+    <feat type="Complete Mage page 44" name="Melodic Casting" desc="You can weave your music and magic together into a single perfect voice." />
+    <feat type="Complete Mage page 45" name="Metamagic School Focus" desc="You are unusually skilled at modifying the effects of a particular school of magic." />
+    <feat type="Complete Mage page 45" name="Metamagic Spell Trigger" desc="You can apply metamagic feats you know to spell effects from magic items you activate with a spell trigger." />
+    <feat type="Complete Mage page 45" name="Metamagic Vigor [Tactical]" desc="The energy you pour into increasing the power of your spells feeds back upon itself in an ever-increasing cycle." />
+    <feat type="Complete Mage page 45" name="Minor Shapeshift [Reserve]" desc="Your mastery of shapeshifting magic allows you to reshape your flesh in small but significant ways." />
+    <feat type="Complete Mage page 45" name="Mystic Backlash [Reserve]" desc="With a touch, your magic corrupts the spells of your enemy." />
+    <feat type="Complete Mage page 46" name="Piercing Evocation" desc="Your evocation spells ignore an amount of energy resistance." />
+    <feat type="Complete Mage page 46" name="Ranged Recall" desc="Your magical ranged attacks rarely miss." />
+    <feat type="Complete Mage page 46" name="Rapid Metamagic" desc="You possess an uncanny mastery of your magic, enabling you to modify spells on the fly much faster than others can." />
+    <feat type="Complete Mage page 46" name="Residual Magic [Tactical]" desc="You can use the lingering energy from a spell you cast to boost the effect of a later spell." />
+    <feat type="Complete Mage page 47" name="Retributive Spell [Metamagic]" desc="You can keep a spell in reserve to use when a foe causes you harm." />
+    <feat type="Complete Mage page 47" name="Shadow Veil [Reserve]" desc="You draw wisps of darkness across your enemy's eyes, obscuring the world around him." />
+    <feat type="Complete Mage page 47" name="Sickening Grasp [Reserve]" desc="You wreak havoc with the inner organs of a target, causing it to grow ill." />
+    <feat type="Complete Mage page 47" name="Somatic Weaponry" desc="You are adept at performing somatic spell components while your hands are occupied." />
+    <feat type="Complete Mage page 47" name="Storm Bolt [Reserve]" desc="The electrical energy contained within your magic rages inside you, begging to be released." />
+    <feat type="Complete Mage page 47" name="Summon Elemental [Reserve]" desc="You can channel the summoning power you hold to briefly bring forth an elemental servant." />
+    <feat type="Complete Mage page 48" name="Sunlight Eyes [Reserve]" desc="The bright magic within you allows you to see through the darkest shadow." />
+    <feat type="Complete Mage page 48" name="Touch of Distraction [Reserve]" desc="Your touch briefly clouds the mind of a foe, impeding its efforts." />
+    <feat type="Complete Mage page 48" name="Toughening Transmutation" desc="Casting a transmutation spell briefly transforms your skin or that of an ally into sterner stuff." />
+    <feat type="Complete Mage page 48" name="Unsettling Enchantment" desc="Your enchantment spells cloud the minds of even those who would otherwise resist their effects." />
+    <feat type="Complete Mage page 48" name="Vengeful Spirit" desc="Your watchful spirit takes revenge on foes that have harmed you." />
+    <feat type="Complete Mage page 48" name="Wind-Guided Arrows [Reserve]" desc="Your mastery of the wind allows you to alter the flight of a ranged weapon." />
+    <feat type="Complete Mage page 48" name="Winter's Blast [Reserve]" desc="The frozen magic within you can burst forth in a hail of frost." /></feats>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd35/dnd35weapons.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,299 @@
+<weapons version="01.02">
+<footnotes>
+<c txt="Shuriken don't get str bonus, 3/attack; don't mark t" ></c>
+<c txt="monkWeap unarmed base, monk att/round, and other monk attack mods" ></c>
+<c txt="fn field for a weapon stands for 'footnotes'" ></c>
+<f mark="b" txt="classified as a bow or sling, str penalty applies " ></f>
+<f mark="c" txt="set for charge weapon, 2x dam when set Vs charging opponent." ></f>
+<f mark="C" txt="2x damaged when used on charge (may need to be mounted) " ></f>
+<f mark="d" txt="Classified as a double weapon." ></f>
+<f mark="m" txt="Monks get special 'Monk Weapon'advantages. " ></f>
+<f mark="o" txt="+2 opposed attack rolls wrt disarming/being disarmed on fail." ></f>
+<f mark="r" txt="Classified as a reach weapon, can strike at 6-10, but cannot strike 0-5" ></f>
+<f mark="R" txt="Classified as a special reach weapon, can strike at 0-10" ></f>
+<f mark="s" txt="Weapon can only do subdual damage." ></f>
+<f mark="t" txt="Classified as a thrown (or throwable) weapon by PH,standard thrown characteristics will be applied" ></f>
+<f mark="T" txt="Weapon can be used for making trip attacks." ></f>
+<f mark="X" txt="Custom weapon, should have appropriate footnotes added." ></f>
+</footnotes>
+<weapon mod="0" fn="" name="Antimatter rifle" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="6d10" critical="x2" range="10" weight="10" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="d" name="Axe, orc double" cost="60" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="x3" range="0" weight="25" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="t" name="Axe, throwing" cost="8" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x2" range="10" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Battleaxe" cost="10" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="7" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Blowgun" cost="1" category="Asian Weapons-Ranged" size="Small" damage="1" critical="x2" range="10" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oT" name="Chain, spiked" cost="25" category="Exotic Weapons-Melee" size="Large" damage="2d4" critical="x2" range="0" weight="15" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="t" name="Club" cost="0" category="Simple Weapons-Melee" size="Medium" damage="1d6" critical="x2" range="10" weight="3" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Crossbow, hand" cost="100" category="Exotic Weapons-Ranged" size="Tiny" damage="1d4" critical="19-20/x2" range="30" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Crossbow, heavy" cost="50" category="Simple Weapons-Ranged" size="Medium" damage="1d10" critical="19-20/x2" range="120" weight="9" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Crossbow, light" cost="35" category="Simple Weapons-Ranged" size="Small" damage="1d8" critical="19-20/x2" range="80" weight="6" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Crossbow, repeating" cost="250" category="Exotic Weapons-Ranged" size="Medium" damage="1d8" critical="19-20/x2" range="80" weight="16" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="t" name="Dagger" cost="2" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="19-20/x2" range="10" weight="1" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Dagger, punching" cost="2" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="x3" range="0" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Dart" cost="0" category="Simple Weapons-Ranged" size="Small" damage="1d4" critical="x2" range="20" weight="0" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Falchion" cost="75" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="18-29/x2" range="0" weight="16" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="doT" name="Flail, dire" cost="90" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="x2" range="0" weight="20" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oT" name="Flail, heavy" cost="15" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="19-20/x2" range="0" weight="20" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oT" name="Flail, light" cost="8" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="5" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Flamer" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="3d6*" critical="-" range="20" weight="8" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Flurry of Blows(Monk Med)" cost="-1" category="Exotic Weapons-Melee" size="Unarmed" damage="Monk Med" critical="x2" range="0" weight="0" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Flurry of Blows(Monk Small)" cost="-1" category="Exotic Weapons-Melee" size="Unarmed" damage="Monk Small" critical="x2" range="0" weight="0" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Gauntlet" cost="2 gp" category="Simple Weapons-Melee" size="Unarmed" damage="*" critical="*" range="0" weight="2" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Gauntlet, spiked" cost="5" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="r" name="Glaive" cost="8" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Greataxe" cost="20" category="Martial Weapons-Melee" size="Large" damage="1d12" critical="x3" range="0" weight="20" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Greatclub" cost="5" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x2" range="0" weight="10" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Greatsword" cost="50" category="Martial Weapons-Melee" size="Large" damage="2d6" critical="19-20/x2" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Grenade launcher" cost="-1" category="Modern Weapons-Ranged" size="Large" damage="*" critical="*" range="200" weight="12" type="*" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="rT" name="Guisarme" cost="9" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="Ts" name="Halberd" cost="10" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x3" range="0" weight="15" type="P&amp;S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="tc" name="Halfspear" cost="1" category="Simple Weapons-Melee" size="Medium" damage="1d6" critical="x3" range="20" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="d" name="Hammer, gnome hooked" cost="20" category="Exotic Weapons-Melee" size="Medium" damage="1d6/1d4" critical="x3/x4" range="0" weight="6" type="B&amp;P" >
+<description >for proper treatment, read PH pg 101</description >
+</weapon>
+<weapon mod="0" fn="t" name="Hammer, light" cost="1" category="Martial Weapons-Melee" size="Small" damage="1d4" critical="x2" range="20" weight="2" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Handaxe" cost="6" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x3" range="0" weight="5" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Javelin" cost="1" category="Simple Weapons-Ranged" size="Medium" damage="1d6" critical="x2" range="30" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Kama" cost="2" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="2" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Kama, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Katana" cost="400" category="Exotic Weapons-Melee" size="Large" damage="1d10" critical="19-20/x2" range="0" weight="6" type="S" >
+    <description >Always masterwork</description >
+</weapon>
+<weapon mod="0" fn="" name="Katana, used 2 handed" cost="400" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="19-20/x2" range="0" weight="6" type="S" >
+    <description >Always masterwork</description >
+</weapon>
+<weapon mod="0" fn="" name="Kukri" cost="8" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="18-29/x2" range="0" weight="3" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oTR" name="Kusari-gama" cost="10" category="Asian Weapons-Melee" size="Medium" damage="1d6" critical="x2" range="0" weight="3" type="S" >
+    <description >like a spiked chain</description >
+</weapon>
+<weapon mod="0" fn="Cr" name="Lance, heavy" cost="10" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="C" name="Lance, light" cost="6" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x3" range="0" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Laser pistol" cost="-1" category="Futuristic Weapons-Ranged" size="Small" damage="2d10" critical="x2" range="100" weight="2" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Laser rifle" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="3d20" critical="x2" range="200" weight="7" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Longbow" cost="75" category="Martial Weapons-Ranged" size="Large" damage="1d8" critical="x3" range="100" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Longbow, composite" cost="100" category="Martial Weapons-Ranged" size="Large" damage="1d8" critical="x3" range="110" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="rc" name="Longspear" cost="5" category="Martial Weapons-Melee" size="Large" damage="1d8" critical="x3" range="0" weight="9" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Longsword" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="19-20/x2" range="0" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Mace, heavy" cost="12" category="Simple Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="12" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Mace, light" cost="5" category="Simple Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="6" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Morningstar" cost="8" category="Simple Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="8" type="B&amp;P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Musket" cost="500" category="Renaissance Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="150" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Net" cost="20" category="Exotic Weapons-Ranged" size="Medium" damage="0" critical="0" range="10" weight="10" type="-" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Nunchaku" cost="2" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="2" type="S" >
+<description >
+</description >
+</weapon>
+<weapon mod="0" fn="m" name="Nunchaku, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pick, heavy" cost="8" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="x4" range="0" weight="6" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pick, light" cost="4" category="Martial Weapons-Melee" size="Small" damage="1d4" critical="x4" range="0" weight="4" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pistol" cost="250" category="Renaissance Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="50" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pistol, automatic" cost="-1" category="Modern Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="150" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pistol, revolver" cost="-1" category="Modern Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="100" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="d" name="Quarterstaff" cost="0" category="Simple Weapons-Melee" size="Large" damage="1d6" critical="x2" range="0" weight="4" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="ro" name="Ranseur" cost="10" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x3" range="0" weight="15" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Rapier" cost="20" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="18-20/x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Rifle, automatic" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="250" weight="12" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Rifle, repeater" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="200" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="s" name="Sap" cost="1" category="Martial Weapons-Melee" size="Small" damage="1d6s" critical="x2" range="0" weight="3" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Scattergun" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="*" critical="*" range="10" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Scimitar" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="18-20/x2" range="0" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Scythe" cost="18" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x4" range="0" weight="12" type="P&amp;S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Shortbow" cost="30" category="Martial Weapons-Ranged" size="Medium" damage="1d6" critical="x3" range="60" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Shortbow, composite" cost="75" category="Martial Weapons-Ranged" size="Medium" damage="1d6" critical="x3" range="70" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="tc" name="Shortspear" cost="2" category="Simple Weapons-Melee" size="Large" damage="1d8" critical="x3" range="20" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Shuriken" cost="1" category="Exotic Weapons-Ranged" size="Tiny" damage="1" critical="x2" range="100" weight="0" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Siangham" cost="3" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Siangham, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Sickle" cost="6" category="Simple Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="3" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Sling" cost="1d4" category="Simple Weapons-Ranged" size="Small" damage="1d4" critical="x2" range="50" weight="0" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="s" name="Strike, unarmed (med)" cost="0" category="Simple Weapons-Melee" size="Unarmed" damage="1d3" critical="x2" range="0" weight="0" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="s" name="Strike, unarmed (small)" cost="0" category="Simple Weapons-Melee" size="Unarmed" damage="1d2" critical="x2" range="0" weight="0" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Sword, bastard" cost="35" category="Exotic Weapons-Melee" size="Medium" damage="1d10" critical="19-20/x2" range="0" weight="10" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Sword, bastard used 2 handed" cost="35" category="Martial Weapons-Melee" size="Medium" damage="1d10" critical="19-20/x2" range="0" weight="10" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Sword, short" cost="10" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="19-20/x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="d" name="Sword, two-bladed" cost="100" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="19-20/X2" range="0" weight="30" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="ct" name="Trident" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="10" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="dc" name="Urgrosh, dwarven" cost="50" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d6" critical="x3" range="0" weight="15" type="S&amp;P" >
+<description >see PH pg 103 for proper treatment </description >
+</weapon>
+<weapon mod="0" fn="m" name="UnArmed(Monk Med)" cost="0" category="Exotic Weapons-Melee" size="Unarmed" damage="Monk Med" critical="x2" range="0" weight="00" type="S&amp;P" >
+<description >An unArmed strike does more dmg as a monk</description >
+</weapon>
+<weapon mod="0" fn="m" name="UnArmed(Monk Small)" cost="0" category="Exotic Weapons-Melee" size="Unarmed" damage="Monk Small" critical="x2" range="0" weight="00" type="S&amp;P" >
+<description >An unArmed strike does more dmg as a monk</description >
+</weapon>
+<weapon mod="0" fn="" name="Wakizashi" cost="300" category="Asian Weapons-Melee" size="Small" damage="1d6" critical="19-20/x2" range="0" weight="3" type="S" >
+<description >Always masterwork</description >
+</weapon>
+<weapon mod="0" fn="" name="Waraxe, dwarven" cost="30" category="Exotic Weapons-Melee" size="Medium" damage="1d10" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Waraxe, dwarven used 2handed" cost="30" category="Martial Weapons-Melee" size="Medium" damage="1d10" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Warhammer" cost="12" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="8" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oT" name="Whip" cost="1" category="Exotic Weapons-Ranged" size="Small" damage="1d2s" critical="x2" range="15" weight="2" type="S" >
+<description >range15, maxrange15, no penalties to max range; ineffective Vs armor or +3 natural adjustment PH pg 104 </description >
+</weapon>
+</weapons>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd35/feats.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2939 @@
+[Creature Type] Trainer^AE^73^You are skilled at training a particular type  of creature.
+Aberrant Dragonmark^ECS^47^Although you are not a recognized member of  one of the dragonmarked families, you have manifested a dragonmark.
+Aberration Banemagic^LoM^178^You can cast spells that do additional damage  to aberrations.
+Aberration Blood^LoM^178^One of your ancestors was an aberration and  has passed the taint of its aberrant physiology down through the generations  to you.
+Aberration Wild Shape^LoM^178^Thanks to your heritage, you have learned to  channel your inhuman bloodline into your shapeshifting power.
+Ability Focus^MM^303^Choose one of the creature's special attacks.  This attack becomes more potent than normal.
+Ability Focus^MM2^18^One of the creature's special attacks is more  potent than normal.
+Ability Focus^MM3^206^The special attack of a creature with this feat  is more potent than normal.
+Ability Focus^SS^30^Choose one of your spell-like abilities. This  attack becomes much more potent than normal.
+Able Learner^RD^150^You have great aptitude for learning.
+Able Sniper^RW^148^You are accomplished at remaining unseen when  you're sniping with a ranged weapon.
+Accurate Jaunt^UA^92^You have an instinctive sense of interplanar  travel.
+Acheron Flurry^PlH^37^You master the secret technique developed by  Acheron-native special forces of limiting a foe's options in hand-to-hand  combat.
+Acrobatic^PH^89^You have excellent body awareness and coordination.
+Acrobatic^SaS^38^You have excellent body awareness and coordination.
+Action Boost^ECS^47^You have the ability to alter your luck drastically  in dire circumstances.
+Action Surge^ECS^50^By spending 2 action points, you can perform  an additional action in a round.
+Adamantine Body^ECS^50^At the cost of mobility, a warforged character's  body can be crafted with a layer of adamantine that provides formidable  protective armor and some damage reduction.
+Adamantine Body^MM3^192^At the cost of mobility, a warforged character's  body can be crafted with a layer of adamantine that provides formidable  protective armor and some damage reduction.
+Adamantine Body^RE^118^At the cost of mobility, your warforged body  can be crafted with a layer of adamantine that provides formidable protective  armor and some damage reduction.
+Additional Magic Item Space^EL^50^You can wear more magic items.
+Adroit Flyby Attack^Dr^67^You can make flyby attacks and get out of reach  quickly.
+Aerenal Beastmaster^RE^105^As an elf of Aerenal, you consider baboons sacred  animals and they serve you obediently.
+Aerial Reflexes^RW^148^Your aerial agility allows you to avoid dangerous  effects while airborne.
+Aerial Superiority^RW^148^You can use your flying ability to gain an advantage  against landbound foes or airborne foes that you can outmaneuver.
+Aftersight^Rac^160^You have a trace of the Sight in your blood,  which enables you to pick up echoes of the past, both wondrous and terrible.
+Agile^PH^89^You are particularly flexible and poised.
+Agile Athlete^RW^148^You rely on your agility to perform athletic  feats, rather than brute strength.
+Agile Tyrant^LoM^44^A creature with this feat develops longer, more  flexible eyestalks than its kin. This extra flexibility allows it to bring  additional eye rays to bear against its foes.
+Agony Touch^Gh^28^Choose one physical ability score. When you  touch a creature, you can deal damage to this ability score.
+Air Heritage^PlH^37^You are descended from creatures native to the  Elemental Plane of Air.
+Alertness^PH^89^You have finely tuned senses.
+Aligned Attack^XPH^41^Your melee or ranged attack overcomes your opponent's  alignment-based damage reduction and deals additional damage.
+Allied Defense^ShS^19^You are good at protecting nearby allies.
+Alluring^SaS^38^Others have an inexplicable urge to believe  your every word.
+Altitude Adaptation^Fr^45^Your body adapts quickly to changes in altitude,  preventing you from suffering as much from altitude sickness.
+Anarchic Heritage^PlH^38^You are descended from creatures native to the  planes of chaos.
+Ancestral Guidance^RE^105^The spirit of your patron ancestor guides your  hands and thoughts in times of trouble.
+Ancestral Knowledge^RS^136^You have a strong connection to the ancestors  of your clan, giving you understanding and knowledge beyond the mortal realms.
+Ancestral Relic^BE^39^You own an ancestral heirloom and can invest  it with increasing power.
+Ancestral Spirit^Rac^161^You have ties to the long-dead spirit of one  of your clan's ancestors, who whispers ancient words of wisdom into your  mind in times of need.
+Animal Affinity^PH^89^You are good with animals.
+Animal Control^MW^20^You can channel the power of nature to gain  mastery over animal creatures.
+Animal Defiance^MW^20^You can channel the power of nature to drive  off animals.
+Animal Friend^BE^41^Animals respond favorably to the aura of goodness  that exudes from you.
+Animal Friends^Rac^161^Your ability to speak with animals has  allowed you to befriend an animal as a permanent ally.
+Ankheg Tribe Ambush^ShS^19^You have learned how to hide and spring to attack,  much like the ankhegs that roam the plains where you hunt.
+Antipsionic Magic^XPH^41^Your spells are more potent when used against  psionic characters and creatures.
+Anvil of Thunder^CW^112^You have mastered the style of fighting with  hammer and axes at the same time, and have learned to deal thunderous blows  with this unique pairing of weapons.
+Appraise Magic Value^CAd^103^Your ability to determine an item's worth and  your knowledge of magic allow you to determine the exact properties of a  magic item without the use of the identify spell or similar magic.
+Apprentice^DMG2^176^A character with this feat has apprenticed himself  to a master in order to speed his learning and bolster his skills.
+Aquatic Shot^Sto^90^You have developed the technique of firing a  ranged weapon into or through the water with better accuracy than normal,  striking at just the right angle to allow it to slice through the obstruction  with precision.
+Aquatic Spellcasting^LoM^178^You know how to cast spells that work equally  well in or out of water.
+Arachnid Rider^Rac^161^You are trained in the art of employing spiders  as steeds.
+Arcane Defense^CAr^73^Choose a school of magic, such as illusion.  You can resist spells from that school better than normal.
+Arcane Defense^TB^38^Choose a school of magic. You can resist spells  from that school better than normal.
+Arcane Disciple^CD^79^Choose a deity, and then select a domain available  to clerics of that deity. You can learn to cast spells associated with that  domain as arcane spells.
+Arcane Insight^RD^154^By immersing yourself in the teachings of Boccob,  you have unearthed magical secrets and gained special insight into arcane  spellcasting.
+Arcane Manipulation^LE^6^You are learned in the arcane ways of Netheril,  where masters of magic once molded and shaped arcane energy to their own  will.
+Arcane Mastery^CAr^73^You are quick and certain in your efforts to  defeat the arcane defenses and spells of others.
+Arcane Preparation^CAr^73^You can prepare an arcane spell ahead of time,  just as a wizard does.
+Arcane Preparation^FRCS^33^You can prepare an arcane spell ahead of time,  just as a wizard does.
+Arcane Preparation^PG^32^You can prepare an arcane spell ahead of time,  just as a wizard does.
+Arcane Preparation^TB^38^You can prepare an arcane spell ahead of time,  just as a wizard does.
+Arcane Schooling^FRCS^33^In your homeland, all who show some skill at  the Art may receive training as a wielder of magic.
+Arcane Schooling^PG^33^In your homeland, all who show some skill at  the Art may receive training as arcane spellcasters.
+Arcane Strike^CW^96^You can channel arcane energy into your melee  attacks.
+Arcane Transfiguration^LE^6^Drawing upon forgotten lore, you broaden your  arcane studies and master a school of magic previously prohibited to you.
+Arctic Adaptation^Rac^161^You have adapted to the snowbound environment  of the arctic reaches of Faerun.
+Area Attack^SS^30^You can wield improvised weapons to attack several  spaces at once.
+Armor Proficiency (Heavy)^PH^89^You are proficient with heavy armor.
+Armor Proficiency (Light)^PH^89^You are proficient with light armor.
+Armor Proficiency (Medium)^PH^89^You are proficient with medium armor.
+Armor Skin^CW^151^Your skin becomes like armor.
+Armor Skin^EL^50^Your skin becomes like armor.
+Art of Fascination^OA^60^You claim descent from Kakita Wayozu, whose  art was so great it is said that she helped create an alternate world.
+Arterial Strike^CW^96^Your sneak attacks target large blood vessels,  leaving wounds that cause massive blood loss.
+Arterial Strike^SaS^38^Your sneak attacks target large blood vessels,  leaving wounds that cause massive blood loss.
+Artic Priest^Fr^46^You can swap out prepared spells to cast spell  to aid in exploring and surviving in frostfell areas.
+Artist^FRCS^33^You come from a culture in which the arts, philosophy,  and music have a prominent place in society.
+Artist^OA^61^You claim descent from Doji, who was known as  a creator of culture and civilization.
+Artist^PG^33^Your people are renowned for their skill at  story and song.
+Ascetic Hunter^CAd^105^You have gone beyond the bounds of your monastic  training to incorporate new modes of bringing the unlawful to justice.
+Ascetic Knight^CAd^105^You belong to a special order of religious monks  that teaches its adherents that self-enlightenment and honorable service  grow from the same well of purity.
+Ascetic Magic^CAd^105^You practice an unusual martial art that mixes  self-taught spellcasting and melee attacks to great effect.
+Ascetic Rogue^CAd^106^You have gone beyond the bounds of your monastic  training to incorporate new modes of stealthy combat.
+Ashbound^ECS^50^You have been trained in the druidic traditions  of the Ashbound, seeing yourself as one of nature's avengers.
+Assume Supernatural Ability^SS^30^You learn to use a supernatural ability of an  assumed form.
+Athletic^PH^89^You have a knack for athletic endeavors.
+Athletic^SaS^38^You're physically fit and adept at outdoor sports.
+Attention to Detail^OA^61^You are descended from Akodo's advisor Ikoma  -- a historian, judge, and storyteller.
+Attune Gem^Mag^21^You can magically imbue gems to hold a spell  until triggered.
+Attune Magic Weapon^ECS^50^Through your study of magic weapons, you have  become adept at eking every advantage out of their enhanced qualities.
+Augment Healing ^CD^79^You can increase your healing ability.
+Augment Summoning^Mag^21^Your summoned creatures are better than normal.
+Augment Summoning^PH^89^Your summoned creatures are more powerful than  normal.
+Augment Summoning^TB^39^Your summoned creatures are more powerful than  normal.
+Augmented Alchemy^CAd^191^You can create alchemical items and substances  that are much more powerful than normal.
+Augmented Alchemy^EL^50^You can create alchemical items and substances  that are much more powerful than normal.
+Auspicious Marking^RS^136^Your [goliath] skin patterns indicate that fate  has marked you for greatness, and the patterns shift slowly to take new  forms.
+Automatic Quicken Spell^CAr^191^You can cast any of your lesser spells with  a moment's thought.
+Automatic Quicken Spell^EL^50^You can cast any of your lesser spells with  a moment's thought.
+Automatic Silent Spell^CAr^191^You can cast any of your lesser spells silently.
+Automatic Silent Spell^EL^51^You can cast any of your lesser spells silently.
+Automatic Still Spell^CAr^191^You can cast any of your lesser spells without  gestures.
+Automatic Still Spell^EL^51^You can cast any of your lesser spells without  gestures.
+Autonomous^XPH^41^You have a knack for psionic self-sufficiency.
+Awaken Frightful Presence^Dr^67^You gain frightful presence.
+Awaken Spell Resistance^Dr^67^You gain spell resistance.
+Awesome Blow^MM^303^The creature can choose to deliver blows that  send its smaller opponents flying like bowling pins.
+Awesome Blow^MM3^206^A creature with this feat can choose to deliver  blows that send its smaller opponents flying like bowling pins.
+Axeshield^Und^24^You know how to defend yourself with a battleaxe.
+Axespike^RS^137^You have mastered the art of fighting in spiked  armor while wielding a greataxe. You blend greataxe blows and armor spike  attacks into one constant, deadly attack form.
+Axethrower^PG^33^You have learned how to hurl weapons to deadly  effect.
+Axiomatic Heritage^PlH^38^You are descended from creatures native to the  planes of law.
+Axiomatic Strike^CW^96^You can turn your fist into an instrument of  law.
+Axiomatic Strike^PG^135^Your attacks deal incredible damage to chaotic  creatures.
+Azerblood^Rac^161^You are descended from the shield dwarves of  Clan Azerkyn, who once ruled the Adamant Kingdom of Xothaerin beneath western  Amn. The blood of the azer runs thick in your veins.
+Baleful Moan^LM^24^Your hollow cry strikes fear into the hearts  of the living.
+Ballista Proficiency^HB^96^You have trained in ballista operation.
+Bane of Enemies^EL^51^Your attacks deal great damage to your favored  enemies.
+Bane of the Unclean^LoM^44^A creature with this feat hates aberrant beholders  so strongly that it gains bonuses when fighting them.
+Barbed Stinger^SK^144^Your stinger is unusually difficult to dislodge.
+Batrider^Rac^161^You are highly skilled in the art of flying  dire bats, a common form of transportation among the shield dwarves of the  Far Hills.
+Battle Caster^CAr^75^Building on your existing training allows you  to avoid the chance of arcane spell failure when you wear armor heavier  than normal.
+Battle Casting^RW^148^You have a knack for staying out of harm's way  when casting spells.
+Battle Hardened^RS^137^Your extensive battle experience has left you  incredibly calm and composed, even in the heat of battle.
+Battle Jump^UE^42^You know how to launch a devastating attack  from above by dropping onto your opponent.
+Battlefield Inspiration^MH^25^You inspire courage in your allies.
+Battleshifter Training^RE^116^Your shifter fighting instincts grant you a  sophisticated blend of defensive techniques and controlled attacks.
+Bear Fang^CW^112^You have mastered the fierce style of fighting  with axe and dagger at the same time.
+Beast Companion^EL^51^You can befriend a beast.
+Beast Shape^ECS^50^You call upon the power of your beast totem  to physically change your form.
+Beast Totem^ECS^51^In the druidic custom of your people, you have  claimed a kind of magical beast as your totem -- a patron, protector, and  source of strength.
+Beast Wild Shape^EL^51^You can wild shape into magical beast form.
+Beasthide Elite^ECS^51^Your shifter trait improves.
+Beckon the Frozen^Fr^47^Creatures you summon are infused with cold energy  and have the cold subtype.
+Bestial Hide^LoM^179^Your skin is thicker, scalier, or furrier than  normal.
+Bind Elemental^ECS^51^You can craft magic items that use bound elementals  for special effects, including weapons, armor, airships, and elemental galleons.
+Black Lore of Moil^CAr^75^Your study of the sinister knowledge and spellcasting  techniques of the long-dead Nightlords of Moil makes your necromancy spells  especially potent.
+Blackwater Invocation^Sto^91^You can call upon negative energy to infuse  normal water around you, transforming it into the dark, cold water found  at the bottom of the deepest ocean trenches.
+Bladebearer of the Valenar^RE^107^Your extensive training makes you especially  adept with the curved blades of the Valenar.
+Bladeproof Skin^UA^92^Your skin has a degree of protection from even  the sharpest edge.
+Blazing Berserker^Sa^49^When you enter your rage, your body becomes  infused with fire.
+Blessed by Tem-Et-Nu^Sa^49^Tem-Et-Nu has marked you as having an important  destiny in her temple.
+Blessed of the Seven Sisters^PG^176^As a result of a personal connection to one  of the Seven Sisters, you have a taste of Mystra's special favor.
+Blind-Fight^PH^89^You know how to fight in melee without being  able to see your foes.
+Blinding Speed^EL^51^You can trigger short bursts of great speed.
+Blindsense^CAd^114^You can sense creatures that you cannot see.
+Blindsight^MW^21^Your senses are as keen as the bat's.
+Blindsight, 5-Foot Radius^SF^5^You sense opponents in the darkness.
+Blindsight, 5-Foot Radius^DD^49^The deity senses opponents in the darkness.
+Block Arrow^HB^96^You can block incoming arrows with your shield.
+Blood of the Warlord^Rac^161^You can influence a large number of orcs.
+Blood Sorcerer^OA^61^You are descended from Yogo, the Scorpion shugenja  who was the first guardian of the Black Scrolls of Fu Leng.
+Blooded^FRCS^33^Enemies find it difficult to catch you off guard.
+Blooded^PG^35^You know what it means to fight for your life,  and you understand the value of quick wits and quicker reactions when blades  are bared and deadly spells are chanted.
+Bloodline of Fire^FRCS^34^You are descended from the efreet who ruled  Calimshan for two millennia.
+Bloodline of Fire^PG^35^You are descended from the efreet who ruled  Calimshan long ago.
+Bloodsoaked Intimidate^CR^17^Your bloody and vicious approach to combat makes  you a fearsome foe.
+Blowhard^SS^31^You can blow targets over with your breath.
+Boar's Ferocity^CD^79^You can continue fighting even at the brink  of death.
+Body Fuel^XPH^41^You can expand your power point total at the  expense of your health.
+Body Pouch^SK^144^You can open a cavity in your body without harm  to yourself and use it to carry or conceal items or creatures.
+Bolster Resistance^LM^25^Undead you raise or create are more resistant  to turning than normal.
+Bonus Breath^SS^31^You can use your breath weapons one more time  per day than you normally could.
+Bonus Domain^CD^89^You have access to one additional domain of  spells.
+Bonus Domain^EL^51^You have access to one additional domain of  spells.
+Boomerang Daze^RE^108^You can daze the targets of your boomerang attacks.
+Boomerang Ricochet^RE^108^You can strike up to two foes with a single  boomerang throw.
+Boost Construct^XPH^43^Your astral constructs have more abilities.
+Boost Spell Resistance^BV^47^By making a deal with an evil power, the character  makes himself even more resistant to magic.
+Boost Spell-Like Ability^BV^47^One of the creature's spell-like abilities is  harder to resist than it otherwise would be.
+Born Duelist^OA^61^You claim descent from Mirumoto, one of the  first two samurai to join Togashi in his meditative retreat.
+Born Flyer^RW^148^You can fly as though born to do it.
+Born of the Three Thunders^CAr^76^You have learned to marry the power of lightning  and thunder in your electricity and sonic spells.
+Bowslinger^Und^24^You can ready ranged weapons surprisingly quickly.
+Brachiation^CAd^106^You can swing through trees like a monkey.
+Brachiation^MW^21^You move through trees like a monkey.
+Breadth of Knowledge^UA^92^Your time spent plumbing the depths of magic  knowledge has resulted in a treasure trove of obscure facts.
+Breathing Link^Rac^161^You can allow a person adjacent to you to breathe  water.
+Breathing Link^Sto^92^You can allow a person adjacent to you to breathe  water.
+Brew Potion^PH^89^You can create potions, which carry spells within  themselves.
+Bright Sigil^RD^150^You have established a greater degree of control  over your sigils. When you concentrate, you can emit strong illumination  from the glowing symbols that surround your head.
+Brutal Throw^CAd^106^You have learned how to hurl weapons to deadly  effect.
+Brute Fighting^RE^116^Your extensive training with two-handed weapons  is revealed through brutally effective tactics.
+Bullheaded^FRCS^34^The stubbornness and determination of your kind  is legendary.
+Bullheaded^PG^37^The stubbornness and determination of your kind  are legendary.
+Bulwark of Defense^EL^51^Your defensive stance bonuses increase.
+Burrow Friend^RS^137^Your natural rapport with burrowing mammals  improves.
+Burrowing Power^XPH^43^Your powers sometimes bypass barriers.
+Calishite Elementalist^Rac^161^You are a student of the Calishite tradition  of elemental magic and have mastered its mysterious lore. You may choose  to specialize in air magic or fire magic.
+Call of the Undying^RE^108^You call upon the power of the Undying Court  to instantly recall a previously cast spell.
+Caravanner^Rac^162^You are skilled at leading caravans along established  trade routes.
+Catfolk Pounce^RW^148^You can rush unaware foes and deliver several  attacks before they have a chance to respond.
+Caustic Adaptation^Und^24^Long have your ancestors hunted and been hunted  in the depths. Natural selection has given your blood an unpalatable, acidic  quality.
+Cavalry Charger^CW^108^Fighting from the back of a steed is second  nature to you.
+Caver^Und^24^You are knowledgeable about the secrets of the  subterranean world and wise in its ways.
+Celestial Bloodline^Rac^162^Some of your latent abilities have matured.
+Celestial Familiar^BE^41^As long as you are able to acquire a new familiar,  you may receive a celestial as a familiar.
+Celestial Heritage^PlH^38^You are descended from creatures native to the  Upper Planes
+Celestial Mount^BE^42^Your special mount is a true creature of the  heavens.
+Celestial Summoning Specialist^PlH^38^You can select from a larger number of options  when summoning good creatures.
+Centaur Trample^RW^148^You have trained to use your large body and  unique physiology against your foes. You have learned how to knock down  opponents and ride over them in combat.
+Chain Power^XPH^44^You can manifest powers that arc to hit other  targets in addition to the primary target.
+Chain Spell^CAr^76^You can cast spells that arc to other targets  in addition to your primary target.
+Chain Spell^TB^39^You can cast spells that arc to other targets  in addition to your primary target.
+Chakram Ricochet^CR^17^You can hurl a chakram so that it strikes two  enemies instead of one.
+Chameleon Hide^SK^144^You can alter the hue of your scales to match  the surrounding terrain.
+Channel Charge^LE^7^You can power a charged magic item with your  own magical ability.
+Channel Legacy^WL^13^You can call upon the hidden strength within  your legacy item to empower yourself for a single spectacular effort.
+Channeled Rage^RD^150^You can focus your rage to counter charms and  compulsions.
+Chant of Fortitude^CAd^113^You can channel the power of your bardic music  to sustain your allies, allowing them to function even after receiving wounds  that would cause others to falter.
+Chaotic Mind^XPH^44^The turbulence of your thoughts prevents others  from gaining insight into your actions.
+Chaotic Rage^EL^51^Your rage is particularly damaging to lawful  creatures.
+Chariot Archery^SF^78^You are skilled at using ranged weapons from  a chariot.
+Chariot Charge^SF^79^You are skilled at charging with your chariot.
+Chariot Combat^SF^78^You are skilled in chariot combat.
+Chariot Sideswipe^SF^79^You are skilled at using your chariot's scythe  blades against foes.
+Chariot Trample^SF^78^You are trained in using your chariot to knock  down opponents.
+Charlatan^SaS^38^You're adept at fooling people. You know how  to tell them just what they want to hear.
+Charm Immunity^SK^145^You are immune to charm effects.
+Charm Resistance^SK^145^You can resist charm effects better than you  otherwise could.
+Cheetah Tribe Sprint^ShS^19^You have learned the secret of lightning-fast  running from the cheetah that roams the plains where you live.
+Cheetah's Speed^CD^79^You can run with the speed of the cheetah.
+Child of Winter^ECS^51^You are trained in the druidic traditions of  the Children of Winter, an Eldeen Reaches sect that embraces death and decay.
+Chink in the Armor^SaS^38^You are an expert at slipping a weapon between  armor plates or into seams.
+Choke Hold^OA^61^You have learned the correct way to apply pressure  to render an opponent unconscious.
+Chondathan Missionary^Rac^162^Your training has emphasized spells that help  you spread the word of your faith.
+Chosen of Iborighu^Fr^47^You gain features that identify you as an ally  to the church of Iborighu and grant you supernatural qualities.
+Chosen Weapon Specialization^PG^135^You deal more damage than normal when wielding  your deity's chosen weapon.
+Circle Kick^SF^5^You kick multiple opponents with the same attack  action.
+Circle Magic^Gh^29^You know how to use your connection to Galaedros  the Wood God to channel magical power to another spellcaster of your faith.
+City Slicker^RD^150^You are very familiar with city life and the  inner workings of your hometown.
+Clan Prestige^RS^137^Your actions have brought you some measure of  fame and respect from your clan, whether from battle prowess or years of  service to the clan.
+Cleave^PH^92^You can follow through with powerful blows.
+Clever Wrestling ^CW^97^You have a better than normal chance to escape  or wriggle free from a big creature's grapple or pin.
+Clever Wrestling^Dr^103^You have a better than normal chance to escape  or wriggle free from a big creature's grapple or pin.
+Clever Wrestling^MW^22^You have a better than normal chance to escape  or wriggle free from a big creature's grapple or pin.
+Clever Wrestling^Sto^92^You have a better than normal chance to escape  or wriggle free from a big creature's grapple or pin.
+Cliffwalk Elite^ECS^52^Your shifter trait improves.
+Cliffwalk Elite^RE^113^Your cliffwalk shifter trait improves.
+Climb Like an Ape^CAd^114^You can improve your climbing ability.
+Clinging Breath^Dr^67^Your breath weapon clings to creatures and continues  to affect them in the round after you breath.
+Cloak Dance^XPH^44^You are skilled at using optical tricks to make  yourself seem to be where you are not.
+Closed Mind^XPH^44^Your mind is better able to resist psionics  than normal.
+Close-Quarters Fighting^Rac^162^You are skilled at resisting grapple attacks  from creatures that usually grapple opponents.
+Close-Quarters Fighting^CW^97^You are skilled at fighting at close range and  resisting grapple attempts.
+Close-Quarters Fighting^Dr^103^You are skilled at fighting at close range and  resisting grapple attempts.
+Close-Quarters Fighting^SF^5^You are skilled at fighting at close range and  resisting grapple attacks.
+Cobra Head^SK^145^You can extend the skin of your neck into a  cobra hood.
+Cold Endurance^Fr^47^You can exist with ease in low-temperature environments.
+Cold Focus^Fr^47^Your cold spells are more potent than normal.
+Cold Iron Tracery^RE^119^Cold-forged iron that runs through your body  allows you to overcome the supernatural defenses of certain creatures and  protecting against some magical attacks.
+Cold Spell Specialization^Fr^47^You do additional damage with cold spells.
+Collegiate Wizard^CAr^181^You have undergone extensive training in a formal  school for wizards.
+Colossal Wild Shape^EL^52^You can wild shape into animals of Colossal  size.
+Combat Archery^CW^151^You can fire a bow in melee safely.
+Combat Archery^EL^52^You can fire a bow in melee safely.
+Combat Brute^CW^110^You employ strength and leverage to great effect  in battle.
+Combat Casting^PH^92^You are adept at casting spells in combat.
+Combat Expertise^PH^92^You are trained at using your combat skill for  defense as well as offense.
+Combat Insight^CW^151^Your keen intellect allows you to place melee  attacks where they will deal the most damage.
+Combat Intuition^CAd^106^Your keen understanding of your opponent's moves  and your instinctive feel for the flow of combat enable you to shrewdly  assess your opponent's combat capabilities.
+Combat Manifestation^XPH^44^You are adept at manifesting powers in combat.
+Combat Reflexes^PH^92^You can respond quickly and repeatedly to opponents  who let their defenses down.
+Communicator^CAr^76^You possess a magical understanding of the essence  of language.
+Complementary Insight^RD^150^You get more out of having skills that work  well together.
+Conductivity^UA^92^You have crude control over electricity effects  near you.
+Confound the Big Folk^RW^153^You excel when battling foes bigger than you  are.
+Consecrate Spell^BE^42^You can imbue your spells with the raw energy  of good, by the grace of a celestial power.
+Consecrate Spell^CD^79^You can imbue your spells with the raw energy  of good.
+Consecrate Spell Trigger^BE^42^You can channel holy power through a spell trigger  item, such as a wand or staff.
+Consecrate Spell-Like Ability^BE^42^You can channel holy power into your spell-like  abilities.
+Construct Lock^RE^119^Your knowledge of construct nature allows you  to deal extra damage to or even immobilize such foes.
+Contagious Paralysis^LM^25^Your paralyzing attack is contagious.
+Control Visage^Gh^29^Your ghost body is shaped as if you were alive  and unharmed, and you can control what your ghost body appears to wear.
+Controlled Immolation^UA^92^If you catch on fire, the flames don't hurt  you.
+Controlled Respiration^SS^31^You can stay out of water longer than you otherwise  could.
+Cool Head^OA^61^You are descended from the great diplomat Ide,  who was chosen to be the voice of Shinjo in all dealings with strangers.
+Cooperative Spell^CAr^76^You can cast spells to greater effect in conjunction  with the same spell cast by another individual.
+Cooperative Spell^TB^39^You can cast spells to greater effect in conjunction  with the same spell cast by another individual.
+Coordinated Shot^HB^96^You are extraordinarily talented at making ranged  attacks past your allies.
+Coordinated Strike^RW^149^You and your animal companion or special mount  can coordinate your melee attacks to gain an advantage in combat.
+Cormanthyran Moon Magic^LE^7^You have mastered the ancient elven techniques  of drawing power from Sehanine Moonbow's light.
+Cornered Rat^DCS^85^You can go from piteous groveling to a murderous  fury in the blink of an eye.
+Corpse Malevolence^Gh^29^You can possess and animate dead bodies.
+Corpsecrafter^LM^25^Undead you raise or create are tougher than  normal.
+Corrupt Arcane Studies^Gh^29^You have dabbled in strange magic that has increased  your power but adversely affected your mind.
+Corrupt Spell^BV^47^The character can transform one of her spells  into a thing of evil due to a deal she makes with an evil power.
+Corrupt Spell^CD^79^You can transform one of your spells into an  evil version of itself.
+Corrupt Spell^CR^17^You can transform one of your spells into a  thing of evil due to a deal you make with an evil power.
+Corrupt Spell-Like Ability^BV^48^One of the creature's spell-like abilities is  powered by evil. A dark pact provides the creature with unholy energy.
+Corrupted Wild Shape^LM^25^You have learned to use the necromantic energy  that powers your undead form to overcome the inability of undead creatures  to wild shape.
+Corrupting Touch^Gh^29^Your touch can damage creatures.
+Cosmopolitan^FRCS^34^Your exposure to the thousand forking paths  of the city has taught you things you ordinarily would never have uncovered.
+Cosmopolitan^PG^37^You've been lied to more times than you can  count.
+Cougar's Vision^CAd^114^You can see in the dark like a cat.
+Courageous Rally^HB^97^You can rally demoralized foes with your bardic  music.
+Courteous Magocracy^FRCS^34^You were raised in a land where mighty wizards  order affairs.
+Cover Your Tracks^ShS^20^You are good at masking your route, making it  difficult for others to track you.
+Craft Aboleth Glyph^LoM^22^An aboleth with this feat can create magic glyphs  that store spells or have specialized effects of their own.
+Craft Alchemical Item^UA^99^You are capable of creating alchemical items  and substances.
+Craft Cognizance Crystal^XPH^44^You can create psionic cognizance crystals  that store power points.
+Craft Construct^MM^303^The creature can create golems and other magic  automatons that obey its orders.
+Craft Construct^MM3^206^A creature with this feat can create golems  and other magic automatons that obey its orders
+Craft Contingent Spell^CAr^77^You know how to attach semipermanent spells  to a creature and set them to activate under certain conditions.
+Craft Contingent Spell^UE^42^You know how to create contingent spells, which  are semipermanent spells that can be 'worn' and activated under certain  conditions.
+Craft Crystal Weapon^OA^61^You can create magic weapons from Kuni crystal,  which is deadly to creatures of the Shadowlands.
+Craft Dorje^XPH^44^You can create slender crystal wands called  dorjes that manifest powers when charges are expended.
+Craft Epic Magic Arms and Armor^EL^52^You can craft magic arms and armor of epic power.
+Craft Epic Rod^EL^52^You can craft magic rods of epic power.
+Craft Epic Staff^EL^52^You can craft magic staffs of epic power.
+Craft Epic Wondrous Item^EL^52^You can craft wondrous items of epic power.
+Craft Magic Arms and Armor^PH^92^You can create magic weapons, armor, and shields.
+Craft Masterwork Armor^UA^99^You are trained in the creation of fine armor  and shields.
+Craft Masterwork Ranged Weapon^UA^99^You are trained in the creation of fine ranged  weapons and ammunition.
+Craft Masterwork Weapon^UA^99^You are trained in the creation of fine melee  and thrown weapons.
+Craft Psicrown^XPH^44^You can create psicrowns, which have multiple  psionic effects.
+Craft Psionic Arms and Armor^XPH^44^You can create psionic weapons, armor, and shields.
+Craft Psionic Construct^XPH^45^You can create golems and other psionic automatons  that obey your orders.
+Craft Psionic Seal^LoM^69^A creature with this feat can create psionic  glyphs or symbols that hold spells or psionic powers until triggered.
+Craft Rod^PH^92^You can create magic rods, which have varied  magical effects.
+Craft Rune Circle^RS^137^You can create rune circles, stationary magic  items that hold a variety of spells and effects.
+Craft Scepter^LE^8^You know the ancient Netherese secret of creating  magic scepters.
+Craft Skull Talisman^Fr^47^You can create skull talismans, which carry  spells within themselves.
+Craft Staff^PH^92^You can create magic staffs, each of which has  multiple magical effects.
+Craft Talisman^OA^61^You can create magic fetishes, single-use magic  items that hold spells until triggered.
+Craft Universal Item^XPH^45^You can create universal psionic items.
+Craft Wand^PH^92^You can create wands, which hold spells.
+Craft Wondrous Item^PH^92^You can create a wide variety of magic items.
+Craven^CR^17^Like most sly rogues, you are a dangerous coward.  However, your sneak attacks deal more damage than normal.
+Create Infusion^MW^22^You store a divine spell within a specially  prepared herb.
+Create Portal^FRCS^34^You have learned the ancient craft of creating  a portal.
+Crescent Moon^CW^113^You have mastered the style of fighting with  sword and dagger.
+Crowd Tactics^RD^156^You are adept at moving through and fighting  in crowds.
+Crush^SS^31^Like a dragon, you can hurl your body onto opponents  to deal tremendous damage.
+Cumbrous Dodge^SS^31^You have a chance to dodge attacks that hit  you, but at a cost.
+Cumbrous Fortitude^SS^31^You have a greater chance than normal to resist  attacks against your vitality, but at a cost.
+Cumbrous Reflexes^SS^31^You have a greater chance to resist attacks  against your agility, but at a cost.
+Cumbrous Will^SS^31^You have a greater chance to resist attacks  against your willpower, but at a cost.
+Cunning Sidestep^Dr^103^You have a better than normal chance to avoid  being bull rushed or tripped.
+Curative Legacy^WL^14^Your item's legacy is so linked with your aura  that it restores your health each time it is activated.
+Curling Wave Strike^Sto^92^Mimicking the forceful power of the wave, you  can trip multiple foes as part of the same strike.
+Dallah Thaun's Luck^RW^149^You can rely on a good dose of luck to get you  through almost any scrape.
+Damage Reduction^CW^151^You can shrug off some damage from attacks.
+Damage Reduction^EL^52^You can shrug off some damage from attacks.
+Dancing Blade^Gh^30^You have an energetic fighting style modeled  after traditional Salkirian dancing.
+Dancing with Shadows^RE^117^You have studied shesan talarash dasyannah,  the martial dance of the kalashtar.
+Danger Sense^CAd^106^You are one twitchy individual.
+Danger Sense^MH^25^You are one twitchy mother goose.
+Darguun Mauler^RE^108^The memory of your people's lost glory drives  your brutal mastery of the weapons of Darguun.
+Dark Speech^BV^48^The character learns a smattering of the language  of truly dark power.
+Darkstalker^LoM^179^You have learned how to stalk and surprise creatures  whose senses are very different from those of a humanoid.
+Dash^CW^97^You can move faster than normal.
+Dash^MH^25^You can move faster than normal.
+Dash^SaS^38^You move faster than normal for your race.
+Daunting Presence^LM^25^You are skilled at inducing fear in your opponents.
+Daunting Presence^MH^25^You are skilled at inducing fear in your opponents.
+Dauntless^PG^37^You can stand up to greater punishment than  most and still keep going.
+Daylight Adaptation^PG^37^You have accustomed yourself to the painful  sunlight of the surface world.
+Daylight Adaptation^RE^108^You have grown accustomed to living in the surface  world, such that bright light no longer blinds or dazzles you.
+Daylight Adaptation^FRCS^34^Through long exile from the shadowed homelands  of your kind, you have learned to endure the painful sunlight of the surface  world.
+Deadly Chill^LM^25^Undead you raise or create deal more damage  than normal.
+Deadly Poison^SK^145^Your poison attack deals more damage than normal.
+Deadly Poison^SS^31^Your poison attack deals more damage than normal.
+Deadly Precision^XPH^45^You empty your mind of all distracting emotion,  becoming an instrument of deadly precision.
+Deadly Spittle^SK^145^You can use your spit attack against multiple  opponents.
+Deafening Song^EL^52^Your bardic music deafens those nearby.
+Death Blow^CAd^106^You waste no time in dealing with downed foes.
+Death Blow^SF^6^You waste no time in dealing with downed foes.
+Death Frenzy^LoM^22^When an aboleth takes this feat, its sense of  immortality rebels against the very concept of death.
+Death Master^LM^26^Foes are especially afraid of your critical  hits.
+Death of Enemies^EL^52^You can instantly slay your favored enemies  with a single strike.
+Deceitful^PH^93^You have a knack for disguising the truth.
+Deep Denizen^SS^32^You are adapted to a subterranean environment.
+Deep Impact^XPH^45^You can strike your foe with a melee weapon  as if making a touch attack.
+Deep Vision^RS^137^Your mental focus helps you see farther with  darkvision.
+Deepening Darkness^Rac^162^Your inherent ability to create darkness is  more powerful than normal.
+Deepspawn^LoM^179^Your body undergoes a shocking degeneration  into something that is strikingly inhuman.
+Defensive Archery^RW^150^You can avoid attacks of opportunity when making  ranged attacks while threatened.
+Defensive Strike^CW^97^You can turn a strong defense into a powerful  offense.
+Defensive Strike^OA^62^You can turn a strong defense into a powerful  offense.
+Defensive Throw^CW^97^You can use your opponent's weight, strength,  and momentum against her, deflecting her attack and throwing her to the  ground.
+Defensive Throw^OA^62^You can use your opponent's  weight, strength, and momentum against her, deflecting her attack and throwing  her to the ground.
+Deflect Arrows^PH^93^You can deflect incoming arrows, as well as  crossbow bolts, spears, and other projectile or thrown weapons.
+Deflective Armor^RS^137^Your armor shields you from touch attacks as  well as regular blows.
+Deformity (Clawed Hands)^BV^48^Because of intentional self-mutilation, the  character has deformed arms and hands ending in sharp claws.
+Deformity (Eyes)^BV^48^The character has either drilled a hole in her  forehead trying to add a third eye, or she has supernaturally scarred one  of her regular eyes.
+Deformity (Face)^BV^48^Because of intentional self-mutilation, the  character has a hideous face.
+Deformity (Gaunt)^BV^48^Through intentional starvation and macabre operations,  the character is grossly underweight.
+Deformity (Obese)^BV^48^Through intentional gorging and general gluttony,  the character is obese.
+Deft Hands^PH^93^You have exceptional manual dexterity.
+Deft Opportunist^CAd^106^You are prepared for the unexpected.
+Deft Opportunist^MH^25^You are prepared for the unexpected.
+Deft Strike^CAd^106^You can place attacks at weak points in your  opponent's defenses.
+Deft Strike^Dr^103^You can place attacks at weak points in your  opponent's defenses.
+Delay Power^XPH^45^You can manifest powers that go off up to 5  rounds later.
+Delay Spell^CAr^77^You can cast spells that take effect after a  short delay of your choosing.
+Delay Spell^FRCS^34^You can cast spells that take effect after a  short delay of your choosing.
+Delay Spell^PG^37^You can cast spells that take effect after a  short delay of your choosing.
+Delay Spell^TB^39^You can cast spells that take effect after a  short delay of your choosing.
+Demonsworn Knight^CR^22^A scornful champion of the demon princes, you  detest and oppose devils and other creatures that refuse to heed the call  of chaos.
+Desert Dweller^SS^32^You are adapted to a desert environment.
+Destruction Retribution^LM^26^Undead you raise or create harbor a retributive  curse that is unleashed if they are destroyed.
+Destructive Rage^CW^97^You can shatter barriers and objects when enraged.
+Destructive Rage^MW^22^You shatter barriers and objects when enraged.
+Detach^SS^32^You can remove a part of your body and use it  as a ranged weapon.
+Devastating Critical^Dr^68^Choose one type of melee weapon, such as a claw  or bite. With that weapon, you are capable of killing any creature with  a single strike.
+Devastating Critical^EL^53^Choose one type of melee weapon, such as longsword  or greataxe. With that weapon, you are capable of killing any creature with  a single strike.
+Devoted Inquisitor^CAd^107^Your faithful service to your patron deity involves  training and methods that many paladins consider questionable.
+Devoted Performer^CAd^107^You have foregone the pursuit of frivolous musical  talents, instead entering religious training in service of honor and justice.
+Devoted Tracker^CAd^108^You have found a balance between your woodland  training and your devotion to religious training, blending these two aspects  into one seamless whole.
+Dexterous Fortitude^EL^53^You are able to resist physical attacks with  exceptional agility.
+Dexterous Will^EL^53^You are able to resist compelling effects with  exceptional agility.
+Diehard^Gh^30^You can remain conscious after attacks that  would fell others.
+Diehard^PH^93^You can remain conscious after attacks that  would fell others.
+Diligent^PH^93^Your meticulousness allows you to analyze minute  details that others miss.
+Diminutive Wild Shape^EL^53^You can wild shape into animals of Diminutive  size.
+Dinosaur Hunter^RE^108^Your extraordinary knowledge of dinosaurs grants  you a special aptitude for tracking and hunting them.
+Dinosaur Wrangler^RE^108^You are attuned to dinosaurs and possess a special  bond with them.
+Dire Charge^Dr^68^You can make a full attack as part of a charge.
+Dire Charge^EL^53^You can make a full attack as part of a charge.
+Dire Flail Smash^CR^17^You have mastered the style of fighting with  the dire flail and have learned to deal thunderous blows with the weapon.
+Dirty Fighting^SF^6^You know the brutal and effective fighting tactics  of the streets and back alleys.
+Dirty Rat^CR^20^You are quite adept at slipping under a foe's  guard while he's distracted.
+Disciple of Darkness^BV^49^The character formally supplicates himself to  an archdevil.
+Disciple of Darkness^CR^23^You formally supplicate yourself to an archdevil.  In return for this obedience, you gain a small measure of the archdevil's  power.
+Disciple of the Sun^CD^80^You can destroy undead instead of merely turning  them.
+Discipline^FRCS^34^Your people are admired for their single-minded  determination and clarity of purpose.
+Discipline^OA^62^Your ancestor, Naka Kaeteru, was the first Grand  Master of all the elements, a master of meditation and contemplation.
+Discipline^PG^38^Your people are admired for their single-minded  determination and clarity of purpose.
+Disentangler^Rac^162^Thanks to the teachings of Thard Harr, you have  practiced evading the attacks of jungle plants.
+Disguise Spell^CAd^108^You can cast spells without observers noticing.
+Disguise Spell^DD^50^The deity can cast spells without observers  noticing it.
+Disguise Spell^SaS^38^You can cast spells without observers noticing.
+Disintegration Finesse^LoM^44^A creature with this feat can use disintegrate  effects to affect smaller, more exacting areas.
+Disjunction Ray^LoM^45^A beholder with this feat can narrow its antimagic  cone down to an eye ray that disjoins magic.
+Distant Shot^EL^53^You can target a thing you can see with a ranged  weapon.
+Disturbing Visage^RE^117^You can change your features to chilling effect.
+Dive for Cover^CAd^108^You can dive behind cover or drop to the ground  quickly enough to avoid many area effects.
+Diverse Background^RD^156^You have a wide and diverse background, giving  you a greater understanding of different occupations.
+Divine Accuracy^LM^26^You can channel positive energy to give your  allies' melee attacks another chance to strike true against incorporeal  creatures.
+Divine Cleansing ^CW^106^You can channel energy to improve your allies'  ability to resist attacks against their vitality and health.
+Divine Cleansing^DF^19^You can channel energy to improve you and your  allies' ability to resist poison and curses.
+Divine Damage Reduction^RS^137^You can channel energy to give yourself a small  amount of protection from weapons.
+Divine Energy Focus^Gh^30^You have a gift for channeling positive or negative  energy.
+Divine Metamagic^CD^80^You can channel energy into some of your divine  spells to make them more powerful.
+Divine Might^CW^106^You can channel energy to increase the damage  you deal in combat.
+Divine Might^DD^50^The deity can channel energy to increase its  damage in combat.
+Divine Might^DF^19^You can channel energy to increase the damage  you deal in combat.
+Divine Might^FP^214^You can channel energy to increase your damage  in combat.
+Divine Resistance^CW^106^You can channel energy to temporarily reduce  damage you and your allies take from some sources.
+Divine Resistance^DF^19^You can channel energy to temporarily reduce  damage you and your allies take from some sources.
+Divine Shield^CW^106^You can channel energy to make your shield more  effective for either offense or defense.
+Divine Shield^DF^19^You can channel energy to make your shield more  effective for either offense or defense.
+Divine Spell Penetration^PG^135^Choose one component of your alignment. Any  divine spells of that alignment that you cast are more capable of defeating  spell resistance than normal.
+Divine Spell Power^CD^80^You can channel positive or negative energy  to enhance your divine spellcasting ability.
+Divine Spellshield^RS^137^You can channel energy to help your allies resist  spells and spell-like effects.
+Divine Vengeance^DD^50^The deity can channel energy to do additional  damage in combat against undead.
+Divine Vengeance^DF^20^You can channel energy to deal additional damage  against undead in melee.
+Divine Vengeance^FP^214^You channel energy to do additional energy damage  in combat against undead.
+Divine Vigor^CW^108^You can channel energy to increase your speed  and durability.
+Divine Vigor^DF^20^You can channel energy to increase your speed  and Constitution.
+Diving Charge^RW^150^You can dive down at a target to deal a devastating  strike.
+Dodge^PH^93^You are adept at dodging blows.
+Domain Focus^CD^80^You have mastered the subtle intricacies of  the divine power you've devoted yourself to.
+Domain Spontaneity ^CD^80^You are so familiar with one of your domains  that you can convert other prepared spells into spells from that domain.
+Doomspeak^CR^20^You can demoralize an enemy with horrible condemnations  and grim portents of impending doom.
+Double Hit^MH^25^You can react with your off hand to make an  additional attack along with an attack of opportunity.
+Double Steel Strike^ECS^52^Through monastic weapon training, you have mastered  a fighting style that makes use of an unusual monk weapon: the two-bladed  sword.
+Double Wand Wielder^CAr^77^You can activate two wands at the same time.
+Draconian Breath Weapon^DCS^85^You have harnessed your draconic heritage and  can attack with a dragonlike breath weapon.
+Draconic Breath^CAr^77^You can convert your arcane spells into a breath  weapon.
+Draconic Claw^CAr^77^You develop the natural weapons of your draconic  ancestors.
+Draconic Flight^CAr^77^The secret of draconic flight is revealed to  you, granting you the ability to fly occasionally.
+Draconic Heritage^CAr^77^You have greater connection with your distant  draconic bloodline.
+Draconic Knowledge^Dr^69^You are attuned to nature and the elements and  can draw on deep wells of knowledge.
+Draconic Legacy^CAr^78^You have realized greater arcane power through  your draconic heritage.
+Draconic Power^CAr^78^You have greater power manipulating the energies  of your heritage.
+Draconic Presence^CAr^78^When you use your magic, your mere presence  can terrify those around you.
+Draconic Resistance^CAr^78^Your bloodline hardens your body against the  energy type of your progenitor.
+Draconic Skin^CAr^78^Your skin takes on the sheen, luster, and hardness  of your draconic parentage.
+Dragon Cohort^Dr^104^You gain the service of a loyal dragon ally.
+Dragon Familiar^Dr^104^When you are able to acquire a new familiar,  you may select a wyrmling dragon as a familiar.
+Dragon Hunter ^Dr^104^You have made a special study of dragons and  know how to defend against a dragon's attacks.
+Dragon Hunter Bravery ^Dr^104^You resist dragons' frightful presence, and  your mere presence helps others resist as well.
+Dragon Hunter Defense^Dr^104^Your insight into the tactics and abilities  of dragons grants you awareness of how best to avoid their magical attacks.
+Dragon Rage^ECS^52^You call upon the power of your dragon totem  to enhance your barbarian rage.
+Dragon Steed^Dr^105^You have earned the service of a loyal draconic  steed.
+Dragon Totem^ECS^52^As a proud warrior of the barbarian tribes of  Argonnessen and Seren, you have claimed one of the true dragon types as  your totem -- a patron, protector, and source of strength.
+Dragon Wild Shape^Dr^105^You can take the form of a dragon.
+Dragon Wild Shape^EL^53^You can take the form of a dragon.
+Dragonbane^Dr^105^You have made a special study of dragons and  are adept at pulling off deliberate attacks that take advantage of a dragon's  weak spots.
+Dragoncrafter^Dr^105^You can make special weapons, armor, and other  items using parts of dragons as materials.
+Dragondoom^Dr^105^You have learned how to place blows against  a dragon that deal tremendous damage.
+Dragonfoe^Dr^105^You have learned how to attack dragons more  effectively than most other individuals.
+Dragonfriend^Dr^105^You are a known and respected ally of dragons.
+Dragon's Toughness^MW^22^You are incredibly tough.
+Dragonsong ^Dr^105^Your song or poetics echo the power of the dragonsong,  an ancient style of vocal performance created by dragons in the distant  past.
+Dragonthrall^Dr^105^You have pledged your life to the service of  evil dragonkind.
+Draw from the Land^UE^43^You can draw strength and sustenance from the  land itself.
+Dread Tyranny^RD^154^A devoted student of Hextor's militant teachings,  you are skilled at intimidating and dominating weaker beings.
+Dreadful Wrath^PG^38^You are terrible to behold in battle, and few  foes have the heart to face you without quailing
+Dreamsight Elite^RE^113^Your dreamsight shifter trait improves.
+Drift Magic^Sa^49^You can tap the power of drift magic.
+Drow Eyes^Rac^162^You have trained your eyes to see in the dark  as well as your full drow ancestors.
+Drow Skirmisher^RE^109^Your experience with the guerrilla-style combat  of the deep jungle grants you mastery of the weapons of the drow.
+Dual Strike^CAd^108^You are an expert skirmisher skilled at fighting  with two weapons.
+Dual Strike^SF^6^Your combat teamwork makes you a more dangerous  foe.
+Duergar Mindshaper^Rac^162^You are accomplished at using the power of your  mind to overcome weaker personalities.
+Dungeoneer's Intuition^CSW^144^You can sense when things don't feel right,  and you have a knack for avoiding deadly traps and ambushes.
+Durable Form^LoM^180^You are much more resilient than the fragile  humanoids that do not share your aberrant heritage.
+Dust Cloud^SS^33^You can sweep dust into the air to hide from  opponents.
+Dwarf's Toughness^MW^22^You are tougher than you were before.
+Dwarven Armor Proficiency^RS^138^You are familiar with exotic armor of dwarven  manufacture and understand how to use it properly.
+Eagle Claw Attack^CW^97^Your superior insight allows you to strike objects  with impressive force.
+Eagle Claw Attack^OA^62^Your unarmed attacks can shatter objects.
+Eagle Claw Attack^SF^6^Your unarmed attacks shatter objects.
+Eagle Tribe Vision^ShS^20^You have keen eyesight reminiscent of the giant  eagles that fly over your tribal lands.
+Eagle's Fury^Sa^49^You know how to wield the eagle's claw with  deadly speed.
+Eagle's Wings^CD^80^You can take wing and fly with the grace of  an eagle.
+Earth Adept^RS^138^You are in tune with the ground at your feet,  making you more dangerous in the shifting conditions of combat.
+Earth Fist^RS^138^Your bond with the earth and martial training  has imbued your fists with the qualities of cold iron.
+Earth Heritage^PlH^38^You are descended from creatures native to the  Elemental Plane of Earth.
+Earth Master^RS^138^You are in tune with the ground at your feet,  helping you anticipate your opponent's movements in combat.
+Earth Power^RS^138^You draw psionic energy from raw stone.
+Earth Sense^RS^138^You are in tune with the earth beneath you.
+Earth Spell^RS^138^You draw magical power from the earth beneath  your feet.
+Earth's Embrace^CW^97^You can crush opponents when you grapple them.
+Earth's Embrace^OA^62^You can crush opponents when you grapple them.
+Earth's Warding^RS^139^You can channel energy to infuse your skin with  the strength of the earth.
+Ecclesiarch^ECS^52^You command a degree of respect in your church's  hierarchy.
+Ectoplasm^Gh^30^You can create ectoplasm, a gooey physical manifestation  of base supernatural spiritual energy.
+Education^ECS^52^Some lands hold the pen in higher regard than  the sword.
+Education^FRCS^34^In your youth you received the benefit of several  years of more or less formal schooling.
+Education^Gh^31^In your youth you received the benefit of several  years of more or less formal schooling.
+Education^PG^38^You hail from a land where the pen is held in  higher regard than the sword.
+Efficient Item Creation^EL^53^Select an item creation feat. You can create  magic items using that feat much more quickly than normal.
+Eldritch Linguist^Rac^162^You have a deep understanding of how words themselves  have their own kind of magic, and a mastery of the secret syntax of power.
+Elemental Bloodline^Rac^163^You have taken on some of the aspects of the  type of element that infuses your flesh.
+Elemental Healing^CD^80^You can channel elemental energy to heal creatures  of a specific elemental subtype.
+Elemental Smiting^CD^81^You can channel elemental energy to deal extra  damage to creatures tied to a specific element.
+Elemental Spellcasting^PlH^39^Choose an element. You cast spells with that  descriptor more effectively than normal.
+Elephant's Hide^CD^81^You can thicken your skin to the toughness of  an elephant's.
+Elf Dilettante^RW^150^Throughout the long years of your life, you  have developed a talent for doing just about anything.
+Elfhunter^Und^25^Because of your cultural hatred for elves, you  have had specific training in how best to fight them.
+Elusive Target ^CW^110^Trying to land a blow against you can be a maddening  experience.
+Embed Spell Focus^Dr^69^You can embed focus components required for  your spells into your body.
+Empower Legacy^WL^14^You can use one of your item's legacy abilities  to greater effect.
+Empower Power^XPH^46^You can manifest powers to greater effect.
+Empower Spell^PH^93^You can cast spells to greater effect.
+Empower Spell-Like Ability^BV^49^The creature can use a spell-like ability with  greater effect.
+Empower Spell-Like Ability^MM^303^The creature can use a spell-like ability with  greater effect than normal.
+Empower Spell-Like Ability^MM3^206^A creature with this feat can use a spell-like  ability with greater effect than normal.
+Empower Spell-Like Ability^SS^33^You can use a spell-like ability with greater  effect than normal.
+Empower Turning^CD^81^You can turn or rebuke more undead with a single  turning attempt.
+Empower Turning^DF^20^You can turn or rebuke more undead with a single  turning attempt.
+Empower Turning^FP^214^You can turn or rebuke more undead with a single  turning attempt.
+Empower Turning^Gh^31^You can turn or rebuke more undead with a single  turning attempt.
+Empower Turning^LM^26^You can turn or rebuke greater numbers of undead  with a single turning attempt.
+Empowered Ability Damage^LM^26^Your ability damage (or ability drain) special  attack is more potent than normal.
+Empty Hand Mastery^OA^80^You have mastered the martial arts style of  'Empty Hand' -- a hard form emphasizing strikes with the hand.
+Enchanting Song^RS^139^You can channel the power of your bardic music  to temporarily increase the power of your enchantment spells.
+Endurance^PH^93^You are capable of amazing feats of stamina.
+Endure Blows^Dr^70^You are adept at lessening the effects of blows.
+Endure Sunlight^LM^26^Your vulnerability to sunlight is reduced.
+Enduring Life^LM^26^You can ignore the effect of negative levels  for a short time.
+Energize Armor^RS^139^You can charge your armor with psionic energy,  making it resistant to energy damage.
+Energize Spell^LM^26^Your spells channel positive energy to deal  extra damage to undead creatures, but are less effective against other opponents.
+Energy Admixture^CAr^78^You can modify a spell that uses one type of  energy to add an equal amount of another energy type.
+Energy Admixture^TB^39^You can modify a spell that uses one type of  energy to mix in an equal amount of another type of energy.
+Energy Affinity^MH^25^You can modify a spell that uses one type of  energy to use another type of energy.
+Energy Resistance^EL^53^You can resist the effects of a chosen type  of energy.
+Energy Substitution^CAr^79^You can modify an energy-based spell to use  another type of energy instead.
+Energy Substitution^DD^50^The deity can modify a spell that uses energy  to use another type of energy.
+Energy Substitution^Mag^21^You can modify a spell that uses one type of  energy to use another type of energy.
+Energy Substitution^TB^40^You can modify a spell that uses one type of  energy to use another type of energy.
+Enervate Spell^LM^26^Your spells channel negative energy to deal  extra damage to undead creatures, but are less effective against unliving  opponents.
+Enervating Touch^Gh^31^Your touch can bestow negative levels upon creatures.
+Enervative Healing^Rac^163^You can use the life energy of an opponent to  heal yourself.
+Enhance Effect^PG^135^You can change the characteristics of a persistent  spell effect that is already in place.
+Enhance Item^EL^114^You can increase the minimum DC for saving throws  of magic items that you
+Enhance Spell^CAr^191^You can increase the power limit of your damage-dealing  spells.
+Enhance Spell^EL^53^You can increase the power limit of your damage-dealing  spells.
+Enhanced Adhesive^Und^25^The natural adhesive you secrete becomes stickier.
+Enhanced Power Sigils^RD^152^Your illumian power sigils are more powerful  than normal.
+Enlarge Breathe^Dr^70^Your breath weapon is larger than normal.
+Enlarge Mucus Cloud^LoM^22^An aboleth with this feat can extend its mucus  cloud into a wider area.
+Enlarge Power^XPH^46^You can manifest powers farther than normal.
+Enlarge Spell^PH^94^You can cast spells farther than normal.
+Entangling Spell^CR^20^Your spell releases residual eldritch power  that entangles your enemies.
+Epic Combat Expertise ^CW^151^You have extraordinary talent at using your  combat skill for defense.
+Epic Counterspell^PG^135^You can counterspell any number of spells each  round.
+Epic Devotion^CD^89^Choose an alignment component different from  your own alignment. You are particularly resistant to spells of that alignment.
+Epic Devotion^PG^135^Choose an alignment component that you do not  possess. You are particularly resistant to spells with that descriptor.
+Epic Dodge^CAd^191^You are able to evade attacks with exceptional  agility.
+Epic Dodge^EL^54^You are able to evade attacks with exceptional  agility.
+Epic Endurance^EL^54^You are capable of legendary feats of stamina.
+Epic Expanded Knowledge^XPH^34^You learn another power.
+Epic Fortitude^Dr^70^You have tremendously high fortitude.
+Epic Fortitude^EL^54^You have tremendously high fortitude.
+Epic Inspiration^EL^54^Your bardic music provides greater inspiration  than normally possible.
+Epic Leadership^EL^54^You attract more powerful cohorts and followers  than normally possible.
+Epic Prowess^CW^151^You have great skill in combat.
+Epic Prowess^EL^54^You gain great skill in combat.
+Epic Psionic Focus^XPH^34^You can expend your psionic focus to greater  effect.
+Epic Reflexes^Dr^70^You have tremendously fast reflexes.
+Epic Reflexes^EL^54^You have tremendously fast reflexes.
+Epic Reputation^CAd^191^Your reputation provides great bonuses on interactions  with others.
+Epic Reputation^EL^54^Your reputation provides great bonuses on interactions  with others.
+Epic Skill Focus^CAd^191^Choose a skill, such as Move Silently. You have  a legendary knack with that skill.
+Epic Skill Focus^EL^54^Choose a skill, such as Move Silently. You have  a legendary knack with that skill.
+Epic Speed^EL^54^You can move much more quickly than a normal  person.
+Epic Spell Focus ^CAr^192^Choose a school of magic, such as illusion.  Your spells of that school are for more potent than normal.
+Epic Spell Focus^EL^54^Choose a school of magic, such as illusion.  Your spells of that school are for more potent than normal.
+Epic Spell Penetration^CAr^192^Your spells are tremendously potent, breaking  through spell resistance with ease.
+Epic Spell Penetration^EL^54^Your spells are tremendously potent, breaking  through spell resistance with ease.
+Epic Spellcasting^EL^55^You can create and cast spells that transcend  the most powerful existing spells.
+Epic Spellfire Wielder^PG^136^You can store more spellfire energy levels than  normal.
+Epic Sunder^CW^151^You are preternaturally tough.
+Epic Toughness^CW^151^You are specially good at using one chosen type  of weapon.
+Epic Toughness^EL^55^You are preternaturally tough.
+Epic Weapon Focus ^CW^151^You deal extra damage when attacking objects.
+Epic Weapon Focus^EL^55^Choose one type of weapon, such as a greataxe.  You are especially good at using this weapon.
+Epic Weapon Specialization^EL^55^Choose one type of weapon, such as a greataxe.  You deal extraordinary damage wielding this weapon.
+Epic Will^Dr^70^You have tremendously strong willpower.
+Epic Will^EL^55^You have tremendously strong willpower.
+Eschew Materials ^DD^50^The deity can cast spells without material components.
+Eschew Materials^EL^69^You can cast spells without material components.
+Eschew Materials^FP^214^You can cast spells without material components.
+Eschew Materials^LD^189^You can cast spells without material components.
+Eschew Materials^Mag^22^You can cast spells without material components.
+Eschew Materials^PH^94^You can cast spells without relying on material  components.
+Eschew Materials^TB^40^You can cast spells without material components.
+Eternal Strength^RD^155^You have taken Kord's fighting ways to heart.  Throwing yourself into every brawl, you draw upon your mighty deity's strength.
+Ethereal Sidestep^Gh^31^You can teleport yourself a short distance.
+Ethran^FRCS^34^You have been initiated into the secrets of  the Witches of Rashemen as a member of the Ethran, the 'untried.'
+Ethran^PG^38^You have been initiated into  the secrets of the Witches of Rashemen as a member of the Ethran, the 'untrained.'
+Ettercap Berserker^UE^43^The intense physical training required to join  your lodge has made you tougher.
+Evil Brand^BV^49^The character is physically marked forever as  a servant of an evil power or as a villain.
+Evil Brand^CR^23^You are physically marked forever as a servant  of an evil power or as a villain.
+Evil Embraced^CR^23^You embrace the power of your fiendish patron  and call upon that power in moments of great need.
+Eviscerator^LM^26^The allies of your foes are especially afraid  of your critical hits.
+Exalted Companion^BE^42^Instead of an animal companion, you have a magical  beast of good alignment.
+Exalted Smite^BE^42^Your smite ability is empowered with holy energy.
+Exalted Spell Resistance^BE^42^You are particularly resistant to evil spells.
+Exalted Turning^BE^42^You turn undead with such power that affected  undead take damage.
+Exalted Wild Shape^BE^42^You can use your wild shape ability to take  the form of a good-aligned magical beast.
+Exceptional Artisan^ECS^52^You are an expert at creating magic items faster  than usual.
+Exceptional Deflection^EL^55^You can deflect any type of ranged attack.
+Exotic Armor Proficiency^RS^139^Choose a type of exotic armor. You understand  how to wear that type of exotic armor properly.
+Exotic Armor Proficiency^Und^25^Choose a type of exotic armor. You understand  how to wear that type of exotic armor properly.
+Exotic Shield Proficiency^RS^139^Choose an exotic shield. You are proficient  with that type of exotic shield.
+Exotic Weapon Proficiency^PH^94^Choose a type of exotic weapon. You understand  how to use that type of exotic weapon in combat.
+Expanded Aura of Courage^HB^97^Your aura of courage protects more allies than  normal.
+Expanded Knowledge^XPH^46^You learn another power.
+Expanded Possession^Gh^31^You can ride or possess an additional type of  creature.
+Expeditious Dodge^RW^150^You're good at avoiding attacks while moving  quickly.
+Expert Siege Engineer^HB^97^You are particularly skilled at operating siege  weapons, such as catapults and battering rams.
+Expert Swimmer^Sto^92^You swim like a fish. You can stay underwater  far longer than others of your race, and you are at home in the water.
+Expert Tactician^CAd^109^Your tactical skills work to your advantage.
+Expert Tactician^SaS^38^Your tactical skills work to your advantage.
+Expert Tactician^SF^6^Your tactical skills work to your advantage.
+Explosive Spell^CAr^79^You can cast spells that blast creatures off  their feet.
+Explosive Spell^UE^43^You can cast spells that blast creatures off  their feet.
+Extend Power^XPH^46^You can manifest powers that last longer than  normal.
+Extend Rage^CW^97^You are able to maintain your rage longer than  most.
+Extend Rage^ECS^52^You are able to maintain your rage longer than  most.
+Extend Spell^PH^94^You can cast spells that last longer than normal.
+Extend Spreading Breath^Dr^70^You can convert your breath weapon into a spread  effect that can be used at range.
+Extended Life Span^EL^56^You are exceptionally long-lived.
+Extended Rage^MW^22^Your rage lasts longer than it normally would.
+Extended Rage^UE^43^Your rage lasts longer than it normally would.
+Extended Reach^SS^34^Your flexible body allows you to reach farther  than normal.
+Extra Domain Spell^MH^26^You have chosen to be more specialized in a  particular domain.
+Extra Edge^CAr^79^Your ability to deal spell damage is particularly  striking.
+Extra Favored Enemy^Gh^32^You select an additional favored enemy.
+Extra Favored Enemy^MW^22^You select an additional favored enemy.
+Extra Followers^HB^97^Your charismatic magnetism attracts even more  followers to your banner.
+Extra Invocation^CAr^79^You learn an additional invocation.
+Extra Item Space^SS^34^You can wear more magic items than are normally  allowed.
+Extra Music^CAd^109^You can use your bardic music more often than  you otherwise could.
+Extra Music^DD^50^The deity can use its bardic songs more often  than it otherwise could.
+Extra Music^ECS^52^You can use your bardic music more often than  you otherwise could.
+Extra Music^SaS^39^You can use your bardic music more often than  you otherwise could.
+Extra Rage^CW^98^You may rage more frequently than normal.
+Extra Rage^MW^22^You rage more frequently than you normally would.
+Extra Rings^ECS^53^Your familiarity with forging magic rings allows  you to make use of more rings than normal.
+Extra Shifter Trait^ECS^53^You manifest a second shifter trait while shifting.
+Extra Shifter Trait^MM3^150^You manifest a second shifter trait while shifting.
+Extra Shifter Trait^RE^114^You manifest a second shifter trait while shifting.
+Extra Silence^RS^139^You can generate a field of silence more often  than other whisper gnomes can.
+Extra Slot^CAr^79^You can cast an additional spell.
+Extra Slot^Gh^32^You can cast an extra spell.
+Extra Slot^TB^40^You can cast an extra spell.
+Extra Smiting^CW^98^You can make more smite attacks.
+Extra Smiting^DF^20^You can make more smite attacks.
+Extra Spell^CAr^79^You learn an additional spell.
+Extra Spell^TB^40^You can learn one more spell.
+Extra Spell Secret^CAr^80^You learn an additional spell secret.
+Extra Stunning^CW^98^You gain extra stunning attacks.
+Extra Stunning Attacks^SF^6^You gain extra stunning attacks when fighting  unarmed.
+Extra Turning^PH^94^You can turn or rebuke creatures more often  than normal.
+Extra Wild Shape^CD^81^You can use wild shape more frequently than  you normally could.
+Extra Wild Shape^Gh^32^You can use wild shape more frequently than  you normally could.
+Extra Wild Shape^MW^22^You use wild shape more frequently than you  normally could.
+Extra Wild Shape^Und^25^You can use wild shape more frequently than  you normally could.
+Extraordinary Artisan^ECS^53^You are an expert at creating magic items at  a lower cost than usual.
+Extraordinary Concentration^CAd^109^Your mind is so focused that you can cast spells  even while concentrating on another spell.
+Extraordinary Spell Aim^CAd^109^You can shape a spell's area to exclude one  creature from its effects.
+Eyes in the Back of Your Head^CW^98^Your superior battle sense helps minimize the  threat of flanking.
+Eyes in the Back of Your Head^DD^50^The deity's superior battle sense helps minimize  the threat of flanking attacks.
+Eyes in the Back of Your Head^SF^6^Your superior battle sense helps minimize the  threat of flanking.
+Eyes of Light^Rac^163^You can focus the holy power within you to create  a beam of destructive light energy.
+Eyes to the Sky^UA^93^You have an instinctive sense of when someone  is magically watching you.
+Fade^Gh^32^You can make your ghost body more diaphanous  and difficult to detect.
+Faith in the Frost^Fr^48^You channel frozen energies from your deity  when you turn or rebuke creatures.
+Falling Far Strike^OA^62^You have mastered the art of striking a nerve  that blinds a humanoid opponent.
+False Pretenses^UA^93^Those who try to charm you get an unpleasant  surprise.
+Familiar Concentration^LE^8^In the tradition of Narfell's ancient summoners,  your familiar can concentrate to maintain spells for you.
+Familiar Spell^DMG^209^Your familiar can cast a spell.
+Familiar Spell^EL^56^Your familiar can use one of your spells as  a spell-like ability.
+Familiar Spell^Und^25^You are so well acquainted with the spells you  have mastered that you can store the prepared spells in the mind of your  familiar.
+Far Horizons^RD^155^By dedicating yourself to the philosophies of  Fharlanghn, you have become a more world-wise and capable traveler.
+Far Shot^PH^94^You can get greater distance out of a ranged  weapon.
+Fast Healing^Dr^70^You heal your wounds very quickly.
+Fast Healing^EL^56^You heal your wounds very quickly.
+Fast Wild Shape^CD^81^You assume your wild shape faster and more easily  than you otherwise could.
+Fast Wild Shape^Gh^32^You can assume your wild shape faster and more  easily than you normally could.
+Fast Wild Shape^MW^22^You assume your wild shape faster and more easily  than you otherwise could.
+Faster Healing^CW^98^You recover faster than normal.
+Faster Healing^MW^22^You recover faster than others do.
+Favored Critical^MW^23^You know how to hit your favored enemies where  it hurts.
+Favored in House^ECS^53^You are a member of one of the dragonmarked  mercantile houses and wield some influence in that house.
+Favored of the Companions^BE^43^You swear allegiance to the Talisid or one of  the Five Companions, the paragons of the guardinals, and in exchange gain  power to act on their behalf.
+Favored of the Zulkirs^PG^176^Through your position of prestige among the  Red Wizards, you have gained access to secrets of evil magic known to few  outside the zulkirs themselves.
+Favored Power Attack^CW^98^You are able to deal more damage against your  favored enemies.
+Fearless^PG^38^You are a stranger to fear.
+Fearless Destiny^RD^152^Your grand destiny allows you to avoid death.
+Fearsome and Fearless^OA^62^You claim descent from the first Akodo, the  paragon of samurai virtue.
+Feign Weakness^SF^6^You capitalize on your foe's perceptions of  your unarmed status.
+Fell Animate^LM^26^Living foes slain by your spell may rise as  zombies.
+Fell Drain^LM^27^Living foes damaged by your spell also gain  a negative level.
+Fell Frighten^LM^27^Living foes damaged by your spell are also shaken.
+Fell Shot^XPH^46^You can strike your foe with a ranged weapon  as if making a touch attack.
+Fell Weaken^LM^27^Living foes damaged by your spell are also weakened.
+Feral Animal Companion^CR^20^You can enslave a feral animal and adopt it  as your animal companion.
+Fiendish Bloodline^Rac^163^Some of your latent abilities, inherited from  an unusually powerful fiendish ancestor, have matured.
+Fiendish Heritage^PlH^39^You are descended from creatures native to the  Lower Planes.
+Fiendish Summoning Specialist^PlH^39^You can select from a larger number of options  when summoning evil creatures.
+Fiery Spell^Sa^49^Your fire magic is bolstered, further scorching  your enemies.
+Filth Eater^Sh^157^You are highly resistant to the effects of disease  and can usually eat spoiled food without suffering ill effects.
+Final Strike^SS^34^Your death throes are destructive.
+Fine Wild Shape^EL^56^You can wild shape into animals of Fine size.
+Fire Heritage^PlH^39^You are descended from creatures native to the  Elemental Plane of Fire.
+Fist of the Heavens^BE^43^Your stunning attack is empowered by celestial  might.
+Fists of Iron^CW^99^You have learned the secrets of imbuing your  unarmed attacks with extra force.
+Fists of Iron^OA^62^You have learned the secrets of imbuing your  unarmed attacks with extra force.
+Fists of Iron^SF^6^You have learned the secrets of imbuing your  unarmed attacks with extra force.
+Flay Foe^CR^20^You are skilled at flaying the flesh from your  enemy's bones.
+Fleet of Foot^CW^99^You run nimbly, able to turn corners without  losing momentum.
+Fleet of Foot^DD^50^The deity runs so nimbly that it can turn corners  without losing momentum.
+Fleet of Foot^PG^38^You are extraordinarily swift.
+Fleet of Foot^SaS^39^You run so nimbly that you can turn corners  without losing momentum.
+Flensing Strike^ECS^53^You have studied a martial style practiced by  monks devoted to the Mockery, which has taught you to cut your opponent's  skin in a very painful way.
+Flick of the Wrist^CW^99^With a single motion, you can draw a light weapon  and make a devastating attack.
+Flick of the Wrist^RW^150^With a single motion, you can draw a light weapon  and make a devastating attack.
+Flick of the Wrist^SaS^39^With a single motion, you can draw a light weapon  and make a devastating attack.
+Fling Ally^RS^139^You can launch your comrades into the air as  if they were thrown weapons.
+Fling Enemy^RS^140^When you're wrestling a foe, you can lift him  into the air and hurl him.
+Fling Enemy^SS^34^You can pick up an opponent and fling it.
+Flyby Attack^MM^303^The creature can attack on the wing.
+Flyby Attack^MM2^18^The creature can attack on the wing.
+Flyby Attack^MM3^206^A creature with this feat can attack on the  wing.
+Flyby Attack^Mon^9^The creature can attack on the wing.
+Flyby Attack^MW^23^You attack while on the wing.
+Flyby Breath^DCS^85^You can employ your breath weapon in a high-speed  attack pass.
+Flying Fish Leap^Sto^92^You can hurl yourself out of the water with  ease.
+Flying Kick^CW^99^You literally leap into battle, dealing devastating  damage.
+Flying Kick^OA^62^You literally leap into battle, dealing devastating  damage.
+Focused Antimagic^LoM^45^A beholder with this feat can focus the antimagic  of its central eye to target a single person or object.
+Focused Mind^RW^151^When you have the opportunity to concentrate  on a task, you usually do very well at it.
+Focused Shield^RS^140^Your mental focus makes you more adept at using  your shield.
+Focused Sunder^XPH^46^You can sense the stress points on others' weapons.
+Foe Hunter^FRCS^34^In lands threatened by evil nonhumans, many  warriors learn ways to fight effectively against these creatures.
+Foe Hunter^Gh^32^You have been trained in the methods of fighting  various kinds of yuan-ti.
+Foe Hunter^PG^38^In a land threatened by fierce raiders, you  have learned to fight effectively against certain foes.
+Foe Specialist^MH^26^You are trained at how to damage a particular  type of foe.
+Foot and Fist Mastery^OA^80^You have mastered the martial arts style of  'Foot and Fist' -- a hard form emphasizing strikes with the hands and feet.
+Force of Personality^CAd^109^You have cultivated an unshakable belief in  your self-worth.
+Force of Will^XPH^46^You are able to resist psionic attacks with  extreme force of will.
+Forceful Staff Style^Gh^32^You can stun people with your quarterstaff and  push them around after you stun them.
+Forest Gnome Phantasist^Rac^163^You can protect your forest home with a variety  of phantasms and patterns to befuddle your foes.
+Forester^FRCS^35^You are knowledgeable about the secrets of the  forest and wise in its ways.
+Forester^PG^39^You are one with Faerun's mighty forests.
+Forge Epic Ring^EL^56^You can craft magic rings of epic power.
+Forge Ring^PH^94^You can create magic rings, which have varied  magical effects.
+Forgeheart^PG^39^Because you are inured to the hellish heat of  your homeland, you are resistant to blasts of fire that would damage other  creatures.
+Forked Tongue^SK^145^You speak with a honeyed voice that bends listeners  to your will.
+Formation Expert^CW^110^You are trained at fighting in ranks and files.
+Fortify Spell^CAr^80^You cast spells that more easily penetrate spell  resistance.
+Fortify Spell^UE^43^You can cast spells that easily penetrate spell  resistance.
+Freezing the Lifeblood^CW^99^You can paralyze a humanoid opponent with an  unarmed attack.
+Freezing the Lifeblood^OA^62^You can paralyze a humanoid opponent with an  unarmed attack.
+Freezing Touch^Gh^33^Your touch is supernaturally cold.
+Frightful Moan^Gh^33^You can unleash a moan that panics creatures  near you.
+Frightful Presence^Dr^106^Like a dragon, your mere presence can terrify  those around you.
+Frightful Presence^Gh^33^Your very presence can cause others to be stricken  with fear.
+Frostfell Prodigy^Fr^48^You gain additional bonus spells in cold regions.
+Frozen Berserker^Fr^48^When you enter your barbarian rage, your body  becomes infused with cold energy.
+Frozen Magic^Fr^48^Your cold spells are more powerful when you  cast them in a cold region.
+Frozen Wild Shape^Fr^48^You can assume the form of magical beasts with  the cold subtype.
+Full Manifestation^Gh^33^You can manifest fully when you would otherwise  be forced to be incorporeal.
+Furious Charge^PG^39^Your people are known for their love of battle,  and they rarely waste time in meeting a foe blade-to-blade.
+Gap of the Serpent^SS^35^You can swallow larger creatures than normal.
+Gape of the Serpent^SK^146^Like a snake, you can stretch your mouth to  an outlandish extent to accommodate immense prey.
+Gargantuan Wild Shape^EL^56^You can wild shape into animals of Gargantuan  size.
+Gatekeeper Initiate^ECS^54^You have been trained in the ancient druidic  tradition of the Gatekeepers, founded originally to ward off an extraplanar  assault by aberrations.
+Genie Lore^Rac^163^You have studied centuries of Calishite lore  regarding geniekind.
+Gestalt Anchor^RE^112^Your strong bond to your quori spirit allows  you and your kalashtar allies to move and act as a fluid unit.
+Ghost Attack^XPH^46^Your deadly strikes against incorporeal foes  always hit their mark.
+Ghost Flight^Gh^33^Your fully manifested ghost body can fly.
+Ghost Glide^Gh^33^Your fully manifested ghost body can slowly  fly.
+Ghost Hand^Gh^33^You can move small objects in a limited manner  when you are a ghost.
+Ghost Healing^Gh^33^You can transfer some of your own ectoplasm  to another ghost to heal it.
+Ghost Ride^Gh^33^You can hide within the physical body of a living  creature, perceiving the world through its senses, but without the ability  to control the host.
+Ghost Scarred^LM^27^You are adept at fighting incorporeal undead.
+Ghost Smiting^Gh^34^You can use your smite ability to smite ghosts.
+Ghostly Grasp^LM^27^You can handle corporeal objects even while  corporeal.
+Ghost-Touch Spell^Gh^34^You know how to tune your damaging spells to  affect ghosts without harming other creatures.
+Giantbane^CW^111^You are trained in fighting foes larger than  you are.
+Giant's Toughness^MW^23^You are amazingly tough.
+Gift of Discernment^PG^176^You can rely on your conscience to steer you  away from evil deeds.
+Gift of Faith^BE^43^You have an unusual capacity to trust in divine  providence working all things for the good.
+Gift of Grace^BE^43^You can improve the saving throws of your allies  by sharing some of your divine grace.
+Gift of Tongues^Gh^34^You have an intuitive talent for learning languages.
+Gift of Tongues^Rac^163^You have an intuitive talent for learning languages.
+Gifted General^OA^62^Your ancestor Daidoji Yurei, an ancient daimyo  of the Daidoji family, was a gifted general -- the first in Rokugan to use  guerilla warfare.
+Glorious Weapons^CD^82^You can channel positive or negative energy  to imbue your allies' weapons with an alignment.
+Gnoll Ferocity^RW^151^You embody the savage ferocity of your people.  When you fly into a berserk rage, you can bite opponents with your powerful  jaws.
+Gnome Foe Killer^RS^140^Your battle techniques against your racial foes  improve.
+Goad ^CAd^109^You are skilled at inducing opponents to attack  you.
+Goad^MH^26^You are skilled at inducing opponents to attack  you.
+Goad^RS^140^You are skilled at inducing opponents to attack  you.
+Godsight^LE^8^You enjoy the special blessing of a deity of  the Mulhorandi pantheon, who has granted you unerring powers of perception.
+Gold Dwarf Dweomersmith^Rac^163^You have learned the secrets of gold dwarf magic  that creates or enhances weapons.
+Gorebrute Elite^RE^114^Your mighty charge attack can knock down foes.
+Graft Flesh^FF^207^You can apply a certain type of graft to other  living creatures or to yourself.
+Graft Flesh^LM^27^You can apply a certain type of graft to other  living creatures or to yourself.
+Graft Flesh^LoM^216^You can apply a certain type of graft to other  living creatures or to yourself.
+Graft Illithid Flesh^Und^25^You can apply illithid grafts to other living  creatures or to yourself.
+Graft Yuan-Ti Flesh^SK^146^You can apply yuan-ti grafts to other living  creatures or to yourself.
+Grand Malevolence^Gh^34^You can possess multiple creatures and control  their actions.
+Grappling Block^OA^63^You can catch and pin an opponent's weapon with  your bare hands.
+Grass Trekker^SS^35^You are adapted to a plains environment.
+Great Bite^ECS^54^You know how to hit where it hurts with your  fangs.
+Great Captain^Sto^92^You are a master pilot and battle leader; your  crew anticipates your every command and leaps to do your bidding.
+Great Charisma^EL^56^Your powers of persuasion and leadership are  greater than normal.
+Great Cleave^PH^94^You can wield a melee weapon with such power  that you can strike multiple times when you fell your foes.
+Great Constitution^EL^56^Your health and endurance are greater than normal.
+Great Crafter^OA^63^Your ancestor, Kaiu, was the first and greatest  blacksmith of the Crab clan.
+Great Dexterity^EL^56^Your agility and coordination are greater than  normal.
+Great Diplomat^OA^63^You are descended from Asako, one of the companions  of the first Phoenix, a great healer, diplomat, and warrior.
+Great Flyby Attack^SS^35^You can make multiple flyby attacks in a round.
+Great Fortitude^PH^94^You are tougher than normal.
+Great Intelligence^EL^56^Your powers of reason and learning are greater  than normal.
+Great Rend^ECS^54^You know how to hit where it hurts with your  claws.
+Great Smiting^DMG^209^Your smite attacks are much more powerful than  normal.
+Great Smiting^EL^56^Your smite attacks are much more powerful than  normal.
+Great Stag Berserker^UE^43^Your fighting style employs aggressive charges  in the manner of your lodge's totem animal.
+Great Stamina^OA^63^Your ancestor, Daidoji Masashigi, gave his life  defending the Kaiu Wall alongside the Crab at the Battle of the Landbridge.
+Great Strength^EL^57^Your muscle and physical power are greater than  normal.
+Great Teamwork^OA^63^You are a descendant of Hida Banuken, the Crab  champion who oversaw the construction of the Kaiu Wall during the battle  of the Cresting Wave.
+Great Wisdom^EL^57^Your willpower and insight are greater than  normal.
+Greater Ki Shout^OA^63^Your ki shout can panic your opponents.
+Greater Cold Focus^Fr^48^Your cold spells are now even more potent than  before.
+Greater Dragonmark^ECS^54^You have a greater dragonmark.
+Greater Heavy Armor Optimization^RS^141^You have mastered the use of heavy armor, maximizing  its protective qualities while moving more easily in it.
+Greater Kiai Shout^CW^99^You kiai shout can panic your opponents.
+Greater Legacy^WL^14^You awaken the most powerful abilities of a  specific item of legacy.
+Greater Manyshot^XPH^47^You are skilled at firing many arrows at once,  even at different opponents.
+Greater Mighty Roar^SS^35^You unsettle opponents with a dreadful roar  as you attack.
+Greater Multigrab^SK^146^You can grapple enemies effortlessly with your  natural weapons.
+Greater Multigrab^SS^35^You can grapple enemies effortlessly with your  natural weapons.
+Greater Multiweapon Fighting^DD^50^A deity with three or more hands can fight with  a weapon in each hand.
+Greater Multiweapon Fighting^EL^69^A creature with three or more hands can fight  with a weapon in each hand.
+Greater Multiweapon Fighting^SS^35^A creature with three or more arms can fight  with a weapon in each hand. The creature can make up to three attacks per  round with each extra weapon.
+Greater Power Penetration^XPH^47^Your powers are especially potent at breaking  through power resistance.
+Greater Power Specialization^XPH^47^You deal more damage with your powers.
+Greater Powerful Charge^ECS^54^You can charge with extra force.
+Greater Powerful Charge^MH^27^You can charge with extra force.
+Greater Psionic Endowment^XPH^47^You can use meditation to focus your powers.
+Greater Psionic Fist^XPH^47^You can charge your unarmed strike or natural  weapon with additional damage potential.
+Greater Psionic Shot^XPH^47^You can charge your ranged attacks with additional  damage potential.
+Greater Psionic Weapon^XPH^47^You can charge your melee weapon with additional  damage potential.
+Greater Resiliency^CW^99^Your extraordinary resilience to damage increases.
+Greater Resiliency^MW^23^Your extraordinary resilience to damage increases.
+Greater Shifter Defense^ECS^54^By delving deeper into your shifter heritage,  you develop the ability to ignore some damage from every attack.
+Greater Spell Focus^DD^50^The deity chooses a school of magic to which  it already has applied the Spell Focus feat.
+Greater Spell Focus^EL^69^Choose a school of magic, such as illusion.  Your spells of that school are far more potent than normal.
+Greater Spell Focus^FRCS^35^Choose a school of magic to which you already  have applied the Spell Focus feat. Your spells of that school are even more  potent than normal.
+Greater Spell Focus^PH^94^Choose a school of magic to which you already  have applied the Spell Focus feat. Your spells of that school are even more  potent than normal.
+Greater Spell Focus^TB^40^Choose a school of magic to which you already  have applied the Spell Focus feat. Your spells of that school are even more  potent than normal.
+Greater Spell Penetration^DD^50^The deity's spells are especially potent, defeating  spell resistance more readily than normal.
+Greater Spell Penetration^EL^69^Your spells are especially potent, defeating  spell resistance more readily than normal.
+Greater Spell Penetration^FRCS^35^Your spells are especially potent, defeating  spell resistance more readily than normal.
+Greater Spell Penetration^PH^94^Your spells are remarkably potent, breaking  through spell resistance more readily than normal.
+Greater Spell Penetration^TB^40^Your spells are especially potent, breaking  through spell resistance more readily than normal.
+Greater Two-Weapon Defense^CW^100^When fighting with two weapons, your defenses  are extraordinarily strong.
+Greater Two-Weapon Fighting^DD^50^The deity is a master at fighting two-handed.
+Greater Two-Weapon Fighting^EL^69^You are a master at fighting two-handed.
+Greater Two-Weapon Fighting^MW^23^You are a master at fighting two-handed.
+Greater Two-Weapon Fighting^PH^95^You are a master at fighting two-handed.
+Greater Weapon Focus^PH^95^Choose one type of weapon for which you have  already selected Weapon Focus. You can also choose unarmed strike or grapple  as your weapon for purposes of this feat. You are especially good at using  this weapon.
+Greater Weapon Specialization^PH^95^Choose one type of weapon for which you have  already selected Weapon Specialization You can also choose unarmed strike  or grapple as your weapon for purposes of this feat. You deal extra damage  when using this weapon.
+Greater Witchlight^Gh^34^Your witchlight can last longer, become hotter,  or give off more light.
+Green Bond^Gh^35^You have an empathic bond with one of the spirit  trees around Manifest
+Green Ear^CAd^110^Your bardic music can affect plant creatures.
+Green Ear^SaS^39^Your bardic music and virtuoso performance affect  plants and plant creatures.
+Greenbound Summoning^LE^8^You are learned in a long-forgotten manner of  summoning once practiced by Eaerlanni elves of the High Forest.
+Greensinger Initiate^ECS^54^You have embraced the druidic traditions of  the Greensingers, a chaotic Eldeen Reaches sect with close ties to the fey.
+Grell Alchemy^LoM^114^A creature that has this feat has studied the  alien and disturbing arcane lore of the grell, and understands the magical  and physical laws by which their spells and devices function.
+Grim Visage^Rac^163^Your eyes have seen a lot, and now they show  everyone that you aren't to be trifled with. Even glib people stammer in  your presence.
+Grizzly's Claws^CD^82^You can grow claws as sharp as those of a bear.
+Group Inspiration^CAd^192^Your bardic powers can inspire more allies than  normal.
+Group Inspiration^EL^57^You can inspire competence or greatness in more  than one ally simultaneously.
+Guardian Spirit^CAr^80^Your watchful spirit is more capable than normal.
+Guerrilla Scout^HB^97^You know how to use your senses to greater effect.
+Guerrilla Warrior^HB^97^You know how to move stealthily, even when armored.
+Halruuan Adept^ShS^20^You have studied the old cooperative spellcasting  traditions of Halruaa, and you are well-versed in the rites and arcana of  Halruuan magic.
+Hammer Fist^Rac^164^You are trained in an unarmed fighting style  that emphasizes a two-handed strike.
+Hammer's Edge^CW^113^You are a master of the style of fighting with  a hammer and sword at the same time.
+Hamstring^CW^100^You can wound your opponents' legs, hampering  their movement.
+Hamstring^SaS^39^You can wound an opponent's legs, hampering  his or her movement.
+Hand of Tyr^CSW^145^You have sacrificed your right hand to Tyr,  the Maimed God, proving your resilience and strength of spirit.
+Hands of a Healer^BE^43^You can heal more damage than normal by laying  on hands.
+Hardened Flesh^LM^27^Undead you raise or create can better handle  themselves in a fight.
+Harem Trained^Rac^164^You have been trained to serve as a jhasin (if  male) or jhasina (if female) and are well versed in song, music, dance,  art, the recitation of great literature, the art of massage, and other duties  of the harem.
+Haunting Appearance^Gh^35^You can make your ghost body assume a terrifying  appearance that can frighten observers.
+Haunting Melody^ECS^54^You can use your music to inspire fear.
+Haunting Voice^Gh^35^You can make your voice originate from another  location.
+Hawk's Vision^CAd^114^You can improve your visual acuity.
+Headlong Rush^Rac^164^You charge your foes with immense force, heedless  of your own safety.
+Healing Factor^ECS^55^When your current period of shifting ends, you  heal a limited amount of damage.
+Healing Factor^MM3^150^When your current period of shifting ends, you  heal a limited amount of damage.
+Healing Flames^Rac^164^You can draw energy from open flames to heal  yourself.
+Hear the Unseen^CAd^110^Your sense of hearing is so acute that you can  partially pinpoint an opponent's location by sound, allowing you to strike  even if the opponent is concealed or displaced.
+Heat Endurance^Sa^50^Either as a result of growing up in the waste,  or by training your body and mind to ignore the effects of searing heat,  you can exist with ease in high-temperature environments.
+Heat Tolerance^ShS^20^You are used to living in hot, humid conditions.
+Heavy Armor Optimization^RS^141^You have trained extensively in heavy armor,  and you have learned to take advantage of the protection it offers.
+Heavy Lithoderms^RS^141^You have stony growths on your skin that afford  you protection against attacks.
+Heighten Breath^Dr^70^Your breath weapon is even more deadly than  normal.
+Heighten Spell^PH^95^You can cast a spell as if it were a higher-level  spell than it actually is.
+Heighten Spell-Like Ability^CAr^80^You can use a spell-like ability as if it were  a higher spell-level equivalent than it actually is.
+Heighten Turning^DF^20^You can affect more powerful undead with your  turning or rebuking attempts.
+Heighten Turning^FP^214^You can affect more powerful undead with your  turning or rebuking attempts.
+Heighten Turning^Gh^35^You can affect more powerful undead with your  turning or rebuking attempts.
+Heighten Turning^LM^27^You can affect more powerful undead with your  turning or rebuking attempts.
+Hellbound Knight^CR^23^A devoted disciple of the Nine Hells, you have  sworn to strike down creatures that oppose law and threaten tyranny.
+Heroic Destiny^RD^152^You have a destiny to fulfill.
+Heroic Metamagic^RE^109^In times of great need, you can call upon a  heroic reserve of power to strengthen your spells.
+Heroic Spirit^ECS^55^You have a larger reservoir of luck than the  average hero.
+High Sword Low Axe^CW^113^You have mastered the style of fighting with  a sword and axe at the same time.
+Highborn Drow^Rac^164^You have learned how to tap into the advanced  magical abilities of your drow noble heritage.
+Highborn Drow^Und^25^You have learned how to tap into the advanced  magical abilities available to you through your drow noble heritage.
+Hin Wandermage^Rac^164^You have a natural affinity for spells that  take you from place to place.
+Hindering Song^EL^57^Your bardic music interferes with opposing spellcasters.
+Hold the Line^CW^100^You are trained in defensive techniques against  charging opponents.
+Hold the Line^DD^51^The deity is trained in defensive techniques  against charging opponents.
+Hold the Line^SF^7^You are trained in defensive techniques against  charging opponents.
+Hold the Line^ShS^20^You are trained in defensive techniques against  charging opponents.
+Holy Ki Strike^BE^44^Your unarmed attacks deal extra damage to evil  creatures.
+Holy Radiance^BE^44^You can increase the intensity of the light  surrounding you to damage undead creatures.
+Holy Strike^CD^89^Your attacks deal great damage to evil creatures.
+Holy Strike^EL^57^Your attacks deal great damage to evil creatures.
+Holy Subdual^BE^44^You can turn bonus damage into nonlethal damage.
+Honest Merchant^OA^63^Your ancestor, Bayushi Tesaguri, was the son  of Bayushi Junzen, Scorpion Clan Champion.
+Honor-Bound^DCS^86^Keeping your word and upholding your honor is  of great importance to you.
+Horrific Appearance^Gh^35^You can blast creatures with your simple appearance.
+Horse Nomad^FRCS^35^You have been raised in a culture that relies  upon riding and shooting for survival.
+Horse Nomad^PG^39^You have been raised in a culture that relies  upon riding and shooting.
+Hostile Mind^XPH^47^Your mind recoils violently against those who  use psionics against you.
+Hover^MM^304^The creature can come to a halt in midair.
+Hover^MM2^18^The creature can halt its forward motion while  flying, regardless of maneuverability.
+Hulking Brute ^DCS^86^You possess a truly impressive stature.
+Human Heritage^RD^152^Your human heritage is more prominent than in  others of your kind.
+Hurling Charge^MH^27^You are trained in using thrown weapons as part  of a charge attack.
+Hyena Tribe Hunter^ShS^20^You have learned the secrets of hunting from  the hyena that roams the lands where your tribe wanders.
+Iaijutsu Master^OA^63^You are not only descended from Kakita, the  greatest duelist ever to have lived, but you share a karmic tie to his spirit.
+Ice Harmonics^Fr^48^Your summon spells work better in the frostfell  if you summon native creatures.
+Ice Troll Berserker^UE^44^When raging, your skin becomes very thick and  tough like the ice trolls that plague parts of your homeland.
+Icy Calling^Fr^48^You can use your voice to shatter ice.
+Ignore Material Components^EL^57^You need not use any material components in  casting spells.
+Imprint Stone^XPH^47^You can create power stones to store psionic  powers.
+Improve Bull Rush^PH^95^You know how to push opponents back.
+Improved Aid^OA^63^You are descended from Hida Tadaka, the great  Crab daimyo who gave his life to avert a war between his clan and the Lion.
+Improved Alignment-Based Casting^EL^57^Your spells of a particular alignment are more  powerful than normal.
+Improved Arrow of Death^EL^57^Your arrows of death are harder to resist.
+Improved Assume Supernatural Ability^SS^35^You gain skills using a supernatural ability  of an assumed form.
+Improved Aura of Courage^EL^57^Your aura of courage is stronger than normal.
+Improved Aura of Despair^EL^57^Your aura of despair is wider than normal.
+Improved Buckler Defense^CW^100^You can attack with an off-hand weapon while  retaining a buckler's shield bonus to your Armor Class.
+Improved Cohort^HB^98^You attract a more powerful cohort than you  normally would.
+Improved Cold Endurance^Fr^48^Your training and natural hardiness have improved  your natural resistance to cold temperatures.
+Improved Combat Casting^CAr^192^You heighten your ability to cast spells while  threatened without fear of being attacked.
+Improved Combat Casting^EL^57^You can cast spells while threatened without  fear of being attacked.
+Improved Combat Expertise^CW^100^You have mastered the art of defense in combat.
+Improved Combat Reflexes^EL^57^You can respond to any number of opponents who  let their defenses down.
+Improved Control Visage^Gh^35^You can change your ghost form's appearance.
+Improved Cooperative Metamagic^PG^136^Your ability to enhance an ally's spell during  casting is expanded.
+Improved Counterspell^EL^70^You understand the nuances of magic to such  an extent that you can counter your opponent's spells with great efficiency.
+Improved Counterspell^FRCS^35^You understand the nuances of magic to such  an extent that you can counter your opponent's spells with great efficiency.
+Improved Counterspell^PH^95^You understand the nuances of magic to such  an extent that you can counter your opponent's spells with great efficiency.
+Improved Critical^PH^95^Choose one type of weapon. With that weapon,  you know how to hit where it hurts.
+Improved Damage Reduction^ECS^55^You gain damage reduction or improve your existing  damage reduction.
+Improved Darkvision^EL^58^Your ability to see in the dark is greater than  normal.
+Improved Death Attack^EL^58^Your death attack is harder to overcome.
+Improved Deflection^Gh^35^You are adept at deflecting things before they  strike you.
+Improved Disarm^PH^95^You know how to disarm opponents in melee combat.
+Improved Diversion^CAd^110^You can create a diversion to hide quickly and  with less effort.
+Improved Draconian Breath Weapon^DCS^86^You have mastered your draconic heritage and  improved on your innate breath weapon.
+Improved Elemental Heritage^PlH^40^You have manifested an even stronger tie to  your elemental ancestor, resulting in a minor resistance to elemental effects.
+Improved Elemental Wild Shape ^DMG^209^You can take the form of a larger variety of  elementals than normal.
+Improved Elemental Wild Shape^EL^58^You can take the form of a greater variety of  elementals than normal.
+Improved Energy Drain^LM^27^You draw extra power from your energy-drained  victims.
+Improved Energy Resistance^Rac^164^Choose one form of energy to which you have  a natural (not spell- or item-generated) resistance. Your inherent resistance  to this kind of energy is more effective than normal.
+Improved Familiar^CW^100^This feat allows spellcasters to acquire a new  familiar from a nonstandard list, but only when they could normally acquire  a new familiar.
+Improved Familiar^FRCS^35^So long as you are able to acquire a new familiar,  you may choose your new familiar from a nonstandard list.
+Improved Familiar^PG^39^Refer to the Improved Familiar feat description  on page 200 of the Dungeon Master's Guide.
+Improved Familiar^Rac^165^See the discussion of the Improved Familiar  feat in Chapter 1 of the Forgotten Realms Campaign Setting. Table A-5 shows  additional improved familiars from this book that are available with this  feat.
+Improved Familiar^SK^146^Refer to the Improved Familiar feat description  in the Dungeon Master's Guide.
+Improved Familiar^TB^40^As long as you are able to acquire a new familiar,  you may choose your new familiar from a nonstandard list.
+Improved Favored Enemy^CW^101^You know how to hit your favored enemies where  it hurts.
+Improved Favored Enemy^DMG^210^Gain bonuses against favored enemies.
+Improved Favored Enemy^EL^58^You gain bonuses against favored enemies.
+Improved Feint^PH^95^You are skilled at misdirecting your opponent's  attention in combat.
+Improved Fiendish Servant^CR^20^You gain the service of a powerful fiendish  animal servitor.
+Improved Flight^CAd^110^You gain greater maneuverability when flying  than you would normally have.
+Improved Flight^MW^23^You gain greater maneuverability when flying  than you would normally have.
+Improved Flight^Rac^165^You gain greater maneuverability when flying  than you would normally have.
+Improved Flight^RW^151^You have gained greater maneuverability when  flying than you would normally have.
+Improved Flight Item (Item Creation)^Sh^157^You have learned to make use of the manifest  zone in Sharn to craft magic items that grant superior flight.
+Improved Flyby Attack^EL^70^The creature can attack on the wing with increased  mobility.
+Improved Flyby Attack^SS^36^You can attack on the wing with increased mobility.
+Improved Fortification^ECS^55^You improve your warforged fortification, gaining  immunity to sneak attacks and extra damage from critical hits.
+Improved Fortification^MM3^192^You improve your warforged fortification, gaining  immunity to sneak attacks and extra damage from critical hits.
+Improved Frosty Touch^Fr^49^Your frosty touch causes more cold damage.
+Improved Ghost Flight^Gh^35^Your ghost body can fly rapidly.
+Improved Grapple ^DD^51^The deity is skilled in martial arts that emphasize  holds and throws.
+Improved Grapple^OA^63^You are skilled in martial arts that emphasize  holds and throws.
+Improved Grapple^PH^95^You are skilled at grappling opponents.
+Improved Grapple^UE^44^You are skilled in martial arts that emphasize  holds and throws.
+Improved Heat Endurance^Sa^50^You can survive even in the most extreme natural  heat conditions.
+Improved Heighten Spell^EL^58^You can cast a spell at any level above its  own.
+Improved Initiative^PH^96^You can react more quickly than normal in a  fight.
+Improved Ki Strike^EL^58^You can strike opponents with great damage reduction.
+Improved Levitation^Rac^165^You have learned to use part of your levitate  spell-like ability at a time, allowing multiple uses with a shorter duration.
+Improved Levitation^Und^25^You have learned to use only part of your levitate  spell-like ability at a time, allowing multiple uses with shorter duration.
+Improved Low Blow^Rac^165^You are especially good at using the Low Blow  feat.
+Improved Low-Light Vision^EL^58^The range of your low-light vision is greater  than normal.
+Improved Maneuverability^Dr^70^Your maneuverability in flight improves.
+Improved Manifestation^EL^58^You can manifest psionic powers more powerful  than the normal limits of manifestation.
+Improved Manifestation^XPH^34^You increase your power point reserve.
+Improved Manyshot^EL^58^You can fire even more arrows as a single attack  against a nearby target.
+Improved Metamagic^DMG^210^You can cast spells using metamagic feats more  easily than normal.
+Improved Metamagic^EL^59^You can cast spells using metamagic feats more  easily than normal.
+Improved Metapsionics^XPH^34^You can manifest powers using metapsionic feats  more often than normal.
+Improved Mounted Archery^CW^101^You can make ranged attacks from a mount almost  as well as you can from the ground.
+Improved Multiattack^Dr^70^You are particularly adept at using all your  natural weapons at once.
+Improved Multiattack^EL^70^The creature is particularly adept at using  all its natural weapons at once.
+Improved Multiattack^SS^36^You are particularly adept at using all your  natural weapons at once.
+Improved Multiweapon Fighting^DD^51^A deity with three or more hands can fight with  a weapon in each hand.
+Improved Multiweapon Fighting^EL^70^A creature with three or more hands can fight  with a weapon in each hand.
+Improved Multiweapon Fighting^SS^36^You are expert at fighting with a weapon in  each of your three or more hands. You can make up to two attacks per round  with each off-hand weapon.
+Improved Natural Armor^MM^304^The creature's natural armor is thicker and  harder than that of other of its kind.
+Improved Natural Armor^MM3^206^The natural armor of a creature with this feat  is thicker and harder than normal for its kind.
+Improved Natural Armor^Rac^165^Your skin is even tougher than that of most  of your kind.
+Improved Natural Attack^ECS^55^One of a creature's natural attacks is more  dangerous than its type and size would otherwise indicate.
+Improved Natural Attack^MM^304^The creature's natural attacks are more dangerous  than its size and type would otherwise dictate.
+Improved Natural Attack^MM3^206^The natural attacks of a creature with this  feat are more dangerous than its size and type would otherwise dictate.
+Improved Outer Planar Heritage^PlH^40^Your ancestral tie to the Outer Planes manifests  as an ability to deal damage with your natural attacks as if they matched  the alignment of your ancestors.
+Improved Overrun^PH^96^You are skilled at knocking down opponents.
+Improved Overrun^SF^7^You are trained in knocking over opponents that  are smaller than you.
+Improved Paralysis^LM^27^You are better at paralyzing your victims.
+Improved Poltergeist Hand^Gh^36^You can move a large object at a distance when  you are a ghost.
+Improved Precise Shot^PH^96^Your ranged attacks can ignore the effects of  cover or concealment.
+Improved Psicrystal^XPH^47^You can upgrade your psicrystal.
+Improved Rapid Shot^CW^101^You are an expert at firing weapons with exceptional  speed.
+Improved Rapidstrike^Dr^70^You can make multiple attacks with a natural  weapon.
+Improved Resiliency^RE^119^You gain a construct's resistance to nonlethal  damage.
+Improved Resist Dragonfear^DCS^86^You are able to demonstrate great courage in  the presence of dragons.
+Improved Rock Hurling^RS^141^Your accuracy and effectiveness with thrown  rocks improves.
+Improved Scent^SS^36^You can detect and track creatures by smell  at greater distances than normal.
+Improved Shield Bash^DF^20^You can push opponents back by bashing them  with your shield.
+Improved Shield Bash^PH^96^You can bash with a shield while retaining its  shield bonus to your Armor Class.
+Improved Shieldmate^MH^27^You have an outstanding ability to protect those  near you with your shield.
+Improved Sigil (Aesh)^RD^152^You tap into your aesh power sigil to  gain enhanced accuracy with your favored melee weapons.
+Improved Sigil (Hoon)^RD^152^You tap into your hoon power sigil to  help survive deadly conditions.
+Improved Sigil (Krau)^RD^153^You tap into your krau power sigil to  augment the energy of your magical utterances.
+Improved Sigil (Naen)^RD^153^You tap into your naen power sigil to  see through illusions and resist language-based effects.
+Improved Sigil (Uur)^RD^153^You tap into your uur power sigil to  gain enhanced accuracy with ranged weapons.
+Improved Sigil (Vaul)^RD^153^You tap into your vaul power sigil to  resist mental effects.
+Improved Skirmish^CAd^192^Your combat mobility improves.
+Improved Smiting^CD^82^Your smite attacks deal more damage to specific  foes, and can damage creature with alignment-based damage reduction.
+Improved Snatch^Dr^71^You can make snatch attacks against bigger opponents  than other creatures can.
+Improved Snatch Spell^PG^136^When you take over a spell from another spellcaster,  you gain more control over its effect.
+Improved Sneak Attack^DMG^210^Your sneak attacks are more deadly than normal.
+Improved Sneak Attack^EL^59^Your sneak attacks are more deadly than normal.
+Improved Speed ^Dr^71^You are faster than others of your kind.
+Improved Spell Capacity^DMG^210^You can prepare spells that exceed the normal  limits of spellcasting.
+Improved Spell Capacity^Dr^71^You can prepare spells that exceed the normal  limits of spellcasting.
+Improved Spell Capacity^EL^59^You can prepare spells that exceed the normal  limits of spellcasting.
+Improved Spell Resistance^EL^60^Your innate resistance to magical effects increases.
+Improved Spellpool Access^PG^136^You can use your spellpool access to call spells  of greater than normal power.
+Improved Spit^SK^146^You can spit farther than normal.
+Improved Stunning Fist^DMG^210^Your stunning attack is more powerful.
+Improved Stunning Fist^EL^60^Your stunning attack is more powerful.
+Improved Sudden Strike^CAd^192^Your ability to strike unaware foes improves.
+Improved Sunder^DD^51^The deity is adept at placing its attacks precisely  where it wants them to land.
+Improved Sunder^EA^42^You are adept at placing your attacks precisely  where you want them to land.
+Improved Sunder^PH^96^You are skilled at attacking your opponents'  weapons and shields, as well as other objects.
+Improved Sunder^SF^7^You are adept at placing your attacks precisely  where you want them to land.
+Improved Swimming^CAd^110^You can swim faster than you normally could.
+Improved Swimming^MW^23^You swim faster than you normally could.
+Improved Toughness^CW^101^You are significantly tougher than normal.
+Improved Toughness^LM^27^You are significantly tougher than normal.
+Improved Toughness^MM3^207^A creature with this feat is significantly tougher  than normal.
+Improved Trip^PH^96^You are trained not only in tripping opponents  safely but also following through with an attack.
+Improved Turn Resistance^Gh^36^You are better able to resist the channeling  of positive or negative energy by clerics and similar classes.
+Improved Turn Resistance^LM^27^You have a better than normal chance to resist  turning.
+Improved Turn Resistance^SS^36^You have a better than normal chance to resist  turning.
+Improved Turning^PH^96^Your turning or rebuking attempts are more powerful  than normal.
+Improved Two-Weapon Defense^CW^101^You gain a significant defensive advantage while  fighting with two weapons.
+Improved Two-Weapon Fighting^PH^96^You are an expert in fighting two-handed.
+Improved Unarmed Strike^PH^96^You are skilled at fighting while unarmed.
+Improved Weapon Familiarity^CW^101^You are familiar with all exotic weapons common  to your people.
+Improved Weapon Familiarity^RS^141^You are familiar with all exotic weapons common  to your people.
+Improved Web^SS^36^You gain additional utility from your webs.
+Improved Whirlwind Attack^EL^60^You become a blurry whirlwind of attacks, striking  out at all enemies near your position.
+Incite Rage^EL^60^You can incite allies into a rage.
+Incorporeal Form^Gh^36^You can become incorporeal even when you would  otherwise be forced to manifest fully.
+Incorporeal Spell Targeting^Gh^36^You know how to cast your spells so they're  more likely to affect incorporeal creatures.
+Incorporeal Target Fighting^Gh^36^You know how to fight incorporeal creatures  in melee.
+Ineluctable Echo^UA^93^Those who use words of power around you hear  the sound of their own voices.
+Infernal Bargainer^Rac^165^You are comfortable making deals with powerful  entities from the Lower Planes.
+Infinite Deflection^EL^61^You can deflect an infinite number of projectiles.
+Inhuman Reach^LoM^180^Your arms elongate, allowing you to touch the  floor with your hands.
+Inhuman Vision^LoM^180^You possess the inhuman eyes of some strange  creature.
+Initiate of Ghaunadaur^CR^23^You have learned the dread secrets of the god  of oozes, slimes, jellies, and outcasts.
+Initiate of Gruumsh^CR^24^The singular eye of the great orc god Gruumsh  watches over you.
+Initiate of Kossuth^CR^24^You have faced the fierce elemental flame and  unlocked some of the secrets of Kossuth's church.
+Initiate of Loviatar^CR^24^With great pain comes great power. This and  other secrets you have learned from the church of Loviatar.
+Initiate of Loviatar^ShS^20^You have been initiated into the greatest secrets  of Loviatar's church.
+Initiate of Shar^CR^24^You have been initiated into the greatest secrets  of Shar's church.
+Initiate of Shar^CSW^145^You have been initiated into the greatest secrets  of Shar's church.
+Initiate of Varae^CR^25^You fervently worship Varae, the serpentine  goddess, and guard well the secrets of your faith.
+Innate Spell^CAr^80^You have mastered a spell so thoroughly that  you can now use it as a spell-like ability.
+Innate Spell^FRCS^36^You have mastered a spell so thoroughly that  you can now use it as a spell-like ability.
+Innate Spell^PG^39^You have mastered a spell so thoroughly that  you can now use it as a spell-like ability.
+Innate Spell^TB^41^You have mastered a spell so thoroughly that  you can now use it as a spell-like ability.
+Inquisitor^XPH^48^You know when others lie.
+Inscribe Epic Runes^PG^136^You can inscribe runes of epic power.
+Inscribe Rune^FRCS^36^You can create magic runes that hold spells  until triggered.
+Inscribe Rune^PG^40^You can create magic runes that hold spells  until triggered.
+Inside Connection^RD^153^Choose a specific organization. You have strong  personal connections within that organization, as well as insight into its  membership.
+Insidious Magic^FRCS^36^You can use the Shadow Weave to make your spells  harder for Weave users to detect.
+Insidious Magic^PG^40^You can use the Shadow Weave to make your spells  harder for Weave users to dispel.
+Insightful^CAr^80^You possess a magical understanding of the workings  of arcane detection.
+Insightful Reflexes^CAd^110^Your keen intellect allows you an uncanny knack  for evading dangerous effects.
+Inspirational Leadership^HB^98^Your cohort and followers are exceptionally  faithful to your cause.
+Inspire Excellence^EL^61^You can improve the abilities of your comrades  through your performance.
+Inspire Spellpower^RS^141^You can use your bardic music to increase the  power of your allies' spells.
+Instant Reload^EL^61^Choose one type of crossbow, such as heavy crossbow.  You can fire that type of crossbow as fast as a bow.
+Instantaneous Rage^CW^102^You activate your rage instantly.
+Instantaneous Rage^MW^23^You activate your rage instantly.
+Intensify Spell^EL^61^You can cast spells with exceptionally great  effect.
+Intimidating Rage^CW^102^Your rage engenders fear in your opponents.
+Intimidating Rage^MW^24^Your rage engenders fear in your opponents.
+Intuitive Attack^BE^44^You fight by faith more than brute strength.
+Inured to Energy^SS^36^You can resist energy attacks more efficiently  than normal.
+Invest Armor^RS^141^You can charge your armor with additional protective  qualities.
+Investigate^ECS^55^You can use the Search skill to find and analyze  clues at the scene of a crime or a mystery.
+Investigator^PH^96^You have a knack for finding information.
+Involuntary Rage^SS^36^Extreme pain drives you berserk.
+Iron Mind^Rac^165^You are descended from duergar who escaped enslavement  by the illithids. The blood of these psionic-resistant former thralls runs  thick in your veins.
+Iron Will^PH^97^You have a stronger will than normal.
+Ironskin Chant^CAd^113^You can channel the power of your bardic music  to enable yourself to ignore minor injuries.
+Ironwood Body^RE^119^Your body is crafted with a layer of hard ironwood  that cushions blows.
+Irresistible Gaze^SK^146^Your gaze attack is more potent than normal.
+Irresistible Gaze^SS^37^Your gaze attack is more potent than normal.
+Item Reprieve^LE^8^You learn how to use items from a school of  magic previously prohibited to you.
+Jack of All Trades^CAd^110^You have picked up a smattering of even the  most obscure skills.
+Jack of All Trades^DD^51^The deity has picked up a smattering of even  the most obscure skills.
+Jack of All Trades^FP^214^You've picked up a smattering of even the most  obscure skills.
+Jack of All Trades^SaS^40^You've picked up a smattering of even the most  obscure skills.
+Jaws of Death^RE^119^Gnashing teeth and a powerful set of jaws allow  you to bite foes.
+Jergal's Pact^LE^8^You have made a bargain with Jergal, seneschal  to the god of death.
+Jester's Magic^CSW^145^You are a skilled master of magical jests, capable  of inciting audiences to laughter or lulling them to sleep.
+Jotunbrud^Rac^166^You are descended from the giants who ruled  the mountain-spanning empire of Ostoria in ages past, and possess a truly  impressive stature.
+Judged by Aurifar^Sa^50^Aurifar, the Caliph of the Sky, has judged you,  and he shows you special favor.
+Jungle Stamina^Rac^166^You are acclimated to the disease-ridden jungles  of southwestern Faerun.
+Kalashtar Thoughtshifter^RE^118^You have learned to control your mind blade  for maximum impact in battle.
+Kami's Intuition^OA^63^You are descended from Shinjo, the first Unicorn,  the kindest and most compassionate of the kami.
+Karmic Strike^CW^102^You have learned to strike when your opponent  is more vulnerable -- the same instant your opponent strikes you.
+Karmic Strike^OA^63^You have learned to strike when your opponent  is more vulnerable -- the same instant your opponent strikes you.
+Karmic Twin^OA^64^You are descended from Bayushi, the first Scorpion,  whos love for his daughter proved his final downfall.
+Keen Intellect^OA^64^You are descended from Agasha, the founder of  the original Dragon shugenja school, a shugenja known for her keen intellect  and powers of observation.
+Keen Strike^EL^61^Your unarmed strikes become as sharp as blades.
+Ki Shout^OA^64^You can bellow forth a ki-empowered shout  that strikes terror into your enemies.
+Kiai Shout^CW^102^You can bellow forth a shout that strikes terror  into your enemies.
+Kihu-Sherem Guardian^Gh^36^You are one of the Kihu-Sherem, magically altered  in the womb to allow you to better protect the sorcerers of your homeland.
+Killoren Ancient^RW^151^You favor the killoren aspect of the ancient.
+Killoren Destroyer^RW^151^You favor the killoren aspect of the destroyer.
+Killoren Hunter^RW^151^You favor the killoren aspect of the hunter
+Knifefighter^PG^40^You're an expert at using weapons in a grapple.
+Knight of Stars ^BE^44^You swear allegiance to the Court of Stars,  the paragons of the eladrin, and in exchange gain power to act on their  behalf.
+Knight Training^ECS^56^You are part of a knightly order that combines  the divine calling of the paladin class with another form of training.
+Knockback^RS^142^By putting your bulk behind a blow, you can  push your enemy backward.
+Knock-Down^DD^51^The deity's mighty blows can knock foes off  their feet.
+Knock-Down^SF^7^Your mighty blows can knock foes off their feet.
+Landlord^SB^10^By knowing the right nobles, making contacts  with masons and artisans, or performing great deeds for a liege-lord, you  have resources that help you build and expand your stronghold.
+Landwalker^Rac^166^You can survive out of water for a longer period  of time than most of your kind.
+Landwalker^Sto^92^You can survive out of water for a longer period  of time than most of your kind.
+Large and In Charge^Dr^71^You can prevent opponents from closing inside  your reach.
+Large and in Charge^SF^61^You can prevent opponents from closing inside  your reach.
+Lasting Inspiration^DMG^210^Your songs continue to inspire allies long after  your words have faded.
+Lasting Inspiration^EL^61^Your songs continue to inspire allies long after  your words have faded.
+Lasting Life^LM^28^You can shed negative levels with an act of  will.
+Law Inviolate^RD^155^Your unshakable faith in St. Cuthbert allows  you to better apprehend fugitives or overcome villains who transgress the  law.
+Leadership^PH^97^You are the sort of person others want to follow,  and you have done some work attempting to recruit cohorts and followers.
+Leap Attack^CAd^110^You can combine a powerful charge and a mighty  leap into one devastating attack.
+Least Dragonmark^ECS^56^You have a least dragonmark.
+Least Legacy^WL^14^You awaken the basic abilities of a specific  item of legacy.
+Legacy Focus^WL^15^Your item's legacy abilities are more potent  than normal.
+Legendary Acrobat^CAd^192^You can balance and tumble much more easily  than a normal person.
+Legendary Artisan^ECS^56^You have mastered the method of creating magic  items.
+Legendary Climber^CAd^192^You can climb rapidly much more easily than  a normal person.
+Legendary Climber^EL^61^You can climb rapidly much more easily than  a normal person.
+Legendary Commander^EL^62^You attract and lead great armies of followers  through sheer force of personality.
+Legendary Leaper^CAd^192^You can cover great distances with only a brief  start.
+Legendary Leaper^EL^62^You can jump much farther than normal for your  size.
+Legendary Rider^CW^152^You can ride a mount in combat with ease, even  bareback.
+Legendary Rider^EL^62^You can ride any mount without penalty (even  bareback) and can control any mount in combat.
+Legendary Tracker^CAd^192^You can track prey across or through the water,  or even through the air.
+Legendary Tracker^EL^62^You can track prey across or through the water,  or even through the air.
+Legendary Wrestler^EL^62^You are exceptionally proficient at grappling.
+Lesser Dragonmark^ECS^56^You have a lesser dragonmark.
+Lesser Legacy^WL^15^You awaken more powerful abilities of a specific  item of legacy.
+Lichloved^BV^49^By repeatedly committing perverted sex acts  with the undead, the character gains dread powers.
+Life Drain^LM^28^You drain additional life energy from your foes.
+Life Leech^UA^93^You automatically try to steal the last bit  of life energy from anyone nearby.
+Lifebond^LM^28^Select a specific living creature that is friendly  to you. You create a special bond with that creature.
+Lifesense^LM^28^You see the light that all living creatures  emit.
+Light of Aurifar^Sa^51^Undead that you turn or rebuke immolate.
+Light to Daylight^Rac^166^Your inherent ability to create light is more  powerful than normal.
+Lightbringer^Rac^166^You can channel positive energy into your spells  so that they glow with holy power.
+Lightfeet^RW^151^You have an incredibly soft step, making it  difficult to track or hear you.
+Lightning Fists^SF^7^Your skill and agility allow you to attempt  a series of blindingly fast blows.
+Lightning Mace^CW^113^You are a master of fighting with two maces  at the same time.
+Lightning Reflexes^PH^97^You have faster than normal reflexes.
+Lingering Breath^Dr^71^Your breath weapon forms a lingering cloud.
+Lingering Damage^EL^62^Your sneak attacks continue to deal damage even  after you strike.
+Lingering Song^CAd^111^Your inspirational bardic music stays with the  listeners long after the last note has died away.
+Lingering Song^SaS^40^Your bardic music stays with the listeners long  after the last note has died away.
+Lingering Spell^CR^20^Residual eldritch energy from your spell continues  to harm your enemies after the spell's main effect has expired.
+Lion Spy^OA^64^Your ancestor, Akodo Shinju, was the greatest  spy of the Lion clan.
+Lion Tribe Warrior^ShS^20^You have learned how to pounce on your foes,  like the lion that roams your lands.
+Lion's Pounce^CD^82^You can deliver a terrible attack at the end  of a charge.
+Live My Nightmare^UA^94^Those who magically pry into your mind become  privy to your most frightening dreams.
+Lliira's Blessing^PG^176^Thanks to the favor of the goddess of freedom,  you are difficult to restrain.
+Lolth's Blessing^Rac^166^The Spider Queen has blessed you with additional  magical abilities.
+Lolth's Meat^Und^26^Like all drow raised in cities that are ruled  by Lolth's priestesses, you know that you exist only to provide your goddess  with food and pleasure. This knowledge lends you a certain bloodthirsty  readiness.
+Long Reach^UE^44^You know how to use your great stature to reach  an opponent more than 5 feet away with a spearlike weapon.
+Longstride Elite^ECS^57^Your shifter trait improves.
+Longstride Elite^RE^114^Your longstride shifter trait improves.
+Longtooth Elite^RE^114^Your longtooth shifter trait improves.
+Lord of the Uttercold^CAr^80^Through careful study of the Elemental Planes  and their interactions with the Negative Energy Plane, you have learned  to wield the uttercold.
+Low Blow^Rac^166^You can get underfoot and attack creatures larger  than you.
+Low Profile^UA^182^You are less famous than others of your class  and level, or you wish to maintain a less visible presence than others of  your station.
+Luck of Heroes^FRCS^36^Your land is known for producing heroes; you  receive a luck bonus on all saving throws.
+Luck of Heroes^OA^64^You are descended from the quick-footed and  quick-witted Hiruma, the archetypal hunter and scout.
+Luck of Heroes^PG^40^Your land is known for producing heroes.
+Lunar Magic^CSW^146^Your spells and spell-like abilities are tied  to the phase of the moon, rising and falling with the strength of Selune.
+Lycanthropic Spell^FP^214^You cast spells while in your lycanthropic animal  form.
+Lyric Spell^CAd^113^You can channel the power of your bardic music  into your magic, allowing you to expend uses of your bardic music ability  to cast spells.
+Mage Slayer^CAr^81^You have studied the ways and weaknesses of  spellcasters and can time your attacks and defenses against them expertly.
+Mage Slayer^MH^27^You have studied the ways and weaknesses of  spellcasters and can time your attacks and defenses against them expertly.
+Magic in the Blood^OA^64^You claim a karmic link with Iuchi, one of the  most resourceful shugenjas in early Rokugan.
+Magic in the Blood^PG^40^You have a knack for getting the most out of  your innate magic abilities.
+Magic of the Land^RW^152^Your intimate understanding of the natural world  allows you to imbue your spells with life-giving magical power from the  land itself.
+Magical Aptitude^PH^97^You have a knack for magical endeavors.
+Magical Artisan^FRCS^36^You have mastered the method of creating certain  magic items.
+Magical Artisan^OA^64^You are descended from Asahina Yajinden, a shugenja  of Crane clan who became the greatest lieutenant of the dread sorcerer Iuchiban.
+Magical Artisan^PG^41^You have mastered the method of creating a certain  kind of magic item.
+Magical Beast Wild Shape^CD^90^You can wild shape into magical beast form.
+Magical Beast Wild Shape^EL^62^You can wild shape into magical beast form.
+Magical Training^FRCS^36^Every crafter and laborer knows a cantrip or  two to ease her work.
+Magical Training^PG^41^You come from a land where cantrips are taught  to all who have the aptitude to learn magic.
+Magistrate's Mind^OA^64^You claim descent from Soshi Saibankan, a great  Scorpion judge who helped establish the Empire's institution of Emerald  magistrates.
+Malevolence^Gh^36^You can possess a creature and control its actions.
+Malign Spell Focus^BV^49^The character's spells that have the evil descriptor  are more potent than normal due to a deal she makes with an evil power.
+Malign Spell Focus^CR^20^Your evil spells are more potent than normal  due to a deal forged with an evil power.
+Manifest Flight^Sh^157^You have learned to make use of the manifest  zone in Sharn to improve your natural ability to fly.
+Manifest Leap^Sh^157^You have learned to make use of the manifest  zone in Sharn to increase your ability to jump and reduce the damage you  take when you fall.
+Mantis Leap^SF^7^You deliver a powerful attack after making a  jump.
+Many Masks^OA^64^You are descended from Shosuro Furuyari, an  important Scorpion playwright.
+Manyshot^EL^70^You can fire multiple arrows as a single attack  against a nearby target.
+Manyshot^PH^97^You can fire multiple arrows simultaneously  against a nearby target.
+Mark of Hleid^Fr^49^You bear a mark that identifies you as an ally  of the church of Hleid and grants you supernatural qualities.
+Markings of the Blessed^RS^142^Your skin markings shift into a pattern that  resists a wide array of harmful effects in times of trouble.
+Markings of the Hunter^RS^142^Your skin markings shift into a pattern that  makes you hard to get the drop on.
+Markings of the Magi^RS^142^Your skin markings shift into a pattern that  denotes you as having strong magical talent.
+Markings of the Maker^RS^142^Your skin markings shift into a pattern that  gives you fate's edge when using skills.
+Markings of the Warrior^RS^142^Your skin markings have shifted over time into  a pattern that gives you fate's deathly accuracy in times of trouble.
+Martial Throw^MH^27^You can switch positions with an opponent you  hit in melee by throwing that opponent.
+Martial Weapon Proficiency^PH^97^Choose a type of martial weapon. You understand  how to use that type of martial weapon in combat.
+Master Legacy^WL^15^You temporarily gain access to legacy abilities  beyond your normal reach.
+Master Linguist^RE^109^You have a broad knowledge of language.
+Master Staff^CAr^192^You can activate a staff without using a charge.
+Master Staff^EL^62^You can activate a staff without using a charge.
+Master Wand^CAr^192^You can activate a wand without using a charge.
+Master Wand^EL^62^You can activate a wand without using a charge.
+Maximize Breath^Dr^71^You can take a full-round action to use your  breath weapon to maximum effect.
+Maximize Power^XPH^48^You can manifest powers to maximum effect.
+Maximize Spell^PH^97^You can cast spells to maximum effect.
+Maximize Spell-Like Ability^CAr^81^You can use a spell-like ability at its maximum  effect.
+Meditation of War Mastery^OA^81^You have mastered the martial arts style of  'Meditation of War' -- a hard/soft form emphasizing weapon use and strikes  to pressure points.
+Memory Eater^LoM^22^An aboleth with this feat is particularly adept  at extracting memories and knowledge from the bodies of those it consumes.
+Menacing Demeanor^RD^155^You can tap into your savage heritage to improve  your intimidation techniques.
+Mental Leap^XPH^48^You can make amazing jumps.
+Mental Resistance^XPH^48^Your mind is armored against mental intrusion.
+Mentor^DMG2^176^A character who takes this feat has offered  his knowledge and skill to a lower-level NPC and takes that NPC on as an  apprentice.
+Mercantile Background^FRCS^36^You come from a family that excels at a particular  trade and knows well the value of any kind of trade good or commodity.
+Mercantile Background^PG^41^You come from a wealthy family with numerous  contacts in the trading costers and craft guilds of Faerun's bustling cities.
+Metallurgy^Rac^166^You are skilled in the act of metallurgy, creating  metal alloys both for their appearance and their properties.
+Metamagic Song^RS^142^You can channel the power of your bardic music  into your magic, allowing you to pay the cost of metamagic feats by spending  uses of your bardic music ability.
+Metamorphic Transfer^XPH^48^You can gain a supernatural ability of a metamorphed  form.
+Metanode Spell^CR^25^You cast metamagic spells to greater effect  in nodes to which you are attuned than elsewhere.
+Metanode Spell^Und^26^You cast metamagic spells to greater effect  in earth nodes than elsewhere.
+Metaray^LoM^45^A beholder with this feat can apply the effects  of metamagic feats to its eye rays.
+Might Makes Right^Rac^166^Your great strength draws more followers.
+Mighty Leaping^SS^37^You have developed your leg muscles and trained  yourself to make mighty leaps.
+Mighty Rage^EL^63^Your rage becomes even more powerful than normal.
+Mighty Roar^SS^37^You unsettle opponents with a dreadful roar  as you attack.
+Mighty Works Mastery I^OA^80^You have mastered the initial secrets of the  'Mighty Works' martial arts style -- a hard/soft form emphasizing locks  and hand strikes.
+Mighty Works Mastery II^OA^80^You have mastered the deeper secrets of the  'Mighty Works' martial arts style.
+Militia^FRCS^36^You served in a local militia, training with  weapons suitable for use on the battlefield.
+Militia^Gh^37^You served in a local militia, training with  weapons suitable for use on the battlefield.
+Militia^PG^41^Your people rely on a well-trained and well-armed  militia to defend their land.
+Mind Over Body^FRCS^37^The arcane spellcasters of some lands have learned  to overcome the frailties of the body with the unyielding power of the mind.
+Mind Over Body^PG^41^The aesthetics and mystics of your homeland  have learned to overcome the frailties of the body with the unyielding power  of the mind.
+Mind Over Body^XPH^48^Your ability damage heals more rapidly.
+Mindsight^LoM^126^A creature that has this feat possesses innate  telepathic ability that allows it to precisely pinpoint other thinking beings  within range of its telepathy.
+Minor Malevolence^Gh^37^You can possess a creature for a short while  and control its actions.
+Misleading Song^RS^142^You can channel the power of your bardic music  to temporarily increase the power of your illusion spells.
+Mithral Body^ECS^57^A warforged character's body can be crafted  with a layer of mithral that provides some protection without hindering  speed or gracefulness.
+Mithral Body^MM3^192^A warforged character's body can be crafted  with a layer of mithral that provides some protection without hindering  speed or gracefulness.
+Mithral Body^RE^119^Your warforged body can be crafted with a layer  of mithral that provides some protection without hindering speed or gracefulness.
+Mithral Fluidity^ECS^57^Your movements are smoother and more fluid than  those of other warforged.
+Mithral Fluidity^MM3^192^Your movements are smoother and more fluid than  those of other warforged.
+Mobile Defense^EL^63^You can adjust your position while maintaining  a defensive stance.
+Mobile Spell-Casting^CAd^111^Your focused concentration allows you to move  while casting a spell.
+Mobility^PH^98^You are skilled at dodging past opponents and  avoiding blows.
+Momentary Alteration^UA^94^You can briefly transform yourself into a second  form, acquiring its physical qualities.
+Monastic Training^ECS^57^You are part of an order that combines the monastic  discipline of the monk class with another form of training.
+Monkey Grip^CW^103^You are able to use a larger weapon than other  people your size.
+Monkey Grip^SF^7^You use a wider variety of sizes of weapons.
+Moradin's Smile^RS^142^Through the favor of Moradin, you are skilled  at interacting with others.
+Mortalbane^BV^49^The creature can make a spell-like ability particularly  deadly to mortals.
+Mortifying Attack^CR^20^Those who witness your brutal death attack are  unnerved and jarred by the experience.
+Mother Cyst^LM^28^You gain the ability to cast necrotic cyst spells  by growing a cyst of your own.
+Mountain Warrior^RS^142^You are adept at fighting on the uneven ground  of mountainous terrain.
+Mountaineer^Fr^49^You are a particularly gifted explorer and mountain  climber.
+Mounted Archery^PH^98^You are skilled at using ranged weapons while  mounted.
+Mounted Combat^PH^98^You are skilled in mounted combat.
+Mounted Mobility^HB^98^You are skilled at dodging past opponents while  mounted.
+Mounting Casting^MH^27^You are skilled at casting spells while riding  a mount.
+Mror Stalwart^RE^109^You have been trained to make devastating strikes  with the weapons of the dwarves of the Mror Holds.
+Multiattack^MM^304^The creature is adept at using all its natural  weapons at once.
+Multiattack^MM2^18^The creature is adept at using all its natural  weapons at once.
+Multiattack^MM3^207^A creature with this feat is adept at using  all its natural weapons at once.
+Multiattack^Mon^9^The creature is adept at using all its natural  weapons at once.
+Multiattack^MW^24^You are adept at using all your natural weapons  at once.
+Multicultural^SaS^40^You blend in well with members of another race.
+Multidexterity^MM2^18^The creature is adept at using all its hands  in combat.
+Multidexterity^Mon^9^The creature is adept at using all its hands  in combat.
+Multidexterity^MW^24^You are skilled at utilizing all your hands  in combat.
+Multigrab^SK^146^You can grapple enemies more firmly than normal  with your natural attacks.
+Multigrab^SS^37^You can grapple enemies more firmly than normal  with your natural attacks.
+Multilingual^LE^8^You have an uncanny knack for languages.
+Multisnatch^Dr^72^You can grapple enemies more firmly with only  one of your natural attacks.
+Multispell^EL^63^You can cast an additional quickened spell in  a round.
+Multitasking^SF^62^You can perform different tasks with different  limbs.
+Multitasking^SS^37^You can perform different tasks with different  limbs.
+Multivoice^SS^37^If you have two or more heads, you can cast  more spells than usual in a round.
+Multiweapon Fighting^MM^304^A creature with three or more hands can fight  with a weapon in each hand.
+Multiweapon Fighting^MM2^18^A creature with three or more hands can fight  with a weapon in each hand.
+Multiweapon Fighting^Mon^9^A creature with three or more hands can fight  with a weapon in each hand.
+Multiweapon Rend^EL^63^You can rend opponents when fighting with more  than two limbs.
+Music of Growth^ECS^57^Your music can enhance the power of animals  and plant creatures.
+Music of Making^ECS^57^Echoing the music of creation, your own performance  enhances any process of creation.
+Music of the Gods^EL^63^You can use your bardic music to influence creatures  immune to mind-affecting effects.
+Music of the Outer Spheres^LoM^181^You can use your bardic music to create discordant,  insane sounds.
+Mutable Body^RE^110^Your enhanced control over your shapechanging  ability grants you extra power from transmutation spells.
+Mutilator^CR^20^After striking down your enemy in battle, you  can skillfully mutilate the corpse to prevent others from raising it from  the dead.
+Narrow Mind^XPH^48^Your ability to concentrate is as keen as an  arrowhead, allowing you to gain your psionic focus even in the most turbulent  situations.
+Narrowed Gaze^SK^146^Your gaze attack has a limited field of effect.
+Narrowed Gaze^SS^37^Your gaze attack has a limited field of effect.
+Natural Bond^CAd^111^Your bond with your animal companion is exceptionally  strong.
+Natural Bully^CR^21^You easily terrify weaker adversaries.
+Natural Heavyweight^PlH^40^You are descended from creatures native to a  plane of heavy gravity.
+Natural Leader^HB^98^You have a natural commanding presence.
+Natural Scavenger^ShS^21^You are particularly adept at finding food while  on the move.
+Natural Spell^Gh^37^You can cast spells while in wild shape or shifted  form.
+Natural Spell^MW^24^You cast spells while in a wild shape.
+Natural Spell^PH^98^You can cast spells while in a wild shape.
+Natural Trickster^RS^143^You have greater natural access to your race's  powers of illusion.
+Naturalized Denizen^UA^94^You are unusually anchored to your location.
+Nauseating Touch^Gh^37^When you touch a living creature, you can make  it nauseated.
+Necromantic Might^LM^28^Undead you control gain benefits when they are  near you.
+Necromantic Presence^LM^28^Undead you control are harder to turn when they  are near you.
+Necropolis Born^CAr^81^You possess a magical understanding of the essence  of mortal dread.
+Necropotent^LM^29^Your special melee or ranged attack with one  type of weapon is especially effective against undead.
+Necrotic Reserve^LM^29^You are not immediately destroyed when your  hit points fall to 0 or lower.
+Negative Energy Burst^CD^90^You can use your rebuke/command undead ability  to unleash a burst of negative energy.
+Negative Energy Burst^EL^63^You can use your rebuke/command undead ability  to unleash a burst of negative energy.
+Negotiator^PH^98^You are good at gauging and swaying attitudes.
+Nemesis^BE^44^You are the holy bane of creatures of a particular  type.
+Neraph Charge^PlH^40^You master the Limbo-native neraph martial art  of motion camouflage when you charge your foe.
+Neraph Throw^PlH^40^You master the Limbo-native neraph martial art  of motion camouflage for your thrown weapons.
+Net and Trident^CW^114^You are a master of fighting with the net and  the trident.
+Netherese Battle Curse^LE^8^You can channel your own arcane energy into  a powerful curse upon those who dare to face you in battle.
+Night Haunt^CAr^81^You possess a magical understanding of the workings  of the unseen.
+Nimble Bones^LM^29^Undead you raise or create are faster and more  nimble than normal.
+Nimble Fingers^PH^98^You are adept at manipulating small, delicate  objects.
+Nimbus of Light^BE^44^You are cloaked in the radiant light that marks  you as a servant of the purest ideals.
+Nobody's Fool^Rac^166^You have an uncommon streak of skepticism and  common sense, and have a knack for discerning falsehood from truth.
+Node Defense^CR^25^You can use the magical power of a node to defend  yourself from harm.
+Node Defense^Und^26^You can use the magical power of a node to defend  yourself from harm.
+Node Sensitive^CR^25^You can perceive a node just by passing near  it.
+Node Sensitive^Und^26^You can perceive an earth node just by passing  near it.
+Node Spellcasting^CR^25^You have discovered the secret of the magic  of a particular type of node.
+Node Spellcasting^Und^26^You have discovered the secret of node magic.
+Node Store^CR^26^You can store a prepared spell in a node to  which you are attuned.
+Node Store^Und^26^You can store a prepared spell in an earth node.
+Nomadic Trekker^ShS^21^You are particularly efficient at overland movement  across the great grasslands.
+Nonlethal Substitution^BE^44^You can modify a spell that uses energy to deal  damage to deal nonlethal damage instead.
+Nonlethal Substitution^CAr^81^You can modify an energy spell to deal nonlethal  damage.
+Nonverbal Spell^PlH^40^You can cast spells that have verbal components  without actually verbalizing the words.
+Nymph's Kiss^BE^44^By maintaining an intimate relationship with  a good-aligned fey, you gain some of the characteristics of fey.
+Oaken Resilience^CD^82^You can take on the sturdiness of the mighty  oak.
+Obscure Lore^CAd^111^You are a treasure trove of little-known information.
+Obscure Lore^SaS^40^You are a treasure trove of little-known information.
+Obtain Familiar^CAr^81^You gain a familiar.
+Ocular Spell^LoM^181^Your study of the terrible powers of the beholder  has given you insight into new ways to prepare and cast spells.
+Off-Hand Parry^MW^24^You use your off-hand weapon to defend against  melee attacks.
+Off-Hand Parry^SF^7^You use your off-hand weapon to defend against  melee attacks.
+Old Salt^Sto^93^You are an old hand at shipboard life, having  mastered the myriad skills that are required of the experience sailor. Additionally,  you have an eye for the weather.
+Omniscient Whispers^UA^94^A constant, barely audible muttering echoes  in your ears, usually beyond your comprehension. But if you focus all your  energy on listening, you sometimes catch a sentence or two that bears directly  on your current situation.
+Oni's Bane^OA^64^Your ancestor, Isawa Akuma, was a Phoenix shugenja  who sought to understand the mystery of identity.
+Open Minded^XPH^48^You are naturally able to reroute your memory,  mind, and skill expertise.
+Open Minded^CAd^111^You are naturally able to reroute your memory  and skill expertise.
+Opportunity Power^XPH^48^You can make power-enhanced attacks of opportunity.
+Oral History^Rac^167^You are well versed in the art of storytelling  and the oral history of your culture.
+Otherworldly^PG^41^Your folk are known for their mystic power and  seem to transcend their mortal forms.
+Outsider Wings^Rac^167^You have sprouted wings appropriate to your  heritage, revealing the power of your supernatural bloodline.
+Overchannel^XPH^49^You burn your life force to strengthen your  powers.
+Overcome Weakness^Dr^72^You can overcome an innate vulnerability through  sheer willpower.
+Overhead Thrust^Dr^106^You can deal a nasty attack to anything that  tries to crush or run over you.
+Oversized Two-Weapon Fighting^CAd^111^You are adept at wielding larger than normal  weapons in your off hand.
+Overwhelming Critical^DMG^210^Choose one type of melee weapon, such as longsword  or greataxe. With that weapon, you do more damage on a critical hit.
+Overwhelming Critical^Dr^72^Choose one type of melee weapon, such as a claw  or bite. With that weapon, you deal more damage on a critical hit.
+Overwhelming Critical^EL^63^Choose one type of melee weapon, such as longsword  or greataxe. With that weapon, you do more damage on a critical hit.
+Owlbear Berserker^UE^44^Your fighting style emulates the owlbear, the  totem beast of your berserker lodge.
+Pain Mastery^SS^37^Injuries send you into a fury, increasing your  physical power.
+Pain Touch^CW^103^You cause intense pain in an opponent with a  successful stunning attack.
+Pain Touch^OA^64^You cause intense pain in an opponent with a  successful stunning attack.
+Pain Touch^SF^8^You cause intense pain in an opponent with a  successful stunning attack.
+Parrying Shield^LoM^181^You have studied advanced techniques for battling  foes whose attacks normally bypass armor.
+Path of Shadows^RE^110^You can use dancelike maneuvers to aid your  defense.
+Peak Hopper^SS^37^You are adapted to a hilly or mountainous environment.
+Penetrating Damage Reduction^EL^63^You can bypass a creature's damage reduction.
+Perfect Health^EL^63^You are immune to normal diseases and common  poisons.
+Perfect Multiweapon Fighting^EL^63^A creature with three or more hands can fight  with a weapon in each hand.
+Perfect Two-Weapon Fighting^CW^152^You can attack with your off-hand weapon as  frequently as with your primary weapon.
+Perfect Two-Weapon Fighting^EL^64^You can attack with your off-hand weapon as  frequently as with your primary weapon.
+Permanent Emanation^EL^64^One of your personal emanation spells becomes  permanent.
+Pernicious Magic^FRCS^37^You can use the Shadow Weave to make your spells  harder for Weave users to counter.
+Pernicious Magic^PG^42^You can use the Shadow Weave to make your spells  harder for Weave users to dispel.
+Persistent Spell^CAr^81^You can make a spell last all day.
+Persistent Spell^DD^51^The deity makes one of its spells last all day.
+Persistent Spell^FRCS^37^You make one of your spells last all day.
+Persistent Spell^PG^42^You can make a spell last all day.
+Persistent Spell^TB^41^You make one of your spells last all day.
+Persona Immersion^RE^110^Your assumption of another's physical identity  grants you defenses against mental intrusion.
+Personal Touchstone^PlH^41^You draw more power form one of the planar touchstone  locations to which you have forged a link.
+Persuasive^PH^98^You have a way with words and body language.
+Persuasive^SaS^40^You could sell a tindertwig hat to a troll.
+Pervasive Gaze^SK^146^Your gaze attack is more effective than normal.
+Pervasive Gaze^SS^37^Your gaze attack is more effective than normal.
+Petrification Immunity^SK^147^You are immune to petrification effects.
+Petrification Resistance^SK^147^You can resist petrification effects better  than you otherwise could.
+Phalanx Fighting^CW^103^You are trained in fighting in close formation  with your allies.
+Phalanx Fighting^LD^189^You are trained in fighting in close formation  with your allies.
+Pharaoh's Fist^Sa^51^Your unarmed strikes echo with thunder, stunning  your foe and those nearby.
+Photosynthetic Skin^UA^94^Your skin toughens when it draws energy from  the sun.
+Pierce Magical Concealment^CAr^81^You ignore the miss chance provided by certain  magical effects.
+Pierce Magical Protection^CAr^82^You can overcome the magical protections of  your enemies.
+Pierce the Darkness^RS^143^You can channel positive energy to temporarily  increase the range of your darkvision.
+Piercing Cold^Fr^49^Your cold spells are so cold that they can damage  creatures normally resistant or immune to cold.
+Piercing Gaze^SK^147^Your gaze attack has a greater range than normal.
+Piercing Gaze^SS^38^Your gaze attack has a greater range than normal.
+Piercing Sight^RS^143^Your fundamental familiarity with illusion allows  you to better recognize them.
+Pin Shield^CW^103^You know how to get inside your opponent's guard  by pinning his shield out of the way.
+Pin Shield^SF^8^You know how to get inside your opponent's guard  by pinning his shield out of the way.
+Pious Defense^CD^86^Your connection to a greater power sometimes  gives you flashes of insight that keep you safe.
+Pious Soul^CD^86^By adhering to the precepts of your religion  or philosophy, you gain an extra edge when you need it most.
+Pious Spellsurge^CD^87^You can use the strength of your faith to augment  a spell cast at a critical juncture.
+Plague Resistant^Rac^167^You are descended from the handful of combatants  who fought on the Fields of Nun and survived Chondath's Rotting War in 902  DR.
+Planar Familiar^PlH^41^When you are ready and able to acquire a new  familiar, you may choose one of several nonstandard familiars.
+Planar Touchstone^PlH^41^Forge a link between you and power-rich planar  locations, referred to as planar touchstones.
+Planar Turning^DMG^210^You can turn or rebuke outsiders.
+Planer Turning^EL^64^You can turn or rebuke outsiders.
+Planetouched Animal Affinity^Rac^167^You have a special affinity for a kind of animal  associated with your deity ancestor.
+Plant Control^DD^51^The deity can channel the power of nature to  gain mastery over plant creatures.
+Plant Control^MW^24^You channel the power of nature to gain mastery  over plant creatures.
+Plant Defiance^DD^51^The deity can channel the power of nature to  drive off or stop plant creatures.
+Plant Defiance^MW^24^You channel the power of nature to drive off  plant creatures.
+Plant Wild Shape^EL^65^You can wild shape into plant form.
+Plunging Shot^HB^99^You can use the force of gravity to make your  ranged attacks deal extra damage if your target is below you.
+Plunging Shot^RW^152^You can use the force of gravity to make your  ranged attacks deal extra damage if your target is below you.
+Point Blank Shot^PH^98^You are skilled at making well-placed shots  with ranged weapons at close range.
+Poison Immunity^BV^49^After prolonged exposure to a poison or toxin,  the character has rendered himself immune to it.
+Poison Immunity^CR^21^After prolonged exposure to a poison or toxin,  you have rendered yourself immune to it.
+Poison Immunity^SK^147^You can ignore the effects of poison.
+Poison Immunity^SS^38^You can ignore the effects of poison.
+Poison Resistance^SK^147^You can resist poison better than you otherwise  could.
+Poison Resistance^SS^38^You can resist poison better than you otherwise  could.
+Polar Chill^UA^94^You can call forth the cold of the arctic regions,  making movement and fighting difficult for the unprepared.
+Poltergeist Hand^Gh^37^You can move small objects in a limited manner  at a distance when you are a ghost.
+Poltergeist Rage^Gh^37^You can throw heavy objects with the power of  your mind.
+Polyglot^CAd^192^You can speak, read, and write all languages.
+Polyglot^EL^65^You can speak, read, and write all languages.
+Portal Master^PG^42^You are especially proficient at creating portals.
+Portal Sensitive^Und^27^You can perceive a portal just by passing  near it.
+Positive Energy Aura^CD^90^You automatically turn (or even destroy) lesser  undead.
+Positive Energy Aura^EL^65^You automatically turn (or even destroy) lesser  undead.
+Positive Energy Resistance^LM^29^You are resistant to the damage dealt by positive  energy effects.
+Power Attack^PH^98^You can make exceptionally powerful melee attacks.
+Power Attack - Iaijutsu^OA^64^Your ancestor, Kakita Rensei, was a renowned  duelist whose strength was legendary.
+Power Attack - Shadowlands^OA^65^You are descended from Kaiu Gineza, the engineer  who not only helped construct the tomb of Iuchiban, but also remained in  the tomb to set the last trap.
+Power Climb^Dr^72^If you fly in a straight line, you can gain  altitude in flight more easily than others.
+Power Critical^CW^103^Choose one weapon, such as a longsword or a  greataxe. With that weapon, you know how to hit where it hurts.
+Power Critical^DD^51^The deity chooses one kind of weapon, such as  a longsword or greataxe. With this weapon, the deity knows how to hit where  it hurts when it counts.
+Power Critical^MW^24^Choose one weapon, such as a longsword or a  greataxe. With that weapon, you know how to hit where it hurts.
+Power Dive^Dr^72^You can fall upon an opponent from the sky.
+Power Dive^SS^38^You can fall upon an opponent from the sky.
+Power Knowledge^XPH^34^You add two additional powers to your list of  powers known.
+Power Lunge^EA^50^Your ferocious attack may catch an opponent  unprepared.
+Power Lunge^Gh^37^Your ferocious attack may catch an opponent  unprepared.
+Power Lunge^SF^8^Your ferocious attack may catch an opponent  unprepared.
+Power Penetration^XPH^49^Your powers are especially potent at breaking  through power resistance.
+Power Specialization^XPH^49^You deal more damage with your powers.
+Power Throw^CAd^111^You have learned how to hurl weapons to deadly  effect.
+Powerful Bite^LoM^23^An aboleth with this feat develops jaws that  are much more muscular than normal, allowing it to bite more efficiently.
+Powerful Charge^ECS^57^You can charge with extra force.
+Powerful Charge^MH^27^You can charge with extra force.
+Powerful Charge^MM3^207^A creature with this feat can charge with extra  force.
+Powerful Voice^OA^65^You are karmically linked to Utaku, Shinjo's  most trusted lieutenant and devoted bodyguard.
+Powerful Wild Shape^RS^143^You retain your powerful build while in wild  shape form.
+Practiced Cohort^HB^99^Your cohort works well as part of your team.
+Practiced Spellcaster^CAr^82^Choose a spellcasting class that you possess.  Your spells cast from that class are more powerful.
+Practiced Spellcaster^CD^82^Choose a spellcasting class that you possess.  Your spells cast from that class are more powerful.
+Precise Shot^PH^98^You are skilled at timing and aiming ranged  attacks.
+Precise Swing^ECS^58^You can ignore most obstacles when making a  melee attack against an opponent.
+Precocious Apprentice^CAr^181^Your master has shown you the basics of a spell  beyond the normal limits of your experience and training.
+Prehensile Tail^SK^147^You can use your tail to manipulate objects.
+Prehensile Tail^SS^38^You can use your tail to manipulate objects.
+Priest of the Waste^Sa^51^You can swap out prepared spells for others  that aid in exploring and surviving in wastelands.
+Primeval Wild Shape^Fr^49^Your wild shape forms are stronger than normal.
+Primitive Caster^Fr^49^You use screeches, wild gesticulations, and  extra material components to give your spells additional power.
+Primitive Caster^Rac^167^You use screeches, wild gesticulations, and  extra material components to give your spells additional powers.
+Profane Boost^CD^84^You can channel negative energy to increase  the power of inflict wounds spells cast near you.
+Profane Lifeleech^LM^29^You can channel negative energy to draw the  life force from nearby living creatures.
+Profane Outburst^CR^21^With a horrendous release of divine energy,  you steel your undead allies and minions against harm.
+Profane Vigor^LM^29^You can channel negative energy to heal nearby  undead allies of physical damage.
+Prone Attack^CW^103^You can attack from a prone position without  penalty.
+Prone Attack^OA^65^You attack from a prone position without penalty.
+Prone Attack^SF^8^You attack from a prone position without penalty.
+Proportionate Wild Shape^MW^24^You use wild shape to become animals of your  own size even if your wild shape ability would normally exclude that size  category.
+Protected Destiny^RD^153^Your heroic destiny is guarded against the whims  of misfortune.
+Psicrystal Affinity^XPH^49^You have created a psicrystal.
+Psicrystal Containment^XPH^49^Your psicrystal has advanced enough that it  can hold a psionic focus that you store within it.
+Psicrystal Power^XPH^34^Your psicrystal can manifest a power.
+Psionic Affinity^XPH^49^You have a knack for psionic endeavors.
+Psionic Body^XPH^49^Your mind reinforces your body.
+Psionic Charge^XPH^50^You can charge in crooked line.
+Psionic Dodge^XPH^50^You are proficient at dodging blows.
+Psionic Endowment^XPH^50^You can endow your manifestations with more  concentrated focus.
+Psionic Fist^XPH^50^You can charge your unarmed strike or natural  weapon with additional damage potential.
+Psionic Hole^XPH^50^You are anathema to psionic creatures and characters.
+Psionic Meditation^XPH^50^You can focus your mind faster than normal,  even under duress.
+Psionic Shot^XPH^50^You can charge your ranged attacks with additional  damage potential.
+Psionic Talent^XPH^50^You gain additional power points to supplement  those you already had.
+Psionic Weapon^XPH^50^You can charge your melee weapon with additional  damage potential.
+Puff Torso^SK^147^You can puff out your skin to appear larger  and more threatening.
+Pulverize Foe^CR^21^You enjoy smashing your opponents into submission.
+Purify Spell^BE^44^You can charge your damaging spells with celestial  energy that leaves good creatures unharmed.
+Purify Spell Trigger^BE^45^You can channel holy power through a spell trigger  item, such as a wand or staff.
+Purify Spell-Like Ability^BE^45^You can charge your damaging spell-like abilities  with celestial energy that leaves good creatures unharmed.
+Pursue^ECS^58^You have the ability to follow in an opponent's  wake.
+Pushback^MH^27^You can knock opponents back when you hit them  in melee.
+Pyro^SaS^40^You're good at lighting objects and opponents  on fire.
+Quell the Profane^BE^45^Your mightiest attacks weaken evil foes.
+Quick Change^RE^110^You can quickly alter your features and physiology.
+Quick Change^SS^38^You can shift to an alternate form faster and  more easily than you otherwise could.
+Quick Draw^PH^98^You can draw weapons with startling speed.
+Quick Reconnoiter^CAd^112^You can learn a lot of information from just  a quick scan of an area or object.
+Quick Recovery^LoM^181^It's hard to keep you down for long. You have  a talent for shaking off effects that leave others unable to act.
+Quick Staff^CW^114^You have mastered the style of fighting with  a quarterstaff.
+Quicken Breath^Dr^73^You can loose your breath weapon with but a  thought.
+Quicken Legacy^WL^15^You can activate one of your item's legacy abilities  with a moment's thought.
+Quicken Manifestation^LM^29^You can manifest from the Ethereal Plane with  a moment's thought.
+Quicken Power^XPH^50^You can manifest a power with a moment's thought.
+Quicken Spell^PH^98^You can cast a spell with a moment's thought.
+Quicken Spell-Like Ability^BV^49^The creature can use a spell-like ability with  a moment's thought.
+Quicken Spell-Like Ability^MM^304^The creature can employ a spell-like ability  with a moment's thought.
+Quicken Spell-like Ability^MM2^18^The creature can use a spell-like ability with  a moment's thought.
+Quicken Spell-Like Ability^MM3^207^A creature with this feat can employ a spell-like  ability with a moment's thought.
+Quicken Spell-Like Ability^SS^38^You can use a spell-like ability with a moment's  thought.
+Quicken Turning^CD^84^You can turn or rebuke undead with a moment's  thought.
+Quicken Turning^DF^20^You can turn or rebuke undead with a moment's  thought.
+Quicken Turning^FP^215^You can turn or rebuke undead with a moment's  thought.
+Quicken Turning^Gh^37^You can turn or rebuke undead with a moment's  thought.
+Quicken Turning^LM^29^You can turn or rebuke undead with a moment's  thought.
+Quicker Than the Eye^SaS^40^Your hands can move so quickly that observers  don't see what you've done.
+Quickslime^LoM^23^The slime attack of an aboleth with this feat  is particularly fast and difficult to resist.
+Racial Emulation^RE^110^You can emulate a humanoid more closely with  your minor change shape ability.
+Radiant Fire^RD^155^Pelor has ignited your faith and conviction,  making you better able to fight the creatures of darkness.
+Ragewild Fighting^RE^118^You have mastered a merciless form of combat  that emphasizes using brute strength to shatter your foes.
+Raging Luck^ECS^58^When raging, you have a greater ability to alter  your luck than most others do.
+Rampaging Bull Rush^RS^143^You can use brute force to slam into and knock  down your enemies.
+Ranged Disarm^CW^103^You can disarm a foe from a distance.
+Ranged Inspiration^EL^65^You can use your bardic music at a greater range  than normal.
+Ranged Pin^CW^104^You can perform a ranged grapple attempt against  an opponent not adjacent to you.
+Ranged Smite Evil^BE^45^You smite ability can be channeled through your  ranged weapon.
+Ranged Spell Specialization^CAr^82^You deal more damage with ranged touch attack  spells.
+Ranged Sunder^CW^104^You can attack an opponent's weapon from a distance.
+Rapid Breath^SS^39^You do not have to wait as long to reuse your  breath weapons as you normally would.
+Rapid Inspiration^EL^66^You can inspire your allies with bardic music  more quickly than normal.
+Rapid Metabolism^XPH^50^Your wounds heal rapidly.
+Rapid Reload^EL^70^You reload a crossbow more quickly than normal.
+Rapid Reload^PH^99^Choose a type of crossbow. You can reload a  crossbow of that type more quickly than normal.
+Rapid Reload^SF^9^You reload a crossbow more quickly than normal.
+Rapid Shot^PH^99^You can use ranged weapons with exceptional  speed.
+Rapid Spell^CD^84^You can cast spells with long casting times  more quickly.
+Rapid Stunning^CW^104^You can use your stunning attacks in rapid succession.
+Rapid Swimming^Rac^167^You are one with the water.
+Rapid Swimming^Sto^93^You are one with the water.
+Rapidstrike^Dr^73^You can attack more than once with a natural  weapon.
+Raptor School^CW^111^You know martial arts techniques inspired by  hunting birds.
+Rashemi Elemental Summoning^UE^45^You may summon Rashemen's native elementals  in any situation where you could summon an air or earth elemental.
+Rattlesnake Strike^Sa^51^Having observed the ways of a desert viper,  you have learned to use ki in a fashion similar to poison.
+Razing Strike^CAd^112^You have mastered the art of delivering precise  strikes against nonliving creatures while channeling spell energy through  your melee attacks.
+Razorclaw Elite^RE^114^Your razorclaw shifter trait improves.
+Reach Bite^LoM^23^An aboleth with this feat can extend its jaws  and esophagus out from its body to make attacks beyond its normal reach.
+Reach Spell^CD^84^You can cast touch spells without touching the  spell recipient.
+Reach Spell^DD^51^The deity can cast touch spells without touching  the spell recipient.
+Reach Spell^DF^20^You can cast touch spells without touching the  spell recipient.
+Reach Spell^FP^215^You can cast touch spells without touching the  spell recipient.
+Reactive Countersong^EL^66^You can use countersong as a reaction to a sonic  or language-dependent magical attack.
+Reactive Counterspell^Mag^22^You can react quickly to counterspells cast  by opponents.
+Reactive Counterspell^PG^42^You can react quickly to counter  spells cast by opponents.
+Reactive Shifting^RE^115^You can shift with a mere thought.
+Ready Shot^HB^99^You can make devastating attacks with ranged  weapons against charging opponents.
+Reaping Spell^CR^21^The dark energy of your spell devours the soul  of any creature killed by it.
+Reckless Charge^MH^27^You can charge with wild abandon.
+Reckless Offense^XPH^51^You can shift your focus from defense to offense.
+Reckless Offensive^EA^41^You lower your guard in order to make a telling  attack.
+Reckless Offensive^Rac^167^You lower your guard in order to make a telling  attack.
+Reckless Rage^RS^143^You are considered extreme even among other  barbaric warriors, and you enter a deeper state of rage than others.
+Reckless Wand Wielder^CAr^82^You can increase the effectiveness of spells  cast from a wand.
+Recognize Impostor^ECS^58^You are extremely skilled at spotting imposters.
+Recover Breath^Dr^73^You wait less time before being able to use  your breath weapon again.
+Reflect Arrows^EL^66^You reflect ranged attacks back upon the attacker.
+Relic Hunter^RE^111^You possess great knowledge of the relics and  crafts of the ancient cultures of Eberron.
+Remain Conscious^MW^25^You have the tenacity of will that supports  you even when things look bleak.
+Remain Conscious^OA^65^You have a tenacity of will that supports you  even when you are disabled or dying.
+Remain Conscious^SF^9^You have a tenacity of will that supports you  even when things look bleak.
+Rend^Dr^73^You can rend opponents with your claws.
+Rend Ghost^Gh^37^Your touch can maul the ectoplasm of another  ghost.
+Rending Constriction^SK^147^You can pull grappled enemies apart.
+Rending Constriction^SS^39^You can pull grappled enemies apart.
+Renown^UA^182^You have a better chance of being recognized.
+Repeat Spell^CAr^82^You can cast a spell that repeats on the following  round.
+Repeat Spell^DD^51^The deity can cast a spell that repeats the  following round.
+Repeat Spell^TB^41^You can cast a spell that repeats on the following  round.
+Repel Aberration^ECS^58^Your Gatekeeper training allows you to keep  aberrations at bay.
+Requiem^LM^29^Your bardic music affects undead creatures.
+Requiem^SaS^40^Your bardic music affects undead creatures.
+Research^ECS^59^You can use your Knowledge skills to extract  information from books, scrolls, and other repositories of facts and figures.
+Reserves of Strength^DCS^86^When you cast a spell, you can choose to increase  its effective caster level at the cost of exhausting yourself.
+Residual Rebound^UA^94^Sometimes spells cast at you rebound on the  caster instead.
+Resist Death^EL^111^You are capable of withstanding tremendous amounts  of damage without risk of instant death.
+Resist Disease^MW^25^You have developed a natural resistance to diseases.
+Resist Disease^ShS^21^You have developed a natural resistance to diseases.
+Resist Dragonfear^DCS^86^You are able to show courage in the presence  of dragons.
+Resist Ghost^Gh^37^You are resistant to the effects of ghost powers.
+Resist Poison^FRCS^37^Over years, some among your people carefully  expose themselves to poisons in controlled dosages in order to build up  immunity to their effects.
+Resist Poison^MW^25^You have built up an immunity to the effects  of poisons by exposing yourself to controlled doses of them.
+Resist Poison^OA^65^Your ancestor, Agasha Kitsuki, founded the fourth  family of the Dragon clan and a school for magistrates renowned for teaching  skills of investigation and deduction.
+Resist Poison^PG^43^Your people have become inured to many deadly  substances through controlled exposure or the simple hostility of your home  environment.
+Resist Taint^OA^65^You are descended from Kuni, the founder of  the Kuni family, a scholar of -- and mighty warrior against -- the Shadowlands.
+Resistance to Energy^MW^25^You channel the power of nature to resist a  particular energy type.
+Resounding Blow^BE^45^Your mightiest attacks cause your foes to tremble  before you.
+Resourceful Buyer^RD^153^You know where to look in a community for anything  you need.
+Return Shot^XPH^51^You can return incoming arrows, as well as crossbow  bolts, spears, and other projectile thrown weapons.
+Reverberation^SS^39^Your sonic attack is more potent than normal.
+Rhinoceros Tribe Charge^ShS^21^You use the power of the rhinoceros's charge  in battle.
+Ride-By Attack^PH^99^You are skilled at making fast attacks from  your mount.
+Right of Counsel^ECS^59^You have the legal and sacral right to seek  advice from one of your ancestors, a deathless elf in Aerenal's City of  the Dead.
+Righteous Strike^EL^66^Your unarmed strikes are particularly damaging  to chaotic creatures.
+Righteous Wrath^BE^45^Your rage is empowered with divine fury.
+Rock Gnome Trickster^Rac^167^Your glamers are particularly likely to fool  the senses of your target.
+Rock Hurling^RS^143^You can throw rocks like a giant can.
+Roll With It^SS^39^You are adept at lessening the effects of blows.
+Roofwalker^RD^156^You are adept at moving and fighting on rooftops  and ledges.
+Roots of the Mountain^RS^143^You can channel energy to make yourself immovable.
+Roundabout Kick^CW^105^You can follow up on a particularly powerful  unarmed attack with a mighty kick, spinning in a complete circle before  landing the kick.
+Roundabout Kick^OA^65^You can follow up on a particularly powerful  unarmed attack with a mighty kick, spinning in a complete circle before  landing the kick.
+Ruinous Rage^EL^66^While in a rage, you can deal tremendous damage  to objects.
+Run^PH^99^You are fleet of foot.
+Runesmith^Rac^167^You can fashion runes that take the place of  material components for your spells.
+Sacred Boost^CD^84^You can channel positive energy to increase  the power of cure wounds spells cast near you.
+Sacred Healing^CD^84^You can channel positive energy to grant nearby  living creatures the ability to recover form their wounds quickly.
+Sacred Spell^DD^51^The deity's damaging spells are imbued with  divine power.
+Sacred Spell^DF^20^Your damaging spells are imbued with divine  power.
+Sacred Spell^FP^215^Your damaging spells are imbued with divine  power.
+Sacred Strike^BE^45^Your sneak attack is enhanced by your unshakable  faith in a good-aligned deity.
+Sacred Tattoo^Rac^168^You have been spiritually touched by one of  the god-kings of the Old Empires and bear his or her symbol in the form  of a tattoo in the shape of a holy symbol.
+Sacred Vengeance^CW^108^You can channel energy to deal extra damage  against undead in melee.
+Sacred Vengeance^LM^30^You can channel energy to deal extra damage  against undead in melee.
+Sacred Vitality^LM^30^You can channel positive energy to gain protection  from damage to your abilities or your life force.
+Sacred Vow^BE^45^You have willingly given yourself to the service  of a good deity or cause, denying yourself an ordinary life to better serve  you highest ideals.
+Sacrificial Mastery^BV^50^The character is skilled at offering living  sacrifices to evil gods or fiends.
+Saddleback^FRCS^37^Your people are as comfortable riding as walking.
+Saddleback^Gh^38^You were raised among people who are as comfortable  riding as walking.
+Saddleback^OA^65^You have a unique karmic tie to Moto Chai, one  of the greatest riders ever to live, even by Unicorn standards.
+Saddleback^PG^43^You've spent endless hours learning how to handle  a mount in a fight.
+Sahuagin Flip^Sto^93^You can safely attack and withdraw underwater.
+Sailor's Balance^Sto^93^You are experienced with the rolling decks of  the ship and maintain strong footing, even in a terrible storm.
+Sanctify Ki Strike^BE^46^Sacred power suffuses your unarmed strikes.
+Sanctify Martial Strike^BE^46^Sacred power suffuses your attacks with a certain  kind of weapon.
+Sanctify Natural Attack^BE^46^You can focus holy power into your natural attacks.
+Sanctify Relic^CD^84^You can create magic items that are imbued with  a connection to your deity.
+Sanctify Water^Sto^93^You can call upon positive energy to momentarily  transform normal water around you into holy water.
+Sanctify Weapon^BE^46^You can focus holy power into your weapon.
+Sanctum Spell^CAr^82^Your spells are especially potent on home ground.
+Sanctum Spell^TB^41^Your spells have a home ground advantage.
+Sand Camouflage^Sa^51^You can hide yourself in sand with a moment's  notice.
+Sand Dancer^Sa^52^While making another attack, you attempt to  blind a foe with thrown sand.
+Sand Snare^Sa^52^When you knock your foes into the sand, they  have a hard time regaining their feet.
+Sand Spinner^Sa^52^You spray sand with your acrobatic maneuvers.
+Sandskimmer^Sa^52^You are particularly adept at moving over sand.
+Savage Grapple^CAd^114^While transformed into the shape of a wild animal,  you can savagely tear at any creature that you manage to grapple.
+Scavenging Gullet^LoM^181^The taint of the aberration in your blood has  gifted you with the ability to gain nourishment from things that others  would never consider as food.
+Scent^CAd^114^You can sharpen your sense of smell.
+Scent^MW^25^Your olfactory senses are as sharp as the wolf's.
+Scholar of Nature^OA^65^You are descended from Asako Hanasku, a great  scholar who threw himself into the study of medicine, herbs, and poison.
+Scion of Sorrow^CR^23^You formally supplicate yourself to a powerful  yugoloth lord.
+Scorpion's Grasp^Sa^52^Like the scorpion, you can grab and hold your  prey.
+Scorpion's Instincts^Sa^53^You are hard to find in the waste.
+Scorpion's Resolve^Sa^53^Like the scorpion, you are not easily distracted.
+Scorpion's Sense^Sa^53^Like the scorpion, you sense other creatures  simply by perceiving their contact with the sand.
+Scourge of the Seas^Sto^93^You have a sinister reputation as a pirate and  can intimidate enemy captains by your mere presence.
+Scramble^SS^39^Your slippery ways allow you to evade a damaging  blow.
+Scribe Epic Scroll^EL^66^You can scribe scrolls of epic power.
+Scribe Scroll^PH^99^You can create scrolls, from which you or another  spellcaster can cast the scribed spells.
+Scribe Tattoo^XPH^51^You can create psionic tattoos, which store  powers within their designs.
+Sculpt Ghost Body^Gh^38^You can reshape your ghost body's ectoplasm  to enhance one physical ability score at the expense of another.
+Sculpt Spell^CAr^83^You can alter the area of your spells.
+Sculpt Spell^TB^42^You can alter the shape of a spell's area.
+Sea Legs^Fr^49^You are accustomed to the rolling motion on  board a ship, and can use this motion to your advantage.
+Sea Legs^OA^65^You are descended from Yasuki Fumoki, a notorious  pirate who preyed on Crane merchant ships off the coast.
+Sea Legs^Sto^93^You are accustomed to the rolling motion on  board a ship, and can use this motion to your advantage.
+Searing Spell^Sa^53^Your fire spells are so hot that they can damage  creatures that normally have resistance or immunity to fire.
+Second Slam^RE^120^You have learned to use your form to the utmost  and can make two slam attacks.
+Second Wind^MH^28^You can shrug off minor wounds with ease.
+Selective Spell^ShS^21^You can screen allies from the effects of your  area spells.
+Self-Concealment^EL^66^When in combat, your form becomes blurry and  indistinct, making it difficult to land a blow against you.
+Self-Sufficient^PH^100^You can take care of yourself in harsh environments  and situations.
+Sense Weakness^Dr^106^You can take advantage of subtle weaknesses  in your opponents' defenses.
+Serpent Fang^Sa^53^You are able to project your ki to strike  foes as though you had extended reach.
+Serpent Strike^ECS^60^Through monastic weapon training, you have mastered  a fighting style that makes use of an unusual monk weapon: the longspear.
+Serpent's Venom^CD^84^You can deliver a toxic bite attack reminiscent  of the viper.
+Servant of the Fallen^LE^9^You keep alive the worship of a deity who has  died or vanished.
+Servant of the Heavens^BE^46^You swear allegiance to one of the Tome Archons  who rules the Seven Heavens, and in exchange gain power to act on their  behalf.
+Shadow^MW^25^You have a better chance than most to trail  someone unnoticed.
+Shadow^SaS^40^You are good at following someone surreptitiously.
+Shadow Heritage^PlH^42^You are descended from creatures native to the  Plane of Shadow.
+Shadow Marches Warmonger^RE^111^The ancient martial pride of your people grants  you mastery of their style of battle.
+Shadow Shield^Rac^168^Your ancestors long battled the insidious influence  of shadow magic, and some of their descendants (including you) have a greater  resistance to its effects.
+Shadow Song^Rac^168^A dark legacy of the Shadowking's ambitions  is the shadow of sorrow that cloaks many Tethyrian songs and ballads. Some  bards have learned to infuse their performances with the sense of loss and  suffering that suffuses the Shadow Weave.
+Shadow Weave Magic^FRCS^37^You have discovered the dark and dangerous secret  of the Shadow Weave.
+Shadow Weave Magic^PG^43^You have discovered the dangerous secret of  the Shadow Weave.
+Shadowform Familiar^CR^22^You can summon a familiar from the Plane of  Shadow.
+Shadowstrike^CR^22^Due to your ties to the Plane of Shadow, you  strike more effectively in areas of dim illumination.
+Shape Breath^Dr^73^You can make the area of your breath weapon  a cone or a line, as you see fit.
+Shape Ectoplasm^Gh^38^You can make equipment out of ectoplasm.
+Shaped Splash^RE^111^Your expertise with thrown weapons enables you  to use splash weapons more effectively.
+Shared Fury^RW^152^Your fearsome rage spurs your animal companion  to greater heights.
+Sharp-Shooting^CW^105^Your skill with ranged weapons lets you score  hits others would miss due to an opponent's cover.
+Sharp-Shooting^DD^52^The deity's skill with ranged weapons lets it  score hits others would miss due to an opponent's cover.
+Sharp-Shooting^SF^9^Your skill with ranged weapons lets you score  hits others would miss due to an opponent's cover.
+Shattering Strike^EL^66^You can shatter objects with your unarmed strike.
+Sherem-Lar Sorcery^Gh^38^You are one of the Sherem-Lar, magically altered  in the womb to enhance your potential as a sorcerer.
+Sherezem-Lar Sorcery^Gh^38^You are one of the Sherezem-Lar, an elite group  within the Sherem-Lar, head and shoulders above the others in power.
+Shield Charge^CW^105^You deal extra damage if you use your shield  as a weapon when charging.
+Shield Charge^DF^20^You deal extra damage if you use your shield  as a weapon when charging.
+Shield Dwarf Warder^Rac^168^You are a student of the protective magics of  the shield dwarves, learned at great cost during centuries of warfare and  wandering.
+Shield Expert^SF^9^You use a shield as an off-hand weapon while  retaining its armor bonus.
+Shield of Thought^RE^113^You wield your spirit as both weapon and shield.
+Shield Proficiency^PH^100^You are proficient with bucklers, small shields,  and large shields.
+Shield Slam^CW^105^You can use your shield to daze your opponent.
+Shield Wall^HB^99^You are skilled in using shields when in formation  with other shield-bearers.
+Shielded Axe^RS^144^You have mastered the style of fighting with  a dwarven waraxe and a handaxe while keeping a buckler strapped to your  offhand, and you have learned to use this unusual combination of weapons  and buckler to protect yourself while wielding both axes effectively.
+Shielded Casting^RS^144^You are skilled at covering yourself with your  shield when casting spells in combat.
+Shielded Manifesting^RS^144^You are skilled at covering yourself with your  shield when manifesting psionic powers in combat.
+Shieldmate^MH^28^You can protect those near you with your shield.
+Shifter Agility^RE^115^Your heritage of speed and ferocity has honed  your reflexes, allowing you to avoid attacks.
+Shifter Defense^ECS^60^By delving deeper into your shifter heritage,  you have developed the ability to ignore a little damage from every attack.
+Shifter Defense^MM3^150^By delving into your shifter heritage, you have  developed the ability to ignore a little damage from every attack.
+Shifter Ferocity^ECS^60^You are a tenacious combatant, continuing to  fight when others would succumb to pain and injury.
+Shifter Ferocity^RE^115^You are a tenacious combatant, continuing to  fight when others would succumb to pain and injury.
+Shifter Instincts^MM3^150^Your heritage has given you sharp senses and  quick reflexes, and you have learned to trust your equally sharp instincts.
+Shifter Instincts^RE^115^Your heritage has given you sharp senses and  quick reflexes, and you have learned to trust your equally sharp instincts.
+Shifter Multiattack^ECS^60^You are adept at using your natural attack in  conjunction with another weapon.
+Shifter Savagery^RE^115^The bestial fury of your lycanthrope ancestors  allows you to deal devastating strikes with your natural weapons.
+Shifter Stamina^RE^115^Yours is a heritage of endurance and tenacity,  and you can shrug off bruises and fatigue.
+Ship Savvy^RE^112^Your heritage among the sailors and shipwrights  of Zilargo gives you an edge in shipboard combat.
+Ship's Mage^Sto^93^You form a potent supernatural bond with a ship.  Your spells have a more potent effect when cast aboard this ship.
+Shock Trooper^CW^112^You are adept at breaking up formations of soldiers  when you rush into battle.
+Shock Wave^Dr^73^You can strike the ground with your tail so  hard it knocks other creatures down.
+Shot on the Run^PH^100^You are highly trained in skirmish ranged weapons  tactics.
+Shriveling Touch^Gh^38^Choose one physical ability score. When you  touch a creature, you can cause permanent drain to this ability score.
+Sidestep^MH^28^You can move nimbly around the battlefield.
+Sidestep Charge^XPH^51^You are skilled at dodging past charging opponents  and taking advantage when they miss.
+Signature Spell^FRCS^37^You are so familiar with a mastered spell that  you can convert other prepared spells into that spell.
+Signature Spell^PG^43^You are so familiar with a mastered spell that  you can convert other prepared spells into that spell.
+Silencing Strike^RS^144^You can infuse your sneak attacks with the magical  essence of silence.
+Silent Spell^PH^100^You can cast spells silently.
+Silver Palm^FRCS^37^Your culture is based on haggling and the art  of the deal.
+Silver Palm^PG^43^Your culture is based on haggling and the art  of the deal.
+Silver Smite^ECS^60^You wield the power of the Silver Flame to smite  evil.
+Silver Tongue^OA^65^Your ancestor, Mirumoto Kaijuko, was the first  woman to become daimyo of the Mirumoto family.
+Silver Tracery^RE^120^Alchemical silver tracery covers your body,  allowing you to overcome the supernatural defenses of certain creatures  and protecting against some magical attacks.
+Simple Weapon Proficiency^PH^100^You understand how to use all types of simple  weapons in combat.
+Skewer Foe^CR^22^A ruthless combatant, you like to impale enemies  on spears and similar piercing weapons.
+Skill Focus^PH^100^Choose a skill. You have a special knack with  that skill.
+Skilled Telekinetic^LoM^45^A creature with this feat becomes so skilled  with its telekinesis ability that it can manipulate and use magic  items via telekinesis.
+Skyrider^Rac^168^You have trained and served with the hippogriff  cavalry that guards the Great Rift.
+Smatterings^RD^153^You have a talent for acquiring languages --  at least enough of each one to get by.
+Smite Fiery Foe^Fr^50^You can smite creatures with the fire subtype.
+Smooth Talk^FRCS^37^Your people are accustomed to dealing with strangers  and foreigners without needing to draw weapons to make their point.
+Smooth Talk^OA^66^You are descended from Doji Taehime, a Crane  ambassador to the Scorpion court -- a courtier skilled at discovering falsehoods  and uncovering plots.
+Smooth Talk^PG^43^Your people rarely have to draw their weapons  to deal with potential adversaries.
+Snake Blood^FRCS^38^The taint of the yuan-ti runs in your veins.
+Snake Blood^PG^43^The taint of the yuan-ti runs in your veins.
+Snatch^MM2^18^The creature can grapple more easily with its  claws or bite.
+Snatch^MW^25^You can grapple more easily with your claws  or bite.
+Snatch and Swallow^Dr^73^You can swallow creatures you have grabbed with  your bite attack.
+Snatch Arrows^MM^304^The creature can grab opponents much smaller  than itself and hold them in its mouth or claw.
+Snatch Arrows^PH^100^You are adept at grabbing incoming arrows, as  well as crossbow bolts, spears, and other projectile or thrown weapons.
+Snatch Arrows^SF^9^You are adept at grabbing incoming arrows, as  well as crossbow bolts, spears, and other projectile or thrown weapons.
+Snatch Trophy^CR^22^You can quickly and skillfully collect a trophy  of your victory over a fallen foe.
+Snatch Weapon^SaS^40^You can disarm an opponent, then pluck the weapon  from midair.
+Sneak Attack of Opportunity^EL^66^Whenever your opponent lets his guard down,  you can make a sneak attack.
+Snow Tiger Berserker^UE^45^You have learned how to pounce on your foes,  much like your totem spirit.
+Snowcasting^Fr^50^You add ice or snow to your spell's components  to make them more powerful.
+Snowflake Wardance^Fr^50^You are particularly adept at moving through  snow and over ice.
+Snowrunner^Fr^50^You have mastered the snowflake wardance, a  mystical style of fighting with slashing weapons that allows you to leap  and almost seem to float haphazardly across the battlefield like a whirling,  razor-edged snowflake.
+Sociable Personality^RD^153^You are adroit at avoiding social gaffes.
+Solid Visage^Gh^39^Your ghost body appears solid and alive.
+Song of the Heart^ECS^60^Your bardic music reaches the depths of its  listeners' hearts.
+Soothe the Beast^ECS^60^Echoing the music of creation, your music has  powers to calm animals.
+Soul of Honor^OA^66^Your ancestor Shinjo Martera, the firstborn  son of Shinjo, was the living incarnation of bushido for the Unicorn,  utterly without fault or failing.
+Soul of Loyalty^OA^66^Your ancestor, Mirumoto Tokeru, was renowned  for his loyalty to his twin brother, Ryudumu.
+Soul of Sincerity^OA^66^You are descended from the famous Scorpion daimyo  Bayushi Tangen, author of Lies and Little Truths.
+Soul of the North^CAr^83^You possess a magical understanding of the nature  of cold.
+Soulblade Warrior^RE^120^The spirit of a quori warrior grants you deadly  speed and combat prowess with your mind blade.
+Southern Magician^Rac^168^Your magical studies in Mulan lands have taught  you spellcasting techniques unknown in the north that blur the line between  arcane and divine magic.
+Speaking Wild Shape^MW^25^While in wild shape, you can communicate with  animals or elementals of the same kind as your current form.
+Spear of Doom^DCS^87^Few can avoid death on your spearpoint when  you brace yourself for their attack.
+Spectacular Death Throes^DCS^87^Your body seethes with unchecked power, promising  dire consequences to your killer.
+Spectral Strike^CD^90^You can strike incorporeal creatures as if they  were solid.
+Spectral Strike^EL^66^You can strike incorporeal creatures as if they  were solid.
+Speed of Thought^XPH^51^The energy of your mind energizes the alacrity  of your body.
+Spell Drain^LM^30^You can cast any spell that you drain from a  creature's mind.
+Spell Focus^CD^84^Your spells with an alignment descriptor are  more potent than normal.
+Spell Focus^PH^100^Choose a school of magic. Your spells of that  school are more potent than normal.
+Spell Focus (Good)^BE^46^Your spells with the good descriptor are more  potent than normal due to your relationship with the powers of good.
+Spell Girding^Mag^22^Your spells are particularly hardy, resisting  dispel checks more readily than normal.
+Spell Hand^CAr^83^You possess a magical understanding of the manipulation  of force.
+Spell Knowledge^DMG^210^You add two additional arcane spells to your  repertoire.
+Spell Knowledge^EL^67^You add two additional arcane spells to your  repertoire.
+Spell Mastery^PH^100^You are so intimately familiar with certain  spells that you don't need a spellbook to prepare them anymore.
+Spell Opportunity^EL^67^You can cast a touch spell as an attack of opportunity.
+Spell Penetration^PH^100^Your spells are especially potent, breaking  through spell resistance more readily than normal.
+Spell Power^OA^66^Your lineage traces back to the young shugenja  Kuni Osaku, who single-handedly held off a massive army of oni at the Battle  of the Cresting Wave.
+Spell Reprieve^LE^9^Your studies of the less restrictive arcane  traditions of old allow you to cast one spell from a prohibited school.
+Spell Specialization^TB^42^You deal more damage with ray or energy missile  spells.
+Spell Stowaway^EL^67^Choose a spell-like ability you possess or a  spell you can cast. You gain the benefits of this magic whenever it is used  near you.
+Spell Thematics^Mag^22^Your spells have a distinct visual or auditory  effect in their manifestation.
+Spell Thematics^PG^44^Your spells manifest with a distinct theme or  appearance.
+Spellcaster Support^OA^66^Your ancestor, Shiba Kaigen, was a samurai who  used his knowledge of spellcraft to help defend a mountain pass from a Lion  invasion.
+Spellcasting Harrier^Dr^74^Spellcasters you threaten find it difficult  to cast defensively.
+Spellcasting Harrier^EL^67^Spellcasters you threaten find it difficult  to cast defensively.
+Spellcasting Prodigy^FRCS^38^You have an exceptional gift for magic.
+Spellcasting Prodigy^PG^44^You have an exceptional gift for magic.
+Spellfire Wielder^Mag^23^You are one of the rare people who have the  innate talent to control raw magic in the form of spellfire.
+Spell-Like Ability Focus^Rac^168^Choose one of your spell-like abilities. This  attack becomes much more potent than normal.
+Spellrazor^RS^144^You have mastered the style of combining a gnome  quickrazor with spellcasting.
+Spellwise^PG^44^You were raised in a land where mighty wizards  are common.
+Spider Bite^PG^176^You gain a poisonous bite like that of a spider.
+Spiked Body^RE^120^Your body is overlaid with hundreds of protruding  spikes that can deal great damage to foes.
+Spinning Halberd^CW^114^You have mastered the style of fighting with  a halberd.
+Spire Walking^Rac^168^Iriaebor is justly known as the City of a Thousand  Spires, for fantastically bizarre, many-storied towers rise from all quarters  of the city and are tightly packed together. As a result, it is possible  to navigate Iriaebor via a network of arches, bridges, stairs, and leapable  gulfs far above the city streets. You are well versed in the skill of navigating  the skyroads of Iriaebor.
+Spirited Charge^PH^100^You are trained at making a devastating mounted  charge.
+Spiritual Force^RE^113^Your forceful inner spirit allows you to deal  more damage with your mind blade.
+Spit Poison^LoM^94^A creature with this feat can spit its poison  as a ranged touch attack.
+Spit Venom^SK^147^You can spit venom in the manner of a spitting  cobra.
+Split Breath^Dr^74^You can split your breath weapon into a pair  of weaker effects.
+Split Psionic Ray^XPH^51^You can affect two targets with a single ray.
+Split Ray^CAr^83^Your ray spells can affect an additional target.
+Split Ray^TB^42^You can affect two targets with a single ray.
+Spontaneous Casting^ECS^61^You can swap a prepared spell on the fly.
+Spontaneous Domain Access^EL^67^Select a domain of spells you have access to.  You can spontaneously convert spells into spells of this domain.
+Spontaneous Healer^CD^84^You can use your spellcasting ability to spontaneously  cast cure spells.
+Spontaneous Spell^EL^67^Select a spell you can cast. You can spontaneously  convert spells of that spell's level into that spell.
+Spontaneous Summoner^CD^85^You can spontaneously cast summon nature's  ally spells.
+Spontaneous Wounder^CD^85^You can use your spellcasting ability to spontaneously  cast inflict spells.
+Spreading Breath^Dr^74^You can convert your breath weapon into a spread  effect.
+Spring Attack^PH^100^You are trained in fast melee attacks and fancy  footwork.
+Spurn Death's Touch^LM^30^You can channel divine energy to remove some  of the harmful effects of attacks made by undead creatures.
+Stable Footing^RE^112^Because of your training and wariness, you are  skilled at keeping your feet in combat and able to move over difficult terrain  with ease.
+Staggering Strike^CAd^112^You can deliver a wound that hampers an opponent's  movement.
+Staggering Strike^Rac^169^You are particularly adept at making cruel and  demoralizing sneak attacks.
+Stalwart Planar Ally^PlH^42^The allies you summon from a specific plane  are tougher than normal.
+Stamp^SS^39^You can stamp the ground to crush and disrupt  opponents.
+Stand Still^XPH^51^You can prevent foes from fleeing or closing.
+Starspawn^LoM^181^Your abnormal body and heritage has become more  pronounced. You grow membranous wings and are comfortable in extreme elevations.
+Steady Concentration^RS^144^You are an expert at avoiding distractions and  focusing your mind, and you can concentrate clearly even in the most stressful  conditions.
+Steady Mountaineer^RS^144^You are so good at climbing cliffs and leaping  across crevasses that distractions don't affect you.
+Stealthy^FRCS^38^Your people are known for their stealthiness.
+Stealthy^PH^101^You are particularly good at avoiding notice.
+Steam Magic^Sto^93^You are skilled at casting fiery spells into  the water, causing terrible gouts of scalding steam.
+Stench of the Dead^UA^94^The odor of decay hangs heavy on you, causing  others to gasp and retch.
+Stigmata^BE^46^You bear the marks of wounds on your body, as  sort of a living martyrdom.
+Still Spell^PH^101^You can cast spells without gestures.
+Stitched Flesh Familiar^LM^30^When you are ready and able to acquire a new  familiar, you may choose to gain a stitched flesh familiar.
+Stone Colossus^Rac^169^You can focus a part of your power to increase  the toughness of your skin.
+Stone Form^RS^144^You can use wild shape to assume a rocklike  form.
+Stone Rage^RS^144^Your bond with the earth and tough hide makes  it easier for you to shrug off blows while you are raging.
+Stone Slide^Rac^169^You have attuned yourself to stone to such an  extent that you can merge with it for a short time.
+Stone Soul^Und^27^You were born with a dwarflike, innate sense  about rock, stone, and construction.
+Stoneback^RS^144^You have studied the techniques of fighting  underground, and you can protect yourself from the dangers of multiple attackers  whenever you can put your back to a solid wall.
+Stoneblood^Rac^169^Your blood is thick like cooling lava, making  it difficult for you to die after falling from injuries.
+Stoneshaper^Rac^169^You have a deep and abiding tie to earth and  stone.
+Stonewalker Fist^Rac^169^You are trained in an unarmed fighting style  that draws on your ability to pass through minerals as if they were air.
+Storm Magic^Fr^50^You gain a boost in spellcasting power during  storms.
+Storm Magic^Sto^94^You gain a boost in spellcasting power during  storms.
+Storm of Throws^EL^67^You become a flurry of thrown weapons, targeting  all nearby opponents.
+Stormheart^PG^44^The sea is in your blood.
+Strafing Breath^DCS^87^You can sustain your breath weapon when you  use it on the wing, covering a larger ground area in its effect.
+Street Smart^FRCS^38^You have learned how to keep informed, ask questions,  and interact with the underworld without raising suspicion.
+Street Smart^PG^44^You know how to keep informed, ask questions,  and interact with the underworld without raising suspicions.
+Strength of the Charger^OA^66^You share the spirit of Utaku Shiko, the founder  of the Utaku Battle Maiden tradition.
+Strength of the Crab^OA^87^You claim descent from Hida, the first Crab.
+Strength of Two^RE^113^Your quori spirit gives you unmatched willpower.
+Strong Mind^ECS^61^You are unusually hard to affect with psionic  powers and mind attacks.
+Strong Mind^Und^27^You are unusually difficult to affect with psionic  powers and mind attacks.
+Strong Soul^FRCS^38^The souls of your people are hard to separate  from their bodies.
+Strong Soul^OA^66^You claim descent from Moto Soro, the simple  peasant who earned his place among samurai and founded the Moto family.
+Strong Soul^PG^44^You possess an innate resistance to fell magic  and supernatural attacks.
+Stunning Fist^PH^101^You know how to strike opponents in vulnerable  areas.
+Subdual Substitution^DD^52^The deity can modify a spell that uses energy  to deal damage to deal subdual damage instead.
+Subdual Substitution^TB^42^You can modify a spell that uses energy to deal  damage to deal nonlethal damage instead.
+Subduing Strike^BE^46^You are adept at striking to deal nonlethal  damage even with normal weapons.
+Subsonics^CAd^112^Your music can affect even those who do not  consciously hear it.
+Subsonics^SaS^40^Your music can affect even those who do not  consciously hear it.
+Subtle Sigil^RD^154^You are able to fade your sigils into invisibility,  but still tap into their magical energy.
+Sudden Empower^CAr^83^You can cast a spell to greater effect without  special preparation.
+Sudden Empower^MH^28^You can cast one spell per day to greater effect  without special preparation.
+Sudden Energy Affinity^MH^28^You can modify a spell's energy type once per  day without special preparation.
+Sudden Enlarge^MH^28^You may cast one spell per day with a greater  range than normal without special preparation.
+Sudden Extend^CAr^83^You can make a spell last longer than normal  without special preparation.
+Sudden Extend^MH^28^You can cast one spell per day with a longer  duration than normal without special preparation.
+Sudden Maximize^CAr^83^You can cast a spell to maximum effect without  special preparation.
+Sudden Maximize^MH^28^Once per day you can cast a spell to maximum  effect without special preparation.
+Sudden Quicken^CAr^83^You can cast a spell with a moment's thought  without special preparation.
+Sudden Quicken^MH^28^Once per day you can cast a spell with a moment's  thought without special preparation.
+Sudden Silent^CAr^83^You can cast a spell silently without special  preparation.
+Sudden Silent^MH^28^Once per day you can cast a spell silently without  special preparation.
+Sudden Still^CAr^83^You can cast a spell without gestures or special  preparation.
+Sudden Still^MH^28^Once per day you can cast a spell without gestures  without special preparation.
+Sudden Widen^CAr^83^You can increase a spell's area without special  preparation.
+Sudden Widen^MH^28^Once per day you can increase the area of a  spell without special preparation.
+Sugliin Mastery^Fr^50^You are a master at fighting with the massive  sugliin.
+Summon Earth Elemental^Rac^169^Like many experienced deep gnomes, you have  developed the ability to summon earth elementals to help you with tasks.
+Sun School^CW^112^You have learned a number of esoteric martial  arts techniques inspired by the sun.
+Sunken Song^Sto^94^You can project your voice underwater.
+Superior Expertise^DD^52^The deity has mastered the art of defense in  combat.
+Superior Expertise^FP^215^You have mastered the art of defense in combat.
+Superior Expertise^OA^66^You have mastered the art of defense in combat.
+Superior Initiative^EL^67^You can react even more quickly than normal  in a fight.
+Supernatural Blow^MW^25^Choose one favored enemy that is immune to critical  hits. You know how to place blows against this opponent for best effect.
+Supernatural Transformation^SS^39^You convert a spell-like ability to a supernatural  ability.
+Suppress Weakness ^Dr^74^Your vulnerability to an energy type is reduced.
+Surefooted^PG^45^You are used to fighting on steep slopes and  treacherous surfaces.
+Surrogate Spellcasting^SS^39^You use substitute verbal and somatic components  when casting spells.
+Survivor^FRCS^38^Your people thrive in regions that others find  uninhabitable, and excel at uncovering the secrets of the wilderness and  surviving to tell the tale.
+Survivor^Gh^39^Your people thrive in a region that others find  uninhabitable, and you excel at uncovering the secrets of the wilderness  and surviving to tell the tale.
+Survivor^PG^45^Your people thrive in places that others find  almost uninhabitable, and you know many of the secrets of the wilderness.
+Svirfneblin Figment^Rac^169^Your time underground has made you acutely aware  of even slight differences in sound and vision in caves that have never  seen the sun. Accordingly, your illusions are finely tuned and ultra-realistic.
+Swamp Stalker^SS^40^You are adapted to a marshy environment.
+Swarm of Arrows^EL^67^You can fire a veritable storm of arrows at  nearby opponents.
+Swarmfighting^CW^105^You and allies with this feat can coordinate  melee attacks against a single target and are adept at fighting side by  side in close quarters.
+Swarmfighting^Rac^169^You and allies with this feat can coordinate  melee attacks against a single target and are adept at fighting side by  side in close quarters.
+Swarm's Embrace^Sh^158^You have a natural affinity for swarms and can  stand in the midst of a swarm with few harmful effects.
+Swift and Silent^PG^45^The shadows are your friends, and your footfalls  are whispers of death.
+Swiftwing Elite^RE^116^Your swiftwing shifter trait improves.
+Swim Like a Fish^CD^85^You can breathe and swim underwater with grace.
+Swim-By Attack^Sto^94^You can attack in the middle of a fast pass  by your opponent.
+Tactile Trapsmith^CAd^112^You can rely on your rapid reflexes and nimble  fingers instead of your intellect when searching a room or when disabling  a trap.
+Tail Constrict^Dr^74^You can make constriction attacks with your  tail.
+Tail Rattle^SK^147^Your tail gains a rattle like that of a serpent.
+Tail Sweep Knockdown^Dr^74^Your tail sweep attack knocks opponents prone.
+Talenta Warrior^RE^112^You have trained with the ancestral weapons  of the Talenta halflings and are particularly adept at striking from the  back of a dinosaur mount.
+Talented^XPH^51^You can overchannel powers with less cost to  yourself.
+Talfirian Song^Rac^170^You can use the power of your bardic music to  enhance your illusion spells.
+Tall Mouther Hunter^ShS^21^Because of your cultural hatred for tall mouthers,  you have had specific training in how best to fight them.
+Tattoo Focus^DMG^194^You bear the powerful magical tattoos of a Red  Wizard of Thay.
+Tattoo Focus^FRCS^38^You bear the powerful magic tattoos of a Red  Wizard of Thay.
+Tattoo Focus^PG^45^You bear the powerful magical tattoos of a Red  Wizard of Thay.
+Tattoo Magic^LD^189^You can create tattoos that store spells.
+Tattoo Magic^Rac^170^You can create tattoos that store spells.
+Temper Ectoplasm^Gh^39^You can make durable equipment out of ectoplasm.
+Tempest Breath^Dr^74^You can make your breath weapon strike with  the force of a windstorm.
+Tenacious Magic^EL^68^Choose one of your spells or spell-like abilities.  That magic cannot be dispelled, only suppressed.
+Tenacious Magic^FRCS^38^You can use the Shadow Weave to make your spells  harder for Weave users to dispel.
+Tenacious Magic^PG^45^You can use the Shadow Weave to make your spells  harder for Weave users to dispel.
+Terrifying Rage^DMG^210^While in a rage, you panic your opponents.
+Terrifying Rage^EL^68^While in a rage, you panic your opponents.
+The Gentle Way Mastery^OA^81^You have mastered the martial arts style of  'The Gentle Way' -- a soft form emphasizing throws and movement.
+Theocrat^Rac^170^You have the delicate touch needed to maintain  the favor of your patron deity and the political skills needed to survive  in the trenches of bureaucratic warfare common in the lands ruled by agents  of the Mulhorandi pantheon.
+Thicken Mucus^LoM^23^An aboleth with this feat can produce mucus  that is thicker than normal, and other creatures find it difficult to swim  through.
+Thick-Skinned^SS^40^Your tough hide grants improved damage reduction.
+Thrall Bred^LoM^182^Spawned in the breeding pits of the mind flayers  or the beholders, you have unusual strength and hardiness, as well as loyalty.
+Thrall to Demon^BV^50^The character formally supplicates himself to  a demon prince.
+Thrall to Demon^CR^23^You formally supplicate yourself to a demon  prince.
+Three Mountains^CW^114^You are a master of fighting with powerful bludgeoning  weapons.
+Throw Anything^CW^105^In your hands, any weapon becomes a deadly ranged  weapon.
+Throw Anything^SF^9^In your hands, any weapon becomes a deadly ranged  weapon.
+Thug^FRCS^38^Your people know how to get the jump on the  competition and push other people around.
+Thug^PG^45^You have a knack for getting the jump on the  competition and pushing other people around.
+Thunder Twin^FRCS^38^You are one of the dwarven generation of twins  born after Moradin's Thunder Blessing in the Year of Thunder (1306 DC).
+Thunder Twin^PG^46^You are one of the generation of dwarf twins  born after Moradin's Thunder Blessing in the Year of Thunder.
+Thunderclap^SS^40^You create a cone of deafening sound by clapping  two limbs together.
+Thundering Rage^EL^68^Your rage attacks can cause thunderous roars  that can deafen opponents.
+Tireless^PG^46^You don't know the meaning of the word 'quit.'
+Titan Fighting^RS^145^You have been trained to fight larger creatures,  and you are adept at dodging their attacks.
+Tomb-Born Fortitude^LM^30^The power of undeath taints you, body and soul.  Its power has hardened your flesh and given it the foul look of the grave.
+Tomb-Born Resilience^LM^30^The power of undeath taints you, deadening your  mind and body to the effects of mind-controlling magic, poison, and disease.
+Tomb-Born Vitality^LM^31^The power of undeath taints you, body and soul.  Its power has removed your need to sleep and eat.
+Tomb-Tainted Soul^LM^31^Your soul is tainted by the foul touch of undeath.
+Toothed Blow^Sto^94^You are able to hammer your foes more effectively  underwater.
+Tormented Knight^CR^23^You are inexorably bound to the loathsome yugoloths  that lurk in the Barrens of Doom and Despair, and you strive to bring misery  and pain to all creatures that oppose them.
+Totem Companion^ECS^61^Instead of an animal companion, you have your  totem magical beast as a companion.
+Touch Attack Specialization^Gh^39^Choose one of your ghost touch attacks that  deals hit point damage, ability damage, or ability drain, such as Corrupting  Touch. You are especially good at using this touch attack.
+Touch of Benevolence^CR^22^Despite your evil alignment, you are prone to  moments of benevolence and mercy.
+Touch of Golden Ice^BE^47^Your touch is poisonous to evil creatures.
+Touch of Hate^PG^177^Because you are favored by Bane, you can transform  animals into evil minions.
+Touch Spell Specialization^CAr^83^You deal extra damage with touch spells.
+Touchstone^Sa^53^You forge a link with a power-rich location,  referred to as a touchstone site.
+Toughness^PH^101^You are tougher than normal.
+Tower Shield Proficiency^PH^101^You are proficient with tower shields.
+Toxic Mucus^LoM^23^An aboleth with this feat can produce mucus  that is poisonous to other creatures.
+Track^PH^101^You can follow the trails of creatures and characters  across most types of terrain.
+Trample^PH^101^You are trained in using your mount to knock  down opponents.
+Transdimensional Spell^CAr^84^You can cast spells that affect targets lurking  in coexistent planes and extradimensional spaces whose entrances fall within  the spell's area.
+Transdimensional Spell^CD^85^You can cast spells that affect targets lurking  in coexistent planes and extradimensional spaces whose entrances fall within  the spell's area.
+Transdimensional Spell^UE^45^You can cast spells that affect targets lurking  in coexistent planes and extradimensional spaces whose entrances fall within  the spell's area.
+Transfer Legacy^WL^16^You can temporarily transfer one of your legacy  item's abilities to another magic item.
+Trap Sense^EL^68^You can sense nearby traps even if not actively  searching for them.
+Trapmaster^LE^9^You have studied the funereal architecture and  lethal traps of a dozen long-dead cultures, which gives you an uncanny knack  for avoiding traps.
+Treefriend^SS^40^You are adapted to a forest environment.
+Treetopper^FRCS^38^Your people are at home in the trees and high  places, daring falls that paralyze most other folk in abject terror.
+Treetopper^PG^46^Your people are at home in the trees and high  places.
+Tremendous Charge^DCS^87^You know how to use your mount's power to make  your lance attacks even more deadly.
+Trivial Knowledge^RS^145^You have the ability to dredge up obscure knowledge  in appropriate situations.
+True Believer^CD^86^Your deity rewards your unquestioning faith  and dedication.
+Truebond^DMG2^232^Your bond to your chosen item becomes stronger.
+Truedive Elite^RE^116^Your truedive shifter trait improves.
+Trustworthy^SaS^40^Others feel comfortable telling you their secrets.
+Tunnel Fighting^RS^145^You are adept at maneuvering and fighting in  tight spaces and underground passages.
+Tunnel Riding^RS^145^You are particularly adept at maneuvering mounts  through tight spaces and underground passages.
+Tunnelfighter^Und^27^You can fight more naturally in the cramped  and close quarters of caves and tunnels than usual.
+Tunnelrunner^Und^27^You can move naturally in the cramped quarters  of caves and tunnels.
+Turtle Dart^RS^145^You have mastered the style of fighting with  a short sword while wearing extremely heavy armor and carrying a large shield.
+Twin Power^XPH^51^You can manifest a power simultaneously with  another power just like it.
+Twin Spell^CAr^84^You can simultaneously cast a single spell twice.
+Twin Spell^FRCS^39^You can cast a spell simultaneously with another  spell just like it.
+Twin Spell^PG^46^You can cast a spell simultaneously with another  spell just like it.
+Twin Spell^TB^42^You can cast a spell simultaneously with another  spell just like it.
+Twin Sword Style^FRCS^39^You have mastered a style of defense that others  find frustrating.
+Twin Sword Style^Gh^39^You have mastered a style of defense that others  find frustrating.
+Twin Sword Style^PG^46^You have mastered a defensive style based on  wielding a blade in each hand.
+Two-Weapon Defense^PH^102^Your two-weapon fighting style bolsters your  defense as well as your offense.
+Two-Weapon Fighting^PH^102^You can fight with a weapon in each hand. You  can make one extra attack each round with the second weapon.
+Two-Weapon Rend^EL^68^You can rend opponents when fighting with two  weapons.
+Unarmored Body^RE^120^Your body is crafted without its normal layer  of armor, trading off physical strength for magical potential.
+Unavoidable Strike^XPH^52^You can make an unarmed strike or use a natural  weapon against your foe as if delivering a touch attack.
+Unbalancing Strike^OA^66^You can strike a humanoid opponent's joints  to knock him off balance.
+Uncanny Accuracy^EL^68^You can ignore anything less than total cover  or total concealment when using ranged weapons.
+Uncanny Scent^SS^40^You can pinpoint scents at a greater distance.
+Unconditional Power^XPH^52^Disabling conditions do not hold you back.
+Undead Empathy^ECS^61^You are adept at communicating with and influencing  the undead.
+Undead Leadership^LM^31^You gain the service of loyal undead followers.
+Undead Mastery^CD^90^You can command a greater number of undead than  normal.
+Undead Mastery^EL^68^You can command a greater number of undead than  normal.
+Underfoot Combat^RW^152^You can enter the space that a foe at least  two size categories bigger than you occupies.
+Undying Fate^RD^155^You have pledged your unswerving obedience to  Wee Jas, and she in turn has granted you special insight into life and death.
+Unholy Strike^CD^90^Your attacks deal great damage to good creatures.
+Unholy Strike^EL^68^Your attacks deal great damage to good creatures.
+Unquenchable Flame of Life^LM^31^You are hardened to the attacks of the undead.
+Up the Walls^XPH^52^You can run on walls for brief distances.
+Urban Stealth^RD^154^You are particularly adept at moving quietly  and unnoticed through the city.
+Urban Tracking^ECS^61^You can track the location of missing persons  or wanted individuals within communities.
+Urban Tracking^RD^154^You can track down the location of missing persons  or wanted individuals within communities.
+Vampire Hunter^LM^31^Your knowledge of vampires has given you the  extraordinary ability to detect subtle signs of their presence and to resist  their dominating gaze ability.
+Veil of Cyric^CSW^146^You have reconciled yourself to the unfortunate  truth that hard decisions and regrettable actions are necessary in the service  of your deity.
+Vermin Companion^ECS^62^Instead of an animal companion, you have a vermin  creature as a companion.
+Vermin Shape^ECS^62^You can use your wild shape ability to assume  vermin forms instead of animal forms.
+Vermin Wild Shape^EL^68^You can wild shape into vermin form.
+Verminfriend^BV^50^Vermin regard the character better than they  would normally.
+Versatile Performer^CAd^112^You are skilled at many kinds of performances.
+Veteran Knowledge^HB^99^You are capable of seeing potential battlefield  advantages where others cannot.
+Via Negativa^CR^22^You can channel greater amounts of negative  energy into your inflict spells.
+Vicious Wound^SS^40^Damage you deal causes wounds that bleed excessively.
+Vile Ki Strike^BV^50^The character can focus evil power into his  unarmed strike.
+Vile Martial Strike^BV^50^The character can focus evil power in her weapon  blows.
+Vile Natural Attack^BV^50^The character can focus evil power into his  natural attacks.
+Violate Spell^BV^50^The character can transform one of his spells  into an evil spell, and the wounds the spell inflicts are tainted with the  foulest evil.
+Violate Spell-Like Ability^BV^50^The creature's spell-like abilities are particularly  tainted with evil.
+Virulent Poison^SS^40^Your poison attack is more effective.
+Vorpal Strike^EL^68^Your unarmed strikes can behead your opponents.
+Vow of Abstinence^BE^47^You have taken a sacred vow to abstain from  alcoholic beverages, drugs, stimulants such as caffeine, and intoxication.
+Vow of Chastity^BE^47^You have taken a sacred vow to refrain from  marriage and sexual intercourse.
+Vow of Nonviolence^BE^47^You have taken a sacred vow to avoid violence  against humanoids.
+Vow of Obedience^BE^48^You have taken a sacred vow to live according  to the dictates of another, generally your superior in a religious order  or similar organization.
+Vow of Peace^BE^48^You have taken a sacred vow to abstain from  harming any living creature.
+Vow of Poverty^BE^48^You have taken a sacred vow to forswear material  possessions.
+Vow of Purity^BE^48^You have taken a sacred vow to avoid contact  with dead flesh.
+Vremyonni Training^UE^45^You have had more than the typical amount of  training with the vremyonni, the Old Ones who research spells and  craft magic items for the Witches of Rashemen.
+Wand Mastery^ECS^62^Wands are far more potent in your hands.
+Wandstrike^CAr^84^You can channel the magical energy of a wand  through your melee attacks.
+Warden Initiate^ECS^62^You have been trained in the ancient druidic  tradition of the Wardens of the Wood, a sect dedicated to protecting the  eastern plain and the great woods of the Eldeen Reaches.
+Warped Mind^LoM^182^Your tainted form has altered the physical nature  of your brain, making you resistant to mental effects and more capable of  unleashing the power of your mind on others.
+Warrior Instinct^OA^66^Your ancestor, Matsu Hitomi, was the most famous  female samurai of the early Empire.
+Warrior Shugenja^OA^66^Your ancestor, Agasha Nodotai, was a shugenja  well versed in the code of bushido and the way of war.
+Water Adaptation^Rac^170^You favor your aquatic elven parent and have  developed the ability to breathe and move about in water easily.
+Water Adaptation^Sto^94^You favor your aquatic elf parent and have developed  the ability to breathe and move about in water easily.
+Water Heritage^PlH^42^You are descended from creatures native to the  Plane of Water.
+Waterspawn^LoM^182^Your abnormal body and heritage has become more  pronounced. You have prominent fins and are supremely well adapted to the  icy deeps.
+Weakening Touch^CW^106^You can temporarily weaken an opponent with  your unarmed strike.
+Weapon Finesse^PH^102^You are especially skilled at using weapons  that can benefit as much from Dexterity as from Strength.
+Weapon Focus^PH^102^Choose one type of weapon. You can also choose  unarmed strike or grapple (or ray, if you are a spellcaster) as a weapon  for purposes of this feat. You are especially good with this weapon.
+Weapon Group (Axes)^UA^95^You understand how to use axes and axelike weapons.
+Weapon Group (Basic Weapons)^UA^95^You understand how to use a few basic weapons.
+Weapon Group (Bows)^UA^95^You understand how to use bows.
+Weapon Group (Claw Weapons)^UA^95^You understand how to use weapons strapped to  the hands.
+Weapon Group (Crossbows)^UA^95^You understand how to use crossbows.
+Weapon Group (Druid Weapons)^UA^95^You understand how to use weapons favored by  druids.
+Weapon Group (Exotic Double Weapon)^UA^95^You understand how to use the exotic double  weapons associated with the weapon groups that you have mastered.
+Weapon Group (Exotic Weapons)^UA^96^You understand how to use the exotic weapons  associated with the weapon groups that you have mastered.
+Weapon Group (Flails and Chains)^UA^96^You understand how to use flails and chain weapons.
+Weapon Group (Heavy Blades)^UA^96^You understand how to use large bladed weapons.
+Weapon Group (Light Blades)^UA^96^You understand how to use light bladed weapons.
+Weapon Group (Maces and Clubs)^UA^96^You understand how to use maces and clubs.
+Weapon Group (Monk Weapons)^UA^97^You understand how to use weapons normally favored  by monks.
+Weapon Group (Picks and Hammers)^UA^97^You understand how to use picks and hammers.
+Weapon Group (Polearms)^UA^97^You understand how to use polearms.
+Weapon Group (Slings and Thrown Weapons)^UA^97^You understand how to use slings and handheld  thrown weapons.
+Weapon Group (Spears and Lances)^UA^97^You understand how to use spears and javelins.
+Weapon Specialization^PH^102^Choose one type of weapon for which you have  already selected the Weapon Focus feat. You can also choose unarmed strike  or grapple as your weapon for purposes of this feat. You deal extra damage  when using this weapon.
+Whirling Steel Strike^ECS^62^Through monastic weapon training, you have mastered  a fighting style that makes use of an unusual monk weapon: the longsword.
+Whirlwind Attack^PH^102^You can strike nearby opponents in an amazing,  spinning attack
+Whirlwind Tail Sweep^Dr^75^You can sweep your tail in a circular arc.
+Whispered Secrets^RD^155^You revere the Maimed Lord and have devoted  your miserable, worthless life to learning but a few of the Whispered One's  secrets.
+White Scorpion Strike^RE^112^Your fists and feet sting like the dread white  scorpion and are particularly effective against undead.
+Widen Aura of Courage^EL^69^Your aura of courage is wider than normal.
+Widen Aura of Despair^EL^69^Your aura of despair is wider than normal.
+Widen Power^XPH^52^You can increase the area of your powers.
+Widen Spell ^DD^52^The deity can increase the area of its spells.
+Widen Spell^Mag^23^You can increase the area of your spells.
+Widen Spell^PH^102^You can increase the area of your spells.
+Widen Spell^TB^42^You can increase the area of your spells.
+Wield Oversized Weapon^CW^153^You can use larger than normal weapons with  ease.
+Wild Talent^LoM^182^Your mind wakes to a previously unrealized talent  for psionics.
+Wild Talent^XPH^52^Your mind wakes to a previously unrealized talent  for psionics.
+Wildhunt Elite^RE^116^Your shifter-enhanced instincts and senses allow  you to detect concealed and invisible creatures.
+Willing Deformity^BV^50^Through scarification, self-mutilation, and  supplication to dark power, the character intentionally mars her own body.
+Winged Warrior^RW^153^You use your wings for more than just flying.
+Wingover^MM^304^The creature can change direction quickly while  flying.
+Wingover^MM2^18^The creature can change direction quickly while  flying.
+Wingover^MW^25^You change direction quickly once per round  while airborne.
+Wingsinger^Sto^94^You can use song or a wind instrument to compel  the winds to obey you.
+Wingstorm^Dr^75^You can flatten targets with blasts of air from  your wings.
+Wingstorm^SS^40^You can flatten targets with blasts of air from  your wings.
+Winter's Champion^Fr^50^Your paladin spell list is enhanced.
+Winter's Child^SS^40^You are adapted to a cold environment.
+Winter's Mount^Fr^50^Your special mount is native to the frostfell.
+Wisdom Breeds Caution^Und^27^Not getting into a dangerous situation is generally  the wisest course, but if danger is unavoidable, you're prepared. You rely  more on caution and forethought than you do on physical prowess.
+Wise to Your Ways^Gh^39^You are particularly resistant to the unusual  attacks of your favored enemy.
+Witchlight^Gh^39^You can create witchlight, a harmless faint  light, on yourself or an object.
+Wolf Berserker^UE^45^You have studied the fighting style of the wolf  and employ its tactics in combat.
+Wolfpack^RW^153^You can gain an extra advantage when you and  your allies can gang up on a foe.
+Wolverine's Rage^CD^86^You can fly into a berserk rage when injured.
+Woodland Archer^RW^154^You have honed your archery ability in the wilds  of the forest.
+Woodwise^ShS^21^You are trained in fighting in woodlands and  know how to use the terrain to best advantage.
+Woodwise^UE^45^You are trained in fighting in woodlands and  know how to use the terrain to best advantage.
+Words of Creation^BE^48^You have learned a few of the words that were  spoken to create the world.
+Wounding Attack^XPH^52^Your vicious attacks wound your foe.
+Wounding Spell^LE^9^Because you have studied the cruel arts of the  Athalantan magelords of old, you know how to cast spells that cause terrible,  bleeding wounds.
+Yondalla's Sense^RW^152^You display a shrewd perception of danger. Other  halflings say the blessing of Yondalla is upon you.
+Zen Archery^CW^106^Your intuition guides your hand when you use  a ranged weapon.
+Zen Archery^SF^9^Your intuition guides your hand when you use  a ranged weapon.
+Zone of Animation^CD^90^You can channel negative energy to animate  undead.
+Zone of Animation^EL^69^You can channel negative energy to animate  undead.
+Broken One's Sacrifice^CV^28^Your dedication to Ilmater's philosophy has given you the power to take attacks directed at others.
+Carmendine Monk^CV^28^You have learned that study is just as important as insight to finding enlightenment.
+Defender of the Homeland^CV^28^You have sworn a sacred oath to protect your country from evil.
+Detect Shadow Weave User^CV^28^You can determine if a magic item or spellcaster is using the Weave or the Shadow Weave.
+Druuth Slayer^CV^29^You have studied the lore of the druuth (a cabal of doppelgangers led by a mind flayer) and know how to recognize and resist their powers.
+Duerran Metaform Training^CV^29^Your studies have shown you the way to link your psionics and your enlarge person spell-like ability.
+Duerran Stealth Training^CV^29^Your studies have shown you the way to link your psionics and your invisibility spell-like ability.
+From Smite to Song^CV^29^You can channel your destructive holy energy into powerful song magic for the glory of Milil.
+Initiate of Anhur^CV^30^You have been initiated into the greatest secrets of Anhur's church.
+Initiate of Arvoreen^CV^30^You have been initiated into the greatest secrets of Arvoreen's church.
+Initiate of Baravar Cloakshadow^CV^30^You have been initiated into the greatest secrets of Baravar Cloakshadow's church.
+Initiate of Eilistraee^CV^30^You have been initiated into the greatest secrets of Eilistraee's church.
+Initiate of the Holy Realm^CV^30^You have been initiated into the greatest secrets of one of the faiths of the Holy Realm (Chauntea, Helm, Lathander, Selune, or Sune).
+Initiate of Horus-Re^CV^30^You have been initiated into the greatest secrets of Horus-Re's church.
+Initiate of Milil^CV^31^You have been initiated into the greatest secrets of Milil's church.
+Initiate of Nobanion^CV^31^You have been initiated into the greatest secrets of Nobanion's church.
+Initiate of Torm^CV^31^You have been initiated into the greatest secrets of Torm's church.
+Initiate of Tymora^CV^32^You have been initiated into the greatest secrets of Tymora's church.
+Knight of the Red Falcon^CV^32^Your military order has a legendary ability to survive against overwhelming odds.
+Knight of the Risen Scepter^CV^32^Your military order is dedicated to fighting Set and his minions, and even death cannot stop you from this task.
+Knight of Tyr's Holy Judgment^CV^32^You can draw upon the power of Tyr to sense and understand the law and to locate devils.
+Knight ot Tyr's Merciful Sword^CV^33^You can draw upon the power of Tyr to sense where you are needed.
+Mark of the Triad^CV^33^You have been initiated into the greatest secrets of the Triad, the godly triumvirate of Tyr, Torm, and Ilmater.
+Overcome Shadow Weave^CV^33^You understand the strengths and weaknesses of the Shadow Weave and are more resistant to its tricks.
+Paladin of the Noble Heart^CV^33^You are tasked by Ilmater to eliminate cruelty from the world, particularly that of Loviatar.
+Silver Blood^CV^33^You have magically or alchemically imbued your flesh and blood with silver, making you resistant to lycanthrope attacks.
+Silver Fang^CV^33^By following a ritual taught by the Fangshields, your natural attacks are suffused with the power of silver and are fully effective against lycanthropes.
+Smiting Power^CV^33^You use your smite ability to augment other combat maneuvers.
+Sword of the Arcane Order^CV^34^Members of your military order have a special connection with arcane magic.
+Sun Soul Monk^CV^34^Your training with this monk order gives you special powers depending on which sect you follow.
+Archivist of Nature^HH^119^In addition to your studies of the darkness, you have spent time studying giants and fey.
+Bane Magic^HH^119^Your spells deal extra damage to a particular type of creature.
+Blood Calls to Blood^HH^120^Exploring the latent potential in your blood due to your fiendish descent, you learn how to better adapt to the mystical attacks of your forebears.
+Corrupt Arcana^HH^120^You can prepare and cast corrupt spells.
+Corrupt Spell Focus^HH^120^All spells you cast that have a corrupt component (such as call forth the beast, master's lament, or chain of sorrow) are more potent than normal.
+Debilitating Spell^HH^120^By calling upon the taint within, you add a malign power to your offensive spells.
+Debilitating Strike^HH^120^By calling upon the taint within, you add a malign power to your melee attacks.
+Deformity (Skin)^HH^121^Due to a regimen of deliberate abuse, you have roughened your skin until it has grown as coarse and tough as rhino hide.
+Deformity (Tall)^HH^121^Through long and painful stints on the rack, bolstered by the surgical implantation of various splints and struts, you have stretched yourself to well over 7 feet in height.
+Deformity (Teeth)^HH^121^By filing your teeth to points and brutalizing your gums, you gain a hideous smile full of razor-sharp teeth that enable you to make a grisly bite attack.
+Deformity (Tongue)^HH^121^Through protracted self-mutilation that involves frequently piercing your tongue and dipping it in acid, your tongue becomes hideous to behold but oddly sensitive to the environment.
+Disease Immunity^HH^121^Whether due to prolonged exposure or natural hardiness, you have grown immune to some diseases and resistant to all others.
+Draconic Archivist^HH^122^In addition to your studies of the darkness, you have spent time studying dragons and constructs.
+Dreamtelling^HH^122^You can use your Knowledge (the planes) skill to interpret your dreams or the dreams of others, thus gleaning useful information and insights.
+Eldritch Corruption^HH^122^You can add power to your spells or spell-like abilities at the expense of your companions' health.
+Font of Life^HH^122^Your life-force is strong enough to make you highly resistant to all forms of energy drain and level loss.
+Forbidden Lore^HH^123^You gain hideous insights into subjects not meant to be understood by mortal minds.
+Greater Corrupt Spell Focus^HH^123^Your corrupt spells are now even more potent than they were before.
+Haunting Melody^HH^123^You can use your music to inspire fear.
+Improved Oneiromancy^HH^123^With the Improved Oneiromancy feat, you gain additional dream-related spellcasting abilities.
+Lunatic Insight^HH^123^Your madness grants you insight and knowledge.
+Mad Faith^HH^123^Your depravity has twisted the connection between you and your patron deity. You suffer flashes of insight interrupted by flashes of madness.
+Master of Knowledge^HH^123^You have spent most of your life in study, and it comes naturally to you now.
+Oneiromancy^HH^123^You gain a number of abilities and advantages related to dreams and magic.
+Pure Soul^HH^124^Your faith or purity of mind overrides the evils within you. You are immune to taint.
+Spirit Sense^HH^124^You can see and communicate with the souls of the recently departed.
+Surge of Malevolence^HH^124^You empower yourself by drawing on the taint within.
+Tainted Fury^HH^124^You can channel your physical corruption into a state of fury.
+Touch of Taint^HH^124^One of your attack forms that normally deals ability damage, ability drain, or energy drain can also deal corruption or depravity.
+Unnatural Will^HH^124^You have learned to focus your force of personality and inner strength to stand against fearful circumstances.
+Willing Deformity^HH^125^Through scarification, self-mutilation, or supplication to dark powers, you intentionally mar your own body.
+Augment Elemental^MoE^46^Your knowledge of planar magic allows you to imbue your summoned elementals with extraordinary combat prowess and durability.
+Cull Wand Essence^MoE^46^You can focus the raw magical energy of a wand or staff into a beam of energy.
+Deathless Fleshgrafter^MoE^46^You can grow and graft the tissues and body parts of deathless creatures onto others, granting the recipients of your grafts new, potent abilities.
+Dorje Mastery^MoE^46^Psionic dorjes are more potent in your hands.
+Dragon Prophesier^MoE^46^Like the dragons, you seek to untangle and perceive the record of everything that has been, and more important, what will be.
+Dragon Totem Focus^MoE^46^Your focus allows you to enjoy the benefit of a dragon totem ritual longer than normal.
+Dragon Totem Lorekeeper^MoE^47^You have been instructed in how to perform the rituals of dragon totem magic.
+Dragon Totem Scion^MoE^47^You are naturally attuned to the magic of the dragon totem ritual.
+Eldeen Plantgrafter^MoE^47^You can create and apply plant grafts onto others, granting the recipients of your grafts new, potent abilities.
+Elemental Grafter^MoE^47^You can create and apply elemental grafts onto others, granting the recipients of your grafts new, potent abilities.
+Elemental Helmsman^MoE^47^You are more capable of piloting an elemental vessel.
+Elemental Smite^MoE^47^You can channel the energy associated with one of your elemental grafts into your melee attacks.
+Etch Schema^MoE^47^You can create a minor schema.
+Heroic Companion^MoE^48^Your luck extends to your companion creature.
+Heroic Focus^MoE^48^Despite the dangers all around, you can quickly regain your psionic focus.
+Improved Homunculus^MoE^49^You are adept at improving and modifying your homunculus. Whenever you advance your homunculus's Hit Dice, you can also imbue it with special supernatural abilities.
+Prophecy's Artifex^MoE^50^Your perception of the draconic Prophecy gives you insights that allow you to transcend the normal limits of magic item use.
+Prophecy's Explorer^MoE^50^Your perception of the draconic Prophecy imbues you with a preternatural sense of your surroundings, enabling you to move easily and quickly through dangerous areas.
+Prophecy's Hero^MoE^50^Your perception of the draconic Prophecy charges you with the will to prevail, providing you with the opportunity to see a way to victory even when the odds are stacked against you.
+Prophecy's Mind^MoE^50^You meld your perception of the draconic Prophecy with a mental focus that provides you with momentary warning when death is at hand.
+Prophecy's Shaper^MoE^50^Your perception of the draconic Prophecy is such that you can disrupt reality and make your spells more powerful than reality would normally allow.
+Prophecy's Shepherd^MoE^50^Your perception of the draconic Prophecy is such that you can alter the natural flow of the world by connecting your knowledge of life-force with the world around you.
+Prophecy's Slayer^MoE^51^Your perception of the draconic Prophecy includes a keen appreciation of life. You recognize how fragile and tenuous life truly is when balanced against your lethal foreknowledge.
+Psiforged Body^MoE^51^As a warforged, your body can be crafted using trace amounts of psionically resonant deep crystal, providing you with increased psionic power and the ability to store psionic energy in your body. If you take this feat, you will often be referred to as a psiforged.
+Psionic Luck^MoE^51^Your psionic focus improves your luck.
+Psychic Rush^MoE^51^You can occasionally manifest a psionic power with less effort.
+Quicken Dragonmark^MoE^51^You can use your dragonmark abilities more quickly.
+Rapid Infusion^MoE^51^You can imbue an item with an infusion more quickly than normal.
+Symbiont Mastery^MoE^51^You have stronger control over an attached symbiont than regular creatures, and you gain vitality for each symbiont attached to you.
+Wand Surge^MoE^51^You can squeeze more magic out of charged items.
+Azure Enmity^MoI^34^You can channel incarnum to enhance your ability to deal damage to your favored enemies.
+Azure Talent^MoI^34^The soul energy of incarnum increases your mental capacity.
+Azure Touch^MoI^34^You can channel incarnum to enhance your abilit to heal.
+Azure Toughness^MoI^35^You can use incarnumto boost your physical vigor.
+Azure Turning^MoI^35^You can blast ndead with incarnum-purified positive energy.
+Azure Wild Shape^MoI^35^You can channel incarnum to enhance your combat prowess while wild shaped.
+Bonus Essentia^MoI^35^You are better able to harness your personal store of incarnum.
+Cerulean Fortitude^MoI^35^You can use incarnum to boost your ability to resist effects that would adversely affect your health.
+Cerulean Reflexes^MoI^35^You can use incarnum to boost your ability to avoid harm.
+Cerulean Will^MoI^35^You can use incarnum to boost your willpower.
+Cobalt Charge^MoI^35^You can channel incarnum to deal devastating strikes when charging.
+Cobalt Critical^MoI^35^You can focus your spirit into your melee weapon attacks, dealing more damage with successful critical strikes.
+Cobalt Expertise^MoI^35^By channeling the soul energy of weapon masters past, present, and future, you become more adept at maneuvers of skill and expertise.
+Cobalt Power^MoI^37^By channeling the soul energy of brutal warriors past, present, and future, you become more capable of overcoming your enemies through sheer strength.
+Cobalt Precision^MoI^37^You can focus your soul energy into your ranged attacks, dealing more damage with successful critical hits.
+Cobalt Rage^MoI^37^You can channel incarnum to enhance your rage. When you do so, your eyes turn deep blue in color.
+Divine Soultouch^MoI^37^You can channel positive or negative energy to imbue yourself with incarnum.
+Double Chakra^MoI^38^One of your chakras becomes capable of holding more incarnum than it is normally capable of containing.
+Expanded Soulmeld Capacity^MoI^38^Your soul's tie to incarnum allows you to maintain more essentia in a single soulmeld.
+Healing Soul^MoI^38^You can draw upon the soul energy of incarnum to heal your wounds.
+Heart of Incarnum^MoI^38^You tap into the power of your heart chakra to gain resilience.
+Improved Essentia Capacity^MoI^38^Your capability of investing essentia improves.
+Incarnum-Fortified Body^MoI^38^The incarnum within you strengthens your body's toughness, enabling you to withstand greater injury.
+Incarnum Resistance^MoI^38^Your body, untainted by incarnum, is not easily affected by the power of soul energy.
+Incarnum Spellshaping^MoI^38^You gain the ability to invest incarnum into your spellcasting.
+Indigo Strike^MoI^38^You can channel incarnum to enhance your ability to deal damage with your skirmish attack, sneak attack or sudden strike.
+Midnight Augmentation^MoI^38^You can augment a psionic power with your personal soul energy rather than mental energy.
+Midnight Dodge^MoI^39^You can channel incarnum to enhance your ability to avoid attacks against you.
+Midnight Metamagic^MoI^39^You can channel incarnum to alter your prepared spells.
+Necrocarnum Acolyte^MoI^39^You have experienced the power of necrocarnum, a dark and twisted form of incarnum.
+Open Greater Chakra^MoI^39^You open up one of your body's centers of power, allowing you to bind a soulmeld or a magic item to that chakra.
+Open Least Chakra^MoI^39^You open up one of your body's centers of power, allowing you to bind a soulmeld or a magic item to that chakra.
+Open Lesser Chakra^MoI^40^You open up one of your body's centers of power, allowing you to bind a soulmeld or a magic item to that chakra.
+Psycarnum Blade^MoI^40^You can forge your mind blade from a mixture of mental and soul energy, enabling you to deal devastating strikes with the weapon.
+Psycarnum Crystal^MoI^40^Your psycrystal taps into the natural ebb and flow of incarnum, turning it into a small reservoir of soul energy.
+Psycarnum Infusion^MoI^40^You transform your mental focus into a brief burst of soul energy.
+Sapphire Fist^MoI^40^You can channel incarnum to enhance your ability to deliver stunning attacks.
+Sapphire Smite^MoI^40^You can channel incarnum to enhance your ability to deal mighty blows.
+Sapphire Sprint^MoI^40^Drawing on the soul energy of great runners of history, you infuse your body with incarnum to speed your steps.
+Shape Soulmeld^MoI^40^You gain the ability to shape a single soulmeld.
+Share Soulmeld^MoI^41^You can share a soulmeld with an ally with which you have a special bond.
+Soulsight^MoI^41^You can attune your soul to sense living creatures near you.
+Soultouched Spellcasting^MoI^41^By fusing your spells with incarnum, they become more capable of overcoming enemy magic and spell resistance.
+Split Chakra^MoI^41^One of your chakras becomes capable of holding both a bound soulmeld and a magic item.
+Undead Meldshaper^MoI^41^Despite having no soul of your own, you maintain the ability to channel incarnum through force of will alone.
+Aereni Focus^PE^20^From childhood you have studied one particular path, and these decades of devotion result in remarkable skill.
+Aerenal Arcanist^PE^20^Your family has studied wizardry for thousands of years.
+Aerenal Half-Life^PE^20^The Priests of Transition have guided you through strange rituals that left you poised between the world of the living and the dead.
+Perfect Reflection^PE^25^You are particularly skilled at mimicking the forms and mannerisms of others.
+Touch of Captivation^PE^35^You are sakah, and your fiendish gift allows you to captivate people around you.
+Touch of Deception^PE^35^You are sakah, and your fiend gift allows you to alter your appearance and trick others.
+Touch of Summoning^PE^35^You are sakah, and your fiendish gift allows you to summon fell creatures to do your bidding.
+Binding Brand^PE^36^You carry the brand of the binding flame, marking you as a warrior of the Ghaash'kala clans.
+Dragon's Insight^PE^48^You can call on the power of your dragonmark to enhance your natural abilities.
+Shield of Deneith^PE^48^You can channel the power of your Deneith dragonmark to defend yourself in battle.
+Swiftness of Orien^PE^48^You can draw on the power of your Mark of Passage to temporarily enhance your speed or the speed of your mount.
+Aberrant Dragonmark Gift^PE^49^Your aberrant dragonmark is especially potent.
+Aberrant Dragonmark Mystery^PE^49^You can use the power of your aberrant mark to enhance your magical abilities.
+Aberrant Dragonmark Vigor^PE^49^You can channel the energy of your aberrant mark to enhance your health.
+Ritual of Arcane Opposition^PE^60^You have been inured against the effects of arcane magic by a ritual of the Ashbound set.
+Ritual of Blight's Embrace^PE^60^You have been warded from the effects of poison and disease by a ritual of the Children of Winter, solidifying your bond with vermin.
+Ritual of the Timeless Soul^PE^60^You have been blessed by the faerie lords of Thelanis in a ritual of the Greensinger sect, and you temporarily slip from time's grasp.
+Ritual of the Woodland Bond^PE^60^You have formed a bond with the growth of the woods through a ritual of the Wardens of the Wood.
+Friends of the Tribes^PE^75^You are deeply familiar with the tribes of the Talenta Plains.
+Talenta Dinosaur Bond^PE^75^You have undergone grueling training on the dinosaur back and are skilled in the halfling techniques of fighting while mounted.
+Talenta Drifter^PE^75^Your extensive travels on the Talenta Plains give you an advantage while in that region.
+Galifaran Scholar^PE^77^You have made an exhaustive study of the history of Galifar, from the earliest roots of the Five Nations, through the formation of the united Kingdom of Galifar, and on to the Last War and the dissolution of the kingdom.
+Du'ulora Ancestor^PE^83^The tsucora are the most common of the quori, but they are not the only spirits in Dal Quor.
+Hashalaq Ancestor^PE^83^The hashalaq quori essence within you allows you to sense the emotions of others.
+Aberration Banemagic^PE^86^You can cast spells that do extra damage to aberrations.
+Indomitable Discipline^PE^86^Your strict mental discipline allows you to resist attempts to manipulate your thoughts.
+Unnatural Enemy^PE^86^You have been trained in the ways of aberrations, and you know how to recognize them and spot their weaknesses.
+Sudden Willow Strike^PE^109^Your monastic training allows great precision with your quarterstaff.
+Child of the Swamps^PE^119^You can find food and shelter in the deep swamps, and you can move more freely through the difficult terrain.
+Battlebred^PE^122^Due to traumatic experiences in past battles, the plane of Shavarath with its endless war seems never far from you.
+Chosen of the Deathless^PE^122^You carry with you an intimate familiarity with the positive energy that suffuses the City of the Dead.
+Manifest Druid^PE^122^You have a familiarity with the three manifest zones of the Eldeen Reaches and the powers of the planes to which they are linked.
+Mastery of the Azure Sky^PE^125^You have learned to calculate the precise location of Syrania at any given time, and to use that knowledge to enhance spells you cast to grant flight.
+Mastery of the Battleground^PE^125^You have learned to calculate the precise location of Shavarath at any given time, and to use that knowledge to enhance spells of battle that you cast.
+Mastery of Chaos and Order^PE^125^You have learned to calculate the precise locations of Daanvi and Kythri at any given time, and to use that knowledge to imbue your spells with unusual regularity or strinking unpredictability -- or both.
+Mastery of Day and Night^PE^125^You have learned to calculate the precise locations of Irian and Mabar at any given time, and to use that knowledge to enhance your manipulation of positive and negative energy.
+Mastery of the Dead^PE^125^You have learned to calculate the precise location of Dolurrh at any given time, and to use that knowledge to capture the souls of creatures slain with your death spells.
+Mastery of Dreams^PE^125^By physically exploring the realm of Dal Quor, you have learned to instill your spells with the stuff of dreams . . . and nightmares.
+Mastery of Faerie Enchantment^PE^125^You have learned to calculate the precise location of Thelanis at any given time, and to use that knowledge to improve your ability to control the minds of other creatures.
+Mastery of Ice and Fire^PE^126^You have learned to caclulate the precies locations of Fernia and Risia at any given time, and to use that knowledge to enhance cold and fire spells that you use.
+Mastery of Madness^PE^126^You have learned to reach magically to the ever-distant plane of Xoriat and draw some element of its madness into the world -- but these techniques come with some risk.
+Mastery of the Mists^PE^126^By learning of the intricate relationship between the Ethereal Plane and the Material Plane, you gain the ability to see and sometimes reach through the barrier between these two planes.
+Mastery of the Silver Void^PE^126^You have gained a deeper understanding of the Astral Plane and its relationship to the other planes of the cosmos. You can use that knowledge to more quickly access that plane.
+Mastery of Twilight Denizens^PE^126^You have learned to calculate the precise location of Lamannia at any given time, and to use that knowledge to summon more powerful creatures from that plane.
+Mastery of Twisted Shadow^PE^126^You gain the ability to reach into the Plane of Shadow when casting an illusion, concealing yourself in the raw shadowstuff drawn forth.
+Shifter Acrobatics^PE^135^Your heritage makes you agile and light-footed.
+Shifter Magnetism^PE^135^Your heritage gives you a strong animal presence.
+Shifter Stealth^PE^135^You can call upon your bestial heritage to increase your stealth.
+Bladebearer of the Valenar^PE^141^You have trained extensively with scimitars, including the Valenar double scimitar. You are adept at striking from horseback with the curved blades of the Valenar.
+Shield of Blades^PE^141^As a master of the double scimitar, you can weave a web of steel to protect yourself from attack.
+Spirit of the Stallion^PE^141^Your patron ancestor was a legendary cavalry soldier, and her spirit guides you and your mount.
+Valenar Trample^PE^141^You are trained in Valenar cavalry techniques emphasizing trampling your opponents into the ground.
+Shocking Fist^PE^151^Your slam attack can deal a shock.
+Overload Metabolism^PE^151^You can heal damage at a cost to your other physical attributes.
+Heretic of the Faith^PF^46^You stray significantly from the teachings of your faith.
+Prophet of the Divine^PF^49^Your communications with the divine manifest in a public fashion.
+Bane of Infidels^PF^53^In a church locked in eternal conflict with followers of another faith, you have learned to fight effectively against the infidels. You know their ways and how to beat them.
+Initiate of Amaunator^PF^58^You have been initiated into the greatest secrets of Amaunator's faith.
+Rulership^PF^158^You are a ruler of an economic, frontier, governmental, military, religious, transport, or other community.
+Accelerate Metamagic^RDr^98^You can apply a selected metamagic feat to your spells more quickly than normal.
+Dragon Breath^RDr^98^You can use your breath weapon as often as a normal dragon.
+Dragon Tail^RDr^98^Your draconic ancestry manifests as a muscular tail you can use in combat.
+Dragon Trainer^RDr^98^Your draconic nature gives you special insight into training dragons and draconic creatures.
+Dragon Wings^RDr^100^Your draconic ancestry manifests as a pair of wings that aid your jumps and allow you to glide.
+Dragonwrought^RDr^100^You were born a dragonwrought kobold, proof of your race's innate connection to dragons.
+Extraordinary Trapsmith^RDr^100^You are an expert at constructing mechanical traps.
+Heavyweight Wings^RDr^100^Your superior strength allows you to fly while heavily burdened.
+Improved Dragon Wings^RDr^100^Your draconic wings now grant you flight.
+Kobold Endurance^RDr^101^Thanks to your race's determination, you are capable of amazing feats of strength and stamina.
+Kobold Foe Strike^RDr^101^You are more effective in combat against your racial enemies.
+Practical Metamagic^RDr^101^You can apply a selected metamagic feat to your spells more easily.
+Reinforced Wings^RDr^101^You have strengthened the muscles of your wings.
+Versatile Spellcaster^RDr^101^You can use two lower-level spell slots to cast a spell one level higher.
+Wyrmgrafter^RDr^101^You can apply draconic grafts to other living creatures or to yourself.
+Entangling Exhalation^RDr^101^You can use your breath weapon to create an entangling mesh of energy.
+Exhaled Barrier^RDr^101^You can use your breath weapon to create a wall of energy.
+Exhaled Immunity^RDr^102^You can use your breath weapon to grant a willing creature immunity to energy.
+Extra Exhalation^RDr^102^You can use your breath weapon one more time per day than normal.
+Furious Inhalation^RDr^102^While raging, you can use your breath weapon to deal energy damage with your bite attacks.
+Draconic Arcane Grace^RDr^102^You can convert some of your arcane spell energy into a saving throw bonus.
+Draconic Breath^RDr^102^You can convert some of your arcane spell energy into a breath weapon.
+Draconic Claw^RDr^102^You develop natural weapons like those of your draconic ancestors.
+Draconic Flight^RDr^102^The secret of draconic flight has been revealed to you, granting you the ability to fly occasionally.
+Draconic Heritage^RDr^102^You have a greater connection with your draconic bloodline than others of your kind.
+Draconic Legacy^RDr^104^You have realized greater arcane power through your draconic heritage.
+Draconic Persuasion^RDr^104^Your arcane talents lend you a great deal of allure.
+Draconic Power^RDr^104^You have greater power when manipulating the energies of your heritage.
+Draconic Presence^RDr^104^When you use your magic, your mere presence can terrify those around you.
+Draconic Resistance^RDr^105^Your bloodline hardens your body against effects related to the nature of your progenitor.
+Draconic Skin^RDr^105^Your skin takes on a sheen, luster, and hardness related to your draconic ancestor.
+Draconic Toughness^RDr^105^Your draconic nature reinforces your body as you embrace your heritage.
+Spell Rehearsal^RDr^105^Casting the same spell several times in a row or at the same target enables you to perfect it.
+Wing Expert^RDr^105^You can use your wings to create a variety of effects.
+Divine Vigor^RH^126^You can channel energy to increase your speed and durability.
+Dragonthrall^RH^126^You have pledged your life to the service of evil dragonkind.
+Bind Vestige^TM^72^You know how to make pacts with otherworldly spirits called vestiges.
+Bind Vestige, Improved^TM^73^You can bind a wider range of vestiges.
+Defense against the Supernatural^TM^73^Your in-depth knowledge of supernatural forces grants you greater ability to resist their effects.
+Empower Supernatural Ability^TM^73^You can use a supernatural ability with greater effect than normal.
+Enlarge Supenatural Ability^TM^73^You can increase the range of a supernatural attack.
+Expel Vestige^TM^73^You can expel a vestige to which you are bound before the duration of its pact with you has expired.
+Extend Supernatural Ability^TM^73^You can cause a supernatural ability with a duration to last longer than normal.
+Favored Vestige^TM^74^Choose one vestige to which you have access. You establish a close, mystical affinity with that spirit.
+Favored Vestige Focus^TM^74^The supernatural abilities of your favored vestige are more potent than normal.
+Ignore Special Requirements^TM^74^The strange constraints that vestiges place on their summoning are meaningless to you.
+Improved Binding^TM^74^You are so adept at binding vestiges that you can contact powerful ones more easily than other soul binders can.
+Practiced Binder^TM^74^When you bind a vestige, you gain an additional power associated with it.
+Rapid Pact Making^TM^74^Your skill with pact magic lets you bind a vestige extremely quickly, even in the heat of combat.
+Rapid Recovery^TM^74^You can use the abilities of your favored vestige more frequently.
+Skilled Pact Making^TM^74^Your strong will serves you well when making pacts with vestiges.
+Sudden Ability Focus^TM^74^One of your special attacks becomes more potent than usual.
+Supernatural Crusader^TM^75^You are adept at fighting supernatural creatures.
+Supernatural Opportunist^TM^75^You are adept at exploiting a creature's momentary distraction while it activates its supernatural abilities.
+Widen Supernatural Ability^TM^75^You can increase the area of your supernatural abilities.
+Empower Mystery^TM^136^You can cast mysteries to greater effect.
+Enlarge Mystery^TM^136^You can cast mysteries farther than normal.
+Extend Mystery^TM^136^You can cast mysteries that last longer than normal.
+Favored Mystery^TM^136^The mystery you choose becomes easier to cast.
+Greater Path Focus^TM^136^Choose a path of shadow magic to which you have already applied the Path Focus feat. Your mysteries of that path are now even more potent.
+Line of Shadow^TM^136^You can cast a mystery without line of sight or line of effect to the target.
+Maximize Mystery^TM^136^You can cast mysteries to maximum effect.
+Nocturnal Caster^TM^137^You are empowered by darkness, making your abilities stronger at night.
+Path Focus^TM^137^Choose a path of shadow magic, such as Touch of Twilight. Your mysteries of that path are more potent than normal.
+Quicken Mystery^TM^137^You can cast a mystery with a moment's thought.
+Reach Mystery^TM^137^You can cast touch-range mysteries without touching the target.
+Shadow Cast^TM^137^Your shadow shimmers as you cast a spell and you seem to cast your mysteries from elsewhere.
+Shadow Familiar^TM^138^Noctumancers developed this feat in order to gain a mystical companion.
+Shadow Reflection^TM^138^Your shadow flickers and moves in an aggressive, independent manner, enabling you to avoid some attacks of opportunity.
+Shadow Vision^TM^138^Your senses grow so attuned with shadow that you gain a limited ability to see in natural and magical darkness.
+Still Mystery^TM^138^You can cast mysteries without gestures.
+Unseen Arrow^TM^138^Developed by shadowblades, this feat allows a member of that class to apply his unseen weapon abilities to thrown or projectile weapons.
+Empower Utterance^TM^228^Your utterances have more powerful effects.
+Enlarge Utterance^TM^229^You can project the power of an utterance to a greater distance.
+Extend Utterance^TM^229^Your utterances have a more lasting effect on the universe.
+Focused Lexicon^TM^229^Your utterances have greater effect against a certain type of creature.
+Minor Utterance of the Evolving Mind^TM^229^Your mastery of Truespeech has led you to the understanding necessary to perform a simple utterance from the Lexicon of the Evolving Mind.
+Obscure Personal Truename^TM^229^Truenames are notoriously difficult to pronounce, but yours is harder than most.
+Personal Truename Backlash^TM^229^Your personal truename is so charged with magic power that those who fail to speak it properly are warped by reality run amok.
+Truename Rebuttal^TM^229^You are particularly good at negating other truenamers' power with well-chosen truenames.
+Truename Research^TM^229^You have a knack for uncovering the personal truenames of friends and foes alike through study and investigation.
+Truename Training^TM^229^Unlike most of your peers, you have discovered the secret power of truenames.
+Utterance of the Evolving Mind^TM^230^Your further mastery of Truespeech allows you to wield its power more effectively against creatures.
+Utterance of the Crafted Tool^TM^230^As you strive for ever more mastery of Truespeech, you gain more power over the universe around you. You can now use the power of Truespeech to affect objects.
+Utterance of the Perfected Map^TM^230^The power of the Truespeech can alter the state of reality itself. Reaching toward this great power, you have mastered an utterance from the Lexicon of the Perfected Map.
+Utterance Focus^TM^230^You have a particular utterance you favor above others, and your enemies are less able to resist the power of your words.
+Quicken Utterance^TM^231^You can speak an utterance with just a moment's thought.
+Recitation of the Fortified State^TM^231^This recitation allows you to stand unyielding against the blows of your enemies.
+Recitation of the Meditative State^TM^232^This recitation gives you an unparalleled sense of serene calm.
+Recitation of Mindful State^TM^232^This recitation narrows and focuses your perception so you can concentrate on a delicate task at hand.
+Recitation of the Sanguine State^TM^232^This recitation purges all poisons from your body.
+Recitation of Vital State^TM^232^This recitation frees your body of disease and sickness.
+Dazzling Energy^CP^49^Your facility with energy is such that enemies are shaken by your prowess.
+Deep Vision^CP^49^Your mental focus helps you see farther with darkvision
+Dire Flail Mind Blade^CP^49^When you reshape your mind blade, you can change it into an exotic weapon: a dire flail.
+Dire Stun^CP^49^When you choose to stun your foe with your lurk augment ability, your foe might be stunned for a long time.
+Don  Mantle^CP^49^You gain the granted ability of a mantle you have tapped.
+Dromite Barrier^CP^49^You can convert uses of yourenergy ray psi-like ability into walls of energy.
+Dromite Ray^CP^49^You an use yourenergy ray psi-like ability more often.
+Duergar Expansion^CP^49^You can use yourexpansion psi-like ability more often.
+Duergar Invisibility^CP^49^You can use yourinvisibility psi-like ability more often.
+Dwarven Urgrosh Mind Blade^CP^49^When you reshape your mind blade, you can change it into an exotic weapon: a dwarven urgrosh.
+Ectopic Form^CP^50^This feat allows you to create astral constructs with distinct appearances and specialties.
+Elan Repletion^CP^52^As an elan, you can sustain yourself with repletion longer than other members of your race.
+Elan Resilience^CP^52^As an elan, you can prevent greater amounts of damage than other members of your race.
+Elan Resistance, Enhanced^CP^52^As an elan, you can resist harmful effects more readily than other members of your race.
+Elan Retainment^CP^52^You can use your psionic metabolism to aid your ability to retain your psionic focus when you would otherwise expend it.
+Elemental Envoy^CP^52^This feat allows you to acquire an elemental steward.
+Energize Armor^CP^53^You can charge your armor with psionic energy, making it resistant to energy damage.
+Enervation Endurance^CP^53^When facing the aftermath of a wild surge, enervation doesn't sap your power points.
+Enhanced Beneficence^CP^53^Your psychic aura is larger than normal, reflecting your devotion to your deity.
+Envoy Cognizance^CP^53^When your elemental envoy is nearby, its associated energy enhances your ability to manifest energy powers.
+Euphoric Reduction^CP^53^Channel your euphoric surge into a boost for one of your skills.
+Extra Aura^CP^54^You gain the aura ability of a mantle you have donned.
+Focused Perception^CP^54^When you concentrate your faculties, your power of sight pierces the darkness.
+Focused Shield^CP^54^Your mental focus makes you more adept at using your shield.
+Focused Skill User^CP^54^You can take advantage of your psionic focus in new ways.
+Gestalt Anchor^CP^54^You have a strong bond to the psionic entity you host.
+Githyanki Charm^CP^54^You can leverage yourpsionic daze psi-like ability to gain greater control over subjects.
+Githyanki Control^CP^54^You can leverage yourfar hand psi-like ability to gain greater control over objects.
+Githyanki Dismissal^CP^54^You can leverage yourdimension door psi-like ability to gain greater control over other creatures' locations.
+Githyanki Ectoform^CP^54^You can leverage yourconcealing amorpha psi-like ability to gain greater control over your own body.
+Githzerai Burst^CP^54^You can leverage yourcat fall psi-like ability to gain greater control over yourself in your environment.
+Githzerai Feedback^CP^54^You can leverage yourinertial armor psi-like ability to further insulate yourself from harm.
+Githzerai Knock^CP^54^You can leverage yourconcussion blast psi-like ability to gain such fine control over manipulating force that you can open locks or sealed doors.
+Githzerai Link^CP^55^You can leverage yourpsionic daze psi-like ability to forge direct mental contact with another creature.
+Half-Giant Stomp^CP^55^You can use yourstomp psi-like ability more often.
+Half-Giant Thunderer^CP^55^You can use yourstomp psi-like ability to far greater effect.
+Instinctive Consummator^CP^55^You always make good on your threats.
+Invest Armor^CP^55^You can charge your armor with additional protective qualities.
+Lurk Augment, Extra^CP^55^You can use your lurk augment more often than normal.
+Lurk Augment, Ranged^CP^55^You can use some of your lurk augments in conjunction with a ranged attack.
+Lurk Master^CP^55^You are more skilled in augmenting your attack than your training would indicate.
+Maenad Fury^CP^55^You can use your outburst racial trait more often.
+Maenad Scream^CP^55^You can use yourenergy ray (sonic) psi-like ability more often.
+Maenad Deafening Scream^CP^55^You can use yourenergy ray (sonic) psi-like ability to better effect.
+Mantle Focus^CP^55^The powers from one of your mantles become more potent.
+Mental Juggernaut^CP^56^You are adroit at avoiding the mind blasting effects of certain psionic abilities and powers.
+Mind Cleave^CP^56^When you lay low a foe, you drain off a portion of its excess mental energy into the conduit of your mind blade.
+Mind Empowerment^CP^56^When you lay low a foe, you drain off a portion of its excess mental energy into the conduit of your mind blade.
+Mind Strike^CP^56^When you use your psychic strike ability, you deal more damage.
+Mind Strike, Swift^CP^57^You possess a deadly speed when charging your mind blade with psychic energy.
+Orc Double Axe Mind Blade^CP^57^When you reshape your mind blade, you can change it into an exotic weapon: an orc double axe.
+Postpone Enervation^CP^57^You can postpone the onset of your psychic enervation.
+Practiced Manifester^CP^57^Choose a manifesting class that you possess. The powers you manifest from that class are more powerful.
+Privileged Energy^CP^57^You favor one specific energy type over all others.
+Psymbiot^CP^57^You gain benefits when you are near other psionic characters or creatures.
+Skin of the Construct^CP^57^You can wear an astral construct as if it were a second skin.
+Stygian Archon^CP^57^You sear the synapses of your mind with a scar of void and emptiness.
+Synad Multitask, Enhanced^CP^58^As a synad, your threefold mind grants you an additional opportunity to multitask.
+Tap Mantle^CP^58^You gain the ability to access the powers in a new mantle.
+Thri-Kreen Carapace^CP^58^Your carapace is harder than average.
+Thri-Kreen Claw^CP^58^You can use yourmetaphysical claw psi-like ability more often.
+Thri-Kreen Displacement^CP^58^You can use yourpsionic displacement psi-like ability more often.
+Thri-Kreen Poison^CP^58^You can use your poison bite more often.
+Two-Bladed Mind Blade^CP^58^When you reshape your mind blade, you can change it into an exotic weapon: a two-bladed sword.
+Volatile Escalation^CP^58^When you are attacked with a telepathic power, your innate wildness forces a higher mental price on your attacker.
+Volatile Leech^CP^58^You gain the power points your attacker wastes attacking you with a telepathic power.
+Xeph Burst, Extra^CP^59^You can use your burst racial trait more often.
+Xeph Celerity^CP^59^You can use your burst racial trait to gain an extra attack.
+Dorje Mastery^CP^59^Psionic dorjes are more potent in your hands.
+Dual Dorje^CP^59^You can fight with two dorjes at the same time.
+Hostile Mind, Improved^CP^59^You have mental defenses erected  against telepathic attacks.
+Psionic Mastery^CP^59^You are quick and certain in your efforts to defeat the psionic defenses and powers of others.
+Aggressive Mind^CP^60^The psionic entity you host gives you access to psi-like abilities capable of disrupting the mind of your enemy.
+Antagonist^CP^60^The psionic entity you host seeks to cause damage and mayhem, and you have powers to further that end.
+Defensive Shell^CP^60^The psionic entity living in your mind enables you to better resist attacks.
+Host Focus^CP^60^You can use a psi-like ability granted by a host feat an extra time each day.
+Pacifist^CP^60^You host a psionic entity that dislikes combat and provides you psi-like abilities to help you avoid a fight.
+Spiritual Force^CP^60^Your mind blade is an expression of your inner spirit.
+Strength of Two^CP^60^As the host of a formless psionic entity, you possess immense willpower.
+Telepathic Affinity^CP^60^The entity you host gives you the ability to better communicate with other creatures.
+Illithid Blast^CP^61^You can convert your pisonic energy into amind blast.
+Illithid Compulsion^CP^61^You can call upon your heritage and enhance your ability to manipulate the minds of other creatures.
+Illithid Enthusiast^CP^61^When you manipulate the minds of other creatures, you are heartened and emboldened by your success.
+Illithid Extraction^CP^61^Your acceptance of your illithid heritage is so encompassing that you have learned how to extract the brain of a helpless victim.
+Illithid Grapple^CP^61^You embrace more of your illithid heritage, and grow at least one long purplish tentacle that you can reveal and unfurl when you open your mouth.
+Illithid Heritage^CP^62^Somewhere in the deeps of time, your bloodline was polluted with illithid influence.
+Illithid Legacy^CP^62^You have realized greater psionic power through your illithid heritage.
+Illithid Legacy, Greater^CP^62^Your knowledge of psionic power has grown even further due to your illithid heritage.
+Illithid Skin^CP^62^Your skin takes on the glistening, rubbery, green-mauve consistency of your illithid parentage.
+Knockdown Power^CP^62^You can manifest powers that knock creatures off their feet.
+Linked Power^CP^62^You can link a power to the power you manifest in this round so that it goes off next round.
+Metapower^CP^63^You can permanently modify a psionic power you know with a metapsionic feat.
+Paraelemental Power^CP^63^When using a power that allows you to choose a type of energy, you have a wider range of possible choices owing to your ability to mix energy with matter.
+Phrenic Leech^CP^63^Psionic foes damaged by your power are also mentally drained.
+Stygian Power^CP^64^Psionic powers you manifest that utilize negative energy are branded with an imprint of fear.
+Transdimensional Power^CP^64^You can manifest powers that affect targets lurking in coexistent planes and extradimensional spaces whose entrances fall within the power's area.
+Acrobatic Strike^PH2^71^Your dexterous maneuvers and skilled acrobatics allow you to slip past a foe's defenses and deliver an accurate strike against him.
+Active Shield Defense^PH2^71^Your expert use of your shield allows you to strike at vulnerable foes even when you forgo your own attacks in favor of defense.
+Adaptable Flanker^PH2^71^When you and an ally team up against a foe, you know how to maximize the threat your ally poses to ruin your target's defenses.
+Agile Shield Fighter^PH2^74^You are skilled in combining your shield bash attack with an armed strike. When you use your shield in unison with a weapon, your training allows you to score telling blows with both.
+Arcane Accompaniment^PH2^74^You infuse your performance with magical energy, allowing its effects to continue even as you attend to other tasks.
+Arcane Consumption^PH2^74^You can sacrifice your physical health to strengthen a spell. This process leaves you wracked with pain, but the enhanced energy you draw from the spell might provide the margin between victory and defeat.
+Arcane Flourish^PH2^74^You use your magical abilities to improve your performance talents.
+Arcane Thesis^PH2^74^You have studied a single spell in-depth.
+Arcane Toughness^PH2^75^You draw upon the power of your magic to sustain yourself, allowing you to continue fighting long after your physical body has failed you.
+Armor Specialization^PH2^75^Through long wear and hours of combat, you have trained your body to believe in its armor.
+Battle Dancer^PH2^75^You strike at your foes in time with the music you sing or in cadence with an oration you deliver.
+Bonded Familiar^PH2^75^You enjoy a stronger than normal magical bond with your familiar, granting you access to two special abilities.
+Bounding Assault^PH2^75^You can move and attack with superior speed and power.
+Brutal Strike^PH2^76^You can batter foes senseless with your mace, morningstar, quarterstaff, or flail.
+Combat Acrobat^PH2^76^Your acrobatics and agility in combat allow you to maneuver across the battlefield with ease.
+Combat Familiar^PH2^76^Your familiar is skilled in delivering attack spells againstyour foes.
+Combat Tactician^PH2^77^You excel at approaching an opponent from an unexpected direction to deliver deadly attacks.
+Cometary Collision^PH2^77^You are a thunderbolt of destruction on the battlefield.
+Companion Spellbond^PH2^77^You form a special magical link with your animal companion, allowing you to share spells with it over a greater distance.
+Crossbow Sniper^PH2^77^You are skilled in lining up accurate, deadly shots with your crossbow.
+Crushing Strike^PH2^78^You wield a bludgeoning weapon with superior power, allowing you to batter aside an opponent's defenses.
+Cunning Evasion^PH2^78^When an area attack detonates around you, you use the chaos and flash of energy to duck out of sight.
+Dampen Spell^PH2^78^From the lowliest prestidigitator to the most august hierophant, spellcasters both arcane and divine recognize the power of counterspelling.
+Deadeye Shot^PH2^78^You carefully line up a ranged attack, timing it precisely so that you hit your opponent when his guard is down.
+Defensive Sweep^PH2^78^You sweep your weapon through the area you threaten, warding away opponents and forcing them to move away or suffer a fearsome blow.
+Driving Attack^PH2^78^When you strike an opponent with a piercing weapon, the brutal impact of your strike sends him sprawling.
+Elven Spell Lore^PH2^78^You have studied the mighty arcane traditions of the elven, granting you insight into the intricate workings of magic and the theoretical structures behind spells.
+Fade into Violence^PH2^79^While the chaos of battle swirls around you, you rely on your ability to slip into the background to avoid your enemy's notice.
+Fiery Fist^PH2^79^By channeling your kienergy, you sheathe your limbs in magical fire.
+Fiery KiDefense^PH2^79^You channel your kienergy into a cloak of flame that injures any who attempt to strike you.
+Flay^PH2^79^When fighting unarmored opponents, you excel at twisting your weapon just before impact.
+Grenadier^PH2^79^You are skilled in using grenadelike weapons.
+Hindering Opportunist^PH2^79^When you have a chance to strike a distracted foe, you instead use that opportunity to aid or protect an ally against him.
+Intimidating Strike^PH2^79^You make a display of your combat prowess designed to strike terror in your foe.
+Indomitable Soul^PH2^80^Your physical toughness translates into greater mental resiliency.
+Keen-Eared Scout^PH2^80^Your sharp sense of hearing allows you to determine much more about your surroundings.
+KiBlast^PH2^80^You focus your kiinto a ball of energy that you can hurl at an opponent.
+Leap of the Heavens^PH2^80^Your excellent athletic ability and superior conditioning allow you to make near-superhuman leaps.
+Lunging Strike^PH2^80^You make a single attack against a foe who stands just beyond your reach.
+Lurking Familiar^PH2^80^Your familiar hides within the folds of your robe or takes cover behind you as your opponents close in.
+Mad Foam Rager^PH2^80^You fight with the rage that only a rabid badger or a beer-addled dwarf can bring to bear.
+Master Manipulator^PH2^80^Your words are your weapons.
+Melee Evasion^PH2^81^Your speed, agility, and talent for intelligent fighting allow you to avoid your opponent's blows.
+Melee Weapon Mastery^PH2^81^You have mastered a wide range of weapons.
+Overwhelming Assault^PH2^81^If you attack a foe who does nothing to turn aside your attack, you press forward with an indomitable strike.
+Penetrating Shot^PH2^81^You send a powerful shot cleaving through your enemies.
+Ranged Weapon Mastery^PH2^82^You have mastered a wide range of weapons.
+Rapid Blitz^PH2^82^You charge across the battlefield, combining your speed and fighting ability to move and attack with unmatched skill.
+Robilar's Gambit^PH2^82^By offering Robilar's Gambit, you absorb damage to place yourself in an advantageous position.
+Shield Sling^PH2^82^You can hurl your shield as a deadly missile, turning it from a defensive item to a crushing, thrown weapon.
+Shield Specialization^PH2^82^You are skilled in using a shield, allowing you to gain greater defensive benefits from it.
+Shield Ward^PH2^82^You use your shield like a wall of steel and wood.
+Short Haft^PH2^82^You have trained in polearm fighting alongside your comrades in arms, sometimes reaching past them while they shield you, and sometimes shielding them while they attack from behind you.
+Slashing Flurry^PH2^82^You swing your weapon with uncanny speed, slicing apart a foe in the blink of an eye.
+Spectral Skirmisher^PH2^83^You have trained extensively in the use of magic that renders you invisible.
+Spell-Linked Familiar^PH2^83^You and your familiar can share spell energy, allowing your familir to cast a limited number of spells each day.
+Stalwart Defense^PH2^83^You excel at aiding your allies in battle. When an opponent attempts to strike one of them, you make a quick, distracting motion to ruin the foe's efforts.
+Steadfast Determination^PH2^83^Your physical durability allows you to shrug off attacks that would cripple a lesser person.
+Telling Blow^PH2^83^When you strike an opponent's vital areas, you draw on your ability to land crippling blows to make the most of your attack.
+Trophy Collector^PH2^83^A belt of minotaur fur, a hood of cloaker wing-skin, and an amulet fashioned from a petrified dragon's eye -- these are the intimidating symbols of your trade.
+Tumbling Feint^PH2^84^When you move near an opponent, your acrobatic maneuvers leave him confused and unable to properly defend himself.
+Two-Weapon Pounce^PH2^84^When you charge an opponent while wielding two weapons, you can make two quick attacks.
+Two-Weapon Rend^PH2^84^You wield two weapons with an artisan's precision.
+Vatic Gaze^PH2^85^Your arcane studies have brought forth your nascent talent to sense magical auras and the power that others are capable of wielding.
+Versatile Unarmed Strike^PH2^85^You employ a variety of unarmed fighting styles, allowing you to alter the type of damage your attacks deal.
+Vexing Flanker^PH2^85^You excel at picking apart an opponent's defenses when your allies also threaten them.
+Wanderer's Diplomacy^PH2^85^Many halflings journey far and wide across the world, spending no more than a few months in one place.
+Water Splitting Stone^PH2^85^You channel your kienergy to splinter the defenses of creatures whose tough hides or magical natures normally allow them to shrug off your blows.
+Weapon Supremacy^PH2^85^You are a grandmaster in the use of your chosen weapon.
+Ritual Blessing^PH2^86^You call upon the powers of goodness and light to bless your allies.
+Ritual Blood Bonds^PH2^86^You invest your allies with the mighty power of your toten, god, or similar divine entity.
+Combat Awareness^PH2^86^When you maintain your combat focus, you have an uncanny ability to sense the ebb and flow of your opponents' vitality.
+Combat Defense^PH2^87^The state of keen focus and mental discipline you attain in combat allows you to shift the focus of yoru defense from one opponent to another with careful, precise maneuvers.
+Combat Focus^PH2^87^The way of the warrior requires more than simple, brute strength.
+Combat Stability^PH2^87^When you maintain your combat focus, you become difficult to dislodge.
+Combat Strike^PH2^87^Your intense, focused state allows you to see the one critical moment in a battle when you hang suspended between victory and defeat.
+Combat Vigor^PH2^88^When you maintain your combat focus, your clarity of purpose and relentless drive allow you to overcome your body's frailties.
+Divine Armor^PH2^88^You call upon your deity to protect you in your hour of need by wreathing you in divine power that wards off your enemies' attacks.
+Divine Fortune^PH2^88^With a quick prayer, you channel divine energy to help resist a spell, poison, or other deadly effect.
+Divine Justice^PH2^88^You can channel divine energy to turn your foe's strength against him, striking him with the same force that he used against you.
+Divine Ward^PH2^88^You create a channel of divine energy between yourself and a willing ally.
+Profane Aura^PH2^89^You call upon the dark powers you worship to fill the area around you with a dreadful mist that obscures sight.
+Sacred Healing^PH2^89^You can channel divine energy to aid in your efforts to tend to a comrade's injuries, sickness, or other conditions.
+Sacred Purification^PH2^89^You serve as a conduit of divine energy, filling the area around you with power that aids the living and saps the undead.
+Sacred Radiance^PH2^89^You channel divine energy to fill the area around you with a soothing, gentle radiance.
+Celestial Sorcerer Aura^PH2^90^The power of your sorcerous heritage shines through, allowing you to infuse the area around you with a menacing aura.
+Celestial Sorcerer Heritage^PH2^90^Your ancestry manifests in the form of several special abilities.
+Celestial Sorcerer Lance^PH2^90^You can channel your arcane energy into a bolt of power that is baneful to evil creatures.
+Celestial Sorcerer Lore^PH2^90^The power of your ancestry grants you access to a variety of new spells.
+Celestial Sorcerer Wings^PH2^91^You channel your inborn magical abilities to spawn a pair of spectral, magical wings that glow with majestic power.
+Infernal Sorcerer Eyes^PH2^91^Your eyes glow with infernal fire, allowing you to see through magical darkness.
+Infernal Sorcerer Heritage^PH2^91^Your innate magic derives from infernal ancestors.
+Infernal Sorcerer Howl^PH2^91^You channel the fury of your infernal ancesters into a thunderous roar that blasts your enemies with sonic power.
+Infernal Sorcerer Resistance^PH2^91^You are as tough and resilient as an infernal monstrosity, allowing you to shrug off acid and cold damage.
+Blistering Spell^PH2^91^Your fire spells sear the flesh from your enemies' bones, leaving them wracked with pain.
+Earthbound Spell^PH2^91^You bind a spell into the rock and soil, leaving it there until an opponent stumbles across it.
+Flash Frost Spell^PH2^91^Your spells that use cold and ice to damage your foes leave behind a thin layer of slippery frost.
+Imbued Summoning^PH2^92^Your summoning spells gain an element of surprise.
+Smiting Spell^PH2^92^You can channel the energy of a touch spell into a weapon, causing the spell to discharge when you strike an opponent.
+Blood-Spiked Charger^PH2^92^You throw yourself into the fray, using your spiked armor and spiked shield to tear your opponents to pieces.
+Combat Cloak Expert^PH2^93^You are adept at turning your cloak into a vital part of your combat repertoire.
+Combat Panache^PH2^93^Your glowing personality and sharp performance abilities allow you to navigate the battlefield on sheer chutzpah alone.
+Einhander^PH2^94^You excel at wielding a one-handed weapon while carrying nothing in your off hand.
+Mad Alchemist^PH2^94^You are an expert at using alchemical items.
+Shadow Striker^PH2^94^You melt into the shadows, hiding from your enemies until the time is right.
+Abyss-Bound Soul [Vile]^FCI^83^You have pledged your immortal soul to a particular demon lord in return for a gift that aids your evil works in life.
+Blood War Conscript [Vile]^FCI^83^Your evil brand indicates your rank in the armies of the Blood War and infuses you with fury.
+Chaotic Spell Recall [Abyssal Heritor]^FCI^84^A few choice spells never stray far from your mind.
+Claws of the Beast [Abyssal Heritor]^FCI^84^Your hands are twisted like claws. This deformity allows you to deal more damage than usual with your unarmed strikes and sneak attacks.
+Cloak of the Obyrith [Abyssal Heritor]^FCI^85^The chaos of the Abyss suffuses your being, as it does the ancient obyriths.
+Dark Speech [Vile]^FCI^85^You learn a smattering of the language of truly dark power.
+Demonic Conduit [Vile]^FCI^85^Your evil brand incorporates blasphemous runes and sigils that augment magical attacks you make against lawful and/or good targets.
+Demonic Skin [Abyssal Heritor]^FCI^85^Your skin has rough, scaly patches that enhance your natural armor.
+Demonic Sneak Attack [Abyssal Heritor]^FCI^85^You know exactly how to twist the blade to get the most out of your sneak attacks.
+Demon Mastery^FCI^85^You are particularly skilled at summoning demons and convincing them to serve you.
+Evil Brand [Vile]^FCI^85^You are physically marked forever as the servant of an evil power greater than yourself -- in this case, a demon lord. The symbols is unquestionable in its perversity, depicting a depravity so unthinkable that all who see it know beyond a doubt that you serve the lords of the Abyss.
+Extract Demonic Essence^FCI^86^You can draw upon the living essence of a willing or captured demon to fuel the creation of items or the casting of potent spells.
+Eyes of the Abyss [Abyssal Heritor]^FCI^86^Your eyes glow with an inner fire of some unusual color. This glow increases your perception and allows you to see in the dark.
+Heart of the Nabassu^FCI^86^Your ancestry traces back to a place where the Abyss meets the Negative Energy Plane.
+Keeper of Forbidden Lore [Abyssal Heritor]^FCI^86^A shred of demonic racial memory grants you knowledge of numerous ancient magical secrets.
+Ordered Chaos^FCI^86^You are an unusually lawful Abyssal heritor.
+Otherworldly Countenance [Abyssal Heritor]^FCI^87^You are either stunningly beautiful or wretchedly hideous. Either way, your appearance can be terribly unsettling to others upon whom you focus your attentions.
+Poison Healer^FCI^87^Poison isn't always bad for you.
+Poison Talons [Abyssal Heritor]^FCI^87^Your claws drip with poison.
+Precognitive Visions [Abyssal Heritor]^FCI^87^You periodically experience visions from the near future.
+Primordial Scion [Abyssal Heritor]^FCI^87^The Abyss beckons. . . .
+Thrall to Demon [Vile]^FCI^87^You formally become a supplicant to a demon lord. In return for your obedience, you gain a small measure of that demon lord's power.
+Vestigial Wings [Abyssal Heritor]^FCI^87^A pair of vestigial wings sprouts from your shoulders.
+Ability Focus^MM4^202^A particular special ability of a creature with this feat is more potent than normal.
+Awesome Blow^MM4^202^A creature with this feat can choose to deliver blows that send its smaller opponents flying like bowling pins.
+Clinging Breath^MM4^202^This feat enables a creature's breath weapon to cling to creatures and continue to affect them after it has breathed.
+Craft Construct [Item Creation]^MM4^202^A creature with this feat can create golems and other magic automatons that obey its orders.
+Flyby Attack^MM4^202^A creature with this feat can attack on the wing.
+Githyanki Battlecaster^MM4^202^A creature with this feat ignores arcane spell failure chances when wearing light armor.
+Githyanki Dragonrider [Racial]^MM4^202^A creature with this feat has a knack for getting along with red dragons.
+Improved Natural Attack^MM4^203^The natural attacks of a creature with this feat are more dangerous than its size and type would otherwise dictate.
+Improved Toughness^MM4^203^A creature with this feat is significantly tougher than normal.
+Lingering Breath^MM4^203^The breath weapon of a creature with this feat forms a lingering cloud.
+Multiattack^MM4^203^A creature with this feat is adept at using all its natural weapons at once.
+Powerful Charge^MM4^203^A creature with this feat can charge with extra force.
+Quicken Spell-Like Ability^MM4^203^A creature with this feat can employ a spell-like ability with a moment's thought.
+Blessed of Vulkoor [Racial]^SX^134^A scorpion-shaped birthmark denotes you as one of the chosen of Vulkoor.
+Drow Scorpion Warrior [Racial, Tactical]^SX^134^Your study of the ways of the scorpion grants you special tactics.
+Drow Skirmisher [Racial]^SX^134^Your experience with the guerrilla-style combat of the deep jungle grants you mastery of the weapons of the drow.
+Earthquake Stomp^SX^134^Your thunderous steps allow you to knock smaller enemies off their feet.
+Echoing Spell [Metamagic]^SX^134^Your spells return after you cast them, although with lessened effects.
+Elder Giant Magic^SX^135^You have learned a technique developed by ancient giant spellcasters, allowing you to channel additional power in your spells.
+Giant Banemagic^SX^135^You can cast spells that deal additional damage to giants.
+Jungle Veteran^SX^135^You have a knack for surviving in harsh environments and avoiding the deadly ambushes of natives.
+Mysterious Magic^SX^135^Your study of unconventional magic gives your spells an odd appearance and makes them difficult to identify.
+Rending Claws^SX^135^Your expertise with scorpion claw gauntlets allows you to tear apart your opponents with deadly precision.
+Breath of Unlife [Metabreath]^DrF^47^Your breath weapon contains the chill of undeath.
+Transdimensional Breath [Psionic]^DrF^50^Your breath weapon affects bordering planes.
+Follower of the Scaly Way^DrF^57^You are an adherent of Sammaster's teachings.
+Servant of a Dragon Ascendant^DrF^92^You formally supplicate yourself to an immortal dragon quasi-deity.
+Initiate of Tchazzar^DrF^92^You have been initiated into the greatest mysteries of Tchazzar's church.
+Adaptive Style^ToB^28^With just a short period of meditation, you can change your maneuvers and tactics to meet the threat you currently face.
+Avenging Strike^ToB^28^Your strength of will and strong sense of justice allow you to smite your foes.
+Blade Meditation^ToB^28^You have learned a meditation that grants you insight into the martial disciplines you have studied.
+Desert Fire^ToB^29^The power of the Desert Wind surges through you, and you find power in the motion of the hot winds and shifting sands that you can channel into your Desert Wind strikes.
+Desert Wind Dodge^ToB^29^Your training in the Desert Wind discipline allows you to dance across the battlefield like a blistering sirocco.
+Devoted Bulwark^ToB^29^Because of your staunch devotion to your cause and your Devoted Spirit training, you can stand your ground even in the face of an enemy's resounding attack.
+Divine Spirit [Divine]^ToB^29^The fervor and dedication of the Devoted Spirit discipline, combined with your fanatical adherence to a divine power, turns you into a font of spiritual energy.
+Evasive Reflexes^ToB^30^When an opponent gives you an opening in combat, you know exactly what to do: slip away.
+Extra Granted Maneuver^ToB^30^You are especially devout or insightful, and you have more control over which of your martial maneuvers are currently granted than other crusaders.
+Extra Readied Maneuver^ToB^30^You are an unusually perspicacious student of the Sublime Way, and you find it easy to keep a large number of maneuvers ready for use.
+Falling Sun Attack^ToB^31^The discipline of the Setting Sun teaches you how to turn an opponent's strengths into weaknesses.
+Instant Clarity [ Psionic]^ToB^31^You have sharpened your concentration to the point that you can focus your psionic abilities with just an instant's thought.
+Ironheart Aura^ToB^31^Your strength of spirit and martial training inspires those around you.
+Martial Stance^ToB^31^You have mastered the fundamentals of a martial discipline, and you are now able to master one of its stances.
+Martial Study^ToB^31^By studying the basics of a martial discipline, you learn to focus you kiand perfect the form needed to use a maneuver.
+Psychic Renewal [Psionic]^ToB^32^Your mental strength and psionic abilities allow you to focus your mind on combat and use your most devastating maneuvers more frequently.
+Rapid Assault^ToB^32^Your fighting style emphasizes taking foes down with quick, powerful blows.
+Scribe Martial Script [Item Creation]^ToB^32^You know the secret of creating martial scripts -- small slips of paper into which you infuse your own martial power and skill.
+Shadow Blade^ToB^32^In the course of your training in the Shadow Hand discipline, you learn to use your natural agility and speed to augment your attacks with certain weapons.
+Shadow Trickster^ToB^32^Your mastery of the Shadow Hand discipline lets you augment your illusion spells with the stuff of shadow.
+Song of the White Raven^ToB^32^The White Raven discipline shows you how to rouse dedication and fervor within your allies' hearts.
+Snap Kick^ToB^32^You have continued to hone your unarmed combat skills, and you deal more damage with your unarmed strikes.
+Stone Power^ToB^32^The principles of the Stone Dragon discipline teach you how to gather and focus your raw, physical strength into an attack.
+Sudden Recovery^ToB^33^You can instantly recover your focus, balance, and personal energy after using a martial maneuver.
+Superior Unarmed Strike^ToB^33^Your unarmed strikes have become increasingly deadly, enabling you to strike your foes in their most vulnerable areas.
+Tiger Blooded^ToB^33^The Tiger Claw discipline teaches students to mimic the rampant, feral qualities of a wild animal.
+Unnerving Calm^ToB^33^You know that the secret to defeating your enemies lies within the still center of your own mind.
+Vital Recovery^ToB^33^Preparing yourself to execute more of your maneuvers gives you the chance to catch a quick second wind and recover from damage you have sustained in the fight.
+White Raven Defense^ToB^33^The White Raven discipline has taught you to shine as a gleaming beacon of hope and endurance amid the chaos of battle.
+Clarion Commander^ToB^34^On the battlefield, you are a natural leader.
+Distant Horizon^ToB^34^An initiate of the Setting Sun sometimes learns a set of combat maneuvers to create the Distant Horizon fighting form.
+Faith Unswerving^ToB^34^The initiate of the Devoted Spirit knows that his fanaticism and devotion to a cause are enough to carry him through almost anything.
+Gloom Razor^ToB^35^The teachings of the Shadow Hand discipline allow you to confuse your enemies.
+Perfect Clarity of Mind and Body^ToB^35^Your mastery of the Diamond Mind discipline allows you to tap into reserves of spiritual and physical strength that other warriors cannot imagine using.
+Reaping Talons^ToB^35^When fighting with the Tiger Claw discipline's preferred weapons, you can use a variety of combat options that maximize the benefits of wielding two weapons.
+Scorching Sirocco^ToB^35^As a student of the Desert Wind, the burning fury of the desert sirocco is at your command.
+Shards of Granite^ToB^36^Like the great Stone Dragon, you hammer through your opponents' defenses using raw, brutal strength.
+Stormguard Warrior^ToB^36^The Stormguard Warrior feat encompasses a number of the more advanced tactics and techniques you would use as a student of the Iron Heart school.
+Armor of Scales [Ceremony]^DM^15^You imbue a target with the protection of a dragon's blade.
+Black Dragon Lineage [Draconic]^DM^15^You have attuned yourself to your black dragon ancestry and can poison foes with your touch.
+Blue Dragon Lineage [Draconic]^DM^15^You have learned to harness the power of your blue dragon ancestry and can hurl orbs of lightning.
+Brass Dragon Lineage [Draconic]^DM^16^You have unlocked the power of your brass dragon ancestry and can put foes to sleep with ease.
+Bronze Dragon Lineage [Draconic]^DM^16^You have tapped into your bronze dragon blood and can channel arcane energy to repel foes.
+Copper Dragon Lineage [Draconic]^DM^16^You have learned to channel the powers of your copper dragon ancestry to hinder your enemies' mobility.
+Double Draconic Aura^DM^16^You can project two draconic auras simultaneously.
+Draconic Armor [Draconic]^DM^16^You learn to block damage from successful attacks, lessening the blows with spell energy.
+Draconic Aura^DM^16^You can tap into the raw power of dragons to create a variety of potent auras around you.
+Draconic Heritage [Draconic]^DM^17^You have a greater connection with your draconic bloodline than others of your kind.
+Draconic Knowledge [Draconic]^DM^17^Your draconic blood lets you access ancient draconic knowledge.
+Draconic Senses [Draconic]^DM^17^Your draconic blood grants you great sensory powers.
+Draconic Vigor [Draconic]^DM^17^You gain some of the vitality of your draconic ancestry when casting spells.
+Dragonfire Assault [Draconic]^DM^17^You can augment your most powerful melee attacks with draconic power.
+Dragonfire Channeling [Draconic]^DM^17^You channel draconic fire through your holy symbol.
+Dragonfire Inspiration [Draconic]^DM^17^You can channel the power of your draconic ancestry into the attacks of your allies.
+Dragonfire Strike [Draconic]^DM^18^You can call upon your innate draconic powers to augment certain weapon attacks.
+Dragontouched [Draconic]^DM^18^You have a trace of draconic power, a result of dragons in your ancestry or a spiritual connection between you and the forces of dragonkind.
+Gold Dragon Lineage [Draconic]^DM^19^You can harness the legacy of your gold dragon ancestry to protect your allies.
+Heart of Dragons [Ceremony]^DM^19^You imbue your allies with draconic power.
+Initiate of Aasterinian [Initiate]^DM^20^You live for the moment, reveling in new experiences without fear of consequence.
+Initiate of Astilabor [Initiate]^DM^20^You share your deity's desire to acquire and protect treasure, and she has recognized this by granting you an edge in achieving these goals.
+Initiate of Bahamut [Initiate]^DM^20^The Platinum Dragon has entrusted you with great power in the battle against evil.
+Initiate of Falazure [Initiate]^DM^20^Your celebration of death and decay has opened up new magical secrets involving the living and undead.
+Initiate of Garyx [Initiate]^DM^20^You channel the cleansing fire of destruction, as wielded by your deity.
+Initiate of Hlal [Initiate]^DM^21^Fueled by faith in your deity, your audacity and bravery truly know no bounds.
+Initiate of Io [Initiate]^DM^21^Your deity has entrusted you with the responsibility of tending to dragonkind.
+Initiate of Lendys [Initiate]^DM^21^Your dedication to justice grants you the ability to ferret out and punish wrongdoers.
+Initiate of Tamara [Initiate]^DM^21^You wield the twin powers of mercy and death in service to your draconic patron.
+Initiate of Tiamat [Initiate]^DM^21^Your homage to the creator of evil dragonkind has been rewarded with physical and mental power.
+Red Dragon Lineage [Draconic]^DM^21^The fiery blood of red dragons runs within your veins, allowing you to produce flames from thin air.
+Silver Dragon Lineage [Draconic]^DM^22^You are the descendant of silver dragons and can harness your ancestors' power to paralyze your opponents.
+Slayer of Dragons [Ceremony]^DM^22^You protect your allies from the ravages they are sure to face while hunting dragons.
+White Dragon Lineage [Draconic]^DM^22^Your veins run with the savage blood of white dragons, allowing you to whyp yourself into a ragelike state.
+Words of Draconic Power [Ceremony]^DM^22^You tap into the great tradition of draconic magic to enhance the words of your allies.
+Action Healing^FE^145^You can spend an action point to enhance your healing power.
+Ancestral Whispers^FE^145^Through intense focus and divine energies, you can hear the advice of past ancestors.
+Ceremonial Empowerment^FE^145^Your divine might increases on your patron's holy days.
+Construct Grafter [Item Creation]^FE^145^You can apply construct grafts to other living creatures or to yourself.
+Divine Alacrity [Divine]^FE^145^You can channel divine energies into your own body, increasing your speed.
+Divine Countermagic [Divine]^FE^146^You channel divine energies to counter spells.
+Divine Warrior [Divine]^FE^146^Through divine power, you wield your deity's favored weapon to devastating effect.
+Domain Spontaneity [Divine]^FE^147^You are so familiar with one of your domains that you can convert other prepared spells into spells from that domain.
+Frantic Rage^FE^147^Your divine madness allows you to channel your fury into frenetic agility rather than might.
+Heroic Channeling [Divine]^FE^147^You can call on your personal strength of will to channel positive or negative energy into divine feats.
+Heroic Devotion [Divine]^FE^147^Your devotion to your faith allows you to manipulate fate at the expense of some spellcasting ability.
+Lucid Channeling^FE^147^When you invite a celestial into your body, you open your mind completely to the divine spirit.
+Nightbringer Initiate^FE^147^You have been trained in the ways of the Nightbringers, a new offshoot of the Children of Winter.
+Sacred Resilience^FE^147^You can channel divine energies to protect your allies from harm.
+Touch of Silver^FE^148^Your devotion to the Silver Flame allows you to burn the Church's foes with holy energies.
+Unquenchable Flame of Life^FE^148^You are hardened to the attacks of the undead.
+Unyielding Bond of Soul^FE^148^You are hardened to the attacks of the beings of other worlds.
+Worldly Focus^FE^148^Your belief in the omnipresence of the gods is so strong, you can channel your spells through the environment rather than a holy symbol.
+Wrest Possession^FE^148^If you resist control by a possessing fiend, you can attempt to seize control of its abilities.
+Enduring Life^Rav^200^You can ignore the effect of negative levels for a short time.
+Lasting Life^Rav^200^You can shed negative levels with an act of will.
+Favored in Guild^Rav^205^You are an active and valued member of your guild.
+Acidic Splatter^CM^37^You can channel magical energy into orbs of acid.
+Alacritous Cogitation^CM^37^You can leave a prepared spell slot open to spontaneously cast a spell.
+Aquatic Breath [Reserve]^CM^39^Your reservoir of magic allows you to breathe normally even underwater.
+Battlecaster Defense [Tactical]^CM^39^You have mastered techniques for taking full advantage of spells in melee while remaining unharmed.
+Battlecaster Offense [Tactical]^CM^40^You cunningly mix melee combat and spellcasting to increase the potency of both.
+Blade of Force [Reserve]^CM^40^You can surround a weapon with a short-lived aura of force.
+Borne Aloft [Reserve]^CM^40^You can channel the magic of the winds to briefly grant you flight.
+Captivating Melody^CM^40^You can expend some of your musical abilities to increase the potency of your enchantment or illusion spells.
+Clap of Thunder [Reserve]^CM^40^You can deliver a thunderous roar with a touch.
+Cloudy Conjuration^CM^40^Your conjured creations and summoned beings appear in a puff of sickening black smoke, and you vanish in a cloud of the same when you teleport.
+Clutch of Earth [Reserve]^CM^40^You briefly increase the earth's pull on the target creature.
+Dazzling Illusion^CM^41^Casting illusions causes the air about you to be filled with flashing colors that dazzle your foes.
+Defending Spirit^CM^41^Your watchful spirit helps keep you safe in combat.
+Delay Potion^CM^41^You can drink a potion and postpone its effects.
+Dimensional Jaunt^CM^41^With a single step, you can cross an entire room.
+Dimensional Reach [Reserve]^CM^41^You can transport small objects to you with an act of will.
+Drowning Glance [Reserve]^CM^41^With a look, you create a small but incapacitating amount of water in the subject's lungs.
+Elemental Adept^CM^42^You can spontaneously cast a spell of the element you have mastered.
+Energy Abjuration^CM^42^Casting an abjuration spell grants you protection from energy damage.
+Energy Gestalt [Tactical]^CM^42^You have learned to combine multiple energy effects to great advantage.
+Face-Changer [Reserve]^CM^42^Your mastery of illusions allows you to subtly alter your appearance at whim.
+Favored Magic Foe^CM^42^Through study, you have learned how best to defend yourself against your favored enemies' spells and how to best affect them with your own.
+Fearsome Necromancy^CM^42^Creatures subjected to your necromantic spells feel the chill of fear.
+Fey Heritage [Heritage]^CM^43^You are descended from creatures native to the fey realms. You are naturally resistant to the most common effects produced by
+Fey Legacy [Heritage]^CM^43^The magical powers of your ancestors manifest in you.
+Fey Power [Heritage]^CM^43^Your fey heritage augments the power of certain types of magic.
+Fey Presence [Heritage]^CM^43^You share your ancestor's knack for playing tricks on the minds of others.
+Fey Skin [Heritage]^CM^43^Your fey heritage guards you against all weapons except those crafted from the dreaded cold iron.
+Fiendish Heritage [Heritage]^CM^43^You are descended from creatures native to the Lower Planes.
+Fiendish Legacy [Heritage]^CM^43^The magical powers of your ancestors manifest in you.
+Fiendish Power [Heritage]^CM^43^Your fiendish heritage augments the power of certain types of magic.
+Fiendish Presence [Heritage]^CM^43^You share your ancestors' ability to tamper with the minds of weak-minded fools.
+Fiendish Resistance [Heritage]^CM^43^Your bloodline inures you against corrosion and fire.
+Fiery Burst [Reserve]^CM^43^You channel your magical talent into a blast of fire.
+Hasty Spirit^CM^44^Your watchful spirit lends you a burst of speed in times of great need.
+Hurricane Breath [Reserve]^CM^44^The power of elemental air you hold in your mind allows you to exhale the wind.
+Insightful Divination^CM^44^Casting a divination spell grants you an uncanny insight into danger.
+Invisible Needle [Reserve]^CM^44^You can create tiny darts of force.
+Magic Device Attunement^CM^44^You have a knack for activating familiar magic items.
+Magic Disruption [Reserve]^CM^44^You can use your powers of abjuration to interfere with other casters' spells.
+Magic Sensitive [Reserve]^CM^44^You literally see the emanations of magic around you.
+Master of Undeath^CM^44^You can control an undead that you create . . . for a time.
+Melodic Casting^CM^44^You can weave your music and magic together into a single perfect voice.
+Metamagic School Focus^CM^45^You are unusually skilled at modifying the effects of a particular school of magic.
+Metamagic Spell Trigger^CM^45^You can apply metamagic feats you know to spell effects from magic items you activate with a spell trigger.
+Metamagic Vigor [Tactical]^CM^45^The energy you pour into increasing the power of your spells feeds back upon itself in an ever-increasing cycle.
+Minor Shapeshift [Reserve]^CM^45^Your mastery of shapeshifting magic allows you to reshape your flesh in small but significant ways.
+Mystic Backlash [Reserve]^CM^45^With a touch, your magic corrupts the spells of your enemy.
+Piercing Evocation^CM^46^Your evocation spells ignore an amount of energy resistance.
+Ranged Recall^CM^46^Your magical ranged attacks rarely miss.
+Rapid Metamagic^CM^46^You possess an uncanny mastery of your magic, enabling you to modify spells on the fly much faster than others can.
+Residual Magic [Tactical]^CM^46^You can use the lingering energy from a spell you cast to boost the effect of a later spell.
+Retributive Spell [Metamagic]^CM^47^You can keep a spell in reserve to use when a foe causes you harm.
+Shadow Veil [Reserve]^CM^47^You draw wisps of darkness across your enemy's eyes, obscuring the world around him.
+Sickening Grasp [Reserve]^CM^47^You wreak havoc with the inner organs of a target, causing it to grow ill.
+Somatic Weaponry^CM^47^You are adept at performing somatic spell components while your hands are occupied.
+Storm Bolt [Reserve]^CM^47^The electrical energy contained within your magic rages inside you, begging to be released.
+Summon Elemental [Reserve]^CM^47^You can channel the summoning power you hold to briefly bring forth an elemental servant.
+Sunlight Eyes [Reserve]^CM^48^The bright magic within you allows you to see through the darkest shadow.
+Touch of Distraction [Reserve]^CM^48^Your touch briefly clouds the mind of a foe, impeding its efforts.
+Toughening Transmutation^CM^48^Casting a transmutation spell briefly transforms your skin or that of an ally into sterner stuff.
+Unsettling Enchantment^CM^48^Your enchantment spells cloud the minds of even those who would otherwise resist their effects.
+Vengeful Spirit^CM^48^Your watchful spirit takes revenge on foes that have harmed you.
+Wind-Guided Arrows [Reserve]^CM^48^Your mastery of the wind allows you to alter the flight of a ranged weapon.
+Winter's Blast [Reserve]^CM^48^The frozen magic within you can burst forth in a hail of frost.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/dnd3earmor.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,87 @@
+<ac>
+<armor  name="Banded mail" cost="250" type="Heavy" maxdex="1" bonus="6" spellfailure="35" checkpenalty="-6" weight="35" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of overlapping strips of metal sewn to a backing of leather and chainmail. The strips cover vulnerable areas, while the chain and leather protect the joints and provide freedom of movement. Straps and buckles distribute the weight evenly. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Breastplate" cost="200" type="Medium" maxdex="3" bonus="5" spellfailure="25" checkpenalty="-4" weight="30" speed="20" speed20="15" speed30="20" >
+<description >A breastplate covers the front and back. It comes with a helmet and matching greaves (plates to cover the lower legs). A light suit or skirt of studded leather beneath the breastplate protects limbs without restricting
+movement much.
+</description >
+</armor>
+<armor  name="Buckler" cost="15" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="5" speed="30" speed20="20" speed30="30" >
+<description >This small metal shield is strapped to the forearm, allowing it to be worn and still use the hand. A bow or crossbow can be used without penalty. An off-hand weapon can be used, but a -1 penalty on attack rolls is imposed because of the extra weight on your arm. This penalty stacks with those for fighting with the off hand and, if appropriate, for fighting with two weapons. In any case, if a weapon is used in the off-hand, the character doesn't get the buckler's AC bonus for the rest of the round.
+</description >
+</armor>
+<armor  name="Chainmail" cost="150" type="Medium" maxdex="2" bonus="5" spellfailure="30" checkpenalty="-5" weight="40" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of interlocking metal rings. It includes a layer of quilted fabric underneath it to prevent chafing and to cushion the impact of blows. Several layers of mail are hung over vital areas. Most of the armor's weight hangs from the shoulders, making chainmail uncomfortable to wear for long periods of time. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Chainshirt" cost="100" type="Light" maxdex="4" bonus="4" spellfailure="20" checkpenalty="-2" weight="25" speed="30" speed20="20" speed30="30" >
+<description >A shirt of chainmail protects the torso while leaving the limbs free and mobile. A layer of quilted fabric underneath it prevents chafing and cushions the impact of blows. It comes with a steel cap.
+</description >
+</armor>
+<armor  name="Full Plate" cost="1500" type="Heavy" maxdex="1" bonus="8" spellfailure="35" checkpenalty="-6" weight="50" speed="20" speed20="15" speed30="20" >
+<description >This armor consists of shaped and fitted metal plates riveted and interlocked to cover the entire body. It includes gauntlets, heavy leather boots, and a visored helmet.
+</description >
+</armor>
+<armor  name="Half-Plate" cost="600" type="Heavy" maxdex="0" bonus="7" spellfailure="40" checkpenalty="-7" weight="50" speed="20" speed20="15" speed30="20" >
+<description >This armor is a combination of chainmail with metal plates (breastplate, epaulettes, elbow guards, gauntlets, tasses, and greaves) covering vital areas. Buckles and straps hold the whole suit together and distribute the weight, but the armor still hangs more loosely than full plate. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Hide" cost="15" type="Medium" maxdex="4" bonus="3" spellfailure="20" checkpenalty="-3" weight="25" speed="20" speed20="15" speed30="20" >
+<description >This armor is prepared from multiple layers of leather and animal hides. It is stiff and hard to move in.
+</description >
+</armor>
+<armor  name="Large SteelShield" cost="20" type="Shield" maxdex="100" bonus="2" spellfailure="15" checkpenalty="-2" weight="15" speed="30" speed20="20" speed30="30" >
+<description >A large shield is too heavy to use the shield hand for anything else.
+</description >
+</armor>
+<armor  name="Large Wooden Shield" cost="7" type="Shield" maxdex="100" bonus="2" spellfailure="15" checkpenalty="-2" weight="10" speed="30" speed20="20" speed30="30" >
+<description >A large shield is too heavy to use the shield hand for anything else.
+</description >
+</armor>
+<armor  name="Leather" cost="10" type="Light" maxdex="6" bonus="2" spellfailure="10" checkpenalty="0" weight="10" speed="30" speed20="20" speed30="30" >
+<description >The breastplate and shoulder protectors of this armor are made of leather that has been stiffened by boiling in oil. The rest of the armor is softer and more flexible leather.
+</description >
+</armor>
+<armor  name="Padded" cost="5" type="Light" maxdex="8" bonus="1" spellfailure="5" checkpenalty="0" weight="10" speed="30" speed20="20" speed30="30" >
+<description >Padded armor features quilted layers of cloth and batting.
+</description >
+</armor>
+<armor  name="Samll Steel Shield" cost="9" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="6" speed="30" speed20="20" speed30="30" >
+<description >A small shield's light weight lets a character carry other items in that hand (although the character cannot use weapons).
+</description >
+</armor>
+<armor  name="Scale Mail" cost="50" type="Medium" maxdex="3" bonus="4" spellfailure="25" checkpenalty="-4" weight="30" speed="20" speed20="15" speed30="20" >
+<description >This is a coat and leggings (and perhaps a separate skirt) of leather covered with overlapping pieces of metal, much like the scales of a fish. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Small Wooden Shield" cost="300" type="Shield" maxdex="100" bonus="1" spellfailure="5" checkpenalty="-1" weight="5" speed="30" speed20="20" speed30="30" >
+<description >A small shield's light weight lets a character carry other items in that hand (although the character cannot use weapons).
+</description >
+</armor>
+<armor  name="Splint mail" cost="200" type="Heavy" maxdex="0" bonus="6" spellfailure="40" checkpenalty="-7" weight="45" speed="20" speed20="15" speed30="20" >
+<description >This armor is made of narrow vertical strips of metal riveted to a backing of leather that is worn over cloth padding. Flexible chainmail protects the joints. It includes gauntlets.
+</description >
+</armor>
+<armor  name="Studded Leather" cost="25" type="Light" maxdex="5" bonus="3" spellfailure="15" checkpenalty="-1" weight="20" speed="30" speed20="20" speed30="30" >
+<description >This armor is made from tough but flexible leather (not hardened leather as with normal leather armor) reinforced with close-set metal rivets.
+</description >
+</armor>
+<armor  name="Tower Shield" cost="30" type="Shield" maxdex="100" bonus="0" spellfailure="50" checkpenalty="-10" weight="45" speed="30" speed20="20" speed30="30" >
+<description >This massive wooden shield is nearly as tall as the wielder. Basically, it is a portable wall meant to provide cover. It can provide up to total cover, depending on how far a character comes out from behind it. A tower shield, however, does not provide cover against targeted spells; a spellcaster can cast a spell on a character by targeting the shield. A tower shield cannot be used for the shield bash action.
+</description >
+</armor>
+<armor  name="Ring of Protection +1" cost="xx" type="Ring" maxdex="100" bonus="1" spellfailure="0" checkpenalty="0" weight="0" speed="30" speed20="20" speed30="30" >
+<description >A ring that gives Plus 1 AC
+</description >
+</armor>
+<armor  name="Monk Wisdom Bonus" cost="xx" type="NA" maxdex="100" bonus="4" spellfailure="0" checkpenalty="0" weight="0" speed="30" speed20="20" speed30="30" >
+<description >Bonus is Based on wisdom Bonus, and can not be removed unless I am uncontius
+</description >
+</armor>
+<armor  name="Inertial Armor Feat" cost="xx" type="NA" maxdex="100" bonus="4" spellfailure="0" checkpenalty="0" weight="0" speed="30" speed20="20" speed30="30" >
+<description >Armor Bonus based on the feat
+</description >
+</armor>
+</ac>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/dnd3echaracter.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,113 @@
+
+<nodehandler class="d20char_handler" icon="knight" module="d20" name="D20 Character Tool">
+<howtouse>
+<howto>
+To use this you do:
+
+To use this you just need to add all the stuff you wish, set your stats ect, then click on the
++(plus) next too the gears, and right click.
+
+This will cause it to roll what is needed to roll, except dmg on spells, you will still need to
+roll that normaly, I will work on a way to add it, but for now it isnt required.
+</howto>
+</howtouse>
+  <general>
+    <name>Player Name</name>
+    <player>Your Name</player>
+    <race>none</race>
+    <alignment abbr="LG">none</alignment>
+    <deity>none</deity>
+    <size acmodifier="0">none</size>
+    <height>none</height>
+    <weight>none</weight>
+    <age>none</age>
+    <gender>none</gender>
+    <eyes>none</eyes>
+    <hair>none</hair>
+    <speed>30</speed>
+  </general>
+  <classes level="0"/>
+  <abilities>
+    <stat abbr="Str" base="0" name="Strength"/>
+    <stat abbr="Dex" base="0" name="Dexterity"/>
+    <stat abbr="Con" base="0" name="Constitution"/>
+    <stat abbr="Int" base="0" name="Intelligence"/>
+    <stat abbr="Wis" base="0" name="Wisdom"/>
+    <stat abbr="Cha" base="0" name="Charisma"/>
+  </abilities>
+<inventory>
+<plat>0</plat>
+<gold>0</gold>
+<silver>0</silver>
+<copper>0</copper>
+<generalgear>None</generalgear>
+<magicalgear>None</magicalgear>
+</inventory>
+  <saves>
+    <save base="0" magmod="0" miscmod="0" name="Fortitude" stat="Con"/>
+    <save base="0" magmod="0" miscmod="0" name="Reflex" stat="Dex"/>
+    <save base="0" magmod="0" miscmod="0" name="Will" stat="Wis"/>
+  </saves>
+  <hp current="0" max="0"/>
+  <pp current1="0" free="0" max1="0" maxfree="0"/>
+  <skills>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Alchemy" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Animal Empathy" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Appraise" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Balance" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Bluff" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="1" misc="0" name="Climb" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Concetration" rank="0" stat="Con" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Craft" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Decipher Script" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Diplomacy" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Disable Device" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Disguise" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Escape Artist" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Forgery" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Gather Information" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Handle Animal" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Heal" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Hide" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Innuendo" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Intimidate" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Intuit Direction" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Jump" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Arcana" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Architecture and Engineering" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Geography" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: History" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Local" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nature" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nobility and Royalty" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: The Planes" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Religion" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Listen" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Move Silently" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Open Lock" rank="0" stat="Dex" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Perform" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Pick Pocket" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Profession" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Read Lips" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Ride" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Scry" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Search" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Sense Motive" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Spellcraft" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Spot" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Swim" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Tumble" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Magic Device" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Rope" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Wilderness Lore" rank="0" stat="Wis" untrained="1"/>
+  </skills>
+  <feats/>
+  <spells/>
+  <powers/>
+<divine/>
+  <attacks>
+    <melee base="0" misc="0"/>
+    <ranged base="0" misc="0"/>
+  </attacks>
+  <ac misc="" natural=""/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/dnd3eclasses.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,628 @@
+<classes>
+<class level="1" name="Arcane Archer" hd="d8" >
+<requirements>Race: Elf or half-elf.
+Base Attack Bonus: +6.
+Feats: Weapon Focus (any bow other than a crossbow), Point Blank Shot, Precise Shot.
+Spellcasting: Ability to cast 1st-level arcane spells.</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>An arcane archer is proficient with all simple and martial weapons, light armor, medium armor, and shields.</wa_proficiency>
+<features>Enchant Arrow: At 1st level, every nonmagical arrow an arcane archer nocks and lets fly becomes enchanted, gaining a +1 enhancement bonus. An archer's magic arrows only function for her. For every two levels of arcane archer the character advances past 1st level in the prestige class, the magic arrows she creates gain +1 greater potency.
+
+Imbue Arrow: At 2nd level, an arcane archer gains this spell-like ability, allowing her to place an area spell upon an arrow. When the arrow is fired, the spell's area is centered upon where the arrow lands, even if the spell could normally be centered only on the caster. This ability allows the archer to use the bow's range rather than the spell's range. It takes a standard action to cast the spell and fire the arrow. The arrow must be fired in the round the spell is cast, or the spell is wasted.
+
+Seeker Arrow: At 4th level, the arcane archer can launch an arrow once per day at a target known to her within range, and the arrow travels to the target, even around corners. Only an unavoidable obstacle or the end of the arrow's range prevents the arrow's flight. This ability negates cover and concealment modifiers, but otherwise the attack is rolled normally. This is a spell-like ability. (Shooting the arrow is part of the action.)
+
+Phase Arrow: At 6th level, the arcane archer can launch an arrow once per day at a target known to her within range, and the arrow travels to the target in a straight path, passing through any nonmagical barrier or wall in its way. (A wall of force, a wall of fire, or the like stops the arrow.) This ability negates cover, concealment, and even armor modifiers, but otherwise the attack is rolled normally. This is a spell-like ability. (Shooting the arrow is part of the action.)
+
+Hail of Arrows: In lieu of her regular attacks, once per day the 8th-level arcane archer can fire an arrow at each and every target within range, to a maximum of one target for every arcane archer level she has earned. Each attack uses the archer's primary attack bonus, and each enemy may only be targeted by a single arrow. This is a spell-like ability.
+
+Arrow of Death: At 10th level, the arcane archer can enchant an arrow of death that forces the target, if damaged by the arrow's attack, to make a Fortitude save (DC 20) or be slain immediately. It takes one day to create an arrow of death, and the arrow only functions for the arcane archer who created it. The enchantment lasts no longer than one year, and the archer can only have one such arrow in existence at a time.</features>
+</class>
+<class level="1" name="Assassin" hd="d6" >
+<requirements>Move Silently: 8 ranks.
+Hide: 8 ranks.
+Disguise: 4 ranks.
+Special: In addition, he must kill someone for no other reason than to join the assassins.</requirements>
+<alignment>Evil</alignment>
+<wa_proficiency>Assassins are proficient with the crossbow (hand, light, or heavy), dagger (any type), dart, rapier, sap, shortbow (normal and composite), and short sword. Assassins are proficient with light armor but not with shields.</wa_proficiency>
+<features>Sneak Attack: Any time the assassin's target would be denied her Dexterity bonus to AC (whether she actually has a Dexterity bonus or not), the assassin's attack deals +1d6 points of damage. This extra damage increases by +1d6 points every other level (+2d6 at 3rd level, +3d6 at 5th level, and so on). Should the assassin score a critical hit with a sneak attack, this extra damage is not multiplied.
+
+It takes precision and penetration to hit a vital spot, so ranged attacks can only count as sneak attacks if the target is 30 feet away or less.
+
+With a sap or an unarmed strike, the assassin can make a sneak attack that deals subdual damage instead of normal damage. He cannot use a weapon that deals normal damage to deal subdual damage in a sneak attack, not even with the usual -4 penalty, because he must make optimal use of his weapon in order to execute the sneak attack.
+
+An assassin can only sneak attack living creatures with discernible anatomies-undead, constructs, oozes, plants, and incorporeal creatures lack vital areas to attack. Additionally, any creature immune to critical hits is similarly immune to sneak attacks. Also, the assassin must also be able to see the target well enough to pick out a vital spot and must be able to reach a vital spot. The assassin cannot sneak attack while striking at a creature with concealment or by striking the limbs of a creature whose vitals are beyond reach.
+
+If an assassin gets a sneak attack bonus from another source (such as rogue levels), the bonuses to damage stack.
+
+Death Attack: If the assassin studies his victim for 3 rounds and then makes a sneak attack with a melee weapon that successfully deals damage, the sneak attack has the additional effect of possibly either paralyzing or killing the target (assassin's choice). While studying the victim, the assassin can undertake other actions so long as his attention stays focused on the target and the target does not detect the assassin or recognize the assassin as an enemy. If the victim of such an attack fails her Fortitude saving throw (DC 10 + the assassin's class level + the assassin's Intelligence modifier) against the kill effect, she dies. If the saving throw fails against the paralysis effect, the victim's mind and body become enervated, rendering her completely helpless and unable to act for 1d6 rounds plus 1 round per level of the assassin. If the victim's saving throw succeeds, the attack is just a normal sneak attack. Once the assassin has completed the 3 rounds of study, he must make the death attack within the next 3 rounds. If a death attack is attempted and fails (the victim makes her save) or if the assassin does not launch the attack within 3 rounds of completing the study, 3 new rounds of study are required before he can attempt another death attack.
+
+Poison Use: Assassins are trained in the use of poison and never risk accidentally poisoning themselves when applying poison to a blade.
+
+Spells: Beginning at 1st level, an assassin gains the ability to cast a small number of arcane spells. To cast a spell, the assassin must have an Intelligence score of at least 10 + the spell's level, so an assassin with an Intelligence of 10 or lower cannot cast these spells. Assassin bonus spells are based on Intelligence, and saving throws against these spells have a DC of 10 + spell level + the assassin's Intelligence modifier (if any). When the assassin gets 0 spells of a given level, such as 0 1st-level spells at 1st level, the assassin gets only bonus spells. An assassin without a bonus spell for that level cannot yet cast a spell of that level. The assassin's spell list appears below. An assassin prepares and casts spells just as a wizard does.
+
+Saving Throw Bonus vs. Poison: Assassins train with poisons of all types and slowly grow more and more resistant to their effects. This is reflected by a natural saving throw bonus to all poisons gained at 2nd level that increases by +1 for every two levels the assassin gains (+1 at 2nd level, +2 at 4th level, +3 at 6th level, and so on).
+
+Uncanny Dodge: Starting at 2nd level, the assassin gains the extraordinary ability to react to danger before his senses would normally allow him to even be aware of it. At 2nd level and above, he retains his Dexterity bonus to AC (if any) regardless of being caught flat-footed or struck by an invisible attacker. (He still loses his Dexterity bonus to AC if immobilized.)
+
+At 5th level, the assassin can no longer be flanked, since he can react to opponents on opposite sides of him as easily as he can react to a single attacker. This defense denies rogues the ability to use flank attacks to sneak attack the assassin. The exception to this defense is that a rogue at least four levels higher than the assassin can flank him (and thus sneak attack him).
+
+At 10th level, the assassin gains an intuitive sense that alerts him to danger from traps, giving him a +1 bonus to Reflex saves made to avoid traps.
+
+If the assassin has another class that grants the uncanny dodge ability, add together all the class levels of the classes that grant the ability and determine the character's uncanny dodge ability on that basis.
+
+Assassins choose their spells from the following list:
+
+1st level-change self, detect poison, ghost sound, obscuring mist, spider climb.
+2nd level-alter self, darkness, pass without trace, undetectable alignment.
+3rd level-deeper darkness, invisibility, misdirection, nondetection.
+4th level-dimension door, freedom of movement, improved invisibility, poison.</features>
+</class>
+<class level="1" name="Barbarian" hd="d12" >
+<requirements>None</requirements>
+<alignment>Nonlawful</alignment>
+<wa_proficiency>A barbarian is proficient with all simple and martial weapons, light armor, medium armor, and shields.</wa_proficiency>
+<features>Barbarian Rage: Barbarian temporarily gains +4 to Strength, +4 to Constitution, and a +2 morale bonus on Will saves, but suffers a -2 penalty to AC.
+
+The increase in Constitution increases the barbarian's hit points by 2 points per level, but these hit points go away at the end of the rage when the Constitution score drops back to normal. While raging, a barbarian cannot use skills or abilities that require patience and concentration. (The only class skills he can't use while raging are Craft, Handle Animal, and Intuit Direction.) He can use any feat he might have except for Expertise, item creation feats, metamagic feats, and Skill Focus (if it's tied to a skill that requires patience or concentration).
+
+A fit of rage lasts for a number of rounds equal to 3 + the character's (newly improved) Constitution modifier. The barbarian may prematurely end the rage voluntarily. At the end of the rage, the barbarian is fatigued (-2 to Strength, -2 to Dexterity, can't charge or run) for the duration of that encounter (unless the barbarian is 20th level, when this limitation no longer applies). The barbarian can only fly into a rage once per encounter, and only a certain number of times per day (determined by level). Entering a rage takes no time itself, but the barbarian can only do it during his action.
+
+Starting at 15th level, the barbarian's rage bonuses become +6 to Strength, +6 to Constitution, and a +3 morale bonus to Will saves. (The AC penalty remains at -2.)
+
+Fast Movement: The barbarian has a speed faster than the norm for his race by +10 feet when wearing no armor, light armor, or medium armor (and not carrying a heavy load).
+
+Uncanny Dodge: At 2nd level and above, the barbarian retains his Dexterity bonus to AC (if any) if caught flat-footed or struck by an invisible attacker.
+
+At 5th level, the barbarian can no longer be flanked. The exception to this defense is that a rogue at least four levels higher than the barbarian can still flank.
+
+At 10th level, the barbarian gains a +1 bonus to Reflex saves made to avoid traps and a +1 dodge bonus to AC against attacks by traps. At 13th level, these bonuses rise to +2. At 16th, they rise to +3, and at 19th they rise to +4.
+
+Damage Reduction: Starting at 11th level, the barbarian gains the extraordinary ability to shrug off some amount of injury from each blow or attack. Subtract 1 from the damage the barbarian takes each time the barbarian is dealt damage. At 14th level, this damage reduction rises to 2. At 17th, it rises to 3. At 20th, it rises to 4. Damage reduction can reduce damage to 0 but not below 0.
+
+Illiteracy: Barbarians are the only characters that do not automatically know how to read and write. A barbarian must spend 2 skill points to gain the ability to read and write any language the barbarian is able to speak.
+
+Ex-Barbarians: A barbarian who becomes lawful loses the ability to rage and cannot gain more levels as a barbarian. The barbarian retains all the other benefits of the class.</features>
+</class>
+<class level="1" name="Bard" hd="d6" >
+<requirements>None</requirements>
+<alignment>Nonlawful</alignment>
+<wa_proficiency>A bard is proficient with all simple weapons. Additionally, the bard is proficient with one of the following weapons: longbow, composite longbow, longsword, rapier, sap, short composite bow, short sword, shortbow, or whip. Bards are proficient with light armor, medium armor, and shields.</wa_proficiency>
+<features>Spells: A bard casts arcane spells. The bard casts these spells without needing to memorize them beforehand or keep a spellbook. Bards receive bonus spells for high Charisma, and to cast a spell a bard must have a Charisma score at least equal to 10 + the level of the spell. The Difficulty Class for a saving throw against a bard's spell is 10 + the spell's level + the bard's Charisma modifier.
+
+Bardic Music: Once per day per level, a bard can use song or poetics to produce magical effects on those around him or her. While these abilities fall under the category of bardic music, they can include reciting poetry, chanting, singing lyrical songs, singing melodies, whistling, playing an instrument, or playing an instrument in combination with some spoken performance. As with casting a spell with a verbal component, a deaf bard suffers a 20% chance to fail with bardic music. If the bard fails, the attempt still counts against the daily limit.
+
+The Bardic Music effects are:
+
+* Inspire Courage: A bard with 3 or more ranks in Perform can to inspire courage in his or her allies. To be affected, an ally must hear the bard sing for a full round. The effect lasts as long as the bard sings and for 5 rounds after the bard stops singing (or 5 rounds after the ally can no longer hear the bard). While singing, the bard can fight but cannot cast spells, activate magic items by spell completion (such as scrolls), or activate magic items by magic word (such as wands). Affected allies receive a +2 morale bonus to saving throws against charm and fear effects and a +1 morale bonus to attack and weapon damage rolls. Inspire courage is a supernatural, mind-affecting ability.
+
+* Countersong: A bard with 3 or more ranks in Perform can counter magical effects that depend on sound (but not spells that simply have verbal components). As with inspire courage, a bard may sing, play, or recite a countersong while taking other mundane actions, but not magical actions. Each round of the countersong, the bard makes a Perform check. Any creature within 30 feet of the bard (including the bard) who is affected by a sonic or language-dependent magical attack may use the bard's Perform check result in place of his saving throw if, after rolling the saving throw, the Perform check result proves to be better. The bard may keep up the countersong for 10 rounds. Countersong is a supernatural ability.
+
+* Fascinate: A bard with 3 or more ranks in Perform can cause a single creature to become fascinated with him. The creature to be fascinated must be able to see and hear the bard and must be within 90 feet. The bard must also see the creature. The creature must be able to pay attention to the bard. The distraction of a nearby combat or other dangers prevents the ability from working. The bard makes a Perform check, and the target can negate the effect with a Will saving throw equal to or greater than the bard's check result. If the saving throw succeeds, the bard cannot attempt to fascinate that creature again for 24 hours. If the saving throw fails, the creature sits quietly and listens to the song for up to 1 round per level of the bard. While fascinated, the target's Spot and Listen checks suffer a -4 penalty. Any potential threat (such as an ally of the bard moving behind the fascinated creature) allows the fascinated creature a second saving throw against a new Perform check result. Any obvious threat, such as casting a spell, drawing a sword, or aiming, automatically breaks the effect.
+
+While fascinating (or attempting to fascinate) a creature, the bard must concentrate, as if casting or maintaining a spell. Fascinate is a spell-like, mind- affecting charm ability.
+
+* Inspire Competence: A bard with 6 or more ranks in Perform can help an ally succeed at a task. The ally must be able to see and hear the bard and must be within 30 feet. The bard must also see the creature. The ally gets a +2 competence bonus on his skill checks with a particular skill as long as he or she continues to hear the bard's music. The DM may rule that certain uses of this ability are infeasible. The bard can maintain the effect for 2 minutes (long enough for the ally to take 20). Inspire competence is a supernatural, mind-affecting ability.
+
+* Suggestion: A bard with 9 or more ranks in Perform can make a suggestion (as the spell) to a creature that he has already fascinated (see above). The suggestion doesn't count against the bard's daily limit on bardic music performances (one per day per level), but the fascination does. A Will saving throw (DC 13 + the bard's Charisma modifier) negates the effect. Suggestion is a spell-like, mind-affecting charm ability.
+
+* Inspire Greatness: A bard with 12 or more ranks in Perform can inspire greatness in another creature. For every three levels the bard attains beyond 9th, the bard can inspire greatness in one additional creature. To inspire greatness, the bard must sing and the creature must hear the bard sing for a full round, as with inspire courage. The creature must also be within 30 feet. A creature inspired with greatness gains temporary Hit Dice, attack bonuses, and saving throw bonuses as long as he or she hears the bard continue to sing and for 5 rounds thereafter. (All these bonuses are competence bonuses.)
+
+The target gains the following boosts:
+* +2 Hit Dice (d10s that grant temporary hit points).
+* +2 competence bonus on attacks.
+* +1 competence bonus on Fortitude saves.
+
+Apply the target's Constitution modifier, if any, to each bonus Hit Die. These extra Hit Dice count as regular Hit Dice for determining effects such as the sleep spell. Inspire greatness is a supernatural, mind-affecting enchantment ability.
+
+Bardic Knowledge: A bard may make a special bardic knowledge check with a bonus equal to his level + his Intelligence modifier to see whether he knows some relevant information about local notable people, legendary items, or noteworthy places. This check will not reveal the powers of a magic item but may give a hint as to its general function. The bard may not take 10 or take 20 on this check; this sort of knowledge is essentially random. The DM will determine the Difficulty Class of the check by referring to the table below. DC      Type of Knowledge
+--      -----------------10      Common, known by at least a substantial minority of the local population.
+20      Uncommon but available, known by only a few people in the area.
+25      Obscure, known by few, hard to come by.
+30      Extremely obscure, known by very few, possibly forgotten by most who once knew it, possibly known only by those who don't understand the significance of the knowledge.
+
+Ex-Bards: A bard who becomes lawful in alignment cannot progress in levels as a bard, though he retains all his bard abilities.</features>
+</class>
+<class level="1" name="Blackguard" hd="d10" >
+<requirements>Base Attack Bonus: +6.
+Knowledge (religion): 2 ranks.
+Hide: 5 ranks.
+Feats: Cleave, Sunder.
+Special: The blackguard must have made peaceful contact with an evil outsider who was summoned by him or someone else to have contracted the taint of true evil.</requirements>
+<alignment>Any Evil</alignment>
+<wa_proficiency>Blackguards are proficient with all simple and martial weapons, with all types of armor, and with shields.</wa_proficiency>
+<features>Detect Good: At will, the blackguard can detect good as a spell-like ability. This ability duplicates the effects of the spell detect good.
+
+Poison Use: Blackguards are skilled in the use of poison and never risk accidentally poisoning themselves when applying poison to a blade.
+
+Dark Blessing: A blackguard applies his Charisma modifier (if positive) as a bonus to all saving throws.
+
+Spells: Beginning at 1st level, a blackguard gains the ability to cast a small number of divine spells. To cast a spell, the blackguard must have a Wisdom score of at least 10 + the spell's level, so a blackguard with a Wisdom of 10 or lower cannot cast these spells. Blackguard bonus spells are based on Wisdom, and saving throws against these spells have a DC of 10 + spell level + the blackguard's Wisdom modifier. When the blackguard gets 0 spells of a given level, such as 0 1st-level spells at 1st level, he gets only bonus spells. (A blackguard without a bonus spell for that level cannot yet cast a spell of that level.) The blackguard's spell list appears below. A blackguard has access to any spell on the list and can freely choose which to prepare, just like a cleric. A blackguard prepares and casts spells just as a cleric does (though the blackguard cannot spontaneously cast cure or inflict spells).
+
+Smite Good: Once a day, a blackguard of 2nd level or higher may attempt to smite good with one normal melee attack. He adds his Charisma modifier (if positive) to his attack roll and deals 1 extra point of damage per class level. For example, a 9th-level blackguard armed with a longsword would deal 1d8+9 points of damage, plus any additional bonuses from high Strength or magical effects that normally apply. If the blackguard accidentally smites a creature that is not good, the smite has no effect but it is still used up for that day. Smite good is a supernatural ability.
+
+Aura of Despair: Beginning at 3rd level, the blackguard radiates a malign aura that causes enemies within 10 feet of him to suffer a -2 morale penalty on all saving throws. Aura of despair is a supernatural ability.
+
+Command Undead: When a blackguard reaches 3rd level, he gains the supernatural ability to command and rebuke undead. He commands undead as would a cleric of two levels lower.
+
+Sneak Attack: If a blackguard can catch an opponent when she is unable to defend herself effectively from his attack, he can strike a vital spot for extra damage. Basically, any time the blackguard's target would be denied her Dexterity bonus to AC (whether she actually has a Dexterity bonus or not), the blackguard's attack deals +1d6 points of damage at 4th level and an additional +1d6 points for every three levels thereafter (+2d6 at 7th level, +3d6 at 10th level, and so on). Should the blackguard score a critical hit with a sneak attack, this extra damage is not multiplied.
+
+Ranged attacks only count as sneak attacks if the target is 30 feet away or less. A blackguard cannot make a sneak attack to deal subdual damage. The blackguard must be able to see the target well enough to pick out a vital spot and must be able to reach a vital spot. He cannot sneak attack while striking at a creature with concealment or by striking the limbs of a creature whose vitals are beyond reach.
+
+A blackguard can only sneak attack living creatures with discernible anatomies. Undead, constructs, oozes, plants, and incorporeal creatures lack vital areas to attack. Additionally, any creature immune to critical hits is not subject to sneak attacks.
+
+If a blackguard gets a sneak attack bonus from another source (such as rogue levels), the bonuses to damage stack.
+
+Blackguards choose their spells from the following list:
+
+1st level-cause fear, cure light wounds, doom, inflict light wounds, magic weapon, summon monster I*.
+2nd level-bull's strength, cure moderate wounds, darkness, death knell, inflict moderate wounds, shatter, summon monster II*.
+3rd level-contagion, cure serious wounds, deeper darkness, inflict serious wounds, protection from elements, summon monster III*.
+4th level-cure critical wounds, freedom of movement, inflict critical wounds, poison, summon monster IV*.
+
+*Evil creatures only.
+
+Fallen Paladins
+
+Blackguards who possess levels of paladin (that is to say, are now ex-paladins) gain extra abilities the more levels of paladin they possess. Those who have tasted the light of goodness and justice and turned away make the foulest villains.</features>
+</class>
+<class level="1" name="Cleric" hd="d8" >
+<requirements>None</requirements>
+<alignment>Varies by deity. A cleric's alignment must be within one step of his deity's, and it may not be neutral unless the deity's alignment is neutral.</alignment>
+<wa_proficiency>Clerics are proficient with all simple weapons. Clerics are proficient with all types of armor (light, medium, and heavy) and with shields.</wa_proficiency>
+<features>Some deities have favored weapons, and clerics consider it a point of pride to wield them. A cleric whose deity's favored weapon is a martial weapon and who chooses War as one of his domains receives the Martial Weapon Proficiency feat related to that weapon for free, as well as the Weapon Focus feat related to that weapon.
+
+Spells: A cleric casts divine spells. A cleric may prepare and cast any spell on the cleric spell list, provided he can cast spells of that level. The Difficulty Class for a saving throw against a cleric's spell is 10 + the spell's level + the cleric's Wisdom modifier.
+
+Each cleric must choose a time at which he must spend an hour each day in quiet contemplation or supplication to regain his daily allotment of spells. Time spent resting has no effect on whether a cleric can prepare spells.
+
+In addition to his standard spells, a cleric gets one domain spell of each spell level, starting at 1st. When a cleric prepares a domain spell, it must come from one of his two domains.
+
+Deity, Domains, and Domain Spells: Choose a deity for your cleric. The cleric's deity influences his alignment, what magic he can perform, his values, and how others see him.
+
+Choose two from among the deity's domains for your cleric's domains. You can only select an alignment domain (such as Good) for your cleric if his alignment matches that domain.
+
+If your cleric is not devoted to a particular deity, you still select two domains to represent his spiritual inclinations and abilities (but the restriction on alignment domains still applies).
+
+Each domain gives your cleric access to a domain spell at each spell level, from 1st on up, as well as a granted power. Your cleric gets the granted powers of all the domains selected. With access to two domain spells at a given spell level, a cleric prepares one or the other each day. If a domain spell is not on the Cleric Spells list, a cleric can only prepare it in his domain slot.
+
+Spontaneous Casting: Good clerics (and neutral clerics of good deities) can channel stored spell energy into healing spells that they haven't prepared ahead of time. The cleric can "lose" a prepared spell in order to cast any cure spell of the same level or lower (a cure spell is any spell with "cure" in its name).
+
+An evil cleric (or a neutral cleric of an evil deity), on the other hand, can't convert prepared spells to cure spells but can convert them to inflict spells (an inflict spell is one with "inflict" in the title).
+
+A cleric who is neither good nor evil and whose deity is neither good nor evil can convert spells either to cure spells or to inflict spells (player's choice), depending on whether the cleric is more proficient at wielding positive or negative energy. Once the player makes this choice, it cannot be reversed. This choice also determines whether the neutral cleric turns or commands undead (see below).
+
+A cleric can't use spontaneous casting to convert domain spells into cure or inflict spells. These spells arise from the particular powers of the cleric's deity, not divine energy in general.
+
+Chaotic, Evil, Good, and Lawful Spells: A cleric can't cast spells of an alignment opposed to his own or to his deity's.
+
+Turn or Rebuke Undead: A good cleric (or a neutral cleric who worships a good deity) has the supernatural ability to turn undead. Evil clerics (and neutral clerics who worship evil deities) can rebuke such creatures. Neutral clerics of neutral deities can do one or the other (player's choice), depending on whether the cleric is more proficient at wielding positive or negative energy. Once the player makes this choice, it cannot be reversed. This choice also determines whether the neutral cleric can cast spontaneous cure or inflict spells (see above).
+
+A cleric may attempt to turn or rebuke undead a number of times per day equal to three plus his Charisma modifier.
+
+Extra Turning: As a feat, a cleric may take Extra Turning. This feat allows the cleric to turn undead four more times per day than normal. A cleric can take this feat multiple times, gaining four extra daily turning attempts each time.
+
+Bonus Languages: A cleric's list of bonus languages includes Celestial, Abyssal, and Infernal, in addition to the bonus languages available to the character because of his race.
+
+Ex-Clerics: A cleric who grossly violates the code of conduct expected by his god (generally acting in ways opposed to the god's alignment or purposes) loses all spells and class features and cannot gain levels as a cleric of that god until he atones.</features>
+</class>
+<class level="1" name="Druid" hd="d8" >
+<requirements>None</requirements>
+<alignment>Neutral good, lawful neutral, neutral, chaotic neutral, or neutral evil.</alignment>
+<wa_proficiency>Druids are proficient with the following weapons: club, dagger, dart, halfspear, longspear, quarterstaff, scimitar, sickle, shortspear, and sling. Their spiritual oaths prohibit them from using weapons other than these. They are proficient with light and medium armors but are prohibited from wearing metal armor (thus, they may wear only padded, leather, or hide armor). They are skilled with shields but must use only wooden ones. </wa_proficiency>
+<features>A druid who wears prohibited armor or wields a prohibited weapon is unable to use any of her magical powers while doing so and for 24 hours thereafter. (Note: A druid can use wooden items that have been altered by the ironwood spell so that they function as though they were steel.)
+
+Spells: A druid casts divine spells. A druid may prepare and cast any spell on the druid spell list provided she can cast spells of that level. She prepares and casts spells the way a cleric does (though she cannot lose a prepared spell to cast a cure spell in its place). To prepare or cast a spell, a druid must have a Wisdom score of at least 10 + the spell's level. The Difficulty Class for a saving throw against a druid's spell is 10 + the spell's level + the druid's Wisdom modifier. Bonus spells for druids are based on Wisdom.
+
+Chaotic, Evil, Good, and Lawful Spells: A druid can't cast spells of an alignment opposed to her own.
+
+Bonus Languages: A druid may substitute Sylvan for one of the bonus languages available to her. In addition, a druid knows the Druidic language. This secret language is known only to druids, and druids are forbidden from teaching it to nondruids. Druidic has its own alphabet.
+
+Nature Sense: A druid can identify plants and animals (their species and special traits) with perfect accuracy. The druid can determine whether water is safe to drink or dangerous.
+
+Animal Companion: A 1st-level druid may begin play with an animal companion. This animal is one that the druid has befriended with the spell animal friendship.
+
+Woodland Stride: Starting at 2nd level, a druid may move through natural thorns, briars, overgrown areas, and similar terrain at his or her normal speed and without suffering damage or other impairment. However, thorns, briars, and overgrown areas that are enchanted or magically manipulated to impede motion still affect the druid.
+
+Trackless Step: Starting at 3rd level, a druid leaves no trail in natural surroundings and cannot be tracked.
+
+Resist Nature's Lure: Starting at 4th level, a druid gains a +4 bonus to saving throws against the spell-like abilities of feys.
+
+Wild Shape: At 5th level, a druid gains the spell-like ability to polymorph self into a Small or Medium-size animal (but not a dire animal) and back again once per day. Unlike the standard use of the spell, however, the druid may only adopt one form. As stated in the spell description, the druid regains hit points as if he or she has rested for a day. The druid does not risk the standard penalty for being disoriented while in the wild shape.
+
+The druid can use this ability more times per day at 6th, 7th, 10th, 14th, and 18th level, as noted. In addition, the druid gains the ability to take the shape of a Large animal at 8th level, a Tiny animal at 11th level, and a Huge animal at 15th level. At 12th level or higher, she can take the form of a dire animal.
+
+At 16th level or higher, the druid may use wild shape to change into a Small, Medium-size, or Large air, earth, fire, or water elemental once per day. The druid gains all the elemental's special abilities. At 18th level, the druid can do this three times per day.
+
+Venom Immunity: At 9th level, a druid gains immunity to all organic poisons, including monster poisons but not mineral poisons or poison gas.
+
+A Thousand Faces: At 13th level, a druid gains the supernatural ability to change his or her appearance at will, as if using the spell alter self.
+
+Timeless Body: After achieving 15th level, a druid no longer suffers ability penalties for aging and cannot be magically aged. Any penalties she may have already suffered, however, remain in place. Bonuses still accrue, and the druid still dies of old age when her time is up.
+
+Ex-Druids: A druid who ceases to revere nature or who changes to a prohibited alignment loses all spells and druidic abilities and cannot gain levels as a druid until she atones.</features>
+</class>
+<class level="1" name="Dwarven Defender" hd="d12" >
+<requirements>Race: Dwarf.
+Base Attack Bonus: +7.
+Feats: Dodge, Endurance, Toughness.</requirements>
+<alignment>Any Lawful</alignment>
+<wa_proficiency>The dwarven defender is proficient with all simple and martial weapons, all types of armor, and shields.</wa_proficiency>
+<features>Defensive Stance: When he needs to, the defender can become a stalwart bastion of defense. In this defensive stance, a defender gains phenomenal strength and durability, but he cannot move from the spot he is defending. He gains the following benefits:+2 Strength, +4 Constitution, +2 resistance bonus on all saves, +4 dodge bonus to AC.
+
+While defending, a defender cannot use skills or abilities that would require him to shift his position, such as Move Silently or Jump. A defensive stance lasts for 3 rounds, plus the character's (newly improved) Constitution modifier. The defender may end the defense voluntarily prior to this limit. At the end of the defense, the defender is winded and suffers a -2 penalty to Strength for the duration of that encounter. The defender can only take his defensive stance a certain number of times per day as determined by his level. Taking the stance takes no time itself, but the defender can only do so during his action.
+
+Defensive Awareness: Starting at 2nd level, the dwarven defender gains the extraordinary ability to react to danger before his senses would normally allow him to even be aware of it. At 2nd level and above, he retains his Dexterity bonus to AC (if any) regardless of being caught flat-footed or struck by an invisible attacker. (He still loses any Dexterity bonus to AC if immobilized.)
+
+At 5th level, the dwarven defender can no longer be flanked, since he can react to opponents on opposite sides of him as easily as he can react to a single attacker. This defense denies rogues the ability to use flank attacks to sneak attack the dwarven defender. The exception to this defense is that a rogue at least 4 levels higher than the dwarven defender can flank him (and thus sneak attack him).
+
+At 10th level, the dwarven defender gains an intuitive sense that alerts him to danger from traps, giving him a +1 bonus to Reflex saves made to avoid traps.
+
+Defensive awareness is cumulative with uncanny dodge. If the dwarven defender has another class that grants the uncanny dodge ability, add together all the class levels of the classes that grant these two abilities and determine the character's defensive awareness ability on that basis.
+
+Damage Reduction: At 6th level, the dwarven defender gains the extraordinary ability to shrug off some amount of injury from each blow or attack. Subtract 3 from the damage the dwarven defender takes each time he is dealt damage. At 10th level, this damage reduction rises to 6. Damage reduction can reduce damage to 0 but not below 0. (That is, the defender cannot actually gain hit points in this manner.)</features>
+</class>
+<class level="1" name="Fighter" hd="d10" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The fighter is proficient in the use of all simple and martial weapons and all armor (heavy, medium, and light) and shields.</wa_proficiency>
+<features>Bonus Feats: At 1st level, the fighter gets a bonus feat in addition to the feat that any 1st-level character gets and the bonus feat granted to humans. The fighter gains an additional bonus feat at 2nd level and every two levels thereafter (4th, 6th, 8th, etc.). These bonus feats must be drawn from the following list: Ambidexterity, Blind-Fight, Combat Reflexes, Dodge (Mobility, Spring Attack), Exotic Weapon Proficiency*, Expertise (Improved Disarm, Improved Trip, Whirlwind Attack), Improved Critical*, Improved Initiative, Improved Unarmed Strike (Deflect Arrows, Stunning Fist), Mounted Combat (Mounted Archery, Trample, Ride-By Attack, Spirited Charge), Point Blank Shot (Far Shot, Precise Shot, Rapid Shot, Shot on the Run), Power Attack (Cleave, Improved Bull Rush, Sunder, Great Cleave), Quick Draw, Two- Weapon Fighting (Improved Two-Weapon Fighting), Weapon Finesse*, Weapon Focus*, Weapon Specialization*.
+
+Some of the bonus feats available to a fighter cannot be acquired until the fighter has gained one or more prerequisite feats; these feats are listed parenthetically after the prerequisite feat. A fighter can select feats marked with an asterisk (*) more than once, but it must be for a different weapon each time. A fighter must still meet all prerequisites for a feat, including ability score and base attack bonus minimums.
+
+Weapon Specialization: On achieving 4th level or higher, as a feat the fighter (and only the fighter) may take Weapon Specialization. Weapon Specialization adds a +2 damage bonus with a chosen weapon. The fighter must have Weapon Focus with that weapon to take Weapon Specialization. If the weapon is a ranged weapon, the damage bonus only applies if the target is within 30 feet, because only at that range can the fighter strike precisely enough to hit more effectively. The fighter may take this feat as a bonus feat or as a regular one.</features>
+</class>
+<class level="1" name="Loremasters" hd="d4" >
+<requirements>Spellcasting: Ability to cast seven different divinations, one of which must be 3rd level or higher.
+Two Knowledge Skills (Any Type): 10 ranks in each.
+Feats: Any three metamagic or item creation feats, plus Skill Focus (Knowledge [any individual Knowledge skill]).</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Loremasters gain no proficiency in any weapon or armor.</wa_proficiency>
+<features>Spells per Day: A loremaster continues training in magic as well as her field of research. Thus, when a new loremaster level is gained, the character gains new spells per day as if she had also gained a level in a spellcasting class she belonged to before she added the prestige class. She does not, however, gain any other benefit a character of that class would have gained (improved chance of controlling or rebuking undead, metamagic or item creation feats, and so on). This essentially means that she adds the level of loremaster to the level of some other spellcasting class the character has, then determines spells per day and caster level accordingly.
+
+If a character had more than one spellcasting class before she became a loremaster, she must decide to which class she adds each level of loremaster for purposes of determining spells per day when she adds the new level.
+
+Secret: In their studies, loremasters stumble upon all sorts of applicable knowledge and secrets. At 1st level and every two levels afterward (3rd, 5th, 7th, and 9th levels), the loremaster chooses one secret from Table: Loremaster Secrets. Her level plus Intelligence modifier determines which secrets she can choose. She can't choose the same secret twice.
+
+Lore: Loremasters gather knowledge. At 2nd level, they gain the ability to know legends or information regarding various topics, just like a bard can with bardic knowledge. The loremaster adds her level and her Intelligence modifier to the Knowledge check. See page 29 in the Player's Handbook for more information on bardic knowledge.
+
+Bonus Languages: Loremasters, in their laborious studies, learn new languages in order to access more knowledge. The loremaster can choose any new language at 4th and 8th level.
+
+Greater Lore: At 6th level, a loremaster gains the ability to identify magic items, as the spell, as an extraordinary ability. She may do this once per item examined.
+
+True Lore: At 10th level, once per day a loremaster can use her knowledge to gain the effects of a legend lore spell or an analyze dweomer spell. True lore is an extraordinary ability.</features>
+</class>
+<class level="1" name="Monk" hd="d8" >
+<requirements>None</requirements>
+<alignment>Any Lawful</alignment>
+<wa_proficiency>Monks are proficient with basic peasant weapons and special weapons whose use is part of monk training. The full list includes club, crossbow (light or heavy), dagger, handaxe, javelin, kama, nunchaku, quarterstaff, shuriken, siangham, and sling.
+
+A monk using a kama, nunchaku, or siangham can strike with his or her unarmed base attack, including her more favorable number of attacks per round (see below). His or her damage, however, is standard for the weapon (1d6, crit X2), not his or her unarmed damage. The weapon must be light, so a Small monk must use Tiny versions of these weapons in order to use the more favorable base attack.
+
+A monk adds her Wisdom bonus (if any) to AC, in addition to her normal Dexterity modifier, and her AC improves as she gains levels. (Only add this extra AC bonus if the total of the monk's Wisdom modifier and the number in the "AC Bonus" column is a positive number.) The Wisdom bonus and the AC bonus represent a preternatural awareness of danger, and a monk does not lose either even in situations when he or she loses her Dexterity modifier due to being unprepared, ambushed, stunned, and so on. (Monks do lose these AC bonuses when immobilized.)
+
+When wearing armor, a monk loses her AC bonus for Wisdom, AC bonus for class and level, favorable multiple unarmed attacks per round, and heightened movement. Furthermore, her special abilities all face the arcane spell failure chance that the armor type normally imposes.</wa_proficiency>
+<features>Unarmed Strike: A monk fighting unarmed gains the benefits of the Improved Unarmed Strike feat and thus does not provoke attacks of opportunity from armed opponents that she attacks.
+
+Making an off-hand attack makes no sense for a monk striking unarmed.
+
+A monk fighting with a one-handed weapon can make an unarmed strike as an off-hand attack, but she suffers the standard penalties for two-weapon fighting. Likewise, a monk with a weapon (other than a special monk weapon) in her off hand gets an extra attack with that weapon but suffers the usual penalties for two-weapon fighting and can't strike with a flurry of blows.
+
+Flurry of Blows: The monk may make one extra attack in a round at her highest base attack, but this attack and each other attack made that round suffer a -2 penalty apiece. This penalty applies for 1 round, so it affects attacks of opportunity the monk might make before her next action. The monk must use the full attack action to strike with a flurry of blows. A monk may also use the flurry of blows if armed with a special monk weapon (kama, nunchaku, or siangham). If armed with one such weapon, the monk makes the extra attack either with that weapon or unarmed. If armed with two such weapons, she uses one for the regular attack (or attacks) and the other for the extra attack. In any case, her damage bonus on the attack with her off hand is not reduced.
+
+Usually, a monk's unarmed strikes deal normal damage rather than subdual damage. However, she can choose to deal her damage as subdual damage when grappling.
+
+Stunning Attack: The monk can use this ability once per round, but no more than once per level per day. The monk must declare she is using a stun attack before making the attack roll (thus, a missed attack roll ruins the attempt). A foe struck by the monk is forced to make a Fortitude saving throw (DC 10 + one-half the monk's level + Wisdom modifier). In addition to receiving normal damage, If the saving throw fails, the opponent is stunned for 1 round. The stunning attack is a supernatural ability.
+
+Evasion: If a monk makes a successful Reflex saving throw against an attack that normally deals half damage on a successful save, the monk instead takes no damage. Evasion can only be used if the monk is wearing light armor or no armor. It is an extraordinary ability.
+
+Deflect Arrows: At 2nd level, a monk gains the Deflect Arrows feat, even if she doesn't have the prerequisite Dexterity score.
+
+Fast Movement: At 3rd level and higher, a monk moves faster than normal. A monk in armor (even light armor) or carrying a medium or heavy load loses this extra speed. A dwarf or a Small monk moves more slowly than a Medium-size monk.
+
+From 9th level on, the monk's running ability is actually a supernatural ability.
+
+Still Mind: At 3rd level, a monk gains a +2 bonus to saving throws against spells and effects from the Enchantment school.
+
+Slow Fall: At 4th level, the monk takes damage as if a fall were 20 feet shorter than it actually is. At 18th level, the monk can use a nearby wall to slow her descent and fall any distance without harm.
+
+Purity of Body: At 5th level, a monk gains immunity to all diseases except for magical diseases.
+
+Improved Trip: At 6th level, a monk gains the Improved Trip feat. She need not have taken the Expertise feat, normally a prerequisite.
+
+Wholeness of Body: At 7th level, a monk can cure her own wounds. She can cure up to twice her current level in hit points each day, and she can spread this healing out among several uses. Wholeness of body is a supernatural ability.
+
+Leap of the Clouds: At 7th level or higher, a monk's jumping distance (vertical or horizontal) is not limited according to her height.
+
+Improved Evasion: At 9th level, a monk only takes half damage on a failed save.
+
+Ki Strike: At 10th level, a monk's unarmed attack is empowered with ki. The unarmed strike damage from such an attack can deal damage to a creature with damage reduction as if the blow were made with a weapon with a +1 enhancement bonus. Ki strike is a supernatural ability.
+
+Diamond Body: At 11th level, a monk gains immunity to poison of all kinds. Diamond body is a supernatural ability.
+
+Abundant Step: At 12th level, a monk can slip magically between spaces, as per the spell dimension door, once per day. This is a spell-like ability, and the monk's effective casting level is one-half her actual level (rounded down).
+
+Diamond Soul: At 13th level, a monk gains spell resistance. Her spell resistance equals her level + 10.
+
+Quivering Palm: Starting at 15th level, a monk can use the quivering palm.
+
+The monk can use the quivering palm attack once a week, and she must announce her intent before making her attack roll. Creatures immune to critical hits cannot be affected. The monk must be of higher level than the target (or have more levels than the target's number of Hit Dice). If the monk strikes successfully and the target takes damage from the blow, the quivering palm attack succeeds. Thereafter the monk can choose to try to slay the victim at any later time within 1 day per level of the monk. The monk merely wills the target to die (a free action), and unless the target makes a Fortitude saving throw (DC 10 + one-half the monk's level + Wisdom modifier), it dies. If the saving throw is successful, the target is no longer in danger from that particular quivering palm attack (but may be affected by another one at a later time). Quivering palm is a supernatural ability.
+
+Timeless Body: After achieving 17th level, a monk no longer suffers ability penalties for aging and cannot be magically aged. (Any penalties she may have already suffered remain in place.) Bonuses still accrue, and the monk still dies of old age when her time is up.
+
+Tongue of the Sun and Moon: A monk of 17th level or above can speak with any living creature.
+
+Empty Body: At 19th level or higher, a monk can assume an ethereal state for 1 round per level per day, as per the spell etherealness. The monk may go ethereal on a number of different occasions during any single day as long as the total number of rounds spent ethereal does not exceed her level. Empty body is a supernatural ability.
+
+Perfect Self: At 20th level, a monk is forevermore treated as an outsider rather than as a humanoid. Additionally, the monk gains damage reduction 20/+1.
+
+Ex-Monks: A monk who becomes nonlawful cannot gain new levels as a monk but retain all monk abilities.</features>
+</class>
+<class level="1" name="Paladin" hd="d10" >
+<requirements>None</requirements>
+<alignment>Lawful</alignment>
+<wa_proficiency>Paladins are proficient with all simple and martial weapons, with all types of armor (heavy, medium, and light), and with shields.</wa_proficiency>
+<features>Detect Evil: At will, the paladin can detect evil as a spell-like ability. This ability duplicates the effects of the spell detect evil.
+
+Divine Grace: A paladin applies her Charisma modifier (if positive) as a bonus to all saving throws.
+
+Lay on Hands: Each day a paladin can cure a total number of hit points equal to the paladin's Charisma bonus (if any) times the paladin's level. The paladin can cure themselves. The paladin may choose to divide her curing among multiple recipients, and he or she doesn't have to use it all at once. Lay on hands is a spell-like ability whose use is a standard action.
+
+Alternatively, the paladin can use any or all of these points to deal damage to undead creatures. Treat this attack just like a touch spell. The paladin decides how many cure points to use as damage after successfully touching the undead creature.
+
+Divine Health: A paladin is immune to all diseases, including magical diseases.
+
+Aura of Courage: Beginning at 2nd level, a paladin is immune to fear (magical or otherwise). Allies within 10 feet of the paladin gain a +4 morale bonus on saving throws against fear effects. Granting the morale bonus to allies is a supernatural ability.
+
+Smite Evil: Once per day, a paladin of 2nd level or higher may attempt to smite evil with one normal melee attack. She adds her Charisma modifier (if positive) to the paladin's attack roll and deals 1 extra point of damage per level. If the paladin accidentally smites a creature that is not evil, the smite has no effect but it is still used up for that day. Smite evil is a supernatural ability.
+
+Remove Disease: Beginning at 3rd level, a paladin can remove disease, as per the spell remove disease, once per week. Remove disease is a spell-like ability for paladins.
+
+Turn Undead: The paladin may use this ability a number of times per day equal to three plus the paladin's Charisma modifier. The paladin turns undead as a cleric of two levels lower would.
+
+Extra Turning: As a feat, a paladin may take Extra Turning. This feat allows the paladin to turn undead four more times per day than normal. A paladin can take this feat multiple times, gaining four extra daily turning attempts each time.
+
+Spells: Beginning at 4th level, a paladin gains the ability to cast a small number of divine spells. To cast a spell, the paladin must have a Wisdom score of at least 10 + the spell's level. Paladin bonus spells are based on Wisdom, and saving throws against these spells have a Difficulty Class of 10 + spell level + Wisdom modifier. When the paladin gets 0 spells of a given level, such as 0 1st-level spells at 4th level, the paladin gets only bonus spells. A paladin has access to any spell on the paladin spell list and can freely choose which to prepare, just as a cleric can.
+
+A paladin prepares and casts spells just as a cleric does (though the paladin cannot use spontaneous casting to substitute a cure spell in place of a prepared spell).
+
+Through 3rd level, a paladin has no caster level. Starting at 4th level, a paladin's caster level is one-half his or her class level.
+
+Special Mount: Upon or after reaching 5th level, a paladin can call an unusually intelligent, strong, and loyal steed to serve him or her in her crusade against evil. This mount is usually a heavy warhorse (for a Medium-size paladin) or a warpony (for a Small paladin).
+
+Should the paladin's mount die, another cannot be called for a year and a day. The new mount has all the accumulated abilities due a mount of the paladin's level.
+
+The DM will provide information about the mount that responds to the paladin's call.
+
+Code of Conduct: A paladin must be of lawful good alignment and loses all special class abilities if she ever willingly commits an act of evil. Additionally, a paladin's code requires that she respect legitimate authority, act with honor (not lying, not cheating, not using poison, etc.), help those who need help (provided they do not use the help for evil or chaotic ends), and punish those that harm or threaten innocents.
+
+Associates: While she may adventure with characters of any good or neutral alignment, a paladin will never knowingly associate with evil characters. A paladin will not continue an association with someone who consistently offends her moral code. A paladin may only hire henchmen or accept followers who are lawful good.
+
+Ex-Paladins: A paladin who ceases to be lawful good, who willfully commits an evil act, or who grossly violates the code of conduct loses all special abilities and spells, including the service of the paladin's warhorse. She also may not progress in levels as a paladin. She regains her abilities if she atones for her violations, as appropriate.</features>
+</class>
+<class level="1" name="Ranger" hd="d10" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>A ranger is proficient with all simple and martial weapons, light armor, medium armor, and shields. </wa_proficiency>
+<features>When wearing light armor or no armor, a ranger can fight with two weapons as if he or she had the feats Ambidexterity and Two-Weapon Fighting. The ranger loses this special bonus when fighting in medium or heavy armor, or when using a double-headed weapon (such as a double sword).
+
+Spells: Beginning at 4th level, a ranger gains the ability to cast a small number of divine spells. To cast a spell, the ranger must have a Wisdom score of at least 10 + the spell's level. Ranger bonus spells are based on Wisdom, and saving throws against these spells have a Difficulty Class of 10 + spell level + Wisdom modifier. When the ranger gets 0 spells of a given level, such as 0 1st-level spells at 4th level, the ranger gets only bonus spells. A ranger without a bonus spell for that level cannot yet cast a spell of that level. A ranger has access to any spell on the ranger spell list and can freely choose which to prepare. A ranger prepares and casts spells just as a cleric does (though the ranger cannot use spontaneous casting to lose a spell and cast a cure or inflict spell in its place).
+
+Through 3rd level, a ranger has no caster level. Starting at 4th level, a ranger's caster level is one-half his class level.
+
+Track: A ranger gains Track as a bonus feat.
+
+Favored Enemy: At 1st level, a ranger may select a type of creature as a favored enemy. (A ranger can only select his own race as a favored enemy if he is evil.) Due to his extensive study of his foes and training in the proper techniques for combating them, the ranger gains a +1 bonus to Bluff, Listen, Sense Motive, Spot, and Wilderness Lore checks when using these skills against this type of creature. Likewise, he gets the same bonus to weapon damage rolls against creatures of this type. A ranger also gets the damage bonus with ranged weapons, but only against targets within 30 feet (the ranger cannot strike with deadly accuracy beyond that range). The bonus doesn't apply to damage against creatures that are immune to critical hits.
+
+At 5th level and at every five levels thereafter (10th, 15th, and 20th level), the ranger may select a new favored enemy, and the bonus associated with every previously selected favored enemy goes up by +1.
+
+* Rangers may not select "humanoid" or "outsider" as a favored enemy, but they may select a more narrowly defined type of humanoid or outsider. A ranger can only select his own race as a favored enemy if he is evil.
+
+Improved Two-Weapon Fighting: A ranger with a base attack bonus of at least +9 can choose to gain the Improved Two-Weapon Fighting feat even if he does not have the other prerequisites for the feat. The ranger must be wearing light armor or no armor in order to use this benefit. </features>
+</class>
+<class level="1" name="Rogue" hd="d6" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>A rogue's weapon training focuses on weapons suitable for stealth and sneak attacks. Thus, all rogues are proficient with the crossbow (hand or light), dagger (any type), dart, light mace, sap, shortbow (normal and composite), and short sword. Medium-size rogues are also proficient with certain weapons that are too big for Small rogues to use and conceal easily: club, heavy crossbow, heavy mace, morningstar, quarterstaff, and rapier. Rogues are proficient with light armor but not with shields. </wa_proficiency>
+<features>Sneak Attack: Any time the rogue's target would be denied a Dexterity bonus to AC (whether the target actually has a Dexterity bonus or not), or when the rogue flanks the target, the rogue's attack deals extra damage. The extra damage is +1d6 at 1st level and an additional 1d6 every two levels thereafter. Should the rogue score a critical hit with a sneak attack, this extra damage is not multiplied.
+
+Ranged attacks can only count as sneak attacks if the target is within 30 feet. The rogue can't strike with deadly accuracy from beyond that range.
+
+With a sap (blackjack) or an unarmed strike, the rogue can make a sneak attack that deals subdual damage instead of normal damage. The rogue cannot use a weapon that deals normal damage to deal subdual damage in a sneak attack, not even with the usual -4 penalty.
+
+A rogue can only sneak attack a living creature with a discernible anatomy. Any creature that is immune to critical hits is also not vulnerable to sneak attacks. The rogue must be able to see the target well enough to pick out a vital spot and must be able to reach a vital spot. The rogue cannot sneak attack while striking a creature with concealment or striking the limbs of a creature whose vitals are beyond reach.
+
+Traps: Rogues (and only rogues) can use the Search skill to locate traps when the task has a Difficulty Class higher than 20. Finding a nonmagical trap has a DC of at least 20, higher if it is well hidden. Finding a magic trap has a DC of 25 + the level of the spell used to create it.
+
+Rogues (and only rogues) can use the Disable Device skill to disarm magic traps. A magic trap generally has a DC of 25 + the level of the spell used to create it.
+
+A rogue who beats a trap's DC by 10 or more with a Disable Device check can generally study a trap, figure out how it works, and bypass it (with his party) without disarming it.
+
+Evasion: At 2nd level, a rogue gains evasion. If exposed to any effect that normally allows a character to attempt a Reflex saving throw for half damage, the rogue takes no damage with a successful saving throw. Evasion can only be used if the rogue is wearing light armor or no armor. It is an extraordinary ability.
+
+Uncanny Dodge: At 3rd level and above, she retains her Dexterity bonus to AC (if any) if caught flat-footed or struck by an invisible attacker.
+
+At 6th level , the rogue can no longer be flanked. Another rogue at least four levels higher can still flank.
+
+At 11th level, the rogue gains a +1 bonus to Reflex saves made to avoid traps and a +1 dodge bonus to AC against attacks by traps. At 14th level, these bonuses rise to +2. At 17th, they rise to +3, and at 20th they rise to +4.
+
+Special Abilities: On achieving 10th level and every three levels thereafter (13th, 16th, and 19th), a rogue chooses a special ability from among the following:
+
+Crippling Strike: When the rogue damages an opponent with a sneak attack, the target also takes 1 point of Strength damage.
+
+Defensive Roll: Once per day, when a rogue would be reduced to 0 hit points or less by damage in combat (from a weapon or other blow, not a spell or special ability), the rogue can attempt to roll with the damage. She makes a Reflex saving throw (DC = damage dealt) and, if successful, takes only half damage from the blow. The rogue must be aware of the attack and able to react to it in order to execute the defensive roll - if the Dexterity bonus to AC is denied, the rogue can't roll. Since this effect would not normally allow a character to make a Reflex save for half damage, the rogue's evasion ability does not apply to the defensive roll.
+
+Improved Evasion: The rogue takes only half damage on a failed save.
+
+Opportunist: Once per round, the rogue can make an attack of opportunity against an opponent who has just been struck for damage in melee by another character. This attack counts as the rogue's attacks of opportunity for that round. Even a rogue with the Combat Reflexes feat can't use the opportunist ability more than once per round.
+
+Skill Mastery: The rogue selects a number of skills equal to 3 + Intelligence modifier. When making a skill check with one of these skills, the rogue may take 10 even if stress and distractions would normally prevent the rogue from doing so. The rogue may gain this special ability multiple times, selecting additional skills for it to apply to each time.
+
+Slippery Mind: If a rogue with a slippery mind is affected by an enchantment and fails the saving throw, 1 round later the rogue can attempt the saving throw again. The rogue only gets this one extra chance to succeed. This is an extraordinary ability.
+
+Feat: A rogue may gain a feat in place of a special ability. </features>
+</class>
+<class level="1" name="Shadowdancer" hd="d8" >
+<requirements>Move Silently: 8 ranks.
+Hide: 10 ranks.
+Perform: 5 ranks.
+Feats: Dodge, Mobility, Combat Reflexes</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Shadowdancers are proficient with the club, crossbow (hand, light, or heavy), dagger (any type), dart, mace, morningstar, quarterstaff, rapier, sap, shortbow (normal and composite), and short sword. Shadowdancers are proficient with light armor but not with shields.</wa_proficiency>
+<features>Hide in Plain Sight: Shadowdancers can use the Hide skill even while being observed. As long as they are within 10 feet of some sort of shadow, shadowdancers can hide themselves from view in the open without anything to actually hide behind. They cannot, however, hide in their own shadows. Hide in plain sight is a supernatural ability.
+
+Evasion: At 2nd level, a shadowdancer gains evasion. If exposed to any effect that normally allows her to attempt a Reflex saving throw for half damage (such as a fireball), she takes no damage with a successful saving throw. The evasion ability can only be used if the shadowdancer is wearing light armor or no armor.
+
+Darkvision: At 2nd level, a shadowdancer can see in the dark as though she were permanently under the affect of a darkvision spell. This is a supernatural ability.
+
+Uncanny Dodge: Starting at 2nd level, the shadowdancer gains the extraordinary ability to react to danger before her senses would normally allow her to even be aware of it. At 2nd level and above, she retains her Dexterity bonus to AC (if any) regardless of being caught flat-footed or struck by an invisible attacker. (She still loses any Dexterity bonus to AC if immobilized.)
+
+At 5th level, the shadowdancer can no longer be flanked, since she can react to opponents on opposite sides of her as easily as she can react to a single attacker. This defense denies rogues the ability to use flank attacks to sneak attack the shadowdancer. The exception to this defense is that a rogue at least 4 levels higher than the shadowdancer can flank her (and thus sneak attack her).
+
+At 10th level, the shadowdancer gains an intuitive sense that alerts her to danger from traps, giving her a +1 bonus to Reflex saves made to avoid traps.
+
+If the shadowdancer has another class that grants the uncanny dodge ability, add together all the class levels of the classes that grant the ability and determine the character's uncanny dodge ability on that basis.
+
+Shadow Illusion: When a shadowdancer reaches 3rd level, she can create visual illusions from surrounding shadows. This spell-like ability is identical to the arcane spell silent image and may be employed once per day.
+
+Summon Shadow: At 3rd level, a shadowdancer can summon a shadow, an undead shade. Unlike a normal shadow, this shadow's alignment matches that of the shadowdancer. The summoned shadow cannot be turned, rebuked, or commanded by any third party. This shadow serves as a companion to the shadowdancer and can communicate intelligibly with the shadowdancer. Every third level gained by the shadowdancer allows her to summon an additional shadow and adds +2 HD (and the requisite base attack and base save bonus increases) to all her shadow companions.
+
+If a shadow companion is destroyed, or the shadowdancer chooses to dismiss it, the shadowdancer must attempt a Fortitude saving throw (DC 15). If the saving throw fails, the shadowdancer loses 200 experience points per shadowdancer level. A successful saving throw reduces the loss by half, to 100 XP per prestige class level. The shadowdancer's experience can never go below 0 as the result of a shadow's dismissal or destruction. A destroyed or dismissed shadow companion cannot be replaced for a year and a day.
+
+Shadow Jump: At 4th level, the shadowdancer gains the ability to travel between shadows as if by means of a dimension door spell. The limitation is that the magical transport must begin and end in an area with at least some shadow. The shadowdancer can jump up to a total of 20 feet each day in this way, although this may be a single jump of 20 feet or two jumps of 10 feet each. Every two levels thereafter, the distance a shadowdancer can jump each day doubles (40 feet at 6th level, 80 feet at 8th level, and 160 feet at 10th level). This amount can be split among many jumps, but each one, no matter how small, counts as a 10-foot increment. (A 6th-level shadowdancer who jumps 32 feet cannot jump again until the next day.)
+
+Defensive Roll: Starting at 5th level, the shadowdancer can roll with a potentially lethal blow to take less damage from it. Once per day, when a shadowdancer would be reduced to 0 hit points or less by damage in combat (from a weapon or other blow, not a spell or special ability), the shadowdancer can attempt to roll with the damage. She makes a Reflex saving throw (DC = damage dealt) and, if successful, takes only half damage from the blow. She must be aware of the attack and able to react to it in order to execute her defensive roll. If she is in a situation that would deny her any Dexterity bonus to AC, she can't attempt the defensive roll.
+
+Slippery Mind: This extraordinary ability, gained at 7th level, represents the shadowdancer's ability to wriggle free from magical effects that would otherwise control or compel her. If the shadowdancer is affected by an enchantment and fails her saving throw, 1 round later she can attempt her saving throw again. She only gets this one extra chance to succeed at her saving throw. If it fails as well, the spell's effects proceed normally.
+
+Improved Evasion: This extraordinary ability, gained at 10th level, works like evasion (see above). The shadowdancer takes no damage at all on successful saving throws against attacks that allow a Reflex saving throw for half damage (breath weapon, fireball, and so on). What's more, she takes only half damage even if she fails her saving throw, since the shadowdancer's reflexes allow her to get out of harm's way with incredible speed.</features>
+</class>
+<class level="1" name="Sorcerer" hd="d4" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Sorcerers are proficient with all simple weapons. They are not proficient with any type of armor, nor with shields. </wa_proficiency>
+<features>Spells: A sorcerer casts arcane spells. The number of spells a sorcerer knows is not affected by his Charisma bonus. The spells a sorcerer knows can be common spells chosen from the sorcerer and wizard spell list, or they can be unusual spells that the sorcerer has gained some understanding of by study.
+
+A sorcerer is limited to casting a certain number of spells of each level per day, but he need not prepare his spells in advance. The number of spells he can cast per day is improved by his bonus spells, if any.
+
+A sorcerer may use a higher-level slot to cast a lower-level spell if he so chooses. The spell is still treated as its actual level, not the level of the slot used to cast it.
+
+To learn or cast a spell, a sorcerer must have a Charisma score of at least 10 + the spell's level. The Difficulty Class for saving throws against sorcerer spells is 10 + the spell's level + the sorcerer's Charisma modifier.</features>
+</class>
+<class level="1" name="Wizard" hd="d4" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Wizards are skilled with the club, dagger, heavy crossbow, light crossbow, and quarterstaff. Wizards are not proficient with any type of armor nor with shields. </wa_proficiency>
+<features>Spells: A wizard casts arcane spells. She is limited to a certain number of spells of each spell level per day, according to her class level. A wizard must prepare spells ahead of time by getting a good night's sleep and spending 1 hour studying her spellbook. While studying, the wizard decides which spells to prepare. To learn, prepare, or cast a spell, a wizard must have an Intelligence score of at least 10 + the spell's level. A wizard's bonus spells are based on Intelligence. The Difficulty Class for saving throws against wizard spells is 10 + the spell's level + the wizard's Intelligence modifier.
+
+Bonus Languages: A wizard may substitute Draconic for one of the bonus languages available to the character.
+
+Familiar: A wizard can summon a familiar in exactly the same manner as a sorcerer. See the sorcerer description.
+
+Scribe Scroll: A wizard has the bonus item creation feat Scribe Scroll, enabling her to create magic scrolls.
+
+Bonus Feats: Every five levels, a wizard gains a bonus feat. This feat must be a metamagic feat, an item creation feat, or Spell Mastery.
+
+Spellbooks: Wizards must study their spellbooks each day to prepare their spells. A wizard cannot prepare any spell not recorded in her spellbook (except for read magic, which all wizards can prepare from memory).
+
+Spell Mastery: A wizard (and only a wizard) can take the special feat Spell Mastery. Each time the wizard takes this feat, choose a number of spells equal to the wizard's Intelligence modifier (they must be spells that the wizard already knows). From that point on, the wizard can prepare these spells without referring to a spellbook.
+
+School Specialization
+
+A school is one of eight groupings of spells, each defined by a common theme, such as illusion or necromancy. A wizard may specialize in one school of magic.
+
+Specialization allows a wizard to cast extra spells from the chosen school, but the wizard then never learns to cast spells from one or more other schools. Spells of the school or schools that the specialist gives up are not available to her, and she can't even cast such spells from scrolls or wands.
+
+The wizard must choose whether to specialize and how at 1st level. She may not change her specialization later.
+
+The specialist can prepare one additional spell (of the school selected as a specialty) per spell level each day.
+
+The specialist gains a +2 bonus to Spellcraft checks to learn the spells of her chosen school.
+
+The eight schools of arcane magic are Abjuration, Conjuration, Divination, Enchantment, Evocation, Illusion, Necromancy, and Transmutation. Spells that do not fall into any of these schools are called universal spells.
+
+Abjuration: To become an abjurer, a wizard must select a prohibited school or schools from the following choices: (1) either Conjuration, Enchantment, Evocation, Illusion, or Transmutation; or (2) both Divination and Necromancy.
+
+Conjuration: To become a conjurer, a wizard must select a prohibited school or schools from one of the following choices: (1) Evocation; (2) any two of the following three schools: Abjuration, Enchantment, and Illusion; (3) Transmutation, or (4) any three schools.
+
+Divination: To become a diviner, a wizard must select any other single school as a prohibited school.
+
+Enchantment: To become an enchanter, a wizard must select a prohibited school or schools from the following choices: (1) either Abjuration, Conjuration, Evocation, Illusion, or Transmutation; or (2) both Divination and Necromancy.
+
+Evocation: To become an evoker, a wizard must select a prohibited school or schools from one of the following choices: (1) Conjuration; (2) any two of the following three schools: Abjuration, Enchantment, and Illusion; (3) Transmutation; or (4) any three schools.
+
+Illusion: To become an illusionist, a wizard must select a prohibited school or schools from the following choices: (1) either Abjuration, Conjuration, Enchantment, Evocation, or Transmutation; or (2) both Divination and Necromancy.
+
+Necromancy: To become a necromancer, a wizard must select any other single school as a prohibited school.
+
+Transmutation: To become a transmuter, a wizard must select a prohibited school or schools from one of the following choices: (1) Conjuration; (2) Evocation; (2) any two of the following three schools: Abjuration, Enchantment, and Illusion; or (4) any three schools.
+
+Universal: Not a school, but a category for spells all wizards can learn. A wizard cannot select universal as a specialty school or as a school to which she does not have access.</features>
+</class>
+<class level="1" name="Adept" hd="d6" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Adepts are skilled with all simple weapons. Adepts are not proficient with any type of armor nor with shields</wa_proficiency>
+<features>Spells: An adept casts divine spells. She is limited to a certain number of spells of each spell level per day, according to her class level. Like a cleric, an adept may prepare and cast any spell on the adept list, provided she can cast spells of that level. Like a cleric, she prepares her spells ahead of time each day. The DC for a saving throw against an adept's spell is 10 + spell level + the adept's Wisdom modifier.
+
+Adepts, unlike wizards, do not acquire their spells from books or scrolls, nor prepare them through study. Instead, they meditate or pray for their spells, receiving them as divine inspiration or through their own strength of faith. Each adept must choose a time each day at which she must spend an hour in quiet contemplation or supplication to regain her daily allotment of spells. Time spent resting has no effect on whether an adept can prepare spells.
+
+When the adept gets 0 spells of a given level, she gets only bonus spells for that spell slot. An adept without a bonus spell for that level cannot yet cast a spell of that level. Bonus spells are based on Wisdom.
+
+Each adept has a particular holy symbol (as a divine focus) depending on the adept's magical tradition.
+
+Familiar: At 2nd level, an adept can call a familiar, just like a sorcerer or wizard can.
+
+Adept Spell List
+0 level-create water, cure minor wounds, detect magic, ghost sound, guidance, light, mending, purify food and drink, read magic.
+1st level-bless, burning hands, cause fear, command, comprehend languages, cure light wounds, detect chaos, detect evil, detect good, detect law, endure elements, obscuring mist, protection from chaos, protection from evil, protection from good, protection from law, sleep.
+2nd level-aid, animal trance, bull's strength, cat's grace, cure moderate wounds, darkness, delay poison, endurance, invisibility, mirror image, resist elements, see invisibility, web.
+3rd level-animate dead, bestow curse, contagion, continual flame, cure serious wounds, daylight, deeper darkness, lightning bolt, neutralize poison, remove curse, remove disease, tongues.
+4th level-cure critical wounds, minor creation, polymorph other, polymorph self, restoration, stoneskin, wall of fire.
+5th level-break enchantment, commune, heal, major creation, raise dead, true seeing, wall of stone.</features>
+</class>
+<class level="1" name="Aristocrat" hd="d8" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The aristocrat is proficient in the use of all simple and martial weapons and with all types of armor and shields.</wa_proficiency>
+<features>None</features>
+</class>
+<class level="1" name="Commoner" hd="d4" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The commoner is proficient with one simple weapon. He is not proficient with weapons, armor, or shields.</wa_proficiency>
+<features>None</features>
+</class>
+<class level="1" name="Expert" hd="d6" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The expert is proficient in the use of all simple weapons and with light armor but not shields. </wa_proficiency>
+<features>None</features>
+</class>
+<class level="1" name="Warrior" hd="d8" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>The warrior is proficient in the use of all simple and martial weapons and all armor and shields.</wa_proficiency>
+<features>None</features>
+</class>
+<class level="1" name="Psion" hd="d4" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Psions are proficient with all simple weapons. They are not proficient with any type of armor, nor with shields. </wa_proficiency>
+<features>Psionics: A Psion manifests psionic powers. The number of pisonic powers know by a psion is a fixed number, by level. Psions must select a specific Discipline to belong to. The psion is required to take a minimum number of powers from his disipline. The psion does not gain extra powers for high stats.
+
+A psion manifest power through the use of power points. The psion does not need to prepare his power before hand as a wizard does. The psion is more like a sorceror in this regard. The psion gains bonus power points for having a high stat in their displine stat.
+
+To learn or manifest a psionic power, a psion must have a stat score of at least 10 + the powers's level in the stat coresponding to it discipline. The Difficulty Class for saving throws against sorcerer spells is 1d20 + the powers's level + the psions's stat modifier.</features>
+</class>
+<class level="1" name="Psychic Warrior" hd="d8" >
+<requirements>None</requirements>
+<alignment>Any</alignment>
+<wa_proficiency>Psychic Warriors are proficient with all simple and martial weapons, and all armour and shields. </wa_proficiency>
+<features>Psionics: A Psychic manifests psionic powers. The number of pisonic powers know by a psion is a fixed number, by level. The psychic warrior does not gain extra powers for high stats. The psychic warriors psionic abilities are inferior to that of the psions.
+
+A psychic warrior manifest power through the use of power points. The psychic warrior does not need to prepare his power before hand as a wizard does. The psychic warrior is more like a sorceror in this regard. The psychic warrior does not gain bonus power points for having a high stat in their displine stat.
+
+To learn or manifest a psionic power, a psychic warrior must have a stat score of at least 10 + the powers's level in the stat coresponding to it discipline. The Difficulty Class for saving throws against sorcerer spells is 1d20 + the powers's level + the psychic warrior's stat modifier.</features>
+</class>
+</classes>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/dnd3edivine.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,660 @@
+<divine>
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Druid Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Create Water' memrz='0' level='0' desc='Creates 2 gallons/level of pure water' />
+<gift name='Cure Minor Wounds' memrz='0' level='0' desc='Cures 1 point of damage' />
+<gift name='Detect Magic' memrz='0' level='0' desc='Detects spells and magic items within 60 ft' />
+<gift name='Detect Poison' memrz='0' level='0' desc='Detects poison in one creature or small object' />
+<gift name='Flare' memrz='0' level='0' desc='Dazzles one creature (-1 attack)' />
+<gift name='Guidance' memrz='0' level='0' desc='+1 on one roll, throw, or check' />
+<gift name='Know Direction' memrz='0' level='0' desc='You discern north' />
+<gift name='Light' memrz='0' level='0' desc='Object shines like a torch' />
+<gift name='Mending' memrz='0' level='0' desc='Makes minor repairs on an object' />
+<gift name='Purify Food and Drink' memrz='0' level='0' desc='Purifies 1 cu. ft./level of food or water' />
+<gift name='Read Magic' memrz='0' level='0' desc='Read scrolls and spellbooks' />
+<gift name='Resistance' memrz='0' level='0' desc='Subject gains +1 on saving throws' />
+<gift name='Animal Friendship' memrz='0' level='1' desc='Gain permanent animal companions' />
+<gift name='Calm Animals' memrz='0' level='1' desc='Calms 2d4 +1/level HD of animals, beasts, and magical beasts' />
+<gift name='Cure Light Wounds' memrz='0' level='1' desc='Cures 1d8 +1/level damage (max +5)' />
+<gift name='Detect Animals or Plants' memrz='0' level='1' desc='Detects species of animals or plants' />
+<gift name='Detect Snares and Pits' memrz='0' level='1' desc='Reveals natural or primitive traps' />
+<gift name='Endure Elements' memrz='0' level='1' desc='Ignores 5 damage/round from one energy type' />
+<gift name='Entangle' memrz='0' level='1' desc='Plants entangle everyone in 40-ft.-radius circle' />
+<gift name='Faerie Fire' memrz='0' level='1' desc='Outlines subjects with light, canceling blur, concealment, etc' />
+<gift name='Goodberry' memrz='0' level='1' desc='2d4 berries each cure 1 hp (max 8 hp/24 hours)' />
+<gift name='Invisibility to Animals' memrz='0' level='1' desc='Animals cannot perceive one subject/level' />
+<gift name='Magic Fang' memrz='0' level='1' desc='One natural weapon of subject creature gets +1 bonus to attack and damage' />
+<gift name='Obscuring Mist' memrz='0' level='1' desc='Fog surrounds you' />
+<gift name='Pass without Trace' memrz='0' level='1' desc='One subject/level leaves no tracks' />
+<gift name='Shillelagh' memrz='0' level='1' desc='Cudgel or quarterstaff becomes +1 weapon (1d10 damage) for 1 minute/level' />
+<gift name='Summon Natures Ally I' memrz='0' level='1' desc='Calls creature to fight' />
+<gift name='Animal Messenger' memrz='0' level='2' desc='Sends a Tiny animal to a specific place' />
+<gift name='Animal Trance' memrz='0' level='2' desc='Fascinates 2d6 HD of animals' />
+<gift name='Barkskin' memrz='0' level='2' desc='Grants +3 natural armor bonus (or higher)' />
+<gift name='Charm Person or Animal' memrz='0' level='2' desc='Makes one person or animal your friend' />
+<gift name='Chill Metal' memrz='0' level='2' desc='Cold metal damages those who touch it' />
+<gift name='Delay Poison' memrz='0' level='2' desc='Stops poison from harming subject for 1 hour/level' />
+<gift name='Fire Trap' memrz='0' level='2' desc='Opened object deals 1d4 +1/level damage' />
+<gift name='Flame Blade' memrz='0' level='2' desc='Touch attack deals 1d8 +1/two levels damage' />
+<gift name='Flaming Sphere' memrz='0' level='2' desc='Rolling ball of fire, 2d6 damage, lasts 1 round/level' />
+<gift name='Heat Metal' memrz='0' level='2' desc='Hot metal damages those who touch it' />
+<gift name='Hold Animal' memrz='0' level='2' desc='Holds one animal helpless, 1 round/level' />
+<gift name='Lesser Restoration' memrz='0' level='2' desc='Dispels magic ability penalty or repairs 1d4 ability damage' />
+<gift name='Produce Flame' memrz='0' level='2' desc='1d4 +1/two levels damage, touch or thrown' />
+<gift name='Resist Elements' memrz='0' level='2' desc='Ignores first 12 damage from one energy type each round' />
+<gift name='Soften Earth and Stone' memrz='0' level='2' desc='Turns stone to clay or dirt to sand or mud' />
+<gift name='Speak with Animals' memrz='0' level='2' desc='You can communicate with natural animals' />
+<gift name='Summon Natures Ally II' memrz='0' level='2' desc='Calls creature to fight' />
+<gift name='Summon Swarm' memrz='0' level='2' desc='Summons swarm of small crawling or flying creatures' />
+<gift name='Tree Shape' memrz='0' level='2' desc='You look exactly like a tree for 1 hour/level' />
+<gift name='Warp Wood' memrz='0' level='2' desc='Bends wood (shaft, handle, door, plank)' />
+<gift name='Wood Shape' memrz='0' level='2' desc='Rearranges wooden objects to suit you' />
+<gift name='Call Lightning' memrz='0' level='3' desc='Directs lightning bolts (1d10/level) during storms' />
+<gift name='Contagion' memrz='0' level='3' desc='Infects subject with chosen disease' />
+<gift name='Cure Moderate Wounds' memrz='0' level='3' desc='Cures 2d8 +1/level damage (max +10)' />
+<gift name='Diminish Plants' memrz='0' level='3' desc='Reduces size or blights growth of normal plants' />
+<gift name='Dominate Animal' memrz='0' level='3' desc='Subject animal obeys silent mental commands' />
+<gift name='Greater Magic Fang' memrz='0' level='3' desc='One natural weapon of subject creature gets +1 for each 3 caster levels (max +5)' />
+<gift name='Meld into Stone' memrz='0' level='3' desc='You and your gear merge with stone' />
+<gift name='Neutralize Poison' memrz='0' level='3' desc='Detoxifies venom in or on subject' />
+<gift name='Plant Growth' memrz='0' level='3' desc='Grows vegetation, improves crops' />
+<gift name='Poison' memrz='0' level='3' desc='Touch deals 1d10 Con damage, repeats in 1 min' />
+<gift name='Protection from Elements' memrz='0' level='3' desc='Absorb 12 damage/level from one kind of energy' />
+<gift name='Remove Disease' memrz='0' level='3' desc='Cures all diseases affecting subject' />
+<gift name='Snare' memrz='0' level='3' desc='Creates a magical booby trap' />
+<gift name='Speak with Plants' memrz='0' level='3' desc='You can talk to normal plants and plant creatures' />
+<gift name='Spike Growth' memrz='0' level='3' desc='Creatures in area take 1d4 damage, may be slowed' />
+<gift name='Stone Shape' memrz='0' level='3' desc='Sculpts stone into any form' />
+<gift name='Summon Natures Ally III' memrz='0' level='3' desc='Calls creature to fight' />
+<gift name='Water Breathing' memrz='0' level='3' desc='Subjects can breathe underwater' />
+<gift name='Antiplant Shell' memrz='0' level='4' desc='Keeps animated plants at bay' />
+<gift name='Control Plants' memrz='0' level='4' desc='Talk to and control plants and fungi' />
+<gift name='Cure Serious Wounds' memrz='0' level='4' desc='Cures 3d8 +1/level damage (max +15)' />
+<gift name='Dispel Magic' memrz='0' level='4' desc='Cancels magical spells and effects' />
+<gift name='Flame Strike' memrz='0' level='4' desc='Smites foes with divine fire (1d6/level)' />
+<gift name='Freedom of Movement' memrz='0' level='4' desc='Subject moves normally despite impediments' />
+<gift name='Giant Vermin' memrz='0' level='4' desc='Turns insects into giant vermin' />
+<gift name='Quench' memrz='0' level='4' desc='Extinguishes nonmagical fires or one magic item' />
+<gift name='Reincarnate' memrz='0' level='4' desc='Brings dead subject back in a random body' />
+<gift name='Repel Vermin' memrz='0' level='4' desc='Insects stay 10 ft. away' />
+<gift name='Rusting Grasp' memrz='0' level='4' desc='Your touch corrodes iron and alloys' />
+<gift name='Scrying' memrz='0' level='4' desc='Spies on subject from a distance' />
+<gift name='Sleet Storm' memrz='0' level='4' desc='Hampers vision and movement' />
+<gift name='Spike Stones' memrz='0' level='4' desc='Creatures in area take 1d8 damage, may be slowed' />
+<gift name='Summon Natures Ally IV' memrz='0' level='4' desc='Calls creature to fight' />
+<gift name='Animal Growth' memrz='0' level='5' desc='One animal/two levels doubles in size, HD' />
+<gift name='Atonement' memrz='0' level='5' desc='Removes burden of misdeeds from subject' />
+<gift name='Awaken' memrz='0' level='5' desc='Animal or tree gains human intellect' />
+<gift name='Commune with Nature' memrz='0' level='5' desc='Learn about terrain for one mile/level' />
+<gift name='Control Winds' memrz='0' level='5' desc='Change wind direction and speed' />
+<gift name='Cure Critical Wounds' memrz='0' level='5' desc='Cures 4d8 +1/level damage (max +20)' />
+<gift name='Death Ward' memrz='0' level='5' desc='Grants immunity to all death spells and effects' />
+<gift name='Hallow' memrz='0' level='5' desc='Designates location as holy' />
+<gift name='Ice Storm' memrz='0' level='5' desc='Hail deals 5d6 damage in cylinder 40 ft. across' />
+<gift name='Insect Plague' memrz='0' level='5' desc='Insect horde limits vision, inflicts damage, and weak creatures flee' />
+<gift name='Summon Natures Ally V' memrz='0' level='5' desc='Calls creature to fight' />
+<gift name='Transmute Mud to Rock' memrz='0' level='5' desc='Transforms two 10-ft. cubes/level' />
+<gift name='Transmute Rock to Mud' memrz='0' level='5' desc='Transforms two 10-ft. cubes/level' />
+<gift name='Tree Stride' memrz='0' level='5' desc='Step from one tree to another far away' />
+<gift name='Unhallow' memrz='0' level='5' desc='Designates location as unholy' />
+<gift name='Wall of Fire' memrz='0' level='5' desc='2d4 fire damage out to 10 ft. and 1d4 out to 20 ft. Passing through wall deals 2d6 +1/level' />
+<gift name='Wall of Thorns' memrz='0' level='5' desc='Thorns damage anyone who tries to pass' />
+<gift name='Antilife Shell' memrz='0' level='6' desc='10-ft. field hedges out living creatures' />
+<gift name='Find the Path' memrz='0' level='6' desc='Shows most direct way to a location' />
+<gift name='Fire Seeds' memrz='0' level='6' desc='Acorns and berries become grenades and bombs' />
+<gift name='Greater Dispelling' memrz='0' level='6' desc='As dispel magic, but +20 on check' />
+<gift name='Healing Circle' memrz='0' level='6' desc='Cures 1d8 +1/level damage in all directions' />
+<gift name='Ironwood' memrz='0' level='6' desc='Magical wood is strong as steel' />
+<gift name='Liveoak' memrz='0' level='6' desc='Oak becomes treant guardian' />
+<gift name='Repel Wood' memrz='0' level='6' desc='Pushes away wooden objects' />
+<gift name='Spellstaff' memrz='0' level='6' desc='Stores one spell in wooden quarterstaff' />
+<gift name='Stone Tell' memrz='0' level='6' desc='Talk to natural or worked stone' />
+<gift name='Summon Natures Ally VI' memrz='0' level='6' desc='Calls creature to fight' />
+<gift name='Transport via Plants' memrz='0' level='6' desc='Move instantly from one plant to another of the same species' />
+<gift name='Wall of Stone' memrz='0' level='6' desc='20 hp/four levels; can be shaped' />
+<gift name='Changestaff' memrz='0' level='7' desc='Your staff becomes a treant on command' />
+<gift name='Control Weather' memrz='0' level='7' desc='Changes weather in local area' />
+<gift name='Creeping Doom' memrz='0' level='7' desc='Carpet of insects attacks at your command' />
+<gift name='Fire Storm' memrz='0' level='7' desc='Deals 1d6 fire damage/level' />
+<gift name='Greater Scrying' memrz='0' level='7' desc='As scrying, but faster and longer' />
+<gift name='Harm' memrz='0' level='7' desc='Subject loses all but 1d4 hp' />
+<gift name='Heal' memrz='0' level='7' desc='Cures all damage, diseases, and mental conditions' />
+<gift name='Summon Natures Ally VII' memrz='0' level='7' desc='Calls creature to fight' />
+<gift name='Sunbeam' memrz='0' level='7' desc='Beam blinds and deals 3d6 damage' />
+<gift name='Transmute Metal to Wood' memrz='0' level='7' desc='Metal within 40 ft. becomes wood' />
+<gift name='True Seeing' memrz='0' level='7' desc='See all things as they really are' />
+<gift name='Wind Walk' memrz='0' level='7' desc='You and your allies turn vaporous and travel fast' />
+<gift name='Animal Shapes' memrz='0' level='8' desc='One ally/level polymorphs into chosen animal' />
+<gift name='Command Plants' memrz='0' level='8' desc='Plants animate and vegetation entangles' />
+<gift name='Finger of Death' memrz='0' level='8' desc='Kills one subject' />
+<gift name='Repel Metal or Stone' memrz='0' level='8' desc='Pushes away metal and stone' />
+<gift name='Reverse Gravity' memrz='0' level='8' desc='Objects and creatures fall upward' />
+<gift name='Summon Natures Ally VIII' memrz='0' level='8' desc='Calls creature to fight' />
+<gift name='Sunburst' memrz='0' level='8' desc='Blinds all within 10 ft., deals 3d6 damage' />
+<gift name='Whirlwind' memrz='0' level='8' desc='Cyclone inflicts damage and can pick up creatures' />
+<gift name='Word of Recall' memrz='0' level='8' desc='Teleports you back to designated place' />
+<gift name='Antipathy' memrz='0' level='9' desc='Object or location affected by spell repels certain creatures' />
+<gift name='Earthquake' memrz='0' level='9' desc='Intense tremor shakes 5-ft./level radius' />
+<gift name='Elemental Swarm' memrz='0' level='9' desc='Summons 2d4 Large, 1d4 Huge elementals' />
+<gift name='Foresight' memrz='0' level='9' desc='6th sense warns of impending danger' />
+<gift name='Mass Heal' memrz='0' level='9' desc='As heal, but with several subjects' />
+<gift name='Shambler' memrz='0' level='9' desc='Summons 1d4+2 shambling mounds to fight for you' />
+<gift name='Shapechange' memrz='0' level='9' desc='Transforms you into any creature, and change forms once per round' />
+<gift name='Summon Natures Ally IX' memrz='0' level='9' desc='Calls creature to fight' />
+<gift name='Sympathy' memrz='0' level='9' desc='Object or location attracts certain creatures' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Cleric Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Create Water' memrz='0' level='0' desc='Creates 2 gallons/level of pure water' />
+<gift name='Cure Minor Wounds' memrz='0' level='0' desc='Cures 1 point of damage' />
+<gift name='Detect Magic' memrz='0' level='0' desc='Detects spells and magic items within 60 ft' />
+<gift name='Detect Poison' memrz='0' level='0' desc='Detects poison in one creature or small object' />
+<gift name='Guidance' memrz='0' level='0' desc='+1 on one roll, save, or check' />
+<gift name='Inflict Minor Wounds' memrz='0' level='0' desc='Touch attack, 1 point of damage' />
+<gift name='Light' memrz='0' level='0' desc='Object shines like a torch' />
+<gift name='Mending' memrz='0' level='0' desc='Makes minor repairs on an object' />
+<gift name='Purify Food and Drink' memrz='0' level='0' desc='Purifies 1 cu. ft./level of food or water' />
+<gift name='Read Magic' memrz='0' level='0' desc='Read scrolls and spellbooks' />
+<gift name='Resistance' memrz='0' level='0' desc='Subject gains +1 on saving throws' />
+<gift name='Virtue' memrz='0' level='0' desc='Subject gains 1 temporary hp' />
+<gift name='Bane' memrz='0' level='1' desc='Enemies suffer -1 attack, -1 on saves against fear' />
+<gift name='Bless' memrz='0' level='1' desc='Allies gain +1 attack and +1 on saves against fear' />
+<gift name='Bless Water' memrz='0' level='1' desc='Makes holy water' />
+<gift name='Cause Fear' memrz='0' level='1' desc='One creature flees for 1d4 rounds' />
+<gift name='Command' memrz='0' level='1' desc='One subject obeys one-word command for 1 round' />
+<gift name='Comprehend Languages' memrz='0' level='1' desc='Understand all spoken and written languages' />
+<gift name='Cure Light Wounds' memrz='0' level='1' desc='Cures 1d8 +1/level damage (max +5)' />
+<gift name='Curse Water' memrz='0' level='1' desc='Makes unholy water' />
+<gift name='Deathwatch' memrz='0' level='1' desc='Sees how wounded subjects within 30 ft. are' />
+<gift name='Detect Chaos/Evil/Good/Law' memrz='0' level='1' desc='Reveals creatures, spells, or objects' />
+<gift name='Detect Undead' memrz='0' level='1' desc='Reveals undead within 60 ft' />
+<gift name='Divine Favor' memrz='0' level='1' desc='You gain attack, damage bonus, +1/three levels' />
+<gift name='Doom' memrz='0' level='1' desc='One subject suffers -2 on attacks, damage, saves, and checks' />
+<gift name='Endure Elements' memrz='0' level='1' desc='Ignores 5 damage/round from one energy type' />
+<gift name='Entropic Shield' memrz='0' level='1' desc='Ranged attacks against you suffer 20 percent miss chance' />
+<gift name='Inflict Light Wounds' memrz='0' level='1' desc='Touch, 1d8 +1/level damage (max +5)' />
+<gift name='Invisibility to Undead' memrz='0' level='1' desc='Undead can not perceive one subject/level' />
+<gift name='Magic Stone' memrz='0' level='1' desc='Three stones gain +1 attack, deal 1d6+1 damage' />
+<gift name='Magic Weapon' memrz='0' level='1' desc='Weapon gains +1 bonus' />
+<gift name='Obscuring Mist' memrz='0' level='1' desc='Fog surrounds you' />
+<gift name='Protection from Chaos/Evil/Good/Law' memrz='0' level='1' desc='+2 AC and saves, counter mind control, hedge out elementals/outsiders' />
+<gift name='Random Action' memrz='0' level='1' desc='One creature acts randomly for one round' />
+<gift name='Remove Fear' memrz='0' level='1' desc='+4 on saves against fear for one subject +1/four levels' />
+<gift name='Sanctuary' memrz='0' level='1' desc='Opponents can not attack you, and you can not attack' />
+<gift name='Shield of Faith' memrz='0' level='1' desc='Aura grants +2 or higher deflection bonus' />
+<gift name='Summon Monster I' memrz='0' level='1' desc='Calls outsider to fight for you' />
+<gift name='Aid' memrz='0' level='2' desc='+1 attack, +1 on saves against fear, 1d8 temporary hit points' />
+<gift name='Animal Messenger' memrz='0' level='2' desc='Sends a Tiny animal to a specific place' />
+<gift name='Augury' memrz='0' level='2' desc='Learns whether an action will be good or bad' />
+<gift name='Bulls Strength' memrz='0' level='2' desc='Subject gains 1d4+1 Str for 1 hr./level' />
+<gift name='Calm Emotions' memrz='0' level='2' desc='Calms 1d6 subjects/level, negating emotion effects' />
+<gift name='Consecrate' memrz='0' level='2' desc='Fills area with positive energy, making undead weaker' />
+<gift name='Cure Moderate Wounds' memrz='0' level='2' desc='Cures 2d8 +1/level damage (max +10)' />
+<gift name='Darkness. 20-ft' memrz='0' level='2' desc='radius of supernatural darkness' />
+<gift name='Death Knell' memrz='0' level='2' desc='Kills dying creature; you gain 1d8 temporary hp, +2 Str, and +1 level' />
+<gift name='Delay Poison' memrz='0' level='2' desc='Stops poison from harming subject for 1 hour/level' />
+<gift name='Desecrate' memrz='0' level='2' desc='Fills area with negative energy, making undead stronger' />
+<gift name='Endurance' memrz='0' level='2' desc='Gain 1d4+1 Con for 1 hr./level' />
+<gift name='Enthrall' memrz='0' level='2' desc='Captivates all within 100 ft. + 10 ft./level' />
+<gift name='Find Traps' memrz='0' level='2' desc='Notice traps as a rogue does' />
+<gift name='Gentle Repose' memrz='0' level='2' desc='Preserves one corpse' />
+<gift name='Hold Person' memrz='0' level='2' desc='Holds one person helpless; 1 round/level' />
+<gift name='Inflict Moderate Wounds' memrz='0' level='2' desc='Touch attack, 2d8 +1/level damage (max +10)' />
+<gift name='Lesser Restoration' memrz='0' level='2' desc='Dispels magic ability penalty or repairs 1d4 ability damage' />
+<gift name='Make Whole' memrz='0' level='2' desc='Repairs an object' />
+<gift name='Remove Paralysis' memrz='0' level='2' desc='Frees one or more creatures from paralysis, hold, or slow' />
+<gift name='Resist Elements' memrz='0' level='2' desc='Ignores 12 damage/round from one energy type' />
+<gift name='Shatter' memrz='0' level='2' desc='Sonic vibration damages objects or crystalline creatures' />
+<gift name='Shield Other' memrz='0' level='2' desc='You take half of subjects damage' />
+<gift name='Silence' memrz='0' level='2' desc='Negates sound in 15-ft. radius' />
+<gift name='Sound Burst' memrz='0' level='2' desc='Deals 1d8 sonic damage to subjects; may stun them' />
+<gift name='Speak with Animals' memrz='0' level='2' desc='You can communicate with natural animals' />
+<gift name='Spiritual Weapon' memrz='0' level='2' desc='Magical weapon attacks on its own' />
+<gift name='Summon Monster II' memrz='0' level='2' desc='Calls outsider to fight for you' />
+<gift name='Undetectable Alignment' memrz='0' level='2' desc='Conceals alignment for 24 hours' />
+<gift name='Zone of Truth' memrz='0' level='2' desc='Subjects within range cannot lie' />
+<gift name='Animate Dead' memrz='0' level='3' desc='Creates undead skeletons and zombies' />
+<gift name='Bestow Curse' memrz='0' level='3' desc='-6 to an ability; -4 on attacks/saves/checks; or 50 percent chance of losing each action' />
+<gift name='Blindness/Deafness' memrz='0' level='3' desc='Makes subject blind or deaf' />
+<gift name='Contagion' memrz='0' level='3' desc='Infects subject with chosen disease' />
+<gift name='Continual Flame' memrz='0' level='3' desc='Makes a permanent, heatless torch' />
+<gift name='Create Food and Water' memrz='0' level='3' desc='Feeds three humans (or one horse)/level' />
+<gift name='Cure Serious Wounds' memrz='0' level='3' desc='Cures 3d8 +1/level damage (max +15)' />
+<gift name='Daylight' memrz='0' level='3' desc='60-ft. radius of bright light' />
+<gift name='Deeper Darkness' memrz='0' level='3' desc='Object sheds absolute darkness in 60-ft. radius' />
+<gift name='Dispel Magic' memrz='0' level='3' desc='Cancels magical spells and effects' />
+<gift name='Glyph of Warding' memrz='0' level='3' desc='Inscription harms those who pass it' />
+<gift name='Helping Hand' memrz='0' level='3' desc='Ghostly hand leads subject to you' />
+<gift name='Inflict Serious Wounds' memrz='0' level='3' desc='Touch attack, 3d8 +1/level damage (max +15)' />
+<gift name='Invisibility Purge' memrz='0' level='3' desc='Dispels invisibility within 5 ft./level' />
+<gift name='Locate Object' memrz='0' level='3' desc='Senses direction toward object (specific or type)' />
+<gift name='Magic Circle against Chaos/Evil/Good/Law' memrz='0' level='3' desc='As protection spells, but 10-ft. radius and 10min./level' />
+<gift name='Magic Vestment' memrz='0' level='3' desc='Armor or shield gains +1 enhancement/three levels' />
+<gift name='Meld into Stone' memrz='0' level='3' desc='You and your gear merge with stone' />
+<gift name='Negative Energy Protection' memrz='0' level='3' desc='Subject resists level and ability drains' />
+<gift name='Obscure Object' memrz='0' level='3' desc='Masks object against divination' />
+<gift name='Prayer' memrz='0' level='3' desc='Allies gain +1 on most rolls, and enemies suffer -1' />
+<gift name='Protection from Elements' memrz='0' level='3' desc='Absorb 12 damage/level from one kind of energy' />
+<gift name='Remove Blindness/Deafness' memrz='0' level='3' desc='Cures normal or magical conditions' />
+<gift name='Remove Curse' memrz='0' level='3' desc='Frees object or person from curse' />
+<gift name='Remove Disease' memrz='0' level='3' desc='Cures all diseases affecting subject' />
+<gift name='Searing Light' memrz='0' level='3' desc='Ray deals 1d8/two levels, more against undead' />
+<gift name='Speak with Dead' memrz='0' level='3' desc='Corpse answers one question/two levels' />
+<gift name='Speak with Plants' memrz='0' level='3' desc='You can talk to normal plants and plant creatures' />
+<gift name='Stone Shape' memrz='0' level='3' desc='Sculpts stone into any form' />
+<gift name='Summon Monster III' memrz='0' level='3' desc='Calls outsider to fight for you' />
+<gift name='Water Breathing' memrz='0' level='3' desc='Subjects can breathe underwater' />
+<gift name='Water Walk' memrz='0' level='3' desc='Subject treads on water as if solid' />
+<gift name='Wind Wall' memrz='0' level='3' desc='Deflects arrows, smaller creatures, and gases' />
+<gift name='Air Walk' memrz='0' level='4' desc='Subject treads on air as if solid (climb at 45-degree angle)' />
+<gift name='Control Water' memrz='0' level='4' desc='Raises, lowers, or parts bodies of water' />
+<gift name='Cure Critical Wounds' memrz='0' level='4' desc='Cures 4d8 +1/level damage (max +20)' />
+<gift name='Death Ward' memrz='0' level='4' desc='Grants immunity to death spells and effects' />
+<gift name='Dimensional Anchor' memrz='0' level='4' desc='Bars extradimensional movement' />
+<gift name='Discern Lies' memrz='0' level='4' desc='Reveals deliberate falsehoods' />
+<gift name='Dismissal' memrz='0' level='4' desc='Forces a creature to return to native plane' />
+<gift name='Divination' memrz='0' level='4' desc='Provides useful advice for specific proposed actions' />
+<gift name='Divine Power' memrz='0' level='4' desc='You gain attack bonus, 18 Str, and 1 hp/level' />
+<gift name='Freedom of Movement' memrz='0' level='4' desc='Subject moves normally despite impediments' />
+<gift name='Giant Vermin' memrz='0' level='4' desc='Turns insects into giant vermin' />
+<gift name='Greater Magic Weapon' memrz='0' level='4' desc='+1 bonus/three levels (max +5)' />
+<gift name='Imbue with Spell Ability' memrz='0' level='4' desc='Transfer spells to subject' />
+<gift name='Inflict Critical Wounds' memrz='0' level='4' desc='Touch attack, 4d8 +1/level damage (max +20)' />
+<gift name='Lesser Planar Ally' memrz='0' level='4' desc='Exchange services with an 8 HD outsider' />
+<gift name='Neutralize Poison' memrz='0' level='4' desc='Detoxifies venom in or on subject' />
+<gift name='Poison' memrz='0' level='4' desc='Touch deals 1d10 Con damage, repeats in 1 min' />
+<gift name='Repel Vermin' memrz='0' level='4' desc='Insects stay 10 ft. away' />
+<gift name='Restoration' memrz='0' level='4' desc='Restores level and ability score drains' />
+<gift name='Sending' memrz='0' level='4' desc='Delivers short message anywhere, instantly' />
+<gift name='Spell Immunity' memrz='0' level='4' desc='Subject is immune to one spell/four levels' />
+<gift name='Status' memrz='0' level='4' desc='Monitors condition, position of allies' />
+<gift name='Summon Monster IV' memrz='0' level='4' desc='Calls outsider to fight for you' />
+<gift name='Tongues' memrz='0' level='4' desc='Speak any language' />
+<gift name='Atonement' memrz='0' level='5' desc='Removes burden of misdeeds from subject' />
+<gift name='Break Enchantment' memrz='0' level='5' desc='Frees subjects from enchantments, alterations, curses, and petrification' />
+<gift name='Circle of Doom' memrz='0' level='5' desc='Deals 1d8 +1/level damage in all directions' />
+<gift name='Commune' memrz='0' level='5' desc='Deity answers one yes-or-no question/level' />
+<gift name='Dispel Chaos/Evil/Good/Law' memrz='0' level='5' desc='+4 bonus against attacks' />
+<gift name='Ethereal Jaunt' memrz='0' level='5' desc='You become ethereal for 1 round/level' />
+<gift name='Flame Strike' memrz='0' level='5' desc='Smites foes with divine fire (1d6/level)' />
+<gift name='Greater Command' memrz='0' level='5' desc='As command, but affects one subject/level' />
+<gift name='Hallow' memrz='0' level='5' desc='Designates location as holy' />
+<gift name='Healing Circle' memrz='0' level='5' desc='Cures 1d8 +1/level damage in all directions' />
+<gift name='Insect Plague' memrz='0' level='5' desc='Insect horde limits vision, inflicts damage, and weak creatures flee' />
+<gift name='Mark of Justice' memrz='0' level='5' desc='Designates action that will trigger curse on subject' />
+<gift name='Plane Shift' memrz='0' level='5' desc='Up to eight subjects travel to another plane' />
+<gift name='Raise Dead' memrz='0' level='5' desc='Restores life to subject who died up to 1 day/level ago' />
+<gift name='Righteous Might' memrz='0' level='5' desc='Your size increases, and you gain +4 Str' />
+<gift name='Scrying' memrz='0' level='5' desc='Spies on subject from a distance' />
+<gift name='Slay Living' memrz='0' level='5' desc='Touch attack kills subject' />
+<gift name='Spell Resistance' memrz='0' level='5' desc='Subject gains +12 +1/level SR' />
+<gift name='Summon Monster V' memrz='0' level='5' desc='Calls outsider to fight for you' />
+<gift name='True Seeing' memrz='0' level='5' desc='See all things as they really are' />
+<gift name='Unhallow' memrz='0' level='5' desc='Designates location as unholy' />
+<gift name='Wall of Stone' memrz='0' level='5' desc='20 hp/four levels; can be shaped' />
+<gift name='Animate Objects' memrz='0' level='6' desc='Objects attack your foes' />
+<gift name='Antilife Shell' memrz='0' level='6' desc='10-ft. field hedges out living creatures' />
+<gift name='Banishment' memrz='0' level='6' desc='Banishes 2 HD/level extraplanar creatures' />
+<gift name='Blade Barrier' memrz='0' level='6' desc='Blades encircling you deal 1d6 damage/level' />
+<gift name='Create Undead' memrz='0' level='6' desc='Ghouls, shadows, ghasts, wights, or wraiths' />
+<gift name='Etherealness' memrz='0' level='6' desc='Travel to Ethereal Plane with companions' />
+<gift name='Find the Path' memrz='0' level='6' desc='Shows most direct way to a location' />
+<gift name='Forbiddance' memrz='0' level='6' desc='Denies area to creatures of another alignment' />
+<gift name='Geas/Quest' memrz='0' level='6' desc='As lesser geas, plus it affects any creature' />
+<gift name='Greater Dispelling' memrz='0' level='6' desc='As dispel magic, but up to +20 on check' />
+<gift name='Greater Glyph of Warding' memrz='0' level='6' desc='As glyph of warding, but up to 10d8 damage or 6th-level spell' />
+<gift name='Harm' memrz='0' level='6' desc='Subject loses all but 1d4 hp' />
+<gift name='Heal' memrz='0' level='6' desc='Cures all damage, diseases, and mental conditions' />
+<gift name='Heroes Feast' memrz='0' level='6' desc='Food for one creature/level cures and blesses' />
+<gift name='Planar Ally' memrz='0' level='6' desc='As lesser planar ally, but up to 16 HD' />
+<gift name='Summon Monster VI' memrz='0' level='6' desc='Calls outsider to fight for you' />
+<gift name='Wind Walk' memrz='0' level='6' desc='You and your allies turn vaporous and travel fast' />
+<gift name='Word of Recall' memrz='0' level='6' desc='Teleports you back to designated place' />
+<gift name='Blasphemy' memrz='0' level='7' desc='Kills, paralyzes, weakens, or dazes nonevil subjects' />
+<gift name='Control Weather' memrz='0' level='7' desc='Changes weather in local area' />
+<gift name='Destruction' memrz='0' level='7' desc='Kills subject and destroys remains' />
+<gift name='Dictum' memrz='0' level='7' desc='Kills, paralyzes, weakens, or dazes nonlawful subjects' />
+<gift name='Greater Restoration' memrz='0' level='7' desc='As restoration, plus restores all levels and ability scores' />
+<gift name='Greater Scrying' memrz='0' level='7' desc='As scrying, but faster and longer' />
+<gift name='Holy Word' memrz='0' level='7' desc='Kills, paralyzes, weakens, or dazes nongood subjects' />
+<gift name='Refuge' memrz='0' level='7' desc='Alters item to transport its possessor to you' />
+<gift name='Regenerate' memrz='0' level='7' desc='Subjects severed limbs grow back' />
+<gift name='Repulsion' memrz='0' level='7' desc='Creatures can not approach you' />
+<gift name='Resurrection' memrz='0' level='7' desc='Fully restore dead subject' />
+<gift name='Summon Monster VII' memrz='0' level='7' desc='Calls outsider to fight for you' />
+<gift name='Word of Chaos' memrz='0' level='7' desc='Kills, confuses, stuns, or deafens nonchaotic subjects' />
+<gift name='Antimagic Field' memrz='0' level='8' desc='Negates magic within 10 ft' />
+<gift name='Cloak of Chaos' memrz='0' level='8' desc='+4 AC, +4 resistance, and SR 25 against lawful spells' />
+<gift name='Create Greater Undead' memrz='0' level='8' desc='Mummies, spectres, vampires, or ghosts' />
+<gift name='Discern Location' memrz='0' level='8' desc='Exact location of creature or object' />
+<gift name='Earthquake' memrz='0' level='8' desc='Intense tremor shakes 5-ft./level radius' />
+<gift name='Fire Storm' memrz='0' level='8' desc='Deals 1d6 fire damage/level' />
+<gift name='Greater Planar Ally' memrz='0' level='8' desc='As lesser planar ally, but up to 24 HD' />
+<gift name='Holy Aura' memrz='0' level='8' desc='+4 AC, +4 resistance, and SR 25 against evil spells' />
+<gift name='Mass Heal' memrz='0' level='8' desc='As heal, but with several subjects' />
+<gift name='Shield of Law' memrz='0' level='8' desc='+4 AC, +4 resistance, and SR 25 against chaotic spells' />
+<gift name='Summon Monster VIII' memrz='0' level='8' desc='Calls outsider to fight for you' />
+<gift name='Symbol' memrz='0' level='8' desc='Triggered runes have array of effects' />
+<gift name='Unholy Aura' memrz='0' level='8' desc='+4 AC, +4 resistance, and SR 25 against good spells' />
+<gift name='Astral Projection' memrz='0' level='9' desc='Projects you and companions into Astral Plane' />
+<gift name='Energy Drain' memrz='0' level='9' desc='Subject gains 2d4 negative levels' />
+<gift name='Gate' memrz='0' level='9' desc='Connects two planes for travel or summoning' />
+<gift name='Implosion' memrz='0' level='9' desc='Kills one creature/round' />
+<gift name='Miracle' memrz='0' level='9' desc='Requests a deitys intercession' />
+<gift name='Soul Bind' memrz='0' level='9' desc='Traps newly dead soul to prevent resurrection' />
+<gift name='Storm of Vengeance' memrz='0' level='9' desc='Storm rains acid, lightning, and hail' />
+<gift name='Summon Monster IX' memrz='0' level='9' desc='Calls outsider to fight for you' />
+<gift name='True Resurrection' memrz='0' level='9' desc='As resurrection, plus remains are not needed' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Air Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Obscuring Mist' memrz='0' level='1' desc='Fog surrounds you' />
+<gift name='Wind Wall' memrz='0' level='2' desc='Deflects arrows, smaller creatures, and gases' />
+<gift name='Gaseous Form' memrz='0' level='3' desc='Subject becomes insubstantial and can fly slowly' />
+<gift name='Air Walk' memrz='0' level='4' desc='Subject treads on air as if solid (climb at 45-degree angle)' />
+<gift name='Control Winds' memrz='0' level='5' desc='Change wind direction and speed' />
+<gift name='Chain Lightning' memrz='0' level='6' desc='1d6 damage/level; secondary bolts' />
+<gift name='Control Weather' memrz='0' level='7' desc='Changes weather in local area' />
+<gift name='Whirlwind' memrz='0' level='8' desc='Cyclone inflicts damage and can pick up creatures' />
+<gift name='Elemental Swarm' memrz='0' level='9' desc='(Air spell only) Summons 2d4 Large, 1d4 Huge elementals' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Animal Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Calm Animals' memrz='0' level='1' desc='Calms 2d4 +1/level HD of animals, beasts, and magical beasts' />
+<gift name='Hold Animal' memrz='0' level='2' desc='Hold one animal helpless; 1 round/level' />
+<gift name='Dominate Animal' memrz='0' level='3' desc='Subject animal obeys silent mental commands' />
+<gift name='Repel Vermin' memrz='0' level='4' desc='Insects stay 10 ft. away' />
+<gift name='Commune with Nature' memrz='0' level='5' desc='Learn about terrain for one mile/level' />
+<gift name='Antilife Shell' memrz='0' level='6' desc='10-ft. field hedges out living creatures' />
+<gift name='Animal Shapes' memrz='0' level='7' desc='One ally/level polymorphs into chosen animal' />
+<gift name='Creeping Doom' memrz='0' level='8' desc='Carpet of insects attacks at your command' />
+<gift name='Shapechange' memrz='0' level='9' desc='Transforms you into any creature, and change forms once per round' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Chaos Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Protection from Law' memrz='0' level='1' desc='+2 AC and saves, counter mind control, hedge out elementals and outsiders' />
+<gift name='Shatter' memrz='0' level='2' desc='Sonic vibration damages objects or crystalline creatures' />
+<gift name='Magic Circle against Law' memrz='0' level='3' desc='As protection spells, but 10-ft. radius and 10 min./level' />
+<gift name='Chaos Hammer' memrz='0' level='4' desc='Damages and staggers lawful creatures' />
+<gift name='Dispel Law' memrz='0' level='5' desc='+4 bonus against attacks by lawful creatures' />
+<gift name='Animate Objects' memrz='0' level='6' desc='Objects attack your foes' />
+<gift name='Word of Chaos' memrz='0' level='7' desc='Kills, confuses, stuns, or deafens nonchaotic subjects' />
+<gift name='Cloak of Chaos' memrz='0' level='8' desc='+4 AC, +4 resistance, SR 25 against lawful spells' />
+<gift name='Summon Monster IX' memrz='0' level='9' desc='(Chaos spell only) Calls outsider to fight for you' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Death Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Cause Fear' memrz='0' level='1' desc='One creature flees for 1d4 rounds' />
+<gift name='Death Knell' memrz='0' level='2' desc='Kill dying creature and gain 1d8 temp. hp, +2 Str, and +1 caster level' />
+<gift name='Animate Dead' memrz='0' level='3' desc='Creates undead skeletons and zombies' />
+<gift name='Death Ward' memrz='0' level='4' desc='Grants immunity to death spells and effects' />
+<gift name='Slay Living' memrz='0' level='5' desc='Touch attack kills subject' />
+<gift name='Create Undead' memrz='0' level='6' desc='Ghouls, shadows, ghasts, wights, or wraiths' />
+<gift name='Destruction' memrz='0' level='7' desc='Kills subject and destroys remains' />
+<gift name='Create Greater Undead' memrz='0' level='8' desc='Mummies, spectres, vampires, or ghosts' />
+<gift name='Wail of the Banshee' memrz='0' level='9' desc='Kills one creature/level' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Destruction Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Inflict Light Wounds' memrz='0' level='1' desc='Touch attack, 1d8 +1/level damage (max +5)' />
+<gift name='Shatter' memrz='0' level='2' desc='Sonic vibration damages objects or crystalline creatures' />
+<gift name='Contagion' memrz='0' level='3' desc='Infects subject with chosen disease' />
+<gift name='Inflict Critical Wounds' memrz='0' level='4' desc='Touch attack, 4d8 +1/level damage (max +20)' />
+<gift name='Circle of Doom' memrz='0' level='5' desc='Deals 1d8 +1/level damage in all directions' />
+<gift name='Harm' memrz='0' level='6' desc='Subject loses all but 1d4 hp' />
+<gift name='Disintegrate' memrz='0' level='7' desc='Makes one creature or object vanish' />
+<gift name='Earthquake' memrz='0' level='8' desc='Intense tremor shakes 5-ft./level radius' />
+<gift name='Implosion' memrz='0' level='9' desc='Kills one creature/round' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Earth Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Magic Stone' memrz='0' level='1' desc='Three stones become +1 projectiles, 1d6+1 damage' />
+<gift name='Soften Earth and Stone' memrz='0' level='2' desc='Turns stone to clay or dirt to sand or mud' />
+<gift name='Stone Shape' memrz='0' level='3' desc='Sculpts stone into any form' />
+<gift name='Spike Stones' memrz='0' level='4' desc='Creatures in area take 1d8 damage, may be slowed' />
+<gift name='Wall of Stone' memrz='0' level='5' desc='20 hp/four levels; can be shaped' />
+<gift name='Stoneskin' memrz='0' level='6' desc='Stops blows, cuts, stabs, and slashes' />
+<gift name='Earthquake' memrz='0' level='7' desc='Intense tremor shakes 5-ft./level radius' />
+<gift name='Iron Body' memrz='0' level='8' desc='Your body becomes living iron' />
+<gift name='Elemental Swarm' memrz='0' level='9' desc='(Earth spell only) Summons 2d4 Large, 1d4 Huge elementals' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Evil Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Protection from Good' memrz='0' level='1' desc='+2 AC and saves, counter mind control, hedge out elementals and outsiders' />
+<gift name='Desecrate' memrz='0' level='2' desc='Fills area with negative energy, making undead stronger' />
+<gift name='Magic Circle against Good' memrz='0' level='3' desc='As protection spells, but 10-ft. radius and 10 min./level' />
+<gift name='Unholy Blight' memrz='0' level='4' desc='Damages and sickens good creatures' />
+<gift name='Dispel Good' memrz='0' level='5' desc='+4 bonus against attacks by good creatures' />
+<gift name='Create Undead' memrz='0' level='6' desc='Ghouls, shadows, ghasts, wights, or wraiths' />
+<gift name='Blasphemy' memrz='0' level='7' desc='Kills, paralyzes, weakens, or dazes nonevil subjects' />
+<gift name='Unholy Aura' memrz='0' level='8' desc='+4 AC, +4 resistance, SR 25 against good spells' />
+<gift name='Summon Monster IX' memrz='0' level='9' desc='(Evil spell only) Calls outsider to fight for you' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Fire Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Burning Hands' memrz='0' level='1' desc='1d4 fire damage/level (max 5d4)' />
+<gift name='Produce Flame' memrz='0' level='2' desc='1d4 +1/two levels damage, touch or thrown' />
+<gift name='Resist Elements' memrz='0' level='3' desc='Ignore first 12 damage from one energy type (cold or fire only) each round' />
+<gift name='Wall of Fire' memrz='0' level='4' desc='Deals 2d4 fire damage out to 10 ft. and 1d4 out to 20 ft. Passing through wall deals 2d6 +1/level' />
+<gift name='Fire Shield' memrz='0' level='5' desc='Creatures attacking you take fire damage; you are protected from heat or cold' />
+<gift name='Fire Seeds' memrz='0' level='6' desc='Acorns and berries become grenades and bombs' />
+<gift name='Fire Storm' memrz='0' level='7' desc='Deals 1d6 fire damage/level' />
+<gift name='Incendiary Cloud' memrz='0' level='8' desc='Cloud deals 4d6 fire damage/round' />
+<gift name='Elemental Swarm' memrz='0' level='9' desc='(Fire spell only) Summons 2d4 Large, 1d4 Huge elementals' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Good Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Protection from Evil' memrz='0' level='1' desc='+2 AC and saves, counter mind control, hedge out elementals and outsiders' />
+<gift name='Aid' memrz='0' level='2' desc='+1 attack, +1 on saves against fear, 1d6 temporary hit points' />
+<gift name='Magic Circle against Evil' memrz='0' level='3' desc='As protection spells, but 10-ft. radius and 10 min./level' />
+<gift name='Holy Smite' memrz='0' level='4' desc='Damages and blinds evil creatures' />
+<gift name='Dispel Evil' memrz='0' level='5' desc='+4 bonus against attacks by evil creatures' />
+<gift name='Blade Barrier' memrz='0' level='6' desc='Blades encircling you deal 1d6 damage/level' />
+<gift name='Holy Word' memrz='0' level='7' desc='Kills, paralyzes, weakens, or dazes nongood subjects' />
+<gift name='Holy Aura' memrz='0' level='8' desc='+4 AC, +4 resistance, and SR 25 against evil spells' />
+<gift name='Summon Monster IX' memrz='0' level='9' desc='(Good spell only) Calls outsider to fight for you' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Healing Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Cure Light Wounds' memrz='0' level='1' desc='Cures 1d8 +1/level damage (max +5)' />
+<gift name='Cure Moderate Wounds' memrz='0' level='2' desc='Cures 2d8 +1/level damage (max +10)' />
+<gift name='Cure Serious Wounds' memrz='0' level='3' desc='Cures 3d8 +1/level damage (max +15)' />
+<gift name='Cure Critical Wounds' memrz='0' level='4' desc='Cures 4d8 +1/level damage (max +20)' />
+<gift name='Healing Circle' memrz='0' level='5' desc='Cures 1d8 +1/level damage in all directions' />
+<gift name='Heal' memrz='0' level='6' desc='Cures all damage, diseases, and mental conditions' />
+<gift name='Regenerate' memrz='0' level='7' desc='Subjects severed limbs grow back' />
+<gift name='Mass Heal' memrz='0' level='8' desc='As heal, but with several subjects' />
+<gift name='True Resurrection' memrz='0' level='9' desc='As resurrection, plus remains are not needed' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Knowledge Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Detect Secret Doors' memrz='0' level='1' desc='Reveals hidden doors within 60 ft' />
+<gift name='Detect Thoughts' memrz='0' level='2' desc='Allows listening to surface thoughts' />
+<gift name='Clairaudience/Clairvoyance' memrz='0' level='3' desc='Hear or see at a distance for 1 min./level' />
+<gift name='Divination' memrz='0' level='4' desc='Provides useful advice on to specific proposed actions' />
+<gift name='True Seeing' memrz='0' level='5' desc='See all things as they really are' />
+<gift name='Find the Path' memrz='0' level='6' desc='Shows most direct way to a location' />
+<gift name='Legend Lore' memrz='0' level='7' desc='Learn tales about a person, place, or thing' />
+<gift name='Discern Location' memrz='0' level='8' desc='Exact location of creature or object' />
+<gift name='Foresight' memrz='0' level='9' desc='6th sense warns of impending danger' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Law Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Protection from Chaos' memrz='0' level='1' desc='+2 AC and saves, counter mind control, hedge out elementals and outsiders' />
+<gift name='Calm Emotions' memrz='0' level='2' desc='Calms 1d6 creatures/level, negating emotion effects' />
+<gift name='Magic Circle against Chaos' memrz='0' level='3' desc='As protection spells, but 10-ft. radius and 10 min./level' />
+<gift name='Orders Wrath' memrz='0' level='4' desc='Damages and dazes chaotic creatures' />
+<gift name='Dispel Chaos' memrz='0' level='5' desc='+4 bonus against attacks by chaotic creatures' />
+<gift name='Hold Monster' memrz='0' level='6' desc='As hold person, but any creature' />
+<gift name='Dictum' memrz='0' level='7' desc='Kills, paralyzes, weakens, or dazes nonlawful subjects' />
+<gift name='Shield of Law' memrz='0' level='8' desc='+4 AC, +4 resistance, and SR 25 against chaotic spells' />
+<gift name='Summon Monster IX' memrz='0' level='9' desc='(Law spell only) Calls outsider to fight for you' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Luck Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Entropic Shield' memrz='0' level='1' desc='Ranged attacks against you suffer 20% miss chance' />
+<gift name='Aid' memrz='0' level='2' desc='+1 attack, +1 against fear, 1d8 temporary hit points' />
+<gift name='Protection from Elements' memrz='0' level='3' desc='Absorb 12 damage/level from one kind of energy' />
+<gift name='Freedom of Movement' memrz='0' level='4' desc='Subject moves normally despite impediments' />
+<gift name='Break Enchantment' memrz='0' level='5' desc='Frees subjects from enchantments, alterations, curses, and petrification' />
+<gift name='Mislead' memrz='0' level='6' desc='Turns you invisible and creates illusory double' />
+<gift name='Spell Turning' memrz='0' level='7' desc='Reflect 1d4+6 spell levels back at caster' />
+<gift name='Holy Aura' memrz='0' level='8' desc='+4 AC, +4 resistance, and SR 25 against evil spells' />
+<gift name='Miracle' memrz='0' level='9' desc='Requests a deitys intercession' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Magic Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Nystuls Undetectable Aura' memrz='0' level='1' desc='Masks magic items aura' />
+<gift name='Identify' memrz='0' level='2' desc='Determines single feature of magic item' />
+<gift name='Dispel Magic' memrz='0' level='3' desc='Cancels magical spells and effects' />
+<gift name='Imbue with Spell Ability' memrz='0' level='4' desc='Transfer spells to subject' />
+<gift name='Spell Resistance' memrz='0' level='5' desc='Subject gains +12 +1/level SR' />
+<gift name='Antimagic Field' memrz='0' level='6' desc='Negates magic within 10 ft' />
+<gift name='Spell Turning' memrz='0' level='7' desc='Reflect 1d4+6 spell levels back at caster' />
+<gift name='Protection from Spells' memrz='0' level='8' desc='Confers +8 resistance bonus' />
+<gift name='Mordenkainens Disjunction' memrz='0' level='9' desc='Dispels magic, disenchants magic items' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Plant Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Entangle' memrz='0' level='1' desc='Plants entangle everyone in 40-ft.-radius circle' />
+<gift name='Barkskin' memrz='0' level='2' desc='Grants +3 natural armor bonus (or higher)' />
+<gift name='Plant Growth' memrz='0' level='3' desc='Grows vegetation, improves crops' />
+<gift name='Control Plants' memrz='0' level='4' desc='Talk to and control plants and fungi' />
+<gift name='Wall of Thorns' memrz='0' level='5' desc='Thorns damage anyone who tries to pass' />
+<gift name='Repel Wood' memrz='0' level='6' desc='Pushes away wooden objects' />
+<gift name='Changestaff' memrz='0' level='7' desc='Your staff becomes a treant on command' />
+<gift name='Command Plants' memrz='0' level='8' desc='Plants animate and vegetation entangles' />
+<gift name='Shambler' memrz='0' level='9' desc='Summons 1d4+2 shambling mounds to fight for you' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Protection Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Sanctuary' memrz='0' level='1' desc='Opponents cannot attack you, and you cannot attack' />
+<gift name='Shield Other' memrz='0' level='2' desc='You take half of subjects damage' />
+<gift name='Protection from Elements' memrz='0' level='3' desc='Absorb 12 damage/level from one kind of energy' />
+<gift name='Spell Immunity' memrz='0' level='4' desc='Subject is immune to one spell/four levels' />
+<gift name='Spell Resistance' memrz='0' level='5' desc='Subject gains +12 +1/level SR' />
+<gift name='Antimagic Field' memrz='0' level='6' desc='Negates magic within 10 ft' />
+<gift name='Repulsion' memrz='0' level='7' desc='Creatures cannot approach you' />
+<gift name='Mind Blank' memrz='0' level='8' desc='Subject is immune to mental/emotional magic and scrying' />
+<gift name='Prismatic Sphere' memrz='0' level='9' desc='As prismatic wall, but surrounds on all sides' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Strength Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Endure Elements' memrz='0' level='1' desc='Ignores 5 damage/round from one energy type' />
+<gift name='Bulls Strength' memrz='0' level='2' desc='Subject gains 1d4+1 Str for 1 hr./level' />
+<gift name='Magic Vestment' memrz='0' level='3' desc='Armor or shield gains +1 enhancement/three levels' />
+<gift name='Spell Immunity' memrz='0' level='4' desc='Subject is immune to one spell/four levels' />
+<gift name='Righteous Might' memrz='0' level='5' desc='Your size increases, and you gain +4 Str' />
+<gift name='Stoneskin' memrz='0' level='6' desc='Stops blows, cuts, stabs, and slashes' />
+<gift name='Bigbys Grasping Hand' memrz='0' level='7' desc='Hand provides cover, pushes, or grapples' />
+<gift name='Bigbys Clenched Fist' memrz='0' level='8' desc='Large hand attacks your foes' />
+<gift name='Bigbys Crushing Hand' memrz='0' level='9' desc='As Bigbys grasping hand, but stronger' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Sun Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Endure Elements' memrz='0' level='1' desc='Ignores 5 damage/round from one energy type (cold or fire only)' />
+<gift name='Heat Metal' memrz='0' level='2' desc='Make metal so hot it damages those that touch it' />
+<gift name='Searing Light' memrz='0' level='3' desc='Ray deals 1d8/two levels, more against undead' />
+<gift name='Fire Shield' memrz='0' level='4' desc='Creatures attacking you take fire damage; you are protected from heat or cold' />
+<gift name='Flame Strike' memrz='0' level='5' desc='Smite foes with divine fire (1d8/level)' />
+<gift name='Fire Seeds' memrz='0' level='6' desc='Acorns and berries become grenades and bombs' />
+<gift name='Sunbeam' memrz='0' level='7' desc='Beam blinds and deals 3d6 damage' />
+<gift name='Sunburst' memrz='0' level='8' desc='Blinds all within 10 ft., deals 3d6 damage' />
+<gift name='Prismatic Sphere' memrz='0' level='9' desc='As prismatic wall, but surrounds on all sides' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Travel Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Expeditious Retreat' memrz='0' level='1' desc='Doubles your speed' />
+<gift name='Locate Object' memrz='0' level='2' desc='Senses direction toward object (specific or type)' />
+<gift name='Fly' memrz='0' level='3' desc='Subject flies at speed of 90' />
+<gift name='Dimension Door' memrz='0' level='4' desc='Teleports you and up to 500 lb' />
+<gift name='Teleport' memrz='0' level='5' desc='Instantly transports you anywhere' />
+<gift name='Find the Path' memrz='0' level='6' desc='Shows most direct way to a location' />
+<gift name='Teleport without Error' memrz='0' level='7' desc='As teleport, but no off-target arrival' />
+<gift name='Phase Door' memrz='0' level='8' desc='Invisible passage through wood or stone' />
+<gift name='Astral Projection' memrz='0' level='9' desc='Projects you and companions into Astral Plane' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Trickery Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Change Self' memrz='0' level='1' desc='Change own appearance' />
+<gift name='Invisibility' memrz='0' level='2' desc='Subject invisible 10 min./level or until it attacks' />
+<gift name='Nondetection' memrz='0' level='3' desc='Hides subject from divination, scrying' />
+<gift name='Confusion' memrz='0' level='4' desc='Makes subjects behave oddly for 1 round/level' />
+<gift name='False Vision' memrz='0' level='5' desc='Fools scrying with an illusion' />
+<gift name='Mislead' memrz='0' level='6' desc='Turns you invisible and creates illusory double' />
+<gift name='Screen' memrz='0' level='7' desc='Illusion hides area from vision, scrying' />
+<gift name='Polymorph Any Object' memrz='0' level='8' desc='Changes any subject into anything else' />
+<gift name='Time Stop' memrz='0' level='9' desc='You act freely for 1d4+1 rounds' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      War Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Magic Weapon' memrz='0' level='1' desc='Weapon gains +1 bonus' />
+<gift name='Spiritual Weapon' memrz='0' level='2' desc='Magical weapon attacks on its own' />
+<gift name='Magic Vestment' memrz='0' level='3' desc='Armor or shield gains +1 enhancement/three levels' />
+<gift name='Divine Power' memrz='0' level='4' desc='You gain attack bonus, 18 Str, and 1 hp/level' />
+<gift name='Flame Strike' memrz='0' level='5' desc='Smite foes with divine fire (1d6 damage/level)' />
+<gift name='Blade Barrier' memrz='0' level='6' desc='Blades encircling you deal 1d6 damage/level' />
+<gift name='Power Word, Stun' memrz='0' level='7' desc='Stuns creature with up to 150 hp' />
+<gift name='Power Word, Blind' memrz='0' level='8' desc='Blinds 200 hp worth of creatures' />
+<gift name='Power Word, Kill' memrz='0' level='9' desc='Kills one tough subject or many weak ones' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name='      Water Domain Spells' memrz='0' level=' ' desc=' ' />
+<gift name='============================' memrz='0' level=' ' desc=' ' />
+<gift name=' ' memrz='0' level=' ' desc=' ' />
+<gift name='Obscuring Mist' memrz='0' level='1' desc='Fog surrounds you' />
+<gift name='Fog Cloud' memrz='0' level='2' desc='Fog obscures vision' />
+<gift name='Water Breathing' memrz='0' level='3' desc='Subjects can breathe underwater' />
+<gift name='Control Water' memrz='0' level='4' desc='Raise, lower, or part bodies of water' />
+<gift name='Ice Storm' memrz='0' level='5' desc='Hail deals 5d6 damage in cylinder 40 ft. across' />
+<gift name='Cone of Cold' memrz='0' level='6' desc='1d6 cold damage/level' />
+<gift name='Acid Fog' memrz='0' level='7' desc='Fog deals 2d6/round acid damage for 1round/level' />
+<gift name='Horrid Wilting' memrz='0' level='8' desc='Deals 1d8 damage/level within 30 ft' />
+<gift name='Elemental Swarm' memrz='0' level='9' desc='(Water spell only) Summons 2d4 Large, 1d4 Huge elementals' />
+</divine>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/dnd3efeats.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,558 @@
+<feats>
+     <feat name="A Thousand Furs" type="Special" desc="Touched by the Gods page 71"/>
+     <feat name="Acrobatic" type="General" desc="Song and Silence page 38"/>
+     <feat name="Acrobatic (Legends and Lairs version)" type="General" desc="Traps and Treachery page 34"/>
+     <feat name="Airy Gallop" type="Special" desc="Dragon Magazine d20 Special-Annual 6 page 65"/>
+     <feat name="Alertness" type="General" desc="Players Handbook page 80"/>
+     <feat name="Alluring" type="General" desc="Song and Silence page 38"/>
+     <feat name="Ambidexterity" type="General" desc="Players Handbook page 80"/>
+     <feat name="Ancient Lineage" type="General" desc="The Taan page 81"/>
+     <feat name="Arcane Defense" type="General" desc="Tome and Blood page 38"/>
+     <feat name="Arcane Preparation" type="General" desc="Tome and Blood page 38"/>
+     <feat name="Arcane Schooling" type="General" desc="Forgotten Realms Campaign Setting page 33"/>
+     <feat name="Armor Proficiency (Heavy)" type="General" desc="Players Handbook page80"/>
+     <feat name="Armor Proficiency (Light)" type="General" desc="Players Handbook page 80"/>
+     <feat name="Armor Proficiency (Medium)" type="General" desc="Players Handbook page 80"/>
+     <feat name="Art of Fascination" type="Ancestor" desc="Oriental Adventures page 60"/>
+     <feat name="Arterial Strike" type="General" desc="Song and Silence page 38"/>
+     <feat name="Artist" type="General" desc="Forgotten Realms Campaign Setting page 33"/>
+     <feat name="Artist" type="Ancestor" desc="Oriental Adventures page 61"/>
+     <feat name="Athletic" type="General" desc="Song and Silence page 38"/>
+     <feat name="Attention to Detail" type="Ancestor" desc="Oriental Adventures page 61"/>
+     <feat name="Attune Gem" type="Item Creation" desc="Magic of Faerun page 21"/>
+     <feat name="Augment Construct" type="Psionic" desc="Dragon Magazine 287 page 54"/>
+     <feat name="Augment Summoning" type="General" desc="Tome and Blood page 39"/>
+     <feat name="Aura of Serenity" type="Mystic Warrior" desc="Mystic Warriors page 112"/>
+     <feat name="Battle Howl" type="General" desc="Touched by the Gods page 109"/>
+     <feat name="Battle Roar" type="Kaiju" desc="Dragon Magazine 289 page 70"/>
+     <feat name="Blind Casting" type="General" desc="Dungeons page 81"/>
+     <feat name="Blind-Fight" type="General" desc="Players Handbook page 80"/>
+     <feat name="Blindsight 5-foot Radius" type="General" desc="Sword and Fist page 5"/>
+     <feat name="Blood Frenzy" type="General" desc="The Taan page 81"/>
+     <feat name="Blood Sorcerer" type="Ancestor" desc="Oriental Adventures page 61"/>
+     <feat name="Blooded" type="General" desc="Forgotten Realms Campaign Setting page 33"/>
+     <feat name="Bloodline of Fire" type="General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Body Fuel" type="Psionic" desc="Psionics Handbook page 24"/>
+     <feat name="Bootlicker" type="General" desc="Evil page 58"/>
+     <feat name="Born Duelist" type="Ancestor" desc="Oriental Adventures page 61"/>
+     <feat name="Breeze Dance" type="Fighting Stance" desc="Mystic Warriors page 112"/>
+     <feat name="Brew Poison" type="Item Creation" desc="Traps and Treachery page 45"/>
+     <feat name="Brew Potion" type="Item Creation" desc="Players Handbook page 80"/>
+     <feat name="Bribery" type="General" desc="Evil page 58"/>
+     <feat name="Bullheaded" type="General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Cabalistic Spellcasting" type="Metamagic" desc="Sovereign Stone Campaign Sourcebook page 60"/>
+     <feat name="Casing Sense" type="General" desc="Traps and Treachery page 34"/>
+     <feat name="Chain Power" type="Metapsionic" desc="Dragon Magazine 287 page 54"/>
+     <feat name="Chain Spell" type="Metamagic" desc="Tome and Blood page 39"/>
+     <feat name="Chain Spell (Scarred Lands version)" type="Metamagic" desc="Relics and Rituals page 25"/>
+     <feat name="Change Instruction" type="Special" desc="Demonology page 41"/>
+     <feat name="Chariot Archery" type="General" desc="Sword and Fist page 78"/>
+     <feat name="Chariot Charge" type="General" desc="Sword and Fist page 79"/>
+     <feat name="Chariot Combat" type="General" desc="Sword and Fist page 78"/>
+     <feat name="Chariot Sideswipe" type="General" desc="Sword and Fist page 79"/>
+     <feat name="Chariot Trample" type="General" desc="Sword and Fist page 78"/>
+     <feat name="Charlatan" type="General" desc="Song and Silence page 38"/>
+     <feat name="Chink in the Armor" type="General" desc="Song and Silence page 38"/>
+     <feat name="Choke Hold" type="General" desc="Oriental Adventures page 61"/>
+     <feat name="Circle Kick" type="General" desc="Sword and Fist page 5"/>
+     <feat name="Claws/Fangs" type="Infernal" desc="Evil page 24"/>
+     <feat name="Cleave" type="General" desc="Players Handbook page 80"/>
+     <feat name="Close-Order Fighting" type="Special" desc="Dragon Lords of Melnibone page 63"/>
+     <feat name="Close-Quarters Fighting" type="General" desc="Sword and Fist page 5"/>
+     <feat name="Cloud Running" type="Special" desc="Creature Collection page 72"/>
+     <feat name="Combat Agility" type="General" desc="Dragon Magazine 284 page 123"/>
+     <feat name="Combat Casting" type="General" desc="Players Handbook page 80"/>
+     <feat name="Combat Manifestation" type="Psionic" desc="Psionics Handbook page 24"/>
+     <feat name="Combat Reflexes" type="General" desc="Players Handbook page 80"/>
+     <feat name="Combat Sense" type="General" desc="The Taan page 81"/>
+     <feat name="Conjure Mastery" type="Eldritch" desc="The Book of Eldritch Might page 4"/>
+     <feat name="Construct Familiar" type="General" desc="Dragon Magazine 280 page 62"/>
+     <feat name="Controlled Breathing" type="General" desc="Dungeons page 81"/>
+     <feat name="Cool Head" type="General" desc="Oriental Adventures page 61"/>
+     <feat name="Cooperative Spell" type="Metamagic" desc="Tome and Blood page 39"/>
+     <feat name="Cooperative Spellcasting" type="Metamagic" desc="Sovereign Stone Campaign Sourcebook page 60"/>
+     <feat name="Cosmopolitan" type="General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Courteous Magocracy" type="General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Crab Walk" type="Fighting Stance" desc="Mystic Warriors page 112"/>
+     <feat name="Craft Anaema Tool" type="Item Creation" desc="Mythic Races page 15"/>
+     <feat name="Craft Crystal Capacitor" type="Item Creation" desc="Psionics Handbook page 24"/>
+     <feat name="Craft Crystal Weapon" type="Item Creation" desc="Oriental Adventures page 61"/>
+     <feat name="Craft Dorje" type="Item Creation" desc="Psionics Handbook page 24"/>
+     <feat name="Craft Kirpan" type="Item Creation" desc="Mystic Warriors page 112"/>
+     <feat name="Craft Magic Arms and Armor" type="Item Creation" desc="Players Handbook page 81"/>
+     <feat name="Craft Magic Trap" type="Item Creation" desc="Traps and Treachery page 34"/>
+     <feat name="Craft Mystic Talisman" type="Item Creation" desc="Mystic Warriors page 112"/>
+     <feat name="Craft Named Weapon" type="Item Creation" desc="Mystic Warriors page 112"/>
+     <feat name="Craft Psionic Arms and Armor" type="Item Creation" desc="Psionics Handbook page 24"/>
+     <feat name="Craft Rod" type="Item Creation" desc="Players Handbook page 81"/>
+     <feat name="Craft Staff" type="Item Creation" desc="Players Handbook page 81"/>
+     <feat name="Craft Talisman" type="Item Creation" desc="Oriental Adventures page 61"/>
+     <feat name="Craft Universal Item" type="Item Creation" desc="Psionics Handbook page 24"/>
+     <feat name="Craft Vitus Amulet" type="Item Creation" desc="Mystic Warriors page 113"/>
+     <feat name="Craft Wand" type="Item Creation" desc="Players Handbook page 81"/>
+     <feat name="Craft Wondrous Item" type="Item Creation" desc="Players Handbook page 81"/>
+     <feat name="Create Graft" type="Item Creation" desc="Touched by the Gods page 26"/>
+     <feat name="Create Portal" type="Item Creation" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Dance of the Dirk" type="Fighting Stance" desc="Mystic Warriors page 113"/>
+     <feat name="Darkvision" type="Infernal" desc="Evil page 24"/>
+     <feat name="Dash" type="General" desc="Song and Silence page 38"/>
+     <feat name="Daylight Adaptation" type="General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Dead Shot" type="General" desc="Sovereign Stone Campaign Sourcebook page 60"/>
+     <feat name="Death Blow" type="General" desc="Sword and Fist page 6"/>
+     <feat name="Deep Impact" type="Psionic" desc="Psionics Handbook page 25"/>
+     <feat name="Defensive Strike" type="General" desc="Oriental Adventures page 62"/>
+     <feat name="Defensive Throw" type="General" desc="Oriental Adventures page 62"/>
+     <feat name="Deflect Arrows" type="General" desc="Players Handbook page 81"/>
+     <feat name="Deflect Ranged Attack" type="General" desc="Dragon Magazine 274 page 60"/>
+     <feat name="Delay Power" type="Metapsionic" desc="Psionics Handbook page 25"/>
+     <feat name="Delay Spell" type="Metamagic" desc="Tome and Blood page 39"/>
+     <feat name="Dirty Fighting" type="General" desc="Sword and Fist page 6"/>
+     <feat name="Disarm Mind" type="Psionic" desc="Psionics Handbook page 25"/>
+     <feat name="Discipline" type="General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Discipline" type="Ancestor" desc="Oriental Adventures page 62"/>
+     <feat name="Disguise Spell" type="Metamagic" desc="Song and Silence page 38"/>
+     <feat name="Dismiss Demon" type="Special" desc="Demonlogy page 41"/>
+     <feat name="Divine Cleansing" type="Divine" desc="Defenders of the Faith page 19"/>
+     <feat name="Divine Might" type="Divine" desc="Defenders of the Faith page 19"/>
+     <feat name="Divine Perception" type="General" desc="Touched by the Gods page 34"/>
+     <feat name="Divine Resistance" type="Divine" desc="Defenders of the Faith page 19"/>
+     <feat name="Divine Shield" type="Divine" desc="Defenders of the Faith page 19"/>
+     <feat name="Divine Vengeance" type="Divine" desc="Defenders of the Faith page 20"/>
+     <feat name="Divine Vigor" type="Divine" desc="Defenders of the Faith page 20"/>
+     <feat name="Dodge" type="General" desc="Players Handbook page 81"/>
+     <feat name="Dreamspeaking" type="General" desc="The Book of Eldritch Might page 4"/>
+     <feat name="Drug Tolerance" type="General" desc="Caravan of Hope page 27"/>
+     <feat name="Dual Strike" type="General" desc="Sword and Fist page 6"/>
+     <feat name="Eagle Claw Attack" type="General" desc="Sword and Fist page 6"/>
+     <feat name="Earths Embrace" type="General" desc="Oriental Adventures page 62"/>
+     <feat name="Education" type="General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Eidetic Memory" type="General" desc="Dungeons page 81"/>
+     <feat name="Element Resistance" type="Infernal" desc="Evil page 24"/>
+     <feat name="Empathy" type="General" desc="Traps and Treachery page 35"/>
+     <feat name="Empower Spell" type="Metamagic" desc="Players Handbook page 82"/>
+     <feat name="Empower Turning" type="Special" desc="Defenders of the Faith page 20"/>
+     <feat name="Enchant Stone" type="Item Creation" desc="The Taan page 81"/>
+     <feat name="Encode Stone" type="Item Creation" desc="Psionics Handbook page 25"/>
+     <feat name="Endurance" type="General" desc="Players Handbook page 82"/>
+     <feat name="Energy Admixture" type="Metamagic" desc="Tome and Blood page 39"/>
+     <feat name="Energy of Life" type="Special" desc="Creature Collection page 72"/>
+     <feat name="Energy Substitution" type="Metamagic" desc="Tome and Blood page 40"/>
+     <feat name="Enlarge Power" type="Metapsionic" desc="Psionics Handbook page 25"/>
+     <feat name="Enlarge Spell" type="Metamagic" desc="Players Handbook page 82"/>
+     <feat name="Enspell Familiar" type="General" desc="Dragon Magazine 280 page 62"/>
+     <feat name="Eschew Materials" type="Metamagic" desc="Tome and Blood page 40"/>
+     <feat name="Etch Object Rune" type="Item Creation" desc="The Book of Eldritch Might page 4"/>
+     <feat name="Ethran" type="General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Exotic Weapon Proficiency" type="General" desc="Players Handbook page 82"/>
+     <feat name="Expert Tactician" type="General" desc="Song and Silence page 38"/>
+     <feat name="Expertise" type="General" desc="Players Handbook page 82"/>
+     <feat name="Extend Power" type="Metapsionic" desc="Psionics Handbook page 25"/>
+     <feat name="Extend Spell" type="Metamagic" desc="Players Handbook page 82"/>
+     <feat name="Extra Familiar" type="General" desc="Dragon Magazine 280 page 62"/>
+     <feat name="Extra Music" type="General" desc="Song and Silence page 39"/>
+     <feat name="Extra Power" type="Psionic" desc="Dragon Magazine 287 page 55"/>
+     <feat name="Extra Slot" type="General" desc="Tome and Blood page 40"/>
+     <feat name="Extra Smiting" type="Special" desc="Defenders of the Faith page 20"/>
+     <feat name="Extra Spell" type="General" desc="Tome and Blood page 40"/>
+     <feat name="Extra Stunning Attacks" type="General" desc="Sword and Fist page 6"/>
+     <feat name="Extra Turning" type="Special" desc="Players Handbook page 82 (rules: ppage 32 42)"/>
+     <feat name="Eye for Detail" type="General" desc="Traps and Treachery page 35"/>
+     <feat name="Eyes in the Back of Your Head" type="General" desc="Sword and Fist page 6"/>
+     <feat name="Eyes of Calaam" type="Special" desc="Touched by the Gods page 59"/>
+     <feat name="Falling Star Strike" type="General" desc="Oriental Adventures page 62"/>
+     <feat name="Far Shot" type="General" desc="Players Handbook page 82"/>
+     <feat name="Fast Armor" type="General" desc="Dragon Magazine 284 page 123"/>
+     <feat name="Fast Rider" type="General" desc="Dragon Magazine 285 page 98"/>
+     <feat name="Fast Talker" type="General" desc="Traps and Treachery page 35"/>
+     <feat name="Fearsome and Fearless" type="Ancestor" desc="Oriental Adventures page 62"/>
+     <feat name="Feign Weakness" type="General" desc="Sword and Fist page 6"/>
+     <feat name="Fell Shot" type="Psionic" desc="Psionics Handbook page 25"/>
+     <feat name="Firearms Drill" type="General" desc="Dragon Magazine d20 Special/Annual 6 page 70"/>
+     <feat name="Fists of Calaam" type="Special" desc="Touched by the Gods page 59"/>
+     <feat name="Fists of Iron" type="General" desc="Sword and Fist page 6"/>
+     <feat name="Fleet of Foot" type="General" desc="Song and Silence page 39"/>
+     <feat name="Flick of the Wrist" type="General" desc="Song and Silence page 39"/>
+     <feat name="Flight" type="Infernal" desc="Evil page 25"/>
+     <feat name="Flying Kick" type="General] -Oriental Adventures page 62" desc=""/>
+     <feat name="Foe Hunter" type="Fighter General" desc="Forgotten Realms Campaign Setting page 34"/>
+     <feat name="Forester" type="General" desc="Forgotten Realms Campaign Setting page 35"/>
+     <feat name="Forge Ring" type="Item Creation" desc="Players Handbook page 82"/>
+     <feat name="Fortify Power" type="Metapsionic" desc="Dragon Magazine 287 page 55"/>
+     <feat name="Freezing the Lifeblood" type="General" desc="Oriental Adventures page 62 "/>
+     <feat name="Gifted General" type="Ancestor" desc="Oriental Adventures page 62"/>
+     <feat name="Golden Tongue" type="General" desc="Dungeons page 81"/>
+     <feat name="Grace Under Pressure" type="General" desc="Dungeons page 81"/>
+     <feat name="Grappling Block" type="General" desc="Oriental Adventures page 63"/>
+     <feat name="Grasshopper Strike" type="General" desc="Dragon Magazine 279 page 63"/>
+     <feat name="Great Cleave" type="General" desc="Players Handbook page 82"/>
+     <feat name="Great Crafter" type="Ancestor" desc="Oriental Adventures page 63"/>
+     <feat name="Great Diplomat" type="Ancestor" desc="Oriental Adventures page 63"/>
+     <feat name="Great Fortitude" type="General" desc="Players Handbook page 82"/>
+     <feat name="Great Ki Shout" type="General" desc="Oriental Adventures page 63"/>
+     <feat name="Great Stamina" type="Ancestor" desc="Oriental Adventures page 63"/>
+     <feat name="Great Sunder" type="Psionic" desc="Psionics Handbook page 26"/>
+     <feat name="Great Teamwork" type="Ancestor" desc="Oriental Adventures page 63"/>
+     <feat name="Greater Power Penetration" type="Psionic" desc="Psionics Handbook page 26"/>
+     <feat name="Greater Psionic Focus" type="Psionic" desc="Psionics Handbook page 26"/>
+     <feat name="Greater Spell Focus" type="General" desc="Tome and Blood page 40"/>
+     <feat name="Greater Spell Penetration" type="General" desc="Tome and Blood page 40"/>
+     <feat name="Green Ear" type="General" desc="Song and Silence page 39"/>
+     <feat name="Green Viper Style" type="Fighting Stance" desc="Mystic Warriors page 113"/>
+     <feat name="Hammer Fist" type="General" desc="Dragon Magazine 279 page 63"/>
+     <feat name="Hamstring" type="General" desc="Song and Silence page 39"/>
+     <feat name="Heavy Scarring" type="General" desc="The Taan page 82"/>
+     <feat name="Heighten Power" type="Metapsionic" desc="Psionics Handbook page 26"/>
+     <feat name="Heighten Spell" type="Metamagic" desc="Players Handbook page 82"/>
+     <feat name="Heighten Turning" type="Special" desc="Defenders of the Faith page 20"/>
+     <feat name="Heroic Destiny" type="Special" desc="Touched by the Gods page 109"/>
+     <feat name="Hide Power" type="Metapsionic" desc="Psionics Handbook page 26"/>
+     <feat name="Hide Spell" type="Metamagic" desc="Relics and Rituals page 25"/>
+     <feat name="Hill Fighter" type="General" desc="Dragon Magazine 285 page 98"/>
+     <feat name="Hold the Line" type="General" desc="Sword and Fist page 7"/>
+     <feat name="Honest Merchant" type="Ancestor" desc="Oriental Adventures page 63"/>
+     <feat name="Horse Nomad" type="Fighter General" desc="Forgotten Realms Campaign Setting page 35"/>
+     <feat name="Iaijutsu Master" type="Ancestor" desc="Oriental Adventures page 63"/>
+     <feat name="Immortality" type="Infernal" desc="Evil page 25"/>
+     <feat name="Immunity" type="Infernal" desc="Evil page 25"/>
+     <feat name="Imp" type="Infernal" desc="Evil page 25"/>
+     <feat name="Improved Aid" type="Ancestor" desc="Oriental Adventures page 63"/>
+     <feat name="Improved Alertness" type="General" desc="Dungeons page 82"/>
+     <feat name="Improved Bull Rush" type="General" desc="Players Handbook page 82"/>
+     <feat name="Improved Counterspell" type="General" desc="Forgotten Realms Campaign Setting page 35"/>
+     <feat name="Improved Critical" type="General" desc="Players Handbook page 82"/>
+     <feat name="Improved Disarm" type="General" desc="Players Handbook page 83"/>
+     <feat name="Improved Endurance" type="General" desc="Dungeons page 82"/>
+     <feat name="Improved Familiar" type="General" desc="Tome and Blood page 40"/>
+     <feat name="Improved Feint" type="General" desc="Evil page 59"/>
+     <feat name="Improved Flight" type="Infernal" desc="Evil page 25"/>
+     <feat name="Improved Grab" type="General" desc="Mythic Races page 77"/>
+     <feat name="Improved Grapple" type="General" desc="Oriental Adventures page 63"/>
+     <feat name="Improved Initiative" type="General" desc="Players Handbook page 83"/>
+     <feat name="Improved Knockout Attack" type="General" desc="Traps and Treachery page 35"/>
+     <feat name="Improved Low Blow" type="General" desc="Dragon Magazine 285 page 33"/>
+     <feat name="Improved Mounted Archery" type="General" desc="Dragon Magazine 285 page 99"/>
+     <feat name="Improved Mounted Combat" type="General" desc="Sovereign Stone Campaign Sourcebook page 62"/>
+     <feat name="Improved Multiweapon Fighting" type="General" desc="Mythic Races page 137"/>
+     <feat name="Improved Overrun" type="General" desc="Sword and Fist page 7"/>
+     <feat name="Improved Psicrystal" type="Psionic" desc="Psionics Handbook page 26"/>
+     <feat name="Improved Ranged Sneak Attack" type="General" desc="Traps and Treachery page 36"/>
+     <feat name="Improved Rapid Shot" type="General" desc="Dragon Magazine 275 page 41"/>
+     <feat name="Improved Regeneration" type="Infernal" desc="Evil page 25"/>
+     <feat name="Improved Shield Bash" type="General" desc="Defenders of the Faith page 20"/>
+     <feat name="Improved Sneak Attack" type="General" desc="Traps and Treachery page 36"/>
+     <feat name="Improved Sunder" type="General" desc="Sword and Fist page 7"/>
+     <feat name="Improved Trample" type="Kaiju" desc="Dragon Magazine 289 page 70"/>
+     <feat name="Improved Trip" type="General" desc="Players Handbook page 83"/>
+     <feat name="Improved Two-Weapon Fighting" type="General" desc="Players Handbook page 83"/>
+     <feat name="Improved Unarmed Strike" type="General" desc="Players Handbook page 83"/>
+     <feat name="Improvise Thieves Tools" type="General" desc="Traps and Treachery page 37"/>
+     <feat name="Improvised Weapon" type="General" desc="Sovereign Stone Campaign Sourcebook page 62"/>
+     <feat name="Increased Carrying Capacity" type="General" desc="Dungeons page 82"/>
+     <feat name="Increased Movement" type="Infernal" desc="Evil page 26"/>
+     <feat name="Inertial Armor" type="Psionic" desc="Psionics Handbook page 26"/>
+     <feat name="Infernal Pact" type="Infernal" desc="Evil page 26"/>
+     <feat name="Infernal Soul" type="Infernal" desc="Evil page 26"/>
+     <feat name="Information Exchange" type="Special" desc="Touched by the Gods page 7"/>
+     <feat name="Innate Spell" type="General" desc="Tome and Blood page 41"/>
+     <feat name="Inner Peace" type="Mystic Warrior" desc="Mystic Warriors page 113"/>
+     <feat name="Inner Strength" type="Psionic" desc="Psionics Handbook page 26"/>
+     <feat name="Inscribe Magical Tattoo" type="Item Creation" desc="Relics and Rituals page 198"/>
+     <feat name="Inscribe Rune" type="Item Creation" desc="Forgotten Realms Campaign Setting page 36"/>
+     <feat name="Insidious Magic" type="Metamagic" desc="Forgotten Realms Campaign Setting page 36"/>
+     <feat name="Invisibility" type="Infernal" desc="Evil page 27"/>
+     <feat name="Ironbone" type="Special" desc="Creature Collection page 71"/>
+     <feat name="Ironskin" type="Special" desc="Creature Collection page 72"/>
+     <feat name="Iron Will" type="General" desc="Players Handbook page 83"/>
+     <feat name="Item Image" type="Eldritch" desc="The Book of Eldritch Might page 4"/>
+     <feat name="Jack of All Trades" type="General" desc="Song and Silence page 40"/>
+     <feat name="Kamis Intuition" type="Ancestor" desc="Oriental Adventures page 63"/>
+     <feat name="Karmic Strike" type="General" desc="Oriental Adventures page 63"/>
+     <feat name="Karmic Twin" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Keen Intellect" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Keen Vision" type="General" desc="Traps and Treachery page 37"/>
+     <feat name="Ki Projection" type="Special" desc="Creature Collection page 72"/>
+     <feat name="Ki Shout" type="General" desc="Oriental Adventures page 64"/>
+     <feat name="Knock-Down" type="General" desc="Sword and Fist page 7"/>
+     <feat name="Knockout Attack" type="General" desc="Traps and Treachery page 37"/>
+     <feat name="Knowledgeable" type="General" desc="Dungeons page 82"/>
+     <feat name="Lace Spell: Elemental Energies" type="Eldritch" desc="The Book of Eldritch Might page 5"/>
+     <feat name="Lace Spell: Enemy Bane" type="Eldritch" desc="The Book of Eldritch Might page 5"/>
+     <feat name="Lace Spell: Holy/Unholy" type="Eldritch" desc="The Book of Eldritch Might page 5"/>
+     <feat name="Lace Spell: Lawful/Chaotic" type="Eldritch" desc="The Book of Eldritch Might page 5"/>
+     <feat name="Lead Missile Fire" type="General" desc="Evil page 59"/>
+     <feat name="Leadership" type="General" desc="Players Handbook page 83 (rules: Dungeon Masters Guide page 45)"/>
+     <feat name="Lightning Fists" type="General" desc="Sword and Fist page 7"/>
+     <feat name="Lightning Reflexes" type="General" desc="Players Handbook page 83"/>
+     <feat name="Light Sleeper" type="General" desc="Dungeons page 82"/>
+     <feat name="Lingering Song" type="General" desc="Song and Silence page 40"/>
+     <feat name="Lion Spy" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Lions Rage" type="Mystic Warrior Stance" desc="Mystic Warriors page 113"/>
+     <feat name="Living Shield" type="General" desc="Evil page 58"/>
+     <feat name="Low Blow" type="General" desc="Dragon Magazine 285 page 33"/>
+     <feat name="Luck of Heroes" type="General" desc="Forgotten Realms Campaign Setting page 36"/>
+     <feat name="Luck of Heroes" type="Ancestor" desc="Oriental Adventures page 64 "/>
+     <feat name="Magekiss" type="Metamagic" desc="Traps and Treachery page 22"/>
+     <feat name="Magic in the Blood" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Magic Item" type="Infernal" desc="Evil page 27"/>
+     <feat name="Magical Artisan" type="General" desc="Forgotten Realms Campaign Setting page 36"/>
+     <feat name="Magical Artisan" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Magical Talent" type="General" desc="The Book of Eldritch Might page 6"/>
+     <feat name="Magical Training" type="General" desc="Forgotten Realms Campaign Setting page 36"/>
+     <feat name="Magistrates Mind" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Mantis Leap" type="General" desc="Sword and Fist page 7"/>
+     <feat name="Manufacture Magic Poison" type="Item Creation" desc="The Book of Eldritch Might page 6"/>
+     <feat name="Many Masks" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Martial Weapon Proficiency" type="General" desc="Players Handbook page 83"/>
+     <feat name="Master Dorje" type="Metapsionic" desc="Psionics Handbook page 26"/>
+     <feat name="Maximize Power" type="Metapsionic" desc="Psionics Handbook page 26"/>
+     <feat name="Maximize Spell" type="Metamagic" desc="Players Handbook page 83"/>
+     <feat name="Mechanical Aptitude" type="General" desc="Traps and Treachery page 37"/>
+     <feat name="Mental Adversary" type="Psionic" desc="Psionics Handbook page 27"/>
+     <feat name="Mental Leap" type="Psionic" desc="Psionics Handbook page 27"/>
+     <feat name="Mercantile Background" type="General" desc="Forgotten Realms Campaign Setting page 36"/>
+     <feat name="Metacreative" type="Psionic" desc="Psionics Handbook page 27"/>
+     <feat name="Militia" type="General" desc="Forgotten Realms Campaign Setting page 36"/>
+     <feat name="Mind Blind" type="Psionic" desc="Dragon Magazine 287 page 55"/>
+     <feat name="Mind Over Body" type="General" desc="Forgotten Realms Campaign Setting page 37"/>
+     <feat name="Mind Trap" type="Psionic" desc="Psionics Handbook page 27"/>
+     <feat name="Mirror Sight" type="Eldritch" desc="The Book of Eldritch Might page 6"/>
+     <feat name="Mobility" type="General" desc="Players Handbook page 83"/>
+     <feat name="Monkey Grip" type="General" desc="Sword and Fist page 7"/>
+     <feat name="Mounted Archery" type="General" desc="Players Handbook page 83"/>
+     <feat name="Mounted Combat" type="General" desc="Players Handbook page 83"/>
+     <feat name="Moving Meditation" type="Special" desc="Creature Collection page 72"/>
+     <feat name="Multiattack (Taan version)" type="General" desc="The Taan page 83"/>
+     <feat name="Multicultural" type="General" desc="Song and Silence page 40"/>
+     <feat name="Multiple Limbs" type="Infernal" desc="Evil page 27"/>
+     <feat name="Multiweapon Fighting (Legends and Lairs version)" type="General" desc="Mythic Races page 137"/>
+     <feat name="Natural Weaponry" type="General" desc="The Taan page 83"/>
+     <feat name="Nature Sense" type="Special" desc="Touched by the Gods page 71"/>
+     <feat name="Nerve Strikes" type="Special" desc="Creature Collection page 72"/>
+     <feat name="Nobodys Fool" type="General" desc="Dragon Magazine 285 page 33"/>
+     <feat name="Obscure Lore" type="General" desc="Song and Silence page 40"/>
+     <feat name="Off-Hand Parry" type="General" desc="Sword and Fist page 7"/>
+     <feat name="Off-Handed" type="General" desc="Evil page 59"/>
+     <feat name="Onis Bane" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Pain Touch" type="General" desc="Sword and Fist page 8"/>
+     <feat name="Pebble Underfoot" type="General" desc="Dragon Magazine 279 page 63"/>
+     <feat name="Penetrate Hardness" type="Kaiju" desc="Dragon Magazine 289 page 70"/>
+     <feat name="Perfect Memory" type="General" desc="Traps and Treachery page 37"/>
+     <feat name="Permanent Control" type="Special" desc="Demonology page 41"/>
+     <feat name="Pernicious Magic" type="Metamagic" desc="Forgotten Realms Campaign Setting page 37"/>
+     <feat name="Persistent Power" type="Metapsionic" desc="Psionics Handbook page 27"/>
+     <feat name="Persistent Spell" type="Metamagic" desc="Tome and Blood page 42"/>
+     <feat name="Persuasive" type="General" desc="Song and Silence page 40"/>
+     <feat name="Pin Shield" type="General" desc="Sword and Fist page 8"/>
+     <feat name="Pinpoint Accuracy" type="General" desc="Sovereign Stone Campaign Sourcebook page 62"/>
+     <feat name="Point Blank Shot" type="General" desc="Players Handbook page 84"/>
+     <feat name="Poison Blood" type="Infernal" desc="Evil page 28"/>
+     <feat name="Poison Immunity" type="General" desc="Traps and Treachery page 37"/>
+     <feat name="Pounce" type="General" desc="Mythic Races page 77"/>
+     <feat name="Pounce and Strike" type="General" desc="Dragon Lords of Melnibone page 50"/>
+     <feat name="Power Attack" type="General" desc="Players Handbook page 84"/>
+     <feat name="Power Attack--Iaijutsu" type="Ancestor" desc="Oriental Adventures page 64"/>
+     <feat name="Power Attack--Shadowlands" type="Ancestor" desc="Oriental Adventures page 65"/>
+     <feat name="Power Lunge" type="General" desc="Sword and Fist page 8"/>
+     <feat name="Power Penetration" type="Psionic" desc="Psionics Handbook page 27"/>
+     <feat name="Power Specialization" type="Psionic" desc="Dragon Magazine 287 page 56"/>
+     <feat name="Power Touch" type="Psionic" desc="Psionics Handbook page 27"/>
+     <feat name="Powerful Voice" type="Ancestor" desc="Oriental Adventures page 65"/>
+     <feat name="Precise Shot" type="General" desc="Players Handbook page 84"/>
+     <feat name="Primal Shout" type="General" desc="The Taan page 83"/>
+     <feat name="Prone Attack" type="General" desc="Sword and Fist page 8"/>
+     <feat name="Psionic Body" type="Psionic" desc="Psionics Handbook page 27"/>
+     <feat name="Psionic Charge" type="Psionic" desc="Psionics Handbook page 28"/>
+     <feat name="Psionic Defense" type="Psionic" desc="Dragon Magazine 287 page 54"/>
+     <feat name="Psionic Dodge" type="Psionic" desc="Psionics Handbook page 28"/>
+     <feat name="Psionic Energy Admixture" type="Metapsionic" desc="Dragon Magazine 287 page 55"/>
+     <feat name="Psionic Energy Substitution" type="Metapsionic" desc="Dragon Magazine 287 page 54"/>
+     <feat name="Psionic Fist" type="Psionic" desc="Psionics Handbook page 28"/>
+     <feat name="Psionic Focus" type="Psionic" desc="Psionics Handbook page 28"/>
+     <feat name="Psionic Metabolism" type="Psionic" desc="Psionics Handbook page 28"/>
+     <feat name="Psionic Shot" type="Psionic" desc="Psionics Handbook page 28"/>
+     <feat name="Psionic Weapon" type="Psionic" desc="Psionics Handbook page 28"/>
+     <feat name="Psychic Bastion" type="Psionic" desc="Psionics Handbook page 28"/>
+     <feat name="Psychic Inquisitor" type="Psionic" desc="Psionics Handbook page 29"/>
+     <feat name="Psychoanalyst" type="Psionic" desc="Psionics Handbook page 29"/>
+     <feat name="Purifying Light" type="General" desc="Mythic Races page 69"/>
+     <feat name="Pyro" type="General" desc="Song and Silence page 40"/>
+     <feat name="Quick Draw" type="General" desc="Players Handbook page 84"/>
+     <feat name="Quicken Power" type="Metapsionic" desc="Psionics Handbook page 29"/>
+     <feat name="Quicken Spell" type="Metamagic" desc="Players Handbook page 84"/>
+     <feat name="Quicken Summoning" type="Special" desc="Demonology page 41"/>
+     <feat name="Quicken Turning" type="Special" desc="Defenders of the Faith page 20"/>
+     <feat name="Quicker Than the Eye" type="General" desc="Song and Silence page 40"/>
+     <feat name="Quickstrike" type="General" desc="Traps and Treachery page 37"/>
+     <feat name="Rake" type="General" desc="Mythic Races page 77"/>
+     <feat name="Raking Nails" type="General" desc="Mystic Warriors page 113"/>
+     <feat name="Ranged Disarm" type="General" desc="Dragon Magazine 274 page 60"/>
+     <feat name="Ranged Pin" type="General" desc="Dragon Magazine 274 page 60"/>
+     <feat name="Ranged Sunder" type="General" desc="Dragon Magazine 274 page 60"/>
+     <feat name="Rapid Metabolism" type="Psionic" desc="Psionics Handbook page 29"/>
+     <feat name="Rapid Reload" type="General" desc="Sword and Fist page 9"/>
+     <feat name="Rapid Shot" type="General" desc="Players Handbook page 84"/>
+     <feat name="Ray Burst" type="Metamagic" desc="Dragon Magazine Annual 5 page 26"/>
+     <feat name="Ray Coning" type="Metamagic" desc="Dragon Magazine Annual 5 page 26"/>
+     <feat name="Ray Extension" type="Metamagic" desc="Dragon Magazine Annual 5 page 26"/>
+     <feat name="Ray Focus" type="General" desc="Dragon Magazine Annual 5 page 26"/>
+     <feat name="Ray Splitting" type="Metamagic" desc="Dragon Magazine Annual 5 page 26"/>
+     <feat name="Reactive Counterspell" type="General" desc="Magic of Faerun page 22"/>
+     <feat name="Reach Power" type="Metapsionic" desc="Dragon Magazine 287 page 55"/>
+     <feat name="Reach Spell" type="Metamagic" desc="Defenders of the Faith page 20"/>
+     <feat name="Reckless Offensive" type="General" desc="Enemies and Allies page 41"/>
+     <feat name="Recognize Omen" type="General" desc="Sovereign Stone Campaign Sourcebook page 62"/>
+     <feat name="Redirect Attacks" type="General" desc="Evil page 59"/>
+     <feat name="Regeneration" type="Infernal" desc="Evil page 28"/>
+     <feat name="Remain Conscious" type="General" desc="Sword and Fist page 9"/>
+     <feat name="Repeat Power" type="Metapsionic" desc="Dragon Magazine 287 page 56"/>
+     <feat name="Repeat Spell" type="Metamagic" desc="Tome and Blood page 41"/>
+     <feat name="Requiem" type="General" desc="Song and Silence page 40"/>
+     <feat name="Resculpt Mind" type="Psionic" desc="Dragon Magazine 287 page 56"/>
+     <feat name="Resist Poison" type="General" desc="Forgotten Realms Campaign Setting page 37"/>
+     <feat name="Resist Poison" type="Ancestor" desc="Oriental Adventures page 65"/>
+     <feat name="Resist Taint" type="Ancestor" desc="Oriental Adventures page 65"/>
+     <feat name="Return Shot" type="Psionic" desc="Psionics Handbook page 29"/>
+     <feat name="Ride-By-Attack" type="General" desc="Players Handbook page 84"/>
+     <feat name="Rot" type="Infernal" desc="Evil page 28"/>
+     <feat name="Roundabout Kick" type="General" desc="Oriental Adventures page 65"/>
+     <feat name="Run" type="General" desc="Players Handbook page 84 "/>
+     <feat name="Sacred Spell" type="Metamagic" desc="Defenders of the Faith page 20"/>
+     <feat name="Saddleback" type="Fighter General" desc="Forgotten Realms Campaign Setting page 37"/>
+     <feat name="Saddleback" type="Ancestor" desc="Oriental Adventures page 65"/>
+     <feat name="Sanctum Spell" type="Metamagic" desc="Tome and Blood page 41"/>
+     <feat name="Scent" type="General" desc="Dungeon Masters Guide page 81 (variant rule)"/>
+     <feat name="Scholar of Nature" type="Ancestor" desc="Oriental Adventures page 65"/>
+     <feat name="Scribe Scroll" type="Item Creation" desc="Players Handbook page 84"/>
+     <feat name="Scribe Tattoo" type="Item Creation" desc="Psionics Handbook page 29"/>
+     <feat name="Sculpt Power" type="Metapsionic" desc="Dragon Magazine 287 page 56"/>
+     <feat name="Sculpt Spell" type="Metamagic" desc="Tome and Blood page 42"/>
+     <feat name="Sea Legs" type="Ancestor" desc="Oriental Adventures page 65"/>
+     <feat name="Second Wind" type="General" desc="Sovereign Stone Campaign Sourcebook page 63"/>
+     <feat name="Set Spear" type="General" desc="Dragon Lords of Melnibone page 63"/>
+     <feat name="Shadow" type="General" desc="Song and Silence page 40"/>
+     <feat name="Shadow (Legends and Lairs version)" type="General" desc="Traps and Treachery page 37"/>
+     <feat name="Shadow Weave Magic" type="General" desc="Forgotten Realms Campaign Setting page 37"/>
+     <feat name="Shapechange" type="Infernal" desc="Evil page 27"/>
+     <feat name="Shared Spellcasting" type="Metamagic" desc="Sovereign Stone Campaign Sourcebook page 63"/>
+     <feat name="Sharp-Shooting" type="General" desc="Sword and Fist page 9"/>
+     <feat name="Shield Charge" type="General" desc="Defenders of the Faith page 20"/>
+     <feat name="Shield Expert" type="General" desc="Sword and Fist page 9"/>
+     <feat name="Shield Proficiency" type="General" desc="Players Handbook page 85"/>
+     <feat name="Shot on the Run" type="General" desc="Players Handbook page 85"/>
+     <feat name="Side Step" type="General" desc="Mystic Warriors page 113"/>
+     <feat name="Signature Skill" type="General" desc="Traps and Treachery page 38"/>
+     <feat name="Signature Spell" type="General" desc="Forgotten Realms Campaign Setting page 37"/>
+     <feat name="Silent Spell" type="Metamagic" desc="Players Handbook page 85"/>
+     <feat name="Silver Palm" type="General" desc="Forgotten Realms Campaign Setting page 37"/>
+     <feat name="Silver Tongue" type="Ancestor" desc="Oriental Adventures page 65"/>
+     <feat name="Simple Weapon Proficiency" type="General" desc="Players Handbook page 85"/>
+     <feat name="Skill Focus" type="General" desc="Players Handbook page 85"/>
+     <feat name="Smooth Talk" type="General" desc="Forgotten Realms Campaign Setting page 37"/>
+     <feat name="Smooth Talk" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Snake Blood" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Snatch Arrows" type="General" desc="Sword and Fist page 9"/>
+     <feat name="Snatch Weapon" type="General" desc="Song and Silence page 40"/>
+     <feat name="Soul of Honor" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Soul of Loyalty" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Soul of Sincerity" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Speed of Thought" type="Psionic" desc="Psionics Handbook page 29"/>
+     <feat name="Spell Focus" type="General" desc="Players Handbook page 85"/>
+     <feat name="Spell Girding" type="General" desc="Magic of Faerun page 22"/>
+     <feat name="Spell Mastery" type="Special" desc="Players Handbook page 85 (rules: page 54)"/>
+     <feat name="Spell Penetration" type="General" desc="Players Handbook page 85"/>
+     <feat name="Spell Power" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Spell Specialization" type="General" desc="Tome and Blood page 42"/>
+     <feat name="Spell Thematics" type="General" desc="Magic of Faerun page 22"/>
+     <feat name="Spellcaster Support" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Spellcasting Prodigy" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Spellfire Wielder" type="General" desc="Magic of Faerun page 23"/>
+     <feat name="Spirited Charge" type="General" desc="Players Handbook page 85"/>
+     <feat name="Split Psionic Ray" type="Metapsionic" desc="Dragon Magazine 287 page 56"/>
+     <feat name="Split Ray" type="Metamagic" desc="Tome and Blood page 42"/>
+     <feat name="Spook Animals" type="Special" desc="Dragon Magazine d20 Special/Annual 6 page 65"/>
+     <feat name="Spring Attack" type="General" desc="Players Handbook page 85"/>
+     <feat name="Staggering Blow" type="General" desc="Dragon Magazine 279 page 63"/>
+     <feat name="Stand Still" type="Psionic" desc="Psionics Handbook page 29"/>
+     <feat name="Stare-Down" type="General" desc="Scrollworks 11 page 5"/>
+     <feat name="Stealth" type="General" desc="Traps and Treachery page 39"/>
+     <feat name="Stealthy" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Still Spell" type="Metamagic" desc="Players Handbook page 85"/>
+     <feat name="Stoic Composure" type="General" desc="Dragon Magazine 284 page 123"/>
+     <feat name="Street Smart" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Strength of the Charger" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Strength of the Crab" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Strength of Personality" type="Special" desc="Demonology page 41"/>
+     <feat name="Strong Soul" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Strong Soul" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Stunning Fist" type="General" desc="Players Handbook page 85"/>
+     <feat name="Stunning Roar" type="Kaiju" desc="Dragon Magazine 289 page 70"/>
+     <feat name="Subdual Substitution" type="Metamagic" desc="Tome and Blood page 42"/>
+     <feat name="Subduing Strike" type="General" desc="Sovereign Stone Campaign Sourcebook page 63"/>
+     <feat name="Subsonics" type="General" desc="Song and Silence page 40"/>
+     <feat name="Subtle Charm" type="Special" desc="Touched by the Gods page 71"/>
+     <feat name="Sunder" type="General" desc="Players Handbook page 85"/>
+     <feat name="Superior Expertise" type="General" desc="Oriental Adventures page 66"/>
+     <feat name="Survival Instincts" type="General" desc="The Taan page 83"/>
+     <feat name="Survivor" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Swarmfighting" type="General" desc="Dragon Magazine 285 page 33"/>
+     <feat name="Swift Twist Glance" type="General" desc="Mystic Warriors page 113"/>
+     <feat name="Tail Attack" type="Special" desc="Dragon Magazine d20 Special/Annual 6 page 61"/>
+     <feat name="Talented" type="Psionic" desc="Psionics Handbook page 30"/>
+     <feat name="Tattoo Focus" type="Special" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Taut Tug" type="General" desc="Mystic Warriors page 113"/>
+     <feat name="Ten Animal Style" type="Special" desc="Creature Collection page 72"/>
+     <feat name="Tenacious Magic" type="Metamagic" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Thick Hide" type="General" desc="The Taan page 83"/>
+     <feat name="Thick Skin" type="General" desc="Dungeons page 82"/>
+     <feat name="Thrall Master" type="Special" desc="Touched by the Gods page 71"/>
+     <feat name="Throw Anything" type="General" desc="Sword and Fist page 9"/>
+     <feat name="Thug" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Thunder Twin" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Thunderous Roar" type="Kaiju" desc="Dragon Magazine 289 page 70"/>
+     <feat name="Tinker" type="General" desc="Dungeons page 82"/>
+     <feat name="Token Familiar" type="General" desc="Dragon Magazine 280 page 62"/>
+     <feat name="Toughness" type="General" desc="Players Handbook page 85"/>
+     <feat name="Track" type="General" desc="Players Handbook page 85"/>
+     <feat name="Trample" type="General" desc="Players Handbook page 86"/>
+     <feat name="Treetopper" type="General" desc="Forgotten Realms Campaign Setting page 38"/>
+     <feat name="Trigger Power" type="Psionic" desc="Psionics Handbook page 30"/>
+     <feat name="Trustworthy" type="General" desc="Song and Silence page 40"/>
+     <feat name="Turn Outsider" type="Special" desc="Evil page 60"/>
+     <feat name="Turn Resistance" type="General" desc="Mythic Races page 50"/>
+     <feat name="Twin Power" type="Psionic" desc="Psionics Handbook page 30"/>
+     <feat name="Twin Spell" type="Metamagic" desc="Tome and Blood page 42"/>
+     <feat name="Twin Sword Style" type="Fighter General" desc="Forgotten Realms Campaign Setting page 39"/>
+     <feat name="Two-Weapon Fighting" type="General" desc="Players Handbook page 86"/>
+     <feat name="Tyrant" type="General" desc="Evil page 60"/>
+     <feat name="Ultimate Feint" type="General" desc="Evil page 62"/>
+     <feat name="Unavoidable Strike" type="Psionic" desc="Psionics Handbook page 30"/>
+     <feat name="Unbalancing Strike" type="General" desc="Oriental Adventures page 66"/>
+     <feat name="Undead Familiar" type="General" desc="Dragon Magazine 280 page 62"/>
+     <feat name="Undetectable Lie" type="Infernal" desc="Evil page 28"/>
+     <feat name="Unholy Blessing" type="Infernal" desc="Evil page 28"/>
+     <feat name="Unholy Strength" type="Infernal" desc="Evil page 28"/>
+     <feat name="Unorthodox Flurry" type="Special" desc="Dragon Magazine 279 page 63"/>
+     <feat name="Unusual Background" type="General" desc="Diomin page 43"/>
+     <feat name="Unyielding Aura" type="General" desc="Mythic Races page 69"/>
+     <feat name="Up the Walls" type="Psionic" desc="Psionics Handbook page 30"/>
+     <feat name="Upgrade Power" type="Psionic" desc="Dragon Magazine 287 page 56"/>
+     <feat name="Vengeful Strike" type="General" desc="Touched by the Gods page 109"/>
+     <feat name="Vitus Kata" type="Mystic Warrior" desc="Mystic Warriors page 113"/>
+     <feat name="Wall Breaker" type="General" desc="Dragon Magazine 285 page 98"/>
+     <feat name="Warrior Instinct" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Warrior Shugenja" type="Ancestor" desc="Oriental Adventures page 66"/>
+     <feat name="Warsinger" type="Special" desc="Touched by the Gods page 109"/>
+     <feat name="Water-ken" type="General" desc="The Taan page 83"/>
+     <feat name="Wealth" type="Infernal" desc="Evil page 29"/>
+     <feat name="Weapon Expertise: Throwing Spear" type="General" desc="Ragnarok! page 10"/>
+     <feat name="Weapon Finesse" type="General" desc="Players Handbook page 86"/>
+     <feat name="Weapon Focus" type="General" desc="Players Handbook page 86"/>
+     <feat name="Weapon Specialization" type="Special" desc="Players Handbook page 86 (rules: page 37)"/>
+     <feat name="Weapon-Catching" type="General" desc="Ragnarok! page 9"/>
+     <feat name="Whirlwind Attack" type="General" desc="Players Handbook page 86"/>
+     <feat name="Widen Power" type="Metapsionic" desc="Dragon Magazine 287 page 56"/>
+     <feat name="Widen Spell" type="Metamagic" desc="Tome and Blood page 42"/>
+     <feat name="Wish" type="Infernal" desc="Evil page 29"/>
+     <feat name="Wrath of Calaam" type="Special" desc="Touched by the Gods page 59"/>
+     <feat name="Zen Archery" type="General" desc="Sword and Fist page 9"/>
+</feats>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/dnd3epowers.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,294 @@
+<powers>
+<power test='============================' name=' ' level=' ' desc=' ' />
+<power test='    Psionic Combat Powers' name=' ' level=' ' desc=' ' />
+<power test='============================' name=' ' level=' ' desc=' ' />
+<power test='Psionic Attack' name='Ego Whip' level='2' desc='Mental Lash (DC 1d20 + Dex Mod + DC Mod) Damage: 1d4 Dex' point='3' />
+<power test='Psionic Defense' name='Empty Mind' level='1' desc='Hide your mind - DC Mods EW: +1 II: -2 MB: +3 MT: -3 PC: -5' point='1' />
+<power test='Psionic Attack' name='Id Insinuation' level='2' desc='Tendrils of thought disrupt target (DC 1d20 + ST Mod + DC Mod) Damage:1d2 Str' point='3' />
+<power test='Psionic Defense' name='Intellect Fortress' level='1' desc='Incase mind in fortress of determination - DC Mods EW: -2 II: +1 MB: +0 MT: +6 PC: +4 / Mental Hardness 3' point='5' />
+<power test='Psionic Defense' name='Mental Barrier' level='1' desc='A construct of dissembling thoughts - DC Mods EW: -1 II: +1 MB: +0 MT: +1 PC: +3 / Mental Hardness 2' point='3' />
+<power test='Psionic Attack' name='Mind Blast' level='5' desc='Blast the minds of all in 60ft-cone (DC 1d20 + Chr Mod + DC Mod) Damage : 1d4 Chr (nonpsionics stunned for 3d4 rounds)' point='9' />
+<power test='Psionic Attack' name='Mind Thrust' level='1' desc='Massive assult on mental pathways (DC 1d20 + Int Mod + DC Mod) Damage: 1d2 Int' point='1' />
+<power test='Psionic Attack' name='Psychic Crush' level='3' desc='Your will surrounds target squeezes mercilessly (DC 1d20 + Wis Mod + DC Mod)' point='5' />
+<power test='Psionic Defense' name='Thought Shield' level='1' desc='Erect a shield of hope - DC Mods EW: -4 II: -1 MB: -2 MT: +4 PC: +2 / Mental Hardness 1' point='1' />
+<power test='Psionic Defense' name='Tower of Iron Will' level='1' desc='Generates a bastion of thought - DC Mods EW: +3 II: +0 MB: -1 MT: +5 PC: -3 / Mental Hardness 2 (10ft-radius)' point='5' />
+<power test='============================' name=' ' level=' ' desc=' ' />
+<power test='       Psionic Powers' name=' ' level=' ' desc=' ' />
+<power test='============================' name=' ' level=' ' desc=' ' />
+<power test='Metacreativity' name='Bolt' level='0' desc='Create a bolt, arrow, bullet' point='1' />
+<power test='Psychoportation' name='Burst' level='0' desc='Speed +10ft for 1 round' point='1' />
+<power test='Psychoportation' name='Catfall' level='0' desc='You recover well from a fall' point='1' />
+<power test='Psycokinesis' name='Control Shadow' level='0' desc='Control normal shadow like puppet' point='1' />
+<power test='Telepathy' name='Daze' level='0' desc='Creature loses next action' point='1' />
+<power test='Clairsentience' name='Detect Posion' level='0' desc='Detect the presence of poison' point='1' />
+<power test='Clairsentience' name='Detect Psionics' level='0' desc='Detect the presence of psionics' point='1' />
+<power test='Telepathy' name='Distract' level='0' desc='Subjects mind wanders -1 to some actions' point='1' />
+<power test='Psychometabolism' name='Elfsight' level='0' desc='You have low-light vision' point='1' />
+<power test='Psychokinesis' name='Far Hand' level='0' desc='Minor Telekinesis' point='1' />
+<power test='Psychokinesis' name='Far Punch' level='0' desc='Telekinetic punch deals 1 damage' point='1' />
+<power test='Metacreativity' name='Finger of Fire' level='0' desc='Deal 1d3 damage to target' point='1' />
+<power test='Psychoportation' name='Float' level='0' desc='You buoy a subject in water or liquid' point='1' />
+<power test='Telepathy' name='Inkling' level='0' desc='50% likely to know if action is good/bad' point='1' />
+<power test='Clairsentience' name='Know Direction' level='0' desc='You know which way is North' point='1' />
+<power test='Psychometabolism' name='Lesser Natural Armor' level='0' desc='+1 Natural Armor Bonus' point='1' />
+<power test='Telepathy' name='Missive' level='0' desc='Send one-way message to subject' point='1' />
+<power test='Psychokinesis' name='My Light' level='0' desc='Eyes emit 20ft cone of light' point='1' />
+<power test='Psychometabolism' name='Talons' level='0' desc='Unarmed attacks deal +1 damage' point='1' />
+<power test='Telepathy' name='Telempathic Projection' level='0' desc='You modify targets emotions' point='1' />
+<power test='Metacreativity' name='Trinket' level='0' desc='You create a short lived trinket' point='1' />
+<power test='Psychometabolism' name='Valor' level='0' desc='+1 Moral Bonus on saves' point='1' />
+<power test='Psychometabolism' name='Verve' level='0' desc='Gain +1 temp hit point' point='1' />
+<power test='Metacreativity' name='Astral Construct I' level='1' desc='Create Astral construct to fight for you' point='1' />
+<power test='Telepathy' name='Attraction' level='1' desc='Subject has an attraction you specify' point='1' />
+<power test='Psychokinesis' name='Biocurrent' level='1' desc='Deal 1d4 electrical / round to target' point='1' />
+<power test='Psychometabolism' name='Biofeedback' level='1' desc='Take some damage as subdual damage' point='1' />
+<power test='Psychometabolism' name='Bite of the Wolf' level='1' desc='Gain bite attack 1d8 damage' point='1' />
+<power test='Psychoportation' name='Call Weaponry' level='1' desc='Summon weapon' point='1+' />
+<power test='Telepathy' name='Charm Person' level='1' desc='Makes one person your friend' point='1' />
+<power test='Clairsentience' name='Combat Precognition' level='1' desc='+1 insight bonus to AC' point='1' />
+<power test='Psychometabolism' name='Compression' level='1' desc='You shrink 10%/Lvl max 50%' point='1' />
+<power test='Telepathy' name='Conceal Thoughts' level='1' desc='Conceal motive +20 bluff +4 save vs mind reading' point='1' />
+<power test='Psychokinesis' name='Control Light' level='1' desc='Adjust light levels up or down' point='1' />
+<power test='Psychokinesis' name='Control Object' level='1' desc='Telekinetically control small object' point='1' />
+<power test='Psychokinesis' name='Create Sound' level='1' desc='Create any sound desired' point='1' />
+<power test='Telepathy' name='Demoralize' level='1' desc='Foes suffer -1 on some actions' point='1' />
+<power test='Clairsentience' name='Destiny Dissonance' level='1' desc='Touch attack 1d8 subdual' point='1' />
+<power test='Telepathy' name='Disable' level='1' desc='Hold creatures in 15ft radius' point='1' />
+<power test='Psychoportation' name='Dissipating Touch' level='1' desc='Touch deals 1d8 damage' point='1' />
+<power test='Psychometabolism' name='Emapthic Transfer' level='1' desc='Transfer others wounds' point='1' />
+<power test='Telepathy' name='Empathy' level='1' desc='You know targets surface emotions' point='1' />
+<power test='Clairsentience' name='Expanded Vision' level='1' desc='315-degree sight' point='1' />
+<power test='Psychoportation' name='Feather Fall' level='1' desc='Target falls slowly' point='1' />
+<power test='Psychometabolism' name='Feel Light' level='1' desc='Use tactile sensations to see' point='1' />
+<power test='Psychometabolism' name='Feel Sound' level='1' desc='Use tactile sensations to hear' point='1' />
+<power test='Metacreativity' name='Firefall' level='1' desc='Fiery sparks deal 1d4 within 10ft radius' point='1' />
+<power test='Metacreativity' name='Grease' level='1' desc='Makes 10ft square or object slippery' point='1' />
+<power test='Psychometabolism' name='Hammer' level='1' desc='Touch attack deals 1d8' point='1' />
+<power test='Psychometabolism' name='Hear Light' level='1' desc='Use auditory senses to see' point='1' />
+<power test='Psychometabolism' name='Hustle' level='1' desc='You gain one extra partial action next round' point='1' />
+<power test='Clairsentience' name='Identify' level='1' desc='Identify single feature of psionic item' point='1' />
+<power test='Clairsentience' name='Know Location' level='1' desc='You know, generally, where you are' point='1' />
+<power test='Psychometabolism' name='Lesser Body Adjustment' level='1' desc='Heal 1d8 or +1 fort save, or heal 1 abil pt.' point='1' />
+<power test='Psychokinesis' name='Lesser Concussion' level='1' desc='Pummel foe for 1d6' point='1' />
+<power test='Metacreativity' name='Lesser Metaphysical Weapon' level='1' desc='Weapon gains +1 bonus' point='1' />
+<power test='Telepathy' name='Lesser MindLink' level='1' desc='Limited mental bond with target' point='1' />
+<power test='Psychokinesis' name='Matter Agitation' level='1' desc='Heat creature or object' point='1' />
+<power test='Metacreativity' name='Minor Creation' level='1' desc='Create one wood or cloth item' point='1' />
+<power test='Clairsentience' name='Object Reading' level='1' desc='You know about an objects past' point='1' />
+<power test='Metacreativity' name='Psycoluminescence' level='1' desc='Object emits silvery light 20ft-radius' point='1' />
+<power test='Psychometabolism' name='See Sound' level='1' desc='Use visual senses to hear' point='1' />
+<power test='Telepathy' name='Sense Link' level='1' desc='You sense what a subject senses' point='1' />
+<power test='Psychoportation' name='Skate' level='1' desc='Target slides along ground as if ice' point='1' />
+<power test='Psychoportation' name='Spider Climb' level='1' desc='You can walk on walls or ceilings' point='1' />
+<power test='Clairsentience' name='Steadfast Gaze' level='1' desc='Immune to gaze attacks' point='1' />
+<power test='Psychokinesis' name='Stomp' level='1' desc='Create shockwave does 1d4 subdual' point='1' />
+<power test='Clairsentience' name='Steadfast Gaze' level='1' desc='Immune to gaze attacks' point='1' />
+<power test='Psychometabolism' name='Vigor' level='1' desc='Gain +3 temp hp / lvl (18 max)' point='1' />
+<power test='Psychometabolism' name='Animal Affinity' level='2' desc='Increase ability score by 1d4+1 by emulating an animal' point='3' />
+<power test='Metacreativity' name='Astral Construct II' level='2' desc='Create Astral Construct to fight for you' point='3' />
+<power test='Clairsentience' name='Augury' level='2' desc='Learn if action will be good or bad' point='3' />
+<power test='Telepathy' name='Aversion' level='2' desc='Subject has aversion you specify' point='3' />
+<power test='Psychometabolism' name='Body Adjustment' level='2' desc='Heal 3d6, or fort save bonus, or heal 2 abil pts.' point='3' />
+<power test='Psychometabolism' name='Body Equilibrium' level='2' desc='You can walk on non-solid surfaces' point='3' />
+<power test='Telepathy' name='Brain Lock' level='2' desc='Target cant move or take actions' point='3' />
+<power test='Metacreativity' name='Burning Ray' level='2' desc='Do 3d6 damage to target' point='3' />
+<power test='Psychometabolism' name='Chameleon' level='2' desc='+10 bonus to hide' point='3' />
+<power test='Clairsentience' name='Clairaudience/Clairvoyance' level='2' desc='Hear or See at a distance' point='3' />
+<power test='Psychometabolism' name='Claws of the Bear' level='2' desc='Hands do base 1d12 damage' point='3' />
+<power test='Clairsentience' name='Combat Prescience' level='2' desc='+2 Insight Bonus to attack rolls' point='3' />
+<power test='Psychokinesis' name='Concussion' level='2' desc='Pummel foe for 3d6' point='3' />
+<power test='Psychokinesis' name='Control Air' level='2' desc='Increase air speed 10 + 5/lvl mph' point='3' />
+<power test='Psychokinesis' name='Control Body' level='2' desc='Take control of targets limbs' point='3' />
+<power test='Psychokinesis' name='Control Fire' level='2' desc='Control heat and movement of fire' point='3' />
+<power test='Clairsentience' name='Darkvision' level='2' desc='You gain darkvision' point='3' />
+<power test='Telepathy' name='Detect Thoughts' level='2' desc='Detect subjects surface thoughts' point='3' />
+<power test='Metacreativity' name='Ectoplasmic Cocoon' level='2' desc='Encapsulate target, they cant move' point='3' />
+<power test='Metacreativity' name='Ecto Puppet' level='2' desc='Create an astral construct you control' point='3' />
+<power test='Psychometabolism' name='Expansion' level='2' desc='Grow 10%/lvl Max 100%' point='3' />
+<power test='Psychoportation' name='Glide' level='2' desc='Subject glides at 20ft/rd' point='3' />
+<power test='Telepathy' name='Inflict Pain' level='2' desc='Telepathic strike causing 3d6 damage' point='3' />
+<power test='Telepathy' name='Intrusive Sense Link' level='2' desc='Subject senses what you sense' point='3' />
+<power test='Psychokinesis' name='Invisibility' level='2' desc='Subject is invis for 10min/lvl or until attacks' point='3' />
+<power test='Psychoportation' name='Knock' level='2' desc='Opens locked or psionically locked doors' point='3' />
+<power test='Psychoportation' name='Levitate' level='2' desc='Subject moves up or down' point='3' />
+<power test='Psychometabolism' name='Painful Touch' level='2' desc='Touch causes 1d6 subdual damage' point='3' />
+<power test='Psychoportation' name='Psionic Lock' level='2' desc='Psionically lock portal or chest' point='3' />
+<power test='Clairsentience' name='Recall Pain' level='2' desc='Cause 3d6 damage to target' point='3' />
+<power test='Clairsentience' name='See Invisibility' level='2' desc='Reveals invisible objects' point='3' />
+<power test='Psychoportation' name='Sense Psychoportation' level='2' desc='Know when others use this discipline' point='3' />
+<power test='Clairsentience' name='Sensitivity to Psychic Impressions' level='2' desc='Find out about areas past' point='3' />
+<power test='Psychokinesis' name='Sever the Tie' level='2' desc='Cause 3d8 to undead within 10ft radius' point='3' />
+<power test='Metacreativity' name='Sudden Minor Creation' level='2' desc='Quickly create wood or cloth items' point='3' />
+<power test='Telepathy' name='Suggestion' level='2' desc='Compel target to follow suggestion' point='3' />
+<power test='Psychometabolism' name='Sustenance' level='2' desc='You can go with out food or water' point='3' />
+<power test='Clairsentience' name='Vigilance' level='2' desc='See through mists, murk and darkness' point='3' />
+<power test='Metacreativity' name='Astral Construct III' level='3' desc='Create Astral Construct to fight for you' point='5' />
+<power test='Psychoportation' name='Astral Steed' level='3' desc='Astral Steed appears for 1 hour/lvl' point='5' />
+<power test='Psychometabolism' name='Bite of the Tiger' level='3' desc='Your bite deals 2d8 damage' point='5' />
+<power test='Telepathy' name='Charm Monster' level='3' desc='Make monster your ally' point='5' />
+<power test='Psychometabolism' name='Claws of the Vampire' level='3' desc='Your hands deal 1d8 damage and you heal' point='5' />
+<power test='Psychokinesis' name='Cone of Sound' level='3' desc='Sonic Energy deals 5d4 damage' point='5' />
+<power test='Psychokinesis' name='Control Sound' level='3' desc='You can create very specific sounds' point='5' />
+<power test='Metacreativity' name='Create Food and Water' level='3' desc='Feed 3 humans/lvl' point='5' />
+<power test='Telepathy' name='Crisis of Breath' level='3' desc='Disrupt targets breathing' point='5' />
+<power test='Clairsentience' name='Danger Sense' level='3' desc='You gain +4 insight bonus vs traps' point='5' />
+<power test='Psychoportation' name='Dimension Slide' level='3' desc='Move to any spot you can see' point='5' />
+<power test='Psychometabolism' name='Displacement' level='3' desc='Attacks miss you 50% of the time' point='5' />
+<power test='Psychometabolism' name='Duodimensional Hand' level='3' desc='Hands deal slashing damage' point='5' />
+<power test='Psychometabolism' name='Ectoplasmic Form' level='3' desc='Your form becomes hard to damage' point='5' />
+<power test='Telepathy' name='False Sensory Input' level='3' desc='You falsify one of subjects senses' point='5' />
+<power test='Telepathy' name='Fate Link' level='3' desc='You link the fates of 2 targets' point='5' />
+<power test='Psychoportation' name='Fly' level='3' desc='Subject flies at a speed of 90ft' point='5' />
+<power test='Psychokinesis' name='Greater Concussion' level='3' desc='Pummel foe for 5d6' point='5' />
+<power test='Psychometabolism' name='Improved Biofeedback' level='3' desc='You control your damage' point='5' />
+<power test='Clairsentience' name='Invisibility Purge' level='3' desc='Dispell Invis in 5ft/lvl radius' point='5' />
+<power test='Telepathy' name='Lesser Domination' level='3' desc='Forces subject to obey your will' point='5' />
+<power test='Metacreativity' name='Metaphysical Weapon' level='3' desc='Weapon gains +3 Bonus' point='5' />
+<power test='Telepathy' name='Mindlink' level='3' desc='You have a mental bond with others' point='5' />
+<power test='Psychokinesis' name='Negate Psionics' level='3' desc='Cancel psionic powers and effects' point='5' />
+<power test='Clairsentience' name='Nondetection' level='3' desc='Hides subject from remote viewing' point='5' />
+<power test='Clairsentience' name='Poison Sense' level='3' desc='Sense posion in 30ft radius' point='5' />
+<power test='Clairsentience' name='Prowess' level='3' desc='Gain extra attack of opportunity' point='5' />
+<power test='Psychometabolism' name='Rejuventaion' level='3' desc='Heal 1 ability point / hour' point='5' />
+<power test='Clairsentience' name='Remote Viewing' level='3' desc='You see subject from a distance' point='5' />
+<power test='Telepathy' name='Schism' level='3' desc='Split your mind into 2 parts' point='5' />
+<power test='Psychoportation' name='Time Hop' level='3' desc='Subject hops forwardin time 3d6 rounds' point='5' />
+<power test='Clairsentience' name='Ubiquitous Vision' level='3' desc='You have all-around vision' point='5' />
+<power test='Clairsentience' name='Undead Sense' level='3' desc='Detect Undead 25ft + 5ft/2lvl radius' point='5' />
+<power test='Metacreativity' name='Whitefire' level='3' desc='Deals 5d4 damage in 20ft radius' point='5' />
+<power test='Psychokinesis' name='Amplified Invisibility' level='4' desc='Invisibility that lasts 1 attack' point='7' />
+<power test='Clairsentience' name='Anchored Navigation' level='4' desc='You navigate from a fixed reference point that you mentally sense' point='7' />
+<power test='Metacreativity' name='Astral Construct IV' level='4' desc='Create Astral Construct to fight for you' point='7' />
+<power test='Clairsentience' name='Aura Sight' level='4' desc='You can read auras' point='7' />
+<power test='Clairsentience' name='Detect Remote Viewing' level='4' desc='You know when others spy on you remotely' point='7' />
+<power test='Psychoportation' name='Dimensional Anchor' level='4' desc='Bars extradimensional travel' point='7' />
+<power test='Psychoportation' name='Dimension Door' level='4' desc='Teleport short distances (400ft + 40ft/lvl)' point='7' />
+<power test='Psychoportation' name='Dismissal' level='4' desc='Forces target to return to native plane' point='7' />
+<power test='Metacreativity' name='Dismiss Ectoplasm' level='4' desc='Dissipates ectoplasmic targets' point='7' />
+<power test='Psychometabolism' name='Dissolving Touch' level='4' desc='Touch deals 7d6 acid damage' point='7' />
+<power test='Clairsentience' name='Divination' level='4' desc='Provides advice for actions' point='7' />
+<power test='Telepathy' name='Domination' level='4' desc='Subject obeys your will' point='7' />
+<power test='Metacreativity' name='Fabricate' level='4' desc='Make raw goods into finished items' point='7' />
+<power test='Telepathy' name='Fatal Attraction' level='4' desc='Implant death urge into target' point='7' />
+<power test='Clairsentience' name='Fate of One' level='4' desc='You can reroll an bad roll' point='7' />
+<power test='Telepathy' name='Forced Mindlink' level='4' desc='Create mental bond with unwilling subject' point='7' />
+<power test='Psychoportation' name='Freedom of Movement' level='4' desc='Move normally despite impediments' point='7' />
+<power test='Psychometabolism' name='Immovability' level='4' desc='You are alomst impossible to move' point='7' />
+<power test='Psychokinesis' name='Inertial Barrier' level='4' desc='Damage reduction 10/+5 up to 10/lvl (max 150)' point='7' />
+<power test='Psychokinesis' name='Mass Concussion' level='4' desc='Foes take 7d4 in 20-ft radius' point='7' />
+<power test='Telepathy' name='Mindwipe' level='4' desc='Subject has recent experiences wiped away' point='7' />
+<power test='Psychometabolism' name='Natural Armor' level='4' desc='Gain +4 Natural Armor Bonus' point='7' />
+<power test='Psychometabolism' name='Polymorph Self' level='4' desc='Assume a new form' point='7' />
+<power test='Psychometabolism' name='Psychofeedback' level='4' desc='Use power points to boost Str, Con or Dex' point='7' />
+<power test='Metacreativity' name='Quintessence' level='4' desc='Collapse a bit of time into a dollop' point='7' />
+<power test='Clairsentience' name='Steadfast Perception' level='4' desc='+4 Bonus vs illusion +2 to Spot and Search' point='7' />
+<power test='Telepathy' name='Tailor Memory' level='4' desc='Implant false memory' point='7' />
+<power test='Psychokinesis' name='Telekinesis' level='4' desc='Move up to 25lbs/lvl' point='7' />
+<power test='Metacreativity' name='Wall of Ectoplasm' level='4' desc='Create a protective barrier' point='7' />
+<power test='Psychometabolism' name='Adamant Grasp' level='5' desc='+10 Bonus to Grapple' point='9' />
+<power test='Psychometabolism' name='Adapt Body' level='5' desc='Adapt your body to hostile enviornments ' point='9' />
+<power test='Metacreativity' name='Astral Construct V' level='5' desc='Create Astral Construct to fight for you' point='9' />
+<power test='Psychoportation' name='Baleful Teleport' level='5' desc='Destructive teleport causes 9d6' point='9' />
+<power test='Psychokinesis' name='Brilliant Blast' level='5' desc='Light blast deals 9d4 in 20ft-radius' point='9' />
+<power test='Telepathy' name='Catapsi' level='5' desc='Psychic static is a drag' point='9' />
+<power test='Psychokinesis' name='Clairtangency' level='5' desc='Use far hand at any distance' point='9' />
+<power test='Metacreativity' name='Ectoplasmic Armor' level='5' desc='Gain +10 Armor Bonus' point='9' />
+<power test='Metacreativity' name='Ectoplasmic Shambler' level='5' desc='Foglike predator deals 1pt/round to creatures it surrounds' point='9' />
+<power test='Psychometabolism' name='Energy Barrier' level='5' desc='Convert energy attacks into harmless light' point='9' />
+<power test='Psychometabolism' name='Graft Weapon' level='5' desc='Cause a weapon to become part of your body' point='9' />
+<power test='Telepathy' name='Greater Domination' level='5' desc='Subject obeys your will' point='9' />
+<power test='Metacreativity' name='Incarnate' level='5' desc='Make some psionic effects permanent' point='9' />
+<power test='Metacreativity' name='Major Creation' level='5' desc='As minor creation plus stone and metal' point='9' />
+<power test='Psychokinesis' name='Matter Rearrangement' level='5' desc='Transmute one metal into another' point='9' />
+<power test='Telepathy' name='Metaconcert' level='5' desc='Join minds with others who also use this power to create an ubermind ' point='9' />
+<power test='Psychometabolism' name='Metamorphosis' level='5' desc='Take the form of creatures or objects' point='9' />
+<power test='Telepathy' name='Mind Probe' level='5' desc='Discover subjects secret thoughts' point='9' />
+<power test='Clairsentience' name='Power Resistance' level='5' desc='Gain Power Resistance 12 + Level' point='9' />
+<power test='Psychokinesis' name='Psychic Vampire' level='5' desc='Touch attack drains 2 power pts/lvl which you gain' point='9' />
+<power test='Clairsentience' name='Recall Agony' level='5' desc='Target takes 9d6 damage' point='9' />
+<power test='Psychoportation' name='Sending' level='5' desc='Send short message anywhere instantly' point='9' />
+<power test='Telepathy' name='Sense Psionics' level='5' desc='Detect Psionics over a large area' point='9' />
+<power test='Psychoportation' name='Teleport' level='5' desc='Instantly transport anywhere' point='9' />
+<power test='Psychoportation' name='Teleport Trigger' level='5' desc='Event triggers teleport' point='9' />
+<power test='Clairsentience' name='True Seeing' level='5' desc='See all things as they really are' point='9' />
+<power test='Psychokinesis' name='Ablating' level='6' desc='Subject is shielded from one negate psionic effect' point='11' />
+<power test='Metacreativity' name='Astral Construct VI' level='6' desc='Create Astral Construct to fight for you' point='11' />
+<power test='Telepathy' name='Aura Alteration' level='6' desc='Subject sees something it is not' point='11' />
+<power test='Psychoportation' name='Banishment' level='6' desc='Banish extraplanar creatures' point='11' />
+<power test='Psychometabolism' name='Breath of the Dragon' level='6' desc='Breath fire for 11d4 damage' point='11' />
+<power test='Psychoportation' name='Call Cohort' level='6' desc='Teleport your cohort to you' point='11' />
+<power test='Psychokinesis' name='Disintegrate' level='6' desc='Disintegrate 1 creature or object' point='11' />
+<power test='Psychoportation' name='Ethereal Jaunt' level='6' desc='Become ethereal for 1rd/lvl' point='11' />
+<power test='Metacreativity' name='Flaming Shroud' level='6' desc='Target takes 11d6' point='11' />
+<power test='Psychokinesis' name='Greater Biocurrent' level='6' desc='Deal 4d6 damage/rd to up to 4 creatures' point='11' />
+<power test='Metacreativity' name='Improved Fabricate' level='6' desc='As fabricate but x10' point='11' />
+<power test='Psychoportation' name='Improved Fly' level='6' desc='Fly at speed of 180ft' point='11' />
+<power test='Psychometabolism' name='Improved Vigor' level='6' desc='Gain 13 temporary hit points' point='11' />
+<power test='Telepathy' name='Mass Suggestion' level='6' desc='Many targets follow suggested action ' point='11' />
+<power test='Telepathy' name='Mind Switch' level='6' desc='You switch minds with another' point='11' />
+<power test='Psychokinesis' name='Null Psionics Field' level='6' desc='Negates psionics within 10ft' point='11' />
+<power test='Clairsentience' name='Precognition' level='6' desc='More indepth then divination' point='11' />
+<power test='Clairsentience' name='Remote View Trap' level='6' desc='Enemy remote viewers take 4d6 damage' point='11' />
+<power test='Psychoportation' name='Retrieve' level='6' desc='Teleport any item you see to your hand' point='11' />
+<power test='Clairsentience' name='Shield of Prudence' level='6' desc='Gain +6 insight bonus to AC' point='11' />
+<power test='Psychometabolism' name='Suspend Life' level='6' desc='Slow your life functions' point='11' />
+<power test='Psychoportation' name='Trace Teleport' level='6' desc='Learn orgin or goal of subjects teleport' point='11' />
+<power test='Metacreativity' name='Astral Construct VII' level='7' desc='Create Astral Construct to fight for you' point='13' />
+<power test='Metacreativity' name='Contingency' level='7' desc='Sets trigger condition for another power' point='13' />
+<power test='Psychoportation' name='Divert Teleport' level='7' desc='Choose destination for anothers teleport' point='13' />
+<power test='Clairsentience' name='Emulate Power' level='7' desc='Manifest any psionic power of 6th level or lower' point='13' />
+<power test='Psychometabolism' name='Energy Conversion' level='7' desc='Convert energy attacks to one ray energy attack of your own' point='13' />
+<power test='Psychoportation' name='Etherlealness' level='7' desc='Travel to the ethereal plane with compainions' point='13' />
+<power test='Psychometabolism' name='Fission' level='7' desc='You briefly duplicate yourself' point='13' />
+<power test='Clairsentience' name='Improved Anchored Navigation' level='7' desc='You can navigate from a fixed point even across planes' point='13' />
+<power test='Telepathy' name='Insanity' level='7' desc='Subject is permanently confused' point='13' />
+<power test='Metacreativity' name='Mass Cocoon' level='7' desc='As ectoplasmic cocoon but bigger' point='13' />
+<power test='Telepathy' name='Mass Domination' level='7' desc='Many targets subject to your will' point='13' />
+<power test='Psychometabolism' name='Oak Body' level='7' desc='Your body becomes living wood' point='13' />
+<power test='Psychoportation' name='Phase Door' level='7' desc='Invisible passage through wood or stone' point='13' />
+<power test='Psychoportation' name='Plane Shift' level='7' desc='Up to 8 subjects can travel to another plane' point='13' />
+<power test='Psychokinesis' name='Power Turning' level='7' desc='Reflect 1d4+6 power level back at manifester' point='13' />
+<power test='Psychokinesis' name='Reddopsi' level='7' desc='Reflect psionic powers' point='13' />
+<power test='Clairsentience' name='Sequester' level='7' desc='Subject is invisible to sight and remote viewing' point='13' />
+<power test='Psychoportation' name='Teleport without Error' level='7' desc='As Teleport but no off target arrivals' point='13' />
+<power test='Psychokinesis' name='True Concussion' level='7' desc='Pummel foe for 13d6 damage' point='13' />
+<power test='Telepathy' name='Ultrablast' level='7' desc='Mental scream deals 13d4 to all within 15ft' point='13' />
+<power test='Metacreativity' name='Astral Construct VIII' level='8' desc='Create Astral Construct to fight for you' point='15' />
+<power test='Psychoportation' name='Dream Travel' level='8' desc='You travel to other places through your dreams' point='15' />
+<power test='Clairsentience' name='Foresight' level='8' desc='Psionic senses warn you of impending danger' point='15' />
+<power test='Clairsentience' name='Hypercognition' level='8' desc='You can deduce almost anything' point='15' />
+<power test='Psychokinesis' name='Improved Clairtangency' level='8' desc='You use telekinesis at any distance.' point='15' />
+<power test='Psychoportation' name='Improved Etherealness' level='8' desc='As etherealness but longer (10 mins/lvl).' point='15' />
+<power test='Psychometabolism' name='Iron Body' level='8' desc='Your body becomes living iron. Damage Reduction 50/+3' point='15' />
+<power test='Psychokinesis' name='Matter Manipulation' level='8' desc='Increase or decrease an objects base hardness by 5.' point='15' />
+<power test='Telepathy' name='Mind Blank' level='8' desc='Immune to mental/emotional effects, scrying and remotve viewing.' point='15' />
+<power test='Telepathy' name='Mind Seed' level='8' desc='Subject slowly becomes you.' point='15' />
+<power test='Metacreativity' name='Mind Stone' level='8' desc='Store your personality against future need.' point='15' />
+<power test='Clairsentience' name='Recall Death' level='8' desc='Save vs Will or die, else take 3d6+15 damage.' point='15' />
+<power test='Psychometabolism' name='Shadow Body' level='8' desc='You become a living shadow (damage reduction 50/+5)' point='15' />
+<power test='Psychokinesis' name='Telekinetic Sphere' level='8' desc='Mobile force globe protects one subject' point='15' />
+<power test='Psychoportation' name='Teleportation Circle' level='8' desc='Circle teleports any creatures inside it.' point='15' />
+<power test='Psychoportation' name='Temporal Acceleration' level='8' desc='Your time frame accelerates for 2 rounds' point='15' />
+<power test='Metacreativity' name='True Creation' level='8' desc='As major creation but items are permanent.' point='15' />
+<power test='Telepathy' name='True Domination' level='8' desc='Dominated subjects less likely to defy your will.' point='15' />
+<power test='Psychometabolism' name='Affinity Field' level='9' desc='Effects that affect you also affect others.' point='17' />
+<power test='Telepathy' name='Apopsi' level='9' desc='Delete the psionic power of another.' point='17' />
+<power test='Metacreativity' name='Astral Construct IX' level='9' desc='Create Astral Construct to fight for you' point='17' />
+<power test='Psychoportation' name='Astral Projection' level='9' desc='Project yourself and others into the Astral Plane.' point='17' />
+<power test='Telepathy' name='Confidante' level='9' desc='You and antoher share a permanent mental bond.' point='17' />
+<power test='Psychokinesis' name='Detonation' level='9' desc='Pummel foe for 17d6 damage.' point='17' />
+<power test='Psychokinesis' name='Dissolution' level='9' desc='Disintegrate very large obecjts or creatures' point='17' />
+<power test='Metacreativity' name='Genesis' level='9' desc='You instigate a new demiplane in the Astral Plane.' point='17' />
+<power test='Clairsentience' name='Greater Emulation' level='9' desc='Manifest any psionic power of 8th level or lower.' point='17' />
+<power test='Clairsentience' name='Metafaculty' level='9' desc='Subject can not hide name or location from you.' point='17' />
+<power test='Telepathy' name='Microcosm' level='9' desc='Subject explores imaginary world at expense of the real one.' point='17' />
+<power test='Telepathy' name='Monster Domination' level='9' desc='Controls any creature but for shorter time.' point='17' />
+<power test='Psychoportation' name='Probability Travel' level='9' desc='You and others physically enter Astral Plane.' point='17' />
+<power test='Telepathy' name='Psychic Chirurgery' level='9' desc='Repair psychic damage or impart new psionic power' point='17' />
+<power test='Psychometabolism' name='Shapechange' level='9' desc='You become any creature, change forms 1 per/rd.' point='17' />
+<power test='Psychoportation' name='Temporal Velocity' level='9' desc='Your time frame accelerates for 3d4 rounds.' point='17' />
+<power test='Psychoportation' name='Time Regression' level='9' desc='Relive the last 1d4+1 rounds.' point='17' />
+<power test='Telepathy' name='Thrall' level='9' desc='Subject is your slave forever.' point='17' />
+<power test='Psychometabolism' name='True Metabolism' level='9' desc='Regenerate 10pts / round for 1 min.' point='17' />
+<power test='Psychokinesis' name='True Telekinesis' level='9' desc='Lift or move 500lbs/level at long range.' point='17' />
+</powers>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/dnd3espells.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,326 @@
+<spells>
+<spell name='Resistance' memrz='0' level='0' desc='Subject gains +1 on saving throws.' />
+<spell name='Ray of Frost' memrz='0' level='0' desc='Ray deals 1d3 cold damage.' />
+<spell name='Detect Poison' memrz='0' level='0' desc='Detects poison in one creature or small object.' />
+<spell name='Daze' memrz='0' level='0' desc='Creature loses next action.' />
+<spell name='Flare' memrz='0' level='0' desc='Dazzles one creature (-1 attack).' />
+<spell name='Light' memrz='0' level='0' desc='Object shines like a torch.' />
+<spell name='Dancing Lights' memrz='0' level='0' desc='Figment torches or other lights.' />
+<spell name='Ghost Sound' memrz='0' level='0' desc='Figment sounds.' />
+<spell name='Disrupt Undead' memrz='0' level='0' desc='Deals 1d6 damage to one undead.' />
+<spell name='Mage Hand' memrz='0' level='0' desc='5-pound telekinesis.' />
+<spell name='Mending' memrz='0' level='0' desc='Makes minor repairs on an object.' />
+<spell name='Open/Close' memrz='0' level='0' desc='Opens or closes small or light things.' />
+<spell name='Arcane Mark' memrz='0' level='0' desc='Inscribes a personal rune (visible or invisible).' />
+<spell name='Detect Magic' memrz='0' level='0' desc='Detects spells and magic items within 60 ft.' />
+<spell name='Prestidigitation' memrz='0' level='0' desc='Performs minor tricks.' />
+<spell name='Read Magic' memrz='0' level='0' desc='Read scrolls and spellbooks.' />
+<spell name='Alarm' memrz='0' level='1' desc='Wards an area for 2 hours/level.' />
+<spell name='Endure Elements' memrz='0' level='1' desc='Ignores 5 damage/round from one energy type.' />
+<spell name='Hold Portal' memrz='0' level='1' desc='Holds door shut.' />
+<spell name='Protection from Chaos/Evil/Good/Law' memrz='0' level='1' desc='+2 AC and saves, counter mind control, hedge out elementals and outsiders.' />
+<spell name='Shield' memrz='0' level='1' desc='Invisible disc gives cover and blocks magic missiles.' />
+<spell name='Grease' memrz='0' level='1' desc='Makes 10-ft. square or one object slippery.' />
+<spell name='Mage Armor' memrz='0' level='1' desc='Gives subject +4 armor bonus.' />
+<spell name='Mount' memrz='0' level='1' desc='Summons riding horse for 2 hr./level.' />
+<spell name='Obscuring Mist' memrz='0' level='1' desc='Fog surrounds you.' />
+<spell name='Summon Monster I' memrz='0' level='1' desc='Calls outsider to fight for you.' />
+<spell name='Unseen Servant' memrz='0' level='1' desc='Creates invisible force that obeys your commands.' />
+<spell name='Comprehend Languages' memrz='0' level='1' desc='Understands all spoken and written languages.' />
+<spell name='Detect Secret Doors' memrz='0' level='1' desc='Reveals hidden doors within 60 ft.' />
+<spell name='Detect Undead' memrz='0' level='1' desc='Reveals undead within 60 ft.' />
+<spell name='Identify' memrz='0' level='1' desc='Determines single feature of magic item.' />
+<spell name='True Strike' memrz='0' level='1' desc='Adds +20 bonus to your next attack roll.' />
+<spell name='Charm Person' memrz='0' level='1' desc='Makes one person your friend.' />
+<spell name='Hypnotism' memrz='0' level='1' desc='Fascinates 2d4 HD of creatures.' />
+<spell name='Sleep' memrz='0' level='1' desc='Put 2d4 HD of creatures into comatose slumber.' />
+<spell name='Magic Missile' memrz='0' level='1' desc='1d4+1 damage; +1 missile/two levels above 1st (max +5).' />
+<spell name='Tensers Floating Disk' memrz='0' level='1' desc='3-ft.-diameter horizontal disk that holds 100 lb./level.' />
+<spell name='Change Self' memrz='0' level='1' desc='Changes your appearance.' />
+<spell name='Color Spray' memrz='0' level='1' desc='Knocks unconscious, blinds, or stuns 1d6 weak creatures.' />
+<spell name='Nystuls Magical Aura' memrz='0' level='1' desc='Grants object false magic aura.' />
+<spell name='Nystuls Undetectable Aura' memrz='0' level='1' desc='Masks magic items aura.' />
+<spell name='Silent Image' memrz='0' level='1' desc='Creates minor illusion of your design.' />
+<spell name='Ventriloquism' memrz='0' level='1' desc='Throws voice for 1 min./level.' />
+<spell name='Cause Fear' memrz='0' level='1' desc='One creature flees for 1d4 rounds.' />
+<spell name='Chill Touch' memrz='0' level='1' desc='1 touch/level deals 1d6 damage and possibly 1 Str damage.' />
+<spell name='Ray of Enfeeblement' memrz='0' level='1' desc='Ray reduces Str by 1d6 points +1 point/two levels.' />
+<spell name='Animate Rope' memrz='0' level='1' desc='Makes a rope move at your command.' />
+<spell name='Burning Hands' memrz='0' level='1' desc='1d4 fire damage/level (max: 5d4).' />
+<spell name='Enlarge' memrz='0' level='1' desc='Object or creature grows +10%/level (max +50%).' />
+<spell name='Erase' memrz='0' level='1' desc='Mundane or magical writing vanishes.' />
+<spell name='Expeditious Retreat' memrz='0' level='1' desc='Doubles your speed.' />
+<spell name='Feather Fall' memrz='0' level='1' desc='Objects or creatures fall slowly.' />
+<spell name='Jump' memrz='0' level='1' desc='Subject gets +30 on Jump checks.' />
+<spell name='Magic Weapon' memrz='0' level='1' desc='Weapon gains +1 bonus.' />
+<spell name='Message' memrz='0' level='1' desc='Whispered conversation at distance.' />
+<spell name='Reduce' memrz='0' level='1' desc='Object or creature shrinks 10%/level (max 50%).' />
+<spell name='Shocking Grasp' memrz='0' level='1' desc='Touch delivers 1d8 +1/level electricity.' />
+<spell name='Spider Climb' memrz='0' level='1' desc='Grants ability to walk on walls and ceilings.' />
+<spell name='Arcane Lock' memrz='0' level='2' desc='Magically locks a portal or chest.' />
+<spell name='Obscure Object' memrz='0' level='2' desc='Masks object against divination.' />
+<spell name='Protection from Arrows' memrz='0' level='2' desc='Subject immune to most ranged attacks.' />
+<spell name='Resist Elements' memrz='0' level='2' desc='Ignores 12 damage/round from one energy type.' />
+<spell name='Fog Cloud' memrz='0' level='2' desc='Fog obscures vision.' />
+<spell name='Glitterdust' memrz='0' level='2' desc='Blinds creatures, outlines invisible creatures.' />
+<spell name='Melfs Acid Arrow' memrz='0' level='2' desc='Ranged touch attack; 2d4 damage for 1 round + 1 round/three levels.' />
+<spell name='Summon Monster II' memrz='0' level='2' desc='Calls outsider to fight for you.' />
+<spell name='Summon Swarm' memrz='0' level='2' desc='Summons swarm of small crawling or flying creatures.' />
+<spell name='Web' memrz='0' level='2' desc='Fills 10-ft. cube/level with sticky spider webs.' />
+<spell name='Detect Thoughts' memrz='0' level='2' desc='Allows listening to surface thoughts.' />
+<spell name='Locate Object' memrz='0' level='2' desc='Senses direction toward object (specific or type).' />
+<spell name='See Invisibility' memrz='0' level='2' desc='Reveals invisible creatures or objects.' />
+<spell name='Tashas Hideous Laughter' memrz='0' level='2' desc='Subject loses actions for 1d3 rounds.' />
+<spell name='Darkness' memrz='0' level='2' desc='20-ft. radius of supernatural darkness.' />
+<spell name='Daylight' memrz='0' level='2' desc='60-ft. radius of bright light.' />
+<spell name='Flaming Sphere' memrz='0' level='2' desc='Rolling ball of fire, 2d6 damage, lasts 1 round/level.' />
+<spell name='Shatter' memrz='0' level='2' desc='Sonic vibration damages objects or crystalline creatures.' />
+<spell name='Blur' memrz='0' level='2' desc='Attacks miss subject 20% of the time.' />
+<spell name='Continual Flame' memrz='0' level='2' desc='Makes a permanent, heatless torch.' />
+<spell name='Hypnotic Pattern' memrz='0' level='2' desc='Fascinates 2d4+1 HD/level of creatures.' />
+<spell name='Invisibility' memrz='0' level='2' desc='Subject is invisible for 10 min./level or until it attacks.' />
+<spell name='Leomunds Trap' memrz='0' level='2' desc='Makes item seem trapped.' />
+<spell name='Magic Mouth' memrz='0' level='2' desc='Speaks once when triggered.' />
+<spell name='Minor Image' memrz='0' level='2' desc='As silent image, plus some sound.' />
+<spell name='Mirror Image' memrz='0' level='2' desc='Creates decoy duplicates of you (1d4 +1/three levels, max 8).' />
+<spell name='Misdirection' memrz='0' level='2' desc='Misleads divinations for one creature or object.' />
+<spell name='Ghoul Touch' memrz='0' level='2' desc='Paralyzes one subject, who exudes stench (-2 penalty) nearby.' />
+<spell name='Scare' memrz='0' level='2' desc='Panics creatures up to 5 HD (15-ft. radius).' />
+<spell name='Spectral Hand' memrz='0' level='2' desc='Creates disembodied glowing hand to deliver touch attacks.' />
+<spell name='Alter Self' memrz='0' level='2' desc='As change self, plus more drastic changes.' />
+<spell name='Blindness/Deafness' memrz='0' level='2' desc='Makes subject blind or deaf.' />
+<spell name='Bulls Strength' memrz='0' level='2' desc='Subject gains 1d4+1 Str for 1 hr./level.' />
+<spell name='Cats Grace' memrz='0' level='2' desc='Subject gains 1d4+1 Dex for 1 hr./level.' />
+<spell name='Darkvision' memrz='0' level='2' desc='See 60 ft. in total darkness.' />
+<spell name='Endurance' memrz='0' level='2' desc='Gain 1d4+1 Con for 1 hr./level.' />
+<spell name='Knock' memrz='0' level='2' desc='Opens locked or magically sealed door.' />
+<spell name='Levitate' memrz='0' level='2' desc='Subject moves up and down at your direction.' />
+<spell name='Pyrotechnics' memrz='0' level='2' desc='Turns fire into blinding light or choking smoke.' />
+<spell name='Rope Trick' memrz='0' level='2' desc='Up to eight creatures hide in extradimensional space.' />
+<spell name='Whispering Wind' memrz='0' level='2' desc='Sends a short message one mile/level.' />
+<spell name='Dispel Magic' memrz='0' level='3' desc='Cancels magical spells and effects.' />
+<spell name='Explosive Runes' memrz='0' level='3' desc='Deals 6d6 damage when read.' />
+<spell name='Magic Circle against Chaos/Evil/Good/Law' memrz='0' level='3' desc='As protection spells, but 10-ft. radius and 10 min./level.' />
+<spell name='Nondetection' memrz='0' level='3' desc='Hides subject from divination, scrying.' />
+<spell name='Protection from Elements' memrz='0' level='3' desc='Absorb 12 damage/level from one kind of energy.' />
+<spell name='Flame Arrow' memrz='0' level='3' desc='Shoots flaming projectiles (extra damage) or fiery bolts (4d6 damage).' />
+<spell name='Phantom Steed' memrz='0' level='3' desc='Magical horse appears for 1 hour/level.' />
+<spell name='Sepia Snake Sigil' memrz='0' level='3' desc='Creates text symbol that immobilizes reader.' />
+<spell name='Sleet Storm' memrz='0' level='3' desc='Hampers vision and movement.' />
+<spell name='Stinking Cloud' memrz='0' level='3' desc='Nauseating vapors, 1 round/level.' />
+<spell name='Summon Monster III' memrz='0' level='3' desc='Calls outsider to fight for you.' />
+<spell name='Clairaudience/Clairvoyance' memrz='0' level='3' desc='Hear or see at a distance for 1 min./level.' />
+<spell name='Tongues' memrz='0' level='3' desc='Speak any language.' />
+<spell name='Hold Person' memrz='0' level='3' desc='Holds one person helpless; 1 round/level.' />
+<spell name='Suggestion' memrz='0' level='3' desc='Compels subject to follow stated course of action.' />
+<spell name='Fireball' memrz='0' level='3' desc='1d6 damage per level, 20-ft. radius.' />
+<spell name='Gust of Wind' memrz='0' level='3' desc='Blows away or knocks down smaller creatures.' />
+<spell name='Leomunds Tiny Hut' memrz='0' level='3' desc='Creates shelter for 10 creatures.' />
+<spell name='Lightning Bolt' memrz='0' level='3' desc='Electricity deals 1d6 damage/level.' />
+<spell name='Wind Wall' memrz='0' level='3' desc='Deflects arrows, smaller creatures, and gases.' />
+<spell name='Displacement' memrz='0' level='3' desc='Attacks miss subject 50%.' />
+<spell name='Illusory Script' memrz='0' level='3' desc='Only intended reader can decipher.' />
+<spell name='Invisibility Sphere' memrz='0' level='3' desc='Makes everyone within 10 ft. invisible.' />
+<spell name='Major Image' memrz='0' level='3' desc='As silent image, plus sound, smell and thermal effects.' />
+<spell name='Gentle Repose' memrz='0' level='3' desc='Preserves one corpse.' />
+<spell name='Halt Undead' memrz='0' level='3' desc='Immobilizes undead for 1 round/level.' />
+<spell name='Vampiric Touch' memrz='0' level='3' desc='Touch deals 1d6/two caster levels; caster gains damage as hp.' />
+<spell name='Blink' memrz='0' level='3' desc='You randomly vanish and reappear for 1 round/level.' />
+<spell name='Fly' memrz='0' level='3' desc='Subject flies at speed of 90.' />
+<spell name='Gaseous Form' memrz='0' level='3' desc='Subject becomes insubstantial and can fly slowly.' />
+<spell name='Greater Magic Weapon' memrz='0' level='3' desc='+1/three levels (max +5).' />
+<spell name='Haste' memrz='0' level='3' desc='Extra partial action and +4 AC.' />
+<spell name='Keen Edge' memrz='0' level='3' desc='Doubles normal weapons threat range.' />
+<spell name='Secret Page' memrz='0' level='3' desc='Changes one page to hide its real content.' />
+<spell name='Shrink Item' memrz='0' level='3' desc='Object shrinks to one-twelfth size.' />
+<spell name='Slow' memrz='0' level='3' desc='One subject/level takes only partial actions, -2 AC, -2 melee rolls.' />
+<spell name='Water Breathing' memrz='0' level='3' desc='Subjects can breathe underwater.' />
+<spell name='Dimensional Anchor' memrz='0' level='4' desc='Bars extradimensional movement.' />
+<spell name='Fire Trap' memrz='0' level='4' desc='Opened object deals 1d4 +1/level damage.' />
+<spell name='Minor Globe of Invulnerability' memrz='0' level='4' desc='Stops 1st- through 3rd-level spell effects.' />
+<spell name='Remove Curse' memrz='0' level='4' desc='Frees object or person from curse.' />
+<spell name='Stoneskin' memrz='0' level='4' desc='Stops blows, cuts, stabs, and slashes.' />
+<spell name='Evards Black Tentacles' memrz='0' level='4' desc='1d4 +1/level tentacles grapple randomly within 15 ft.' />
+<spell name='Leomunds Secure Shelter' memrz='0' level='4' desc='Creates sturdy cottage.' />
+<spell name='Minor Creation' memrz='0' level='4' desc='Creates one cloth or wood object.' />
+<spell name='Solid Fog' memrz='0' level='4' desc='Blocks vision and slows movement.' />
+<spell name='Summon Monster IV' memrz='0' level='4' desc='Calls outsider to fight for you.' />
+<spell name='Arcane Eye' memrz='0' level='4' desc='Invisible floating eye moves 30 ft./round.' />
+<spell name='Detect Scrying' memrz='0' level='4' desc='Alerts you of magical eavesdropping.' />
+<spell name='Locate Creature' memrz='0' level='4' desc='Indicates direction to familiar creature.' />
+<spell name='Scrying' memrz='0' level='4' desc='Spies on subject from a distance.' />
+<spell name='Charm Monster' memrz='0' level='4' desc='Makes monster believe it is your ally.' />
+<spell name='Confusion' memrz='0' level='4' desc='Makes subject behave oddly for 1 round/level.' />
+<spell name='Emotion' memrz='0' level='4' desc='Arouses strong emotion in subject.' />
+<spell name='Lesser Geas' memrz='0' level='4' desc='Commands subject of 7 HD or less.' />
+<spell name='Fire Shield' memrz='0' level='4' desc='Creatures attacking you take fire damage; you are protected from heat or cold.' />
+<spell name='Ice Storm' memrz='0' level='4' desc='Hail deals 5d6 damage in cylinder 40 ft. across.' />
+<spell name='Otilukes Resilient Sphere' memrz='0' level='4' desc='Force globe protects but traps one subject.' />
+<spell name='Shout' memrz='0' level='4' desc='Deafens all within cone and deals 2d6 damage.' />
+<spell name='Wall of Fire' memrz='0' level='4' desc='Deals 2d4 fire damage out to 10 ft. and 1d4 out to 20 ft.' />
+<spell name='Wall of Ice' memrz='0' level='4' desc='Ice plane creates wall with 15 hp +1/level, or hemisphere can trap creatures inside.' />
+<spell name='Hallucinatory Terrain' memrz='0' level='4' desc='Makes one type of terrain appear like another (field into forest, etc).' />
+<spell name='Illusory Wall' memrz='0' level='4' desc='Wall, floor, or ceiling looks real, but anything can pass through.' />
+<spell name='Improved Invisibility' memrz='0' level='4' desc='As invisibility, but subject can attack and stay invisible.' />
+<spell name='Phantasmal Killer' memrz='0' level='4' desc='Fearsome illusion kills subject or deals 3d6 damage.' />
+<spell name='Rainbow Pattern' memrz='0' level='4' desc='Lights prevent 24 HD of creatures from attacking or moving away.' />
+<spell name='Shadow Conjuration' memrz='0' level='4' desc='Mimics conjuring below 4th level.' />
+<spell name='Contagion' memrz='0' level='4' desc='Infects subject with chosen disease.' />
+<spell name='Enervation' memrz='0' level='4' desc='Subject gains 1d4 negative levels.' />
+<spell name='Fear' memrz='0' level='4' desc='Subjects within cone flee for 1 round/level.' />
+<spell name='Bestow Curse' memrz='0' level='4' desc='-6 to an ability; -4 on attacks, saves, and checks; or 50% chance of losing each action.' />
+<spell name='Dimension Door' memrz='0' level='4' desc='Teleports you and up to 500 lb.' />
+<spell name='Polymorph Other' memrz='0' level='4' desc='Gives one subject a new form.' />
+<spell name='Polymorph Self' memrz='0' level='4' desc='You assume a new form.' />
+<spell name='Rarys Mnemonic Enhancer' memrz='0' level='4' desc='Prepares extra spells or retains one just cast. Wizard only.' />
+<spell name='Dismissal' memrz='0' level='5' desc='Forces a creature to return to native plane.' />
+<spell name='Cloudkill' memrz='0' level='5' desc='Kills 3 HD or less; 4-6 HD save or die.' />
+<spell name='Leomunds Secret Chest' memrz='0' level='5' desc='Hides expensive chest on Ethereal Plane; you retrieve it at will.' />
+<spell name='Lesser Planar Binding' memrz='0' level='5' desc='Traps outsider until it performs a task.' />
+<spell name='Major Creation' memrz='0' level='5' desc='As minor creation, plus stone and metal.' />
+<spell name='Mordenkainens Faithful Hound' memrz='0' level='5' desc='Phantom dog can guard, attack.' />
+<spell name='Summon Monster V' memrz='0' level='5' desc='Calls outsider to fight for you.' />
+<spell name='Wall of Iron' memrz='0' level='5' desc='30 hp/four levels; can topple onto foes.' />
+<spell name='Wall of Stone' memrz='0' level='5' desc='20 hp/four levels; can be shaped.' />
+<spell name='Contact Other Plane' memrz='0' level='5' desc='Ask question of extraplanar entity.' />
+<spell name='Prying Eyes' memrz='0' level='5' desc='1d4 floating eyes +1/level scout for you.' />
+<spell name='Rarys Telepathic Bond' memrz='0' level='5' desc='Link lets allies communicate.' />
+<spell name='Dominate Person' memrz='0' level='5' desc='Controls humanoid telepathically.' />
+<spell name='Feeblemind' memrz='0' level='5' desc='Subjects Int drops to 1.' />
+<spell name='Hold Monster' memrz='0' level='5' desc='As hold person, but any creature.' />
+<spell name='Mind Fog' memrz='0' level='5' desc='Subjects in fog get -10 Wis, Will checks.' />
+<spell name='Bigbys Interposing Hand' memrz='0' level='5' desc='Hand provides 90% cover against one opponent.' />
+<spell name='Cone of Cold' memrz='0' level='5' desc='1d6 cold damage/level.' />
+<spell name='Sending' memrz='0' level='5' desc='Delivers short message anywhere, instantly.' />
+<spell name='Wall of Force' memrz='0' level='5' desc='Wall is immune to damage.' />
+<spell name='Dream' memrz='0' level='5' desc='Sends message to anyone sleeping.' />
+<spell name='False Vision' memrz='0' level='5' desc='Fools scrying with an illusion.' />
+<spell name='Greater Shadow Conjuration' memrz='0' level='5' desc='As shadow conjuration, but up to 4th level and 40% real.' />
+<spell name='Mirage Arcana' memrz='0' level='5' desc='As hallucinatory terrain, plus structures.' />
+<spell name='Nightmare' memrz='0' level='5' desc='Sends vision dealing 1d10 damage, fatigue.' />
+<spell name='Persistent Image' memrz='0' level='5' desc='As major image, but no concentration required.' />
+<spell name='Seeming' memrz='0' level='5' desc='Changes appearance of one person/two levels.' />
+<spell name='Shadow Evocation' memrz='0' level='5' desc='Mimics evocation less than 5th level.' />
+<spell name='Animate Dead' memrz='0' level='5' desc='Creates undead skeletons and zombies.' />
+<spell name='Magic Jar' memrz='0' level='5' desc='Enables possession of another creature.' />
+<spell name='Animal Growth' memrz='0' level='5' desc='One animal/two levels doubles in size, HD.' />
+<spell name='Fabricate' memrz='0' level='5' desc='Transforms raw materials into finished items.' />
+<spell name='Passwall' memrz='0' level='5' desc='Breaches walls 1 ft. thick/level.' />
+<spell name='Stone Shape' memrz='0' level='5' desc='Sculpts stone into any form.' />
+<spell name='Telekinesis' memrz='0' level='5' desc='Lifts or moves 25 lb./level at long range.' />
+<spell name='Teleport' memrz='0' level='5' desc='Instantly transports you anywhere.' />
+<spell name='Transmute Mud to Rock' memrz='0' level='5' desc='Transforms two 10-ft. cubes/level.' />
+<spell name='Transmute Rock to Mud' memrz='0' level='5' desc='Transforms two 10-ft. cubes/level.' />
+<spell name='Permanency' memrz='0' level='5' desc='Makes certain spells permanent; costs XP.' />
+<spell name='Antimagic Field' memrz='0' level='6' desc='Negates magic within 10 ft.' />
+<spell name='Globe of Invulnerability' memrz='0' level='6' desc='As minor globe, plus 4th level.' />
+<spell name='Greater Dispelling' memrz='0' level='6' desc='As dispel magic, but +20 on check.' />
+<spell name='Guards and Wards' memrz='0' level='6' desc='Array of magic effects protect area.' />
+<spell name='Repulsion' memrz='0' level='6' desc='Creatures cant approach you.' />
+<spell name='Acid Fog' memrz='0' level='6' desc='Fog deals acid damage.' />
+<spell name='Planar Binding' memrz='0' level='6' desc='As lesser planar binding, but up to 16 HD.' />
+<spell name='Summon Monster VI' memrz='0' level='6' desc='Calls outsider to fight for you.' />
+<spell name='Analyze Dweomer' memrz='0' level='6' desc='Reveals magical aspects of subject.' />
+<spell name='Legend Lore' memrz='0' level='6' desc='Learn tales about a person, place, or thing.' />
+<spell name='True Seeing' memrz='0' level='6' desc='See all things as they really are.' />
+<spell name='Geas/Quest' memrz='0' level='6' desc='As lesser geas, plus it affects any creature.' />
+<spell name='Mass Suggestion' memrz='0' level='6' desc='As suggestion, plus one/level subjects.' />
+<spell name='Bigbys Forceful Hand' memrz='0' level='6' desc='Hand pushes creatures away.' />
+<spell name='Chain Lightning' memrz='0' level='6' desc='1d6 damage/level; secondary bolts.' />
+<spell name='Contingency' memrz='0' level='6' desc='Sets trigger condition for another spell.' />
+<spell name='Otilukes Freezing Sphere' memrz='0' level='6' desc='Freezes water or deals cold damage.' />
+<spell name='Greater Shadow Evocation' memrz='0' level='6' desc='As shadow evocation, but up to 5th level.' />
+<spell name='Mislead' memrz='0' level='6' desc='Turns you invisible and creates illusory double.' />
+<spell name='Permanent Image' memrz='0' level='6' desc='Includes sight, sound, and smell.' />
+<spell name='Programmed Image' memrz='0' level='6' desc='As major image, plus triggered by event.' />
+<spell name='Project Image' memrz='0' level='6' desc='Illusory double can talk and cast spells.' />
+<spell name='Shades' memrz='0' level='6' desc='As shadow conjuration, but up to 5th level and 60% real.' />
+<spell name='Veil' memrz='0' level='6' desc='Changes appearance of group of creatures.' />
+<spell name='Circle of Death' memrz='0' level='6' desc='Kills 1d4 HD/level.' />
+<spell name='Control Water' memrz='0' level='6' desc='Raises, lowers, or parts bodies of water.' />
+<spell name='Control Weather' memrz='0' level='6' desc='Changes weather in local area.' />
+<spell name='Disintegrate' memrz='0' level='6' desc='Makes one creature or object vanish.' />
+<spell name='Eyebite' memrz='0' level='6' desc='Charm, fear, sicken or sleep one subject.' />
+<spell name='Flesh to Stone' memrz='0' level='6' desc='Turns subject creature into statue.' />
+<spell name='Mass Haste' memrz='0' level='6' desc='As haste, affects one/level subjects.' />
+<spell name='Mordenkainens Lucubration' memrz='0' level='6' desc='Recalls spell of 5th level or less. Wizard Only.' />
+<spell name='Move Earth' memrz='0' level='6' desc='Digs trenches and build hills.' />
+<spell name='Stone to Flesh' memrz='0' level='6' desc='Restores petrified creature.' />
+<spell name='Tensers Transformation' memrz='0' level='6' desc='You gain combat bonuses.' />
+<spell name='Banishment' memrz='0' level='7' desc='Banishes 2 HD/level extraplanar creatures.' />
+<spell name='Sequester' memrz='0' level='7' desc='Subject is invisible to sight and scrying.' />
+<spell name='Spell Turning' memrz='0' level='7' desc='Reflect 1d4+6 spell levels back at caster.' />
+<spell name='Drawmijs Instant Summons' memrz='0' level='7' desc='Prepared object appears in your hand.' />
+<spell name='Mordenkainens Magnificent Mansion' memrz='0' level='7' desc='Door leads to extradimensional mansion.' />
+<spell name='Phase Door' memrz='0' level='7' desc='Invisible passage through wood or stone.' />
+<spell name='Power Word, Stun' memrz='0' level='7' desc='Stuns creature with up to 150 hp.' />
+<spell name='Summon Monster VII' memrz='0' level='7' desc='Calls outsider to fight for you.' />
+<spell name='Greater Scrying' memrz='0' level='7' desc='As scrying, but faster and longer.' />
+<spell name='Vision' memrz='0' level='7' desc='As legend lore, but quicker and strenuous.' />
+<spell name='Insanity' memrz='0' level='7' desc='Subject suffers continuous confusion.' />
+<spell name='Bigbys Grasping Hand' memrz='0' level='7' desc='Hand provides cover, pushes, or grapples.' />
+<spell name='Delayed Blast Fireball' memrz='0' level='7' desc='1d8 fire damage/level; you can delay blast for 5 rounds.' />
+<spell name='Forcecage' memrz='0' level='7' desc='Cube of force imprisons all inside.' />
+<spell name='Mordenkainens Sword' memrz='0' level='7' desc='Floating magic blade strikes opponents.' />
+<spell name='Prismatic Spray' memrz='0' level='7' desc='Rays hit subjects with variety of effects.' />
+<spell name='Mass Invisibility' memrz='0' level='7' desc='As invisibility, but affects all in range.' />
+<spell name='Shadow Walk' memrz='0' level='7' desc='Step into shadow to travel rapidly.' />
+<spell name='Simulacrum' memrz='0' level='7' desc='Creates partially real double of a creature.' />
+<spell name='Control Undead' memrz='0' level='7' desc='Undead dont attack you while under your command.' />
+<spell name='Finger of Death' memrz='0' level='7' desc='Kills one subject.' />
+<spell name='Ethereal Jaunt' memrz='0' level='7' desc='You become ethereal for 1 round/level.' />
+<spell name='Plane Shift' memrz='0' level='7' desc='Up to eight subjects travel to another plane.' />
+<spell name='Reverse Gravity' memrz='0' level='7' desc='Objects and creatures fall upward.' />
+<spell name='Statue' memrz='0' level='7' desc='Subject can become a statue at will.' />
+<spell name='Teleport without Error' memrz='0' level='7' desc='As teleport, but no off-target arrival.' />
+<spell name='Vanish' memrz='0' level='7' desc='As teleport, but affects a touched object.' />
+<spell name='Limited Wish' memrz='0' level='7' desc='Alters reality-within spell limits.' />
+<spell name='Mind Blank' memrz='0' level='8' desc='Subject is immune to mental/emotional magic and scrying.' />
+<spell name='Prismatic Wall' memrz='0' level='8' desc='Walls colors have array of effects.' />
+<spell name='Protection from Spells' memrz='0' level='8' desc='Confers +8 resistance bonus.' />
+<spell name='Greater Planar Binding' memrz='0' level='8' desc='As lesser planar binding, but up to 24 HD.' />
+<spell name='Incendiary Cloud' memrz='0' level='8' desc='Cloud deals 4d6 fire damage/round.' />
+<spell name='Maze' memrz='0' level='8' desc='Traps subject in extradimensional maze.' />
+<spell name='Power Word, Blind' memrz='0' level='8' desc='Blinds 200 hp worth of creatures.' />
+<spell name='Summon Monster VIII' memrz='0' level='8' desc='Calls outsider to fight for you.' />
+<spell name='Trap the Soul' memrz='0' level='8' desc='Imprisons subject within gem.' />
+<spell name='Discern Location' memrz='0' level='8' desc='Exact location of creature or object.' />
+<spell name='Antipathy' memrz='0' level='8' desc='Object or location affected by spell repels certain creatures.' />
+<spell name='Binding' memrz='0' level='8' desc='Array of techniques to imprison a creature.' />
+<spell name='Demand' memrz='0' level='8' desc='As sending, plus you can send suggestion.' />
+<spell name='Mass Charm' memrz='0' level='8' desc='As charm monster, but all within 30 ft.' />
+<spell name='Ottos Irresistible Dance' memrz='0' level='8' desc='Forces subject to dance.' />
+<spell name='Sympathy' memrz='0' level='8' desc='Object or location attracts certain creatures.' />
+<spell name='Bigbys Clenched Fist' memrz='0' level='8' desc='Large hand attacks your foes.' />
+<spell name='Otilukes Telekinetic Sphere' memrz='0' level='8' desc='As Otilukes resilient sphere, but you move sphere telekinetically.' />
+<spell name='Sunburst' memrz='0' level='8' desc='Blinds all within 10 ft., deals 3d6 damage.' />
+<spell name='Screen' memrz='0' level='8' desc='Illusion hides area from vision, scrying.' />
+<spell name='Clone' memrz='0' level='8' desc='Duplicate awakens when original dies.' />
+<spell name='Horrid Wilting' memrz='0' level='8' desc='Deals 1d8 damage/level within 30 ft.' />
+<spell name='Etherealness' memrz='0' level='8' desc='Travel to Ethereal Plane with companions.' />
+<spell name='Iron Body' memrz='0' level='8' desc='Your body becomes living iron.' />
+<spell name='Polymorph Any Object' memrz='0' level='8' desc='Changes any subject into anything else.' />
+<spell name='Symbol' memrz='0' level='8' desc='Triggered runes have array of effects.' />
+<spell name='Freedom' memrz='0' level='9' desc='Releases creature suffering imprisonment.' />
+<spell name='Imprisonment' memrz='0' level='9' desc='Entombs subject beneath the earth.' />
+<spell name='Mordenkainens Disjunction' memrz='0' level='9' desc='Dispels magic, disenchants magic items.' />
+<spell name='Prismatic Sphere' memrz='0' level='9' desc='As prismatic wall, but surrounds on all sides.' />
+<spell name='Gate' memrz='0' level='9' desc='Connects two planes for travel or summoning.' />
+<spell name='Power Word, Kill' memrz='0' level='9' desc='Kills one tough subject or many weak ones.' />
+<spell name='Summon Monster IX' memrz='0' level='9' desc='Calls outsider to fight for you.' />
+<spell name='Foresight' memrz='0' level='9' desc='Sixth sense warns of impending danger.' />
+<spell name='Dominate Monster' memrz='0' level='9' desc='As dominate person, but any creature.' />
+<spell name='Bigbys Crushing Hand' memrz='0' level='9' desc='As Bigbys interposing hand, but stronger.' />
+<spell name='Meteor Swarm' memrz='0' level='9' desc='Deals 24d6 fire damage, plus bursts.' />
+<spell name='Weird' memrz='0' level='9' desc='As phantasmal killer, but affects all within 30 ft.' />
+<spell name='Astral Projection' memrz='0' level='9' desc='Projects you and companions into Astral Plane.' />
+<spell name='Energy Drain' memrz='0' level='9' desc='Subject gains 2d4 negative levels.' />
+<spell name='Soul Bind' memrz='0' level='9' desc='Traps newly dead soul to prevent resurrection.' />
+<spell name='Wail of the Banshee' memrz='0' level='9' desc='Kills one creature/level.' />
+<spell name='Refuge' memrz='0' level='9' desc='Alters item to transport its possessor to you.' />
+<spell name='Shapechange' memrz='0' level='9' desc='Transforms you into any creature, and change forms once per round.' />
+<spell name='Teleportation Circle' memrz='0' level='9' desc='Circle teleports any creature inside to designated spot.' />
+<spell name='Temporal Stasis' memrz='0' level='9' desc='Puts subject into suspended animation.' />
+<spell name='Time Stop' memrz='0' level='9' desc='You act freely for 1d4+1 rounds.' />
+<spell name='Wish' memrz='0' level='9' desc='As limited wish, but with fewer limits.' />
+</spells>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/dnd3eweapons.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,299 @@
+<weapons version="01.02">
+<footnotes>
+<c txt="Shuriken don't get str bonus, 3/attack; don't mark t" ></c>
+<c txt="monkWeap unarmed base, monk att/round, and other monk attack mods" ></c>
+<c txt="fn field for a weapon stands for 'footnotes'" ></c>
+<f mark="b" txt="classified as a bow or sling, str penalty applies " ></f>
+<f mark="c" txt="set for charge weapon, 2x dam when set Vs charging opponent." ></f>
+<f mark="C" txt="2x damaged when used on charge (may need to be mounted) " ></f>
+<f mark="d" txt="Classified as a double weapon." ></f>
+<f mark="m" txt="Monks get special 'Monk Weapon'advantages. " ></f>
+<f mark="o" txt="+2 opposed attack rolls wrt disarming/being disarmed on fail." ></f>
+<f mark="r" txt="Classified as a reach weapon, can strike at 6-10, but cannot strike 0-5" ></f>
+<f mark="R" txt="Classified as a special reach weapon, can strike at 0-10" ></f>
+<f mark="s" txt="Weapon can only do subdual damage." ></f>
+<f mark="t" txt="Classified as a thrown (or throwable) weapon by PH,standard thrown characteristics will be applied" ></f>
+<f mark="T" txt="Weapon can be used for making trip attacks." ></f>
+<f mark="X" txt="Custom weapon, should have appropriate footnotes added." ></f>
+</footnotes>
+<weapon mod="0" fn="" name="Antimatter rifle" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="6d10" critical="x2" range="10" weight="10" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="d" name="Axe, orc double" cost="60" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="x3" range="0" weight="25" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="t" name="Axe, throwing" cost="8" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x2" range="10" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Battleaxe" cost="10" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="7" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Blowgun" cost="1" category="Asian Weapons-Ranged" size="Small" damage="1" critical="x2" range="10" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oT" name="Chain, spiked" cost="25" category="Exotic Weapons-Melee" size="Large" damage="2d4" critical="x2" range="0" weight="15" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="t" name="Club" cost="0" category="Simple Weapons-Melee" size="Medium" damage="1d6" critical="x2" range="10" weight="3" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Crossbow, hand" cost="100" category="Exotic Weapons-Ranged" size="Tiny" damage="1d4" critical="19-20/x2" range="30" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Crossbow, heavy" cost="50" category="Simple Weapons-Ranged" size="Medium" damage="1d10" critical="19-20/x2" range="120" weight="9" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Crossbow, light" cost="35" category="Simple Weapons-Ranged" size="Small" damage="1d8" critical="19-20/x2" range="80" weight="6" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Crossbow, repeating" cost="250" category="Exotic Weapons-Ranged" size="Medium" damage="1d8" critical="19-20/x2" range="80" weight="16" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="t" name="Dagger" cost="2" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="19-20/x2" range="10" weight="1" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Dagger, punching" cost="2" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="x3" range="0" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Dart" cost="0" category="Simple Weapons-Ranged" size="Small" damage="1d4" critical="x2" range="20" weight="0" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Falchion" cost="75" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="18-29/x2" range="0" weight="16" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="doT" name="Flail, dire" cost="90" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="x2" range="0" weight="20" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oT" name="Flail, heavy" cost="15" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="19-20/x2" range="0" weight="20" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oT" name="Flail, light" cost="8" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="5" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Flamer" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="3d6*" critical="-" range="20" weight="8" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Flurry of Blows(Monk Med)" cost="-1" category="Exotic Weapons-Melee" size="Unarmed" damage="Monk Med" critical="x2" range="0" weight="0" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Flurry of Blows(Monk Small)" cost="-1" category="Exotic Weapons-Melee" size="Unarmed" damage="Monk Small" critical="x2" range="0" weight="0" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Gauntlet" cost="2 gp" category="Simple Weapons-Melee" size="Unarmed" damage="*" critical="*" range="0" weight="2" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Gauntlet, spiked" cost="5" category="Simple Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="r" name="Glaive" cost="8" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Greataxe" cost="20" category="Martial Weapons-Melee" size="Large" damage="1d12" critical="x3" range="0" weight="20" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Greatclub" cost="5" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x2" range="0" weight="10" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Greatsword" cost="50" category="Martial Weapons-Melee" size="Large" damage="2d6" critical="19-20/x2" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Grenade launcher" cost="-1" category="Modern Weapons-Ranged" size="Large" damage="*" critical="*" range="200" weight="12" type="*" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="rT" name="Guisarme" cost="9" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="Ts" name="Halberd" cost="10" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="x3" range="0" weight="15" type="P&amp;S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="tc" name="Halfspear" cost="1" category="Simple Weapons-Melee" size="Medium" damage="1d6" critical="x3" range="20" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="d" name="Hammer, gnome hooked" cost="20" category="Exotic Weapons-Melee" size="Medium" damage="1d6/1d4" critical="x3/x4" range="0" weight="6" type="B&amp;P" >
+<description >for proper treatment, read PH pg 101</description >
+</weapon>
+<weapon mod="0" fn="t" name="Hammer, light" cost="1" category="Martial Weapons-Melee" size="Small" damage="1d4" critical="x2" range="20" weight="2" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Handaxe" cost="6" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x3" range="0" weight="5" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Javelin" cost="1" category="Simple Weapons-Ranged" size="Medium" damage="1d6" critical="x2" range="30" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Kama" cost="2" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="2" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Kama, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Katana" cost="400" category="Exotic Weapons-Melee" size="Large" damage="1d10" critical="19-20/x2" range="0" weight="6" type="S" >
+    <description >Always masterwork</description >
+</weapon>
+<weapon mod="0" fn="" name="Katana, used 2 handed" cost="400" category="Martial Weapons-Melee" size="Large" damage="1d10" critical="19-20/x2" range="0" weight="6" type="S" >
+    <description >Always masterwork</description >
+</weapon>
+<weapon mod="0" fn="" name="Kukri" cost="8" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="18-29/x2" range="0" weight="3" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oTR" name="Kusari-gama" cost="10" category="Asian Weapons-Melee" size="Medium" damage="1d6" critical="x2" range="0" weight="3" type="S" >
+    <description >like a spiked chain</description >
+</weapon>
+<weapon mod="0" fn="Cr" name="Lance, heavy" cost="10" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="C" name="Lance, light" cost="6" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="x3" range="0" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Laser pistol" cost="-1" category="Futuristic Weapons-Ranged" size="Small" damage="2d10" critical="x2" range="100" weight="2" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Laser rifle" cost="-1" category="Futuristic Weapons-Ranged" size="Medium" damage="3d20" critical="x2" range="200" weight="7" type="Special" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Longbow" cost="75" category="Martial Weapons-Ranged" size="Large" damage="1d8" critical="x3" range="100" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Longbow, composite" cost="100" category="Martial Weapons-Ranged" size="Large" damage="1d8" critical="x3" range="110" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="rc" name="Longspear" cost="5" category="Martial Weapons-Melee" size="Large" damage="1d8" critical="x3" range="0" weight="9" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Longsword" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="19-20/x2" range="0" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Mace, heavy" cost="12" category="Simple Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="12" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Mace, light" cost="5" category="Simple Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="6" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Morningstar" cost="8" category="Simple Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="0" weight="8" type="B&amp;P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Musket" cost="500" category="Renaissance Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="150" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Net" cost="20" category="Exotic Weapons-Ranged" size="Medium" damage="0" critical="0" range="10" weight="10" type="-" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Nunchaku" cost="2" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="2" type="S" >
+<description >
+</description >
+</weapon>
+<weapon mod="0" fn="m" name="Nunchaku, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pick, heavy" cost="8" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="x4" range="0" weight="6" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pick, light" cost="4" category="Martial Weapons-Melee" size="Small" damage="1d4" critical="x4" range="0" weight="4" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pistol" cost="250" category="Renaissance Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="50" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pistol, automatic" cost="-1" category="Modern Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="150" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Pistol, revolver" cost="-1" category="Modern Weapons-Ranged" size="Small" damage="1d10" critical="x3" range="100" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="d" name="Quarterstaff" cost="0" category="Simple Weapons-Melee" size="Large" damage="1d6" critical="x2" range="0" weight="4" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="ro" name="Ranseur" cost="10" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x3" range="0" weight="15" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Rapier" cost="20" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="18-20/x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Rifle, automatic" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="250" weight="12" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Rifle, repeater" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="1d12" critical="x3" range="200" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="s" name="Sap" cost="1" category="Martial Weapons-Melee" size="Small" damage="1d6s" critical="x2" range="0" weight="3" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Scattergun" cost="-1" category="Modern Weapons-Ranged" size="Medium" damage="*" critical="*" range="10" weight="10" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Scimitar" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d6" critical="18-20/x2" range="0" weight="4" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Scythe" cost="18" category="Martial Weapons-Melee" size="Large" damage="2d4" critical="x4" range="0" weight="12" type="P&amp;S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Shortbow" cost="30" category="Martial Weapons-Ranged" size="Medium" damage="1d6" critical="x3" range="60" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Shortbow, composite" cost="75" category="Martial Weapons-Ranged" size="Medium" damage="1d6" critical="x3" range="70" weight="2" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="tc" name="Shortspear" cost="2" category="Simple Weapons-Melee" size="Large" damage="1d8" critical="x3" range="20" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Shuriken" cost="1" category="Exotic Weapons-Ranged" size="Tiny" damage="1" critical="x2" range="100" weight="0" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Siangham" cost="3" category="Exotic Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="m" name="Siangham, halfling" cost="2" category="Exotic Weapons-Melee" size="Tiny" damage="1d4" critical="x2" range="0" weight="1" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Sickle" cost="6" category="Simple Weapons-Melee" size="Small" damage="1d6" critical="x2" range="0" weight="3" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="b" name="Sling" cost="1d4" category="Simple Weapons-Ranged" size="Small" damage="1d4" critical="x2" range="50" weight="0" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="s" name="Strike, unarmed (med)" cost="0" category="Simple Weapons-Melee" size="Unarmed" damage="1d3" critical="x2" range="0" weight="0" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="s" name="Strike, unarmed (small)" cost="0" category="Simple Weapons-Melee" size="Unarmed" damage="1d2" critical="x2" range="0" weight="0" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Sword, bastard" cost="35" category="Exotic Weapons-Melee" size="Medium" damage="1d10" critical="19-20/x2" range="0" weight="10" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Sword, bastard used 2 handed" cost="35" category="Martial Weapons-Melee" size="Medium" damage="1d10" critical="19-20/x2" range="0" weight="10" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Sword, short" cost="10" category="Martial Weapons-Melee" size="Small" damage="1d6" critical="19-20/x2" range="0" weight="3" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="d" name="Sword, two-bladed" cost="100" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d8" critical="19-20/X2" range="0" weight="30" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="ct" name="Trident" cost="15" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x2" range="10" weight="5" type="P" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="dc" name="Urgrosh, dwarven" cost="50" category="Exotic Weapons-Melee" size="Large" damage="1d8/1d6" critical="x3" range="0" weight="15" type="S&amp;P" >
+<description >see PH pg 103 for proper treatment </description >
+</weapon>
+<weapon mod="0" fn="m" name="UnArmed(Monk Med)" cost="0" category="Exotic Weapons-Melee" size="Unarmed" damage="Monk Med" critical="x2" range="0" weight="00" type="S&amp;P" >
+<description >An unArmed strike does more dmg as a monk</description >
+</weapon>
+<weapon mod="0" fn="m" name="UnArmed(Monk Small)" cost="0" category="Exotic Weapons-Melee" size="Unarmed" damage="Monk Small" critical="x2" range="0" weight="00" type="S&amp;P" >
+<description >An unArmed strike does more dmg as a monk</description >
+</weapon>
+<weapon mod="0" fn="" name="Wakizashi" cost="300" category="Asian Weapons-Melee" size="Small" damage="1d6" critical="19-20/x2" range="0" weight="3" type="S" >
+<description >Always masterwork</description >
+</weapon>
+<weapon mod="0" fn="" name="Waraxe, dwarven" cost="30" category="Exotic Weapons-Melee" size="Medium" damage="1d10" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Waraxe, dwarven used 2handed" cost="30" category="Martial Weapons-Melee" size="Medium" damage="1d10" critical="x3" range="0" weight="15" type="S" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="" name="Warhammer" cost="12" category="Martial Weapons-Melee" size="Medium" damage="1d8" critical="x3" range="0" weight="8" type="B" >
+<description ></description >
+</weapon>
+<weapon mod="0" fn="oT" name="Whip" cost="1" category="Exotic Weapons-Ranged" size="Small" damage="1d2s" critical="x2" range="15" weight="2" type="S" >
+<description >range15, maxrange15, no penalties to max range; ineffective Vs armor or +3 natural adjustment PH pg 104 </description >
+</weapon>
+</weapons>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/dnd3e/feats.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,556 @@
+A Thousand Furs [Special] - Touched by the Gods, p. 71
+Acrobatic [General] - Song and Silence, p. 38
+Acrobatic (Legends & Lairs version) [General] - Traps & Treachery, p. 34
+Airy Gallop [Special] - Dragon Magazine d20 Special/Annual #6, p. 65
+Alertness [General] - Player's Handbook, p. 80
+Alluring [General] - Song and Silence, p. 38
+Ambidexterity [General] - Player's Handbook, p. 80
+Ancient Lineage [General] - The Taan, p. 81
+Arcane Defense [General] - Tome and Blood, p. 38
+Arcane Preparation [General] - Tome and Blood, p. 38
+Arcane Schooling [General] - Forgotten Realms Campaign Setting, p. 33
+Armor Proficiency (Heavy) [General] - Player's Handbook, p.80
+Armor Proficiency (Light) [General] - Player's Handbook, p. 80
+Armor Proficiency (Medium) [General] - Player's Handbook, p. 80
+Art of Fascination [Ancestor] - Oriental Adventures, p. 60
+Arterial Strike [General] - Song and Silence, p. 38
+Artist [General] - Forgotten Realms Campaign Setting, p. 33
+Artist [Ancestor] - Oriental Adventures, p. 61
+Athletic [General] - Song and Silence, p. 38
+Attention to Detail [Ancestor] - Oriental Adventures, p. 61
+Attune Gem [Item Creation] - Magic of Faerun, p. 21
+Augment Construct [Psionic] - Dragon Magazine #287, p. 54
+Augment Summoning [General] - Tome and Blood, p. 39
+Aura of Serenity [Mystic Warrior] - Mystic Warriors, p. 112
+Battle Howl [General] - Touched by the Gods, p. 109
+Battle Roar [Kaiju] - Dragon Magazine #289, p. 70
+Blind Casting [General] - Dungeons, p. 81
+Blind-Fight [General] - Player's Handbook, p. 80
+Blindsight, 5-foot Radius [General] - Sword and Fist, p. 5
+Blood Frenzy [General] - The Taan, p. 81
+Blood Sorcerer [Ancestor] - Oriental Adventures, p. 61
+Blooded [General] - Forgotten Realms Campaign Setting, p. 33
+Bloodline of Fire [General] - Forgotten Realms Campaign Setting, p. 34
+Body Fuel [Psionic] - Psionics Handbook, p. 24
+Bootlicker [General] - Evil, p. 58
+Born Duelist [Ancestor] - Oriental Adventures, p. 61
+Breeze Dance [Fighting Stance] - Mystic Warriors, p. 112
+Brew Poison [Item Creation] - Traps & Treachery, p. 45
+Brew Potion [Item Creation] - Player's Handbook, p. 80
+Bribery [General] - Evil, p. 58
+Bullheaded [General] - Forgotten Realms Campaign Setting, p. 34
+Cabalistic Spellcasting [Metamagic] - Sovereign Stone Campaign Sourcebook, p. 60
+Casing Sense [General] - Traps & Treachery, p. 34
+Chain Power [Metapsionic] - Dragon Magazine #287, p. 54
+Chain Spell [Metamagic] - Tome and Blood, p. 39
+Chain Spell (Scarred Lands version) [Metamagic] - Relics and Rituals, p. 25
+Change Instruction [Special] - Demonology, p. 41
+Chariot Archery [General] - Sword and Fist, p. 78
+Chariot Charge [General] - Sword and Fist, p. 79
+Chariot Combat [General] - Sword and Fist, p. 78
+Chariot Sideswipe [General] - Sword and Fist, p. 79
+Chariot Trample [General] - Sword and Fist, p. 78
+Charlatan [General] - Song and Silence, p. 38
+Chink in the Armor [General] - Song and Silence, p. 38
+Choke Hold [General] - Oriental Adventures, p. 61
+Circle Kick [General] - Sword and Fist, p. 5
+Claws/Fangs [Infernal] - Evil, p. 24
+Cleave [General] - Player's Handbook, p. 80
+Close-Order Fighting [Special] - Dragon Lords of Melnibone, p. 63
+Close-Quarters Fighting [General] - Sword and Fist, p. 5
+Cloud Running [Special] - Creature Collection, p. 72
+Combat Agility [General] - Dragon Magazine #284, p. 123
+Combat Casting [General] - Player's Handbook, p. 80
+Combat Manifestation [Psionic] - Psionics Handbook, p. 24
+Combat Reflexes [General] - Player's Handbook, p. 80
+Combat Sense [General] - The Taan, p. 81
+Conjure Mastery [Eldritch] - The Book of Eldritch Might, p. 4
+Construct Familiar [General] - Dragon Magazine #280, p. 62
+Controlled Breathing [General] - Dungeons, p. 81
+Cool Head [General] - Oriental Adventures, p. 61
+Cooperative Spell [Metamagic] - Tome and Blood, p. 39
+Cooperative Spellcasting [Metamagic] - Sovereign Stone Campaign Sourcebook, p. 60
+Cosmopolitan [General] - Forgotten Realms Campaign Setting, p. 34
+Courteous Magocracy [General] - Forgotten Realms Campaign Setting, p. 34
+Crab Walk [Fighting Stance] - Mystic Warriors, p. 112
+Craft Anaema Tool [Item Creation] - Mythic Races, p. 15
+Craft Crystal Capacitor [Item Creation] - Psionics Handbook, p. 24
+Craft Crystal Weapon [Item Creation] - Oriental Adventures, p. 61
+Craft Dorje [Item Creation] - Psionics Handbook, p. 24
+Craft Kirpan [Item Creation] - Mystic Warriors, p. 112
+Craft Magic Arms and Armor [Item Creation] - Player's Handbook, p. 81
+Craft Magic Trap [Item Creation] - Traps & Treachery, p. 34
+Craft Mystic Talisman [Item Creation] - Mystic Warriors, p. 112
+Craft Named Weapon [Item Creation] - Mystic Warriors, p. 112
+Craft Psionic Arms and Armor [Item Creation] - Psionics Handbook, p. 24
+Craft Rod [Item Creation] - Player's Handbook, p. 81
+Craft Staff [Item Creation] - Player's Handbook, p. 81
+Craft Talisman [Item Creation] - Oriental Adventures, p. 61
+Craft Universal Item [Item Creation] - Psionics Handbook, p. 24
+Craft Vitus Amulet [Item Creation] - Mystic Warriors, p. 113
+Craft Wand [Item Creation] - Player's Handbook, p. 81
+Craft Wondrous Item [Item Creation] - Player's Handbook, p. 81
+Create Graft [Item Creation] - Touched by the Gods, p. 26
+Create Portal [Item Creation] - Forgotten Realms Campaign Setting, p. 34
+Dance of the Dirk [Fighting Stance] - Mystic Warriors, p. 113
+Darkvision [Infernal] - Evil, p. 24
+Dash [General] - Song and Silence, p. 38
+Daylight Adaptation [General] - Forgotten Realms Campaign Setting, p. 34
+Dead Shot [General] - Sovereign Stone Campaign Sourcebook, p. 60
+Death Blow [General] - Sword and Fist, p. 6
+Deep Impact [Psionic] - Psionics Handbook, p. 25
+Defensive Strike [General] - Oriental Adventures, p. 62
+Defensive Throw [General] - Oriental Adventures, p. 62
+Deflect Arrows [General] - Player's Handbook, p. 81
+Deflect Ranged Attack [General] - Dragon Magazine #274, p. 60
+Delay Power [Metapsionic] - Psionics Handbook, p. 25
+Delay Spell [Metamagic] - Tome and Blood, p. 39
+Dirty Fighting [General] - Sword and Fist, p. 6
+Disarm Mind [Psionic] - Psionics Handbook, p. 25
+Discipline [General] - Forgotten Realms Campaign Setting, p. 34
+Discipline [Ancestor] - Oriental Adventures, p. 62
+Disguise Spell [Metamagic] - Song and Silence, p. 38
+Dismiss Demon [Special] - Demonlogy, p. 41
+Divine Cleansing [Divine] - Defenders of the Faith, p. 19
+Divine Might [Divine] - Defenders of the Faith, p. 19
+Divine Perception [General] - Touched by the Gods, p. 34
+Divine Resistance [Divine] - Defenders of the Faith, p. 19
+Divine Shield [Divine] - Defenders of the Faith, p. 19
+Divine Vengeance [Divine] - Defenders of the Faith, p. 20
+Divine Vigor [Divine] - Defenders of the Faith, p. 20
+Dodge [General] - Player's Handbook, p. 81
+Dreamspeaking [General] - The Book of Eldritch Might, p. 4
+Drug Tolerance [General] - Caravan of Hope, p. 27
+Dual Strike [General] - Sword and Fist, p. 6
+Eagle Claw Attack [General] - Sword and Fist, p. 6
+Earth's Embrace [General] - Oriental Adventures, p. 62
+Education [General] - Forgotten Realms Campaign Setting, p. 34
+Eidetic Memory [General] - Dungeons, p. 81
+Element Resistance [Infernal] - Evil, p. 24
+Empathy [General] - Traps & Treachery, p. 35
+Empower Spell [Metamagic] - Player's Handbook, p. 82
+Empower Turning [Special] - Defenders of the Faith, p. 20
+Enchant Stone [Item Creation] - The Taan, p. 81
+Encode Stone [Item Creation] - Psionics Handbook, p. 25
+Endurance [General] - Player's Handbook, p. 82
+Energy Admixture [Metamagic] - Tome and Blood, p. 39
+Energy of Life [Special] - Creature Collection, p. 72
+Energy Substitution [Metamagic] - Tome and Blood, p. 40
+Enlarge Power [Metapsionic] - Psionics Handbook, p. 25
+Enlarge Spell [Metamagic] - Player's Handbook, p. 82
+Enspell Familiar [General] - Dragon Magazine #280, p. 62
+Eschew Materials [Metamagic] - Tome and Blood, p. 40
+Etch Object Rune [Item Creation] - The Book of Eldritch Might, p. 4
+Ethran [General] - Forgotten Realms Campaign Setting, p. 34
+Exotic Weapon Proficiency [General] - Player's Handbook, p. 82
+Expert Tactician [General] - Song and Silence, p. 38
+Expertise [General] - Player's Handbook, p. 82
+Extend Power [Metapsionic] - Psionics Handbook, p. 25
+Extend Spell [Metamagic] - Player's Handbook, p. 82
+Extra Familiar [General] - Dragon Magazine #280, p. 62
+Extra Music [General] - Song and Silence, p. 39
+Extra Power [Psionic] - Dragon Magazine #287, p. 55
+Extra Slot [General] - Tome and Blood, p. 40
+Extra Smiting [Special] - Defenders of the Faith, p. 20
+Extra Spell [General] - Tome and Blood, p. 40
+Extra Stunning Attacks [General] - Sword and Fist, p. 6
+Extra Turning [Special] - Player's Handbook, p. 82 (rules: pp. 32, 42)
+Eye for Detail [General] - Traps & Treachery, p. 35
+Eyes in the Back of Your Head [General] - Sword and Fist, p. 6
+Eyes of Calaam [Special] - Touched by the Gods, p. 59
+Falling Star Strike [General] - Oriental Adventures, p. 62
+Far Shot [General] - Player's Handbook, p. 82
+Fast Armor [General] - Dragon Magazine #284, p. 123
+Fast Rider [General] - Dragon Magazine #285, p. 98
+Fast Talker [General] - Traps & Treachery, p. 35
+Fearsome and Fearless [Ancestor] - Oriental Adventures, p. 62
+Feign Weakness [General] - Sword and Fist, p. 6
+Fell Shot [Psionic] - Psionics Handbook, p. 25
+Firearms Drill [General] - Dragon Magazine d20 Special/Annual #6, p. 70
+Fists of Calaam [Special] - Touched by the Gods, p. 59
+Fists of Iron [General] - Sword and Fist, p. 6
+Fleet of Foot [General] - Song and Silence, p. 39
+Flick of the Wrist [General] - Song and Silence, p. 39
+Flight [Infernal] - Evil, p. 25
+Flying Kick [General] -Oriental Adventures, p. 62
+Foe Hunter [Fighter, General] - Forgotten Realms Campaign Setting, p. 34
+Forester [General] - Forgotten Realms Campaign Setting, p. 35
+Forge Ring [Item Creation] - Player's Handbook, p. 82
+Fortify Power [Metapsionic] - Dragon Magazine #287, p. 55
+Freezing the Lifeblood [General] - Oriental Adventures, p. 62
+Gifted General [Ancestor] - Oriental Adventures, p. 62
+Golden Tongue [General] - Dungeons, p. 81
+Grace Under Pressure [General] - Dungeons, p. 81
+Grappling Block [General] - Oriental Adventures, p. 63
+Grasshopper Strike [General] - Dragon Magazine #279, p. 63
+Great Cleave [General] - Player's Handbook, p. 82
+Great Crafter [Ancestor] - Oriental Adventures, p. 63
+Great Diplomat [Ancestor] - Oriental Adventures, p. 63
+Great Fortitude [General] - Player's Handbook, p. 82
+Great Ki Shout [General] - Oriental Adventures, p. 63
+Great Stamina [Ancestor] - Oriental Adventures, p. 63
+Great Sunder [Psionic] - Psionics Handbook, p. 26
+Great Teamwork [Ancestor] - Oriental Adventures, p. 63
+Greater Power Penetration [Psionic] - Psionics Handbook, p. 26
+Greater Psionic Focus [Psionic] - Psionics Handbook, p. 26
+Greater Spell Focus [General] - Tome and Blood, p. 40
+Greater Spell Penetration [General] - Tome and Blood, p. 40
+Green Ear [General] - Song and Silence, p. 39
+Green Viper Style [Fighting Stance] - Mystic Warriors, p. 113
+Hammer Fist [General] - Dragon Magazine #279, p. 63
+Hamstring [General] - Song and Silence, p. 39
+Heavy Scarring [General] - The Taan, p. 82
+Heighten Power [Metapsionic] - Psionics Handbook, p. 26
+Heighten Spell [Metamagic] - Player's Handbook, p. 82
+Heighten Turning [Special] - Defenders of the Faith, p. 20
+Heroic Destiny [Special] - Touched by the Gods, p. 109
+Hide Power [Metapsionic] - Psionics Handbook, p. 26
+Hide Spell [Metamagic] - Relics and Rituals, p. 25
+Hill Fighter [General] - Dragon Magazine #285, p. 98
+Hold the Line [General] - Sword and Fist, p. 7
+Honest Merchant [Ancestor] - Oriental Adventures, p. 63
+Horse Nomad [Fighter, General] - Forgotten Realms Campaign Setting, p. 35
+Iaijutsu Master [Ancestor] - Oriental Adventures, p. 63
+Immortality [Infernal] - Evil, p. 25
+Immunity [Infernal] - Evil, p. 25
+Imp [Infernal] - Evil, p. 25
+Improved Aid [Ancestor] - Oriental Adventures, p. 63
+Improved Alertness [General] - Dungeons, p. 82
+Improved Bull Rush [General] - Player's Handbook, p. 82
+Improved Counterspell [General] - Forgotten Realms Campaign Setting, p. 35
+Improved Critical [General] - Player's Handbook, p. 82
+Improved Disarm [General] - Player's Handbook, p. 83
+Improved Endurance [General] - Dungeons, p. 82
+Improved Familiar [General] - Tome and Blood, p. 40
+Improved Feint [General] - Evil, p. 59
+Improved Flight [Infernal] - Evil, p. 25
+Improved Grab [General] - Mythic Races, p. 77
+Improved Grapple [General] - Oriental Adventures, p. 63
+Improved Initiative [General] - Player's Handbook, p. 83
+Improved Knockout Attack [General] - Traps & Treachery, p. 35
+Improved Low Blow [General] - Dragon Magazine #285, p. 33
+Improved Mounted Archery [General] - Dragon Magazine #285, p. 99
+Improved Mounted Combat [General] - Sovereign Stone Campaign Sourcebook, p. 62
+Improved Multiweapon Fighting [General] - Mythic Races, p. 137
+Improved Overrun [General] - Sword and Fist, p. 7
+Improved Psicrystal [Psionic] - Psionics Handbook, p. 26
+Improved Ranged Sneak Attack [General] - Traps & Treachery, p. 36
+Improved Rapid Shot [General] - Dragon Magazine #275, p. 41
+Improved Regeneration [Infernal] - Evil, p. 25
+Improved Shield Bash [General] - Defenders of the Faith, p. 20
+Improved Sneak Attack [General] - Traps & Treachery, p. 36
+Improved Sunder [General] - Sword and Fist, p. 7
+Improved Trample [Kaiju] - Dragon Magazine #289, p. 70
+Improved Trip [General] - Player's Handbook, p. 83
+Improved Two-Weapon Fighting [General] - Player's Handbook, p. 83
+Improved Unarmed Strike [General] - Player's Handbook, p. 83
+Improvise Thieves' Tools [General] - Traps & Treachery, p. 37
+Improvised Weapon [General] - Sovereign Stone Campaign Sourcebook, p. 62
+Increased Carrying Capacity [General] - Dungeons, p. 82
+Increased Movement [Infernal] - Evil, p. 26
+Inertial Armor [Psionic] - Psionics Handbook, p. 26
+Infernal Pact [Infernal] - Evil, p. 26
+Infernal Soul [Infernal] - Evil, p. 26
+Information Exchange [Special] - Touched by the Gods, p. 7
+Innate Spell [General] - Tome and Blood, p. 41
+Inner Peace [Mystic Warrior] - Mystic Warriors, p. 113
+Inner Strength [Psionic] - Psionics Handbook, p. 26
+Inscribe Magical Tattoo [Item Creation] - Relics and Rituals, p. 198
+Inscribe Rune [Item Creation] - Forgotten Realms Campaign Setting, p. 36
+Insidious Magic [Metamagic] - Forgotten Realms Campaign Setting, p. 36
+Invisibility [Infernal] - Evil, p. 27
+Ironbone [Special] - Creature Collection, p. 71
+Ironskin [Special] - Creature Collection, p. 72
+Iron Will [General] - Player's Handbook, p. 83
+Item Image [Eldritch] - The Book of Eldritch Might, p. 4
+Jack of All Trades [General] - Song and Silence, p. 40
+Kami's Intuition [Ancestor] - Oriental Adventures, p. 63
+Karmic Strike [General] - Oriental Adventures, p. 63
+Karmic Twin [Ancestor] - Oriental Adventures, p. 64
+Keen Intellect [Ancestor] - Oriental Adventures, p. 64
+Keen Vision [General] - Traps & Treachery, p. 37
+Ki Projection [Special] - Creature Collection, p. 72
+Ki Shout [General] - Oriental Adventures, p. 64
+Knock-Down [General] - Sword and Fist, p. 7
+Knockout Attack [General] - Traps & Treachery, p. 37
+Knowledgeable [General] - Dungeons, p. 82
+Lace Spell: Elemental Energies [Eldritch] - The Book of Eldritch Might, p. 5
+Lace Spell: Enemy Bane [Eldritch] - The Book of Eldritch Might, p. 5
+Lace Spell: Holy/Unholy [Eldritch] - The Book of Eldritch Might, p. 5
+Lace Spell: Lawful/Chaotic [Eldritch] - The Book of Eldritch Might, p. 5
+Lead Missile Fire [General] - Evil, p. 59
+Leadership [General] - Player's Handbook, p. 83 (rules: Dungeon Master's Guide, p. 45)
+Lightning Fists [General] - Sword and Fist, p. 7
+Lightning Reflexes [General] - Player's Handbook, p. 83
+Light Sleeper [General] - Dungeons, p. 82
+Lingering Song [General] - Song and Silence, p. 40
+Lion Spy [Ancestor] - Oriental Adventures, p. 64
+Lion's Rage [Mystic Warrior Stance] - Mystic Warriors, p. 113
+Living Shield [General] - Evil, p. 58
+Low Blow [General] - Dragon Magazine #285, p. 33
+Luck of Heroes [General] - Forgotten Realms Campaign Setting, p. 36
+Luck of Heroes [Ancestor] - Oriental Adventures, p. 64
+Magekiss [Metamagic] - Traps & Treachery, p. 22
+Magic in the Blood [Ancestor] - Oriental Adventures, p. 64
+Magic Item [Infernal] - Evil, p. 27
+Magical Artisan [General] - Forgotten Realms Campaign Setting, p. 36
+Magical Artisan [Ancestor] - Oriental Adventures, p. 64
+Magical Talent [General] - The Book of Eldritch Might, p. 6
+Magical Training [General] - Forgotten Realms Campaign Setting, p. 36
+Magistrate's Mind [Ancestor] - Oriental Adventures, p. 64
+Mantis Leap [General] - Sword and Fist, p. 7
+Manufacture Magic Poison [Item Creation] - The Book of Eldritch Might, p. 6
+Many Masks [Ancestor] - Oriental Adventures, p. 64
+Martial Weapon Proficiency [General] - Player's Handbook, p. 83
+Master Dorje [Metapsionic] - Psionics Handbook, p. 26
+Maximize Power [Metapsionic] - Psionics Handbook, p. 26
+Maximize Spell [Metamagic] - Player's Handbook, p. 83
+Mechanical Aptitude [General] - Traps & Treachery, p. 37
+Mental Adversary [Psionic] - Psionics Handbook, p. 27
+Mental Leap [Psionic] - Psionics Handbook, p. 27
+Mercantile Background [General] - Forgotten Realms Campaign Setting, p. 36
+Metacreative [Psionic] - Psionics Handbook, p. 27
+Militia [General] - Forgotten Realms Campaign Setting, p. 36
+Mind Blind [Psionic] - Dragon Magazine #287, p. 55
+Mind Over Body [General] - Forgotten Realms Campaign Setting, p. 37
+Mind Trap [Psionic] - Psionics Handbook, p. 27
+Mirror Sight [Eldritch] - The Book of Eldritch Might, p. 6
+Mobility [General] - Player's Handbook, p. 83
+Monkey Grip [General] - Sword and Fist, p. 7
+Mounted Archery [General] - Player's Handbook, p. 83
+Mounted Combat [General] - Player's Handbook, p. 83
+Moving Meditation [Special] - Creature Collection, p. 72
+Multiattack (Taan version) [General] - The Taan, p. 83
+Multicultural [General] - Song and Silence, p. 40
+Multiple Limbs [Infernal] - Evil, p. 27
+Multiweapon Fighting (Legends & Lairs version) [General] - Mythic Races, p. 137
+Natural Weaponry [General] - The Taan, p. 83
+Nature Sense [Special] - Touched by the Gods, p. 71
+Nerve Strikes [Special] - Creature Collection, p. 72
+Nobody's Fool [General] - Dragon Magazine #285, p. 33
+Obscure Lore [General] - Song and Silence, p. 40
+Off-Hand Parry [General] - Sword and Fist, p. 7
+Off-Handed [General] - Evil, p. 59
+Oni's Bane [Ancestor] - Oriental Adventures, p. 64
+Pain Touch [General] - Sword and Fist, p. 8
+Pebble Underfoot [General] - Dragon Magazine #279, p. 63
+Penetrate Hardness [Kaiju] - Dragon Magazine #289, p. 70
+Perfect Memory [General] - Traps & Treachery, p. 37
+Permanent Control [Special] - Demonology, p. 41
+Pernicious Magic [Metamagic] - Forgotten Realms Campaign Setting, p. 37
+Persistent Power [Metapsionic] - Psionics Handbook, p. 27
+Persistent Spell [Metamagic] - Tome and Blood, p. 42
+Persuasive [General] - Song and Silence, p. 40
+Pin Shield [General] - Sword and Fist, p. 8
+Pinpoint Accuracy [General] - Sovereign Stone Campaign Sourcebook, p. 62
+Point Blank Shot [General] - Player's Handbook, p. 84
+Poison Blood [Infernal] - Evil, p. 28
+Poison Immunity [General] - Traps & Treachery, p. 37
+Pounce [General] - Mythic Races, p. 77
+Pounce & Strike [General] - Dragon Lords of Melnibone, p. 50
+Power Attack [General] - Player's Handbook, p. 84
+Power Attack--Iaijutsu [Ancestor] - Oriental Adventures, p. 64
+Power Attack--Shadowlands [Ancestor] - Oriental Adventures, p. 65
+Power Lunge [General] - Sword and Fist, p. 8
+Power Penetration [Psionic] - Psionics Handbook, p. 27
+Power Specialization [Psionic] - Dragon Magazine #287, p. 56
+Power Touch [Psionic] - Psionics Handbook, p. 27
+Powerful Voice [Ancestor] - Oriental Adventures, p. 65
+Precise Shot [General] - Player's Handbook, p. 84
+Primal Shout [General] - The Taan, p. 83
+Prone Attack [General] - Sword and Fist, p. 8
+Psionic Body [Psionic] - Psionics Handbook, p. 27
+Psionic Charge [Psionic] - Psionics Handbook, p. 28
+Psionic Defense [Psionic] - Dragon Magazine #287, p. 54
+Psionic Dodge [Psionic] - Psionics Handbook, p. 28
+Psionic Energy Admixture [Metapsionic] - Dragon Magazine #287, p. 55
+Psionic Energy Substitution [Metapsionic] - Dragon Magazine #287, p. 54
+Psionic Fist [Psionic] - Psionics Handbook, p. 28
+Psionic Focus [Psionic] - Psionics Handbook, p. 28
+Psionic Metabolism [Psionic] - Psionics Handbook, p. 28
+Psionic Shot [Psionic] - Psionics Handbook, p. 28
+Psionic Weapon [Psionic] - Psionics Handbook, p. 28
+Psychic Bastion [Psionic] - Psionics Handbook, p. 28
+Psychic Inquisitor [Psionic] - Psionics Handbook, p. 29
+Psychoanalyst [Psionic] - Psionics Handbook, p. 29
+Purifying Light [General] - Mythic Races, p. 69
+Pyro [General] - Song and Silence, p. 40
+Quick Draw [General] - Player's Handbook, p. 84
+Quicken Power [Metapsionic] - Psionics Handbook, p. 29
+Quicken Spell [Metamagic] - Player's Handbook, p. 84
+Quicken Summoning [Special] - Demonology, p. 41
+Quicken Turning [Special] - Defenders of the Faith, p. 20
+Quicker Than the Eye [General] - Song and Silence, p. 40
+Quickstrike [General] - Traps & Treachery, p. 37
+Rake [General] - Mythic Races, p. 77
+Raking Nails [General] - Mystic Warriors, p. 113
+Ranged Disarm [General] - Dragon Magazine #274, p. 60
+Ranged Pin [General] - Dragon Magazine #274, p. 60
+Ranged Sunder [General] - Dragon Magazine #274, p. 60
+Rapid Metabolism [Psionic] - Psionics Handbook, p. 29
+Rapid Reload [General] - Sword and Fist, p. 9
+Rapid Shot [General] - Player's Handbook, p. 84
+Ray Burst [Metamagic] - Dragon Magazine Annual #5, p. 26
+Ray Coning [Metamagic] - Dragon Magazine Annual #5, p. 26
+Ray Extension [Metamagic] - Dragon Magazine Annual #5, p. 26
+Ray Focus [General] - Dragon Magazine Annual #5, p. 26
+Ray Splitting [Metamagic] - Dragon Magazine Annual #5, p. 26
+Reactive Counterspell [General] - Magic of Faerun, p. 22
+Reach Power [Metapsionic] - Dragon Magazine #287, p. 55
+Reach Spell [Metamagic] - Defenders of the Faith, p. 20
+Reckless Offensive [General] - Enemies and Allies, p. 41
+Recognize Omen [General] - Sovereign Stone Campaign Sourcebook, p. 62
+Redirect Attacks [General] - Evil, p. 59
+Regeneration [Infernal] - Evil, p. 28
+Remain Conscious [General] - Sword and Fist, p. 9
+Repeat Power [Metapsionic] - Dragon Magazine #287, p. 56
+Repeat Spell [Metamagic] - Tome and Blood, p. 41
+Requiem [General] - Song and Silence, p. 40
+Resculpt Mind [Psionic] - Dragon Magazine #287, p. 56
+Resist Poison [General] - Forgotten Realms Campaign Setting, p. 37
+Resist Poison [Ancestor] - Oriental Adventures, p. 65
+Resist Taint [Ancestor] - Oriental Adventures, p. 65
+Return Shot [Psionic] - Psionics Handbook, p. 29
+Ride-By-Attack [General] - Player's Handbook, p. 84
+Rot [Infernal] - Evil, p. 28
+Roundabout Kick [General] - Oriental Adventures, p. 65
+Run [General] - Player's Handbook, p. 84
+Sacred Spell [Metamagic] - Defenders of the Faith, p. 20
+Saddleback [Fighter, General] - Forgotten Realms Campaign Setting, p. 37
+Saddleback [Ancestor] - Oriental Adventures, p. 65
+Sanctum Spell [Metamagic] - Tome and Blood, p. 41
+Scent [General] - Dungeon Master's Guide, p. 81 (variant rule)
+Scholar of Nature [Ancestor] - Oriental Adventures, p. 65
+Scribe Scroll [Item Creation] - Player's Handbook, p. 84
+Scribe Tattoo [Item Creation] - Psionics Handbook, p. 29
+Sculpt Power [Metapsionic] - Dragon Magazine #287, p. 56
+Sculpt Spell [Metamagic] - Tome and Blood, p. 42
+Sea Legs [Ancestor] - Oriental Adventures, p. 65
+Second Wind [General] - Sovereign Stone Campaign Sourcebook, p. 63
+Set Spear [General] - Dragon Lords of Melnibone, p. 63
+Shadow [General] - Song and Silence, p. 40
+Shadow (Legends & Lairs version) [General] - Traps & Treachery, p. 37
+Shadow Weave Magic [General] - Forgotten Realms Campaign Setting, p. 37
+Shapechange [Infernal] - Evil, p. 27
+Shared Spellcasting [Metamagic] - Sovereign Stone Campaign Sourcebook, p. 63
+Sharp-Shooting [General] - Sword and Fist, p. 9
+Shield Charge [General] - Defenders of the Faith, p. 20
+Shield Expert [General] - Sword and Fist, p. 9
+Shield Proficiency [General] - Player's Handbook, p. 85
+Shot on the Run [General] - Player's Handbook, p. 85
+Side Step [General] - Mystic Warriors, p. 113
+Signature Skill [General] - Traps & Treachery, p. 38
+Signature Spell [General] - Forgotten Realms Campaign Setting, p. 37
+Silent Spell [Metamagic] - Player's Handbook, p. 85
+Silver Palm [General] - Forgotten Realms Campaign Setting, p. 37
+Silver Tongue [Ancestor] - Oriental Adventures, p. 65
+Simple Weapon Proficiency [General] - Player's Handbook, p. 85
+Skill Focus [General] - Player's Handbook, p. 85
+Smooth Talk [General] - Forgotten Realms Campaign Setting, p. 37
+Smooth Talk [Ancestor] - Oriental Adventures, p. 66
+Snake Blood [General] - Forgotten Realms Campaign Setting, p. 38
+Snatch Arrows [General] - Sword and Fist, p. 9
+Snatch Weapon [General] - Song and Silence, p. 40
+Soul of Honor [Ancestor] - Oriental Adventures, p. 66
+Soul of Loyalty [Ancestor] - Oriental Adventures, p. 66
+Soul of Sincerity [Ancestor] - Oriental Adventures, p. 66
+Speed of Thought [Psionic] - Psionics Handbook, p. 29
+Spell Focus [General] - Player's Handbook, p. 85
+Spell Girding [General] - Magic of Faerun, p. 22
+Spell Mastery [Special] - Player's Handbook, p. 85 (rules: p. 54)
+Spell Penetration [General] - Player's Handbook, p. 85
+Spell Power [Ancestor] - Oriental Adventures, p. 66
+Spell Specialization [General] - Tome and Blood, p. 42
+Spell Thematics [General] - Magic of Faerun, p. 22
+Spellcaster Support [Ancestor] - Oriental Adventures, p. 66
+Spellcasting Prodigy [General] - Forgotten Realms Campaign Setting, p. 38
+Spellfire Wielder [General] - Magic of Faerun, p. 23
+Spirited Charge [General] - Player's Handbook, p. 85
+Split Psionic Ray [Metapsionic] - Dragon Magazine #287, p. 56
+Split Ray [Metamagic] - Tome and Blood, p. 42
+Spook Animals [Special] - Dragon Magazine d20 Special/Annual #6, p. 65
+Spring Attack [General] - Player's Handbook, p. 85
+Staggering Blow [General] - Dragon Magazine #279, p. 63
+Stand Still [Psionic] - Psionics Handbook, p. 29
+Stare-Down [General] - Scrollworks #11, p. 5
+Stealth [General] - Traps & Treachery, p. 39
+Stealthy [General] - Forgotten Realms Campaign Setting, p. 38
+Still Spell [Metamagic] - Player's Handbook, p. 85
+Stoic Composure [General] - Dragon Magazine #284, p. 123
+Street Smart [General] - Forgotten Realms Campaign Setting, p. 38
+Strength of the Charger [Ancestor] - Oriental Adventures, p. 66
+Strength of the Crab [Ancestor] - Oriental Adventures, p. 66
+Strength of Personality [Special] - Demonology, p. 41
+Strong Soul [General] - Forgotten Realms Campaign Setting, p. 38
+Strong Soul [Ancestor] - Oriental Adventures, p. 66
+Stunning Fist [General] - Player's Handbook, p. 85
+Stunning Roar [Kaiju] - Dragon Magazine #289, p. 70
+Subdual Substitution [Metamagic] - Tome and Blood, p. 42
+Subduing Strike [General] - Sovereign Stone Campaign Sourcebook, p. 63
+Subsonics [General] - Song and Silence, p. 40
+Subtle Charm [Special] - Touched by the Gods, p. 71
+Sunder [General] - Player's Handbook, p. 85
+Superior Expertise [General] - Oriental Adventures, p. 66
+Survival Instincts [General] - The Taan, p. 83
+Survivor [General] - Forgotten Realms Campaign Setting, p. 38
+Swarmfighting [General] - Dragon Magazine #285, p. 33
+Swift Twist Glance [General] - Mystic Warriors, p. 113
+Tail Attack [Special] - Dragon Magazine d20 Special/Annual #6, p. 61
+Talented [Psionic] - Psionics Handbook, p. 30
+Tattoo Focus [Special] - Forgotten Realms Campaign Setting, p. 38
+Taut Tug [General] - Mystic Warriors, p. 113
+Ten Animal Style [Special] - Creature Collection, p. 72
+Tenacious Magic [Metamagic] - Forgotten Realms Campaign Setting, p. 38
+Thick Hide [General] - The Taan, p. 83
+Thick Skin [General] - Dungeons, p. 82
+Thrall Master [Special] - Touched by the Gods, p. 71
+Throw Anything [General] - Sword and Fist, p. 9
+Thug [General] - Forgotten Realms Campaign Setting, p. 38
+Thunder Twin [General] - Forgotten Realms Campaign Setting, p. 38
+Thunderous Roar [Kaiju] - Dragon Magazine #289, p. 70
+Tinker [General] - Dungeons, p. 82
+Token Familiar [General] - Dragon Magazine #280, p. 62
+Toughness [General] - Player's Handbook, p. 85
+Track [General] - Player's Handbook, p. 85
+Trample [General] - Player's Handbook, p. 86
+Treetopper [General] - Forgotten Realms Campaign Setting, p. 38
+Trigger Power [Psionic] - Psionics Handbook, p. 30
+Trustworthy [General] - Song and Silence, p. 40
+Turn Outsider [Special] - Evil, p. 60
+Turn Resistance [General] - Mythic Races, p. 50
+Twin Power [Psionic] - Psionics Handbook, p. 30
+Twin Spell [Metamagic] - Tome and Blood, p. 42
+Twin Sword Style [Fighter, General] - Forgotten Realms Campaign Setting, p. 39
+Two-Weapon Fighting [General] - Player's Handbook, p. 86
+Tyrant [General] - Evil, p. 60
+Ultimate Feint [General] - Evil, p. 62
+Unavoidable Strike [Psionic] - Psionics Handbook, p. 30
+Unbalancing Strike [General] - Oriental Adventures, p. 66
+Undead Familiar [General] - Dragon Magazine #280, p. 62
+Undetectable Lie [Infernal] - Evil, p. 28
+Unholy Blessing [Infernal] - Evil, p. 28
+Unholy Strength [Infernal] - Evil, p. 28
+Unorthodox Flurry [Special] - Dragon Magazine #279, p. 63
+Unusual Background [General] - Diomin, p. 43
+Unyielding Aura [General] - Mythic Races, p. 69
+Up the Walls [Psionic] - Psionics Handbook, p. 30
+Upgrade Power [Psionic] - Dragon Magazine #287, p. 56
+Vengeful Strike [General] - Touched by the Gods, p. 109
+Vitus Kata [Mystic Warrior] - Mystic Warriors, p. 113
+Wall Breaker [General] - Dragon Magazine #285, p. 98
+Warrior Instinct [Ancestor] - Oriental Adventures, p. 66
+Warrior Shugenja [Ancestor] - Oriental Adventures, p. 66
+Warsinger [Special] - Touched by the Gods, p. 109
+Water-ken [General] - The Taan, p. 83
+Wealth [Infernal] - Evil, p. 29
+Weapon Expertise: Throwing Spear [General] - Ragnarok!, p. 10
+Weapon Finesse [General] - Player's Handbook, p. 86
+Weapon Focus [General] - Player's Handbook, p. 86
+Weapon Specialization [Special] - Player's Handbook, p. 86 (rules: p. 37)
+Weapon-Catching [General] - Ragnarok!, p. 9
+Whirlwind Attack [General] - Player's Handbook, p. 86
+Widen Power [Metapsionic] - Dragon Magazine #287, p. 56
+Widen Spell [Metamagic] - Tome and Blood, p. 42
+Wish [Infernal] - Evil, p. 29
+Wrath of Calaam [Special] - Touched by the Gods, p. 59
+Zen Archery [General] - Sword and Fist, p. 9
Binary file images/8ball.gif has changed
Binary file images/WAmisc7.ico has changed
Binary file images/WAmisc9.ico has changed
Binary file images/add_filter.gif has changed
Binary file images/apoc.gif has changed
Binary file images/arc.png has changed
Binary file images/b_d10.gif has changed
Binary file images/b_d100.gif has changed
Binary file images/b_d12.gif has changed
Binary file images/b_d20.gif has changed
Binary file images/b_d4.gif has changed
Binary file images/b_d6.gif has changed
Binary file images/b_d8.gif has changed
Binary file images/bold.gif has changed
Binary file images/book.gif has changed
Binary file images/bricktile.gif has changed
Binary file images/browser.gif has changed
Binary file images/bullet.gif has changed
Binary file images/car.gif has changed
Binary file images/ccmap.gif has changed
Binary file images/chess.gif has changed
Binary file images/circle.png has changed
Binary file images/clear.gif has changed
Binary file images/close_wnd.bmp has changed
Binary file images/compass.gif has changed
Binary file images/compass.ico has changed
Binary file images/connect.gif has changed
Binary file images/crosshair.gif has changed
Binary file images/cyborg.gif has changed
Binary file images/d10.gif has changed
Binary file images/d20.gif has changed
Binary file images/d20.ico has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/images/d20.xpm	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2448 @@
+/* XPM */
+static char * d20_xpm[] = {
+"64 64 2381 2",
+"  	c None",
+". 	c #8E8481",
+"+ 	c #6B615A",
+"@ 	c #6D635B",
+"# 	c #6B615D",
+"$ 	c #4E4443",
+"% 	c #514641",
+"& 	c #564B46",
+"* 	c #615954",
+"= 	c #6A6763",
+"- 	c #474344",
+"; 	c #5D5857",
+"> 	c #676163",
+", 	c #7E777B",
+"' 	c #746B67",
+") 	c #665C5A",
+"! 	c #605858",
+"~ 	c #655E61",
+"{ 	c #585155",
+"] 	c #4A4143",
+"^ 	c #373232",
+"/ 	c #6A6564",
+"( 	c #787272",
+"_ 	c #7F7979",
+": 	c #796E6D",
+"< 	c #807373",
+"[ 	c #7F7A78",
+"} 	c #7E7978",
+"| 	c #6F6668",
+"1 	c #645C61",
+"2 	c #6B6466",
+"3 	c #494446",
+"4 	c #27262D",
+"5 	c #626169",
+"6 	c #625E63",
+"7 	c #6E696D",
+"8 	c #7A7679",
+"9 	c #5B595A",
+"0 	c #2B2A2A",
+"a 	c #151212",
+"b 	c #615C5A",
+"c 	c #726968",
+"d 	c #6C6565",
+"e 	c #5C5555",
+"f 	c #675E5D",
+"g 	c #7C7474",
+"h 	c #716D6E",
+"i 	c #6D6B70",
+"j 	c #817877",
+"k 	c #817D80",
+"l 	c #807D7D",
+"m 	c #78726B",
+"n 	c #7A7871",
+"o 	c #6C6B65",
+"p 	c #777773",
+"q 	c #605E5D",
+"r 	c #453D37",
+"s 	c #7E746A",
+"t 	c #766C67",
+"u 	c #766D6E",
+"v 	c #7A7779",
+"w 	c #706767",
+"x 	c #919296",
+"y 	c #87838B",
+"z 	c #7D7A82",
+"A 	c #746C68",
+"B 	c #7B736D",
+"C 	c #665D58",
+"D 	c #4B413F",
+"E 	c #5B5350",
+"F 	c #4E4643",
+"G 	c #352F2C",
+"H 	c #393534",
+"I 	c #3B3836",
+"J 	c #3F3B37",
+"K 	c #4A4541",
+"L 	c #5F5954",
+"M 	c #6A615D",
+"N 	c #645C53",
+"O 	c #6F675B",
+"P 	c #655C55",
+"Q 	c #534948",
+"R 	c #695E59",
+"S 	c #6A6160",
+"T 	c #4C4644",
+"U 	c #5A5551",
+"V 	c #5B5652",
+"W 	c #544845",
+"X 	c #4E413E",
+"Y 	c #3B3633",
+"Z 	c #483F3F",
+"` 	c #554A46",
+" .	c #605657",
+"..	c #635959",
+"+.	c #6D6564",
+"@.	c #766F72",
+"#.	c #5D585C",
+"$.	c #4E494D",
+"%.	c #494547",
+"&.	c #494342",
+"*.	c #595453",
+"=.	c #7B7674",
+"-.	c #6B6566",
+";.	c #645A61",
+">.	c #584E4F",
+",.	c #695F61",
+"'.	c #685E5D",
+").	c #756D6C",
+"!.	c #706C6D",
+"~.	c #585758",
+"{.	c #6F6763",
+"].	c #716E6C",
+"^.	c #6F6D69",
+"/.	c #746E65",
+"(.	c #534E49",
+"_.	c #2F2B28",
+":.	c #6D6863",
+"<.	c #6D665D",
+"[.	c #6B6258",
+"}.	c #716A5E",
+"|.	c #665E57",
+"1.	c #6A6363",
+"2.	c #7E787F",
+"3.	c #7E7875",
+"4.	c #838786",
+"5.	c #8C898E",
+"6.	c #88858A",
+"7.	c #7C7373",
+"8.	c #867C7A",
+"9.	c #86807F",
+"0.	c #7E7973",
+"a.	c #777070",
+"b.	c #686161",
+"c.	c #5C5657",
+"d.	c #666263",
+"e.	c #565250",
+"f.	c #6C6762",
+"g.	c #747067",
+"h.	c #736F65",
+"i.	c #6D6660",
+"j.	c #7C766B",
+"k.	c #676055",
+"l.	c #6E675D",
+"m.	c #787066",
+"n.	c #837872",
+"o.	c #7E7573",
+"p.	c #5A5250",
+"q.	c #7C7571",
+"r.	c #837C79",
+"s.	c #7F746F",
+"t.	c #625751",
+"u.	c #726966",
+"v.	c #776B68",
+"w.	c #8E8276",
+"x.	c #817770",
+"y.	c #726865",
+"z.	c #7B716F",
+"A.	c #6C625E",
+"B.	c #7A706C",
+"C.	c #88817C",
+"D.	c #8D8882",
+"E.	c #76706B",
+"F.	c #88807C",
+"G.	c #675F5A",
+"H.	c #6D6762",
+"I.	c #6C6564",
+"J.	c #7C7276",
+"K.	c #675C5F",
+"L.	c #726769",
+"M.	c #7D7374",
+"N.	c #807779",
+"O.	c #7E787A",
+"P.	c #5B5959",
+"Q.	c #625C59",
+"R.	c #5C5855",
+"S.	c #5A5751",
+"T.	c #615D53",
+"U.	c #7F7B73",
+"V.	c #625F57",
+"W.	c #68635C",
+"X.	c #6E6462",
+"Y.	c #756C6B",
+"Z.	c #7A7573",
+"`.	c #7C7778",
+" +	c #7C767B",
+".+	c #736C70",
+"++	c #757273",
+"@+	c #636766",
+"#+	c #696A6C",
+"$+	c #646165",
+"%+	c #726860",
+"&+	c #6E605A",
+"*+	c #847D7B",
+"=+	c #1E1E1B",
+"-+	c #0B0A0B",
+";+	c #0E0D0D",
+">+	c #0C0B0B",
+",+	c #0D0C0C",
+"'+	c #0C0B0C",
+")+	c #0F0D0D",
+"!+	c #110E0E",
+"~+	c #100D0D",
+"{+	c #080606",
+"]+	c #090606",
+"^+	c #0A0808",
+"/+	c #0E0C0C",
+"(+	c #0F0C0C",
+"_+	c #100F0D",
+":+	c #0B0A09",
+"<+	c #0B0B09",
+"[+	c #100F0E",
+"}+	c #0F0E0D",
+"|+	c #131010",
+"1+	c #141011",
+"2+	c #130E0E",
+"3+	c #100C09",
+"4+	c #0E0D09",
+"5+	c #0F0E0E",
+"6+	c #110F11",
+"7+	c #0C0908",
+"8+	c #0F0C0B",
+"9+	c #11100F",
+"0+	c #100E0E",
+"a+	c #0B0A0A",
+"b+	c #111010",
+"c+	c #090808",
+"d+	c #0E0B0B",
+"e+	c #0A0707",
+"f+	c #0A0B09",
+"g+	c #030302",
+"h+	c #100D0E",
+"i+	c #100F0F",
+"j+	c #0E0D0E",
+"k+	c #110F13",
+"l+	c #333132",
+"m+	c #78777A",
+"n+	c #73777F",
+"o+	c #747476",
+"p+	c #766E68",
+"q+	c #796E68",
+"r+	c #7C7268",
+"s+	c #121010",
+"t+	c #000000",
+"u+	c #000001",
+"v+	c #000002",
+"w+	c #24201F",
+"x+	c #706B68",
+"y+	c #7F8384",
+"z+	c #636865",
+"A+	c #796D72",
+"B+	c #655753",
+"C+	c #6E6158",
+"D+	c #211F1F",
+"E+	c #6D6663",
+"F+	c #75736F",
+"G+	c #626466",
+"H+	c #817A74",
+"I+	c #7F7471",
+"J+	c #6C605D",
+"K+	c #1B1A1B",
+"L+	c #665C5B",
+"M+	c #554F4C",
+"N+	c #414144",
+"O+	c #797270",
+"P+	c #6C6360",
+"Q+	c #605651",
+"R+	c #0D0B0B",
+"S+	c #201F20",
+"T+	c #524B4C",
+"U+	c #666060",
+"V+	c #585458",
+"W+	c #615759",
+"X+	c #605856",
+"Y+	c #625957",
+"Z+	c #0C0B0A",
+"`+	c #232324",
+" @	c #5E5D60",
+".@	c #666163",
+"+@	c #716968",
+"@@	c #827D7C",
+"#@	c #645F61",
+"$@	c #7B7476",
+"%@	c #67676D",
+"&@	c #7C7B80",
+"*@	c #7F7B7D",
+"=@	c #84817F",
+"-@	c #787174",
+";@	c #6C6364",
+">@	c #242425",
+",@	c #767780",
+"'@	c #9B9EA7",
+")@	c #858890",
+"!@	c #7D7776",
+"~@	c #877C81",
+"{@	c #877C7D",
+"]@	c #242325",
+"^@	c #82828A",
+"/@	c #8E8F95",
+"(@	c #7A7B81",
+"_@	c #706867",
+":@	c #6D6065",
+"<@	c #7B6E6F",
+"[@	c #232325",
+"}@	c #706F75",
+"|@	c #717074",
+"1@	c #858386",
+"2@	c #5F5E5A",
+"3@	c #433B39",
+"4@	c #4C403F",
+"5@	c #252528",
+"6@	c #6E6D76",
+"7@	c #7F7D84",
+"8@	c #746F71",
+"9@	c #7D7E81",
+"0@	c #4C4747",
+"a@	c #5F5552",
+"b@	c #0C0A09",
+"c@	c #797A82",
+"d@	c #8E8F98",
+"e@	c #86838E",
+"f@	c #6E6D71",
+"g@	c #494445",
+"h@	c #595150",
+"i@	c #000102",
+"j@	c #020102",
+"k@	c #242321",
+"l@	c #43423D",
+"m@	c #3E3E3B",
+"n@	c #1C1D1D",
+"o@	c #020204",
+"p@	c #010102",
+"q@	c #272729",
+"r@	c #89878C",
+"s@	c #8B8A8C",
+"t@	c #8E8B90",
+"u@	c #636164",
+"v@	c #393334",
+"w@	c #423B3B",
+"x@	c #060304",
+"y@	c #0B0B0A",
+"z@	c #3F3D38",
+"A@	c #918C80",
+"B@	c #929086",
+"C@	c #767576",
+"D@	c #707070",
+"E@	c #626260",
+"F@	c #3B3B3A",
+"G@	c #0B0B0B",
+"H@	c #262628",
+"I@	c #817D86",
+"J@	c #928D91",
+"K@	c #8F8B86",
+"L@	c #5A575A",
+"M@	c #696464",
+"N@	c #827C7C",
+"O@	c #010101",
+"P@	c #22221F",
+"Q@	c #565753",
+"R@	c #80817C",
+"S@	c #7C7D7D",
+"T@	c #777776",
+"U@	c #787471",
+"V@	c #777471",
+"W@	c #727071",
+"X@	c #737277",
+"Y@	c #717177",
+"Z@	c #6F6F74",
+"`@	c #636363",
+" #	c #4E4E4E",
+".#	c #252525",
+"+#	c #030303",
+"@#	c #29282A",
+"##	c #7F7C82",
+"$#	c #8B8788",
+"%#	c #928E8F",
+"&#	c #302B2B",
+"*#	c #84817D",
+"=#	c #817C78",
+"-#	c #090908",
+";#	c #3E3D3A",
+">#	c #7A7973",
+",#	c #868680",
+"'#	c #808080",
+")#	c #7A7A7E",
+"!#	c #797781",
+"~#	c #737279",
+"{#	c #79777B",
+"]#	c #7B7779",
+"^#	c #807B7D",
+"/#	c #77777A",
+"(#	c #77787C",
+"_#	c #75767B",
+":#	c #78787A",
+"<#	c #787877",
+"[#	c #747372",
+"}#	c #5D5B5C",
+"|#	c #312F30",
+"1#	c #0A0A0A",
+"2#	c #242323",
+"3#	c #777175",
+"4#	c #8E8787",
+"5#	c #868285",
+"6#	c #3A383A",
+"7#	c #7B716E",
+"8#	c #776B67",
+"9#	c #0A0908",
+"0#	c #1E1E1C",
+"a#	c #50504D",
+"b#	c #838480",
+"c#	c #93928E",
+"d#	c #888683",
+"e#	c #767577",
+"f#	c #706F76",
+"g#	c #787681",
+"h#	c #7E7D82",
+"i#	c #7F7F80",
+"j#	c #636361",
+"k#	c #535350",
+"l#	c #6B6A68",
+"m#	c #6C6B6B",
+"n#	c #777678",
+"o#	c #7E7D7F",
+"p#	c #807F82",
+"q#	c #828281",
+"r#	c #797877",
+"s#	c #716F70",
+"t#	c #767475",
+"u#	c #6C6C6C",
+"v#	c #4A4A4A",
+"w#	c #181818",
+"x#	c #020202",
+"y#	c #161515",
+"z#	c #625A5A",
+"A#	c #797676",
+"B#	c #797472",
+"C#	c #756A66",
+"D#	c #807571",
+"E#	c #010000",
+"F#	c #070606",
+"G#	c #31302F",
+"H#	c #656564",
+"I#	c #8A8A87",
+"J#	c #8A8B86",
+"K#	c #878884",
+"L#	c #777775",
+"M#	c #7D7C7D",
+"N#	c #7D7C7F",
+"O#	c #78787B",
+"P#	c #807F83",
+"Q#	c #7E7D84",
+"R#	c #4E4D52",
+"S#	c #38383A",
+"T#	c #474649",
+"U#	c #535354",
+"V#	c #575455",
+"W#	c #6D696A",
+"X#	c #8B8889",
+"Y#	c #858485",
+"Z#	c #767575",
+"`#	c #7A7979",
+" $	c #737273",
+".$	c #7E7E7E",
+"+$	c #787878",
+"@$	c #6E6E6F",
+"#$	c #636365",
+"$$	c #28282A",
+"%$	c #070707",
+"&$	c #1E1D1D",
+"*$	c #686164",
+"=$	c #46464B",
+"-$	c #787377",
+";$	c #79706F",
+">$	c #756A63",
+",$	c #766B64",
+"'$	c #090806",
+")$	c #010001",
+"!$	c #2B292A",
+"~$	c #6C6A6B",
+"{$	c #72716F",
+"]$	c #7E7D7B",
+"^$	c #7D7D7C",
+"/$	c #737372",
+"($	c #6F6F71",
+"_$	c #808082",
+":$	c #868688",
+"<$	c #767678",
+"[$	c #7C7C7F",
+"}$	c #1E1E21",
+"|$	c #2C2C2D",
+"1$	c #434342",
+"2$	c #3E3B3A",
+"3$	c #625E5E",
+"4$	c #7A7777",
+"5$	c #7F7D7E",
+"6$	c #737172",
+"7$	c #7A797A",
+"8$	c #7F7F7F",
+"9$	c #7C7C7C",
+"0$	c #797979",
+"a$	c #717171",
+"b$	c #646465",
+"c$	c #616161",
+"d$	c #2C2C2C",
+"e$	c #212021",
+"f$	c #555053",
+"g$	c #737073",
+"h$	c #757074",
+"i$	c #6E6564",
+"j$	c #6A5F58",
+"k$	c #756B64",
+"l$	c #080706",
+"m$	c #050404",
+"n$	c #666465",
+"o$	c #656364",
+"p$	c #747371",
+"q$	c #6A6967",
+"r$	c #6B6B6B",
+"s$	c #646467",
+"t$	c #69686B",
+"u$	c #727274",
+"v$	c #777779",
+"w$	c #838483",
+"x$	c #7A7A7A",
+"y$	c #74737A",
+"z$	c #73727A",
+"A$	c #78777C",
+"B$	c #555558",
+"C$	c #444446",
+"D$	c #474746",
+"E$	c #666360",
+"F$	c #86837F",
+"G$	c #7F7C79",
+"H$	c #706E6F",
+"I$	c #6B696A",
+"J$	c #797879",
+"K$	c #676767",
+"L$	c #60615F",
+"M$	c #5F5F5D",
+"N$	c #5E5F5C",
+"O$	c #5B5B5B",
+"P$	c #666666",
+"Q$	c #111111",
+"R$	c #222222",
+"S$	c #5E5B5D",
+"T$	c #73696E",
+"U$	c #726B70",
+"V$	c #776C6C",
+"W$	c #746761",
+"X$	c #695E5E",
+"Y$	c #040404",
+"Z$	c #060606",
+"`$	c #6F6F6F",
+" %	c #5F5F5F",
+".%	c #666664",
+"+%	c #626261",
+"@%	c #626264",
+"#%	c #69696B",
+"$%	c #717173",
+"%%	c #7B7B7D",
+"&%	c #777778",
+"*%	c #747378",
+"=%	c #6C6B72",
+"-%	c #807F86",
+";%	c #6D6C71",
+">%	c #737373",
+",%	c #848381",
+"'%	c #7C7B79",
+")%	c #7F7E7C",
+"!%	c #6A696B",
+"~%	c #6E6E70",
+"{%	c #575757",
+"]%	c #595958",
+"^%	c #565656",
+"/%	c #626262",
+"(%	c #202020",
+"_%	c #050505",
+":%	c #252223",
+"<%	c #423F41",
+"[%	c #797478",
+"}%	c #80777C",
+"|%	c #7D7477",
+"1%	c #5C524E",
+"2%	c #6B6262",
+"3%	c #030202",
+"4%	c #000100",
+"5%	c #090909",
+"6%	c #0D0D0D",
+"7%	c #0F0F0F",
+"8%	c #121212",
+"9%	c #151515",
+"0%	c #1B1B1B",
+"a%	c #585858",
+"b%	c #525051",
+"c%	c #61615F",
+"d%	c #5D5D5C",
+"e%	c #5A5A5D",
+"f%	c #5C5C5E",
+"g%	c #5E5E60",
+"h%	c #69696A",
+"i%	c #6B6B6C",
+"j%	c #6C6C70",
+"k%	c #747477",
+"l%	c #7D7D7D",
+"m%	c #757573",
+"n%	c #81807E",
+"o%	c #757472",
+"p%	c #747273",
+"q%	c #757374",
+"r%	c #646365",
+"s%	c #595959",
+"t%	c #545454",
+"u%	c #535353",
+"v%	c #282828",
+"w%	c #1A1A1A",
+"x%	c #161616",
+"y%	c #272425",
+"z%	c #5C5659",
+"A%	c #736D72",
+"B%	c #8E898D",
+"C%	c #6A6468",
+"D%	c #544B4B",
+"E%	c #080908",
+"F%	c #0C0D0D",
+"G%	c #141414",
+"H%	c #1D1D1D",
+"I%	c #212121",
+"J%	c #1F1F20",
+"K%	c #202021",
+"L%	c #212122",
+"M%	c #5D5D5E",
+"N%	c #505051",
+"O%	c #4E4D4D",
+"P%	c #414041",
+"Q%	c #353534",
+"R%	c #4B4B4D",
+"S%	c #515154",
+"T%	c #555557",
+"U%	c #5B5B5D",
+"V%	c #575758",
+"W%	c #68686B",
+"X%	c #67676B",
+"Y%	c #6C6C6E",
+"Z%	c #767677",
+"`%	c #7C7C7D",
+" &	c #6A6A69",
+".&	c #6F6E6D",
+"+&	c #626160",
+"@&	c #646263",
+"#&	c #646363",
+"$&	c #5B5B5C",
+"%&	c #575759",
+"&&	c #545456",
+"*&	c #4F4F50",
+"=&	c #494949",
+"-&	c #4C4C4D",
+";&	c #4D4D4E",
+">&	c #4D4D4D",
+",&	c #262626",
+"'&	c #232323",
+")&	c #1F1F1F",
+"!&	c #191919",
+"~&	c #080808",
+"{&	c #1A1819",
+"]&	c #716967",
+"^&	c #827D79",
+"/&	c #82817F",
+"(&	c #787477",
+"_&	c #494341",
+":&	c #4B4542",
+"<&	c #1D1A19",
+"[&	c #1B1B19",
+"}&	c #242424",
+"|&	c #2E2E2E",
+"1&	c #303030",
+"2&	c #2F2F2F",
+"3&	c #2D2D2D",
+"4&	c #2F2F31",
+"5&	c #303032",
+"6&	c #2C2C2E",
+"7&	c #5D5D5F",
+"8&	c #4F4F51",
+"9&	c #383838",
+"0&	c #0C0C0D",
+"a&	c #323234",
+"b&	c #4A4A4C",
+"c&	c #49494B",
+"d&	c #515153",
+"e&	c #565658",
+"f&	c #59595B",
+"g&	c #5C5B60",
+"h&	c #5A595E",
+"i&	c #616065",
+"j&	c #5E5E5E",
+"k&	c #5A5A5A",
+"l&	c #525253",
+"m&	c #4C4C4E",
+"n&	c #4E4E50",
+"o&	c #474749",
+"p&	c #434345",
+"q&	c #424244",
+"r&	c #4D4D4F",
+"s&	c #363638",
+"t&	c #353535",
+"u&	c #373737",
+"v&	c #333333",
+"w&	c #272727",
+"x&	c #1B1A1A",
+"y&	c #171717",
+"z&	c #665E5D",
+"A&	c #7B7675",
+"B&	c #767478",
+"C&	c #65615F",
+"D&	c #343331",
+"E&	c #454341",
+"F&	c #272423",
+"G&	c #2A2A28",
+"H&	c #313130",
+"I&	c #343434",
+"J&	c #3B3B3B",
+"K&	c #3D3D3D",
+"L&	c #363636",
+"M&	c #373738",
+"N&	c #3E3E40",
+"O&	c #3F3F41",
+"P&	c #343436",
+"Q&	c #1C1C1C",
+"R&	c #1B1B1C",
+"S&	c #525254",
+"T&	c #505052",
+"U&	c #535257",
+"V&	c #545358",
+"W&	c #525155",
+"X&	c #515151",
+"Y&	c #4F4F4F",
+"Z&	c #525252",
+"`&	c #464648",
+" *	c #3A3A3B",
+".*	c #434343",
+"+*	c #444444",
+"@*	c #424242",
+"#*	c #414141",
+"$*	c #3F3F3F",
+"%*	c #393939",
+"&*	c #2A2929",
+"**	c #373637",
+"=*	c #6F656A",
+"-*	c #8A8489",
+";*	c #726F72",
+">*	c #5B564E",
+",*	c #1D1A15",
+"'*	c #272624",
+")*	c #302F2D",
+"!*	c #444442",
+"~*	c #3D3C3F",
+"{*	c #444245",
+"]*	c #454443",
+"^*	c #484646",
+"/*	c #484647",
+"(*	c #4C4B4C",
+"_*	c #535355",
+":*	c #484849",
+"<*	c #555555",
+"[*	c #313131",
+"}*	c #484848",
+"|*	c #474748",
+"1*	c #4A4A4B",
+"2*	c #4B4B4C",
+"3*	c #4B4B4B",
+"4*	c #454547",
+"5*	c #414143",
+"6*	c #464647",
+"7*	c #3A3A3A",
+"8*	c #48484A",
+"9*	c #515152",
+"0*	c #49494A",
+"a*	c #444445",
+"b*	c #3C3B3D",
+"c*	c #444242",
+"d*	c #756F71",
+"e*	c #888489",
+"f*	c #7D7D80",
+"g*	c #4B443C",
+"h*	c #39312B",
+"i*	c #171617",
+"j*	c #32312F",
+"k*	c #454442",
+"l*	c #494946",
+"m*	c #4D4C4E",
+"n*	c #595758",
+"o*	c #515051",
+"p*	c #535152",
+"q*	c #5A5859",
+"r*	c #5E5D5E",
+"s*	c #464646",
+"t*	c #3C3C3C",
+"u*	c #505050",
+"v*	c #474747",
+"w*	c #4C4C4C",
+"x*	c #404042",
+"y*	c #3C3C3E",
+"z*	c #444447",
+"A*	c #656565",
+"B*	c #676768",
+"C*	c #58585A",
+"D*	c #4A494C",
+"E*	c #514E4E",
+"F*	c #8C8585",
+"G*	c #837D7F",
+"H*	c #797777",
+"I*	c #554E48",
+"J*	c #251D19",
+"K*	c #282525",
+"L*	c #3C3A39",
+"M*	c #61615E",
+"N*	c #5D5C5D",
+"O*	c #656465",
+"P*	c #5E5D5C",
+"Q*	c #545357",
+"R*	c #656365",
+"S*	c #686767",
+"T*	c #747376",
+"U*	c #69696C",
+"V*	c #515053",
+"W*	c #616062",
+"X*	c #4E4D4F",
+"Y*	c #494948",
+"Z*	c #1C1C1B",
+"`*	c #20201F",
+" =	c #1E1E1E",
+".=	c #424243",
+"+=	c #414142",
+"@=	c #424145",
+"#=	c #424246",
+"$=	c #6F6F70",
+"%=	c #666667",
+"&=	c #666668",
+"*=	c #6A6A6B",
+"==	c #595756",
+"-=	c #7F7676",
+";=	c #7C7677",
+">=	c #76706F",
+",=	c #555354",
+"'=	c #464542",
+")=	c #343130",
+"!=	c #474444",
+"~=	c #5F5F5B",
+"{=	c #636260",
+"]=	c #79787A",
+"^=	c #777774",
+"/=	c #777777",
+"(=	c #757473",
+"_=	c #807F7E",
+":=	c #747275",
+"<=	c #69676A",
+"[=	c #565458",
+"}=	c #565459",
+"|=	c #4F4D51",
+"1=	c #4A4A49",
+"2=	c #4C4C4A",
+"3=	c #4F4F4D",
+"4=	c #464644",
+"5=	c #333331",
+"6=	c #31312F",
+"7=	c #292929",
+"8=	c #3F403F",
+"9=	c #444548",
+"0=	c #3F4044",
+"a=	c #706F74",
+"b=	c #747475",
+"c=	c #6A6A6C",
+"d=	c #848482",
+"e=	c #6B6B6D",
+"f=	c #61605F",
+"g=	c #736A6B",
+"h=	c #756F73",
+"i=	c #7B7372",
+"j=	c #8E8C91",
+"k=	c #413E3F",
+"l=	c #494746",
+"m=	c #646560",
+"n=	c #636163",
+"o=	c #71706F",
+"p=	c #80807F",
+"q=	c #7B7C79",
+"r=	c #7B7B78",
+"s=	c #7D7C7A",
+"t=	c #767573",
+"u=	c #7D7B7A",
+"v=	c #4D4B4B",
+"w=	c #444244",
+"x=	c #5C5A5C",
+"y=	c #585856",
+"z=	c #3C3C3A",
+"A=	c #3B3B39",
+"B=	c #454545",
+"C=	c #3D3F3E",
+"D=	c #4B4C4E",
+"E=	c #454549",
+"F=	c #5F5E63",
+"G=	c #6F6E73",
+"H=	c #767676",
+"I=	c #7A7A7B",
+"J=	c #737375",
+"K=	c #818181",
+"L=	c #7C7C7A",
+"M=	c #838281",
+"N=	c #6E6D6F",
+"O=	c #736C6E",
+"P=	c #736A67",
+"Q=	c #77716D",
+"R=	c #8E8C90",
+"S=	c #8C898F",
+"T=	c #4F4D50",
+"U=	c #484745",
+"V=	c #6B6C68",
+"W=	c #6C6C6D",
+"X=	c #747474",
+"Y=	c #7E7E7C",
+"Z=	c #757574",
+"`=	c #4E4D4E",
+" -	c #4A494B",
+".-	c #4F4E50",
+"+-	c #51514F",
+"@-	c #535352",
+"#-	c #555553",
+"$-	c #535351",
+"%-	c #515150",
+"&-	c #1D1E1F",
+"*-	c #2C2D2E",
+"=-	c #434144",
+"--	c #39383C",
+";-	c #3E3E42",
+">-	c #49484D",
+",-	c #676769",
+"'-	c #787979",
+")-	c #787A79",
+"!-	c #7B7D7C",
+"~-	c #797A78",
+"{-	c #797978",
+"]-	c #7C7B7C",
+"^-	c #6C6C6B",
+"/-	c #5E5754",
+"(-	c #413A36",
+"_-	c #6B6661",
+":-	c #918F92",
+"<-	c #86848A",
+"[-	c #38363B",
+"}-	c #757576",
+"|-	c #878787",
+"1-	c #838383",
+"2-	c #4F4F52",
+"3-	c #545452",
+"4-	c #565655",
+"5-	c #595957",
+"6-	c #2D2D2F",
+"7-	c #303133",
+"8-	c #2D2E30",
+"9-	c #434245",
+"0-	c #413F43",
+"a-	c #39393A",
+"b-	c #4A4B48",
+"c-	c #4B4B48",
+"d-	c #454546",
+"e-	c #535356",
+"f-	c #7A7B7B",
+"g-	c #717372",
+"h-	c #7A7C7B",
+"i-	c #6D6E6F",
+"j-	c #707173",
+"k-	c #6D6D6E",
+"l-	c #666565",
+"m-	c #686766",
+"n-	c #665E58",
+"o-	c #706862",
+"p-	c #716A63",
+"q-	c #9A989B",
+"r-	c #7F7C83",
+"s-	c #5D5B62",
+"t-	c #404144",
+"u-	c #616165",
+"v-	c #757578",
+"w-	c #757577",
+"x-	c #8D8D8D",
+"y-	c #8B8B8B",
+"z-	c #848484",
+"A-	c #3D3D40",
+"B-	c #565657",
+"C-	c #525350",
+"D-	c #525250",
+"E-	c #5B5B5A",
+"F-	c #535451",
+"G-	c #3A3B3C",
+"H-	c #3E3D40",
+"I-	c #3F3E40",
+"J-	c #3F3F3E",
+"K-	c #4E4E4A",
+"L-	c #51514D",
+"M-	c #474745",
+"N-	c #7D7D7E",
+"O-	c #7B7C7D",
+"P-	c #6F7071",
+"Q-	c #737475",
+"R-	c #6A6A6E",
+"S-	c #6F6E6F",
+"T-	c #5A514A",
+"U-	c #776D64",
+"V-	c #736A62",
+"W-	c #928E91",
+"X-	c #87878C",
+"Y-	c #939296",
+"Z-	c #3E4046",
+"`-	c #5F5E65",
+" ;	c #6B686E",
+".;	c #69686D",
+"+;	c #6D6E70",
+"@;	c #868583",
+"#;	c #92918F",
+"$;	c #939391",
+"%;	c #888886",
+"&;	c #92928F",
+"*;	c #858583",
+"=;	c #838384",
+"-;	c #49484B",
+";;	c #424148",
+">;	c #4C4C4F",
+",;	c #575755",
+"';	c #4D4E4D",
+");	c #4D4E4B",
+"!;	c #52524E",
+"~;	c #4D4D4C",
+"{;	c #4B4849",
+"];	c #504D4D",
+"^;	c #545553",
+"/;	c #50504F",
+"(;	c #4B4C4B",
+"_;	c #424443",
+":;	c #404241",
+"<;	c #3D3D3E",
+"[;	c #363639",
+"};	c #3A393C",
+"|;	c #6A6A6D",
+"1;	c #78777B",
+"2;	c #868686",
+"3;	c #7D7D7B",
+"4;	c #7C7B7A",
+"5;	c #4C4C4B",
+"6;	c #4C433F",
+"7;	c #766D64",
+"8;	c #7A7571",
+"9;	c #95989E",
+"0;	c #89878A",
+"a;	c #4E4B50",
+"b;	c #5D5D60",
+"c;	c #6F7172",
+"d;	c #757474",
+"e;	c #858482",
+"f;	c #898886",
+"g;	c #8C8C8A",
+"h;	c #828280",
+"i;	c #898986",
+"j;	c #8D8D8B",
+"k;	c #464549",
+"l;	c #43414B",
+"m;	c #4C4B50",
+"n;	c #555554",
+"o;	c #525251",
+"p;	c #4B4B49",
+"q;	c #575853",
+"r;	c #575850",
+"s;	c #52534C",
+"t;	c #4A4A46",
+"u;	c #474A49",
+"v;	c #474445",
+"w;	c #4F494C",
+"x;	c #504F4D",
+"y;	c #4A4A47",
+"z;	c #3C3C39",
+"A;	c #2E2D31",
+"B;	c #2A292C",
+"C;	c #686869",
+"D;	c #7B7B7B",
+"E;	c #81817F",
+"F;	c #7A7A78",
+"G;	c #6B6A6D",
+"H;	c #565554",
+"I;	c #62595A",
+"J;	c #67625C",
+"K;	c #74716C",
+"L;	c #8D8B93",
+"M;	c #7B7683",
+"N;	c #71686E",
+"O;	c #524F50",
+"P;	c #737272",
+"Q;	c #878684",
+"R;	c #868684",
+"S;	c #7C7C79",
+"T;	c #6A6968",
+"U;	c #706F72",
+"V;	c #6B6A6E",
+"W;	c #5E5E63",
+"X;	c #48474C",
+"Y;	c #403F44",
+"Z;	c #404041",
+"`;	c #49484A",
+" >	c #454445",
+".>	c #4C4B4B",
+"+>	c #454446",
+"@>	c #575655",
+"#>	c #5B5B58",
+"$>	c #454543",
+"%>	c #282827",
+"&>	c #3C3B3B",
+"*>	c #484748",
+"=>	c #444345",
+"->	c #3F3D41",
+";>	c #464547",
+">>	c #4E4D4B",
+",>	c #454542",
+"'>	c #4B4A48",
+")>	c #434240",
+"!>	c #4E4D4A",
+"~>	c #504F4A",
+"{>	c #484441",
+"]>	c #424141",
+"^>	c #2F2E30",
+"/>	c #2D2C2D",
+"(>	c #2A2928",
+"_>	c #424140",
+":>	c #7A7A75",
+"<>	c #84847E",
+"[>	c #807F80",
+"}>	c #807E7F",
+"|>	c #727172",
+"1>	c #7B7B7A",
+"2>	c #727271",
+"3>	c #727272",
+"4>	c #6A6C6B",
+"5>	c #60605E",
+"6>	c #767271",
+"7>	c #696560",
+"8>	c #6E6960",
+"9>	c #8C848B",
+"0>	c #7D747F",
+"a>	c #776E71",
+"b>	c #4A4949",
+"c>	c #5F5E60",
+"d>	c #656266",
+"e>	c #797878",
+"f>	c #81807D",
+"g>	c #6E6D6C",
+"h>	c #686769",
+"i>	c #656469",
+"j>	c #535258",
+"k>	c #3A3940",
+"l>	c #454449",
+"m>	c #3B3B3D",
+"n>	c #343236",
+"o>	c #3B3A3A",
+"p>	c #3F3D3E",
+"q>	c #4C4B48",
+"r>	c #1F1F1D",
+"s>	c #161614",
+"t>	c #3E3D3E",
+"u>	c #484744",
+"v>	c #4B4A46",
+"w>	c #4D4C48",
+"x>	c #4B4B47",
+"y>	c #41413D",
+"z>	c #43443F",
+"A>	c #3B3734",
+"B>	c #3E3B3C",
+"C>	c #2E2C2D",
+"D>	c #323132",
+"E>	c #3A3937",
+"F>	c #4D4C4A",
+"G>	c #474644",
+"H>	c #797875",
+"I>	c #70706F",
+"J>	c #7F7E7D",
+"K>	c #7A7978",
+"L>	c #7B7C7B",
+"M>	c #747571",
+"N>	c #676461",
+"O>	c #5D5955",
+"P>	c #77726D",
+"Q>	c #7C756F",
+"R>	c #878087",
+"S>	c #777171",
+"T>	c #4E4D4C",
+"U>	c #656466",
+"V>	c #666367",
+"W>	c #6A6868",
+"X>	c #696765",
+"Y>	c #746F6F",
+"Z>	c #787473",
+"`>	c #767477",
+" ,	c #747374",
+".,	c #656567",
+"+,	c #37373C",
+"@,	c #2D2C31",
+"#,	c #29292B",
+"$,	c #333232",
+"%,	c #4D4C49",
+"&,	c #3D3D3A",
+"*,	c #494748",
+"=,	c #51504D",
+"-,	c #41403D",
+";,	c #262624",
+">,	c #242422",
+",,	c #323231",
+"',	c #373636",
+"),	c #4B4A47",
+"!,	c #54534D",
+"~,	c #464541",
+"{,	c #4F4E4A",
+"],	c #454843",
+"^,	c #3A3C37",
+"/,	c #3D3E39",
+"(,	c #4D4D49",
+"_,	c #44403E",
+":,	c #393738",
+"<,	c #3A3839",
+"[,	c #383737",
+"},	c #383735",
+"|,	c #5E5C5D",
+"1,	c #6C6B6C",
+"2,	c #71716F",
+"3,	c #8A8984",
+"4,	c #8B8A86",
+"5,	c #767674",
+"6,	c #747373",
+"7,	c #77766F",
+"8,	c #75706B",
+"9,	c #726A65",
+"0,	c #6E6663",
+"a,	c #7B7878",
+"b,	c #7F747A",
+"c,	c #857A80",
+"d,	c #898187",
+"e,	c #4C4A4C",
+"f,	c #696768",
+"g,	c #6A6365",
+"h,	c #797273",
+"i,	c #716F6E",
+"j,	c #757373",
+"k,	c #787677",
+"l,	c #676566",
+"m,	c #3A393B",
+"n,	c #30302E",
+"o,	c #393936",
+"p,	c #3D3D3B",
+"q,	c #41413F",
+"r,	c #333332",
+"s,	c #3F3F3D",
+"t,	c #4B4B45",
+"u,	c #48483F",
+"v,	c #494942",
+"w,	c #4E4F4A",
+"x,	c #4D4E49",
+"y,	c #474844",
+"z,	c #363533",
+"A,	c #373634",
+"B,	c #3F3E3C",
+"C,	c #2C2C2A",
+"D,	c #363634",
+"E,	c #5A5955",
+"F,	c #686667",
+"G,	c #626164",
+"H,	c #6D6D6D",
+"I,	c #797976",
+"J,	c #8E8D88",
+"K,	c #8F8E89",
+"L,	c #8C8C86",
+"M,	c #7B7B74",
+"N,	c #82817A",
+"O,	c #6B6B66",
+"P,	c #646360",
+"Q,	c #6C675F",
+"R,	c #696862",
+"S,	c #6B6A6C",
+"T,	c #857A7D",
+"U,	c #61575A",
+"V,	c #6C6569",
+"W,	c #494749",
+"X,	c #6E6D6B",
+"Y,	c #6E6E6E",
+"Z,	c #7C7A7B",
+"`,	c #767071",
+" '	c #847D7D",
+".'	c #7C7A78",
+"+'	c #706F6F",
+"@'	c #6E6C6D",
+"#'	c #706E70",
+"$'	c #5D5B5F",
+"%'	c #242426",
+"&'	c #282829",
+"*'	c #2A2A2B",
+"='	c #3D3D3C",
+"-'	c #434341",
+";'	c #40403E",
+">'	c #424240",
+",'	c #363635",
+"''	c #363637",
+")'	c #2D2D2E",
+"!'	c #42423C",
+"~'	c #404039",
+"{'	c #3A3935",
+"]'	c #383636",
+"^'	c #373736",
+"/'	c #40403F",
+"('	c #2D2D30",
+"_'	c #3B3A38",
+":'	c #31302E",
+"<'	c #2E2D2B",
+"['	c #262523",
+"}'	c #454441",
+"|'	c #555451",
+"1'	c #5D5C5C",
+"2'	c #605F5F",
+"3'	c #70716D",
+"4'	c #74746E",
+"5'	c #807F7A",
+"6'	c #868580",
+"7'	c #83827E",
+"8'	c #71706D",
+"9'	c #6B6A66",
+"0'	c #484749",
+"a'	c #5B5453",
+"b'	c #5D5B5A",
+"c'	c #7E7476",
+"d'	c #6C6163",
+"e'	c #676061",
+"f'	c #5F5C5A",
+"g'	c #72706F",
+"h'	c #746F70",
+"i'	c #68676A",
+"j'	c #656467",
+"k'	c #6A686C",
+"l'	c #5D5C62",
+"m'	c #4E4C52",
+"n'	c #222123",
+"o'	c #212123",
+"p'	c #272728",
+"q'	c #393938",
+"r'	c #3C3C3B",
+"s'	c #3A3B3D",
+"t'	c #3A3A3D",
+"u'	c #323235",
+"v'	c #393839",
+"w'	c #3D3C39",
+"x'	c #3A3835",
+"y'	c #3D3A39",
+"z'	c #3D3A3A",
+"A'	c #424241",
+"B'	c #2C2C2B",
+"C'	c #343335",
+"D'	c #282726",
+"E'	c #222020",
+"F'	c #242320",
+"G'	c #3B3B38",
+"H'	c #444340",
+"I'	c #535250",
+"J'	c #6B6A69",
+"K'	c #7C7B75",
+"L'	c #7C7B73",
+"M'	c #7E7E79",
+"N'	c #767671",
+"O'	c #6D6D6B",
+"P'	c #5C5B5A",
+"Q'	c #575756",
+"R'	c #534E50",
+"S'	c #534F50",
+"T'	c #4F4D4C",
+"U'	c #655C5F",
+"V'	c #625A5D",
+"W'	c #797374",
+"X'	c #4D4A4B",
+"Y'	c #656262",
+"Z'	c #676764",
+"`'	c #716F6D",
+" )	c #7D7978",
+".)	c #7C7A7A",
+"+)	c #777579",
+"@)	c #737276",
+"#)	c #6C6B6E",
+"$)	c #5D5C60",
+"%)	c #38383B",
+"&)	c #404040",
+"*)	c #3B3C3D",
+"=)	c #37383A",
+"-)	c #2C2D30",
+";)	c #383837",
+">)	c #3C3938",
+",)	c #383634",
+"')	c #383836",
+"))	c #373735",
+"!)	c #2F2E2C",
+"~)	c #2D2C2B",
+"{)	c #302F2E",
+"])	c #363534",
+"^)	c #363531",
+"/)	c #383733",
+"()	c #40403B",
+"_)	c #51524B",
+":)	c #63645D",
+"<)	c #6B6A63",
+"[)	c #6B6A61",
+"})	c #6E6E65",
+"|)	c #60615D",
+"1)	c #5E5F5B",
+"2)	c #5F5F5E",
+"3)	c #5D5D5D",
+"4)	c #565353",
+"5)	c #4A4645",
+"6)	c #474342",
+"7)	c #393435",
+"8)	c #5B5658",
+"9)	c #757275",
+"0)	c #666568",
+"a)	c #6F6F6E",
+"b)	c #706E6C",
+"c)	c #7D7A78",
+"d)	c #7A7976",
+"e)	c #727171",
+"f)	c #818081",
+"g)	c #646464",
+"h)	c #3D3D3F",
+"i)	c #313133",
+"j)	c #323232",
+"k)	c #3F4041",
+"l)	c #404143",
+"m)	c #323335",
+"n)	c #474843",
+"o)	c #3F3F3C",
+"p)	c #393937",
+"q)	c #343432",
+"r)	c #2E2E2C",
+"s)	c #454440",
+"t)	c #44433F",
+"u)	c #403F3B",
+"v)	c #3F3E3A",
+"w)	c #3E3D39",
+"x)	c #41423E",
+"y)	c #4F4F4C",
+"z)	c #575754",
+"A)	c #545455",
+"B)	c #60605F",
+"C)	c #636362",
+"D)	c #696969",
+"E)	c #686868",
+"F)	c #4E4B4B",
+"G)	c #3B3736",
+"H)	c #3A3635",
+"I)	c #696067",
+"J)	c #4B474A",
+"K)	c #76757D",
+"L)	c #504F55",
+"M)	c #787775",
+"N)	c #797474",
+"O)	c #847F7C",
+"P)	c #808078",
+"Q)	c #898883",
+"R)	c #838381",
+"S)	c #797977",
+"T)	c #737271",
+"U)	c #5A5957",
+"V)	c #464543",
+"W)	c #464545",
+"X)	c #3E3E3E",
+"Y)	c #30312F",
+"Z)	c #393A35",
+"`)	c #3F403B",
+" !	c #3B3B3C",
+".!	c #2F3032",
+"+!	c #38393A",
+"@!	c #3F3E3D",
+"#!	c #3A3A38",
+"$!	c #323230",
+"%!	c #2B2B2A",
+"&!	c #3E3F3B",
+"*!	c #3C3D38",
+"=!	c #42433E",
+"-!	c #525256",
+";!	c #555459",
+">!	c #6F6C6B",
+",!	c #605F63",
+"'!	c #787875",
+")!	c #636262",
+"!!	c #3C3A3D",
+"~!	c #1B191A",
+"{!	c #4F4D4B",
+"]!	c #7C7984",
+"^!	c #545159",
+"/!	c #6A686B",
+"(!	c #6C6C6F",
+"_!	c #7B7A76",
+":!	c #7A7672",
+"<!	c #918C87",
+"[!	c #9A9A91",
+"}!	c #7C7B77",
+"|!	c #7F7F7C",
+"1!	c #514F50",
+"2!	c #454344",
+"3!	c #4D4C4D",
+"4!	c #323331",
+"5!	c #2F2F2D",
+"6!	c #2A2A2C",
+"7!	c #252629",
+"8!	c #333231",
+"9!	c #5F5F60",
+"0!	c #706D6C",
+"a!	c #717071",
+"b!	c #5E5D61",
+"c!	c #6E6E6B",
+"d!	c #3D3B3B",
+"e!	c #646264",
+"f!	c #7F7E88",
+"g!	c #77737C",
+"h!	c #3E3C3C",
+"i!	c #82807B",
+"j!	c #88847E",
+"k!	c #9A978F",
+"l!	c #878780",
+"m!	c #787672",
+"n!	c #6D6E6C",
+"o!	c #666365",
+"p!	c #5F5956",
+"q!	c #4E4E51",
+"r!	c #464548",
+"s!	c #434244",
+"t!	c #353536",
+"u!	c #323233",
+"v!	c #353537",
+"w!	c #434243",
+"x!	c #424344",
+"y!	c #3C3D3D",
+"z!	c #3E3E3F",
+"A!	c #3F3F40",
+"B!	c #2A2B2C",
+"C!	c #282929",
+"D!	c #2B2A29",
+"E!	c #2E2E2D",
+"F!	c #373635",
+"G!	c #3A3939",
+"H!	c #383631",
+"I!	c #2F2E2A",
+"J!	c #373839",
+"K!	c #393A3A",
+"L!	c #373433",
+"M!	c #3D3F3F",
+"N!	c #39393B",
+"O!	c #333434",
+"P!	c #3A3836",
+"Q!	c #696869",
+"R!	c #707272",
+"S!	c #636562",
+"T!	c #656060",
+"U!	c #5D5554",
+"V!	c #757672",
+"W!	c #3F3E3F",
+"X!	c #636161",
+"Y!	c #69686A",
+"Z!	c #67666D",
+"`!	c #726A68",
+" ~	c #6A6260",
+".~	c #6F6A6C",
+"+~	c #403F42",
+"@~	c #75716E",
+"#~	c #8D8986",
+"$~	c #8F8C86",
+"%~	c #94948C",
+"&~	c #898B8D",
+"*~	c #595B5C",
+"=~	c #725B40",
+"-~	c #B88F59",
+";~	c #A58852",
+">~	c #5D5544",
+",~	c #323239",
+"'~	c #3B3535",
+")~	c #353032",
+"!~	c #373835",
+"~~	c #36373C",
+"{~	c #3B3B40",
+"]~	c #4D4B4E",
+"^~	c #424247",
+"/~	c #39393E",
+"(~	c #3D3D41",
+"_~	c #3E3E3D",
+":~	c #3F4343",
+"<~	c #3E4140",
+"[~	c #37312A",
+"}~	c #453830",
+"|~	c #463D30",
+"1~	c #50423B",
+"2~	c #6C5C43",
+"3~	c #483B2B",
+"4~	c #353334",
+"5~	c #362D2F",
+"6~	c #473E2E",
+"7~	c #534835",
+"8~	c #6F5F48",
+"9~	c #796E4B",
+"0~	c #746C42",
+"a~	c #4B4744",
+"b~	c #5A5B5B",
+"c~	c #565654",
+"d~	c #524334",
+"e~	c #7F6A59",
+"f~	c #94785D",
+"g~	c #CCB685",
+"h~	c #827867",
+"i~	c #716D70",
+"j~	c #575657",
+"k~	c #6A666A",
+"l~	c #636066",
+"m~	c #6E6E76",
+"n~	c #706662",
+"o~	c #6F6561",
+"p~	c #6E6664",
+"q~	c #423F40",
+"r~	c #888482",
+"s~	c #95928C",
+"t~	c #706B5E",
+"u~	c #817158",
+"v~	c #B09A72",
+"w~	c #9F8C79",
+"x~	c #535454",
+"y~	c #7A6A4B",
+"z~	c #A3834D",
+"A~	c #3C3A33",
+"B~	c #745F48",
+"C~	c #836C42",
+"D~	c #4D433B",
+"E~	c #3C3D3F",
+"F~	c #464546",
+"G~	c #444243",
+"H~	c #424143",
+"I~	c #484845",
+"J~	c #4D5153",
+"K~	c #3F3627",
+"L~	c #6D5B4B",
+"M~	c #544932",
+"N~	c #3A3C38",
+"O~	c #2E3035",
+"P~	c #34312A",
+"Q~	c #897549",
+"R~	c #37332E",
+"S~	c #343133",
+"T~	c #696143",
+"U~	c #5E5141",
+"V~	c #5E5747",
+"W~	c #343231",
+"X~	c #3E3834",
+"Y~	c #86744E",
+"Z~	c #635947",
+"`~	c #3D3B3A",
+" {	c #695545",
+".{	c #7C7168",
+"+{	c #4A484A",
+"@{	c #63615A",
+"#{	c #6E726D",
+"${	c #707271",
+"%{	c #5A595A",
+"&{	c #5B5557",
+"*{	c #6D686C",
+"={	c #656369",
+"-{	c #857972",
+";{	c #887A7A",
+">{	c #847A78",
+",{	c #6A6B6D",
+"'{	c #6C6A64",
+"){	c #B59B6E",
+"!{	c #A59D89",
+"~{	c #B19773",
+"{{	c #4E4D47",
+"]{	c #615C5E",
+"^{	c #464844",
+"/{	c #936940",
+"({	c #443E3A",
+"_{	c #352F30",
+":{	c #725938",
+"<{	c #4A3E2A",
+"[{	c #584731",
+"}{	c #6C593B",
+"|{	c #383434",
+"1{	c #444343",
+"2{	c #36312D",
+"3{	c #3F352E",
+"4{	c #4A3D30",
+"5{	c #69543C",
+"6{	c #484949",
+"7{	c #49402E",
+"8{	c #504542",
+"9{	c #3C3C38",
+"0{	c #554938",
+"a{	c #8A7B56",
+"b{	c #4B4A41",
+"c{	c #454646",
+"d{	c #534E45",
+"e{	c #363539",
+"f{	c #8B7648",
+"g{	c #4F4A43",
+"h{	c #332A24",
+"i{	c #8B7A58",
+"j{	c #333533",
+"k{	c #3F3B3A",
+"l{	c #675E39",
+"m{	c #716E51",
+"n{	c #373B3A",
+"o{	c #3E413F",
+"p{	c #7D693D",
+"q{	c #635B4C",
+"r{	c #514531",
+"s{	c #75695B",
+"t{	c #444441",
+"u{	c #645D60",
+"v{	c #797B79",
+"w{	c #7A7B79",
+"x{	c #7C7A76",
+"y{	c #7A7A79",
+"z{	c #423C3D",
+"A{	c #474340",
+"B{	c #4F4B4D",
+"C{	c #8E8178",
+"D{	c #81736E",
+"E{	c #877D7B",
+"F{	c #535553",
+"G{	c #757779",
+"H{	c #868683",
+"I{	c #746449",
+"J{	c #B69368",
+"K{	c #585456",
+"L{	c #8A7B67",
+"M{	c #545150",
+"N{	c #494847",
+"O{	c #98774A",
+"P{	c #504C40",
+"Q{	c #323030",
+"R{	c #967B4D",
+"S{	c #494441",
+"T{	c #2F2F36",
+"U{	c #64502F",
+"V{	c #6F5C46",
+"W{	c #232624",
+"X{	c #5E4730",
+"Y{	c #53504E",
+"Z{	c #221E1A",
+"`{	c #7A5D41",
+" ]	c #393531",
+".]	c #2B2B2F",
+"+]	c #72532D",
+"@]	c #55503D",
+"#]	c #483A2E",
+"$]	c #6C6156",
+"%]	c #827349",
+"&]	c #5F5742",
+"*]	c #4A4B45",
+"=]	c #332719",
+"-]	c #958755",
+";]	c #4E4436",
+">]	c #534830",
+",]	c #6A5E4F",
+"']	c #47443D",
+")]	c #3B3C3B",
+"!]	c #464141",
+"~]	c #2B2622",
+"{]	c #857953",
+"]]	c #666356",
+"^]	c #544937",
+"/]	c #8F8261",
+"(]	c #423937",
+"_]	c #998257",
+":]	c #4C4844",
+"<]	c #57575C",
+"[]	c #584D43",
+"}]	c #605647",
+"|]	c #705742",
+"1]	c #6B6667",
+"2]	c #676062",
+"3]	c #625E5A",
+"4]	c #797575",
+"5]	c #776B63",
+"6]	c #776C62",
+"7]	c #897F7D",
+"8]	c #747577",
+"9]	c #959794",
+"0]	c #AB9E6A",
+"a]	c #B6A977",
+"b]	c #61645E",
+"c]	c #747573",
+"d]	c #484238",
+"e]	c #AD9766",
+"f]	c #4A4547",
+"g]	c #3F3E2E",
+"h]	c #8E734E",
+"i]	c #2A2B29",
+"j]	c #444046",
+"k]	c #5D4C34",
+"l]	c #725D45",
+"m]	c #3F301D",
+"n]	c #856747",
+"o]	c #373025",
+"p]	c #524433",
+"q]	c #5D544F",
+"r]	c #2A2B2E",
+"s]	c #2C261D",
+"t]	c #957E4C",
+"u]	c #67563E",
+"v]	c #5C584B",
+"w]	c #34363C",
+"x]	c #7C6E41",
+"y]	c #766C53",
+"z]	c #353430",
+"A]	c #7F6A49",
+"B]	c #9C9663",
+"C]	c #766953",
+"D]	c #9F955E",
+"E]	c #3D3E3F",
+"F]	c #424545",
+"G]	c #63655F",
+"H]	c #4F4F4B",
+"I]	c #665E4F",
+"J]	c #AFA578",
+"K]	c #73644F",
+"L]	c #706A58",
+"M]	c #767266",
+"N]	c #45443E",
+"O]	c #4A4034",
+"P]	c #A68D5D",
+"Q]	c #524F4F",
+"R]	c #4A4945",
+"S]	c #92806F",
+"T]	c #8B705F",
+"U]	c #D0A46A",
+"V]	c #615D56",
+"W]	c #80837E",
+"X]	c #787876",
+"Y]	c #6E6D6A",
+"Z]	c #6E6969",
+"`]	c #615D5A",
+" ^	c #7A7676",
+".^	c #908682",
+"+^	c #716763",
+"@^	c #6A5F5D",
+"#^	c #545151",
+"$^	c #7C7C7E",
+"%^	c #909196",
+"&^	c #96926F",
+"*^	c #DAD4A6",
+"=^	c #5E5A57",
+"-^	c #767973",
+";^	c #5E5B62",
+">^	c #736041",
+",^	c #75695C",
+"'^	c #474846",
+")^	c #5B4F39",
+"!^	c #907D5C",
+"~^	c #373A38",
+"{^	c #525459",
+"]^	c #6E563B",
+"^^	c #665C56",
+"/^	c #554331",
+"(^	c #876E4F",
+"_^	c #625B5B",
+":^	c #4E4B4C",
+"<^	c #211E1F",
+"[^	c #2E2720",
+"}^	c #44423D",
+"|^	c #745D32",
+"1^	c #87714B",
+"2^	c #454340",
+"3^	c #565357",
+"4^	c #78683D",
+"5^	c #84765B",
+"6^	c #3C3535",
+"7^	c #6C5C4C",
+"8^	c #888164",
+"9^	c #908E6D",
+"0^	c #3E3E3C",
+"a^	c #918A63",
+"b^	c #6E6045",
+"c^	c #60605D",
+"d^	c #666461",
+"e^	c #50524D",
+"f^	c #494137",
+"g^	c #B2A785",
+"h^	c #474840",
+"i^	c #3D3B40",
+"j^	c #52544F",
+"k^	c #52483E",
+"l^	c #B6A975",
+"m^	c #564D4E",
+"n^	c #5C5C5B",
+"o^	c #655740",
+"p^	c #D7BB7F",
+"q^	c #4E4E4C",
+"r^	c #878785",
+"s^	c #727270",
+"t^	c #75736E",
+"u^	c #76726D",
+"v^	c #6D6967",
+"w^	c #7C7879",
+"x^	c #8E8483",
+"y^	c #867C7B",
+"z^	c #7C7171",
+"A^	c #595656",
+"B^	c #828385",
+"C^	c #8E9092",
+"D^	c #787977",
+"E^	c #E5E5C7",
+"F^	c #9E9877",
+"G^	c #5A5449",
+"H^	c #504945",
+"I^	c #574E38",
+"J^	c #776860",
+"K^	c #3A3A3E",
+"L^	c #5D5E5A",
+"M^	c #695033",
+"N^	c #7F6147",
+"O^	c #403838",
+"P^	c #3B352A",
+"Q^	c #756254",
+"R^	c #45433E",
+"S^	c #514134",
+"T^	c #896C48",
+"U^	c #44392B",
+"V^	c #463928",
+"W^	c #645745",
+"X^	c #56504A",
+"Y^	c #282529",
+"Z^	c #AD996B",
+"`^	c #7A6C5C",
+" /	c #3A383D",
+"./	c #595D5A",
+"+/	c #594A35",
+"@/	c #A09066",
+"#/	c #6E5D47",
+"$/	c #7C7469",
+"%/	c #888764",
+"&/	c #7D7C6C",
+"*/	c #51534E",
+"=/	c #575346",
+"-/	c #B8AA81",
+";/	c #817A60",
+">/	c #706960",
+",/	c #5A4D3A",
+"'/	c #A1936E",
+")/	c #656563",
+"!/	c #5E5D5B",
+"~/	c #585451",
+"{/	c #938A5C",
+"]/	c #A59C5E",
+"^/	c #5E5042",
+"//	c #77624C",
+"(/	c #D0BF80",
+"_/	c #AEA27C",
+":/	c #4F5352",
+"</	c #888887",
+"[/	c #70706D",
+"}/	c #74706B",
+"|/	c #7F7B7A",
+"1/	c #6F6B6C",
+"2/	c #7E7475",
+"3/	c #857A7C",
+"4/	c #5F5C5D",
+"5/	c #828484",
+"6/	c #8C8E92",
+"7/	c #818386",
+"8/	c #737167",
+"9/	c #B2AA85",
+"0/	c #A59171",
+"a/	c #7F7265",
+"b/	c #635D5A",
+"c/	c #3D3E3B",
+"d/	c #575354",
+"e/	c #5A575C",
+"f/	c #8A6C3E",
+"g/	c #7E6C55",
+"h/	c #615B49",
+"i/	c #5D5850",
+"j/	c #3C3A3A",
+"k/	c #505150",
+"l/	c #414346",
+"m/	c #534A42",
+"n/	c #655849",
+"o/	c #564B45",
+"p/	c #3F3838",
+"q/	c #3A3A3C",
+"r/	c #434142",
+"s/	c #5E5545",
+"t/	c #3D3838",
+"u/	c #626462",
+"v/	c #52504C",
+"w/	c #84806B",
+"x/	c #5F5A4C",
+"y/	c #3D3C3B",
+"z/	c #AEA87C",
+"A/	c #615E57",
+"B/	c #5D5D58",
+"C/	c #6B6C62",
+"D/	c #86836D",
+"E/	c #4E4A46",
+"F/	c #5E5D62",
+"G/	c #715E41",
+"H/	c #8D816B",
+"I/	c #464643",
+"J/	c #646460",
+"K/	c #686864",
+"L/	c #696866",
+"M/	c #676665",
+"N/	c #545350",
+"O/	c #817A68",
+"P/	c #96886A",
+"Q/	c #6C6454",
+"R/	c #D5CD8A",
+"S/	c #6F675C",
+"T/	c #686A6A",
+"U/	c #696764",
+"V/	c #676362",
+"W/	c #7E7976",
+"X/	c #756F70",
+"Y/	c #827877",
+"Z/	c #867C7D",
+"`/	c #5F5D5D",
+" (	c #828282",
+".(	c #727372",
+"+(	c #5B5B5E",
+"@(	c #57585A",
+"#(	c #706C6F",
+"$(	c #514D4E",
+"%(	c #C3AE78",
+"&(	c #484645",
+"*(	c #6A6A6A",
+"=(	c #525156",
+"-(	c #4C4A4D",
+";(	c #4C4B4A",
+">(	c #4F4E4C",
+",(	c #504D4E",
+"'(	c #545253",
+")(	c #717070",
+"!(	c #676765",
+"~(	c #61605B",
+"{(	c #6D685B",
+"](	c #454240",
+"^(	c #6A6965",
+"/(	c #777672",
+"((	c #73726E",
+"_(	c #61615A",
+":(	c #5D5C5A",
+"<(	c #656765",
+"[(	c #5F5751",
+"}(	c #504B43",
+"|(	c #555452",
+"1(	c #6D6C6A",
+"2(	c #6F6E6C",
+"3(	c #6C6B69",
+"4(	c #706F6D",
+"5(	c #5F5D5B",
+"6(	c #544F48",
+"7(	c #B5A27F",
+"8(	c #71706E",
+"9(	c #676664",
+"0(	c #5D5958",
+"a(	c #66605F",
+"b(	c #605A5A",
+"c(	c #766C6B",
+"d(	c #6D6362",
+"e(	c #766F6F",
+"f(	c #7E7B7A",
+"g(	c #8B8883",
+"h(	c #807D7A",
+"i(	c #737174",
+"j(	c #656667",
+"k(	c #717172",
+"l(	c #7B7B79",
+"m(	c #6D6A6C",
+"n(	c #5C564D",
+"o(	c #C2A969",
+"p(	c #5C5C5D",
+"q(	c #616061",
+"r(	c #6B6B6A",
+"s(	c #656566",
+"t(	c #5F5D61",
+"u(	c #6B696C",
+"v(	c #615F60",
+"w(	c #555254",
+"x(	c #595658",
+"y(	c #6B6B6E",
+"z(	c #6B6768",
+"A(	c #716D6B",
+"B(	c #656562",
+"C(	c #747270",
+"D(	c #81807A",
+"E(	c #777771",
+"F(	c #7A7A73",
+"G(	c #6D6D6C",
+"H(	c #636062",
+"I(	c #797776",
+"J(	c #787674",
+"K(	c #706E6D",
+"L(	c #696361",
+"M(	c #575C5C",
+"N(	c #525354",
+"O(	c #67696A",
+"P(	c #7B7978",
+"Q(	c #63615F",
+"R(	c #65615E",
+"S(	c #736C6F",
+"T(	c #645F5E",
+"U(	c #847772",
+"V(	c #5F5855",
+"W(	c #72716A",
+"X(	c #847F7A",
+"Y(	c #79746C",
+"Z(	c #767774",
+"`(	c #676669",
+" _	c #646662",
+"._	c #6B696B",
+"+_	c #6A665F",
+"@_	c #5F5955",
+"#_	c #97845A",
+"$_	c #3D3A3F",
+"%_	c #5B585C",
+"&_	c #676565",
+"*_	c #71716D",
+"=_	c #4E4B4E",
+"-_	c #5D5A5D",
+";_	c #626162",
+">_	c #5F5E5E",
+",_	c #656464",
+"'_	c #585657",
+")_	c #616163",
+"!_	c #4C4D4F",
+"~_	c #525355",
+"{_	c #676160",
+"]_	c #706B6A",
+"^_	c #626061",
+"/_	c #716F6F",
+"(_	c #676666",
+"__	c #74716F",
+":_	c #6B6966",
+"<_	c #7E7D7C",
+"[_	c #4D4A47",
+"}_	c #463F3A",
+"|_	c #57514F",
+"1_	c #867D75",
+"2_	c #90837B",
+"3_	c #211D1D",
+"4_	c #464442",
+"5_	c #595955",
+"6_	c #6F6E6B",
+"7_	c #6D6460",
+"8_	c #5A5958",
+"9_	c #605F5E",
+"0_	c #615F5E",
+"a_	c #605B59",
+"b_	c #67625F",
+"c_	c #6A6362",
+"d_	c #525353",
+"e_	c #4B484A",
+"f_	c #4C4C50",
+"g_	c #64605E",
+"h_	c #5E5859",
+"i_	c #635D60",
+"j_	c #625D61",
+"k_	c #605D5E",
+"l_	c #5B5858",
+"m_	c #5E5D5D",
+"n_	c #615F5F",
+"o_	c #5C5859",
+"p_	c #5D595A",
+"q_	c #6E6F6E",
+"r_	c #5D5B59",
+"s_	c #5B5957",
+"t_	c #585655",
+"u_	c #474548",
+"v_	c #645F60",
+"w_	c #554D4D",
+"x_	c #646160",
+"y_	c #5B5958",
+"z_	c #3B393A",
+"A_	c #403E3F",
+"B_	c #474744",
+"C_	c #5D5C5B",
+"D_	c #6B6A6A",
+"E_	c #696663",
+"F_	c #65625F",
+"G_	c #656360",
+"H_	c #64625F",
+"I_	c #65635F",
+"J_	c #64635D",
+"K_	c #57534F",
+"L_	c #6F6967",
+"M_	c #6C6866",
+"N_	c #6E6A6A",
+"O_	c #867A70",
+"P_	c #83756D",
+"Q_	c #2F2A27",
+"R_	c #655A4E",
+"S_	c #7D7671",
+"T_	c #7E7A74",
+"U_	c #74685E",
+"V_	c #786A69",
+"W_	c #716868",
+"X_	c #827A78",
+"Y_	c #7E7370",
+"Z_	c #756966",
+"`_	c #7A6E6A",
+" :	c #96888B",
+".:	c #7A7675",
+"+:	c #6C6463",
+"@:	c #7A7275",
+"#:	c #69656C",
+"$:	c #5D595B",
+"%:	c #666061",
+"&:	c #595154",
+"*:	c #797274",
+"=:	c #655B5A",
+"-:	c #5E5555",
+";:	c #574C4C",
+">:	c #6C5F60",
+",:	c #7B6F6F",
+"':	c #6E6262",
+"):	c #6F6565",
+"!:	c #645A5A",
+"~:	c #888383",
+"{:	c #6F6A68",
+"]:	c #57514A",
+"^:	c #4B443E",
+"/:	c #5F5854",
+"(:	c #545051",
+"_:	c #6B6668",
+"::	c #504A4B",
+"<:	c #72696B",
+"[:	c #757171",
+"}:	c #7A7776",
+"|:	c #63605F",
+"1:	c #302C2B",
+"2:	c #312B2A",
+"3:	c #504949",
+"4:	c #69605D",
+"5:	c #635B57",
+"6:	c #635B55",
+"7:	c #554E43",
+"8:	c #585049",
+"9:	c #605750",
+"0:	c #5E564E",
+"a:	c #676059",
+"b:	c #615950",
+"c:	c #655952",
+"d:	c #6D6560",
+"e:	c #5F5753",
+"f:	c #817874",
+"g:	c #756D69",
+"h:	c #584E48",
+"i:	c #68625B",
+"j:	c #837C78",
+"k:	c #86807D",
+"l:	c #696158",
+"m:	c #706152",
+"n:	c #60574D",
+"o:	c #8A7F72",
+"p:	c #7D7366",
+"q:	c #847E76",
+"r:	c #6E645C",
+"s:	c #70615D",
+"t:	c #756662",
+"u:	c #796A63",
+"v:	c #80736B",
+"w:	c #776C66",
+"x:	c #746A67",
+"y:	c #827575",
+"z:	c #847C7A",
+"A:	c #7B7273",
+"B:	c #7D757C",
+"C:	c #90888A",
+"D:	c #9B999E",
+"E:	c #868689",
+"F:	c #645E60",
+"G:	c #675F60",
+"H:	c #6E6568",
+"I:	c #565050",
+"J:	c #5C5254",
+"K:	c #63595B",
+"L:	c #706769",
+"M:	c #776E70",
+"N:	c #736C6D",
+"O:	c #585150",
+"P:	c #4A4140",
+"Q:	c #5D5756",
+"R:	c #5D5853",
+"S:	c #56524A",
+"T:	c #3F3A35",
+"U:	c #5E5956",
+"V:	c #6A6665",
+"W:	c #595558",
+"X:	c #615D60",
+"Y:	c #625C5E",
+"Z:	c #555051",
+"`:	c #524F4E",
+" <	c #726B69",
+".<	c #635957",
+"+<	c #726866",
+"@<	c #6C635E",
+"#<	c #776E69",
+"$<	c #6E645F",
+"%<	c #706659",
+"&<	c #685F4E",
+"*<	c #665A49",
+"=<	c #51473C",
+"-<	c #776F69",
+";<	c #686057",
+"><	c #5F5551",
+",<	c #5D5652",
+"'<	c #6D6260",
+")<	c #857B7A",
+"!<	c #5D544E",
+"~<	c #645C54",
+"{<	c #746863",
+"]<	c #78706B",
+"^<	c #6E6256",
+"/<	c #6D5F56",
+"(<	c #433C3A",
+"_<	c #635C58",
+":<	c #645550",
+"<<	c #736662",
+"[<	c #635858",
+"}<	c #5E534E",
+"|<	c #7C6E6A",
+"1<	c #786B63",
+"2<	c #7D706B",
+"3<	c #6E6361",
+"4<	c #736969",
+"5<	c #7F7272",
+"6<	c #716A68",
+"7<	c #827874",
+"8<	c #8B8486",
+"9<	c #817D7E",
+"0<	c #897E7B",
+"a<	c #7B7877",
+"b<	c #857D7E",
+"c<	c #7D7779",
+"d<	c #7A777F",
+"e<	c #7E7D83",
+"f<	c #656167",
+"g<	c #6A656B",
+"h<	c #726F73",
+"i<	c #87878D",
+"j<	c #817C7D",
+"k<	c #584F4D",
+"l<	c #6F6968",
+"m<	c #716C67",
+"n<	c #7D7971",
+"o<	c #544F4C",
+"p<	c #6B6766",
+"q<	c #5E595C",
+"r<	c #544F52",
+"s<	c #545555",
+"t<	c #201C1D",
+"u<	c #706869",
+"v<	c #7F7778",
+"w<	c #736A68",
+"x<	c #6D645E",
+"y<	c #726963",
+"z<	c #6F6862",
+"A<	c #847E78",
+"B<	c #7B756F",
+"C<	c #7E7B6F",
+"D<	c #737067",
+"E<	c #655E5B",
+"F<	c #5D5953",
+"G<	c #636057",
+"H<	c #5D5650",
+"I<	c #54514D",
+"J<	c #3C3837",
+"K<	c #756F6F",
+"L<	c #7A7171",
+"M<	c #6E6463",
+"N<	c #6B6161",
+"O<	c #6F5F5D",
+"P<	c #5A4F47",
+". + @ # $ % & * = - ; > , ' ) ! ~ { ] ^ / ( _ : < [ } | 1 2 3 4 5 6 7 8 9 0 a b c d e f g h i j k l m n o p q r s t u v w x y z ",
+"c A B C D E F G H I J K L M N O P Q R S T U V W X Y Z `  ...+.@.~ #.$.%.&.*.=.-.;.>.,.'.).!.~.{.].^./.(._.:.<.[.}.|.1.2.3.4.5.6.",
+"7.8.9.0.&.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H.I.J.K.L.M.N.O.P.Q.R.S.T.U.V.W.X.Y.Z.`. +.+++@+#+$+",
+"%+&+*+=+-+;+>+,+'+;+)+!+~+{+]+^+/+(+_+:+<+[+}+|+~+1+2+3+4+5+6+7+8+9+5+5+0+;+,+>+,+a+;+5+,+b+c+>+;+d+e+<+f+g+h+h+i+b+j+k+l+m+n+o+",
+"p+q+r+s+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+u+v+v+t+t+t+t+t+t+t+u+u+u+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+w+x+y+z+",
+"A+B+C+)+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+D+E+F+G+",
+"H+I+J+d+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+K+L+M+N+",
+"O+P+Q+R+u+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+S+T+U+V+",
+"W+X+Y+Z+v+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+`+ @.@+@",
+"@@#@$@)+v+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+`+%@&@*@",
+"=@-@;@,+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+>@,@'@)@",
+"!@~@{@;+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+u+]@^@/@(@",
+"_@:@<@/+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+[@}@|@1@",
+"2@3@4@7+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+5@6@7@8@",
+"9@0@a@b@t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+5@c@d@e@",
+"f@g@h@/+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+i@j@k@l@m@n@o@p@t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+q@r@s@t@",
+"u@v@w@x@t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+y@z@g.A@B@C@D@E@F@G@t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+u+H@I@J@K@",
+"L@M@N@0+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+O@P@Q@R@S@T@U@V@W@X@Y@Z@`@ #.#+#t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+@###$#%#",
+"&#*#=#9+u+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+-#;#>#,#'#)#!#~#{#]#^#/#(#_#:#<#[#}#|#1#t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+2#3#4#5#",
+"6#7#8#9#v+t+t+t+t+t+t+t+t+t+t+t+t+t+t+O@0#a#b#c#d#e#f#g#h#i#j#k#l#m#n#o#p#q#r#s#t#u#v#w#x#t+t+t+t+t+t+t+t+t+t+t+t+t+t+t+y#z#A#B#",
+"p.C#D#9#t+t+t+t+t+t+t+t+t+t+t+t+E#F#G#H#I#J#K#L#M#N#O#P#Q#R#S#T#U#V#W#X#Y#Z#`# $'#.$+$@$#$$$%$t+t+t+t+t+t+t+t+t+t+t+t+t+&$*$=$-$",
+";$>$,$'$t+t+t+t+t+t+t+t+t+t+t+)$!$~${$]$<#^$/$($_$:$<$[$~#R#}$|$1$2$3$4$5$6$7$8$9$0$.$a$b$b$c$d$O@t+t+t+t+t+t+t+t+t+t+t+e$f$g$h$",
+"i$j$k$l$t+t+t+t+t+t+t+t+t+t+t+m$n$o$p$q$r$s$t$u$v$w$x$y$z$A$B$C$D$E$F$G$H$I$J$a$D@r$K$L$M$N$O$P$Q$t+t+t+t+t+t+t+t+t+t+t+R$S$T$U$",
+"V$W$X$j@t+t+t+t+t+t+t+O@x#Y$Z$Q$`$ %o$n$.%+%@%#%$%%%&%*%=%-%;%>%+$,%'%)%5$W@!%~%b$O$`@{%]%^%O$/%(%%$_%x#O@t+t+t+t+t+t+t+:%<%[%}%",
+"|%1%2%3%4%t+O@+#Z$5%6%7%8%8%9%0%c$a%b%}#c%d%e%f%g%h%i%|@j%X@k%l%x$m%n%o%p%q%r%@%f%a%s%{%t%u%u% %v%w%w#x%Q$6%1#%$Y$x#t+t+y%z%A%B%",
+"C%Y+D%R+E%F%Q$G%x%H%I%I%(%J%K%L%M%N%O%P%Q%v#R%S%T%U%V%W%X%s$Y%Z%`% &.&+&@&#&$&%&&& #*&=&-&N%;&>&|$v%,&,&'&)&H%!&x%Q$j+~&{&]&^&/&",
+"(&_&:&<&[&)&}&,&v%|&1&2&3&4&5&6&7&8&9&!&0&a&b&c&d&&&e&f&g&h&i&g%7&u#P$j&k&{%l&m&m&n&m&o&p&q&p&r&s&t&u&u&v&v&1&|&w&'&I%x&y&z&A&B&",
+"C&D&E&F&G&H&I&1&J&K&L&L&M&N&O&P&%&l&L&Q&R&q@d&S&8&T&n&e&U&V&W&&&r&X&Y&u%Z& #N%b&o&`&c&`&C$p&c&m& *.*+*@*+*#*$*$*%*I&1&&***=*-*;*",
+">*,*.#'*)*!*@*~*{*]*^*/*(*_*r&:*/%<*%*[*[*[*}*|*-&Y&X&1*1*X&2*l&3*}* #+* #Y&3*|*}*4*C$5*p&q&6*l&7*^%O$T%8&8*9*d&0*a*N&b*c*d*e*f*",
+"g*h*i*j*k*l*$*m*n*o*p*q*r*f%U%2* %u%s*t*t*$*>&u*>&Z&3*u%X&s%^%>&v*s*(%G%w&3*>&w*=&4*`&C$x*y*y*z*c&A*`$B*U%e&U%_*C*n&c&D*E*F*G*H*",
+"I*J*K*L*k#M*N*O*P*Q*R*S*#&T*U*V*W*X*>&3*=&^%Y& #>&Y&>&u% #X&v#3*Y*K&Z*`* =$*=&v#3*a*o&a*.=+=@=#=#=$%$=@$u$%=#$#$&=B**=S&==-=;=>=",
+",='=)=!=~={=I$]=^=/=(=n%_=:=<=[=}=|=>&Z&w*w*3*=&}*3*v#>&Y&X&1=2=3=4=5=6=7=$*v#3*}*v*>&.*=&$*8=9=0=h&a=b=l%/=$%c=^$d=^$e=f=g=h=i=",
+"j=6$k=l=m=n=~$o=p=q=r=s=t=u=[#v=w=x=a%t%Y&X& #}*w*w*v# #w*w*>&3=y=3=z=A=u&s*>&@*|&#*B=s*=&$*C=D=E=F=G=b=H=I=J=$%K=L=M=N=H#O=P=Q=",
+"R=S=T=U=V=W=b$b=v$+$^$T@X=Y=Z=`= -9*Y&.-+-]%@-^%v#B=v* #>&3*<*#-$-]%%-Y*B=>&-&>@&-*-b&=---9&7*;->-,-[$'-)-!-L=Y=~-)-{-]-^-/-(-_-",
+":-<-[-}*`@}-J=}-|-1-'#x$H=K=/=:*r&4*u*2-a#y=3-w*s* #{%Y&>& #4-y=5-^%Z&Y&=&Y&6*6-7-8-9-0-a-b-c-d-e-,-($f-g-h-/=<#i-j-k-l-m-n-o-p-",
+"q-r-s-t-u-v-u$w-|-x-y-8$z-|-+$6*A-B-1=D*C-D-+-:*@*}*1*a*w*a%E-#-F-Y&w*>&u*<*1*G-N&7-H-I-J-K-L-M--&c=N-O-P-Q-f*/#k%R-j-S-4-T-U-V-",
+"W-X-Y-Z-`- ;.;+;@;#;$;%;&;*;=;-;;;>;,;3-%-u*w*';);!;~;C$-&{;];#-^;/;t%t%X&u%(;_;:;<;[;};H%0%u&w*:*|;N#1;;%U*l%2;{-3;4;c=5;6;7;8;",
+"k%9;0;a;b;c;%=d;e;f;g;h;i;j;$=k;l;m;n;,;o;}*.*p;q;r;s;t;u;v;w;-&U#-&v*w*>&3*x;y;z;u&A;B;x%0%}&.*B=k-C@0$C;`$D;9$E;F;<#G;H;I;J;K;",
+"L;M;N;O;&=o+P;Q;Y=R;S;T;U;V;W;X;Y;Z;`; >.>>&3*+>@>#>$>%>&>*>=>->;>>>y;,>'>)>!>~>{>]>^>/>v%,&(>_>'>:><>{-A*[>}>|>1>2>3>4>5>6>7>8>",
+"9>0>a>b>c>d>e>f>s=E;g>o$h>i>j>k>l>m>n>o>1$v##*p>!>q>r>s>w%t*t>/*u>v>w>x>x>y>z>w>A>B>C>D>L&[*E>F>G>H>)%I>A*P;J>K><#X=L>M>N>O>P>Q>",
+"~@R>S>T>U>V>W>X>Y>Z>`> ,q$.,c>+,@,#,$,%,&,@*@**,=,-,;,>,)&,,',),!,~>~,{,],^,/,(,_,:,<,t>[,9&},E>)>|,p%1, %2,3,4,5,6,F;7,8,9,0,a,",
+"b,c,d,e,{=`@f,g,h,i,j,k,p%l,9 m,$$$$n,o,&,p,4=q,$>r,[*v&G&s,&,=,t,u,v,v>w,x,z>y,t*z,A,B,D&)*C,D,m@E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,",
+"T,U,V,W,X,Y,Z,`, '.'+'@'#'$'T=O&q@%'&'*'='-';'-'>','%*7*'')'t&U=!'~'{']'^'/'A=%*('_'A,:'<'['r>5=}'|'1'2'3'4'5'6'7'8'9'T>0'a'b'v*",
+"c'd'e'v;f'j#g'`.h'#'i'j'k'l'm'z*P&#,n'o'p','q'F@^'r'+*L&s't'u'v'w'x'y'z'='A'B'1&C'G#j*)*D'E'F'G'H'I'J'i,K'L'M'N'O'P'Q't%;&R'S'T'",
+"U'V'W'X'Y'Z'`' ).)+)@)#)#)$)>;%)5&4&d$7=)&,&2&L&u&$*.*&)*)=)-)};;)L*>),)')4=n,5=))},!)~){)])^)/)()_):)<)[)})|)1)2)3)K$j&k&4)5)6)",
+"7)8)9)u%0)a)b)c)d)e)f)/=H=g)X&h)s&i)u&2&j)2&7=3&t&t*/'&)k)l)m)9&n)o)')p)p)q)r)G&5=!)},'=s)t)u)v)w)x)y)z)A)T%3)B)C)D)u#E)c$F)G)H)",
+"I)J)K)L),-M)N)O)P)Q)R)S)T)U)V)W)J&L&%*1&%*X)%*9&j)Y)Z)`) !a&.!+!E>@!v&Q%#!$!%!d$^'q,q,&!*!=!r'v&$,I&+*-!;!$'>!U>,!k-'!)!9 !!~!{!",
+"]!^!/!n&(!_!:!<![!}!|!S).&1!2!3!J&9&7*9&7*u&$*v*&)&)4!5!6!6!7!i)!)8!j)[*$!$!F@j)#*M-4=z=;)r,t&9&7*9&}*l&9!l-0!a!b!0$c!l,9 d!2'e!",
+"f!g!h!5;W@i!j!k!l!m!n!o!p!q!r!s!s,y*t!u!v!x*w!x!y!z!|*A!t!|$B!C!D!E!G&F!)>-'G!H!I!J!K!L!M!N!O!P!E>J&1*/;Q!R!S!T!U!#+V!H,W!X!Y!Z!",
+"`! ~.~+~@~#~$~%~&~*~=~-~;~>~,~'~)~!~<;<;~~{~]~^~/~(~h)N&<;B=s*X)K&_~:~<~[~}~|~1~2~3~4~a*5~6~7~8~9~0~a~b~c~d~e~f~g~h~i~g-j~k~l~m~",
+"n~o~p~q~a$r~s~t~u~v~w~x~y~z~A~B~C~D~H&;'E~+,]~F~G~H~4*h)u*u*Y&3*s*I~J~K~L~M~N~O~P~Q~R~S~T~U~V~W~X~Y~Z~`~ {.{+{$-@{Q'#{${%{&{*{={",
+"-{;{>{3=,{4$'{){!{~{{{]{^{/{({_{:{<{[{}{|{1{2{3{4{5{H 6{7{8{9{0{a{b{c{d{e{f{g{>;h{i{j{k{b>l{m{n{o{p{q{r{s{t{u{v{w{p%x{y{{=z{A{B{",
+"C{D{E{F{G{H{I{J{K{L{M{D@N{O{P{Q{R{S{T{U{V{W{X{Y{Z{`{ ].]+]@]#]$]%]&] !*]=]-];]>],]'])]!]~]{]]]-,^]/](]_]:]<][]}]|]1]y{5,.%2]3]4]",
+"5]6]7]@-8]9]0]a]X*b]V#c]d]e]f]g]h]i]j]k]l]m]n]o]p]q]r]s]t]u]v]w]x]y]6*z]A]B]C]D]E]F]G]H]I]J]K]L]M]N]O]P]Q]R]S]T]U]V]W]X]Y]Z]`] ^",
+".^+^@^#^$^%^&^*^=^c]-^;^>^,^'^)^!^~^{^]^^^/^(^_^:^<^[^}^|^1^2^3^4^5^6^7^8^9^0^a^b^c^d^e^f^g^h^r'i^j^k^l^m^2*n^o^p^q^r^s^t^u^v^w^",
+"x^y^z^A^B^C^D^E^F^G^H^I^J^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^`^ /./+/@/#/$/%/&/*/=/-/;/>/N%,/'/0^E-)/!/~/{/]/^///(/_/:/</[/t^}/|/1/",
+"2/2/3/4/5/6/7/8/9/0/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/f&u/v/w/x/y/z/A/B/|)C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/1-t=U/V/W/X/",
+"Y/Y/Z/`/x$|- (.(+(@(q^y=s^#($(%(K-&(=&s%*(B-=(-(;(>(}#,=,('()(O'!(3)c~~({(](^(/(((_(:(<([(}(|(1(2(3(4(g%n$5(6(7(`;0$.$8(9(0(a(b(",
+"c(d(e(s*f(g(h(i(j(k(Z=l(r=m(n(o(q,p(q(P$r(s(i&t(u(v(w(x(y(z(A(.%K>c!B(+%j&+&C(D(E(X,F(G(<(H(I(J(K(X>2(1,g'L(M(N(O(8$9$P(Q(R(S(T(",
+"7]U(V(]*W(X(Y(Z(C*`( _B(._+_@_#_$_j~%_&_*_C)U%<*K$j~=_-_;_>_,_|,'_)_e&!_~_P.{_]_{$X,.%l-}#^_/_@'l-l-(_S*)(J'__:_<_2>X=Y,[_}_|_+(",
+"1_2_3_4_5_6_7_V/8_9_0_a_b_c_d_e_f_g_h_i_j_k_l_.>m_n_o_p_m_2',_q_O$r_s_t_u_v_w_U+9(x_Q(y_z_A_B_y)z)P'C_+&D_9_E_F_G_H_I_J_K_L_M_N_",
+"O_P_Q_R_S_T_U_V_W_X_Y_Z_`_ :.:+:@:#:$:%:&:*:=:-:;:>:,:':):!:e(~:{:]:^:/:(:_:::<:[:}:|:1:2:3:4:5:6:7:8:9:0:a:b:c:d:e:f:g:h:i:j:k:",
+"l:m:n:o:p:q:r:s:t:u:v:w:x:y:z:A:B:C:D:E:F:G:H:I:J:K:L:M:N:O:P:Q:R:S:T:U:V:W:X:Y:Z:`:|: <.<+<@<#<$<%<&<*<=<-<;<><,<Y+'<)<!<~<{<]<",
+"^</<(<_<:<<<[<}<|<1<2<3<4<5<6<7<8<9<0<a<b<c<d<e<f<g<3^h<i<j<k<l<m<n<R:o<p<q<r<s<t<u<v<w<x<y<z<A<B<C<D<E<F<G<H<I<J<K<L<M<N<M<O<P<"};
Binary file images/d20_logo.gif has changed
Binary file images/d4.gif has changed
Binary file images/d8.gif has changed
Binary file images/dash.png has changed
Binary file images/defaultmap.png has changed
Binary file images/delete_filter.gif has changed
Binary file images/dice.bmp has changed
Binary file images/die.gif has changed
Binary file images/divider.png has changed
Binary file images/draw.gif has changed
Binary file images/drugs.gif has changed
Binary file images/earth.gif has changed
Binary file images/edit_filter.gif has changed
Binary file images/fetching.png has changed
Binary file images/flask.gif has changed
Binary file images/flask.ico has changed
Binary file images/fogoff.png has changed
Binary file images/fogon.png has changed
Binary file images/folder.gif has changed
Binary file images/form.png has changed
Binary file images/frame.bmp has changed
Binary file images/gear.gif has changed
Binary file images/goblin.gif has changed
Binary file images/goblin.ico has changed
Binary file images/grenade.gif has changed
Binary file images/grid.gif has changed
Binary file images/grid.ico has changed
Binary file images/gun1.gif has changed
Binary file images/gun2.gif has changed
Binary file images/help.gif has changed
Binary file images/hidefog.png has changed
Binary file images/html.gif has changed
Binary file images/html.ico has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/images/icons.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,52 @@
+<icons>
+<icon name='book' file='book.gif' />
+<icon name='folder' file='folder.gif' />
+<icon name='die' file='die.gif' />
+<icon name='skull' file='skull.gif' />
+<icon name='labtop' file='labtop.gif' />
+<icon name='flask' file='flask.gif' />
+<icon name='goblin' file='goblin.gif' />
+<icon name='note' file='note.gif' />
+<icon name='bullet' file='bullet.gif' />
+<icon name='ccmap' file='ccmap.gif' />
+<icon name='8ball' file='8ball.gif' />
+<icon name='car' file='car.gif' />
+<icon name='chess' file='chess.gif' />
+<icon name='compass' file='compass.gif' />
+<icon name='cyborg' file='cyborg.gif' />
+<icon name='drugs' file='drugs.gif' />
+<icon name='gear' file='gear.gif' />
+<icon name='grenade' file='grenade.gif' />
+<icon name='gun1' file='gun1.gif' />
+<icon name='gun2' file='gun2.gif' />
+<icon name='knight' file='knight.gif' />
+<icon name='money' file='money.gif' />
+<icon name='ninja' file='ninja.gif' />
+<icon name='orc' file='orc.gif' />
+<icon name='help' file='help.gif' />
+<icon name='oriental' file='oriental.gif' />
+<icon name='player' file='player.gif' />
+<icon name='questionhead' file='questionhead.gif' />
+<icon name='r2d2' file='r2d2.gif' />
+<icon name='rome' file='rome.gif' />
+<icon name='shades' file='shades.gif' />
+<icon name='spears' file='spears.gif' />
+<icon name='startrek' file='startrek.gif' />
+<icon name='sword' file='sword.gif' />
+<icon name='tank1' file='tank1.gif' />
+<icon name='tank2' file='tank2.gif' />
+<icon name='thief' file='thief.gif' />
+<icon name='tiefighter' file='tiefighter.gif' />
+<icon name='wizard1' file='wizard1.gif' />
+<icon name='d20' file='d20.gif' />
+<icon name='d10' file='d10.gif' />
+<icon name='d8' file='d8.gif' />
+<icon name='d4' file='d4.gif' />
+<icon name='grid' file='grid.gif' />
+<icon name='html' file='html.gif' />
+<icon name='browser' file='browser.gif' />
+<icon name='image' file='img.gif' />
+<icon name='tabber' file='tabber.png' />
+<icon name='divider' file='divider.png' />
+<icon name='form' file='form.png' />
+</icons>
Binary file images/img.gif has changed
Binary file images/install.gif has changed
Binary file images/italic.gif has changed
Binary file images/knight.gif has changed
Binary file images/labtop.gif has changed
Binary file images/money.gif has changed
Binary file images/mouse.gif has changed
Binary file images/move.gif has changed
Binary file images/ninja.gif has changed
Binary file images/noplayer.gif has changed
Binary file images/note.gif has changed
Binary file images/note.ico has changed
Binary file images/open.bmp has changed
Binary file images/orc.gif has changed
Binary file images/oriental.gif has changed
Binary file images/pin.gif has changed
Binary file images/planet.gif has changed
Binary file images/player-whisper.gif has changed
Binary file images/player.gif has changed
Binary file images/python55.gif has changed
Binary file images/questionhead.gif has changed
Binary file images/r2d2.gif has changed
Binary file images/rectangle.png has changed
Binary file images/rome.gif has changed
Binary file images/save.bmp has changed
Binary file images/sflogo.png has changed
Binary file images/shades.gif has changed
Binary file images/showfog.png has changed
Binary file images/skull.gif has changed
Binary file images/skull_16.gif has changed
Binary file images/smsword2.gif has changed
Binary file images/spears.gif has changed
Binary file images/splash.gif has changed
Binary file images/splash.jpg has changed
Binary file images/splash1.jpg has changed
Binary file images/splash13.jpg has changed
Binary file images/splitwin.bmp has changed
Binary file images/startrek.gif has changed
Binary file images/sword.gif has changed
Binary file images/tab.bmp has changed
Binary file images/tab_close.png has changed
Binary file images/tab_on.png has changed
Binary file images/tabber.png has changed
Binary file images/tank1.gif has changed
Binary file images/tank2.gif has changed
Binary file images/tape.gif has changed
Binary file images/text.png has changed
Binary file images/thief.gif has changed
Binary file images/tiefighter.gif has changed
Binary file images/underlined.gif has changed
Binary file images/wizard1.gif has changed
Binary file images/wxPyButton.png has changed
Binary file images/wxWinButton.png has changed
Binary file images/zoom_in.gif has changed
Binary file images/zoom_out.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/license.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,339 @@
+            GNU GENERAL PUBLIC LICENSE
+               Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+            GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+             END OF TERMS AND CONDITIONS
+
+        How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/myfiles/webfiles/Textures/Copyright Notice.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,12 @@
+The following images are Copyright Paul Bourke and are used with permission, All Rights Reserved.
+
+grass11.jpg
+sandwave.jpg
+steelpops11.jpg
+versa_anigre.jpg
+water06.jpg
+water02.jpg
+
+The following image is Copyright StarFields and used with Fair Usage in mind, All Rights Reserved.
+
+grass-natural.jpg
Binary file myfiles/webfiles/Textures/grass-natural.jpg has changed
Binary file myfiles/webfiles/Textures/grass11.jpg has changed
Binary file myfiles/webfiles/Textures/sandwave.jpg has changed
Binary file myfiles/webfiles/Textures/steelpops11.jpg has changed
Binary file myfiles/webfiles/Textures/versa_anigre.jpg has changed
Binary file myfiles/webfiles/Textures/water06.jpg has changed
Binary file myfiles/webfiles/Textures/water20.jpg has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2 @@
+__all__ = ['chat', 'dieroller', 'gametree','mapper', 'networking', 'tools',
+    'main', 'minidom', 'orpg_version', 'orpg_windows', 'orpg_xml', 'pulldom' ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/chat/chat_msg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,91 @@
+# 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: chat_msg.py
+# Author: Ted Berg
+# Maintainer:
+# Version:
+#   $Id: chat_msg.py,v 1.15 2006/11/04 21:24:19 digitalxero Exp $
+#
+# Description: Contains class definitions for manipulating <chat/> messages
+#
+#
+
+__version__ = "$Id: chat_msg.py,v 1.15 2006/11/04 21:24:19 digitalxero Exp $"
+
+import orpg.orpg_xml
+from chat_version import CHAT_VERSION
+
+CHAT_MESSAGE = 1
+WHISPER_MESSAGE = 2
+EMOTE_MESSAGE = 3
+INFO_MESSAGE = 4
+SYSTEM_MESSAGE = 5
+WHISPER_EMOTE_MESSAGE = 6
+
+class chat_msg:
+    def __init__(self,xml_text="<chat type=\"1\" version=\""+CHAT_VERSION+"\" alias=\"\" ></chat>"):
+        self.chat_dom = None
+        self.takexml(xml_text)
+
+    def __del__(self):
+        if self.chat_dom:
+            self.chat_dom.unlink()
+
+    def toxml(self):
+        return orpg.orpg_xml.toxml(self.chat_dom)
+
+    def takexml(self,xml_text):
+        xml_dom = orpg.orpg_xml.parseXml(xml_text)
+        node_list = xml_dom.getElementsByTagName("chat")
+        if len(node_list) < 1:
+            print "Warning: no <chat/> elements found in DOM."
+        else:
+            if len(node_list) > 1:
+                print "Found more than one instance of <" + self.tagname + "/>.  Taking first one"
+            self.takedom(node_list[0])
+
+    def takedom(self,xml_dom):
+        if self.chat_dom:
+            self.text_node = None
+            self.chat_dom.unlink()
+        self.chat_dom = xml_dom
+        self.text_node = orpg.orpg_xml.safe_get_text_node(self.chat_dom)
+
+    def set_text(self,text):
+        text = orpg.orpg_xml.strip_text(text)
+        self.text_node._set_nodeValue(text)
+
+    def set_type(self,type):
+        self.chat_dom.setAttribute("type",str(type))
+
+    def get_type(self):
+        return int(self.chat_dom.getAttribute("type"))
+
+    def set_alias(self,alias):
+        self.chat_dom.setAttribute("alias",alias)
+
+    def get_alias(self):
+        return self.chat_dom.getAttribute("alias")
+
+    def get_text(self):
+        return self.text_node._get_nodeValue()
+
+    def get_version(self):
+        return self.chat_dom.getAttribute("version")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/chat/chat_util.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,163 @@
+# utility function; see Post() in chatwnd.py
+
+import re
+import string
+
+#============================================
+# simple_html_repair(string)
+#
+# Crude html/xml parser/verifier.
+# Catches many mistyped and/or malformed
+# html tags and prevents them from causing
+# issues with the chat display (see chatwnd.py)
+# DOES NOT catch misused but properly formated
+#    html like <script> or <li> which are known
+#    to cause issues with the chat display
+#
+# Created 04-25-2005 by Snowdog
+#=============================================
+def simple_html_repair(string):
+    "Returns string with extra > symbols to isolate badly formated HTML"
+    #walk though string checking positions of < and > tags.
+    first_instance = string.find('<')
+    if first_instance == -1: return string #no html, bail out.
+
+    #strip string of an instances of ">>" and "<<" recursively
+    #while (string.find(">>") != -1):string = string.replace(">>",">")
+    while (string.find("<<") != -1):string = string.replace("<<","<")
+
+    last_start = first_instance
+    in_tag_flag = 1
+    a = first_instance + 1
+    while a < len(string):
+        if string[a] == '<':
+            if in_tag_flag == 1:
+                #attempt to figure out best place to put missing >
+                #search from last_start to current position
+                at_front = 1
+                for best_pos in range(last_start,a):
+                    if (str(string[best_pos]).isspace())and (at_front == 0):
+                        break
+                    else:
+                        at_front = 0
+                        best_pos = best_pos + 1
+                a = best_pos
+                string = string[:a]+">"+string[a:]
+                in_tag_flag = 0
+                #jump back up one character to catch the last > and reset the in_tag_flag
+                a = a - 1
+            else:
+                in_tag_flag = 1
+                last_start = a
+
+        if string[a] == '>':
+            last_start = a #found a closing tag, move start of scan block up.
+            in_tag_flag = 0
+        if (a >= (len(string)-1))and(in_tag_flag == 1):
+            #at end of string and need a closing tag marker
+            string = string +">"
+        a = a+1
+
+    #strip string of an instances of "<>"
+    string = string.replace("<>","")
+
+    #sanity check. Count the < and > characters, if there arn't enough > chars
+    #tack them on the end to avoid open-tag conditions
+    diff = string.count('<') - string.count('>')
+    if diff > 0:
+        for d in range(1,diff):
+            string = string+">"
+
+    return string
+
+def strip_unicode(txt):
+    for i in xrange(len(txt)):
+        if txt[i] not in string.printable:
+            try:
+                txt = txt.replace(txt[i], '&#' + str(ord(txt[i])) + ';')
+            except:
+                txt = txt.replace(txt[i], '{?}')
+    return txt
+
+#================================================
+# strip_script_tags(string)
+#
+# removes all script tags (start and end)
+# 04-26-2005 Snowdog
+#================================================
+def strip_script_tags(string):
+    #kill the <script> issue
+    p = re.compile( '<(\s*)(/*)[Ss][Cc][Rr][Ii][Pp][Tt](.*?)>')
+    string =  p.sub( "<!-- script tag removed //-->", string)
+    return string
+
+#================================================
+# strip_li_tags(string)
+#
+# removes all li tags (start and end)
+# 05-13-2005
+#================================================
+def strip_li_tags(string):
+    #kill the <li> issue
+    string = re.sub( r'<(\s*)[Ll][Ii](.*?)>', r'<b><font color="#000000" size=+1>*</font></b>    ', string)
+    string = re.sub( r'<(/*)[Ll][Ii](.*?)>', r'<br />', string)
+    return string
+
+#================================================
+# strip_body_tags(string)
+#
+# removes all body tags (start and end) from messages
+# should not break the setting of custom background colors
+#   through legitimate means such as the OpenRPG settings.
+# 07-27-2005 by mDuo13
+#================================================
+def strip_body_tags(string):
+    bodytag_regex = re.compile(r"""<\/?body.*?>""", re.I)
+    string = re.sub(bodytag_regex, "", string)
+    return string
+
+#================================================
+# strip_misalignment_tags(string)
+#
+# removes the alignment aspect of <p> tags, since
+# simply closing one doesn't actually fix the text
+# alignment. (I'm assuming this is a bug in wxWindows'
+# html parser.)
+# However, closing <center> tags does
+# return the text to its normal alignment, so this
+# algorithm simply closes them, allowing them to be
+# used legitimately without causing much annoyance.
+# 07-27-2005 mDuo13
+#================================================
+def strip_misalignment_tags(string):
+    alignment_regex = re.compile(r"""<p([^>]*?)align\s*=\s*('.*?'|".*?"|[^\s>]*)(.*?)>""", re.I)
+    string = re.sub(alignment_regex, "<p\\1\\3>", string)
+
+    center_regex = re.compile(r"""<center.*?>""", re.I)
+    endcenter_regex = re.compile(r"""</center.*?>""", re.I)
+    num_centertags = center_regex.findall(string)
+    num_endcentertags = endcenter_regex.findall(string)
+    if num_centertags > num_endcentertags:
+        missing_tags = len(num_centertags) - len(num_endcentertags)
+        string = string + missing_tags*"</center>"#yes, you can do this.
+    return string
+
+#================================================
+# strip_img_tags(string)
+#
+# removes all img tags (start and end)
+# 05-13-2005
+# redone 07-11-2005 by mDuo13
+#================================================
+def strip_img_tags(string):
+    #This is a Settings definable feature, Allowing users to enable or disable image display to fix the client crash due to large img posted to chat.
+    #p = re.sub( r'<(\s*)(/*)[Ii][Mm][Gg][ ][Ss][Rr][Cc][=](.*?)>', r'<!-- img tag removed //--> <a href=\3>\3</a>', string)
+
+    #this regex is substantially more powerful than the one above
+    img_tag_regex = re.compile(r"""<img.*?src\s*?=\s*('.*?'|".*?"|[^\s>]*).*?>""", re.I)
+    #this is what replaces the regex match. the \\1 refers to the URL from the previous string
+    img_repl_str = "<a href=\\1>[img]</a>"
+
+    #replaces all instances of images in the string with links
+    p = re.sub(img_tag_regex, img_repl_str, string)
+    return p
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/chat/chat_version.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,3 @@
+## this file hold the chat version ##
+
+CHAT_VERSION = "1.0"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/chat/chatwnd.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2140 @@
+# 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: chatutils.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: chatwnd.py,v 1.177 2007/12/07 20:39:48 digitalxero Exp $
+#
+# Description: This file contains some of the basic definitions for the chat
+# utilities in the orpg project.
+#
+# History
+# 2002-01-20 HeroMan
+#   + Added 4 dialog items on toolbar in support of Alias Library Functionallity
+#   + Shrunk the text view button to an image
+# 2005-04-25 Snowdog
+#   + Added simple_html_repair() to post() to fix malformed html in the chat window
+#   + Added strip_script_tags() to post() to remove crash point. See chat_util.py
+# 2005-04-25 Snowdog
+#   + Added simple_html_repair() to post() to fix malformed html in the chat window
+#
+
+__version__ = "$Id: chatwnd.py,v 1.177 2007/12/07 20:39:48 digitalxero Exp $"
+
+
+##
+## Module Loading
+##
+from orpg.orpg_windows import *
+from orpg.player_list import WG_LIST
+import orpg.dirpath
+import orpg.tools.rgbhex
+import orpg.tools.inputValidator
+from orpg.tools.metamenus import MenuEx
+from orpg.orpgCore import open_rpg
+import webbrowser
+from string import *
+from orpg.orpg_version import VERSION
+import commands
+import chat_msg
+import time
+import orpg.tools.predTextCtrl
+from orpg.networking.mplay_client import MPLAY_CONNECTED  # needed to only send typing/not_typing messages while connected
+import os
+import time
+import re
+import sys
+import cStringIO # for reading inline imagedata as a stream
+from HTMLParser import HTMLParser
+import chat_util
+import traceback
+NEWCHAT = False
+try:
+    import wx.webview
+    NEWCHAT = True
+except:
+    pass
+NEWCHAT = False
+
+# Global parser for stripping HTML tags:
+# The 'tag stripping' is implicit, because this parser echoes every
+# type of html data *except* the tags.
+class HTMLStripper(HTMLParser):
+    def __init__(self):
+        self.accum = ""
+        self.special_tags = ['hr', 'br', 'img']
+    def handle_data(self, data):  # quote cdata literally
+        self.accum += data
+    def handle_entityref(self, name): # entities must be preserved exactly
+        self.accum += "&" + name + ";"
+    def handle_starttag(self, tag, attrs):
+        if tag in self.special_tags:
+            self.accum += '<' + tag
+            for attrib in attrs:
+                self.accum += ' ' + attrib[0] + '="' + attrib[1] + '"'
+            self.accum += '>'
+    def handle_charref(self, name):  # charrefs too
+        self.accum += "&#" + name + ";"
+htmlstripper = HTMLStripper()
+
+# utility function;  see Post().
+def strip_html(string):
+    "Return string tripped of html tags."
+    htmlstripper.reset()
+    htmlstripper.accum = ""
+    htmlstripper.feed(string)
+    htmlstripper.close()
+    return htmlstripper.accum
+
+def log( settings, text ):
+    filename = settings.get_setting('GameLogPrefix')
+    if filename > '' and filename[0] != commands.ANTI_LOG_CHAR:
+        filename = filename + time.strftime( '-%Y-%m-%d.html', time.localtime( time.time() ) )
+        #filename = time.strftime( filename, time.localtime( time.time() ) )
+        timestamp = time.ctime(time.time())
+        header = '[%s] : ' % ( timestamp );
+        if settings.get_setting('TimeStampGameLog') != '1':
+            header = ''
+        try:
+            f = open( orpg.dirpath.dir_struct["user"] + filename, 'a' )
+            f.write( '%s%s<br />\n' % ( header, text ) )
+            f.close()
+        except:
+            print "could not open " + orpg.dirpath.dir_struct["user"] + filename + ", ignoring..."
+            pass
+
+# This class displayes the chat information in html?
+#
+# Defines:
+#   __init__(self, parent, id)
+#   OnLinkClicked(self, linkinfo)
+#   CalculateAllFonts(self, defaultsize)
+#   SetDefaultFontAndSize(self, fontname)
+#
+class chat_html_window(wx.html.HtmlWindow):
+    """ a wxHTMLwindow that will load links  """
+    # initialization subroutine
+    #
+    # !self : instance of self
+    # !parent :
+    # !id :
+    def __init__(self, parent, id):
+        wx.html.HtmlWindow.__init__(self, parent, id, style=wx.SUNKEN_BORDER | wx.html.HW_SCROLLBAR_AUTO|wx.NO_FULL_REPAINT_ON_RESIZE)
+        self.parent = parent
+        self.build_menu()
+        self.Bind(wx.EVT_LEFT_UP, self.LeftUp)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.onPopup)
+        if "gtk2" in wx.PlatformInfo:
+            self.SetStandardFonts()
+    # def __init__ - end
+
+    def onPopup(self, evt):
+        self.PopupMenu(self.menu)
+
+    def LeftUp(self, event):
+        event.Skip()
+        wx.CallAfter(self.parent.set_chat_text_focus, None)
+
+    def build_menu(self):
+        self.menu = wx.Menu()
+        item = wx.MenuItem(self.menu, wx.ID_ANY, "Copy", "Copy")
+        self.Bind(wx.EVT_MENU, self.OnM_EditCopy, item)
+        self.menu.AppendItem(item)
+
+    def OnM_EditCopy(self, evt):
+        wx.TheClipboard.Open()
+        wx.TheClipboard.Clear()
+        wx.TheClipboard.SetData(wx.TextDataObject(self.SelectionToText()))
+        wx.TheClipboard.Close()
+
+    def scroll_down(self):
+        maxrange = self.GetScrollRange(wx.VERTICAL)
+        pagesize = self.GetScrollPageSize(wx.VERTICAL)
+        self.Scroll(-1, maxrange-pagesize)
+
+    def mouse_wheel(self, event):
+        amt = event.GetWheelRotation()
+        units = amt/(-(event.GetWheelDelta()))
+        self.ScrollLines(units*3)
+
+    def Header(self):
+        return '<html><body bgcolor="' + self.parent.bgcolor + '" text="' + self.parent.textcolor + '">'
+
+    def StripHeader(self):
+        return self.GetPageSource().replace(self.Header(), '')
+
+    def GetPageSource(self):
+        return self.GetParser().GetSource()
+
+    # This subroutine fires up the webbrowser when a link is clicked.
+    #
+    # !self : instance of self
+    # !linkinfo : instance of a class that contains the link information
+    def OnLinkClicked(self, linkinfo):
+        href = linkinfo.GetHref()
+        wb = webbrowser.get()
+        wb.open(href)
+    # def OnLinkClicked - end
+
+    def CalculateAllFonts(self, defaultsize):
+        return [int(defaultsize * 0.4),
+                int(defaultsize * 0.7),
+                int(defaultsize),
+                int(defaultsize * 1.3),
+                int(defaultsize * 1.7),
+                int(defaultsize * 2),
+                int(defaultsize * 2.5)]
+
+    def SetDefaultFontAndSize(self, fontname, fontsize):
+        """Set 'fontname' to the default chat font.
+           Returns current font settings in a (fontname, fontsize) tuple."""
+        self.SetFonts(fontname, "", self.CalculateAllFonts(int(fontsize)))
+        return (self.GetFont().GetFaceName(), self.GetFont().GetPointSize())
+
+# class chat_html_window - end
+if NEWCHAT:
+    class ChatHtmlWindow(wx.webview.WebView):
+        def __init__(self, parent, id):
+            wx.webview.WebView.__init__(self, parent, id)
+
+            self.parent = parent
+
+            self.__font = wx.Font(10, wx.FONTFAMILY_ROMAN, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, faceName='Ariel')
+
+            self.build_menu()
+            self.Bind(wx.EVT_LEFT_UP, self.LeftUp)
+            self.Bind(wx.EVT_RIGHT_DOWN, self.onPopup)
+            self.Bind(wx.webview.EVT_WEBVIEW_BEFORE_LOAD, self.OnLinkClicked)
+
+        #Wrapers so I dont have to add special Code
+        def SetPage(self, htmlstring):
+            self.SetPageSource(htmlstring)
+
+        def AppendToPage(self, htmlstring):
+            self.SetPageSource(self.GetPageSource() + htmlstring)
+
+        def GetFont(self):
+            return self.__font
+
+        def CalculateAllFonts(self, defaultsize):
+            return
+
+        def SetDefaultFontAndSize(self, fontname, fontsize):
+            self.__font = wx.Font(int(fontsize), wx.FONTFAMILY_ROMAN, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, faceName=fontname)
+            try:
+                self.SetPageSource(self.Header() + self.StripHeader())
+            except Exception, e:
+                print e
+            return (self.GetFont().GetFaceName(), self.GetFont().GetPointSize())
+
+        #Events
+        def OnLinkClicked(self, linkinfo):
+            href = linkinfo.GetHref()
+            wb = webbrowser.get()
+            wb.open(href)
+
+        def onPopup(self, evt):
+            self.PopupMenu(self.menu)
+
+        def LeftUp(self, event):
+            event.Skip()
+            wx.CallAfter(self.parent.set_chat_text_focus, None)
+
+        def OnM_EditCopy(self, evt):
+            self.Copy()
+
+        #Cutom Methods
+        def Header(self):
+            return "<html><head><style>body {font-size: " + str(self.GetFont().GetPointSize()) + "px;font-family: " + self.GetFont().GetFaceName() + ";color: " + self.parent.textcolor + ";background-color: " + self.parent.bgcolor + ";margin: 0;padding: 0 0;height: 100%;}</style></head><body>"
+
+        def StripHeader(self):
+            tmp = self.GetPageSource().split('<BODY>')
+            if tmp[-1].find('<body>') > -1:
+                tmp = tmp[-1].split('<body>')
+
+            return tmp[-1]
+
+        def build_menu(self):
+            self.menu = wx.Menu()
+            item = wx.MenuItem(self.menu, wx.ID_ANY, "Copy", "Copy")
+            self.Bind(wx.EVT_MENU, self.OnM_EditCopy, item)
+            self.menu.AppendItem(item)
+
+        def scroll_down(self):
+            maxrange = self.GetScrollRange(wx.VERTICAL)
+            pagesize = self.GetScrollPageSize(wx.VERTICAL)
+            self.Scroll(-1, maxrange-pagesize)
+
+        def mouse_wheel(self, event):
+            amt = event.GetWheelRotation()
+            units = amt/(-(event.GetWheelDelta()))
+            self.ScrollLines(units*3)
+    chat_html_window = ChatHtmlWindow
+
+#########################
+#chat frame window
+#########################
+# These are kinda global...and static..and should be located somewhere else
+# then the middle of a file between two classes.
+
+###################
+# Tab Types
+###################
+MAIN_TAB = wx.NewId()
+WHISPER_TAB = wx.NewId()
+GROUP_TAB = wx.NewId()
+NULL_TAB = wx.NewId()
+
+# This class defines the tabbed 'notebook' that holds multiple chatpanels.
+# It's the widget attached to the main application frame.
+#
+# Inherits:  wxNotebook
+#
+# Defines:
+#   create_private_tab(self, playerid)
+#   get_tab_index(self, chatpanel)
+#   destroy_private_tab(self, chatpanel)
+#   OnPageChanged(self, event)
+#   set_default_font(self, font, fontsize)
+
+class chat_notebook(orpgTabberWnd):
+    def __init__(self, parent, size):
+        self.log = open_rpg.get_component("log")
+        self.log.log("Enter chat_notebook", ORPG_DEBUG)
+        orpgTabberWnd.__init__(self, parent, True, size=size, style=FNB.FNB_DROPDOWN_TABS_LIST|FNB.FNB_NO_NAV_BUTTONS|FNB.FNB_MOUSE_MIDDLE_CLOSES_TABS)
+        self.settings = open_rpg.get_component("settings")
+        self.whisper_tabs = []
+        self.group_tabs = []
+        self.null_tabs = []
+        self.il = wx.ImageList(16, 16)
+        bmp = wx.Bitmap(orpg.dirpath.dir_struct["icon"]+'player.gif')
+        self.il.Add(bmp)
+        bmp = wx.Bitmap(orpg.dirpath.dir_struct["icon"]+'clear.gif')
+        self.il.Add(bmp)
+        self.SetImageList(self.il)
+        # Create "main" chatpanel tab, undeletable, connected to 'public' room.
+        self.MainChatPanel = chat_panel(self, -1, MAIN_TAB, 'all')
+        self.AddPage(self.MainChatPanel, "Main Room")
+        self.SetPageImage(0, 1)
+        self.chat_timer = wx.Timer(self, wx.NewId())
+        self.Bind(wx.EVT_TIMER, self.MainChatPanel.typingTimerFunc)
+        self.chat_timer.Start(1000)
+        # Hook up event handler for flipping tabs
+        self.Bind(FNB.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.onPageChanged)
+        self.Bind(FNB.EVT_FLATNOTEBOOK_PAGE_CHANGING, self.onPageChanging)
+        self.Bind(FNB.EVT_FLATNOTEBOOK_PAGE_CLOSING, self.onCloseTab)
+        # html font/fontsize is global to all the notebook tabs.
+        self.font, self.fontsize =  self.MainChatPanel.chatwnd.SetDefaultFontAndSize(self.settings.get_setting('defaultfont'), self.settings.get_setting('defaultfontsize'))
+        self.GMChatPanel = None
+        if self.settings.get_setting("GMWhisperTab") == '1':
+            self.create_gm_tab()
+        self.SetSelection(0)
+        self.log.log("Exit chat_notebook", ORPG_DEBUG)
+
+    def get_tab_index(self, chatpanel):
+        "Return the index of a chatpanel in the wxNotebook."
+        self.log.log("Enter chat_notebook->get_tab_index(self, chatpanel)", ORPG_DEBUG)
+
+        for i in xrange(self.GetPageCount()):
+            if (self.GetPage(i) == chatpanel):
+                self.log.log("Exit chat_notebook->get_tab_index(self, chatpanel)", ORPG_DEBUG)
+                return i
+
+    def create_gm_tab(self):
+        self.log.log("Enter chat_notebook->create_gm_tab(self)", ORPG_DEBUG)
+        if self.GMChatPanel == None:
+            self.GMChatPanel = chat_panel(self, -1, MAIN_TAB, 'gm')
+            self.AddPage(self.GMChatPanel, "GM", False)
+            self.SetPageImage(self.GetPageCount()-1, 1)
+            self.GMChatPanel.chatwnd.SetDefaultFontAndSize(self.font, self.fontsize)
+        self.log.log("Exit chat_notebook->create_gm_tab(self)", ORPG_DEBUG)
+
+    def create_whisper_tab(self, playerid):
+        "Add a new chatpanel directly connected to integer 'playerid' via whispering."
+        self.log.log("Enter chat_notebook->create_whisper_tab(self," + str(playerid) +")", ORPG_DEBUG)
+        private_tab = chat_panel(self, -1, WHISPER_TAB, playerid)
+        playername = strip_html(self.MainChatPanel.session.get_player_by_player_id(playerid)[0])
+        self.AddPage(private_tab, playername, False)
+        private_tab.chatwnd.SetDefaultFontAndSize(self.font, self.fontsize)
+        self.whisper_tabs.append(private_tab)
+        self.newMsg(self.GetPageCount()-1)
+        self.AliasLib = open_rpg.get_component('alias')
+        wx.CallAfter(self.AliasLib.RefreshAliases)
+        self.log.log("Exit chat_notebook->create_whisper_tab(self, playerid)", ORPG_DEBUG)
+        return private_tab
+
+    def create_group_tab(self, group_name):
+        "Add a new chatpanel directly connected to integer 'playerid' via whispering."
+        self.log.log("Enter chat_notebook->create_group_tab(self, group_name)", ORPG_DEBUG)
+        private_tab = chat_panel(self, -1, GROUP_TAB, group_name)
+        self.AddPage(private_tab, group_name, False)
+        private_tab.chatwnd.SetDefaultFontAndSize(self.font, self.fontsize)
+        self.group_tabs.append(private_tab)
+        self.newMsg(self.GetPageCount()-1)
+        self.AliasLib = open_rpg.get_component('alias')
+        wx.CallAfter(self.AliasLib.RefreshAliases)
+        self.log.log("Exit chat_notebook->create_group_tab(self, group_name)", ORPG_DEBUG)
+        return private_tab
+
+    def create_null_tab(self, tab_name):
+        "Add a new chatpanel directly connected to integer 'playerid' via whispering."
+        self.log.log("Enter chat_notebook->create_null_tab(self, tab_name)", ORPG_DEBUG)
+        private_tab = chat_panel(self, -1, NULL_TAB, tab_name)
+        self.AddPage(private_tab, tab_name, False)
+        private_tab.chatwnd.SetDefaultFontAndSize(self.font, self.fontsize)
+        self.null_tabs.append(private_tab)
+        self.newMsg(self.GetPageCount()-1)
+        self.AliasLib = open_rpg.get_component('alias')
+        wx.CallAfter(self.AliasLib.RefreshAliases)
+        self.log.log("Exit chat_notebook->create_null_tab(self, tab_name)", ORPG_DEBUG)
+        return private_tab
+
+    def onCloseTab(self, evt):
+        self.log.log("Enter chat_notebook->onCloseTab(self, evt)", ORPG_DEBUG)
+        try:
+            tabid = evt.GetSelection()
+        except:
+            tabid = self.GetSelection()
+
+        if self.GetPageText(tabid) == 'Main Room':
+            #send no close error to chat
+            evt.Veto()
+            return
+        if self.GetPageText(tabid) == 'GM':
+            msg = "Are You Sure You Want To Close This Page?"
+            dlg = wx.MessageDialog(self, msg, "NotebookCtrl Question",
+                                   wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
+
+            if wx.Platform != '__WXMAC__':
+                dlg.SetFont(wx.Font(8, wx.NORMAL, wx.NORMAL, wx.NORMAL, False))
+
+            if dlg.ShowModal() in [wx.ID_NO]:
+                dlg.Destroy()
+                evt.Veto()
+                return
+            dlg.Destroy()
+            self.GMChatPanel = None
+            self.settings.set_setting("GMWhisperTab", "0")
+        panel = self.GetPage(tabid)
+        if panel in self.whisper_tabs:
+            self.whisper_tabs.remove(panel)
+        elif panel in self.group_tabs:
+            self.group_tabs.remove(panel)
+        elif panel in self.null_tabs:
+            self.null_tabs.remove(panel)
+        self.log.log("Exit chat_notebook->onCloseTab(self, evt)", ORPG_DEBUG)
+
+    def newMsg(self, tabid):
+        self.log.log("Enter chat_notebook->newMsg(self, tabid)", ORPG_DEBUG)
+        if tabid != self.GetSelection():
+            self.SetPageImage(tabid, 0)
+        self.log.log("Exit chat_notebook->newMsg(self, tabid)", ORPG_DEBUG)
+
+    def onPageChanging(self, event):
+        """When private chattabs are selected, set the bitmap back to 'normal'."""
+        self.log.log("Enter chat_notebook->onPageChanging(self, event)", ORPG_DEBUG)
+        event.Skip()
+        self.log.log("Exit chat_notebook->onPageChanging(self, event)", ORPG_DEBUG)
+
+    def onPageChanged(self, event):
+        """When private chattabs are selected, set the bitmap back to 'normal'."""
+        self.log.log("Enter chat_notebook->onPageChanged(self, event)", ORPG_DEBUG)
+        selected_idx = event.GetSelection()
+        self.SetPageImage(selected_idx, 1)
+        page = self.GetPage(selected_idx)
+        #wx.CallAfter(page.set_chat_text_focus, 0)
+        event.Skip()
+        self.log.log("Exit chat_notebook->onPageChanged(self, event)", ORPG_DEBUG)
+
+# This class defines and builds the Chat Frame for OpenRPG
+#
+# Inherits: wxPanel
+#
+# Defines:
+#   __init__((self, parent, id, openrpg, sendtarget)
+#   build_ctrls(self)
+#   on_buffer_size(self,evt)
+#   set_colors(self)
+#   set_buffersize(self)
+#   set_chat_text(self,txt)
+#   OnChar(self,event)
+#   on_chat_save(self,evt)
+#   on_text_color(self,event)
+#   colorize(self, color, text)
+#   on_text_format(self,event)
+#   OnSize(self,event)
+#   scroll_down(self)
+#   InfoPost(self,s)
+#   Post(self,s="",send=False,myself=False)
+#   ParsePost(self,s,send=False,myself=False)
+#   ParseDice(self,s)
+#   ParseNodes(self,s)
+#   get_sha_checksum(self)
+#   get_color(self)
+#
+
+class chat_panel(wx.Panel):
+
+    # This is the initialization subroutine
+    #
+    # !self : instance of self
+    # !parent : parent that defines the chatframe
+    # !id :
+    # !openrpg :
+        # !sendtarget:  who gets outbound messages: either 'all' or a playerid
+    def __init__(self, parent, id, tab_type, sendtarget):
+        self.log = open_rpg.get_component("log")
+        self.log.log("Enter chat_panel", ORPG_DEBUG)
+        wx.Panel.__init__(self, parent, id)
+        self.session = open_rpg.get_component('session')
+        self.settings = open_rpg.get_component('settings')
+        self.activeplugins = open_rpg.get_component('plugins')
+        self.parent = parent
+        # who receives outbound messages, either "all" or "playerid" string
+        self.sendtarget = sendtarget
+        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
+        self.set_colors()
+        self.version = VERSION
+        self.histidx = -1
+        self.temptext = ""
+        self.history = []
+        self.storedata = []
+        #self.lasthistevt = None
+        self.parsed=0
+        #chat commands
+        self.lockscroll = False      # set the default to scrolling on.
+        self.chat_cmds = commands.chat_commands(self)
+        self.html_strip = strip_html
+        #Alias Lib stuff
+        self.defaultAliasName = 'Use Real Name'
+        self.defaultFilterName = 'No Filter'
+        self.advancedFilter = False
+        self.lastSend = 0         #  this is used to help implement the player typing indicator
+        self.lastPress = 0        #  this is used to help implement the player typing indicator
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+        self.build_ctrls()
+        #openrpg dir
+        self.root_dir = orpg.dirpath.dir_struct["home"]
+        # html font/fontsize is global to all the notebook tabs.
+        StartupFont = self.settings.get_setting("defaultfont")
+        StartupFontSize = self.settings.get_setting("defaultfontsize")
+        if(StartupFont != "") and (StartupFontSize != ""):
+            try:
+                self.set_default_font(StartupFont, int(StartupFontSize))
+            except:
+                pass
+        self.font = self.chatwnd.GetFont().GetFaceName()
+        self.fontsize = self.chatwnd.GetFont().GetPointSize()
+        self.scroll_down()
+        self.log.log("Exit chat_panel", ORPG_DEBUG)
+
+    def set_default_font(self, fontname=None, fontsize=None):
+        """Set all chatpanels to new default fontname/fontsize. Returns current font settings in a (fontname, fontsize) tuple."""
+        self.log.log("Enter chat_panel->set_default_font(self, fontname=None, fontsize=None)", ORPG_DEBUG)
+        if (fontname is not None):
+            newfont = fontname
+        else:
+            newfont = self.font
+        if (fontsize is not None):
+            newfontsize = int(fontsize)
+        else:
+            newfontsize = int(self.fontsize)
+        self.chatwnd.SetDefaultFontAndSize(newfont, newfontsize)
+        self.InfoPost("Font is now " + newfont + " point size " + `newfontsize`)
+        self.font = newfont
+        self.fontsize = newfontsize
+        self.log.log("Exit chat_panel->set_default_font(self, fontname=None, fontsize=None)", ORPG_DEBUG)
+        return (self.font, self.fontsize)
+
+    def build_menu(self):
+        self.log.log("Enter chat_panel->build_menu(self)", ORPG_DEBUG)
+        top_frame = open_rpg.get_component('frame')
+        menu = wx.Menu()
+        item = wx.MenuItem(menu, wx.ID_ANY, "&Background color", "Background color")
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_BackgroundColor, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "&Text color", "Text color")
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_TextColor, item)
+        menu.AppendItem(item)
+        menu.AppendSeparator()
+        item = wx.MenuItem(menu, wx.ID_ANY, "&Chat Focus\tCtrl-H", "Chat Focus")
+        self.setChatFocusMenu = item
+        top_frame.Bind(wx.EVT_MENU, self.set_chat_text_focus, item)
+        menu.AppendItem(item)
+        menu.AppendSeparator()
+        item = wx.MenuItem(menu, wx.ID_ANY, "Toggle &Scroll Lock", "Toggle Scroll Lock")
+        top_frame.Bind(wx.EVT_MENU, self.lock_scroll, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "Save Chat &Log", "Save Chat Log")
+        top_frame.Bind(wx.EVT_MENU, self.on_chat_save, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "Text &View", "Text View")
+        top_frame.Bind(wx.EVT_MENU, self.pop_textpop, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "Forward Tab\tCtrl+Tab", "Swap Tabs")
+        top_frame.Bind(wx.EVT_MENU, self.forward_tabs, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "Forward Tab\tCtrl+Shift+Tab", "Swap Tabs")
+        top_frame.Bind(wx.EVT_MENU, self.back_tabs, item)
+        menu.AppendItem(item)
+        menu.AppendSeparator()
+        settingmenu = wx.Menu()
+        wndmenu = wx.Menu()
+        tabmenu = wx.Menu()
+        toolmenu = wx.Menu()
+        item = wx.MenuItem(wndmenu, wx.ID_ANY, "Show Images", "Show Images", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_ShowImages, item)
+        wndmenu.AppendItem(item)
+        if self.settings.get_setting("Show_Images_In_Chat") == '1':
+            item.Check(True)
+        item = wx.MenuItem(wndmenu, wx.ID_ANY, "Strip HTML", "Strip HTML", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_StripHTML, item)
+        wndmenu.AppendItem(item)
+        if self.settings.get_setting("striphtml") == '1':
+            item.Check(True)
+        item = wx.MenuItem(wndmenu, wx.ID_ANY, "Chat Time Index", "Chat Time Index", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_ChatTimeIndex, item)
+        wndmenu.AppendItem(item)
+        if self.settings.get_setting("Chat_Time_Indexing") == '1':
+            item.Check(True)
+        item = wx.MenuItem(wndmenu, wx.ID_ANY, "Chat Auto Complete", "Chat Auto Complete", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_ChatAutoComplete, item)
+        wndmenu.AppendItem(item)
+        if self.settings.get_setting("SuppressChatAutoComplete") == '0':
+            item.Check(True)
+        item = wx.MenuItem(wndmenu, wx.ID_ANY, "Show ID in Chat", "Show ID in Chat", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_ShowIDinChat, item)
+        wndmenu.AppendItem(item)
+        if self.settings.get_setting("ShowIDInChat") == '1':
+            item.Check(True)
+        item = wx.MenuItem(wndmenu, wx.ID_ANY, "Log Time Index", "Log Time Index", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_LogTimeIndex, item)
+        wndmenu.AppendItem(item)
+        if self.settings.get_setting("TimeStampGameLog") == '1':
+            item.Check(True)
+        settingmenu.AppendMenu(wx.ID_ANY, 'Chat Window', wndmenu )
+        item = wx.MenuItem(tabmenu, wx.ID_ANY, "Tabbed Whispers", "Tabbed Whispers", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_TabbedWhispers, item)
+        tabmenu.AppendItem(item)
+        if self.settings.get_setting("tabbedwhispers") == '1':
+            item.Check(True)
+        item = wx.MenuItem(tabmenu, wx.ID_ANY, "GM Tab", "GM Tab", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_GMTab, item)
+        tabmenu.AppendItem(item)
+        if self.settings.get_setting("GMWhisperTab") == '1':
+            item.Check(True)
+        item = wx.MenuItem(tabmenu, wx.ID_ANY, "Group Whisper Tabs", "Group Whisper Tabs", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_GroupWhisperTabs, item)
+        tabmenu.AppendItem(item)
+        if self.settings.get_setting("GroupWhisperTab") == '1':
+            item.Check(True)
+        settingmenu.AppendMenu(wx.ID_ANY, 'Chat Tabs', tabmenu)
+        item = wx.MenuItem(toolmenu, wx.ID_ANY, "Dice Bar", "Dice Bar", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_DiceBar, item)
+        toolmenu.AppendItem(item)
+        if self.settings.get_setting("DiceButtons_On") == '1':
+            item.Check(True)
+        item = wx.MenuItem(toolmenu, wx.ID_ANY, "Format Buttons", "Format Buttons", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_FormatButtons, item)
+        toolmenu.AppendItem(item)
+        if self.settings.get_setting("FormattingButtons_On") == '1':
+            item.Check(True)
+        item = wx.MenuItem(toolmenu, wx.ID_ANY, "Alias Tool", "Alias Tool", wx.ITEM_CHECK)
+        top_frame.Bind(wx.EVT_MENU, self.OnMB_AliasTool, item)
+        toolmenu.AppendItem(item)
+        if self.settings.get_setting("AliasTool_On") == '1':
+            item.Check(True)
+        settingmenu.AppendMenu(wx.ID_ANY, 'Chat Tool Bars', toolmenu)
+        menu.AppendMenu(wx.ID_ANY, 'Chat Settings', settingmenu)
+        top_frame.mainmenu.Insert(2, menu, '&Chat')
+        self.log.log("Exit chat_panel->build_menu(self)", ORPG_DEBUG)
+
+    ## Settings Menu Events
+    def OnMB_ShowImages(self, event):
+        self.log.log("Enter chat_panel->OnMB_ShowImages(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("Show_Images_In_Chat", '1')
+        else:
+            self.settings.set_setting("Show_Images_In_Chat", '0')
+        self.log.log("Exit chat_panel->OnMB_ShowImages(self, event)", ORPG_DEBUG)
+
+    def OnMB_StripHTML(self, event):
+        self.log.log("Enter chat_panel->OnMB_StripHTML(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("Sstriphtml", '1')
+        else:
+            self.settings.set_setting("striphtml", '0')
+        self.log.log("Exit chat_panel->OnMB_StripHTML(self, event)", ORPG_DEBUG)
+
+    def OnMB_ChatTimeIndex(self, event):
+        self.log.log("Enter chat_panel->OnMB_ChatTimeIndex(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("Chat_Time_Indexing", '1')
+        else:
+            self.settings.set_setting("Chat_Time_Indexing", '0')
+        self.log.log("Exit chat_panel->OnMB_ChatTimeIndex(self, event)", ORPG_DEBUG)
+
+    def OnMB_ChatAutoComplete(self, event):
+        self.log.log("Enter chat_panel->OnMB_ChatAutoComplete(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("SuppressChatAutoComplete", '0')
+        else:
+            self.settings.set_setting("SuppressChatAutoComplete", '1')
+        self.log.log("Exit chat_panel->OnMB_ChatAutoComplete(self, event)", ORPG_DEBUG)
+
+    def OnMB_ShowIDinChat(self, event):
+        self.log.log("Enter chat_panel->OnMB_ShowIDinChat(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("ShowIDInChat", '1')
+        else:
+            self.settings.set_setting("ShowIDInChat", '0')
+        self.log.log("Exit chat_panel->OnMB_ShowIDinChat(self, event)", ORPG_DEBUG)
+
+    def OnMB_LogTimeIndex(self, event):
+        self.log.log("Enter chat_panel->OnMB_LogTimeIndex(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("TimeStampGameLog", '1')
+        else:
+            self.settings.set_setting("TimeStampGameLog", '0')
+        self.log.log("Exit chat_panel->OnMB_LogTimeIndex(self, event)", ORPG_DEBUG)
+
+    def OnMB_TabbedWhispers(self, event):
+        self.log.log("Enter chat_panel->OnMB_TabbedWhispers(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("tabbedwhispers", '1')
+        else:
+            self.settings.set_setting("tabbedwhispers", '0')
+        self.log.log("Exit chat_panel->OnMB_TabbedWhispers(self, event)", ORPG_DEBUG)
+
+    def OnMB_GMTab(self, event):
+        self.log.log("Enter chat_panel->OnMB_GMTab(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("GMWhisperTab", '1')
+            self.parent.create_gm_tab()
+        else:
+            self.settings.set_setting("GMWhisperTab", '0')
+        self.log.log("Exit chat_panel->OnMB_GMTab(self, event)", ORPG_DEBUG)
+
+    def OnMB_GroupWhisperTabs(self, event):
+        self.log.log("Enter chat_panel->OnMB_GroupWhisperTabs(self, event)", ORPG_DEBUG)
+        if event.IsChecked():
+            self.settings.set_setting("GroupWhisperTab", '1')
+        else:
+            self.settings.set_setting("GroupWhisperTab", '0')
+        self.log.log("Exit chat_panel->OnMB_GroupWhisperTabs(self, event)", ORPG_DEBUG)
+
+
+    def OnMB_DiceBar(self, event):
+        self.log.log("Enter chat_panel->OnMB_DiceBar(self, event)", ORPG_DEBUG)
+        act = '0'
+        if event.IsChecked():
+            self.settings.set_setting("DiceButtons_On", '1')
+            act = '1'
+        else:
+            self.settings.set_setting("DiceButtons_On", '0')
+        self.toggle_dice(act)
+        try:
+            self.parent.GMChatPanel.toggle_dice(act)
+        except:
+            pass
+        for panel in self.parent.whisper_tabs:
+            panel.toggle_dice(act)
+        for panel in self.parent.group_tabs:
+            panel.toggle_dice(act)
+        for panel in self.parent.null_tabs:
+            panel.toggle_dice(act)
+        self.log.log("Exit chat_panel->OnMB_DiceBar(self, event)", ORPG_DEBUG)
+
+    def OnMB_FormatButtons(self, event):
+        self.log.log("Enter chat_panel->OnMB_FormatButtons(self, event)", ORPG_DEBUG)
+        act = '0'
+        if event.IsChecked():
+            self.settings.set_setting("FormattingButtons_On", '1')
+            act = '1'
+        else:
+            self.settings.set_setting("FormattingButtons_On", '0')
+        self.toggle_formating(act)
+        try:
+            self.parent.GMChatPanel.toggle_formating(act)
+        except:
+            pass
+        for panel in self.parent.whisper_tabs:
+            panel.toggle_formating(act)
+        for panel in self.parent.group_tabs:
+            panel.toggle_formating(act)
+        for panel in self.parent.null_tabs:
+            panel.toggle_formating(act)
+        self.log.log("Exit chat_panel->OnMB_FormatButtons(self, event)", ORPG_DEBUG)
+
+    def OnMB_AliasTool(self, event):
+        self.log.log("Enter chat_panel->OnMB_AliasTool(self, event)", ORPG_DEBUG)
+        act = '0'
+        if event.IsChecked():
+            self.settings.set_setting("AliasTool_On", '1')
+            act = '1'
+        else:
+            self.settings.set_setting("AliasTool_On", '0')
+        self.toggle_alias(act)
+        try:
+            self.parent.GMChatPanel.toggle_alias(act)
+        except:
+            pass
+        for panel in self.parent.whisper_tabs:
+            panel.toggle_alias(act)
+        for panel in self.parent.group_tabs:
+            panel.toggle_alias(act)
+        for panel in self.parent.null_tabs:
+            panel.toggle_alias(act)
+        self.log.log("Exit chat_panel->OnMB_AliasTool(self, event)", ORPG_DEBUG)
+
+    def OnMB_BackgroundColor(self, event):
+        self.log.log("Enter chat_panel->OnMB_BackgroundColor(self, event)", ORPG_DEBUG)
+        top_frame = open_rpg.get_component('frame')
+        hexcolor = self.get_color()
+        if hexcolor != None:
+            self.bgcolor = hexcolor
+            self.settings.set_setting('bgcolor', hexcolor)
+            self.chatwnd.SetPage(self.ResetPage())
+            if self.settings.get_setting('ColorTree') == '1':
+                top_frame.tree.SetBackgroundColour(self.settings.get_setting('bgcolor'))
+                top_frame.tree.Refresh()
+                top_frame.players.SetBackgroundColour(self.settings.get_setting('bgcolor'))
+                top_frame.players.Refresh()
+            else:
+                top_frame.tree.SetBackgroundColour('white')
+                top_frame.tree.SetForegroundColour('black')
+                top_frame.tree.Refresh()
+                top_frame.players.SetBackgroundColour('white')
+                top_frame.players.SetForegroundColour('black')
+                top_frame.players.Refresh()
+            self.chatwnd.scroll_down()
+        self.log.log("Exit chat_panel->OnMB_BackgroundColor(self, event)", ORPG_DEBUG)
+
+    def OnMB_TextColor(self, event):
+        self.log.log("Enter chat_panel->OnMB_TextColor(self, event)", ORPG_DEBUG)
+        top_frame = open_rpg.get_component('frame')
+        hexcolor = self.get_color()
+        if hexcolor != None:
+            self.textcolor = hexcolor
+            self.settings.set_setting('textcolor', hexcolor)
+            self.chatwnd.SetPage(self.ResetPage())
+            if self.settings.get_setting('ColorTree') == '1':
+                top_frame.tree.SetForegroundColour(self.settings.get_setting('textcolor'))
+                top_frame.tree.Refresh()
+                top_frame.players.SetForegroundColour(self.settings.get_setting('textcolor'))
+                top_frame.players.Refresh()
+            else:
+                top_frame.tree.SetBackgroundColour('white')
+                top_frame.tree.SetForegroundColour('black')
+                top_frame.tree.Refresh()
+                top_frame.players.SetBackgroundColour('white')
+                top_frame.players.SetForegroundColour('black')
+                top_frame.players.Refresh()
+            self.chatwnd.scroll_down()
+        self.log.log("Exit chat_panel->OnMB_TextColor(self, event)", ORPG_DEBUG)
+
+
+    def get_hot_keys(self):
+        self.log.log("Enter chat_panel->get_hot_keys(self)", ORPG_DEBUG)
+        # dummy menus for hotkeys
+        self.build_menu()
+        entries = []
+        entries.append((wx.ACCEL_CTRL, ord('H'), self.setChatFocusMenu.GetId()))
+        #entries.append((wx.ACCEL_CTRL, wx.WXK_TAB, SWAP_TABS))
+        self.log.log("Enter chat_panel->get_hot_keys(self)", ORPG_DEBUG)
+        return entries
+
+    def forward_tabs(self, evt):
+        self.log.log("Enter chat_panel->swap_tabs(self, evt)", ORPG_DEBUG)
+        self.parent.AdvanceSelection()
+        self.log.log("Exit chat_panel->swap_tabs(self, evt)", ORPG_DEBUG)
+
+    def back_tabs(self, evt):
+        self.log.log("Enter chat_panel->swap_tabs(self, evt)", ORPG_DEBUG)
+        self.parent.AdvanceSelection(False)
+        self.log.log("Exit chat_panel->swap_tabs(self, evt)", ORPG_DEBUG)
+
+    # This subroutine builds the controls for the chat frame
+    #
+    # !self : instance of self
+    def build_ctrls(self):
+        self.log.log("Enter chat_panel->build_ctrls(self)", ORPG_DEBUG)
+        self.chatwnd = chat_html_window(self,-1)
+        self.set_colors()
+        wx.CallAfter(self.chatwnd.SetPage, self.chatwnd.Header())
+        if (self.sendtarget == "all"):
+            wx.CallAfter(self.Post, self.colorize(self.syscolor, "<b>Welcome to <a href='http://www.openrpg.com'>OpenRPG</a> version " + self.version + "...  </b>"))
+            #self.chat_cmds.on_help()
+        self.chattxt = orpg.tools.predTextCtrl.predTextCtrl(self, -1, "", 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 )
+        self.basesizer.Add( self.toolbar_sizer, 0, wx.EXPAND )
+        self.basesizer.Add( self.chattxt, 0, wx.EXPAND )
+        self.SetSizer(self.basesizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        #events
+        self.Bind(wx.EVT_BUTTON, self.on_text_format, self.boldButton)
+        self.Bind(wx.EVT_BUTTON, self.on_text_format, self.italicButton)
+        self.Bind(wx.EVT_BUTTON, self.on_text_format, self.underlineButton)
+        self.Bind(wx.EVT_BUTTON, self.on_text_color, self.color_button)
+        self.Bind(wx.EVT_BUTTON, self.on_chat_save, self.saveButton)
+        self.Bind(wx.EVT_BUTTON, self.onDieRoll, self.d4Button)
+        self.Bind(wx.EVT_BUTTON, self.onDieRoll, self.d6Button)
+        self.Bind(wx.EVT_BUTTON, self.onDieRoll, self.d8Button)
+        self.Bind(wx.EVT_BUTTON, self.onDieRoll, self.d10Button)
+        self.Bind(wx.EVT_BUTTON, self.onDieRoll, self.d12Button)
+        self.Bind(wx.EVT_BUTTON, self.onDieRoll, self.d20Button)
+        self.Bind(wx.EVT_BUTTON, self.onDieRoll, self.d100Button)
+        self.dieIDs = {}
+        self.dieIDs[self.d4Button.GetId()] = 'd4'
+        self.dieIDs[self.d6Button.GetId()] = 'd6'
+        self.dieIDs[self.d8Button.GetId()] = 'd8'
+        self.dieIDs[self.d10Button.GetId()] = 'd10'
+        self.dieIDs[self.d12Button.GetId()] = 'd12'
+        self.dieIDs[self.d20Button.GetId()] = 'd20'
+        self.dieIDs[self.d100Button.GetId()] = 'd100'
+        self.Bind(wx.EVT_BUTTON, self.pop_textpop, self.textpop_lock)
+        self.Bind(wx.EVT_BUTTON, self.lock_scroll, self.scroll_lock)
+        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.log.log("Exit chat_panel->build_ctrls(self)", ORPG_DEBUG)
+    # def build_ctrls - end
+
+    def textCopy(self, event):
+        if self.chattxt.GetStringSelection() == '':
+            self.chatwnd.OnM_EditCopy(None)
+        else:
+            self.chatwnd.Copy()
+
+    def build_bar(self):
+        self.log.log("Enter chat_panel->build_bar(self)", ORPG_DEBUG)
+        self.toolbar_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.scroll_lock = None
+        self.numDieText = None
+        self.dieModText = None
+        if self.settings.get_setting('Toolbar_On') == "1":
+            self.build_alias()
+            self.build_dice()
+            self.build_scroll()
+            self.build_text()
+            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()
+        self.log.log("Exit chat_panel->build_bar(self)", ORPG_DEBUG)
+
+    def build_scroll(self):
+        self.log.log("Enter chat_panel->build_scroll(self)", ORPG_DEBUG)
+        self.scroll_lock = wx.Button( self, wx.ID_ANY, "Scroll ON",size= wx.Size(80,25))
+        self.log.log("Exit chat_panel->build_scroll(self)", ORPG_DEBUG)
+
+    def build_alias(self):
+        self.log.log("Enter chat_panel->build_alias(self)", ORPG_DEBUG)
+        self.aliasList = wx.Choice(self, wx.ID_ANY, size=(100, 25), choices=[self.defaultAliasName])
+        self.aliasButton = createMaskedButton( self, orpg.dirpath.dir_struct["icon"] + 'player.gif', 'Refresh list of aliases from Game Tree', wx.ID_ANY, '#bdbdbd' )
+        self.aliasList.SetSelection(0)
+        self.filterList = wx.Choice(self, wx.ID_ANY, size=(100, 25), choices=[self.defaultFilterName])
+        self.filterButton = createMaskedButton( self, orpg.dirpath.dir_struct["icon"] + 'add_filter.gif', 'Refresh list of filters from Game Tree', wx.ID_ANY, '#bdbdbd' )
+        self.filterList.SetSelection(0)
+        self.toolbar_sizer.Add( self.aliasButton, 0, wx.EXPAND )
+        self.toolbar_sizer.Add( self.aliasList,0,wx.EXPAND)
+        self.toolbar_sizer.Add( self.filterButton, 0, wx.EXPAND )
+        self.toolbar_sizer.Add( self.filterList,0,wx.EXPAND)
+        if self.settings.get_setting('AliasTool_On') == '0':
+            self.toggle_alias('0')
+        else:
+            self.toggle_alias('1')
+        self.log.log("Exit chat_panel->build_alias(self)", ORPG_DEBUG)
+
+    def toggle_alias(self, act):
+        self.log.log("Enter chat_panel->toggle_alias(self, " + str(act) + ")", ORPG_DEBUG)
+        if act == '0':
+            self.toolbar_sizer.Show(self.aliasList, False)
+            self.toolbar_sizer.Show(self.filterList, False)
+            self.toolbar_sizer.Show(self.aliasButton, False)
+            self.toolbar_sizer.Show(self.filterButton, False)
+            self.toolbar_sizer.Layout()
+        else:
+            self.toolbar_sizer.Show(self.aliasList, True)
+            self.toolbar_sizer.Show(self.filterList, True)
+            self.toolbar_sizer.Show(self.aliasButton, True)
+            self.toolbar_sizer.Show(self.filterButton, True)
+            self.toolbar_sizer.Layout()
+        self.log.log("Exit chat_panel->toggle_alias(self, act)", ORPG_DEBUG)
+
+    def build_text(self):
+        self.log.log("Enter chat_panel->build_text(self)", ORPG_DEBUG)
+        self.textpop_lock = createMaskedButton(self, orpg.dirpath.dir_struct["icon"]+'note.gif', 'Open Text View Of Chat Session', wx.ID_ANY, '#bdbdbd')
+        self.log.log("Exit chat_panel->build_text(self)", ORPG_DEBUG)
+
+    def build_dice(self):
+        self.log.log("Enter chat_panel->build_dice(self)", ORPG_DEBUG)
+        self.numDieText = wx.TextCtrl( self, wx.ID_ANY, "1", size= wx.Size(25, 25), validator=orpg.tools.inputValidator.MathOnlyValidator() )
+        self.dieModText = wx.TextCtrl( self, wx.ID_ANY, "", size= wx.Size(50, 25), validator=orpg.tools.inputValidator.MathOnlyValidator() )
+        self.d4Button = createMaskedButton(self, orpg.dirpath.dir_struct["icon"]+'b_d4.gif', 'Roll d4', wx.ID_ANY, '#bdbdbd')
+        self.d6Button = createMaskedButton(self, orpg.dirpath.dir_struct["icon"]+'b_d6.gif', 'Roll d6', wx.ID_ANY, '#bdbdbd')
+        self.d8Button = createMaskedButton(self, orpg.dirpath.dir_struct["icon"]+'b_d8.gif', 'Roll d8', wx.ID_ANY, '#bdbdbd')
+        self.d10Button = createMaskedButton(self, orpg.dirpath.dir_struct["icon"]+'b_d10.gif', 'Roll d10', wx.ID_ANY, '#bdbdbd')
+        self.d12Button = createMaskedButton(self, orpg.dirpath.dir_struct["icon"]+'b_d12.gif', 'Roll d12', wx.ID_ANY, '#bdbdbd')
+        self.d20Button = createMaskedButton(self, orpg.dirpath.dir_struct["icon"]+'b_d20.gif', 'Roll d20', wx.ID_ANY, '#bdbdbd')
+        self.d100Button = createMaskedButton(self, orpg.dirpath.dir_struct["icon"]+'b_d100.gif', 'Roll d100', wx.ID_ANY, '#bdbdbd')
+        self.toolbar_sizer.Add( self.numDieText, 0, wx.ALIGN_CENTER | wx.EXPAND)
+        self.toolbar_sizer.Add( self.d4Button, 0 ,wx.EXPAND)
+        self.toolbar_sizer.Add( self.d6Button, 0 ,wx.EXPAND)
+        self.toolbar_sizer.Add( self.d8Button, 0 ,wx.EXPAND)
+        self.toolbar_sizer.Add( self.d10Button, 0 ,wx.EXPAND)
+        self.toolbar_sizer.Add( self.d12Button, 0 ,wx.EXPAND)
+        self.toolbar_sizer.Add( self.d20Button, 0 ,wx.EXPAND)
+        self.toolbar_sizer.Add( self.d100Button, 0 ,wx.EXPAND)
+        self.toolbar_sizer.Add( self.dieModText, 0, wx.ALIGN_CENTER, 5 )
+        if self.settings.get_setting('DiceButtons_On') == '0':
+            self.toggle_dice('0')
+        else:
+            self.toggle_dice('1')
+        self.log.log("Exit chat_panel->build_dice(self)", ORPG_DEBUG)
+
+    def toggle_dice(self, act):
+        self.log.log("Enter chat_panel->toggle_dice(self, "+ str(act) + ")", ORPG_DEBUG)
+        if act == '0':
+            self.toolbar_sizer.Show(self.numDieText, False)
+            self.toolbar_sizer.Show(self.d4Button, False)
+            self.toolbar_sizer.Show(self.d6Button, False)
+            self.toolbar_sizer.Show(self.d8Button, False)
+            self.toolbar_sizer.Show(self.d10Button, False)
+            self.toolbar_sizer.Show(self.d12Button, False)
+            self.toolbar_sizer.Show(self.d20Button, False)
+            self.toolbar_sizer.Show(self.d100Button, False)
+            self.toolbar_sizer.Show(self.dieModText, False)
+            self.toolbar_sizer.Layout()
+        else:
+            self.toolbar_sizer.Show(self.numDieText, True)
+            self.toolbar_sizer.Show(self.d4Button, True)
+            self.toolbar_sizer.Show(self.d6Button, True)
+            self.toolbar_sizer.Show(self.d8Button, True)
+            self.toolbar_sizer.Show(self.d10Button, True)
+            self.toolbar_sizer.Show(self.d12Button, True)
+            self.toolbar_sizer.Show(self.d20Button, True)
+            self.toolbar_sizer.Show(self.d100Button, True)
+            self.toolbar_sizer.Show(self.dieModText, True)
+            self.toolbar_sizer.Layout()
+        self.log.log("Exit chat_panel->toggle_dice(self, "+ str(act) + ")", ORPG_DEBUG)
+
+    def build_formating(self):
+        self.log.log("Enter chat_panel->build_formating(self)", ORPG_DEBUG)
+        self.boldButton = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'bold.gif', 'Make the selected text Bold', wx.ID_ANY, '#bdbdbd')
+        self.italicButton = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'italic.gif', 'Italicize the selected text', wx.ID_ANY, '#bdbdbd' )
+        self.underlineButton = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'underlined.gif', 'Underline the selected text', wx.ID_ANY, '#bdbdbd' )
+        self.toolbar_sizer.Add( self.boldButton, 0, wx.EXPAND )
+        self.toolbar_sizer.Add( self.italicButton, 0, wx.EXPAND )
+        self.toolbar_sizer.Add( self.underlineButton, 0, wx.EXPAND )
+        if self.settings.get_setting('FormattingButtons_On') == '0':
+            self.toggle_formating('0')
+        else:
+            self.toggle_formating('1')
+        self.log.log("Exit chat_panel->build_formating(self)", ORPG_DEBUG)
+
+    def toggle_formating(self, act):
+        self.log.log("Enter chat_panel->toggle_formating(self, " + str(act) + ")", ORPG_DEBUG)
+        if act == '0':
+            self.toolbar_sizer.Show(self.boldButton, False)
+            self.toolbar_sizer.Show(self.italicButton, False)
+            self.toolbar_sizer.Show(self.underlineButton, False)
+            self.toolbar_sizer.Layout()
+        else:
+            self.toolbar_sizer.Show(self.boldButton, True)
+            self.toolbar_sizer.Show(self.italicButton, True)
+            self.toolbar_sizer.Show(self.underlineButton, True)
+            self.toolbar_sizer.Layout()
+        self.log.log("Exit chat_panel->toggle_formating(self, " + str(act) + ")", ORPG_DEBUG)
+
+    # Heroman - Ideally, we would use static labels...
+    def build_colorbutton(self):
+        self.log.log("Enter chat_panel->build_colorbutton(self)", ORPG_DEBUG)
+        self.color_button = wx.Button(self, wx.ID_ANY, "C",wx.Point(0,0), wx.Size(22,0))
+        self.saveButton = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'save.bmp', 'Save the chatbuffer', wx.ID_ANY, '#c0c0c0', wx.BITMAP_TYPE_BMP )
+        self.color_button.SetBackgroundColour(wx.BLACK)
+        self.color_button.SetForegroundColour(wx.WHITE)
+        self.toolbar_sizer.Add(self.color_button, 0, wx.EXPAND)
+        self.toolbar_sizer.Add( self.saveButton, 0, wx.EXPAND )
+        self.log.log("Exit chat_panel->build_colorbutton(self)", ORPG_DEBUG)
+
+    def OnMotion(self, evt):
+        self.log.log("Enter chat_panel->OnMotion(self, evt)", ORPG_DEBUG)
+        contain = self.chatwnd.GetInternalRepresentation()
+        if contain:
+            sx = sy = 0
+            x = y = 0
+            (sx,sy) = self.chatwnd.GetViewStart()
+            (sx1,sy1) = self.chatwnd.GetScrollPixelsPerUnit()
+            sx = sx*sx1
+            sy = sy*sy1
+            (x,y) = evt.GetPosition()
+            lnk = contain.GetLink(sx+x,sy+y)
+            if lnk:
+                try:
+                    link = lnk.GetHref()
+                    self.session.set_status_url(link)
+                except:
+                    pass
+        else:
+            self.log.log("Error, self.chatwnd.GetInternalRepresentation() return None", ORPG_GENERAL)
+        evt.Skip()
+        self.log.log("Exit chat_panel->OnMotion(self, evt)", ORPG_DEBUG)
+
+    #  This subroutine is registered with predTextCtrl to be run for every OnChar event
+    #  It checks if we need to send a typing message
+
+    #
+    #  self:  duh
+    #  event:  raw KeyEvent from OnChar()
+    def myKeyHook(self, event):
+        self.log.log("Enter chat_panel->myKeyHook(self, event)", ORPG_DEBUG)
+        if self.session.get_status() == MPLAY_CONNECTED:   #  only do if we're connected
+            thisPress = time.time()                #  thisPress is local temp variable
+            if (thisPress - self.lastSend) > 4:    #  Check to see if it's been 5 seconds since our last notice
+                                                   #    If we're not already typing, then self.lastSend will be 0
+                self.sendTyping(1)                 #  send a not typing event here (1 for True)
+            self.lastPress = thisPress             #  either way, record the time of this keystroke for use in
+                                                   #  self.typingTimerFunc()
+        if self.settings.get_setting('SuppressChatAutoComplete') == '1':
+            self.log.log("Exit chat_panel->myKeyHook(self, event) return 1", ORPG_DEBUG)
+            return 1
+        else:
+            self.log.log("Exit chat_panel->myKeyHook(self, event) return 0", ORPG_DEBUG)
+            return 0
+
+    #  This subroutine gets called once a second by the typing Timer
+    #  It checks if we need to send a not_typing message
+    #
+    #  self:  duh
+    def typingTimerFunc(self, event):
+        #following added by mDuo13
+        ##############refresh_counter()##############
+        for plugin_fname in self.activeplugins.keys():
+            plugin = self.activeplugins[plugin_fname]
+            try:
+                plugin.refresh_counter()
+            except Exception, e:
+                if str(e) != "'module' object has no attribute 'refresh_counter'":
+                    self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                    self.log.log("EXCEPTION: " + str(e), ORPG_GENERAL)
+        #end mDuo13 added code
+        if self.lastSend:                          #  This will be zero when not typing, so equiv to if is_typing
+            thisTime = time.time()                 #  thisTime is a local temp variable
+            if (thisTime - self.lastPress) > 4:    #  Check to see if it's been 5 seconds since our last keystroke
+                                               #    If we're not already typing, then self.lastSend will be 0
+
+                self.sendTyping(0)                 #  send a typing event here (0 for False)
+    #  This subroutine actually takes care of sending the messages for typing/not_typing events
+    #
+    #  self:  duh
+    #  typing:  boolean
+    def sendTyping(self, typing):
+        self.log.log("Enter chat_panel->sendTyping(self, typing)", ORPG_DEBUG)
+        if typing:
+            self.lastSend = time.time()  #  remember our send time for use in myKeyHook()
+            #I think this is cleaner
+            status_text = self.settings.get_setting('TypingStatusAlias')
+            if status_text == "" or status_text == None:
+                status_text = "Typing"
+            self.session.set_text_status(status_text)
+        else:
+            self.lastSend = 0                            #  set lastSend to zero to indicate we're not typing
+            #I think this is cleaner
+            status_text = self.settings.get_setting('IdleStatusAlias')
+            if status_text == "" or status_text == None:
+                status_text = "Idle"
+            self.session.set_text_status(status_text)
+        self.log.log("Exit chat_panel->sendTyping(self, typing)", ORPG_DEBUG)
+
+    # This subroutine sets the colors of the chat based on the settings in the
+    # self instance.
+    #
+    # !self : instance of self
+    def set_colors(self):
+        self.log.log("Enter chat_panel->set_colors(self)", ORPG_DEBUG)
+        # chat window backround color
+        self.bgcolor = self.settings.get_setting('bgcolor')
+        # chat window normal text color
+        self.textcolor = self.settings.get_setting('textcolor')
+        # color of text player types
+        self.mytextcolor = self.settings.get_setting('mytextcolor')
+        # color of system warnings
+        self.syscolor = self.settings.get_setting('syscolor')
+        # color of system info messages
+        self.infocolor = self.settings.get_setting('infocolor')
+        # color of emotes
+        self.emotecolor = self.settings.get_setting('emotecolor')
+        # color of whispers
+        self.whispercolor = self.settings.get_setting('whispercolor')
+        self.log.log("Exit chat_panel->set_colors(self)", ORPG_DEBUG)
+    # def set_colors - end
+
+    # This subroutine will insert text into the chat window
+    #
+    # !self : instance of self
+    # !txt : text to be inserted into the chat window
+    def set_chat_text(self, txt):
+        self.log.log("Enter chat_panel->set_chat_text(self, txt)", ORPG_DEBUG)
+        self.chattxt.SetValue(txt)
+        self.chattxt.SetFocus()
+        self.chattxt.SetInsertionPointEnd()
+        self.log.log("Exit chat_panel->set_chat_text(self, txt)", ORPG_DEBUG)
+    # def set_chat_text - end
+
+    def get_chat_text(self):
+        self.log.log("Enter chat_panel->get_chat_text(self)", ORPG_DEBUG)
+        self.log.log("Enter chat_panel->get_chat_text(self)", ORPG_DEBUG)
+        return self.chattxt.GetValue()
+
+    # This subroutine sets the focus to the chat window
+    def set_chat_text_focus(self, event):
+        self.log.log("Enter chat_panel->set_chat_text_focus(self, event)", ORPG_DEBUG)
+        wx.CallAfter(self.chattxt.SetFocus)
+        self.log.log("Exit chat_panel->set_chat_text_focus(self, event)", ORPG_DEBUG)
+    # def set_chat_text_focus - end
+
+    # This subrtouine grabs the user input and make the special keys and
+    # modifiers work.
+    #
+    # !self : instance of self
+    # !event :
+    #
+    # Note:  self.chattxt now handles it's own Key events.  It does, however still
+    #        call it's parent's (self) OnChar to handle "default" behavior.
+    def OnChar(self, event):
+        self.log.log("Enter chat_panel->OnChar(self, event)", ORPG_DEBUG)
+        s = self.chattxt.GetValue()
+        #self.histlen = len(self.history) - 1
+
+        ## 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.log.log("event.GetKeyCode() == wx.WXK_RETURN", ORPG_DEBUG)
+            self.set_colors()
+            if self.session.get_status() == MPLAY_CONNECTED:          #  only do if we're connected
+                self.sendTyping(0)                                    #  Send a "not_typing" event on enter key press
+        macroText=""
+
+	
+        recycle_bin = {wx.WXK_F1: 'event.GetKeyCode() == wx.WXK_F1', wx.WXK_F2: 'event.GetKeyCode() == wx.WXK_F2', wx.WXK_F3: 'event.GetKeyCode() == wx.WXK_F3', wx.WXK_F4: 'event.GetKeyCode() == wx.WXK_F4', wx.WXK_F5: 'event.GetKeyCode() == wx.WXK_F5', wx.WXK_F6: 'event.GetKeyCode() == wx.WXK_F6', wx.WXK_F7: 'event.GetKeyCode() == wx.WXK_F7', wx.WXK_F8: 'event.GetKeyCode() == wx.WXK_F8', wx.WXK_F9: 'event.GetKeyCode() == wx.WXK_F9', wx.WXK_F10: 'event.GetKeyCode() == wx.WXK_F10', wx.WXK_F11: 'event.GetKeyCode() == wx.WXK_F11', wx.WXK_F12: 'event.GetKeyCode() == wx.WXK_F12'}
+# Recycle Bin and Lambda should reduce this whole IF ELIF statement block.
+        bin_event = event.GetKeyCode()
+	if recycle_bin.has_key(bin_event):
+	    self.log.log(lambda bin_event: recycle_bin[bin_event], ORPG_DEBUG)
+	    macroText = self.settings.get_setting(recycle_bin[bin_event][29:])
+	    recycle_bin = {}; del bin_event
+
+        # 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
+
+        ## RETURN KEY (and not text in control)
+        if (event.GetKeyCode() == wx.WXK_RETURN and len(s)) or len(macroText):
+            self.log.log("(event.GetKeyCode() == wx.WXK_RETURN and len(s)) or len(macroText)", ORPG_DEBUG)
+            self.histidx = -1
+            self.temptext = ""
+            self.history = [s] + self.history#prepended instead of appended now, so higher index = greater age
+            if not len(macroText):
+                self.chattxt.SetValue("")
+            # play sound
+            sound_file = self.settings.get_setting("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 )
+            else:
+                self.chat_cmds.docmd(s) # emote is in chatutils.py
+
+        ## UP KEY
+        elif event.GetKeyCode() == wx.WXK_UP:
+            self.log.log("event.GetKeyCode() == wx.WXK_UP", ORPG_DEBUG)
+            if self.histidx < len(self.history)-1:
+                #text that's not in history but also hasn't been sent to chat gets stored in self.temptext
+                #this way if someone presses the up key, they don't lose their current message permanently
+                #(unless they also press enter at the time)
+                if self.histidx is -1:
+                    self.temptext = self.chattxt.GetValue()
+                self.histidx += 1
+                self.chattxt.SetValue(self.history[self.histidx])
+                self.chattxt.SetInsertionPointEnd()
+            else:
+                self.histidx = len(self.history) -1#in case it got too high somehow, this should fix it
+                #self.InfoPost("**Going up? I don't think so.**")
+            #print self.histidx, "in",self.history
+
+        ## DOWN KEY
+        elif event.GetKeyCode() == wx.WXK_DOWN:
+            self.log.log("event.GetKeyCode() == wx.WXK_DOWN", ORPG_DEBUG)
+            #histidx of -1 indicates currently viewing text that's not in self.history
+            if self.histidx > -1:
+                self.histidx -= 1
+                if self.histidx is -1: #remember, it just decreased
+                    self.chattxt.SetValue(self.temptext)
+                else:
+                    self.chattxt.SetValue(self.history[self.histidx])
+                self.chattxt.SetInsertionPointEnd()
+            else:
+                self.histidx = -1 #just in case it somehow got below -1, this should fix it
+                #self.InfoPost("**Going down? I don't think so.**")
+            #print self.histidx, "in",self.history
+
+        ## TAB KEY
+        elif  event.GetKeyCode() == wx.WXK_TAB:
+            self.log.log("event.GetKeyCode() == wx.WXK_TAB", ORPG_DEBUG)
+            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: ## matched more than 1, tell user what matched
+                    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()
+                else: ## put the matched name in the chattxt box
+                    settext = nicks[0] + ": "
+                    self.chattxt.SetValue(settext)
+                    self.chattxt.SetInsertionPointEnd()
+            else: ## not online, and no text in chattxt box
+                self.Post(self.colorize(self.syscolor, " ** That's the Tab key, Dave"))
+
+        ## PAGE UP
+        elif event.GetKeyCode() in (wx.WXK_PRIOR, wx.WXK_PAGEUP):
+            self.log.log("event.GetKeyCode() in (wx.WXK_PRIOR, wx.WXK_PAGEUP)", ORPG_DEBUG)
+            self.chatwnd.ScrollPages(-1)
+            if not self.lockscroll:
+                self.lock_scroll(0)
+
+        ## PAGE DOWN
+        elif event.GetKeyCode() in (wx.WXK_NEXT, wx.WXK_PAGEDOWN):
+            self.log.log("event.GetKeyCode() in (wx.WXK_NEXT, wx.WXK_PAGEDOWN)", ORPG_DEBUG)
+            if not self.lockscroll:
+                self.lock_scroll(0)
+            if ((self.chatwnd.GetScrollRange(1)-self.chatwnd.GetScrollPos(1)-self.chatwnd.GetScrollThumb(1) < 30) and self.lockscroll):
+                self.lock_scroll(0)
+            self.chatwnd.ScrollPages(1)
+
+        ## END
+        elif event.GetKeyCode() == wx.WXK_END:
+            self.log.log("event.GetKeyCode() == wx.WXK_END", ORPG_DEBUG)
+            if self.lockscroll:
+                self.lock_scroll(0)
+                self.Post()
+            event.Skip()
+
+        ## NOTHING
+        else:
+            event.Skip()
+        self.log.log("Exit chat_panel->OnChar(self, event)", ORPG_DEBUG)
+    # def OnChar - end
+
+    def onDieRoll(self, evt):
+        """Roll the dice based on the button pressed and the die modifiers entered, if any."""
+        self.log.log("Enter chat_panel->onDieRoll(self, evt)", ORPG_DEBUG)
+        # Get any die modifiers if they have been entered
+        numDie = self.numDieText.GetValue()
+        dieMod = self.dieModText.GetValue()
+        dieText = numDie
+        # Now, apply and roll die mods based on the button that was pressed
+        id = evt.GetId()
+        if self.dieIDs.has_key(id):
+            dieText += self.dieIDs[id]
+        if len(dieMod) and dieMod[0] not in "*/-+":
+            dieMod = "+" + dieMod
+        dieText += dieMod
+        dieText = "[" + dieText + "]"
+        self.ParsePost(dieText, 1, 1)
+        self.chattxt.SetFocus()
+        self.log.log("Exit chat_panel->onDieRoll(self, evt)", ORPG_DEBUG)
+
+    # This subroutine saves a chat buffer as html to a file chosen via a
+    # FileDialog.
+    #
+    # !self : instance of self
+    # !evt :
+    def on_chat_save(self, evt):
+        self.log.log("Enter chat_panel->on_chat_save(self, evt)", ORPG_DEBUG)
+        f = wx.FileDialog(self,"Save Chat Buffer",".","","HTM* (*.htm*)|*.htm*|HTML (*.html)|*.html|HTM (*.htm)|*.htm",wx.SAVE)
+        if f.ShowModal() == wx.ID_OK:
+            file = open(f.GetPath(), "w")
+            file.write(self.ResetPage() + "</body></html>")
+            file.close()
+        f.Destroy()
+        os.chdir(self.root_dir)
+        self.log.log("Exit chat_panel->on_chat_save(self, evt)", ORPG_DEBUG)
+    # def on_chat_save - end
+
+    def ResetPage(self):
+        self.set_colors()
+        buffertext = self.chatwnd.Header() + "\n"
+        buffertext += chat_util.strip_body_tags(self.chatwnd.StripHeader()).replace("<br>", "<br />").replace('</html>', '').replace("<br />", "<br />\n").replace("\n\n", '')
+        return buffertext
+
+    # This subroutine sets the color of selected text, or base text color if
+    # nothing is selected
+    def on_text_color(self, event):
+        self.log.log("Enter chat_panel->on_text_color(self, event)", ORPG_DEBUG)
+        hexcolor = self.r_h.do_hex_color_dlg(self)
+        if hexcolor != None:
+            (beg,end) = self.chattxt.GetSelection()
+            if beg != end:
+                txt = self.chattxt.GetValue()
+                txt = txt[:beg]+self.colorize(hexcolor,txt[beg:end]) +txt[end:]
+                self.chattxt.SetValue(txt)
+                self.chattxt.SetInsertionPointEnd()
+                self.chattxt.SetFocus()
+            else:
+                self.color_button.SetBackgroundColour(hexcolor)
+                self.mytextcolor = hexcolor
+                self.settings.set_setting('mytextcolor',hexcolor)
+                self.set_colors()
+                self.Post()
+        self.log.log("Exit chat_panel->on_text_color(self, event)", ORPG_DEBUG)
+    # def on_text_color - end
+
+    # This subroutine take a color and a text string and formats it into html.
+    #
+    # !self : instance of self
+    # !color : color for the text to be set
+    # !text : text string to be included in the html.
+    def colorize(self, color, text):
+        """Puts font tags of 'color' around 'text' value, and returns the string"""
+        self.log.log("Enter chat_panel->colorize(self, color, text)", ORPG_DEBUG)
+        self.log.log("Exit chat_panel->colorize(self, color, text)", ORPG_DEBUG)
+        return "<font color='" + color + "'>" + text + "</font>"
+    # def colorize - end
+
+    # This subroutine takes and event and inserts text with the basic format
+    # tags included.
+    #
+    # !self : instance of self
+    # !event :
+    def on_text_format(self, event):
+        self.log.log("Enter chat_panel->on_text_format(self, event)", ORPG_DEBUG)
+        id = event.GetId()
+        txt = self.chattxt.GetValue()
+        (beg,end) = self.chattxt.GetSelection()
+        if beg != end:
+            sel_txt = txt[beg:end]
+        else:
+            sel_txt = txt
+        if id == self.boldButton.GetId():
+            sel_txt = "<b>" + sel_txt + "</b>"
+        elif id == self.italicButton.GetId():
+            sel_txt = "<i>" + sel_txt + "</i>"
+        elif id == self.underlineButton.GetId():
+            sel_txt = "<u>" + sel_txt + "</u>"
+        if beg != end:
+            txt = txt[:beg] + sel_txt + txt[end:]
+        else:
+            txt = sel_txt
+        self.chattxt.SetValue(txt)
+        self.chattxt.SetInsertionPointEnd()
+        self.chattxt.SetFocus()
+        self.log.log("Exit chat_panel->on_text_format(self, event)", ORPG_DEBUG)
+    # def on_text_format - end
+
+    def lock_scroll(self, event):
+        self.log.log("Enter chat_panel->lock_scroll(self, event)", ORPG_DEBUG)
+        if self.lockscroll:
+            self.lockscroll = False
+            self.scroll_lock.SetLabel("Scroll ON")
+            if len(self.storedata) != 0:
+                for line in self.storedata:
+                    self.chatwnd.AppendToPage(line)
+            self.storedata = []
+            self.scroll_down()
+        else:
+            self.lockscroll = True
+            self.scroll_lock.SetLabel("Scroll OFF")
+        self.log.log("Exit chat_panel->lock_scroll(self, event)", ORPG_DEBUG)
+
+    # This subroutine will popup a text window with the chatbuffer contents
+    #
+    # !self : instance of self
+    # !event :
+    def pop_textpop(self, event):
+        """searchable popup text view of chatbuffer"""
+        self.log.log("Enter chat_panel->pop_textpop(self, event)", ORPG_DEBUG)
+        h_buffertext = self.ResetPage()
+        h_dlg = orpgScrolledMessageFrameEditor(self, h_buffertext, "Text View of Chat Window", None, (500,300))
+        h_dlg.Show(True)
+        self.log.log("Exit chat_panel->pop_textpop(self, event)", ORPG_DEBUG)
+
+    # This subroutine will change the dimension of the window
+    #
+    # !self : instance of self
+    # !event :
+    def OnSize(self, event=None):
+        self.log.log("Enter chat_panel->OnSize(self, event=None)", ORPG_DEBUG)
+        event.Skip()
+        wx.CallAfter(self.scroll_down)
+        self.log.log("Exit chat_panel->OnSize(self, event=None)", ORPG_DEBUG)
+    # def OnSize - end
+
+    def scroll_down(self):
+        self.Freeze()
+        self.chatwnd.scroll_down()
+        self.Thaw()
+
+    ###### message helpers ######
+    def PurgeChat(self):
+        self.set_colors()
+        self.chatwnd.SetPage(self.chatwnd.Header())
+
+    def system_message(self, text):
+        self.log.log("Enter chat_panel->system_message(self, text)", ORPG_DEBUG)
+        self.send_chat_message(text,chat_msg.SYSTEM_MESSAGE)
+        self.SystemPost(text)
+        self.log.log("Exit chat_panel->system_message(self, text)", ORPG_DEBUG)
+
+    def info_message(self, text):
+        self.log.log("Enter chat_panel->info_message(self, text)", ORPG_DEBUG)
+        self.send_chat_message(text,chat_msg.INFO_MESSAGE)
+        self.InfoPost(text)
+        self.log.log("Exit chat_panel->info_message(self, text)", ORPG_DEBUG)
+
+    def get_gms(self):
+        self.log.log("Enter chat_panel->get_gms(self)", ORPG_DEBUG)
+        the_gms = []
+        for playerid in self.session.players:
+            if len(self.session.players[playerid])>7:
+                if self.session.players[playerid][7]=="GM" and self.session.group_id != '0':
+                    the_gms += [playerid]
+        self.log.log("Exit chat_panel->get_gms(self)", ORPG_DEBUG)
+        return the_gms
+
+    def GetName(self):
+        self.log.log("Enter chat_panel->GetName(self)", ORPG_DEBUG)
+        self.AliasLib = open_rpg.get_component('alias')
+        player = self.session.get_my_info()
+        if self.AliasLib != None:
+            self.AliasLib.alias = self.aliasList.GetStringSelection();
+            if self.AliasLib.alias[0] != self.defaultAliasName:
+                self.log.log("Exit chat_panel->GetName(self)", ORPG_DEBUG)
+                return [self.chat_display_name([self.AliasLib.alias[0], player[1], player[2]]), self.AliasLib.alias[1]]
+        self.log.log("Exit chat_panel->GetName(self)", ORPG_DEBUG)
+        return [self.chat_display_name(player), "Default"]
+
+    def GetFilteredText(self, text):
+        self.log.log("Enter chat_panel->GetFilteredText(self, text)", ORPG_DEBUG)
+        advregex = re.compile('\"(.*?)\"', re.I)
+        self.AliasLib = open_rpg.get_component('alias')
+        if self.AliasLib != None:
+            self.AliasLib.filter = self.filterList.GetSelection()-1;
+            for rule in self.AliasLib.filterRegEx:
+                if not self.advancedFilter:
+                    text = re.sub(rule[0], rule[1], text)
+                else:
+                    for m in advregex.finditer(text):
+                        match = m.group(0)
+                        newmatch = re.sub(rule[0], rule[1], match)
+                        text = text.replace(match, newmatch)
+        self.log.log("Exit chat_panel->GetFilteredText(self, text)", ORPG_DEBUG)
+        return text
+
+    def emote_message(self, text):
+        self.log.log("Enter chat_panel->emote_message(self, text)", ORPG_DEBUG)
+        text = self.NormalizeParse(text)
+        text = self.colorize(self.emotecolor, text)
+
+        if self.type == MAIN_TAB and self.sendtarget == 'all':
+            self.send_chat_message(text,chat_msg.EMOTE_MESSAGE)
+        elif self.type == MAIN_TAB and self.sendtarget == "gm":
+            msg_type = chat_msg.WHISPER_EMOTE_MESSAGE
+            the_gms = self.get_gms()
+            for each_gm in the_gms:
+                self.send_chat_message(text,chat_msg.WHISPER_EMOTE_MESSAGE, str(each_gm))
+        elif self.type == GROUP_TAB and WG_LIST.has_key(self.sendtarget):
+            for pid in WG_LIST[self.sendtarget]:
+                self.send_chat_message(text,chat_msg.WHISPER_EMOTE_MESSAGE, str(pid))
+        elif self.type == WHISPER_TAB:
+            self.send_chat_message(text,chat_msg.WHISPER_EMOTE_MESSAGE, str(self.sendtarget))
+        elif self.type == NULL_TAB:
+            pass
+        name = self.GetName()[0]
+        text = "** " + name + " " + text + " **"
+        self.EmotePost(text)
+        self.log.log("Exit chat_panel->emote_message(self, text)", ORPG_DEBUG)
+
+    def whisper_to_players(self, text, player_ids):
+        self.log.log("Enter chat_panel->whisper_to_players(self, text, player_ids)", ORPG_DEBUG)
+        tabbed_whispers_p = self.settings.get_setting("tabbedwhispers")
+        # Heroman - apply any filtering selected
+        text = self.NormalizeParse(text)
+        player_names = ""
+        # post to our chat before we colorize
+        for m in player_ids:
+            id = m.strip()
+            if self.session.is_valid_id(id):
+                returned_name = self.session.get_player_by_player_id(id)[0]
+                player_names += returned_name
+                player_names += ", "
+            else:
+                player_names += " Unknown!"
+                player_names += ", "
+        comma = ","
+        comma.join(player_ids)
+        if (self.sendtarget == "all"):
+            self.InfoPost("<i>whispering to "+ player_names + " " + text + "</i> ")
+        # colorize and loop, sending whisper messages to all valid clients
+        text = self.colorize(self.mytextcolor, text)
+        for id in player_ids:
+            id = id.strip()
+            if self.session.is_valid_id(id):
+                self.send_chat_message(text,chat_msg.WHISPER_MESSAGE,id)
+            else:
+                self.InfoPost(id + " Unknown!")
+        self.log.log("Exit chat_panel->whisper_to_players(self, text, player_ids)", ORPG_DEBUG)
+
+    def send_chat_message(self, text, type=chat_msg.CHAT_MESSAGE, player_id="all"):
+        self.log.log("Enter chat_panel->send_chat_message(self, text, type, player_id)", ORPG_DEBUG)
+        #########send_msg()#############
+        send = 1
+        for plugin_fname in self.activeplugins.keys():
+            plugin = self.activeplugins[plugin_fname]
+            try:
+                text, send = plugin.send_msg(text, send)
+            except Exception, e:
+                if str(e) != "'module' object has no attribute 'send_msg'":
+                    self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                    self.log.log("EXCEPTION: " + str(e), ORPG_GENERAL)
+        msg = chat_msg.chat_msg()
+        msg.set_text(text)
+        msg.set_type(type)
+        turnedoff = False
+        if self.settings.get_setting("ShowIDInChat") == "1":
+            turnedoff = True
+            self.settings.set_setting("ShowIDInChat", "0")
+        playername = self.GetName()[0]
+
+        if turnedoff:
+            self.settings.set_setting("ShowIDInChat", "1")
+        msg.set_alias(playername)
+        if send:
+            self.session.send(msg.toxml(),player_id)
+        del msg
+        self.log.log("Exit chat_panel->send_chat_message(self, text, type, player_id)", ORPG_DEBUG)
+
+    #### incoming chat message handler #####
+    def post_incoming_msg(self, msg, player):
+        self.log.log("Enter chat_panel->post_incoming_msg(self, msg, player)", ORPG_DEBUG)
+
+        # pull data
+        type = msg.get_type()
+        text = msg.get_text()
+        alias = msg.get_alias()
+        # who sent us the message?
+        if alias:
+            display_name = self.chat_display_name([alias, player[1], player[2]])
+        elif player:
+            display_name = self.chat_display_name(player)
+        else:
+            display_name = "Server Administrator"
+
+        ######### START plugin_incoming_msg() ###########
+        for plugin_fname in self.activeplugins.keys():
+            plugin = self.activeplugins[plugin_fname]
+            try:
+                text, type, name = plugin.plugin_incoming_msg(text, type, display_name, player)
+            except Exception, e:
+                if str(e) != "'module' object has no attribute 'receive_msg'":
+                    self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                    self.log.log("EXCEPTION: " + str(e), ORPG_GENERAL)
+        #end mDuo13 added code
+        #image stripping for players' names
+        strip_img = self.settings.get_setting("Show_Images_In_Chat")
+        if (strip_img == "0"):
+            display_name = chat_util.strip_img_tags(display_name)
+        #end image stripping. --mDuo13, July 11th, 2005
+        # default sound
+        recvSound = "RecvSound"
+        # act on the type of messsage
+        if (type == chat_msg.CHAT_MESSAGE):
+            text = "<b>" + display_name + "</b>: " + text
+            self.Post(text)
+            self.parent.newMsg(0)
+        elif type == chat_msg.WHISPER_MESSAGE or type == chat_msg.WHISPER_EMOTE_MESSAGE:
+            tabbed_whispers_p = self.settings.get_setting("tabbedwhispers")
+            displaypanel = self
+            whisperingstring = " (whispering): "
+            panelexists = 0
+            GMWhisperTab = self.settings.get_setting("GMWhisperTab")
+            GroupWhisperTab = self.settings.get_setting("GroupWhisperTab")
+            name = '<i><b>' + display_name + '</b>: '
+            text += '</i>'
+            panelexists = 0
+            created = 0
+            try:
+                if GMWhisperTab == '1':
+                    the_gms = self.get_gms()
+                    #Check if whisper if from a GM
+                    if player[2] in the_gms:
+                        msg = name + ' (GM Whisper:) ' + text
+                        if type == chat_msg.WHISPER_MESSAGE:
+                            self.parent.GMChatPanel.Post(msg)
+                        else:
+                            self.parent.GMChatPanel.EmotePost("**" + msg + "**")
+                        idx = self.parent.get_tab_index(self.parent.GMChatPanel)
+                        self.parent.newMsg(idx)
+                        panelexists = 1
+                #See if message if from someone in our groups or for a whisper tab we already have
+                if not panelexists and GroupWhisperTab == "1":
+                    for panel in self.parent.group_tabs:
+                        if WG_LIST.has_key(panel.sendtarget) and WG_LIST[panel.sendtarget].has_key(int(player[2])):
+                            msg = name + text
+                            if type == chat_msg.WHISPER_MESSAGE:
+                                panel.Post(msg)
+                            else:
+                                panel.EmotePost("**" + msg + "**")
+                            idx = self.parent.get_tab_index(panel)
+                            self.parent.newMsg(idx)
+                            panelexists = 1
+                            break
+                if not panelexists and tabbed_whispers_p == "1":
+                    for panel in self.parent.whisper_tabs:
+                        #check for whisper tabs as well, to save the number of loops
+                        if panel.sendtarget == player[2]:
+                            msg = name + whisperingstring + text
+                            if type == chat_msg.WHISPER_MESSAGE:
+                                panel.Post(msg)
+                            else:
+                                panel.EmotePost("**" + msg + "**")
+                            idx = self.parent.get_tab_index(panel)
+                            self.parent.newMsg(idx)
+                            panelexists = 1
+                            break
+                #We did not fint the tab
+                if not panelexists:
+                    #If we get here the tab was not found
+                    if GroupWhisperTab == "1":
+                        for group in WG_LIST.keys():
+                            #Check if this group has the player in it
+                            if WG_LIST[group].has_key(int(player[2])):
+                                #Yup, post message. Player may be in more then 1 group so continue as well
+                                panel = self.parent.create_group_tab(group)
+                                msg = name + text
+                                if type == chat_msg.WHISPER_MESSAGE:
+                                    wx.CallAfter(panel.Post, msg)
+                                else:
+                                    wx.CallAfter(panel.EmotePost, "**" + msg + "**")
+                                created = 1
+                    #Check to see if we should create a whisper tab
+                    if not created and tabbed_whispers_p == "1":
+                        panel = self.parent.create_whisper_tab(player[2])
+                        msg = name + whisperingstring + text
+                        if type == chat_msg.WHISPER_MESSAGE:
+                            wx.CallAfter(panel.Post, msg)
+                        else:
+                            wx.CallAfter(panel.EmotePost, "**" + msg + "**")
+                        created = 1
+                    #Final check
+                    if not created:
+                        #No tabs to create, just send the message to the main chat tab
+                        msg = name + whisperingstring + text
+                        if type == chat_msg.WHISPER_MESSAGE:
+                            self.parent.MainChatPanel.Post(msg)
+                        else:
+                            self.parent.MainChatPanel.EmotePost("**" + msg + "**")
+                        self.parent.newMsg(0)
+            except Exception, e:
+                self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                self.log.log("EXCEPTION: 'Error in posting whisper message': " + str(e), ORPG_GENERAL)
+        elif (type == chat_msg.EMOTE_MESSAGE):
+            text = "** " + display_name + " " + text + " **"
+            self.EmotePost(text)
+            self.parent.newMsg(0)
+        elif (type == chat_msg.INFO_MESSAGE):
+            text = "<b>" + display_name + "</b>: " + text
+            self.InfoPost(text)
+            self.parent.newMsg(0)
+        elif (type == chat_msg.SYSTEM_MESSAGE):
+            text = "<b>" + display_name + "</b>: " + text
+            self.SystemPost(text)
+            self.parent.newMsg(0)
+        # playe sound
+        sound_file = self.settings.get_setting(recvSound)
+        if sound_file != '':
+            self.sound_player.play(sound_file)
+        self.log.log("Exit chat_panel->post_incoming_msg(self, msg, player)", ORPG_DEBUG)
+    #### Posting helpers #####
+
+    def InfoPost(self, s):
+        self.log.log("Enter chat_panel->InfoPost(self, s)", ORPG_DEBUG)
+        self.Post(self.colorize(self.infocolor, s))
+        self.log.log("Exit chat_panel->InfoPost(self, s)", ORPG_DEBUG)
+
+    def SystemPost(self, s):
+        self.log.log("Enter chat_panel->SystemPost(self, s)", ORPG_DEBUG)
+        self.Post(self.colorize(self.syscolor, s))
+        self.log.log("Exit chat_panel->SystemPost(self, s)", ORPG_DEBUG)
+
+    def EmotePost(self, s):
+        self.log.log("Enter chat_panel->EmotePost(self, s)", ORPG_DEBUG)
+        self.Post(self.colorize(self.emotecolor, s))
+        self.log.log("Exit chat_panel->EmotePost(self, s)", ORPG_DEBUG)
+
+    #### Standard Post method #####
+    def Post(self, s="", send=False, myself=False):
+        self.log.log("Enter chat_panel->Post(self, s, send, myself)", ORPG_DEBUG)
+        strip_p = self.settings.get_setting("striphtml")
+        strip_img = self.settings.get_setting("Show_Images_In_Chat")#moved back 7-11-05. --mDuo13
+        if (strip_p == "1"):
+            s = strip_html(s)
+        if (strip_img == "0"):
+            s = chat_util.strip_img_tags(s)
+        s = chat_util.simple_html_repair(s)
+        s = chat_util.strip_script_tags(s)
+        s = chat_util.strip_li_tags(s)
+        s = chat_util.strip_body_tags(s)#7-27-05 mDuo13
+        s = chat_util.strip_misalignment_tags(s)#7-27-05 mDuo13
+        aliasInfo = self.GetName()
+        display_name = aliasInfo[0]
+        if aliasInfo[1] != 'Default':
+            defaultcolor = self.settings.get_setting("mytextcolor")
+            self.settings.set_setting("mytextcolor", aliasInfo[1])
+            self.set_colors()
+        newline = ''
+        #following added by mDuo13
+        #########post_msg() - other##########
+        if not myself and not send:
+            for plugin_fname in self.activeplugins.keys():
+                plugin = self.activeplugins[plugin_fname]
+                try:
+                    s = plugin.post_msg(s, myself)
+                except Exception, e:
+                    if str(e) != "'module' object has no attribute 'post_msg'":
+                        self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                        self.log.log("EXCEPTION: " + str(e), ORPG_GENERAL)
+        #end mDuo13 added code
+        if myself:
+            name = "<b>" + display_name + "</b>: "
+            s = self.colorize(self.mytextcolor, s)
+        else:
+            name = ""
+        if aliasInfo[1] != 'Default':
+            self.settings.set_setting("mytextcolor", defaultcolor)
+            self.set_colors()
+        #following line based on sourceforge patch #880403 from mDuo
+        # EDIT: Had to rework blank line check to handle malformed HTML throwing error.
+        #       this limits the effectiveness of this check -SD
+        lineHasText = 1
+        try:
+            lineHasText = strip_html(s).replace("&nbsp;","").replace(" ","").strip()!=""
+        except:
+            # HTML parser has errored out (most likely). Being as all we are doing is
+            # scanning for empty/blank lines anyway there is no harm in letting a
+            # troublesome message though. Worst case is a blank line to chat.
+            lineHasText = 1
+        if lineHasText:
+            #following added by mDuo13
+            if myself:
+                s2 = s
+                ########post_msg() - self #######
+                for plugin_fname in self.activeplugins.keys():
+                    plugin = self.activeplugins[plugin_fname]
+                    try:
+                        s2 = plugin.post_msg(s2, myself)
+                    except Exception, e:
+                        if str(e) != "'module' object has no attribute 'post_msg'":
+                            self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                            self.log.log("EXCEPTION: " + str(e), ORPG_GENERAL)
+                if s2 != "":
+                    #Italici the messages from tabbed whispers
+                    if self.type == WHISPER_TAB or self.type == GROUP_TAB or self.sendtarget == 'gm':
+                        s2 = s2 + '</i>'
+                        name = '<i>' + name
+                        if self.type == WHISPER_TAB:
+                            name += " (whispering): "
+                        elif self.type == GROUP_TAB:
+                            name += self.settings.get_setting("gwtext") + ' '
+                        elif self.sendtarget == 'gm':
+                            name += " (whispering to GM) "
+                    newline = self.TimeIndexString() + name +  s2 + "<br />"
+                    log( self.settings, name + s2 )
+            else:
+                newline = self.TimeIndexString() + name +  s + "<br />"
+                log( self.settings, name + s )
+        else:
+            send = False
+        newline = chat_util.strip_unicode(newline)
+        if self.lockscroll == 0:
+            self.chatwnd.AppendToPage(newline)
+            self.scroll_down()
+        else:
+            self.storedata.append(newline)
+        if send:
+            if self.type == MAIN_TAB and self.sendtarget == 'all':
+                self.send_chat_message(s)
+            elif self.type == MAIN_TAB and self.sendtarget == "gm":
+                the_gms = self.get_gms()
+                self.whisper_to_players(s, the_gms)
+            elif self.type == GROUP_TAB and WG_LIST.has_key(self.sendtarget):
+                members = []
+                for pid in WG_LIST[self.sendtarget]:
+                    members.append(str(WG_LIST[self.sendtarget][pid]))
+                self.whisper_to_players(self.settings.get_setting("gwtext") + s, members)
+            elif self.type == WHISPER_TAB:
+                self.whisper_to_players(s, [self.sendtarget])
+            elif self.type == NULL_TAB:
+                pass
+            else:
+                self.InfoPost("Failed to send message, unknown send type for this tab")
+        self.parsed=0
+        self.log.log("Exit chat_panel->Post(self, s, send, myself)", ORPG_DEBUG)
+
+    #
+    # TimeIndexString()
+    #
+    # time indexing for chat display only (don't log time indexing)
+    # added by Snowdog 4/04
+    def TimeIndexString(self):
+        self.log.log("Enter chat_panel->TimeIndexString(self)", ORPG_DEBUG)
+        try:
+            mtime = ""
+            if self.settings.get_setting('Chat_Time_Indexing') == "0":
+                pass
+            elif self.settings.get_setting('Chat_Time_Indexing') == "1":
+                mtime = time.strftime("[%I:%M:%S] ", time.localtime())
+            self.log.log("Exit chat_panel->TimeIndexString(self)", ORPG_DEBUG)
+            return mtime
+        except Exception, e:
+            self.log.log(traceback.format_exc(), ORPG_GENERAL)
+            self.log.log("EXCEPTION: " + str(e), ORPG_GENERAL)
+            return "[ERROR]"
+
+    ####  Post with parsing dice ####
+    def ParsePost(self, s, send=False, myself=False):
+        self.log.log("Enter chat_panel->ParsePost(self, s, send, myself)", ORPG_DEBUG)
+        s = self.NormalizeParse(s)
+        self.set_colors()
+        self.Post(s,send,myself)
+        self.log.log("Exit chat_panel->ParsePost(self, s, send, myself)", ORPG_DEBUG)
+
+    def NormalizeParse(self, s):
+        self.log.log("Enter chat_panel->NormalizeParse(self, s)", ORPG_DEBUG)
+        for plugin_fname in self.activeplugins.keys():
+            plugin = self.activeplugins[plugin_fname]
+            try:
+                s = plugin.pre_parse(s)
+            except Exception, e:
+                if str(e) != "'module' object has no attribute 'post_msg'":
+                    self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                    self.log.log("EXCEPTION: " + str(e), ORPG_GENERAL)
+        if self.parsed == 0:
+            s = self.ParseNode(s)
+            s = self.ParseDice(s)
+            s = self.ParseFilter(s)
+            self.parsed = 1
+        self.log.log("Exit chat_panel->NormalizeParse(self, s)", ORPG_DEBUG)
+        return s
+
+    def ParseFilter(self, s):
+        self.log.log("Enter chat_panel->ParseFilter(self, s)", ORPG_DEBUG)
+        s = self.GetFilteredText(s)
+        self.log.log("Exit chat_panel->ParseFilter(self, s)", ORPG_DEBUG)
+        return s
+
+    def ParseNode(self, s):
+        """Parses player input for embedded nodes rolls"""
+        self.log.log("Enter chat_panel->ParseNode(self, s)", ORPG_DEBUG)
+        cur_loc = 0
+        #[a-zA-Z0-9 _\-\.]
+        reg = re.compile("(!@([a-zA-Z0-9 _\-\./]+(::[a-zA-Z0-9 _\-\./]+)*)@!)")
+        matches = reg.findall(s)
+        for i in xrange(0,len(matches)):
+            newstr = self.ParseNode(self.resolve_nodes(matches[i][1]))
+            s = s.replace(matches[i][0], newstr, 1)
+        self.log.log("Exit chat_panel->ParseNode(self, s)", ORPG_DEBUG)
+        return s
+
+    def ParseDice(self, s):
+        """Parses player input for embedded dice rolls"""
+        self.log.log("Enter chat_panel->ParseDice(self, s)", ORPG_DEBUG)
+        reg = re.compile("\[([^]]*?)\]")
+        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:
+                s = s.replace("[" + matches[i] + "]", "<!-- Official Roll [" + newstr1 + "] => " + newstr + "-->" + newstr, 1)
+            else:
+                s = s.replace("[" + matches[i] + "]", "[" + newstr1 + "<!-- Official Roll -->] => " + newstr, 1)
+        self.log.log("Exit chat_panel->ParseDice(self, s)", ORPG_DEBUG)
+        return s
+
+    def PraseUnknowns(self, s):
+	# Uses a tuple. Usage: ?Label}dY. If no Label is assigned then use ?}DY
+        self.log.log("Enter chat_panel->PraseUnknowns(self, s)", ORPG_DEBUG)
+        newstr = "0"
+        reg = re.compile("(\?\{*)([a-zA-Z ]*)(\}*)")
+        matches = reg.findall(s)
+        for i in xrange(0,len(matches)):
+            lb = "Replace '?' with: "
+            if len(matches[i][0]):
+                lb = matches[i][1] + "?: "
+            dlg = wx.TextEntryDialog(self, lb, "Missing Value?")
+            dlg.SetValue('')
+            if matches[i][0] != '':
+                dlg.SetTitle("Enter Value for " + matches[i][1])
+            if dlg.ShowModal() == wx.ID_OK:
+                newstr = dlg.GetValue()
+            if newstr == '':
+                newstr = '0'
+            s = s.replace(matches[i][0], newstr, 1).replace(matches[i][1], '', 1).replace(matches[i][2], '', 1)
+            dlg.Destroy()
+        self.log.log("Exit chat_panel->PraseUnknowns(self, s)", ORPG_DEBUG)
+        return s
+
+    # This subroutine builds a chat display name.
+    #
+    def chat_display_name(self, player):
+        self.log.log("Enter chat_panel->chat_display_name(self, player)", ORPG_DEBUG)
+        if self.settings.get_setting("ShowIDInChat") == "0":
+            display_name = player[0]
+        else:
+            display_name = "("+player[2]+") " + player[0]
+        self.log.log("Exit chat_panel->chat_display_name(self, player)", ORPG_DEBUG)
+        return display_name
+
+    # This subroutine will get a hex color and return it, or return nothing
+    #
+    def get_color(self):
+        self.log.log("Enter chat_panel->get_color(self)", ORPG_DEBUG)
+        data = wx.ColourData()
+        data.SetChooseFull(True)
+        dlg = wx.ColourDialog(self, data)
+        if dlg.ShowModal() == wx.ID_OK:
+            data = dlg.GetColourData()
+            (red,green,blue) = data.GetColour().Get()
+            hexcolor = self.r_h.hexstring(red, green, blue)
+            dlg.Destroy()
+            self.log.log("Exit chat_panel->get_color(self) return hexcolor", ORPG_DEBUG)
+            return hexcolor
+        else:
+            dlg.Destroy()
+            self.log.log("Exit chat_panel->get_color(self) return None", ORPG_DEBUG)
+            return None
+    # def get_color - end
+
+    def replace_quotes(self, s):
+        self.log.log("Enter chat_panel->replace_quotes(self, s)", ORPG_DEBUG)
+        in_tag = 0
+        i = 0
+        rs = s[:]
+        for c in s:
+            if c == "<":
+                in_tag += 1
+            elif c == ">":
+                if in_tag:
+                    in_tag -= 1
+            elif c == '"':
+                if in_tag:
+                    rs = rs[:i] + "'" + rs[i+1:]
+            i += 1
+        self.log.log("Exit chat_panel->replace_quotes(self, s)", ORPG_DEBUG)
+        return rs
+
+    def resolve_loop(self, dom, nodeName, doLoop = False):
+        self.log.log("Enter chat_panel->resolve_loop(self, dom, nodeName)", ORPG_DEBUG)
+        for node in dom:
+            if node._get_tagName() != 'nodehandler':
+                continue
+            if doLoop and node.getAttribute('class') != 'textctrl_handler' and node.hasChildNodes():
+                (found, node) = self.resolve_loop(node.getChildren(), nodeName, doLoop)
+                if not found:
+                    continue
+            if node.getAttribute('name') != nodeName:
+                    continue
+            foundNode = node
+            self.log.log("Exit chat_panel->resolve_loop(self, dom, path) return (True, value)", ORPG_DEBUG)
+            return (True, foundNode)
+        self.log.log("Exit chat_panel->resolve_loop(self, dom, path) return (False, '')", ORPG_DEBUG)
+        return (False, '')
+
+    def resolve_nodes(self, s):
+        self.log.log("Enter chat_panel->resolve_nodes(self, s)", ORPG_DEBUG)
+        value = ""
+        node_path_list = s.split("::")
+        gametree = open_rpg.get_component('tree')
+        dom = gametree.master_dom.getChildren()
+        for nodeName in node_path_list:
+            (found, node) = self.resolve_loop(dom, nodeName)
+            if not found:
+                break
+            dom = node.getChildren()
+        if not found:
+            dom = gametree.master_dom.getChildren()
+            loop = False
+            if len(node_path_list) == 1:
+                loop = True
+            for nodeName in node_path_list:
+                (found, node) = self.resolve_loop(dom, nodeName, loop)
+                if not found:
+                    break
+                dom = node.getChildren()
+                loop = True
+        if found:
+            text = node.getElementsByTagName('text')
+            node = text[0]._get_firstChild()
+            value = node._get_nodeValue()
+        else:
+            value = s
+        self.log.log("Exit chat_panel->resolve_nodes(self, s)", ORPG_DEBUG)
+        return value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/chat/commands.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,739 @@
+# This class implements the basic chat commands available in the chat interface.
+#
+# Defines:
+#   __init__(self,chat)
+#   docmd(self,text)
+#   on_help(self)
+#   on_whisper(self,text)
+#
+
+
+import string
+import time
+import orpg.orpg_version
+import orpg.orpg_windows
+import traceback
+
+##--------------------------------------------------------------
+## dynamically loading module for extended developer commands
+## allows developers to work on new chat commands without
+## integrating them directly into the ORPG code allowing
+## updating of their code without merging changes
+## cmd_ext.py should NOT be included in the CVS or Actual Releases
+
+try:
+    import cmd_ext
+    print "Importing Developer Extended Command Set"
+except:
+    pass
+##----------------------------------------------------------------
+
+ANTI_LOG_CHAR = '!'
+
+class chat_commands:
+
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    def __init__(self,chat):
+        self.post = chat.Post
+        self.colorize = chat.colorize
+        self.session = chat.session
+        #self.send = chat.session.send
+        self.settings = chat.settings
+        self.chat = chat
+        self.cmdlist = {}
+        self.shortcmdlist = {}
+        self.defaultcmds()
+        self.defaultcmdalias()
+        # def __init__ - end
+        self.previous_whisper = []
+
+
+        # This subroutine will take a text string and attempt to match to a series
+        # of implemented emotions.
+        #
+        # !self : instance of self
+        # !text : string of text matching an implemented emotion
+    def addcommand(self, cmd, function, helpmsg):
+        if not self.cmdlist.has_key(cmd) and not self.shortcmdlist.has_key(cmd):
+            self.cmdlist[cmd] = {}
+            self.cmdlist[cmd]['function'] = function
+            self.cmdlist[cmd]['help'] = helpmsg
+            #print 'Command Added: ' + cmd
+
+    def addshortcmd(self, shortcmd, longcmd):
+        if not self.shortcmdlist.has_key(shortcmd) and not self.cmdlist.has_key(shortcmd):
+            self.shortcmdlist[shortcmd] = longcmd
+
+    def removecmd(self, cmd):
+        if self.cmdlist.has_key(cmd):
+            del self.cmdlist[cmd]
+        elif self.shortcmdlist.has_key(cmd):
+            del self.shortcmdlist[cmd]
+
+        #print 'Command Removed: ' + cmd
+
+
+    def defaultcmds(self):
+        self.addcommand('/help', self.on_help, '- Displays this help message')
+        self.addcommand('/version', self.on_version, ' - Displays current version of OpenRPG.')
+        self.addcommand('/me', self.chat.emote_message, ' - Alias for **yourname does something.**')
+        self.addcommand('/ignore', self.on_ignore, '[player_id,player_id,... | ignored_ip,ignored_ip,... | list] - Toggle ignore for user associated with that player ID. Using the IP will remove only not toggle.')
+        self.addcommand('/load', self.on_load, 'filename - Loads settings from another ini file from the myfiles directory.')
+        self.addcommand('/role', self.on_role, '[player_id = GM | Player | Lurker] - Get player roles from ther server, self.or change the role of a player.')
+        self.addcommand('/font', self.on_font, 'fontname - Sets the font.')
+        self.addcommand('/fontsize', self.on_fontsize, 'size - Sets the size of your fonts.  Recomended 8 or better for the size.')
+        self.addcommand('/close', self.on_close, 'Close the chat tab')
+        self.addcommand('/set', self.on_set, '[setting[=value]] - Displays one or all settings, self.or sets a setting.')
+        self.addcommand('/whisper', self.on_whisper, 'player_id_number, ... = message - Whisper to player(s). Can contain multiple IDs.')
+        self.addcommand('/gw', self.on_groupwhisper, 'group_name=message - Type /gw help for more information')
+        self.addcommand('/gm', self.on_gmwhisper, 'message - Whispers to all GMs in the room')
+        self.addcommand('/name', self.on_name, 'name - Change your name.')
+        self.addcommand('/time', self.on_time, '- Display the local and GMT time and date.')
+        self.addcommand('/status', self.on_status, 'your_status - Set your online status (afk,away,etc..).')
+        self.addcommand('/dieroller', self.on_dieroller, '- Set your dieroller or list the available rollers.')
+        self.addcommand('/log', self.on_log, '[ on | off | to <em>filename</em> ] - Check log state, additionally turn logging on, self.off, self.or set the log filename prefix.')
+        self.addcommand('/update', self.on_update, '[get] - Get the latest version of OpenRPG.')
+        self.addcommand('/moderate', self.on_moderate, '[ on | off ][player_id=on|off] - Show who can speak in a moderated room, self.or turn room moderation on or off.')
+        self.addcommand('/tab', self.invoke_tab, 'player_id - Creates a tab so you can whisper rolls to youror what ever')
+        self.addcommand('/ping', self.on_ping, '- Ask for a response from the server.')
+        self.addcommand('/admin', self.on_remote_admin, '- Remote admin commands')
+        self.addcommand('/description', self.on_description, 'message - Creates a block of text, used for room descriptions and such')
+        self.addcommand('/sound', self.on_sound, 'Sound_URL - Plays a sound for all clients in the room.')
+        self.addcommand('/purge', self.on_purge, 'This will clear the entire chat window')
+        self.addcommand('/advfilter', self.on_filter, 'This will toggle the Advanced Filter')
+
+    def defaultcmdalias(self):
+        self.addshortcmd('/?', '/help')
+        self.addshortcmd('/he', '/me')
+        self.addshortcmd('/she', '/me')
+        self.addshortcmd('/i', '/ignore')
+        self.addshortcmd('/w', '/whisper')
+        self.addshortcmd('/nick', '/name')
+        self.addshortcmd('/date', '/time')
+        self.addshortcmd('/desc', '/description')
+        self.addshortcmd('/d', '/description')
+
+        #This is just an example or a differant way the shorcmd can be used
+        self.addshortcmd('/sleep', '/me falls asleep')
+
+
+    def docmd(self,text):
+        cmdsearch = string.split(text,None,1)
+        cmd = string.lower(cmdsearch[0])
+        start = len(cmd)
+        end = len(text)
+        cmdargs = text[start+1:end]
+
+        if self.cmdlist.has_key(cmd):
+            self.cmdlist[cmd]['function'](cmdargs)
+        elif self.shortcmdlist.has_key(cmd):
+            self.docmd(self.shortcmdlist[cmd] + " " + cmdargs)
+        else:
+            msg = "Sorry I don't know what %s is!" % (cmd)
+            self.chat.InfoPost(msg)
+
+    def on_filter(self, cmdargs):
+        #print self.chat.advancedFilter
+        test = not self.chat.advancedFilter
+        #print test
+
+        for tab in self.chat.parent.whisper_tabs:
+            tab.advancedFilter = not self.chat.advancedFilter
+
+        for tab in self.chat.parent.null_tabs:
+            tab.advancedFilter = not self.chat.advancedFilter
+
+        for tab in self.chat.parent.group_tabs:
+            tab.advancedFilter = not self.chat.advancedFilter
+
+        if self.chat.parent.GMChatPanel != None:
+            self.chat.parent.GMChatPanel.advancedFilter = not self.chat.advancedFilter
+
+        self.chat.advancedFilter = not self.chat.advancedFilter
+
+        if self.chat.advancedFilter:
+            self.chat.InfoPost("Advanced Filtering has been turned On")
+        else:
+            self.chat.InfoPost("Advanced Filtering has been turned Off")
+
+    def on_purge(self, cmdargs):
+        self.chat.PurgeChat()
+        self.chat.InfoPost('Chat Buffer has been Purged!')
+
+    def on_sound(self, cmdargs):
+        if len(cmdargs) < 8:
+            self.chat.InfoPost("You must provide a URL for the file name, it does not work for just local sound files")
+            return
+        args = string.split(cmdargs, None, -1)
+
+        if args[0] == 'loop':
+            snd = args[1].replace('&', '&amp;')
+            loop = '1'
+        else:
+            snd = cmdargs.replace('&', '&amp;')
+            loop = ''
+
+        type = 'remote'
+
+        (name, ip, id, text_status, version, protocol_version, client_string, role) = self.session.get_my_info()
+        group_id = self.session.group_id
+        if (role != 'Lurker' and group_id != '0') or self.session.get_status() != 1:
+            try:
+                self.chat.sound_player.play(snd, type, loop)
+                if type == 'remote':
+                    snd_xml = '<sound url="' + snd + '" group_id="' + group_id + '" from="' + id + '" loop="' + str(loop) + '" />'
+                    self.session.send_sound(snd_xml)
+            except Exception, e:
+                print e
+                self.chat.InfoPost("Invalid sound file!")
+        elif role == 'Lurker':
+            self.chat.InfoPost("You must be a player or a GM to send a sound file!")
+        elif group_id == '0':
+            self.chat.InfoPost("You cannot send sound files to the lobby!")
+        else:
+            self.chat.InfoPost("Something dun fuckered up Frank!")
+
+    def on_version(self, cmdargs=""):
+        self.chat.InfoPost("Version is OpenRPG " + self.chat.version)
+
+    def on_load(self, cmdargs):
+        args = string.split(cmdargs,None,-1)
+        try:
+            self.settings.setup_ini(args[0])
+            self.settings.reload_settings(self.chat)
+            self.chat.InfoPost("Settings Loaded from file " + args[0] )
+        except Exception,e:
+            print e
+            self.chat.InfoPost("ERROR Loading settings")
+
+    def on_font(self, cmdargs):
+        try:
+            fontsettings = self.chat.set_default_font(fontname=cmdargs, fontsize=None)
+        except:
+            self.chat.InfoPost("ERROR setting default font")
+
+    def on_fontsize(self, cmdargs):
+        args = string.split(cmdargs,None,-1)
+        try:
+            fontsettings = self.chat.set_default_font(fontname=None, fontsize=int(args[0]))
+        except Exception, e:
+            print e
+            self.chat.InfoPost("ERROR setting default font size")
+
+    def on_close(self, cmdargs):
+        try:
+            chatpanel = self.chat
+            if (chatpanel.sendtarget == "all"):
+                chatpanel.InfoPost("Error:  cannot close public chat tab.")
+            else:
+                chatpanel.chat_timer.Stop()
+                chatpanel.parent.onCloseTab(0)
+        except:
+            self.chat.InfoPost("Error:  cannot close private chat tab.")
+
+    def on_time(self, cmdargs):
+        local_time = time.localtime()
+        gmt_time = time.gmtime()
+        format_string = "%A %b %d, %Y  %I:%M:%S%p"
+        self.chat.InfoPost("<br />Local: " + time.strftime(format_string)+\
+                           "<br />GMT: "+time.strftime(format_string,gmt_time))
+
+    def on_dieroller(self, cmdargs):
+        args = string.split(cmdargs,None,-1)
+        rm = self.chat.DiceManager
+        try:
+            rm.setRoller(args[0])
+            self.chat.SystemPost("You have changed your die roller to the <b>\"" + args[0] + "\"</b> roller.")
+            self.settings.set_setting('dieroller',args[0])
+        except Exception, e:
+            print e
+            self.chat.InfoPost("Available die rollers: " + str(rm.listRollers()))
+            self.chat.InfoPost("You are using the <b>\"" + rm.getRoller() + "\"</b> die roller.")
+
+    def on_ping(self, cmdargs):
+        ct = time.clock()
+        msg = "<ping player='"+self.session.id+"' time='"+str(ct)+"' />"
+        self.session.outbox.put(msg)
+
+    def on_log(self,cmdargs):
+        args = string.split(cmdargs,None,-1)
+        logfile = self.settings.get_setting( 'GameLogPrefix' )
+
+        if len( args ) == 0:
+            self.postLoggingState()
+        elif args[0] == "on" and logfile != '':
+            try:
+                while logfile[ 0 ] == ANTI_LOG_CHAR:
+                    #print logfile
+                    logfile = logfile[ 1: ]
+            except IndexError,e:
+                self.chat.SystemPost("log filename is blank, system will *not* be logging until a valid filename is specified" )
+                self.settings.set_setting( 'GameLogPrefix', logfile )
+                return
+            self.settings.set_setting( 'GameLogPrefix', logfile )
+            self.postLoggingState()
+        elif args[0] == "off":
+            logfile = ANTI_LOG_CHAR+logfile
+            self.settings.set_setting( 'GameLogPrefix', logfile )
+            self.postLoggingState()
+        elif args[0] == "to":
+            if len( args ) > 1:
+                logfile = args[1]
+                self.settings.set_setting( 'GameLogPrefix', logfile )
+            else:
+                self.chat.SystemPost('You must also specify a filename with the <em>/log to</em> command.' )
+            self.postLoggingState()
+        else:
+            self.chat.InfoPost("Unknown logging command, use 'on' or 'off'"  )
+
+    def postLoggingState( self ):
+        logfile = self.settings.get_setting( 'GameLogPrefix' )
+        try:
+            if logfile[0] != ANTI_LOG_CHAR:
+                comment = 'is'
+            else:
+                comment = 'is not'
+        except:
+            comment = 'is not'
+        suffix = time.strftime( '-%d-%m-%y.html', time.localtime( time.time() ) )
+        self.chat.InfoPost('Log filename is "%s%s", system is %s logging.' % (logfile, suffix, comment) )
+
+        # This subroutine will set the players netork status.
+        #
+        #!self : instance of self
+
+    def on_name(self, cmdargs):
+        #only 20 chars no more! :)
+        if cmdargs == "":
+            self.chat.InfoPost("**Incorrect syntax for name.")
+        else:
+            #txt = txt[:50]
+            self.settings.set_setting('player', cmdargs)
+            self.session.set_name(str(cmdargs))
+
+    # def on_status - end
+
+        # This subroutine will set the players netork status.
+        #
+        # !self : instance of self
+    def on_status(self, cmdargs):
+        if cmdargs ==  "":
+            self.chat.InfoPost("Incorrect synatx for status.")
+        else:
+        #only 20 chars no more! :)
+            txt = cmdargs[:20]
+            self.session.set_text_status(str(txt))
+    # def on_status - end
+
+    def on_set(self, cmdargs):
+        args = string.split(cmdargs,None,-1)
+        keys = self.settings.get_setting_keys()
+        #print keys
+        if len(args) == 0:
+            line = "<table border='2'>"
+            for m in keys:
+                line += "<tr><td>" + str(m) + "</td><td> " + str(self.settings.get_setting(m)) + "</td></tr>"
+            line += "</table>"
+            self.chat.InfoPost(line)
+        else:
+            split_name_from_data = cmdargs.find("=")
+            if split_name_from_data == -1:
+                for m in keys:
+                    if m == args[0]:
+                        return_string = "<table border='2'><tr><td>" + args[0] + "</td><td>"\
+                        + self.settings.get_setting(args[0]) + "</td></tr></table>"
+                        self.chat.InfoPost(return_string)
+            else:
+                name = cmdargs[:split_name_from_data].strip()
+                for m in keys:
+                    if m == name:
+                        setting = cmdargs[split_name_from_data+1:].strip()
+                        self.settings.set_setting(name,setting)
+                        return_string = name + " changed to " + setting
+                        self.chat.InfoPost(return_string)
+                        self.session.set_name(self.settings.get_setting("player"))
+                        self.chat.set_colors()
+                        self.chat.set_buffersize()
+
+        # This subroutine will display the correct usage of the different emotions.
+        #
+        #!self : instance of self
+
+    def on_help(self, cmdargs=""):
+        cmds = self.cmdlist.keys()
+        cmds.sort()
+        shortcmds = self.shortcmdlist.keys()
+        shortcmds.sort()
+        msg = '<br /><b>Command Alias List:</b>'
+        for shortcmd in shortcmds:
+            msg += '<br /><b><font color="#0000CC">%s</font></b> is short for <font color="#000000">%s</font>' % (shortcmd, self.shortcmdlist[shortcmd])
+        msg += '<br /><br /><b>Command List:</b>'
+        for cmd in cmds:
+            msg += '<br /><b><font color="#000000">%s</font></b>' % (cmd)
+            for shortcmd in shortcmds:
+                if self.shortcmdlist[shortcmd] == cmd:
+                    msg += ', <b><font color="#0000CC">%s</font></b>' % (shortcmd)
+            msg += ' %s' % (self.cmdlist[cmd]['help'])
+
+        self.chat.InfoPost(msg)
+
+        # This subroutine will either show the list of currently ignored users
+        # !self : instance of self
+        # !text : string that is comprised of a list of users to toggle the ignore flag
+
+    def on_ignore(self, cmdargs):
+        args = string.split(cmdargs,None,-1)
+        (ignore_list, ignore_name) = self.session.get_ignore_list()
+        ignore_output = self.colorize(self.chat.syscolor,"<br /><u>Player IDs Currently being Ignored:</u><br />")
+        if cmdargs == "":
+            if len(ignore_list) == 0:
+                ignore_output += self.colorize(self.chat.infocolor,"No players are currently being ignored.<br />")
+            else:
+                for m in ignore_list:
+                    ignore_txt = m + " " + ignore_name[ignore_list.index(m)] + "<br />"
+                    ignore_output += self.colorize(self.chat.infocolor,ignore_txt)
+            self.chat.Post(ignore_output)
+        else:
+            players = cmdargs.split(",")
+            for m in players:
+                try:
+                    id = `int(m)`
+                    (result, id, name) = self.session.toggle_ignore(id)
+                    if result == 0:
+                        self.chat.InfoPost("Player " + name + " with ID:" + id + " no longer ignored")
+                    if result == 1:
+                        self.chat.InfoPost("Player " + name + " with ID:" + id + " now being ignored")
+                except:
+                    self.chat.InfoPost(m + " was ignored because it is an invalid player ID")
+                    traceback.print_exc()
+
+    def on_role(self, cmdargs):
+        if cmdargs == "":
+            self.session.display_roles()
+            return
+        delim = cmdargs.find("=")
+        if delim < 0:
+            self.chat.InfoPost("**Incorrect synatax for Role." + str(delim))
+            return
+        player_ids = string.split(cmdargs[:delim],",")
+        role = cmdargs[delim+1:].strip()
+        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"
+            try:
+                role_pwd = self.session.orpgFrame_callback.password_manager.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)
+            except:
+                traceback.print_exc()
+#        return
+
+        # This subroutine implements the whisper functionality that enables a user
+        # to whisper to another user.
+        #
+        # !self : instance of self
+        # !text : string that is comprised of a list of users and the message to
+        #whisper.
+
+    def on_whisper(self, cmdargs):
+        delim = cmdargs.find("=")
+
+        if delim < 0:
+            if self.previous_whisper:
+                player_ids = self.previous_whisper
+            else:
+                self.chat.InfoPost("**Incorrect syntax for whisper." + str(delim))
+                return
+        else:
+            player_ids = string.split(cmdargs[:delim], ",")
+        self.previous_whisper = player_ids
+        mesg = string.strip(cmdargs[delim+1:])
+        self.chat.whisper_to_players(mesg,player_ids)
+
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+    def on_groupwhisper(self, cmdargs):
+        args = string.split(cmdargs,None,-1)
+        delim = cmdargs.find("=")
+
+        if delim > 0:
+            group_ids = string.split(cmdargs[:delim], ",")
+        elif args[0] == "add":
+            if not orpg.player_list.WG_LIST.has_key(args[2]):
+                orpg.player_list.WG_LIST[args[2]] = {}
+            orpg.player_list.WG_LIST[args[2]][int(args[1])] = int(args[1])
+            return
+        elif args[0] == "remove" or args[0] == "delete":
+            del orpg.player_list.WG_LIST[args[2]][int(args[1])]
+            if len(orpg.player_list.WG_LIST[args[2]]) == 0:
+                del orpg.player_list.WG_LIST[args[2]]
+            return
+        elif args[0] == "create" or args[0] == "new_group":
+            if not orpg.player_list.WG_LIST.has_key(args[1]):
+                orpg.player_list.WG_LIST[args[1]] = {}
+            return
+        elif args[0] == "list":
+            if orpg.player_list.WG_LIST.has_key(args[1]):
+                for n in orpg.player_list.WG_LIST[args[1]]:
+                    player = self.session.get_player_info(str(n))
+                    self.chat.InfoPost(str(player[0]))
+            else:
+                self.chat.InfoPost("Invalid Whisper Group Name")
+            return
+        elif args[0] == "clear":
+            if orpg.player_list.WG_LIST.has_key(args[1]):
+                orpg.player_list.WG_LIST[args[1]].clear()
+            else:
+                self.chat.InfoPost("Invalid Whisper Group Name")
+            return
+        elif args[0] == "clearall":
+            orpg.player_list.WG_LIST.clear()
+            return
+        else:
+            self.chat.InfoPost("<b>/gw add</b> (player_id) (group_name) - Adds [player_id] to [group_name]")
+            self.chat.InfoPost("<b>/gw remove</b> (player_id) (group_name) - Removes [player_id] from [group_name]")
+            self.chat.InfoPost("<b>/gw</b> (group_name)<b>=</b>(message) - Sends [message] to [group_name]")
+            self.chat.InfoPost("<b>/gw create</b> (group_name) - Creates a whisper group called [group_name]")
+            self.chat.InfoPost("<b>/gw list</b> (group_name) - Lists all players in [group_name]")
+            self.chat.InfoPost("<b>/gw clear</b> (group_name) - Removes all players from [group_name]")
+            self.chat.InfoPost("<b>/gw clearall</b> - Removes all existing whisper groups")
+            return
+        msg = string.strip(cmdargs[delim+1:])
+        for gid in group_ids:
+            idList = ""
+            for n in orpg.player_list.WG_LIST[gid]:
+                if idList == "":
+                    idList = str(n)
+                else:
+                    idList = str(n) + ", " + idList
+            self.on_whisper(idList + "=" + self.settings.get_setting("gwtext") + msg)
+
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+
+    def on_gmwhisper(self, cmdargs):
+        if cmdargs == "":
+            self.chat.InfoPost("**Incorrect syntax for GM Whisper.")
+        else:
+            the_gms = self.chat.get_gms()
+            if len(the_gms):
+                gmstring = ""
+                for each_gm in the_gms:
+                    if gmstring != "":
+                        gmstring += ","
+                    gmstring += each_gm
+                self.on_whisper(gmstring + "=" + cmdargs)
+            else:
+                self.chat.InfoPost("**No GMs to Whisper to.")
+
+    def on_moderate(self, cmdargs):
+        if cmdargs != "":
+            pos = cmdargs.find("=")
+            if (pos < 0):
+                plist = ""
+                if cmdargs.lower() == "on":
+                    action = "enable"
+                elif cmdargs.lower() == "off":
+                    action="disable"
+                else:
+                    self.chat.InfoPost("Wrong syntax for moderate command!")
+                    return
+            else:
+                plist = string.strip(cmdargs[:pos])
+                tag = string.strip(cmdargs[pos+1:])
+                if tag.lower() == "on":
+                    action = "addvoice"
+                elif tag.lower() == "off":
+                    action = "delvoice"
+                else:
+                    self.chat.InfoPost("Wrong syntax for moderate command!")
+                    return
+            pwd = self.session.orpgFrame_callback.password_manager.GetPassword("admin",int(self.session.group_id))
+            if pwd != None:
+                msg = "<moderate"
+                msg += " action = '" + action + "'"
+                msg +=" from = '" + self.session.id + "' pwd='" + pwd + "'"
+                if (plist != ""):
+                    msg += " users='"+plist+"'"
+                msg += " />"
+                self.session.outbox.put(msg)
+            pass
+        else:
+            msg = "<moderate action='list' from='"+self.session.id+"' />"
+            self.session.outbox.put(msg)
+        self.session.update()
+
+    def on_update(self, cmdargs):
+        self.chat.InfoPost("This command is no longer valid")
+
+    def on_description(self, cmdargs):
+        if len(cmdargs) <= 0:
+            self.chat.InfoPost("**No description text to display." + str(delim))
+            return
+        mesg = "<table bgcolor='#c0c0c0' border='3' cellpadding='5' cellspacing='0' width='100%'><tr><td><font color='#000000'>"
+        mesg += string.strip(cmdargs)
+        mesg += "</font></td></tr></table>"
+        self.chat.Post(mesg)
+        self.chat.send_chat_message(mesg)
+
+    def invoke_tab(self, cmdargs):
+        ######START mDuo13's Tab Initiator########
+        try:
+            int(cmdargs)
+            playerid = cmdargs.strip()
+            # Check to see if parent notebook already has a private tab for player
+            for panel in self.chat.parent.whisper_tabs:
+                if (panel.sendtarget == playerid):
+                    self.chat.Post("Cannot invoke tab: Tab already exists.")
+                    return
+            try:
+                displaypanel = self.chat.parent.create_whisper_tab(playerid)
+            except:
+                self.chat.Post("That ID# is not valid.")
+                return
+            nidx = self.chat.parent.get_tab_index(displaypanel)
+            self.chat.parent.newMsg(nidx)
+            return
+        except:
+            displaypanel = self.chat.parent.create_null_tab(cmdargs)
+            nidx = self.chat.parent.get_tab_index(displaypanel)
+            self.chat.parent.newMsg(nidx)
+            return
+        #######END mDuo13's Tab Initiator#########
+
+    def on_remote_admin(self, cmdargs):
+        args = string.split(cmdargs,None,-1)
+        #handles remote administration commands
+        try:
+            pass_state = 0
+            pwd = self.session.orpgFrame_callback.password_manager.GetSilentPassword("server")
+            if  pwd != None:
+                pass_state = 1
+            else: pwd = "<i>[NONE]</i>"
+
+            if len( args ) == 0:
+                #raw command return state info
+                msg = "<br /><b>Remote Administrator Config:</b>"
+                if pass_state != 1 : msg += " Password not set. Remote admin functions disabled<br />"
+                else:
+                    msg += " Enabled. Using password \""+pwd+"\"<br />"
+                self.chat.SystemPost(msg)
+                return
+
+            if pass_state != 1 and args[0] != "set":
+                #no commands under this point will execute unless an admin password has been previously set
+                self.chat.SystemPost("Command ignored. No remote administrator password set!!")
+                return
+            msgbase = "<admin id=\""+self.session.id+"\" group_id=\""+self.session.group_id+"\" pwd=\""+pwd+"\" "
+            if args[0] == "set":
+                if len( args ) > 1:
+                    self.session.orpgFrame_callback.password_manager.server = str( args[1] )
+                    self.chat.SystemPost( "Remote administration commands using password: \""+str(self.session.orpgFrame_callback.password_manager.GetSilentPassword("server"))+"\"" )
+                else:
+                    pwd = self.session.orpgFrame_callback.password_manager.GetPassword("server")
+                    if pwd != None:
+                        self.chat.SystemPost( "Remote administration commands using password: \""+pwd+"\"" )
+
+            elif args[0] == "ban":
+                #Ban a player from the server
+                msg = msgbase + ' cmd="ban" bid="' + str(args[1]) + '" />'
+                self.session.outbox.put(msg)
+
+            elif args[0] == "banip":
+                #Ban a player from the server
+                try:
+                    bname = str(args[2])
+                except:
+                    bname = 'Unknown'
+                msg = msgbase + ' cmd="banip" bip="' + str(args[1]) + '" bname="' + bname + '"/>'
+                self.session.outbox.put(msg)
+
+            elif args[0] == "unban":
+                #remove a group from the server and drop all players within the group
+                msg = msgbase + ' cmd="unban" ip="' + str(args[1]) + '" />'
+                self.session.outbox.put(msg)
+
+            elif args[0] == "banlist":
+                #remove a group from the server and drop all players within the group
+                msg = msgbase + ' cmd="banlist" />'
+                self.session.outbox.put(msg)
+
+            elif args[0] == "help":
+                #request help from server
+                msg = msgbase + " cmd=\"help\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "nameroom":
+                #reqest room renaming on server
+                msg = msgbase + " cmd=\"nameroom\" rmid=\""+ str(args[1])+"\" name=\""+ string.join(args[2:])+"\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "roompasswords":
+                #reqest room renaming on server
+                msg = msgbase + " cmd=\"roompasswords\"/>"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "message":
+                #send message to a specific player on the server via the system administrator
+                msg = msgbase + " cmd=\"message\" to_id=\""+ str(args[1])+"\" msg=\""+ string.join(args[2:])+"\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "broadcast":
+                #send a message to all players on server from the system administrator
+                msg = msgbase + " cmd=\"broadcast\" msg=\""+ string.join(args[1:])+"\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "killgroup":
+                #remove a group from the server and drop all players within the group
+                msg = msgbase + " cmd=\"killgroup\" gid=\""+ str(args[1])+"\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "uptime":
+                #request uptime report from server
+                msg = msgbase + " cmd=\"uptime\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "createroom":
+                #request creation of a (temporary) persistant room
+                if len(args) < 2:
+                    self.chat.SystemPost( "You must supply a name and boot password at least. <br />/admin createroom &lt;name&gt; &lt;boot password&gt; [password]" )
+                    return
+                if len(args) < 3:
+                    self.chat.SystemPost( "You must supply a boot password also.<br />/admin createroom &lt;name&gt; &lt;boot password&gt; [password]" )
+                    return
+                if len(args) < 4: args.append("")
+                msg = msgbase + " cmd=\"createroom\" name=\""+str(args[1])+"\" boot=\""+ str(args[2])+"\" pass=\""+ str(args[3])+"\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "passwd":
+                #request boot password change on a room
+                msg = msgbase + " cmd=\"passwd\" gid=\""+str(args[1])+"\" pass=\""+ str(args[2])+"\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "list":
+                #request a list of rooms and players from server
+                msg = msgbase + " cmd=\"list\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "killserver":
+                #remotely kill the server
+                msg = msgbase + " cmd=\"killserver\" />"
+                self.session.outbox.put(msg)
+
+            elif args[0] == "savemaps":
+                msg = msgbase + ' cmd="savemaps" />'
+                self.session.outbox.put(msg)
+
+            else:
+                self.chat.InfoPost("Unknown administrator command"  )
+        except:
+            self.chat.InfoPost("An error has occured while processing a Remote Administrator command!")
+            traceback.print_exc()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/HOWTO.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,72 @@
+HOW TO CREATE A NEW DIE ROLLER
+
+So you want a make a new roller or add a new option? here's a short guide.
+
+
+Step 1:  Create a new die roller sub class.
+
+You need to derive a new die roller class from an existing die roller class.  
+Most likely, this will be the std die roller class. 
+
+The basics would look like this:
+
+class new_roller(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+        .....
+
+    ....
+
+Step 2: Implement new methods and/or override existing ones.
+
+Now, you just need to implement any new die options and override any 
+existing ones that you want to act differently.  The most common options 
+to override are the sum and __str__ functions.  Sum is used to determine 
+the result of the rolls and __str__ is used to display the results in 
+a user friendly string.
+
+For example:
+
+class new_roller(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+        .....
+
+    def myoption(self,param):
+        ....
+
+    def sum(self):
+        ....
+
+    def __str__(self):
+        ....
+
+REMEMBER!
+Always return an instance of your die roller for each option expect str and sum.
+
+
+Step 3:
+
+Modify Utils.py
+
+You need to make some minor modifications to utils.py to facilitate 
+your new roller.  You need to a) add an import call for your roller, 
+and b) add your roller to the list of available rollers. 
+
+For example:
+
+from die import *
+# add addtional rollers here
+from myroller import *
+....
+
+rollers = ['std','wod','d20','myroller']
+
+Step 4:  You're done! 
+
+Test it and make sure it works.  When you think its done, send it to 
+the openrpg developers and they might include it in future releases.
+
+-Chris Davis
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+__all__ = ['dice', 'rollers', 'die', 'd20', 'std', 'hackmaster', 'hero', 'shadowrun', 'sr4', 'srex', 'utils', 'wod', 'wodex', 'utils']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/d20.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,111 @@
+# (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: d20.py
+# Author: OpenRPG Dev Team
+# Maintainer:
+# Version:
+#   $Id: d20.py,v 1.9 2006/11/04 21:24:19 digitalxero Exp $
+#
+# Description: d20 die roller
+#
+from die import *
+
+__version__ = "$Id: d20.py,v 1.9 2006/11/04 21:24:19 digitalxero Exp $"
+
+# d20 stands for "d20 system" not 20 sided die :)
+
+class d20(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+
+# these methods return new die objects for specific options
+
+    def attack(self,AC,mod,critical):
+        return d20attack(self,AC,mod,critical)
+
+    def dc(self,DC,mod):
+        return d20dc(self,DC,mod)
+
+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)
+
+    def __str__(self):
+        myStr = "[" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr += "] = (" + str(self.sum()) + ")"
+
+        myStr += " vs DC " + str(self.DC)
+
+        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()
+
+    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 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 __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)
+
+        if self.is_critical():
+            myStr += " Critical"
+
+        if self.is_hit():
+            myStr += " Hit!"
+        else:
+            myStr += " Miss!"
+
+        return myStr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/die.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,519 @@
+#!/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: die.py
+# Author: Andrew Bennett
+# Maintainer:
+# Version:
+#   $Id: die.py,v 1.13 2007/03/13 17:53:42 digitalxero Exp $
+#
+# Description: This class is used to make working with dice easier
+#
+
+__version__ = "$Id: die.py,v 1.13 2007/03/13 17:53:42 digitalxero Exp $"
+
+
+import random
+import UserList
+import copy
+#import string
+
+class die_base(UserList.UserList):
+
+    def __init__(self,source = []):
+        if isinstance(source, (int, float, basestring)):
+            s = []
+            s.append(di(source))
+        else:
+            s = source
+        UserList.UserList.__init__(self,s)
+
+
+    def sum(self):
+        s = 0
+        for a in self.data:
+            s += int(a)
+        return s
+
+    def __lshift__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            o = other
+        elif hasattr(other,"sum"):
+            o = other.sum()
+        else:
+            return None
+
+        result = []
+        for die in self:
+            if die < o:
+                result.append(die)
+        return self.__class__(result)
+
+    def __rshift__(self,other):
+
+        if type(other) == type(3) or type(other) == type(3.0):
+            o = other
+        elif hasattr(other,"sum"):
+            o = other.sum()
+        else:
+            return None
+
+        result = []
+        for die in self:
+            if die > o:
+                result.append(die)
+        return self.__class__(result)
+
+    def __rlshift__(self,other):
+        return self.__rshift__(other)
+
+    def __rrshift__(self,other):
+        return self.__lshift__(other)
+
+
+    def __str__(self):
+        if len(self.data) > 0:
+            myStr = "[" + str(self.data[0])
+            for a in self.data[1:]:
+                myStr += ","
+                myStr += str(a)
+            myStr += "] = (" + str(self.sum()) + ")"
+        else:
+            myStr = "[] = (0)"
+        return myStr
+
+    def __lt__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return (self.sum() < other)
+        elif hasattr(other,"sum"):
+            return  (self.sum() < other.sum())
+        else:
+            return UserList.UserList.__lt__(self,other)
+
+    def __le__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return (self.sum() <= other)
+        elif hasattr(other,"sum"):
+            return  (self.sum() <= other.sum())
+        else:
+            return UserList.UserList.__le__(self,other)
+
+    def __eq__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return (self.sum() == other)
+        elif hasattr(other,"sum"):
+            return  (self.sum() == other.sum())
+        else:
+            return UserList.UserList.__eq__(self,other)
+
+    def __ne__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return (self.sum() != other)
+        elif hasattr(other,"sum"):
+            return  (self.sum() != other.sum())
+        else:
+            return UserList.UserList.__ne__(self,other)
+
+    def __gt__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return (self.sum() > other)
+        elif hasattr(other,"sum"):
+            return  (self.sum() > other.sum())
+        else:
+            return UserList.UserList.__gt__(self,other)
+
+    def __ge__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return (self.sum() >= other)
+        elif hasattr(other,"sum"):
+            return  (self.sum() >= other.sum())
+        else:
+            return UserList.UserList.__ge__(self,other)
+
+    def __cmp__(self,other):
+        #  this function included for backwards compatibility
+        #  As of 2.1, lists implement the "rich comparison"
+        #  methods overloaded above.
+        if type(other) == type(3) or type(other) == type(3.0):
+            return cmp(self.sum(), other)
+        elif hasattr(other,"sum"):
+            return  cmp(self.sum(), other.sum())
+        else:
+            return UserList.UserList.__cmp__(self,other)
+
+
+    def __rcmp__(self,other):
+        return self.__cmp__(other)
+
+    def __add__(self,other):
+        mycopy = copy.deepcopy(self)
+        if type(other) == type(3) or type(other) == type(3.0):
+            #if other < 0:
+            #    return self.__sub__(-other)
+            #other = [di(other,other)]
+            other = [static_di(other)]
+            #return self.sum() + other
+
+        elif type(other) == type("test"):
+            return self
+        mycopy.extend(other)
+        #result = UserList.UserList.__add__(mycopy,other)
+        return mycopy
+
+    def __iadd__(self,other):
+        return self.__add__(other)
+
+    def __radd__(self,other):
+        mycopy = copy.deepcopy(self)
+        if type(other) == type(3) or type(other) == type(3.0):
+            new_die = di(0)
+            new_die.set_value(other)
+            other = new_die
+        mycopy.insert(0,other)
+        return mycopy
+
+    def __int__(self):
+        return self.sum()
+
+    def __sub__(self,other):
+        mycopy = copy.deepcopy(self)
+        if type(other) == type(3) or type(other) == type(3.0):
+            neg_die = static_di(-other)
+            #neg_die.set_value(-other)
+            other = [neg_die]
+            #return self.sum() - other
+        else:
+            other = -other
+        mycopy.extend(other)
+        return mycopy
+
+    def __rsub__(self,other):
+        mycopy = -copy.deepcopy(self)
+        #print type(other)
+        if type(other) == type(3) or type(other) == type(3.0):
+            new_die = di(0)
+            new_die.set_value(other)
+            other = new_die
+        mycopy.insert(0,other)
+        return mycopy
+
+    def __isub__(self,other):
+        return self.__sub__(other)
+
+    def __mul__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.sum() * other
+        elif hasattr(other,"sum"):
+            return other.sum() * self.sum()
+        else:
+            return UserList.UserList.__mul__(self,other)
+
+    def __rmul__(self,other):
+        return self.__mul__(other)
+
+    def __div__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return float(self.sum()) / other
+        elif hasattr(other,"sum"):
+            return  float(self.sum()) / other.sum()
+        else:
+            return UserList.UserList.__div__(self,other)
+
+    def __rdiv__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return other / float(self.sum())
+        elif hasattr(other,"sum"):
+            return  other.sum() / float(self.sum())
+        else:
+            return UserList.UserList.__rdiv__(self,other)
+
+    def __mod__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.sum()%other
+        elif hasattr(other,"sum"):
+            return  self.sum() % other.sum()
+        else:
+            return UserList.UserList.__mod__(self,other)
+
+    def __rmod__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return other % self.sum()
+        elif hasattr(other,"sum"):
+            return  other.sum() % self.sum()
+        else:
+            return UserList.UserList.__rmod__(self,other)
+
+    def __neg__(self):
+        for i in range(len(self.data)):
+            self.data[i] = -self.data[i]
+        return self
+
+    def __pos__(self):
+        for i in range(len(self.data)):
+            self.data[i] = +self.data[i]
+        return self
+
+    def __abs__(self):
+        for i in range(len(self.data)):
+            self.data[i] = abs(self.data[i])
+        return self
+        #return abs(self.sum())
+
+    def __pow__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.sum() ** other
+        elif hasattr(other,"sum"):
+            return  self.sum() ** other.sum()
+        else:
+            return UserList.UserList.__pow__(self,other)
+
+
+    def __rpow__(self,other):
+        #  We're overloading exponentiation of ints to create "other" number of dice
+
+        if other >= 1:
+            result = self.__class__(self[0].sides)
+            for t in range(other-1):
+                result+=self.__class__(self[0].sides)
+        else:
+            result = None
+
+        return result
+
+### di class to handle actual dice
+
+class di:
+    def __init__(self,sides,min=1):
+        self.sides = sides
+        self.history = None
+        self.value = None
+        self.target = None
+        self.roll(min)
+
+    def __str__(self):
+        if len(self.history) > 1:
+            return str(self.history)
+        else:
+            return str(self.value)
+
+    def __neg__(self):
+        self.value = -self.value
+        for i in range(len(self.history)):
+            self.history[i] = -self.history[i]
+        return self
+
+    def __pos__(self):
+        self.value = +self.value
+        for i in range(len(self.history)):
+            self.history[i] = +self.history[i]
+        return self
+
+    def __abs__(self):
+        self.value = abs(self.value)
+        for i in range(len(self.history)):
+            self.history[i] = abs(self.history[i])
+        return self
+
+    def __repr__(self):
+        if len(self.history) > 1:
+            return str(self.history)
+        else:
+            return str(self.value)
+
+    def __int__(self):
+        return self.value
+
+
+    def __lt__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.value < other
+        elif hasattr(other,"value"):
+            return self.value < other.value
+        else:
+            return self < other
+
+    def __le__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.value <= other
+        elif hasattr(other,"value"):
+            return self.value <= other.value
+        else:
+            return self <= other
+
+    def __eq__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.value == other
+        elif hasattr(other,"value"):
+            return self.value == other.value
+        else:
+            return self == other
+
+    def __ne__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.value != other
+        elif hasattr(other,"value"):
+            return self.value != other.value
+        else:
+            return self != other
+
+    def __gt__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.value > other
+        elif hasattr(other,"value"):
+            return self.value > other.value
+        else:
+            return self > other
+
+    def __ge__(self,other):
+        if type(other) == type(3) or type(other) == type(3.0):
+            return self.value >= other
+        elif hasattr(other,"value"):
+            return self.value >= other.value
+        else:
+            return self >= other
+
+    def __cmp__(self,other):
+        #  this function included for backwards compatibility
+#  As of 2.1, lists implement the "rich comparison"
+#  methods overloaded above.
+        if type(other) == type(3) or type(other) == type(3.0):
+            return cmp(self.value, other)
+        elif hasattr(other,"value"):
+            return cmp(self.value, other.value)
+        else:
+            return cmp(self,other)
+
+    def roll(self,min=1):
+        if isinstance(self.sides, basestring) and self.sides.lower() == 'f':
+            self.value = random.randint(-1, 1)
+        else:
+            #self.value = random.randint(min, self.sides)
+            self.value = int(random.uniform(min, self.sides+1))
+        self.history = []
+        self.history.append(self.value)
+
+    def extraroll(self):
+        if isinstance(self.sides, basestring) and self.sides.lower() == 'f':
+            result = random.randint(-1, 1)
+        else:
+            #result = random.randint(1, self.sides)
+            result = int(random.uniform(1,self.sides+1))
+
+        self.value += result
+        self.history.append(result)
+
+    def lastroll(self):
+        return self.history[len(self.history)-1]
+
+    def set_value(self,value):
+        self.value = value
+        self.history = []
+        self.history.append(self.value)
+
+    def modify(self,mod):
+        self.value += mod
+        self.history.append(mod)
+
+    def gethistory(self):
+        return self.history[:]
+
+class static_di(di):
+    def __init__(self,value):
+        di.__init__(self,value,value)
+        self.set_value(value)
+
+
+class std(die_base):
+    def __init__(self,source=[]):
+        die_base.__init__(self,source)
+
+    #  Examples of adding member functions through inheritance.
+
+    def ascending(self):
+        result = self[:]
+        result.sort()
+        return result
+
+    def descending(self):
+        result = self[:]
+        result.sort()
+        result.reverse()
+        return result
+
+    def takeHighest(self,num_dice):
+        return self.descending()[:num_dice]
+
+    def takeLowest(self,num_dice):
+        return self.ascending()[:num_dice]
+
+    def extra(self,num):
+        for i in range(len(self.data)):
+            if self.data[i].lastroll() >= num:
+                self.data[i].extraroll()
+        return self
+
+    def open(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.open(num)
+
+    def minroll(self,min):
+        for i in range(len(self.data)):
+            if self.data[i].lastroll() < min:
+                self.data[i].roll(min)
+        return self
+
+    def each(self,mod):
+        mod = int(mod)
+        for i in range(len(self.data)):
+            self.data[i].modify(mod)
+        return self
+
+
+    def vs(self, target):
+        for dn in self.data:
+            dn.target = target
+        return self
+
+
+    ## If we are testing against a saving throw, we check for
+    ## greater than or equal to against the target value and
+    ## we only return the number of successful saves.  A negative
+    ## value will never be generated.
+    def sum(self):
+        retValue = 0
+        for dn in self.data:
+            setValue = reduce( lambda x, y : int(x)+int(y), dn.history )
+            if dn.target:
+                if setValue >= dn.target:
+                    retValue += 1
+
+            else:
+                retValue += setValue
+
+        return retValue
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/dieroller.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,309 @@
+The New Dicing System:  A Proposal for OpenRPG
+----------------------------------------------
+
+The current dice system for OpenRPG has several limitations.  Foremost
+among these are the fact that adding a new, non-standard dicing mechanism
+requires editing of the basic dice code.  There are several secondary 
+limitations, such as the fact that while the dice system can handle math,
+it cannot be used as a calculator -- it will not allow expressions that do
+not involve dice.
+
+This proposal is for a new dicing system to replace the current one in
+OpenRPG.  Since the dicing system is something that users will interact
+with frequently, a new system needs to be considered carefully.  This
+document attempts to describe the new dicing system so that such 
+consideration can be given to it.  It is expected that this document will
+grow and change as it is scrutinized.
+
+
+Design goals for this dicing system:
+
+ 1. Should be easy for new users to get started with, based on knowing
+    standard RPG dice notation (NdX) and basic math.
+
+ 2. Should, as far as practical, maintain compatibility with existing
+    character sheets, etc., that use the current dice system.
+
+ 3. Should allow users to create new dice types and new ways of 
+    counting dice.  Ideally, this should not require programming, except
+    in exceptional cases.
+
+ 4. The dice system should be usable for doing basic math that does
+    not involve dice.
+
+ 5. The dice system should be able to handle most current RPG dicing
+    systems.
+
+Things this dicing system is designed to NOT do:
+
+ 1. Be a programming language.  There are no facilities in it for 
+    user input, output formatting, loops, if-then-else, or similar
+    things.  If these are desired for something involving dice, an
+    appropriate node and nodehandler can be created.
+
+ 2. Handle all theoretically possible dicing systems without the
+    need for programming plugins.  First off, this is impossible.  
+    Second, even making an attempt to would require supporting 
+    dicing methods that don't actually turn up in any real game.
+
+ 3. Handle floating-point math.  I don't know of any systems that
+    use it in their dice schemes right now.  If there are some,
+    we might have to consider adding it.
+
+
+Syntax Specification 
+
+What follows is a BNF specification for the proposed dicing system, with
+explanatory text interleaved.  At the end of this document is a copy of
+the BNF with no explanations, for those who would like to look at it "all
+together".  Note that BNF describes only syntax, and not semantics; thus,
+while anything generated with this grammar should be syntactically correct,
+that doesn't mean it will make sense or be allowed.
+
+
+dice string ::= <expression>
+                <expression> of <comparison> | <comparison>
+
+This is the top level.  The major thing of note here is that comparisons
+only occur at this level.  This is intentional; the result of a comparison
+is a boolean true/false flag rather than a number.  Thus, it makes no 
+sense to allow people to perform further numerical operations on the 
+result of a comparison.  Systems where dice are triggered by the results 
+of other dice are left for the realm of plugins.
+
+                
+comparison ::= <expression> <relation> <expression>
+
+expression ::= <factor> | <factor> <low-op> <factor>
+
+The separation into "low-op" and "high-op" of the operators is to allow
+order of operations to be handled more easily.  Syntactically, it's not
+really necessary, but it should be helpful in implementation.
+
+
+factor :: = <term> | <term> <high-op> <term> |
+            <multi-dice> | <multi-dice> <high-op> <term> |
+            <term> <high-op> <multi-dice>
+
+Here we start to hit some complication.  The intent of the different 
+entries for multi-dice is that we don't want to allow things like
+[3d6 each * 2d6 each].  We are *not* doing vector multiplication!
+The "expression" level doesn't have any such limitation on syntax; 
+things like [3d6 each + 2d6 each] we'll have to either think of a 
+logical way to handle, or disallow on a semantic level.  (Well... I 
+suppose it could be handled in the BNF, but I think it would get
+kind of messy.)
+
+
+term ::= <dice> | <unit>
+
+unit ::= <number> | ( <expression> )
+
+Dice are not considered a unit.  This means that things like [3d6d10] can't
+be done without using parentheses.  I consider that to be a win for 
+clarity.
+
+
+dice ::= <unit>d<unit> | <unit>d<name> | <dice> <flag> | lastroll
+
+The <name> entry here allows for user-created dice (in the syntax, at 
+least...).
+
+
+multi-dice ::= ( <dice>, <dice>+ ) |               # (1d6,1d8)
+               <dice> each |                       # 3d6 each
+               ( <expression> of <expression> ) |  # (3 of 2d6)
+               lastroll |                          # lastroll
+               <multi-dice> <flag>                 # (3 of 2d6) best 2
+
+"lastroll" by itself can be either dice or multi-dice.  I'm thinking that it
+ 
+should be whatever type the last roll was.
+
+
+flag ::= reroll <condition> |          # repeats
+         reroll <slice> |              # once only
+         grow <condition> |            # reroll and add
+         shrink <condition> |          # reroll and subtract
+         drop <condition> |
+         drop <slice> |
+         take <condition> |
+         take <slice> |
+         <slice> |                     # implied "take"
+         <name> <condition> |          # user-created
+         <name>                        # user-created
+
+Technically, we don't need both "drop" and "take" -- one implies the 
+other.  However, having both should make the language easier to use.
+
+"reroll" will work differently depending on whether a condition or a 
+slice is given.  If a condition is given, it will reroll until none of
+the dice in the set meet the reroll condition (or until it hits a maximum
+allowed number of rerolls).  If a slice is given, it will reroll those
+dice once.  IMHO, this behavior makes the most sense.
+
+The <name> entries here are to allow for user-created flags.  Note that
+as I've specified things right now, a user-created flag can have a
+ condition,
+but not a slice.  That's mostly because I couldn't think of a case where
+a slice would be useful... should we add it anyways?
+
+
+slice ::= highest | lowest | highest <number> | lowest <number>
+
+"highest" and "lowest" without a number are equivalent to doing them with
+1 as the number.  This is to simplify things like [4d6 drop lowest].
+
+
+condition ::= <relation> <unit>
+
+This is for conditions on flags.  Note that it can take a unit, so you could
+
+use dice in a condition; however, I think the unit should only be evaluated 
+
+once, to make things faster.  Anyone for repetitive evaluation?
+
+
+low-op ::= + | - | min | max
+
+"min" takes two values and returns the highest of them, and "max"
+returns the lowest of them.  This might seem counterintuitive, but
+it's meant to be used with dice, like so:
+
+  3d6 min 8   - always returns 8 or higher
+
+  3d6 max 15  - always returns 15 or lower
+
+I decided to put min and max as having the same precedence as + and -,
+because if they had higher precedence, then:
+
+  3d6+2 min 10 
+
+would be equivalent to 3d6+10.  (It would take the max of 2 and 10, then
+add that to 3d6).  One problem that does arise here is with multiplication 
+and division:  [1d6 min 5 * 2] will be equivalent to [1d6 min 10], since
+multiplication has higher precedence.  We may just want to warn people 
+that min and max can be screwy unless you parenthesize, unless someone can
+think of a better way to handle them.
+
+
+high-op ::= * | / | mod
+
+The / is integer division, of course, since we're doing integer math.  
+
+
+number ::= <digit>+ | -<digit>+
+
+Positive and negative numbers are allowed.  This means that, syntactically,
+[-2d-4] is legal.  Do we want to modify the BNF to disallow this, or handle
+it on a semantic level?
+
+
+name ::= <letter>[<letter>|<digit>]*
+
+We may want to expand to allow underscores and dashes in user-created names.
+
+
+
+letter ::= A-Z | a-z
+
+digit ::= 0-9
+
+relation ::= < | > | <= | >= | => | =< | = | ==
+
+
+
+Well, that's the BNF.  Again, at the end is a copy without all the running
+commentary.
+
+
+Thoughts on Implementation:
+
+First, I think a sort of "dice library" of common functions needs to be
+created.  This would include rolling a set of dice, getting the highest
+of a group of dice, growing and shrinking dice from a set based on 
+conditions, and so on.  These functions should be available for use by
+custom-written dice types.
+
+Next, that library should be used as a tool in implementing a dice-string
+interpreter.  That will require creating a parser for the dice-string 
+'language'.  This could be either a custom-written parser, or possibly
+one created with some of the Python parser generators.  A custom-written
+parser may take longer to do and be a bit more finicky to maintain, but
+it would remove a dependency from the code.
+
+User-created flags and dice types could be supported in two ways:
+
+ - First, by allowing users to specify strings in the dice language
+   that the flags/expressions would expand to -- basically, allowing
+   dice macros.
+
+ - Second, by adding hooks for python modules to be associated with
+   user-created dice or flag types.  This is likely to be the more
+   complicated of the two solutions, but it would also be more 
+   flexible.
+
+Personally, I think both are desirable -- the first, so that 
+non-programming users can create simple die and flag types.  The
+second, because by design, there are some things this dice system 
+just won't do.
+
+
+Further work needed:
+
+ - specs for the "dice library"
+
+ - specs on an interface for python modules meant to be dice and
+   flag types.
+
+----------------------------------------------------------------
+
+start ::= <expression> |
+          <comparison>
+
+comparison ::= <expression> <condition>
+
+expression ::= <term> | <term> <low-op> <factor>
+
+term ::= <factor> | <factor> <high-op> <unit>
+
+factor ::= <atom> | <dice_set>
+
+atom ::= <number> | ( <expression> )
+
+dice_set ::= <dice> <dice_set>, <dice> | <expr> of <dice> | <dice> each |
+             lastroll
+
+dice ::= <atom>d<atom> | <atom>d<name> | <dice> <flag>
+
+flag ::= reroll <condition> |
+         reroll <slice> |
+         grow <condition> |  
+         shrink <condition> |        
+         drop <condition> |
+         drop <slice> |
+         take <condition> |
+         take <slice> |
+         <slice> |         
+         <name> <condition> |
+	 <name> <slice> |
+         <name>
+
+slice ::= highest | lowest | highest <number> | lowest <number>
+
+condition ::= <relation> <unit>
+
+low-op ::= + | -
+
+high-op ::= * | / | mod| max | min
+
+number ::= <digit>+ | -<digit>+
+
+name ::= <letter>[<letter>|<digit>]*
+
+letter ::= A-Z | a-z
+
+digit ::= 0-9
+
+relation ::= < | > | <= | >= | => | =< | = | ==
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/gurps.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,691 @@
+#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 die import *
+from time import time, clock
+import random
+
+
+__version__ = "$Id: gurps.py,v 1.5 2007/05/06 16:42:55 digitalxero Exp $"
+
+# gurps
+
+class gurps(std):
+    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)
+
+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/orpg/dieroller/hackmaster.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,206 @@
+#!/usr/bin/env python
+# Copyright Not Yet, see how much I trust you
+#
+#       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: hackmaster.py
+# Author: Ric Soard
+# Maintainer:
+# Version:
+#   $Id: hackmaster.py,v 0.4 2003/08/12
+#
+# Description: special die roller for HackMaster(C)(TM) RPG
+#               has penetration damage - .damage(bonus,honor)
+#               has attack - .attack(bonus, honor)
+#               has severity .severity(honor)
+#               has help - .help()
+#
+#
+import random
+from die import *
+
+__version__ = "$Id: hackmaster.py,v 1.8 2006/11/15 12:11:22 digitalxero Exp $"
+
+#hackmaster Class basically passes into functional classes
+class hackmaster(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+
+    def damage(self, mod, hon):
+        return HMdamage(self, mod, hon)
+
+    def attack(self, mod, hon):
+        return HMattack(self, mod, hon)
+
+    def help(self):
+        return HMhelp(self)
+
+    def severity(self, honor):
+        return HMSeverity(self, honor)
+
+
+# HM Damage roller - rolles penetration as per the PHB - re-rolles on max die - 1, adds honor to the penetration rolls
+# and this appears to be invisible to the user ( if a 4 on a d4 is rolled a 3 will appear and be followed by another
+# die. if High honor then a 4 will appear followed by a another die.
+class HMdamage(std):
+    def __init__(self,source=[], mod = 0, hon = 0):
+        std.__init__(self,source)
+        self.mod = mod
+        self.hon = hon
+        self.check_pen()
+        #here we roll the mod die
+        self.append(static_di(self.mod))
+        #here we roll the honor die
+        self.append(static_di(self.hon))
+
+    def damage(mod = 0, hon = 0):
+        self.mod = mod
+        self.hon = hon
+
+# This function is called by default to display the die string to the chat window.
+# Our die string attempts to explain the results
+    def __str__(self):
+        myStr = "Damage "
+        myStr += "[Damage Roll, Modifiers, Honor]: " + " [" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr += "] = (" + str(self.sum()) + ")"
+
+        return myStr
+
+# This function checks to see if we need to reroll for penetration
+    def check_pen(self):
+        for i in range(len(self.data)):
+            if self.data[i].lastroll() >= self.data[i].sides:
+                self.pen_roll(i)
+
+#this function rolls the penetration die, and checks to see if it needs to be re-rolled again.
+    def pen_roll(self,num):
+        result = int(random.uniform(1,self.data[num].sides+1))
+        self.data[num].value += (result - 1 + self.hon)
+        self.data[num].history.append(result - 1 + self.hon)
+        if result >= self.data[num].sides:
+            self.pen_roll(num)
+
+# this function rolls for the HM Attack. the function checks for a 20 and displays critical, and a 1
+# and displays fumble
+class HMattack(std):
+    def __init__(self, source=[], mod = 0, base_severity = 0, hon = 0, size = 0):
+        std.__init__(self,source)
+        self.size = size
+        self.mod = mod
+        self.base_severity = base_severity
+        self.hon = hon
+        self.fumble = 0
+        self.crit = 0
+        self.check_crit()
+        #this is a static die that adds the modifier
+        self.append(static_di(self.mod))
+        #this is a static die that adds honor, we want high rolls so it's +1
+        self.append(static_di(self.hon))
+
+
+    def check_crit(self):
+        if self.data[0] == self.data[0].sides:
+            self.crit = 1
+        if self.data[0] == 1:
+            self.fumble = 1
+
+
+    #this function is the out put to the chat window, it basicaly just displays the roll unless
+    #it's a natural 20, or a natural 1
+    def __str__(self):
+        if self.crit > 0:
+            myStr = "Critical Hit!!: "
+        elif self.fumble > 0:
+            myStr = "FUMBLE!!"
+        else:
+            myStr = "To Hit:"
+        myStr += "[To Hit Roll, Modifiers, Honor]" + " [" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr += "] = (" + str(self.sum()) + ")"
+        return myStr
+
+class HMhelp(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+        self.source = source
+
+    def __str__(self):
+        myStr = " <br /> .attack(Bonus, Honor): <br />"
+        myStr += " The attack roll rolles the dice and adds your bonus <br />"
+        myStr += " and honor modifier and returns you final roll. <br />"
+        myStr += " On a natural 20 the dieroller displays Critical Hit!! <br />"
+        myStr += " On a natural 1 the dieroller displays FUMBLE!! <br />"
+        myStr += " Example A 1st level fighter with +1 to hit and a +2 sword and High Honor <br />"
+        myStr += " would roll [1d20.attack(3,1)] <br />"
+        myStr += " .damage(Bonus, Honor): <br />"
+        myStr += " The damage roll rolls the dice and rerolls on a max roll for <br />"
+        myStr += " penetration damage, the penetration die is -1 and is rerolled on a max roll <br />"
+        myStr += " The roller returns the damage dice, monidifiers, and honor <br />"
+        myStr += " Example A magic-user uses a quaterstaff +1 with high honor, he would roll <br />"
+        myStr += " [1d6.damage(1,1)] <br />"
+        myStr += " .severity(honor): <br />"
+        myStr += " the severity is for critical hit resolution - the character rolls <br />"
+        myStr += " a d8 and adds honor bonus. the die is rerolled on natural 8 and natural 1 with a -1 modifier <br />"
+        myStr += " on an 8 the reroll is added on a 1 the reroll is subtracted <br />"
+        myStr += " Example [1d8.severity(1)] <br />"
+        myStr += " .help() : <br />"
+        myStr += " displays this message <br />"
+
+        return myStr
+
+# the severity roll is for critical resolution. The die is rerolled and added
+#on a natural 8 and rerolled and subtracted on a 1
+class HMSeverity(std):
+    def __init__(self, source =[], honor=0):
+        std.__init__(self,source)
+        self.source = source
+        self.hon = honor
+        self.data = []
+        self.append(di(8))
+        self.CheckReroll()
+        self.append(static_di(self.hon))
+
+
+    def __str__(self):
+        myStr = "[Severity Dice, Honor]" + " [" + str(self.data[0])
+        for a in self.data[1:]:
+            myStr += ","
+            myStr += str(a)
+        myStr += "] = (" + str(self.sum()) + ")"
+        return myStr
+
+    def CheckReroll(self):
+        if self.data[0] == self.data[0].sides:
+            self.crit_chain(0,1)
+        if self.data[0] == 1:
+            self.crit_chain(0,-1)
+
+    #this function needes moved for severity
+    def crit_chain(self,num,neg):
+        result = int(random.uniform(1,self.data[num].sides+1))
+        self.data[num].value += (((result - 1) * neg) + self.hon)
+        self.data[num].history.append(((result - 1) * neg) + self.hon)
+        if result >= self.data[num].sides:
+            self.crit_chain(num,1)
+        if result == 1:
+            self.crit_chain(num,-1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/hero.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,227 @@
+# (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
+
+from die import *
+from time import time, clock
+import random
+
+
+__version__ = "$Id: hero.py,v 1.15 2006/11/04 21:24:19 digitalxero Exp $"
+
+# Hero stands for "Hero system" not 20 sided die :)
+
+class hero(std):
+    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)
+
+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/orpg/dieroller/runequest.py	Tue Jul 14 16:41:58 2009 -0500
@@ -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
+#
+
+from die import *
+from time import time, clock
+import random
+from math import floor
+
+
+__version__ = "$Id: runequest.py,v 1.4 2006/11/15 12:11:22 digitalxero Exp $"
+
+# rq stands for "Runequest"
+
+class runequest(std):
+   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)
+
+#  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/orpg/dieroller/savage.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,507 @@
+# (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
+#
+
+from die import *
+#from whrandom import randint
+#import random
+import string
+from random import *
+__version__ = "$Id: savage.py,v 1.2 2007/05/06 16:42:55 digitalxero Exp $"
+
+# 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)
+
+
+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/orpg/dieroller/shadowrun.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,140 @@
+## 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: shadowrun.py
+# Author: Michael Edwards (AKA akoman)
+# Maintainer:
+# Version: 1.0
+#
+# Description: A modified form of the World of Darkness die roller to
+#              conform to ShadowRun rules-sets. Thanks to the ORPG team
+#              for the original die rollers.
+#              Thanks to tdb30_ for letting me think out loud with him.
+#         I take my hint from the HERO dieroller: It creates for wildly variant options
+#         Further, .vs and .open do not work together in any logical way. One method of
+#         chaining them results in a [Bad Dice Format] and the other results in a standard
+#         output from calling .open()
+
+#         vs is a classic 'comparison' method function, with one difference. It uses a
+#           c&p'ed .open(int) from die.py but makes sure that once the target has been exceeded
+#           then it stops rerolling. The overhead from additional boolean checking is probably
+#           greater than the gains from not over-rolling. The behaviour is in-line with
+#           Shadowrun Third Edition which recommends not rolling once you've exceeded the target
+#         open is an override of .open(int) in die.py. The reason is pretty simple. In die.py open
+#           refers to 'open-ended rolling' whereas in Shadowrun it refers to an 'Open Test' where
+#           the objective is to find the highest die total out of rolled dice. This is then generally
+#           used as the target in a 'Success Test' (for which .vs functions)
+from die import *
+
+__version__ = "1.0"
+
+class shadowrun(std):
+    def __init__(self,source=[],target=2):
+        std.__init__(self,source)
+
+    def vs(self,target):
+        return srVs(self, target)
+
+    def open(self):
+        return srOpen(self)
+
+class srVs(std):
+    def __init__(self,source=[], target=2):
+        std.__init__(self, source)
+        # In Shadowrun, not target number may be below 2. All defaults are set to two and any
+        # thing lower is scaled up.
+        if target < 2:
+            self.target = 2
+        else:
+            self.target = target
+        # Shadowrun was built to use the d6 but in the interests of experimentation I have
+        # made the dieroller generic enough to use any die type
+        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) and (self.data[i] < self.target):
+                self.data[i].extraroll()
+                done = 0
+        if done:
+            return self
+        else:
+            return self.openended(num)
+
+    def __sum__(self):
+        s = 0
+        for r in self.data:
+            if r >= self.target:
+                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 += "] vs " + str(self.target) + " for a result of (" + str(self.sum()) + ")"
+        else:
+            myStr = "[] = (0)"
+
+        return myStr
+
+class srOpen(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+        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)
+
+    def __sum__(self):
+        s = 0
+        for r in self.data:
+            if r > s:
+                s = r
+        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)
+            self.takeHighest(1)
+            myStr += "] for a result of (" + str(self.__sum__().__int__()) + ")"
+        else:
+            myStr = "[] = (0)"
+
+        return myStr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/sr4.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,232 @@
+## 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.
+
+from die import *
+
+__version__ = "1.1"
+
+MIN_TARGET_NUMBER = 5
+GLITCH_NUMBER = 1
+
+class sr4(std):
+    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
+
+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 ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/srex.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,192 @@
+## 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: srex.py
+# Original Author: Michael Edwards (AKA akoman)
+# Maintainer:
+# Original Version: 1.0
+#
+# Description: A modified form of the World of Darkness die roller to
+#              conform to ShadowRun rules-sets. Thanks to the ORPG team
+#              for the original die rollers.
+#              Thanks to tdb30_ for letting me think out loud with him.
+#         I take my hint from the HERO dieroller: It creates for wildly variant options
+#         Further, .vs and .open do not work together in any logical way. One method of
+#         chaining them results in a [Bad Dice Format] and the other results in a standard
+#         output from calling .open()
+
+#         vs is a classic 'comparison' method function, with one difference. It uses a
+#           c&p'ed .open(int) from die.py but makes sure that once the target has been exceeded
+#           then it stops rerolling. The overhead from additional boolean checking is probably
+#           greater than the gains from not over-rolling. The behaviour is in-line with
+#           Shadowrun Third Edition which recommends not rolling once you've exceeded the target
+#         open is an override of .open(int) in die.py. The reason is pretty simple. In die.py open
+#           refers to 'open-ended rolling' whereas in Shadowrun it refers to an 'Open Test' where
+#           the objective is to find the highest die total out of rolled dice. This is then generally
+#           used as the target in a 'Success Test' (for which .vs functions)
+
+# Modified by: Darloth
+# Mod Version: 1.1
+# Modified Desc:
+#       I've altered the vs call to make it report successes against every target number (tn)
+#       in a specified (default 3) range, with the original as median.
+#       This reduces rerolling if the TN was calculated incorrectly, and is also very useful
+#       when people are rolling against multiple TNs, which is the case with most area-effect spells.
+#       To aid in picking the specified TN out from the others, it will be in bold.
+#       vswide is a version which can be used with no arguments, or can be used to get a very wide range, by
+#       directly specifying the upper bound (Which is limited to 30)
+
+from die import *
+
+__version__ = "1.1"
+
+class srex(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+
+    def vs(self,actualtarget=4,tnrange=3):        #reports all tns around specified, max distance of range
+        return srVs(self,actualtarget,(actualtarget-tnrange),(actualtarget+tnrange))
+
+    def vswide(self,actualtarget=4,maxtarget=12):    #wide simply means it reports TNs from 2 to a specified max.
+        return srVs(self,actualtarget,2,maxtarget)
+
+    def open(self):         #unchanged from standard shadowrun open.
+        return srOpen(self)
+
+class srVs(std):
+    def __init__(self,source=[],actualtarget=4,mintn=2,maxtn=12):
+        std.__init__(self, source)
+        if actualtarget > 30:
+            actualtarget = 30
+        if mintn > 30:
+            mintn = 30
+        if maxtn > 30:
+            maxtn = 30
+        # In Shadowrun, not target number may be below 2. Any
+        # thing lower is scaled up.
+        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
+        #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)
+
+        # Shadowrun was built to use the d6 but in the interests of experimentation I have
+        # made the dieroller generic enough to use any die type
+        self.openended(self[0].sides)
+
+    def openended(self,num):
+        if num <= 1:
+            self
+        done = 1
+
+        #reroll dice if they hit the highest number, until they are greater than the max TN (recursive)
+        for i in range(len(self.data)):
+            if (self.data[i].lastroll() >= num) and (self.data[i] < self.maxtn):
+                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
+    def __sum__(self):
+        s = 0
+        for r in self.data:
+            if r >= self.target:
+                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.
+    def xsum(self,curtarget):
+        s = 0
+        for r in self.data:
+            if r >= curtarget:
+                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)"
+
+        return myStr
+
+class srOpen(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+        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)
+
+    def __sum__(self):
+        s = 0
+        for r in self.data:
+            if r > s:
+                s = r
+        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)
+            self.takeHighest(1)
+            myStr += "] for a result of (" + str(self.__sum__().__int__()) + ")"
+        else:
+            myStr = "[] = (0)"
+
+        return myStr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/trinity.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,83 @@
+## 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: trinity.py
+# Author: Jacob Matthew, Talisan Creations
+# Maintainer:
+# Version:
+#   $Id: trinity.py,v 1.2 2007/05/05 05:30:10 digitalxero Exp $
+#
+# Description: Aeon Trinity die roller
+# Modified from the WoD dieroller "$Id: trinity.py,v 1.2 2007/05/05 05:30:10 digitalxero Exp $"
+# Targetthr is the Threshhold target
+# for compatibility with Mage die rolls.
+# Threshhold addition by robert t childers
+# Threshhold functionality removed, some tags remain in code.
+from die import *
+
+__version__ = "$Id: trinity.py,v 1.2 2007/05/05 05:30:10 digitalxero Exp $"
+
+
+class trinity(std):
+   def __init__(self,source=[],target=7,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 sum(self):
+       rolls = []
+       s = 0
+       b = 0
+       for a in self.data:
+           rolls.extend(a.gethistory())
+       for r in rolls:
+           if r >= self.target:
+               s += 1
+           elif r == 1:
+               b -= 1
+       if s == 0:
+           return b
+       else:
+           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 += "] result of a (" + str(self.sum()) + ") botch"
+           elif self.sum() == 0:
+               myStr += "] result of a failure"
+           else:
+               myStr += "] result of (" + str(self.sum()) + ") success"
+
+
+       return myStr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/utils.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,100 @@
+#!/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: dieroller/utils.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: utils.py,v 1.22 2007/05/05 05:30:10 digitalxero Exp $
+#
+# Description: Classes to help manage the die roller
+#
+
+__version__ = "$Id: utils.py,v 1.22 2007/05/05 05:30:10 digitalxero Exp $"
+
+from die import *
+from wod import *
+from d20 import *
+from hero import *
+from shadowrun import *
+from sr4 import *
+from hackmaster import *
+from wodex import *
+from srex import *
+from gurps import *
+from runequest import *
+from savage import *
+from trinity import *
+
+import re
+
+rollers = ['std','wod','d20','hero','shadowrun', 'sr4','hackmaster','srex','wodex', 'gurps', 'runequest', 'sw', 'trinity']
+
+class roller_manager:
+    def __init__(self,roller_class="d20"):
+        try:
+            self.setRoller(roller_class)
+        except:
+            self.roller_class = "std"
+
+    def setRoller(self,roller_class):
+        try:
+            rollers.index(roller_class)
+            self.roller_class = roller_class
+        except:
+            raise Exception, "Invalid die roller!"
+
+    def getRoller(self):
+        return self.roller_class
+
+    def listRollers(self):
+        return rollers
+
+    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
+
+        return "(" + num.strip() + "**" + self.roller_class + "(" + sides.strip() + "))"
+
+    #  Use this to convert ndm-style (3d6) dice to d_base format
+    def convertTheDieString(self,s):
+        reg = re.compile("\d+\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
+
+
+    def proccessRoll(self,s):
+        return str(eval(self.convertTheDieString(s)))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/wod.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,87 @@
+## 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: wod.py
+# Author: OpenRPG Dev Team
+# Maintainer:
+# Version:
+#   $Id: wod.py,v 1.14 2007/05/09 19:57:00 digitalxero Exp $
+#
+# Description: WOD die roller
+#
+# Targetthr is the Threshhold target
+# for compatibility with Mage die rolls.
+# Threshhold addition by robert t childers
+from die import *
+
+__version__ = "$Id: wod.py,v 1.14 2007/05/09 19:57:00 digitalxero Exp $"
+
+
+class wod(std):
+    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 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dieroller/wodex.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,286 @@
+## 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: wodex.py
+# Original Author: Darloth
+# Maintainer:
+# Original Version: 1.0
+#
+# Description: A modified form of the World of Darkness die roller to
+#              conform to ShadowRun rules-sets, then modified back to the WoD for
+#              the new WoD system. Thanks to the ORPG team
+#              for the original die rollers.
+#              Much thanks to whoever wrote the original shadowrun roller (akoman I believe)
+
+
+from die import *
+
+__version__ = "$Id: wodex.py,v 1.9 2007/05/06 16:42:55 digitalxero Exp $"
+
+class wodex(std):
+    def __init__(self,source=[]):
+        std.__init__(self,source)
+
+    def vs(self,actualtarget=6):
+        return oldwodVs(self,actualtarget,(6))
+
+    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)
+
+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)
+
+        # WoD etc uses d10 but i've left it so it can roll anything openended
+        # self.openended(self[0].sides)
+
+    #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)"
+
+        return myStr
+
+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)
+
+    #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)
+
+    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[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)
+
+        if actualtarget > 10:
+            actualtarget = 10
+
+        if actualtarget < 2:
+            self.target = 2
+        else:
+            self.target = actualtarget
+
+
+    def xsum(self, target):
+        s = 0
+
+        for r in self.data:
+            if r >= target:
+                s += 1
+            if r == 10:
+                s += 1
+
+        return s
+
+
+    def __str__(self):
+        if len(self.data) > 0:
+            myStr = str(self.data)
+            myStr += " Results: "
+
+            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"
+
+            return myStr
+
+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 xsum(self, target):
+        s = 0
+
+        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: "
+
+            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"
+
+            return myStr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dirpath/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,46 @@
+# Old dirpath.py replaced with new dirpath 'package/module' to allow dynamic
+# checking on directory structure at dirpath import without requiring alteration
+# of almost every openrpg1 python file
+#
+# This module is functionally identical to the dirpath.py file it replaces.
+# All directory locations are now handled by the load_paths() function
+# in the dirpath_tools.py file  -- Snowdog 3-8-05
+
+# CHANGE LOG
+# -----------------------------
+# * Reworked path verification process to attempt to fall back on the
+#   current working directory if approot fails to verify before
+#   asking the user to locate the root directory -- Snowdog 12-20-05
+
+import sys
+import os
+from dirpath_tools import *
+
+root_dir = None
+
+try:
+    import approot
+    root_dir = approot.basedir
+except:
+    #attempt to load default path
+    t = __file__.split(os.sep)
+    if len(t) > 2:
+        root_dir = os.sep.join(t[:-3])
+    else:
+        root_dir = os.getcwd()   #default ORPG root dir
+
+dir_struct = {}
+
+if not verify_home_path(root_dir):
+    root_dir = os.getcwd()
+    if not verify_home_path(root_dir):
+        root_dir = get_user_located_root()
+        while not verify_home_path(root_dir):
+            root_dir = get_user_located_root()
+
+#switch backslashes to forward slashes just for display on screen only (avoids issues with escaped characters)
+clean = str(root_dir)
+clean = str.replace(clean,'\\','/')
+print "Rooting OpenRPG at: " + clean
+
+load_paths(dir_struct, root_dir)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/dirpath/dirpath_tools.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,157 @@
+import sys
+import os
+import errno
+from orpg.orpg_wx import *
+
+if WXLOADED:
+    class tmpApp(wx.App):
+        def OnInit(self):
+            return True
+
+
+#-------------------------------------------------------
+# void load_paths( dir_struct_reference )
+# moved structure loading from dirpath.py by Snowdog 3-8-05
+#-------------------------------------------------------
+def load_paths(dir_struct, root_dir):
+    dir_struct["home"] = root_dir + os.sep
+    dir_struct["core"] = dir_struct["home"] + "orpg"+ os.sep
+    dir_struct["data"] = dir_struct["home"] + "data"+ os.sep
+    dir_struct["d20"] = dir_struct["data"] + "d20" + os.sep
+    dir_struct["dnd3e"] = dir_struct["data"] + "dnd3e" + os.sep
+    dir_struct["dnd35"] = dir_struct["data"] + "dnd35" + os.sep
+    dir_struct["SWd20"] = dir_struct["data"] + "SWd20" + os.sep
+    dir_struct["icon"] = dir_struct["home"] + "images" + os.sep
+    dir_struct["template"] = dir_struct["core"] + "templates" + os.sep
+
+    dir_struct["plugins"] = dir_struct["home"] + "plugins" + os.sep
+    dir_struct["nodes"] = dir_struct["template"] + "nodes" + os.sep
+    dir_struct["rollers"] = dir_struct["core"] + "dieroller" + os.sep + "rollers" + os.sep
+
+
+    _userbase_dir = _userbase_dir = os.environ['OPENRPG_BASE']
+    _user_dir = _userbase_dir + os.sep + "myfiles" + os.sep
+
+
+    try:
+        os.makedirs(_user_dir)
+        os.makedirs(_user_dir + "runlogs" + os.sep);
+        os.makedirs(_user_dir + "logs" + os.sep);
+        os.makedirs(_user_dir + "webfiles" + os.sep);
+    except OSError, e:
+        if e.errno != errno.EEXIST:
+            raise
+
+    dir_struct["user"] = _user_dir
+
+    dir_struct["logs"] = dir_struct["user"] + "logs" + os.sep
+
+
+
+#-------------------------------------------------------
+# int verify_home_path( directory_name )
+# added by Snowdog 3-8-05
+# updated with bailout code. Snowdog 7-25-05
+#-------------------------------------------------------
+def verify_home_path( path ):
+    """checks for key ORPG files in the openrpg tree
+       and askes for user intervention if their is a problem"""
+
+    try:
+        #verify that the root dir (as supplied) exists
+        if not verify_file(path): return 0
+
+        #These checks require that 'path' have a separator at the end.
+        #Check and temporarily add one if needed
+        if (path[(len(path)-len(os.sep)):] != os.sep):
+            path = path + os.sep
+
+        # These files should always exist at the root orpg dir
+        check_files = ["orpg","data","images"]
+        for n in range(len(check_files)):
+            if not verify_file(path + check_files[n]): return 0
+
+    except:
+        # an error occured while verifying the directory structure
+        # bail out with error signal
+        return 0
+
+    #all files and directories exist.
+    write_approot(path)
+    return 1
+
+
+
+#-------------------------------------------------------
+# int verify_file( absolute_path )
+# added by Snowdog 3-8-05
+#-------------------------------------------------------
+def verify_file(abs_path):
+    """Returns True if file or directory exists"""
+    try:
+        os.stat(abs_path)
+        return 1
+    except OSError:
+        #this exception signifies the file or dir doesn't exist
+        return 0
+
+
+#-------------------------------------------------------
+# pathname get_user_help()
+# added by Snowdog 3-8-05
+# bug fix (SF #1242456) and updated with bailout code. Snowdog 7-25-05
+#-------------------------------------------------------
+def get_user_located_root():
+    """Notify the user of directory problems
+    and show directory selection dialog """
+
+    if WXLOADED:
+        app = tmpApp(0)
+        app.MainLoop()
+
+        dir = None
+
+        try:
+            msg = "OpenRPG cannot locate critical files.\nPlease locate the openrpg1 directory in the following window"
+            alert= wx.MessageDialog(None,msg,"Warning",wx.OK|wx.ICON_ERROR)
+            alert.Show()
+            if alert.ShowModal() == wx.OK:
+                alert.Destroy()
+            dlg = wx.DirDialog(None, "Locate the openrpg1 directory:",style=wx.DD_DEFAULT_STYLE)
+            if dlg.ShowModal() == wx.ID_OK:
+                dir = dlg.GetPath()
+            dlg.Destroy()
+            app.Destroy()
+            return dir
+        except Exception, e:
+            print e
+            print "OpenRPG encountered a problem while attempting to load file dialog to locate the OpenRPG root directory."
+            print "please delete the files ./openrpg/orpg/dirpath/aproot.py and ./openrpg/orpg/dirpath/aproot.pyc and try again."
+    else:
+        dir = raw_input("Enter the full path to your openrpg folder:  ")
+        return dir
+
+
+
+#-------------------------------------------------------
+# void write_approot( orpg_root_path )
+# added by snowdog 3-10-05
+#-------------------------------------------------------
+
+def write_approot( orpg_root_path):
+    try:
+        #if a trailing path separator is on the path string remove it.
+        if (orpg_root_path[(len(orpg_root_path)-len(os.sep)):] == os.sep):
+            orpg_root_path = orpg_root_path[:(len(orpg_root_path)-len(os.sep))]
+        fn = orpg_root_path + os.sep + "orpg" + os.sep + "dirpath" + os.sep + "approot.py"
+        f = open(fn, "w")
+        #trim off the appended os.sep character(s) to avoid duplicating them on re-loading of path
+        code = 'basedir = "' + orpg_root_path + '"' + "\n"
+        #fix path string for windows boxes that lamely use an escape character for a path separator
+        code = str.replace(code,'\\','\\\\')
+        f.write(code)
+        f.close()
+    except IOError:
+        print "[WARNING] Could not create approot file."
+        print "[WARNING] Automatic directory resolution not configured."
+    return
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+__all__ = ['nodehandlers']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/gametree.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1073 @@
+# 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: gametree.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: gametree.py,v 1.68 2007/12/07 20:39:48 digitalxero Exp $
+#
+# Description: The file contains code fore the game tree shell
+#
+
+__version__ = "$Id: gametree.py,v 1.68 2007/12/07 20:39:48 digitalxero Exp $"
+
+from orpg.orpg_wx import *
+from orpg.orpg_windows import *
+from orpg.orpgCore import open_rpg
+import orpg.dirpath
+from nodehandlers import core
+import orpg.gametree.nodehandlers.containers
+import orpg.gametree.nodehandlers.forms
+import orpg.gametree.nodehandlers.dnd3e
+import orpg.gametree.nodehandlers.dnd35
+import orpg.gametree.nodehandlers.chatmacro
+import orpg.gametree.nodehandlers.map_miniature_nodehandler
+import orpg.gametree.nodehandlers.minilib
+import orpg.gametree.nodehandlers.rpg_grid
+import orpg.gametree.nodehandlers.d20
+import orpg.gametree.nodehandlers.StarWarsd20
+import orpg.gametree.nodehandlers.voxchat
+from gametree_version import GAMETREE_VERSION
+import string
+import urllib
+import time
+import os
+
+STD_MENU_DELETE = wx.NewId()
+STD_MENU_DESIGN = wx.NewId()
+STD_MENU_USE = wx.NewId()
+STD_MENU_PP = wx.NewId()
+STD_MENU_RENAME = wx.NewId()
+STD_MENU_SEND = wx.NewId()
+STD_MENU_SAVE = wx.NewId()
+STD_MENU_ICON = wx.NewId()
+STD_MENU_CLONE = wx.NewId()
+STD_MENU_ABOUT = wx.NewId()
+STD_MENU_HTML = wx.NewId()
+STD_MENU_EMAIL = wx.NewId()
+STD_MENU_CHAT = wx.NewId()
+STD_MENU_WHISPER = wx.NewId()
+STD_MENU_WIZARD = wx.NewId()
+STD_MENU_NODE_SUBMENU = wx.NewId()
+STD_MENU_NODE_USEFUL = wx.NewId()
+STD_MENU_NODE_USELESS = wx.NewId()
+STD_MENU_NODE_INDIFFERENT = wx.NewId()
+STD_MENU_MAP = wx.NewId()
+TOP_IFILE = wx.NewId()
+TOP_INSERT_URL = wx.NewId()
+TOP_NEW_TREE = wx.NewId()
+TOP_SAVE_TREE = wx.NewId()
+TOP_SAVE_TREE_AS = wx.NewId()
+TOP_TREE_PROP = wx.NewId()
+TOP_FEATURES = wx.NewId()
+
+class game_tree(wx.TreeCtrl):
+    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.log = open_rpg.get_component('log')
+        self.log.log("Enter game_tree", ORPG_DEBUG)
+        self.validate = open_rpg.get_component('validate')
+        self.xml = open_rpg.get_component('xml')
+        self.settings = open_rpg.get_component('settings')
+        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()
+        self.nodehandlers = {}
+        self.nodes = {}
+        self.init_nodehandlers()
+        self.Bind(wx.EVT_LEFT_DCLICK, self.on_ldclick)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.on_rclick)
+        self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.on_drag, id=id)
+        self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
+        self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
+        self.Bind(wx.EVT_TREE_END_LABEL_EDIT, self.on_label_change, id=self.GetId())
+        self.Bind(wx.EVT_TREE_BEGIN_LABEL_EDIT, self.on_label_begin, id=self.GetId())
+        self.Bind(wx.EVT_CHAR, self.on_char)
+        self.Bind(wx.EVT_KEY_UP, self.on_key_up)
+        self.id = 1
+        self.dragging = False
+        self.root_dir = orpg.dirpath.dir_struct["home"]
+        self.last_save_dir = orpg.dirpath.dir_struct["user"]
+
+        #Create tree from default if it does not exist
+        self.validate.config_file("tree.xml","default_tree.xml")
+        open_rpg.add_component("tree", self)
+        #build tree
+        self.root = self.AddRoot("Game Tree",self.icons['gear'])
+        self.was_labeling = 0
+        self.rename_flag = 0
+        self.image_cache = {}
+        self.log.log("Exit game_tree", ORPG_DEBUG)
+
+    def add_nodehandler(self, nodehandler, nodeclass):
+        self.log.log("Enter game_tree->add_nodehandler(self, nodehandler, nodeclass)", ORPG_DEBUG)
+        if not self.nodehandlers.has_key(nodehandler):
+            self.nodehandlers[nodehandler] = nodeclass
+        else:
+            self.log.log("Nodehandler for " + nodehandler + " already exists!", ORPG_DEBUG, True)
+        self.log.log("Exit game_tree->add_nodehandler(self, nodehandler, nodeclass)", ORPG_DEBUG)
+
+    def remove_nodehandler(self, nodehandler):
+        self.log.log("Enter game_tree->remove_nodehandler(self, nodehandler)", ORPG_DEBUG)
+        if self.nodehandlers.has_key(nodehandler):
+            del self.nodehandlers[nodehandler]
+        else:
+            self.log.log("No nodehandler for " + nodehandler + " exists!", ORPG_DEBUG, True)
+        self.log.log("Exit game_tree->remove_nodehandler(self, nodehandler)", ORPG_DEBUG)
+
+    def init_nodehandlers(self):
+        self.log.log("Enter game_tree->init_nodehandlers(self)", ORPG_DEBUG)
+        self.add_nodehandler('group_handler', orpg.gametree.nodehandlers.containers.group_handler)
+        self.add_nodehandler('tabber_handler', orpg.gametree.nodehandlers.containers.tabber_handler)
+        self.add_nodehandler('splitter_handler', orpg.gametree.nodehandlers.containers.splitter_handler)
+        self.add_nodehandler('form_handler', orpg.gametree.nodehandlers.forms.form_handler)
+        self.add_nodehandler('textctrl_handler', orpg.gametree.nodehandlers.forms.textctrl_handler)
+        self.add_nodehandler('listbox_handler', orpg.gametree.nodehandlers.forms.listbox_handler)
+        self.add_nodehandler('link_handler', orpg.gametree.nodehandlers.forms.link_handler)
+        self.add_nodehandler('webimg_handler', orpg.gametree.nodehandlers.forms.webimg_handler)
+        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('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)
+        self.add_nodehandler('voxchat_handler', orpg.gametree.nodehandlers.voxchat.voxchat_handler)
+        self.add_nodehandler('file_loader', core.file_loader)
+        self.add_nodehandler('node_loader', core.node_loader)
+        self.add_nodehandler('url_loader', core.url_loader)
+        self.add_nodehandler('min_map', core.min_map)
+        self.log.log("Exit game_tree->init_nodehandlers(self)", ORPG_DEBUG)
+
+    #   event = wxKeyEvent
+    #   set to be called by wxWindows by EVT_CHAR macro in __init__
+    def on_key_up(self, evt):
+        self.log.log("Enter game_tree->on_key_up(self, evt)", ORPG_DEBUG)
+        key_code = evt.GetKeyCode()
+        if self.dragging and (key_code == wx.WXK_SHIFT):
+            curSelection = self.GetSelection()
+            cur = wx.StockCursor(wx.CURSOR_ARROW)
+            self.SetCursor(cur)
+            self.dragging = False
+            obj = self.GetPyData(curSelection)
+            self.SelectItem(curSelection)
+            if(isinstance(obj,core.node_handler)):
+                obj.on_drop(evt)
+                self.drag_obj = None
+        evt.Skip()
+        self.log.log("Exit game_tree->on_key_up(self, evt)", ORPG_DEBUG)
+
+    def on_char(self, evt):
+        self.log.log("Enter game_tree->on_char(self, evt)", ORPG_DEBUG)
+        key_code = evt.GetKeyCode()
+        curSelection = self.GetSelection()                            #  Get the current selection
+        if evt.ShiftDown() and ((key_code == wx.WXK_UP) or (key_code == wx.WXK_DOWN)) and not self.dragging:
+            curSelection = self.GetSelection()
+            obj = self.GetPyData(curSelection)
+            self.SelectItem(curSelection)
+            if(isinstance(obj,core.node_handler)):
+                self.dragging = True
+                cur = wx.StockCursor(wx.CURSOR_HAND)
+                self.SetCursor(cur)
+                self.drag_obj = obj
+        elif key_code == wx.WXK_LEFT:
+            self.Collapse(curSelection)
+
+        elif key_code == wx.WXK_DELETE:                          #  Handle the delete key
+            if curSelection:
+                nextSelect = self.GetItemParent(curSelection)
+                self.on_del(evt)
+                try:
+                    if self.GetItemText(nextSelect) != "":
+                        self.SelectItem(nextSelect)
+                except:
+                    pass
+        elif key_code == wx.WXK_F2:
+            self.rename_flag = 1
+            self.EditLabel(curSelection)
+        evt.Skip()
+        self.log.log("Exit game_tree->on_char(self, evt)", ORPG_DEBUG)
+
+    ## locate_valid_tree
+    ## GUI based dialogs to locate/fix missing treefile issues --Snowdog 3/05
+    def locate_valid_tree(self, error, msg, dir, filename):
+        """prompts the user to locate a new tree file or create a new one"""
+        self.log.log("Enter game_tree->locate_valid_tree(self, error, msg, dir, filename)", ORPG_DEBUG)
+        response = wx.MessageDialog(self, msg, error, wx.YES|wx.NO|wx.ICON_ERROR)
+        if response == wx.YES:
+            file = None
+            filetypes = "Gametree (*.xml)|*.xml|All files (*.*)|*.*"
+            dlg = wx.FileDialog(self, "Locate Gametree file", dir, filename, filetypes,wx.OPEN | wx.CHANGE_DIR)
+            if dlg.ShowModal() == wx.ID_OK: file = dlg.GetPath()
+            dlg.Destroy()
+            if not file: self.load_tree(error=1)
+            else: self.load_tree(file)
+            self.log.log("Exit game_tree->locate_valid_tree(self, error, msg, dir, filename)", ORPG_DEBUG)
+            return
+        else:
+            self.validate.config_file("tree.xml","default_tree.xml")
+            self.load_tree(error=1)
+            self.log.log("Exit game_tree->locate_valid_tree(self, error, msg, dir, filename)", ORPG_DEBUG)
+            return
+
+    def load_tree(self, filename=orpg.dirpath.dir_struct["user"]+'tree.xml', error=0):
+        self.log.log("Enter game_tree->load_tree(self, filename, error)", ORPG_DEBUG)
+        self.settings.set_setting("gametree", filename)
+        tmp = None
+        xml_dom = None
+        xml_doc = None
+        try:
+            self.log.log("Reading Gametree file: " + filename + "...", ORPG_INFO, True)
+            tmp = open(filename,"r")
+            xml_doc = self.xml.parseXml(tmp.read())
+            tmp.close()
+            if xml_doc == None:
+                pass
+            else:
+                xml_dom = xml_doc._get_documentElement()
+            self.log.log("done.", ORPG_INFO, True)
+
+        except IOError:
+            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)"
+            fn = filename[ ((filename.rfind(os.sep))+len(os.sep)):]
+            self.locate_valid_tree("Gametree Error", emsg, orpg.dirpath.dir_struct["user"], fn)
+            self.log.log(emsg, ORPG_GENERAL)
+            self.log.log("Exit game_tree->load_tree(self, filename, error)", ORPG_DEBUG)
+            return
+
+        if not xml_dom:
+            os.rename(filename,filename+".corrupt")
+            fn = filename[ ((filename.rfind(os.sep))+len(os.sep)):]
+            emsg = "Your gametree is being regenerated.\n\n"\
+                   "To salvage a recent version of your gametree\n"\
+                   "exit OpenRPG and copy the lastgood.xml file in\n"\
+                   "your myfiles directory to "+fn+ "\n"\
+                   "in your myfiles directory.\n\n"\
+                   "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, orpg.dirpath.dir_struct["user"], fn)
+            self.log.log(emsg, ORPG_GENERAL)
+            self.log.log("Exit game_tree->load_tree(self, filename, error)", ORPG_DEBUG)
+            return
+
+        if xml_dom._get_tagName() != "gametree":
+            fn = filename[ ((filename.rfind(os.sep))+len(os.sep)):]
+            emsg = fn+" 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, orpg.dirpath.dir_struct["user"], fn)
+            self.log.log(emsg, ORPG_DEBUG)
+            self.log.log("Exit game_tree->load_tree(self, filename, error)", ORPG_DEBUG)
+            return
+
+        # get gametree version - we could write conversion code here!
+        self.master_dom = xml_dom
+        self.log.log("Master Dom Set", ORPG_DEBUG)
+
+        try:
+            version = self.master_dom.getAttribute("version")
+            # see if we should load the gametree
+            loadfeatures = int(self.settings.get_setting("LoadGameTreeFeatures"))
+            if loadfeatures:
+                xml_dom = self.xml.parseXml(open(orpg.dirpath.dir_struct["template"]+"feature.xml","r").read())
+                xml_dom = xml_dom._get_documentElement()
+                xml_dom = self.master_dom.appendChild(xml_dom)
+                self.settings.set_setting("LoadGameTreeFeatures","0")
+
+            ## load tree
+            self.log.log("Features loaded (if required)", ORPG_DEBUG)
+            self.CollapseAndReset(self.root)
+            children = self.master_dom._get_childNodes()
+            self.log.log("Parsing Gametree Nodes ", ORPG_INFO, True)
+            for c in children:
+                print '.',
+                self.load_xml(c,self.root)
+            self.log.log("done", ORPG_INFO, True)
+            self.Expand(self.root)
+            self.SetPyData(self.root,self.master_dom)
+            if error != 1:
+                infile = open(filename, "rb")
+                outfile = open(orpg.dirpath.dir_struct["user"]+"lastgood.xml", "wb")
+                outfile.write(infile.read())
+            else:
+                self.log.log("Not overwriting lastgood.xml file.", ORPG_INFO, True)
+
+        except Exception, e:
+            self.log.log(traceback.format_exc(), ORPG_GENERAL)
+            wx.MessageBox("Corrupt Tree!\nYour game tree is being regenerated. To\nsalvage a recent version of your gametree\nexit OpenRPG and copy the lastgood.xml\nfile in your myfiles directory\nto "+filename+ "\nin your myfiles directory.\nlastgood.xml WILL BE OVERWRITTEN NEXT TIME YOU RUN OPENRPG.")
+            os.rename(filename,filename+".corrupt")
+            self.validate.config_file("tree.xml","default_tree.xml")
+            self.load_tree(error=1)
+        self.log.log("Exit game_tree->load_tree(self, filename, error)", ORPG_DEBUG)
+
+    def build_std_menu(self, obj=None):
+        self.log.log("Enter game_tree->build_std_menu(self, obj)", ORPG_DEBUG)
+
+        # build useful menu
+        useful_menu = wx.Menu()
+        useful_menu.Append(STD_MENU_NODE_USEFUL,"Use&ful")
+        useful_menu.Append(STD_MENU_NODE_USELESS,"Use&less")
+        useful_menu.Append(STD_MENU_NODE_INDIFFERENT,"&Indifferent")
+        # build standard menu
+        self.std_menu = wx.Menu()
+        self.std_menu.SetTitle("game tree")
+        self.std_menu.Append(STD_MENU_USE,"&Use")
+        self.std_menu.Append(STD_MENU_DESIGN,"&Design")
+        self.std_menu.Append(STD_MENU_PP,"&Pretty Print")
+        self.std_menu.AppendSeparator()
+        self.std_menu.Append(STD_MENU_SEND,"Send To Player")
+        self.std_menu.Append(STD_MENU_MAP,"Send To Map")
+        self.std_menu.Append(STD_MENU_CHAT,"Send To Chat")
+        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_DELETE,"D&elete")
+        self.std_menu.Append(STD_MENU_CLONE,"&Clone")
+        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.Bind(wx.EVT_MENU, self.on_send_to, id=STD_MENU_SEND)
+        self.Bind(wx.EVT_MENU, self.indifferent, id=STD_MENU_NODE_INDIFFERENT)
+        self.Bind(wx.EVT_MENU, self.useful, id=STD_MENU_NODE_USEFUL)
+        self.Bind(wx.EVT_MENU, self.useless, id=STD_MENU_NODE_USELESS)
+        self.Bind(wx.EVT_MENU, self.on_del, id=STD_MENU_DELETE)
+        self.Bind(wx.EVT_MENU, self.on_send_to_map, id=STD_MENU_MAP)
+        self.Bind(wx.EVT_MENU, self.on_node_design, id=STD_MENU_DESIGN)
+        self.Bind(wx.EVT_MENU, self.on_node_use, id=STD_MENU_USE)
+        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_clone, id=STD_MENU_CLONE)
+        self.Bind(wx.EVT_MENU, self.on_about, id=STD_MENU_ABOUT)
+        self.Bind(wx.EVT_MENU, self.on_send_to_chat, id=STD_MENU_CHAT)
+        self.Bind(wx.EVT_MENU, self.on_whisper_to, id=STD_MENU_WHISPER)
+        self.Bind(wx.EVT_MENU, self.on_export_html, id=STD_MENU_HTML)
+        self.top_menu = wx.Menu()
+        self.top_menu.SetTitle("game tree")
+        self.top_menu.Append(TOP_IFILE,"&Insert File")
+        self.top_menu.Append(TOP_INSERT_URL,"Insert &URL")
+        self.top_menu.Append(TOP_FEATURES, "Insert &Features Node")
+        self.top_menu.Append(TOP_NEW_TREE, "&Load New Tree")
+        self.top_menu.Append(TOP_SAVE_TREE,"&Save Tree")
+        self.top_menu.Append(TOP_SAVE_TREE_AS,"Save Tree &As...")
+        self.top_menu.Append(TOP_TREE_PROP,"&Tree Properties")
+        self.Bind(wx.EVT_MENU, self.on_insert_file, id=TOP_IFILE)
+        self.Bind(wx.EVT_MENU, self.on_insert_url, id=TOP_INSERT_URL)
+        self.Bind(wx.EVT_MENU, self.on_save_tree_as, id=TOP_SAVE_TREE_AS)
+        self.Bind(wx.EVT_MENU, self.on_save_tree, id=TOP_SAVE_TREE)
+        self.Bind(wx.EVT_MENU, self.on_load_new_tree, id=TOP_NEW_TREE)
+        self.Bind(wx.EVT_MENU, self.on_tree_prop, id=TOP_TREE_PROP)
+        self.Bind(wx.EVT_MENU, self.on_insert_features, id=TOP_FEATURES)
+        self.log.log("Exit game_tree->build_std_menu(self, obj)", ORPG_DEBUG)
+
+    def do_std_menu(self, evt, obj):
+        self.log.log("Enter game_tree->do_std_menu(self, evt, obj)", ORPG_DEBUG)
+        try:
+            self.std_menu.Enable(STD_MENU_MAP, obj.checkToMapMenu())
+        except:
+            self.std_menu.Enable(STD_MENU_MAP, obj.map_aware())
+        self.std_menu.Enable(STD_MENU_CLONE, obj.can_clone())
+        self.PopupMenu(self.std_menu)
+        self.log.log("Exit game_tree->do_std_menu(self, evt, obj)", ORPG_DEBUG)
+
+    def strip_html(self, player):
+        self.log.log("Enter game_tree->strip_html(self, player)", ORPG_DEBUG)
+        ret_string = ""
+        x = 0
+        in_tag = 0
+        for x in xrange(len(player[0])) :
+            if player[0][x] == "<" or player[0][x] == ">" or in_tag == 1 :
+                if player[0][x] == "<" :
+                    in_tag = 1
+                elif player[0][x] == ">" :
+                    in_tag = 0
+                else :
+                    pass
+            else :
+                ret_string = ret_string + player[0][x]
+        self.log.log(ret_string, ORPG_DEBUG)
+        self.log.log("Exit game_tree->strip_html(self, player)", ORPG_DEBUG)
+        return ret_string
+
+    def on_receive_data(self, data, player):
+        self.log.log("Enter game_tree->on_receive_data(self, data, player)", ORPG_DEBUG)
+        beg = string.find(data,"<tree>")
+        end = string.rfind(data,"</tree>")
+        data = data[6:end]
+        self.insert_xml(data)
+        self.log.log("Exit game_tree->on_receive_data(self, data, player)", ORPG_DEBUG)
+
+    def on_send_to_chat(self, evt):
+        self.log.log("Enter game_tree->on_send_to_chat(self, evt)", ORPG_DEBUG)
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        obj.on_send_to_chat(evt)
+        self.log.log("Exit game_tree->on_send_to_chat(self, evt)", ORPG_DEBUG)
+
+    def on_whisper_to(self, evt):
+        self.log.log("Enter game_tree->on_whisper_to(self, evt)", ORPG_DEBUG)
+        players = self.session.get_players()
+        opts = []
+        myid = self.session.get_id()
+        me = None
+        for p in players:
+            if p[2] != myid:
+                opts.append("("+p[2]+") " + self.strip_html(p))
+            else:
+                me = p
+        if len(opts):
+            players.remove(me)
+        if len(opts):
+            dlg = orpgMultiCheckBoxDlg( self.GetParent(),opts,"Select Players:","Whisper To",[] )
+            if dlg.ShowModal() == wx.ID_OK:
+                item = self.GetSelection()
+                obj = self.GetPyData(item)
+                selections = dlg.get_selections()
+                if len(selections) == len(opts):
+                    self.chat.ParsePost(obj.tohtml(),True,True)
+                else:
+                    player_ids = []
+                    for s in selections:
+                        player_ids.append(players[s][2])
+                    self.chat.whisper_to_players(obj.tohtml(),player_ids)
+        self.log.log("Exit game_tree->on_whisper_to(self, evt)", ORPG_DEBUG)
+
+    def on_export_html(self, evt):
+        self.log.log("Enter game_tree->on_export_html(self, evt)", ORPG_DEBUG)
+        f = wx.FileDialog(self,"Select a file", self.last_save_dir,"","HTML (*.html)|*.html",wx.SAVE)
+        if f.ShowModal() == wx.ID_OK:
+            item = self.GetSelection()
+            obj = self.GetPyData(item)
+            type = f.GetFilterIndex()
+            file = open(f.GetPath(),"w")
+            data = "<html><head><title>"+obj.master_dom.getAttribute("name")+"</title></head>"
+            data += "<body bgcolor=\"#FFFFFF\" >"+obj.tohtml()+"</body></html>"
+            for tag in ("</tr>","</td>","</th>","</table>","</html>","</body>"):
+                data = data.replace(tag,tag+"\n")
+            file.write(data)
+            file.close()
+            self.last_save_dir, throwaway = os.path.split( f.GetPath() )
+        f.Destroy()
+        os.chdir(self.root_dir)
+        self.log.log("Exit game_tree->on_export_html(self, evt)", ORPG_DEBUG)
+
+    def indifferent(self, evt):
+        self.log.log("Enter game_tree->indifferent(self, evt)", ORPG_DEBUG)
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        obj.usefulness("indifferent")
+        self.log.log("Exit game_tree->indifferent(self, evt)", ORPG_DEBUG)
+
+    def useful(self, evt):
+        self.log.log("Enter game_tree->useful(self, evt)", ORPG_DEBUG)
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        obj.usefulness("useful")
+        self.log.log("Exit game_tree->useful(self, evt)", ORPG_DEBUG)
+
+    def useless(self, evt):
+        self.log.log("Enter game_tree->useless(self, evt)", ORPG_DEBUG)
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        obj.usefulness("useless")
+        self.log.log("Exit game_tree->useless(self, evt)", ORPG_DEBUG)
+
+    def on_email(self,evt):
+        pass
+
+    def on_send_to(self, evt):
+        self.log.log("Enter game_tree->on_send_to(self, evt)", ORPG_DEBUG)
+        players = self.session.get_players()
+        opts = []
+        myid = self.session.get_id()
+        me = None
+        for p in players:
+            if p[2] != myid:
+                opts.append("("+p[2]+") " + self.strip_html(p))
+            else:
+                me = p
+        if len(opts):
+            players.remove(me)
+            dlg = orpgMultiCheckBoxDlg( None, opts, "Select Players:", "Send To", [] )
+            if dlg.ShowModal() == wx.ID_OK:
+                item = self.GetSelection()
+                obj = self.GetPyData(item)
+                xmldata = "<tree>" + self.xml.toxml(obj) + "</tree>"
+                selections = dlg.get_selections()
+                if len(selections) == len(opts):
+                    self.session.send(xmldata)
+                else:
+                    for s in selections:
+                        self.session.send(xmldata,players[s][2])
+            dlg.Destroy()
+        self.log.log("Exit game_tree->on_send_to(self, evt)", ORPG_DEBUG)
+
+    def on_icon(self, evt):
+        self.log.log("Enter game_tree->on_icon(self, evt)", ORPG_DEBUG)
+        icons = self.icons.keys()
+        icons.sort()
+        dlg = wx.SingleChoiceDialog(self,"Choose Icon?","Change Icon",icons)
+        if dlg.ShowModal() == wx.ID_OK:
+            key = dlg.GetStringSelection()
+            item = self.GetSelection()
+            obj = self.GetPyData(item)
+            obj.change_icon(key)
+        dlg.Destroy()
+        self.log.log("Exit game_tree->on_icon(self, evt)", ORPG_DEBUG)
+
+    def on_wizard(self, evt):
+        self.log.log("Enter game_tree->on_wizard(self, evt)", ORPG_DEBUG)
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        name = "New " + obj.master_dom.getAttribute("name")
+        icon = obj.master_dom.getAttribute("icon")
+        xml_data = "<nodehandler name=\""+name+"\" icon=\"" + icon + "\" module=\"core\" class=\"node_loader\" >"
+        xml_data += self.xml.toxml(obj)
+        xml_data += "</nodehandler>"
+        self.insert_xml(xml_data)
+        self.log.log(xml_data, ORPG_DEBUG)
+        self.log.log("Exit game_tree->on_wizard(self, evt)", ORPG_DEBUG)
+
+    def on_clone(self, evt):
+        self.log.log("Enter game_tree->on_clone(self, evt)", ORPG_DEBUG)
+        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
+            xml_dom = self.xml.parseXml(self.xml.toxml(obj))
+            xml_dom = xml_dom._get_firstChild()
+            parent = obj.master_dom._get_parentNode()
+            xml_dom = parent.insertBefore(xml_dom, obj.master_dom)
+            self.load_xml(xml_dom, parent_node, prev_sib)
+        self.log.log("Exit game_tree->on_clone(self, evt)", ORPG_DEBUG)
+
+    def on_save(self, evt):
+        """save node to a xml file"""
+        self.log.log("Enter game_tree->on_save(self, evt)", ORPG_DEBUG)
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        obj.on_save(evt)
+        os.chdir(self.root_dir)
+        self.log.log("Exit game_tree->on_save(self, evt)", ORPG_DEBUG)
+
+    def on_save_tree_as(self, evt):
+        self.log.log("Enter game_tree->on_save_tree_as(self, evt)", ORPG_DEBUG)
+        f = wx.FileDialog(self,"Select a file", self.last_save_dir,"","*.xml",wx.SAVE)
+        if f.ShowModal() == wx.ID_OK:
+            self.save_tree(f.GetPath())
+            self.last_save_dir, throwaway = os.path.split( f.GetPath() )
+        f.Destroy()
+        os.chdir(self.root_dir)
+        self.log.log("Exit game_tree->on_save_tree_as(self, evt)", ORPG_DEBUG)
+
+    def on_save_tree(self, evt=None):
+        self.log.log("Enter game_tree->on_save_tree(self, evt)", ORPG_DEBUG)
+        filename = self.settings.get_setting("gametree")
+        self.save_tree(filename)
+        self.log.log("Exit game_tree->on_save_tree(self, evt)", ORPG_DEBUG)
+
+    def save_tree(self, filename=orpg.dirpath.dir_struct["user"]+'tree.xml'):
+        self.log.log("Enter game_tree->save_tree(self, filename)", ORPG_DEBUG)
+        self.master_dom.setAttribute("version",GAMETREE_VERSION)
+        self.settings.set_setting("gametree",filename)
+        file = open(filename,"w")
+        file.write(self.xml.toxml(self.master_dom,1))
+        file.close()
+        self.log.log("Exit game_tree->save_tree(self, filename)", ORPG_DEBUG)
+
+    def on_load_new_tree(self, evt):
+        self.log.log("Enter game_tree->on_load_new_tree(self, evt)", ORPG_DEBUG)
+        f = wx.FileDialog(self,"Select a file", self.last_save_dir,"","*.xml",wx.OPEN)
+        if f.ShowModal() == wx.ID_OK:
+            self.load_tree(f.GetPath())
+            self.last_save_dir, throwaway = os.path.split( f.GetPath() )
+        f.Destroy()
+        os.chdir(self.root_dir)
+        self.log.log("Exit game_tree->on_load_new_tree(self, evt)", ORPG_DEBUG)
+
+    def on_insert_file(self, evt):
+        """loads xml file into the tree"""
+        self.log.log("Enter game_tree->on_insert_file(self, evt)", ORPG_DEBUG)
+        if self.last_save_dir == ".":
+            self.last_save_dir = orpg.dirpath.dir_struct["user"]
+        f = wx.FileDialog(self,"Select a file", self.last_save_dir,"","*.xml",wx.OPEN)
+        if f.ShowModal() == wx.ID_OK:
+            self.insert_xml(open(f.GetPath(),"r").read())
+            self.last_save_dir, throwaway = os.path.split( f.GetPath() )
+        f.Destroy()
+        os.chdir(self.root_dir)
+        self.log.log("Exit game_tree->on_insert_file(self, evt)", ORPG_DEBUG)
+
+    def on_insert_url(self, evt):
+        """loads xml url into the tree"""
+        self.log.log("Enter game_tree->on_insert_url(self, evt)", ORPG_DEBUG)
+        dlg = wx.TextEntryDialog(self,"URL?","Insert URL", "http://")
+        if dlg.ShowModal() == wx.ID_OK:
+            path = dlg.GetValue()
+            file = urllib.urlopen(path)
+            self.insert_xml(file.read())
+        dlg.Destroy()
+        self.log.log("Exit game_tree->on_insert_url(self, evt)", ORPG_DEBUG)
+
+    def on_insert_features(self, evt):
+        self.log.log("Enter game_tree->on_insert_features(self, evt)", ORPG_DEBUG)
+        self.insert_xml(open(orpg.dirpath.dir_struct["template"]+"feature.xml","r").read())
+        self.log.log("Exit game_tree->on_insert_features(self, evt)", ORPG_DEBUG)
+
+    def on_tree_prop(self, evt):
+        self.log.log("Enter game_tree->on_tree_prop(self, evt)", ORPG_DEBUG)
+        dlg = gametree_prop_dlg(self, self.settings)
+        if dlg.ShowModal() == wx.ID_OK:
+            pass
+        dlg.Destroy()
+        self.log.log("Exit game_tree->on_tree_prop(self, evt)", ORPG_DEBUG)
+
+    def on_node_design(self, evt):
+        self.log.log("Enter game_tree->on_node_design(self, evt)", ORPG_DEBUG)
+
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        obj.on_design(evt)
+
+        self.log.log("Exit game_tree->on_node_design(self, evt)", ORPG_DEBUG)
+
+    def on_node_use(self, evt):
+        self.log.log("Enter game_tree->on_node_use(self, evt)", ORPG_DEBUG)
+
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        obj.on_use(evt)
+
+        self.log.log("Exit game_tree->on_node_use(self, evt)", ORPG_DEBUG)
+
+    def on_node_pp(self, evt):
+        self.log.log("Enter game_tree->on_node_pp(self, evt)", ORPG_DEBUG)
+
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        obj.on_html_view(evt)
+
+        self.log.log("Exit game_tree->on_node_pp(self, evt)", ORPG_DEBUG)
+
+    def on_del(self, evt):
+        self.log.log("Enter game_tree->on_del(self, evt)", ORPG_DEBUG)
+        status_value = "none"
+        try:
+            item = self.GetSelection()
+            if item:
+                obj = self.GetPyData(item)
+                parent_obj = obj
+                try:
+                    status_value = parent_obj.master_dom.getAttribute('status')
+                    name = parent_obj.master_dom.getAttribute('name')
+                except:
+                    status_value = "none"
+                parent_obj = parent_obj.master_dom._get_parentNode()
+                while status_value!="useful" and status_value!="useless":
+                    try:
+                        status_value = parent_obj.getAttribute('status')
+                        name = parent_obj.getAttribute('name')
+                        if status_value == "useless":
+                            break
+                        elif status_value == "useful":
+                            break
+                    except:
+                        status_value = "none"
+                    try:
+                        parent_obj = parent_obj._get_parentNode()
+                    except:
+                        break
+                if status_value == "useful":
+                    dlg = wx.MessageDialog(self, `name` + "  And everything beneath it are considered useful.  \n\nAre you sure you want to delete this item?",'Important Item',wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
+                    if dlg.ShowModal() == wx.ID_YES:
+                        obj.delete()
+                else:
+                    obj.delete()
+        except:
+            if self.GetSelection() == self.GetRootItem():
+                msg = wx.MessageDialog(None,"You can't delete the root item.","Delete Error",wx.OK)
+            else:
+                msg = wx.MessageDialog(None,"Unknown error deleting node.","Delete Error",wx.OK)
+            msg.ShowModal()
+            msg.Destroy()
+
+        self.log.log("Exit game_tree->on_del(self, evt)", ORPG_DEBUG)
+
+    def on_about(self, evt):
+        self.log.log("Enter game_tree->on_about(self, evt)", ORPG_DEBUG)
+
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        about = MyAboutBox(self,obj.about())
+        about.ShowModal()
+        about.Destroy()
+
+        self.log.log("Exit game_tree->on_about(self, evt)", ORPG_DEBUG)
+
+    def on_send_to_map(self, evt):
+        self.log.log("Enter game_tree->on_send_to_map(self, evt)", ORPG_DEBUG)
+
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        if hasattr(obj,"on_send_to_map"):
+            obj.on_send_to_map(evt)
+
+        self.log.log("Exit game_tree->on_send_to_map(self, evt)", ORPG_DEBUG)
+
+    def insert_xml(self, txt):
+        self.log.log("Enter game_tree->insert_xml(self, txt)", ORPG_DEBUG)
+        #Updated to allow safe merging of gametree files
+        #without leaving an unusable and undeletable node.
+        #                               -- Snowdog 8/03
+        xml_dom = self.xml.parseXml(txt)
+        if xml_dom == None:
+            wx.MessageBox("Import Failed: Invalid or missing node data")
+            self.log.log("Import Failed: Invalid or missing node data", ORPG_DEBUG)
+            self.log.log("Exit game_tree->insert_xml(self, txt)", ORPG_DEBUG)
+            return
+        xml_temp = xml_dom._get_documentElement()
+
+        if not xml_temp:
+            wx.MessageBox("Error Importing Node or Tree")
+            self.log.log("Error Importing Node or Tree", ORPG_DEBUG)
+            self.log.log("Exit game_tree->insert_xml(self, txt)", ORPG_DEBUG)
+            return
+
+        if xml_temp._get_tagName() == "gametree":
+            children = xml_temp._get_childNodes()
+            for c in children:
+                self.load_xml(c, self.root)
+            self.log.log("Exit game_tree->insert_xml(self, txt)", ORPG_DEBUG)
+            return
+
+        if not xml_dom:
+            wx.MessageBox("XML Error")
+            self.log.log("XML Error", ORPG_DEBUG)
+            self.log.log("Exit game_tree->insert_xml(self, txt)", ORPG_DEBUG)
+            return
+
+        xml_dom = xml_dom._get_firstChild()
+        child = self.master_dom._get_firstChild()
+        xml_dom = self.master_dom.insertBefore(xml_dom,child)
+        self.load_xml(xml_dom,self.root,self.root)
+        self.log.log("Exit game_tree->insert_xml(self, txt)", ORPG_DEBUG)
+
+    def build_img_list(self):
+        """make image list"""
+        self.log.log("Enter game_tree->build_img_list(self)", ORPG_DEBUG)
+        helper = img_helper()
+        self.icons = { }
+        self._imageList= wx.ImageList(16,16,False)
+        man = open(orpg.dirpath.dir_struct["icon"]+"icons.xml","r")
+        xml_dom = self.xml.parseXml(man.read())
+        man.close()
+        xml_dom = xml_dom._get_documentElement()
+        node_list = xml_dom._get_childNodes()
+        for n in node_list:
+            key = n.getAttribute("name")
+            path = orpg.dirpath.dir_struct["icon"] + n.getAttribute("file")
+            img = helper.load_file(path)
+            self.icons[key] = self._imageList.Add(img)
+        self.SetImageList(self._imageList)
+        self.log.log("Exit game_tree->build_img_list(self)", ORPG_DEBUG)
+
+    def load_xml(self, xml_dom, parent_node, prev_node=None):
+        self.log.log("Enter game_tree->load_xml(self, xml_dom, parent_node, prev_node)", ORPG_DEBUG)
+        #add the first tree node
+        i = 0
+        text = xml_dom.getAttribute("name")
+        icon = xml_dom.getAttribute("icon")
+        if self.icons.has_key(icon):
+            i = self.icons[icon]
+        name = xml_dom._get_nodeName()
+        self.log.log("Text, icon and name set\n" + text + "\n" + icon + "\n" + name, ORPG_DEBUG)
+        if prev_node:
+            if prev_node == parent_node:
+                new_tree_node = self.PrependItem(parent_node, text, i, i)
+            else:
+                new_tree_node = self.InsertItem(parent_node,prev_node, text, i, i)
+        else:
+            new_tree_node = self.AppendItem(parent_node, text, i, i)
+
+        self.log.log("Node Added to tree", ORPG_DEBUG)
+        #create a nodehandler or continue loading xml into tree
+        if name == "nodehandler":
+            #wx.BeginBusyCursor()
+            self.log.log("We have a Nodehandler", ORPG_DEBUG)
+            try:
+                py_class = xml_dom.getAttribute("class")
+                self.log.log("nodehandler class: " + py_class, ORPG_DEBUG)
+                if not self.nodehandlers.has_key(py_class):
+                    raise Exception, "Unknown Nodehandler for " + py_class
+                self.nodes[self.id] = self.nodehandlers[py_class](xml_dom, new_tree_node)
+                self.SetPyData(new_tree_node, self.nodes[self.id])
+                self.log.log("Node Data set", ORPG_DEBUG)
+                bmp = self.nodes[self.id].get_scaled_bitmap(16,16)
+                if bmp:
+                    self.cached_load_of_image(bmp,new_tree_node,)
+                self.log.log("Node Icon loaded", ORPG_DEBUG)
+                self.id = self.id + 1
+            except Exception, er:
+                self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                #self.log.log("Error Info: " + xml_dom.getAttribute("class") + "\n" + str(er), "Tree Node Load Error", ORPG_GENERAL, True) # Arbitrary fix! TaS. (gametree should thread in the future.)
+		self.log.log("Error Info: " + xml_dom.getAttribute("class") + "\n" + str(er), ORPG_GENERAL, True)
+                self.Delete(new_tree_node)
+                parent = xml_dom._get_parentNode()
+                parent.removeChild(xml_dom)
+            #wx.EndBusyCursor()
+        self.log.log("Exit game_tree->load_xml(self, xml_dom, parent_node, prev_node)", ORPG_DEBUG)
+        return new_tree_node
+
+    def cached_load_of_image(self, bmp_in, new_tree_node):
+        self.log.log("Enter game_tree->cached_load_of_image(self, bmp_in, new_tree_node)", ORPG_DEBUG)
+        image_list = self.GetImageList()
+        img = wx.ImageFromBitmap(bmp_in)
+        img_data = img.GetData()
+        image_index = None
+        for key in self.image_cache.keys():
+            if self.image_cache[key] == str(img_data):
+                image_index = key
+                break
+
+        if image_index is None:
+            image_index = image_list.Add(bmp_in)
+            self.image_cache[image_index] = img_data
+        self.SetItemImage(new_tree_node,image_index)
+        self.SetItemImage(new_tree_node,image_index, wx.TreeItemIcon_Selected)
+        self.log.log("Exit game_tree->cached_load_of_image(self, bmp_in, new_tree_node)", ORPG_DEBUG)
+        return image_index
+
+
+    def on_rclick(self, evt):
+        self.log.log("Enter game_tree->on_rclick(self, evt)", ORPG_DEBUG)
+        pt = evt.GetPosition()
+        (item, flag) = self.HitTest(pt)
+        if item.IsOk():
+            obj = self.GetPyData(item)
+            self.SelectItem(item)
+            if(isinstance(obj,core.node_handler)):
+                obj.on_rclick(evt)
+            else:
+                self.PopupMenu(self.top_menu)
+        else:
+            self.PopupMenu(self.top_menu,pt)
+        self.log.log("Exit game_tree->on_rclick(self, evt)", ORPG_DEBUG)
+
+    def on_ldclick(self, evt):
+        self.log.log("Enter game_tree->on_ldclick(self, evt)", ORPG_DEBUG)
+        self.rename_flag = 0
+        pt = evt.GetPosition()
+        (item, flag) = self.HitTest(pt)
+        if item.IsOk():
+            obj = self.GetPyData(item)
+            self.SelectItem(item)
+            if(isinstance(obj,core.node_handler)):
+                if not obj.on_ldclick(evt):
+                    action = self.settings.get_setting("treedclick")
+                    if action == "use":
+                        obj.on_use(evt)
+                    elif action == "design":
+                        obj.on_design(evt)
+                    elif action == "print":
+                        obj.on_html_view(evt)
+                    elif action == "chat":
+                        self.on_send_to_chat(evt)
+        self.log.log("Exit game_tree->on_ldclick(self, evt)", ORPG_DEBUG)
+
+    def on_left_down(self, evt):
+        self.log.log("Enter game_tree->on_left_down(self, evt)", ORPG_DEBUG)
+        pt = evt.GetPosition()
+        (item, flag) = self.HitTest(pt)
+        if item.IsOk() and self.was_labeling:
+            self.SelectItem(item)
+            self.rename_flag = 0
+            self.was_labeling = 0
+        elif (flag & wx.TREE_HITTEST_ONITEMLABEL) == wx.TREE_HITTEST_ONITEMLABEL and self.IsSelected(item):
+            #  this next if tests to ensure that the mouse up occurred over a label, and not the icon
+            self.rename_flag = 1
+        else:
+            self.SelectItem(item)
+        evt.Skip()
+        self.log.log("Exit game_tree->on_left_down(self, evt)", ORPG_DEBUG)
+
+    def on_left_up(self, evt):
+        self.log.log("Enter game_tree->on_left_up(self, evt)", ORPG_DEBUG)
+        if self.dragging:
+            cur = wx.StockCursor(wx.CURSOR_ARROW)
+            self.SetCursor(cur)
+            self.dragging = False
+            pt = evt.GetPosition()
+            (item, flag) = self.HitTest(pt)
+            if item.IsOk():
+                obj = self.GetPyData(item)
+                self.SelectItem(item)
+                if(isinstance(obj,core.node_handler)):
+                    obj.on_drop(evt)
+                    self.drag_obj = None
+        self.log.log("Exit game_tree->on_left_up(self, evt)", ORPG_DEBUG)
+
+    def on_label_change(self, evt):
+        self.log.log("Enter game_tree->on_label_change(self, evt)", ORPG_DEBUG)
+        item = evt.GetItem()
+        txt = evt.GetLabel()
+        self.was_labeling = 0
+        self.rename_flag = 0
+        if txt != "":
+            obj = self.GetPyData(item)
+            obj.master_dom.setAttribute('name',txt)
+        else:
+            evt.Veto()
+        self.log.log("Exit game_tree->on_label_change(self, evt)", ORPG_DEBUG)
+
+    def on_label_begin(self, evt):
+        self.log.log("Enter game_tree->on_label_begin(self, evt)", ORPG_DEBUG)
+        if not self.rename_flag:
+            evt.Veto()
+        else:
+            self.was_labeling = 1
+            item = evt.GetItem()
+            if item == self.GetRootItem():
+                evt.Veto()
+        self.log.log("Exit game_tree->on_label_begin(self, evt)", ORPG_DEBUG)
+
+    def on_drag(self, evt):
+        self.log.log("Enter game_tree->on_drag(self, evt)", ORPG_DEBUG)
+        self.rename_flag = 0
+        item = self.GetSelection()
+        obj = self.GetPyData(item)
+        self.SelectItem(item)
+        if(isinstance(obj,core.node_handler) and obj.drag):
+            self.dragging = True
+            cur = wx.StockCursor(wx.CURSOR_HAND)
+            self.SetCursor(cur)
+            self.drag_obj = obj
+        self.log.log("Exit game_tree->on_drag(self, evt)", ORPG_DEBUG)
+
+    def is_parent_node(self, node, compare_node):
+        self.log.log("Enter game_tree->is_parent_node(self, node, compare_node)", ORPG_DEBUG)
+
+        parent_node = self.GetItemParent(node)
+        if compare_node == parent_node:
+
+            self.log.log("parent node", ORPG_DEBUG)
+            self.log.log("Exit game_tree->is_parent_node(self, node, compare_node)", ORPG_DEBUG)
+            return 1
+        elif parent_node == self.root:
+
+            self.log.log("not parent", ORPG_DEBUG)
+            self.log.log("Exit game_tree->is_parent_node(self, node, compare_node)", ORPG_DEBUG)
+            return 0
+        else:
+
+            self.log.log("Exit game_tree->is_parent_node(self, node, compare_node)", ORPG_DEBUG)
+            return self.is_parent_node(parent_node, compare_node)
+
+CTRL_TREE_FILE = wx.NewId()
+CTRL_YES = wx.NewId()
+CTRL_NO = wx.NewId()
+CTRL_USE = wx.NewId()
+CTRL_DESIGN = wx.NewId()
+CTRL_CHAT = wx.NewId()
+CTRL_PRINT = wx.NewId()
+
+class gametree_prop_dlg(wx.Dialog):
+    def __init__(self, parent, settings):
+        wx.Dialog.__init__(self, parent, wx.ID_ANY, "Game Tree Properties")
+        self.settings  = settings
+
+        #sizers
+        sizers = {}
+        sizers['but'] = wx.BoxSizer(wx.HORIZONTAL)
+        sizers['main'] = wx.BoxSizer(wx.VERTICAL)
+
+        #box sizers
+        box_sizers = {}
+        box_sizers["save"] = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, "Save On Exit"), wx.HORIZONTAL)
+        box_sizers["file"] = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, "Tree File"), wx.HORIZONTAL)
+        box_sizers["dclick"] = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, "Double Click Action"), wx.HORIZONTAL)
+        self.ctrls = {  CTRL_TREE_FILE : FileBrowseButtonWithHistory(self, wx.ID_ANY,  labelText="" ) ,
+                        CTRL_YES : wx.RadioButton(self, CTRL_YES, "Yes", style=wx.RB_GROUP),
+                        CTRL_NO : wx.RadioButton(self, CTRL_NO, "No"),
+                        CTRL_USE : wx.RadioButton(self, CTRL_USE, "Use", style=wx.RB_GROUP),
+                        CTRL_DESIGN : wx.RadioButton(self, CTRL_DESIGN, "Desgin"),
+                        CTRL_CHAT : wx.RadioButton(self, CTRL_CHAT, "Chat"),
+                        CTRL_PRINT : wx.RadioButton(self, CTRL_PRINT, "Pretty Print")
+                        }
+        self.ctrls[CTRL_TREE_FILE].SetValue(settings.get_setting("gametree"))
+        opt = settings.get_setting("SaveGameTreeOnExit")
+        self.ctrls[CTRL_YES].SetValue(opt=="1")
+        self.ctrls[CTRL_NO].SetValue(opt=="0")
+        opt = settings.get_setting("treedclick")
+        self.ctrls[CTRL_DESIGN].SetValue(opt=="design")
+        self.ctrls[CTRL_USE].SetValue(opt=="use")
+        self.ctrls[CTRL_CHAT].SetValue(opt=="chat")
+        self.ctrls[CTRL_PRINT].SetValue(opt=="print")
+        box_sizers['save'].Add(self.ctrls[CTRL_YES],0, wx.EXPAND)
+        box_sizers['save'].Add(wx.Size(10,10))
+        box_sizers['save'].Add(self.ctrls[CTRL_NO],0, wx.EXPAND)
+        box_sizers['file'].Add(self.ctrls[CTRL_TREE_FILE], 0, wx.EXPAND)
+        box_sizers['dclick'].Add(self.ctrls[CTRL_USE],0, wx.EXPAND)
+        box_sizers['dclick'].Add(wx.Size(10,10))
+        box_sizers['dclick'].Add(self.ctrls[CTRL_DESIGN],0, wx.EXPAND)
+        box_sizers['dclick'].Add(wx.Size(10,10))
+        box_sizers['dclick'].Add(self.ctrls[CTRL_CHAT],0, wx.EXPAND)
+        box_sizers['dclick'].Add(wx.Size(10,10))
+        box_sizers['dclick'].Add(self.ctrls[CTRL_PRINT],0, wx.EXPAND)
+
+        # buttons
+        sizers['but'].Add(wx.Button(self, wx.ID_OK, "Apply"), 1, wx.EXPAND)
+        sizers['but'].Add(wx.Size(10,10))
+        sizers['but'].Add(wx.Button(self, wx.ID_CANCEL, "Cancel"), 1, wx.EXPAND)
+        sizers['main'].Add(box_sizers['save'], 1, wx.EXPAND)
+        sizers['main'].Add(box_sizers['file'], 1, wx.EXPAND)
+        sizers['main'].Add(box_sizers['dclick'], 1, wx.EXPAND)
+        sizers['main'].Add(sizers['but'], 0, wx.EXPAND|wx.ALIGN_BOTTOM )
+
+        #sizers['main'].SetDimension(10,10,csize[0]-20,csize[1]-20)
+        self.SetSizer(sizers['main'])
+        self.SetAutoLayout(True)
+        self.Fit()
+        self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)
+
+    def on_ok(self,evt):
+        self.settings.set_setting("gametree",self.ctrls[CTRL_TREE_FILE].GetValue())
+        self.settings.set_setting("SaveGameTreeOnExit",str(self.ctrls[CTRL_YES].GetValue()))
+        if self.ctrls[CTRL_USE].GetValue():
+            self.settings.set_setting("treedclick","use")
+        elif self.ctrls[CTRL_DESIGN].GetValue():
+            self.settings.set_setting("treedclick","design")
+        elif self.ctrls[CTRL_PRINT].GetValue():
+            self.settings.set_setting("treedclick","print")
+        elif self.ctrls[CTRL_CHAT].GetValue():
+            self.settings.set_setting("treedclick","chat")
+        self.EndModal(wx.ID_OK)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/gametree_version.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,3 @@
+### this file holds the gametree version ###
+
+GAMETREE_VERSION = "1.0"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/StarWarsd20.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1833 @@
+# 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: StarWarsd20.py
+# Author: Chris Davis; Mark Twombley
+# Maintainer: Mark Twombley
+# Version:
+#   $Id: StarWarsd20.py,v 1.18 2006/11/15 12:11:23 digitalxero Exp $
+#
+# Description: The file contains code for the StarWarsd20 nodehanlers
+#
+
+__version__ = "$Id: StarWarsd20.py,v 1.18 2006/11/15 12:11:23 digitalxero Exp $"
+
+from core import *
+
+SWD20_EXPORT = wx.NewId()
+############################
+## StarWarsd20 character node handler
+############################
+##The whole look and easy of use redone by Digitalxero
+class container_handler(node_handler):
+    """ should not be used! only a base class!
+    <nodehandler name='?'  module='core' class='container_handler'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.load_children()
+
+    def load_children(self):
+        children = self.master_dom._get_childNodes()
+        for c in children:
+            self.tree.load_xml(c,self.mytree_node)
+
+
+    def on_drop(self,evt):
+        drag_obj = self.tree.drag_obj
+        #if self.is_my_child(self.mytree_node,drag_obj.mytree_node):
+        #    return
+        if drag_obj == self:
+            return
+        opt = wx.MessageBox("Add node as child?","Container Node",wx.YES_NO|wx.CANCEL)
+        if opt == wx.YES:
+            xml_dom = self.tree.drag_obj.delete()
+            xml_dom = self.master_dom.insertBefore(xml_dom,None)
+            self.tree.load_xml(xml_dom, self.mytree_node)
+            self.tree.Expand(self.mytree_node)
+        elif opt == wx.NO:
+            node_handler.on_drop(self,evt)
+
+    def tohtml(self):
+        cookie = 0
+        html_str = "<table border=\"1\" ><tr><td>"
+        html_str += "<b>"+self.master_dom.getAttribute("name") + "</b>"
+        html_str += "</td></tr>\n"
+        html_str += "<tr><td>"
+        max = tree.GetChildrenCount(handler.mytree_node)
+        try:
+            (child,cookie)=self.tree.GetFirstChild(self.mytree_node,cookie)
+        except: # If this happens we probably have a newer version of wxPython
+            (child,cookie)=self.tree.GetFirstChild(self.mytree_node)
+        obj = self.tree.GetPyData(child)
+        for m in xrange(max):
+            html_str += "<p>" + obj.tohtml()
+            if m < max-1:
+                child = self.tree.GetNextSibling(child)
+                obj = self.tree.GetPyData(child)
+        html_str += "</td></tr></table>"
+        return html_str
+
+    def get_size_constraint(self):
+        return 1
+
+    def get_char_name( self ):
+        return self.child_handlers['general'].get_char_name()
+
+##    def set_char_pp(self,attr,evl):
+##        return self.child_handlers['pp'].set_char_pp(attr,evl)
+##
+##    def get_char_pp( self, attr ):
+##        return self.child_handlers['pp'].get_char_pp(attr)
+##
+    def get_char_lvl( self, attr ):
+        return self.child_handlers['classes'].get_char_lvl(attr)
+
+
+
+class SWd20char_handler(node_handler):
+    """ Node handler for a SWd20 charactor
+        <nodehandler name='?'  module='StarWarsd20' class='SWd20char_handler'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('howtouse','HowTO use this tool',SWd20howto,'note')
+        self.new_child_handler('general','General Information',SWd20general,'gear')
+        self.new_child_handler('inventory','Money and Inventory',SWd20inventory,'money')
+        self.new_child_handler('abilities','Abilities Scores',SWd20ability,'gear')
+        self.new_child_handler('classes','Classes',SWd20classes,'knight')
+        self.new_child_handler('saves','Saves',SWd20saves,'skull')
+        self.new_child_handler('skills','Skills',SWd20skill,'book')
+        self.new_child_handler('feats','Feats',SWd20feats,'book')
+        self.new_child_handler('wp','Wound Points',SWd20hp,'gear')
+        self.new_child_handler('vp','Vitality Points',SWd20vp,'gear')
+        self.new_child_handler('attacks','Attacks',SWd20attacks,'spears')
+        self.new_child_handler('ac','Armor',SWd20armor,'spears')
+        self.myeditor = None
+
+
+    def on_version(self,old_version):
+        node_handler.on_version(self,old_version)
+        if old_version == "":
+            tmp = open(orpg.dirpath.dir_struct["nodes"]+"StarWars_d20character.xml","r")
+            xml_dom = parseXml_with_dlg(self.tree,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            ## add new nodes
+            for tag in ("howtouse","inventory","powers","divine","pp"):
+                node_list = xml_dom.getElementsByTagName(tag)
+                self.master_dom.appendChild(node_list[0])
+
+            ## add new atts
+            melee_attack = self.master_dom.getElementsByTagName('melee')[0]
+            melee_attack.setAttribute("second","0")
+            melee_attack.setAttribute("third","0")
+            melee_attack.setAttribute("forth","0")
+            melee_attack.setAttribute("fifth","0")
+            melee_attack.setAttribute("sixth","0")
+            range_attack = self.master_dom.getElementsByTagName('ranged')[0]
+            range_attack.setAttribute("second","0")
+            range_attack.setAttribute("third","0")
+            range_attack.setAttribute("forth","0")
+            range_attack.setAttribute("fifth","0")
+            range_attack.setAttribute("sixth","0")
+
+            gen_list = self.master_dom.getElementsByTagName('general')[0]
+
+            for tag in ("currentxp","xptolevel"):
+                node_list = xml_dom.getElementsByTagName(tag)
+                gen_list.appendChild(node_list[0])
+            ## temp fix
+            #parent = self.master_dom._get_parentNode()
+            #old_dom = parent.replaceChild(xml_dom,self.master_dom)
+            #self.master_dom = xml_dom
+        print old_version
+
+
+    def get_char_name( self ):
+        return self.child_handlers['general'].get_char_name()
+
+##    def set_char_pp(self,attr,evl):
+##        return self.child_handlers['pp'].set_char_pp(attr,evl)
+##
+##    def get_char_pp( self, attr ):
+##        return self.child_handlers['pp'].get_char_pp(attr)
+
+    def get_char_lvl( self, attr ):
+        return self.child_handlers['classes'].get_char_lvl(attr)
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+
+    def tohtml(self):
+        html_str = "<table><tr><td colspan=2 >"+self.child_handlers['general'].tohtml()+"</td></tr>"
+        html_str += "<tr><td width='50%' valign=top >"+self.child_handlers['abilities'].tohtml()
+        html_str += "<P>" + self.child_handlers['saves'].tohtml()
+        html_str += "<P>" + self.child_handlers['attacks'].tohtml()
+        html_str += "<P>" + self.child_handlers['ac'].tohtml()
+        html_str += "<P>" + self.child_handlers['feats'].tohtml()
+        #html_str += "<P>" + self.child_handlers['spells'].tohtml()
+        #html_str += "<P>" + self.child_handlers['divine'].tohtml()
+        #html_str += "<P>" + self.child_handlers['powers'].tohtml()
+        html_str += "<P>" + self.child_handlers['inventory'].tohtml() +"</td>"
+        html_str += "<td width='50%' valign=top >"+self.child_handlers['classes'].tohtml()
+        html_str += "<P>" + self.child_handlers['wp'].tohtml()
+        html_str += "<P>" + self.child_handlers['vp'].tohtml()
+        #html_str += "<P>" + self.child_handlers['pp'].tohtml()
+        html_str += "<P>" + self.child_handlers['skills'].tohtml() +"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+    def about(self):
+        html_str = "<img src='" + orpg.dirpath.dir_struct["icon"]+'d20_logo.gif' "><br /><b>d20 Character Tool v0.7 beta</b>"
+        html_str += "<br />by Chris Davis<br />chris@rpgarchive.com"
+        return html_str
+
+    def get_char_name( self ):
+        return self.child_handlers['general'].get_char_name()
+    def get_armor_class( self ):
+        return self.child_handlers['ac'].get_armor_class()
+    def get_max_hp( self ):
+        return self.child_handlers['wp'].get_max_hp()
+    def get_current_hp( self ):
+        return self.child_handlers['wp'].get_current_hp()
+    def get_max_vp( self ):
+        return self.child_handlers['vp'].get_max_vp()
+    def get_current_vp( self ):
+        return self.child_handlers['vp'].get_current_vp()
+
+##    def set_char_pp(self,attr,evl):
+##        return self.child_handlers['pp'].set_char_pp(attr,evl)
+##
+##    def get_char_pp( self, attr ):
+##        return self.child_handlers['pp'].get_char_pp(attr)
+
+    def get_char_lvl( self, attr ):
+        return self.child_handlers['classes'].get_char_lvl(attr)
+
+class tabbed_panel(wx.Notebook):
+    def __init__(self, parent, handler, mode):
+        wx.Notebook.__init__(self, parent, -1, size=(1200,800))
+        self.handler = handler
+        self.parent = parent
+        tree = self.handler.tree
+        max = tree.GetChildrenCount(handler.mytree_node)
+
+        cookie = 0
+        max = tree.GetChildrenCount(handler.mytree_node)
+        try:
+            (child,cookie)=tree.GetFirstChild(handler.mytree_node,cookie)
+        except: # If this happens we probably have a newer version of wxPython
+            (child,cookie)=tree.GetFirstChild(handler.mytree_node)
+        obj = tree.GetPyData(child)
+        for m in xrange(max):
+            if mode == 1:
+                panel = obj.get_design_panel(self)
+            else:
+                panel = obj.get_use_panel(self)
+            name = obj.master_dom.getAttribute("name")
+
+            if panel:
+                self.AddPage(panel,name)
+            if m < max-1:
+                child = tree.GetNextSibling(child)
+                obj = tree.GetPyData(child)
+
+
+    def about(self):
+        html_str = "<img src='" + orpg.dirpath.dir_struct["icon"]+'d20_logo.gif' "><br /><b>d20 Character Tool v0.7 beta</b>"
+        html_str += "<br />by Chris Davis<br />chris@rpgarchive.com"
+        return html_str
+
+    def get_char_name( self ):
+        return self.child_handlers['general'].get_char_name()
+
+##    def set_char_pp(self,attr,evl):
+##        return self.child_handlers['pp'].set_char_pp(attr,evl)
+##
+##    def get_char_pp( self, attr ):
+##        return self.child_handlers['pp'].get_char_pp(attr)
+
+    def get_char_lvl( self, attr ):
+        return self.child_handlers['classes'].get_char_lvl(attr)
+
+class SWd20_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by SWd20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        node_handler.__init__(self,xml_dom, tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+        if self.myeditor == None or self.myeditor.destroyed:
+            title = self.master_dom.getAttribute('name') + " Editor"
+            self.myeditor = orpgPFrame(self.frame,title,orpg.dirpath.dir_struct["icon"]+'grid.ico')
+            wnd = self.get_design_panel(self.myeditor)
+            self.myeditor.panel = wnd
+            self.wnd = wnd
+            self.myeditor.Show(1)
+        else:
+            self.myeditor.Raise()
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+
+class SWd20skill(SWd20_char_child):
+    """ Node Handler for skill.  This handler will be
+        created by SWd20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        SWd20_char_child.__init__(self, xml_dom, tree_node, parent)
+        tree = self.tree
+        icons = self.tree.icons
+        node_list = self.master_dom.getElementsByTagName('skill')
+        self.skills={}
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.skills[name] = n
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+
+    def get_mod(self,name):
+        skill = self.skills[name]
+        stat = skill.getAttribute('stat')
+        ac = int(skill.getAttribute('armorcheck'))
+        if ac:
+            ac = self.char_hander.child_handlers['ac'].get_check_pen()
+        stat_mod = self.char_hander.child_handlers['abilities'].get_mod(stat)
+        rank = int(skill.getAttribute('rank'))
+        misc = int(skill.getAttribute('misc'))
+        total = stat_mod + rank + misc + ac
+        return total
+
+    def on_rclick(self,evt):
+        #updated with code for untrained use check
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        skill = self.skills[name]
+        rank = int(skill.getAttribute('rank'))
+        untrained = int(skill.getAttribute('untrained'))
+        chat = self.chat
+        if item == self.mytree_node:
+            SWd20_char_child.on_ldclick(self,evt)
+        else:
+            if rank == 0 and untrained == 0:
+                chat.Post('Can\'t use untrained!',True,True)
+            else:
+                mod = self.get_mod(name)
+                if mod >= 0:
+                    mod1 = "+"
+                else:
+                    mod1 = ""
+                txt = '%s Skill Check: [1d20%s%s]' % (name, mod1, mod)
+                chat.ParsePost(txt,True,True)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,skill_grid,"Skills")
+        wnd.title = "Skills (edit)"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 ><th width='30%'>Skill</th><th>Key</th>
+                    <th>Rank</th><th>Abil</th><th>Misc</th><th>Total</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('skill')
+        for n in node_list:
+            name = n.getAttribute('name')
+            stat = n.getAttribute('stat')
+            rank = n.getAttribute('rank')
+            html_str = html_str + "<tr ALIGN='center'><td>"+name+"</td><td>"+stat+"</td><td>"+rank+"</td>"
+            stat_mod = str(self.char_hander.child_handlers['abilities'].get_mod(stat))
+            misc = n.getAttribute('misc')
+            mod = str(self.get_mod(name))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str = html_str + "<td>"+stat_mod+"</td><td>"+misc+'</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+
+class SWd20ability(SWd20_char_child):
+    """ Node Handler for ability.   This handler will be
+        created by SWd20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        SWd20_char_child.__init__(self, xml_dom, tree_node, parent)
+        self.abilities = {}
+        node_list = self.master_dom.getElementsByTagName('stat')
+        tree = self.tree
+        icons = tree.icons
+        for n in node_list:
+            name = n.getAttribute('abbr')
+            self.abilities[name] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['gear'], icons['gear'] )
+            tree.SetPyData( new_tree_node, self )
+
+    def on_rclick( self, evt ):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        if item == self.mytree_node:
+            SWd20_char_child.on_ldclick( self, evt )
+        else:
+            mod = self.get_mod( name )
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s check: [1d20%s%s]' % ( name, mod1, mod )
+            chat.ParsePost( txt, True, True )
+
+    def get_mod(self,abbr):
+        score = int(self.abilities[abbr].getAttribute('base'))
+        mod = (score - 10) / 2
+        return mod
+
+    def set_score(self,abbr,score):
+        if score >= 0:
+            self.abilities[abbr].setAttribute("base",str(score))
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,abil_grid,"Abilities")
+        wnd.title = "Abilities (edit)"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100%><tr BGCOLOR=#E9E9E9 ><th width='50%'>Ability</th>
+                    <th>Base</th><th>Modifier</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('stat')
+        for n in node_list:
+            name = n.getAttribute('name')
+            abbr = n.getAttribute('abbr')
+            base = n.getAttribute('base')
+            mod = str(self.get_mod(abbr))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str = html_str + "<tr ALIGN='center'><td>"+name+"</td><td>"+base+'</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+class SWd20saves(SWd20_char_child):
+    """ Node Handler for saves.   This handler will be
+        created by SWd20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        SWd20_char_child.__init__(self, xml_dom, tree_node, parent)
+        tree = self.tree
+        icons = self.tree.icons
+        node_list = self.master_dom.getElementsByTagName('save')
+        self.saves={}
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.saves[name] = n
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+
+    def get_mod(self,name):
+        save = self.saves[name]
+        stat = save.getAttribute('stat')
+        stat_mod = self.char_hander.child_handlers['abilities'].get_mod(stat)
+        base = int(save.getAttribute('base'))
+        miscmod = int(save.getAttribute('miscmod'))
+#        magmod = int(save.getAttribute('magmod'))
+#        total = stat_mod + base + miscmod + magmod
+        total = stat_mod + base + miscmod
+        return total
+
+    def on_rclick(self,evt):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            SWd20_char_child.on_ldclick(self,evt)
+            #wnd = save_grid(self.frame.note,self)
+            #wnd.title = "Saves"
+            #self.frame.add_panel(wnd)
+        else:
+            mod = self.get_mod(name)
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s save: [1d20%s%s]' % (name, mod1, mod)
+            chat.ParsePost( txt, True, True )
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,save_grid,"Saves")
+        wnd.title = "Saves"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 ><th width='30%'>Save</th>
+                    <th>Key</th><th>Base</th><th>Abil</th><th>Magic</th>
+                    <th>Misc</th><th>Total</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('save')
+        for n in node_list:
+            name = n.getAttribute('name')
+            stat = n.getAttribute('stat')
+            base = n.getAttribute('base')
+            html_str = html_str + "<tr ALIGN='center'><td>"+name+"</td><td>"+stat+"</td><td>"+base+"</td>"
+            stat_mod = str(self.char_hander.child_handlers['abilities'].get_mod(stat))
+            mag = n.getAttribute('magmod')
+            misc = n.getAttribute('miscmod')
+            mod = str(self.get_mod(name))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str = html_str + "<td>"+stat_mod+"</td><td>"+mag+"</td>"
+            html_str = html_str + '<td>'+misc+'</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+
+class SWd20general(SWd20_char_child):
+    """ Node Handler for general information.   This handler will be
+        created by SWd20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        SWd20_char_child.__init__(self, xml_dom, tree_node, parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,gen_grid,"General Information")
+        wnd.title = "General Info"
+        return wnd
+
+    def tohtml(self):
+        n_list = self.master_dom._get_childNodes()
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>General Information</th></tr><tr><td>"
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+            html_str += "<B>"+n._get_tagName().capitalize() +":</B> "
+            html_str += t_node._get_nodeValue() + ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def on_name_change(self,name):
+        self.char_hander.rename(name)
+
+    def get_char_name( self ):
+        node = self.master_dom.getElementsByTagName( 'name' )[0]
+        t_node = safe_get_text_node( node )
+        return t_node._get_nodeValue()
+
+
+class SWd20classes(SWd20_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by SWd20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        SWd20_char_child.__init__(self, xml_dom, tree_node, parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,class_panel,"Classes")
+        wnd.title = "Classes"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Classes</th></tr><tr><td>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += n.getAttribute('name') + " ("+n.getAttribute('level')+"), "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        node_list = self.master_dom.getElementsByTagName('class')
+        for n in node_list:
+            lvl = n.getAttribute('level')
+            type = n.getAttribute('name')
+            if attr == "level":
+                return lvl
+            elif attr == "class":
+                return type
+
+
+class SWd20feats(SWd20_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        SWd20_char_child.__init__(self, xml_dom, tree_node, parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,feat_panel,"Feats")
+        wnd.title = "Feats"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Feats</th></tr><tr><td>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+class SWd20howto(SWd20_char_child):
+    """ Node Handler for hit points.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        SWd20_char_child.__init__(self, xml_dom, tree_node, parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,howto_panel,"How To")
+        wnd.title = "How To"
+        return wnd
+
+class SWd20inventory(SWd20_char_child):
+    """ Node Handler for general information.   This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self, xml_dom, tree_node, parent):
+        SWd20_char_child.__init__(self, xml_dom, tree_node, parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,inventory_grid,"Inventory")
+        wnd.title = "General Info"
+        return wnd
+
+    def tohtml(self):
+        n_list = self.master_dom._get_childNodes()
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>General Information</th></tr><tr><td>"
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+            html_str += "<B>"+n._get_tagName().capitalize() +":</B> "
+            html_str += t_node._get_nodeValue() + "<br />"
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def on_name_change(self,name):
+        self.char_hander.rename(name)
+
+    def get_char_name( self ):
+        node = self.master_dom.getElementsByTagName( 'name' )[0]
+        t_node = safe_get_text_node( node )
+        return t_node._get_nodeValue()
+
+class SWd20hp(SWd20_char_child):
+    """ Node Handler for hit points.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        SWd20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,hp_panel,"Wound Points")
+        wnd.title = "Wound Points"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th colspan=4>Wound Points</th></tr>"
+        html_str += "<tr><th>Max:</th><td>"+self.master_dom.getAttribute('max')+"</td>"
+        html_str += "<th>Current:</th><td>"+self.master_dom.getAttribute('current')+"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+    def get_max_hp( self ):
+        try:
+            return eval( self.master_dom.getAttribute( 'max' ) )
+        except:
+            return 0
+    def get_current_hp( self ):
+        try:
+            return eval( self.master_dom.getAttribute( 'current' ) )
+        except:
+            return 0
+
+class SWd20vp(SWd20_char_child):
+    """ Node Handler for hit points.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        SWd20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,vp_panel,"Vitality Points")
+        wnd.title = "Vitality Points"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th colspan=4>Vitality Points</th></tr>"
+        html_str += "<tr><th>Max:</th><td>"+self.master_dom.getAttribute('max')+"</td>"
+        html_str += "<th>Current:</th><td>"+self.master_dom.getAttribute('current')+"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+    def get_max_vp( self ):
+        try:
+            return eval( self.master_dom.getAttribute( 'max' ) )
+        except:
+            return 0
+    def get_current_vp( self ):
+        try:
+            return eval( self.master_dom.getAttribute( 'current' ) )
+        except:
+            return 0
+
+class SWd20attacks(SWd20_char_child):
+    """ Node Handler for attacks.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        SWd20_char_child.__init__(self,xml_dom,tree_node,parent)
+        node_list = self.master_dom.getElementsByTagName('melee')
+        self.melee = node_list[0]
+        node_list = self.master_dom.getElementsByTagName('ranged')
+        self.ranged = node_list[0]
+        self.refresh_weapons()
+
+    def refresh_weapons(self):
+        self.weapons = {}
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('weapon')
+        for n in node_list:
+            name = n.getAttribute('name')
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['sword'],icons['sword'])
+            tree.SetPyData(new_tree_node,self)
+            self.weapons[name]=n
+
+    def get_mod(self,type='m'):
+        (base, base2, base3, base4, base5, base6, stat_mod, misc) = self.get_attack_data(type)
+        return base + misc + stat_mod
+
+    def get_attack_data(self,type='m'):
+        if type=='m' or type=='0':
+            stat_mod = self.char_hander.child_handlers['abilities'].get_mod('Str')
+            temp = self.melee
+        else:
+            stat_mod = self.char_hander.child_handlers['abilities'].get_mod('Dex')
+            temp = self.ranged
+        base = int(temp.getAttribute('base'))
+        base2 = int(temp.getAttribute('second'))
+        base3 = int(temp.getAttribute('third'))
+        base4 = int(temp.getAttribute('forth'))
+        base5 = int(temp.getAttribute('fifth'))
+        base6 = int(temp.getAttribute('sixth'))
+        misc = int(temp.getAttribute('misc'))
+        return (base, base2, base3, base4, base5, base6, stat_mod ,misc)
+
+    def on_rclick(self,evt):
+        #removed the DnD specific code
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            SWd20_char_child.on_ldclick(self,evt)
+            #self.frame.add_panel(self.get_design_panel(self.frame.note))
+        else:
+            mod = int(self.weapons[name].getAttribute('mod'))
+            if self.weapons[name].getAttribute('range') == '0':
+                mod = mod + self.get_mod('m')
+                if mod >= 0:
+                    mod1 = "+"
+                else:
+                    mod1 = ""
+            else:
+                mod = mod + self.get_mod('r')
+                if mod >= 0:
+                    mod1 = "+"
+                else:
+                    mod1 = ""
+            chat = self.chat
+            dmg = self.weapons[name].getAttribute('damage')
+            lvl = self.get_char_lvl('level')
+            cname = self.char_hander.get_char_name()
+            txt = '%s %s Attack Roll: [1d20%s%s] ===> DMG: [%s%s%s]' % (cname, name, mod1, mod, dmg, mod1, mod)
+            chat.ParsePost( txt, True, False )
+            temp = self.melee
+            stat_mod = self.char_hander.child_handlers['abilities'].get_mod('Str')
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,attack_panel,"Attacks")
+        wnd.title = "Attacks"
+        return wnd
+
+    def get_char_lvl( self, attr ):
+        return self.char_hander.get_char_lvl(attr)
+
+    def tohtml(self):
+        melee = self.get_attack_data('m')
+        ranged = self.get_attack_data('r')
+        html_str = """<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Attack</th>
+                    <th>Total</th><th >Base</th><th>Abil</th><th>Misc</th></tr>"""
+        html_str += "<tr ALIGN='center' ><th >Melee:</th>"
+        html_str += "<td>"+str(melee[0]+melee[1]+melee[2])+"</td>"
+        html_str += "<td>"+str(melee[0])+"</td>"
+        html_str += "<td>"+str(melee[1])+"</td>"
+        html_str += "<td>"+str(melee[2])+"</td></tr>"
+
+        html_str += "<tr ALIGN='center' ><th >Ranged:</th>"
+        html_str += "<td>"+str(ranged[0]+ranged[1]+ranged[2])+"</td>"
+        html_str += "<td>"+str(ranged[0])+"</td>"
+        html_str += "<td>"+str(ranged[1])+"</td>"
+        html_str += "<td>"+str(ranged[2])+"</td></tr></table>"
+
+        n_list = self.master_dom.getElementsByTagName('weapon')
+        for n in n_list:
+            mod = n.getAttribute('mod')
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            ran = n.getAttribute('range')
+            total = str(int(mod) + self.get_mod(ran))
+            html_str += """<P><table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th colspan=2>Weapon</th>
+                    <th>Attack</th><th >Damage</th><th>Critical</th></tr>"""
+            html_str += "<tr ALIGN='center' ><td  colspan=2>"+n.getAttribute('name')+"</td><td>"+total+"</td>"
+            html_str += "<td>"+n.getAttribute('damage')+"</td><td>"+n.getAttribute('critical')+"</td></tr>"
+            html_str += """<tr BGCOLOR=#E9E9E9 ><th>Range</th><th>Weight</th>
+                        <th>Type</th><th>Size</th><th>Misc Mod</th></tr>"""
+            html_str += "<tr ALIGN='center'><td>"+ran+"</td><td>"+n.getAttribute('weight')+"</td>"
+            html_str += "<td>"+n.getAttribute('type')+"</td><td>"+n.getAttribute('size')+"</td>"
+            html_str += '<td>%s%s</td></tr></table>' % (mod1, mod)
+        return html_str
+
+class SWd20armor(SWd20_char_child):
+    """ Node Handler for ac.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        SWd20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_total_weight(self):
+        return self.get_total('weight')
+
+    def get_check_pen(self):
+        return self.get_total('checkpenalty')
+
+    def get_armor_class(self):
+        ac_total = 10
+        ac_total += self.get_total('bonus')
+        dex_mod = self.char_hander.child_handlers['abilities'].get_mod('Dex')
+        max_dex = self.get_max_dex()
+        if dex_mod < max_dex:
+            ac_total += dex_mod
+        else:
+            ac_total += max_dex
+        return ac_total
+
+    def get_max_dex(self):
+        armor_list = self.master_dom.getElementsByTagName('armor')
+        dex = 10
+        for a in armor_list:
+            temp = int(a.getAttribute("maxdex"))
+            if temp < dex:
+                dex = temp
+        return dex
+
+    def get_total(self,attr):
+        armor_list = self.master_dom.getElementsByTagName('armor')
+        total = 0
+        for a in armor_list:
+            total += int(a.getAttribute(attr))
+        return total
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,ac_panel,"Armor")
+        wnd.title = "Armor"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>AC</th>
+                    <th>Check Penalty</th><th >Spell Failure</th><th>Max Dex</th><th>Total Weight</th></tr>"""
+        html_str += "<tr ALIGN='center' ><td>"+str(self.get_armor_class())+"</td>"
+        html_str += "<td>"+str(self.get_check_pen())+"</td>"
+        html_str += "<td>"+str(self.get_spell_failure())+"</td>"
+        html_str += "<td>"+str(self.get_max_dex())+"</td>"
+        html_str += "<td>"+str(self.get_total_weight())+"</td></tr></table>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += """<P><table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th colspan=3>Armor</th>
+                    <th>Type</th><th >Bonus</th></tr>"""
+            html_str += "<tr ALIGN='center' ><td  colspan=3>"+n.getAttribute('name')+"</td>"
+            html_str += "<td>"+n.getAttribute('type')+"</td>"
+            html_str += "<td>"+n.getAttribute('bonus')+"</td></tr>"
+            html_str += """<tr BGCOLOR=#E9E9E9 ><th>Check Penalty</th><th>Spell Failure</th>
+                        <th>Max Dex</th><th>Speed</th><th>Weight</th></tr>"""
+            html_str += "<tr ALIGN='center'><td>"+n.getAttribute('checkpenalty')+"</td>"
+            html_str += "<td>"+n.getAttribute('maxdex')+"</td>"
+            html_str += "<td>"+n.getAttribute('speed')+"</td>"
+            html_str += "<td>"+n.getAttribute('weight')+"</td></tr></table>"
+        return html_str
+
+
+########################
+##  d20 char windows
+########################
+
+class base_panel(wx.Panel):
+    def __init__(self, parent):
+        wx.Panel.__init__(self, parent, -1)
+
+        #self.build_ctrls()
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        #self.splitter.SetDimensions(0,0,s[0],s[1])
+
+class outline_panel(wx.Panel):
+    def __init__(self, parent, handler, wnd, txt,):
+        wx.Panel.__init__(self, parent, -1)
+        self.panel = wnd(self,handler)
+        self.outline = wx.StaticBox(self,-1,txt)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.panel.SetDimensions(20,20,s[0]-40,s[1]-40)
+        self.outline.SetDimensions(5,5,s[0]-10,s[1]-10)
+
+class char_panel(wx.ScrolledWindow):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'TWO')
+        wx.ScrolledWindow.__init__(self, parent, -1,style=wx.VSCROLL | wx.SUNKEN_BORDER  )
+        self.height = 1200
+        self.SetScrollbars(10, 10,80, self.height/10)
+        self.main_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.panels = {}
+        keys = handler.child_handlers.keys()
+        for k in keys:
+            self.panels[k] = handler.child_handlers[k].get_design_panel(self, [k])
+        self.sub_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.sub_sizer2 = wx.BoxSizer(wx.VERTICAL)
+        self.sub_sizer.Add(self.panels['general'], 1, wx.EXPAND)
+        self.sub_sizer.Add(self.panels['abilities'], 1, wx.EXPAND)
+
+        self.sub_sizer.Add(self.panels['attacks'], 2, wx.EXPAND)
+        self.sub_sizer.Add(self.panels['ac'], 1, wx.EXPAND)
+        #self.sub_sizer.Add(self.panels['spells'], 1, wx.EXPAND)
+
+        self.sub_sizer2.Add(self.panels['classes'], 2, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['wp'], 1, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['vp'], 1, wx.EXPAND)
+        #self.sub_sizer2.Add(self.panels['pp'], 1, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['saves'], 2, wx.EXPAND)
+
+        self.sub_sizer2.Add(self.panels['feats'], 2, wx.EXPAND)
+        #self.sub_sizer2.Add(self.panels['powers'], 2, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['skills'], 3, wx.EXPAND)
+
+        self.main_sizer.Add(self.sub_sizer,   1, wx.EXPAND)
+        self.main_sizer.Add(self.sub_sizer2,   1, wx.EXPAND)
+        self.panels['abilities'].panel.char_wnd = self
+        self.SetSizer(self.main_sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.SetScrollbars(10, 10,s[0]/10, self.height/10)
+        dc = wx.ClientDC(self)
+        x = dc.DeviceToLogicalX(0)
+        y = dc.DeviceToLogicalY(0)
+        self.main_sizer.SetDimension(x,y,s[0],self.height)
+        evt.Skip()
+
+    def refresh_data(self):
+        self.panels['saves'].panel.refresh_data()
+        self.panels['skills'].panel.refresh_data()
+        self.panels['attacks'].panel.refresh_data()
+#        self.panels['powers'].panel.refresh_data()
+#        self.panels['spells'].panel.refresh_data()
+
+HOWTO_MAX = wx.NewId()
+
+class howto_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        pname = handler.master_dom.setAttribute("name", 'How To')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+        self.sizer.AddMany([ (wx.StaticText(self, -1, t_node._get_nodeValue()),   0, wx.ALIGN_CENTER_VERTICAL),
+                 ])
+        self.sizer.AddGrowableCol(1)
+        self.SetSizer(self.sizer)
+
+
+WP_CUR = wx.NewId()
+WP_MAX = wx.NewId()
+
+class hp_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        pname = handler.master_dom.setAttribute("name", 'WoundPoints')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+        self.sizer.AddMany([ (wx.StaticText(self, -1, "WP Current:"),   0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, WP_CUR, self.master_dom.getAttribute('current')),   0, wx.EXPAND),
+                 (wx.StaticText(self, -1, "WP Max:"), 0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, WP_MAX, self.master_dom.getAttribute('max')),  0, wx.EXPAND),
+                 ])
+        self.sizer.AddGrowableCol(1)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=WP_MAX)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=WP_CUR)
+        self.SetSizer(self.sizer)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == WP_CUR:
+            self.master_dom.setAttribute('current',evt.GetString())
+        elif id == WP_MAX:
+            self.master_dom.setAttribute('max',evt.GetString())
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+VP_CUR = wx.NewId()
+VP_MAX = wx.NewId()
+
+class vp_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        pname = handler.master_dom.setAttribute("name", 'VitalityPoints')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+        self.sizer.AddMany([ (wx.StaticText(self, -1, "VP Current:"),   0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, VP_CUR, self.master_dom.getAttribute('current')),   0, wx.EXPAND),
+                 (wx.StaticText(self, -1, "VP Max:"), 0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, VP_MAX, self.master_dom.getAttribute('max')),  0, wx.EXPAND),
+                 ])
+        self.sizer.AddGrowableCol(1)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=VP_MAX)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=VP_CUR)
+        self.SetSizer(self.sizer)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == VP_CUR:
+            self.master_dom.setAttribute('current',evt.GetString())
+        elif id == VP_MAX:
+            self.master_dom.setAttribute('max',evt.GetString())
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+#PP_CUR = wx.NewId()
+#PP_MAX = wx.NewId()
+#PP_FRE = wx.NewId()
+#PP_MFRE = wx.NewId()
+
+#class pp_panel(wx.Panel):
+#    def __init__(self, parent, handler):
+#        wx.Panel.__init__(self, parent, -1)
+#        pname = handler.master_dom.setAttribute("name", 'PowerPoints')
+#        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+#        self.master_dom = handler.master_dom
+#
+#        self.sizer.AddMany([ (wx.StaticText(self, -1, "PP Current:"),   0, wx.ALIGN_CENTER_VERTICAL),
+#                 (wx.TextCtrl(self, PP_CUR, self.master_dom.getAttribute('current1')),   0, wx.EXPAND),
+#                 (wx.StaticText(self, -1, "PP Max:"), 0, wx.ALIGN_CENTER_VERTICAL),
+#                 (wx.TextCtrl(self, PP_MAX, self.master_dom.getAttribute('max1')),  0, wx.EXPAND),
+#                 (wx.StaticText(self, -1, "Current Free Talants per day:"), 0, wx.ALIGN_CENTER_VERTICAL),
+#                 (wx.TextCtrl(self, PP_FRE, self.master_dom.getAttribute('free')),  0, wx.EXPAND),
+#                 (wx.StaticText(self, -1, "Max Free Talants per day:"), 0, wx.ALIGN_CENTER_VERTICAL),
+#                 (wx.TextCtrl(self, PP_MFRE, self.master_dom.getAttribute('maxfree')),  0, wx.EXPAND),
+#                 ])
+#        self.sizer.AddGrowableCol(1)
+#        self.Bind(wx.EVT_SIZE, self.on_size)
+#        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_MAX)
+#        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_CUR)
+#        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_FRE)
+#        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_MFRE)
+#
+#    def on_text(self,evt):
+#        id = evt.GetId()
+#        if id == PP_CUR:
+#            self.master_dom.setAttribute('current1',evt.GetString())
+#        elif id == PP_MAX:
+#            self.master_dom.setAttribute('max1',evt.GetString())
+#        elif id == PP_FRE:
+#            self.master_dom.setAttribute('free',evt.GetString())
+#        elif id == PP_MFRE:
+#            self.master_dom.setAttribute('maxfree',evt.GetString())
+#
+#    def on_size(self,evt):
+#        s = self.GetClientSizeTuple()
+#        self.sizer.SetDimension(0,0,s[0],s[1])
+
+
+class gen_grid(wx.grid.Grid):
+    """grid for gen info"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'General')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        n_list = handler.master_dom._get_childNodes()
+        self.CreateGrid(len(n_list),2)
+        self.SetRowLabelSize(0)
+        self.SetColLabelSize(0)
+        self.n_list = n_list
+        i = 0
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        t_node = self.n_list[row]._get_firstChild()
+        t_node._set_nodeValue(value)
+        if row==0: self.handler.on_name_change(value)
+
+    def refresh_row(self,rowi):
+        t_node = safe_get_text_node(self.n_list[rowi])
+        self.SetCellValue(rowi,0,self.n_list[rowi]._get_tagName())
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,t_node._get_nodeValue())
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+class inventory_grid(wx.grid.Grid):
+    """grid for gen info"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Money and Inventory')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        n_list = handler.master_dom._get_childNodes()
+        self.CreateGrid(len(n_list),2)
+        self.SetRowLabelSize(0)
+        self.SetColLabelSize(0)
+        self.n_list = n_list
+        i = 0
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        t_node = self.n_list[row]._get_firstChild()
+        t_node._set_nodeValue(value)
+        if row==0: self.handler.on_name_change(value)
+
+    def refresh_row(self,rowi):
+        t_node = safe_get_text_node(self.n_list[rowi])
+        self.SetCellValue(rowi,0,self.n_list[rowi]._get_tagName())
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,t_node._get_nodeValue())
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+class abil_grid(wx.grid.Grid):
+    """grid for abilities"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Stats')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        stats = handler.master_dom.getElementsByTagName('stat')
+        self.CreateGrid(len(stats),3)
+        self.SetRowLabelSize(0)
+        col_names = ['Ability','Score','Modifier']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.stats = stats
+        i = 0
+        for i in range(len(stats)):
+            self.refresh_row(i)
+        self.char_wnd = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            self.stats[row].setAttribute('base',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+        if self.char_wnd:
+            self.char_wnd.refresh_data()
+
+    def refresh_row(self,rowi):
+        s = self.stats[rowi]
+        name = s.getAttribute('name')
+        abbr = s.getAttribute('abbr')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,s.getAttribute('base'))
+        self.SetCellValue(rowi,2,str(self.handler.get_mod(abbr)))
+        self.SetReadOnly(rowi,2)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()-1):
+            self.refresh_row(r)
+
+
+class save_grid(wx.grid.Grid):
+    """grid for saves"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Saves')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        saves = handler.master_dom.getElementsByTagName('save')
+        self.stats = handler.char_hander.child_handlers['abilities']
+        self.CreateGrid(len(saves),7)
+        self.SetRowLabelSize(0)
+        col_names = ['Save','Key','base','Abil','Magic','Misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.saves = saves
+        i = 0
+        for i in range(len(saves)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            if col == 2:
+                self.saves[row].setAttribute('base',value)
+            elif col ==4:
+                self.saves[row].setAttribute('magmod',value)
+            elif col ==4:
+                self.saves[row].setAttribute('miscmod',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_row(self,rowi):
+        s = self.saves[rowi]
+        name = s.getAttribute('name')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        stat = s.getAttribute('stat')
+        self.SetCellValue(rowi,1,stat)
+        self.SetReadOnly(rowi,1)
+        self.SetCellValue(rowi,2,s.getAttribute('base'))
+        self.SetCellValue(rowi,3,str(self.stats.get_mod(stat)))
+        self.SetReadOnly(rowi,3)
+        self.SetCellValue(rowi,4,s.getAttribute('magmod'))
+        self.SetCellValue(rowi,5,s.getAttribute('miscmod'))
+        mod = str(self.handler.get_mod(name))
+        self.SetCellValue(rowi,6,mod)
+        self.SetReadOnly(rowi,6)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()):
+            self.refresh_row(r)
+
+
+class skill_grid(wx.grid.Grid):
+    """ panel for skills """
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Skills')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        skills = handler.master_dom.getElementsByTagName('skill')
+        self.stats = handler.char_hander.child_handlers['abilities']
+        self.CreateGrid(len(skills),7)
+        self.SetRowLabelSize(0)
+        col_names = ['Skill','Key','Rank','Abil','Misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        rowi = 0
+        self.skills = skills
+        for i in range(len(skills)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        #print value
+        try:
+            int(value)
+            if col == 3:
+                self.skills[row].setAttribute('rank',value)
+            elif col ==5:
+                self.skills[row].setAttribute('misc',value)
+            elif col == 1:
+                self.skills[row].setAttribute('untrained',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_row(self,rowi):
+        s = self.skills[rowi]
+        name = s.getAttribute('name')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,s.getAttribute('untrained'))
+        stat = s.getAttribute('stat')
+        self.SetCellValue(rowi,2,stat)
+        self.SetReadOnly(rowi,2)
+        self.SetCellValue(rowi,3,s.getAttribute('rank'))
+        self.SetCellValue(rowi,4,str(self.stats.get_mod(stat)))
+        self.SetReadOnly(rowi,4)
+        self.SetCellValue(rowi,5,s.getAttribute('misc'))
+        mod = str(self.handler.get_mod(name))
+        self.SetCellValue(rowi,6,mod)
+        self.SetReadOnly(rowi,6)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()):
+            self.refresh_row(r)
+
+
+
+class feat_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Feats')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Feat"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Feat"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        EVT_BUTTON(self, 10, self.on_remove)
+        EVT_BUTTON(self, 20, self.on_add)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),2,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"Feat")
+        self.grid.SetColLabelValue(1,"Type")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def refresh_row(self,i):
+        feat = self.n_list[i]
+        name = feat.getAttribute('name')
+        type = feat.getAttribute('type')
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,type)
+        self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["SWd20"]+"d20feats.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('feat')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Feat','Feats',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+
+class attack_grid(wx.grid.Grid):
+    """grid for attacks"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Melee')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.parent = parent
+        self.handler = handler
+        self.rows = (self.handler.melee,self.handler.ranged)
+        self.CreateGrid(2,10)
+        self.SetRowLabelSize(0)
+        col_names = ['Type','base','base 2','base 3','base 4','base 5','base 6','abil','misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.SetCellValue(0,0,"Melee")
+        self.SetCellValue(1,0,"Ranged")
+        self.refresh_data()
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            if col==1:
+                self.rows[row].setAttribute('base',value)
+            elif col==2:
+                self.rows[row].setAttribute('second',value)
+            elif col==3:
+                self.rows[row].setAttribute('third',value)
+            elif col==4:
+                self.rows[row].setAttribute('forth',value)
+            elif col==5:
+                self.rows[row].setAttribute('fifth',value)
+            elif col==6:
+                self.rows[row].setAttribute('sixth',value)
+            elif col==8:
+                self.rows[row].setAttribute('misc',value)
+            self.parent.refresh_data()
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_data(self):
+        melee = self.handler.get_attack_data('m')
+        ranged = self.handler.get_attack_data('r')
+        for i in range(0,7):
+            self.SetCellValue(0,i+1,str(melee[i]))
+            self.SetCellValue(1,i+1,str(ranged[i]))
+        self.SetCellValue(0,9,str(melee[0]+melee[6]+melee[7]))
+        self.SetCellValue(1,9,str(ranged[0]+ranged[6]+ranged[7]))
+        self.SetReadOnly(0,0)
+        self.SetReadOnly(1,0)
+        self.SetReadOnly(0,7)
+        self.SetReadOnly(1,7)
+        self.SetReadOnly(0,9)
+        self.SetReadOnly(1,9)
+
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+1)
+        self.SetColSize(0,col_w*2)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+class weapon_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Weapons')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Weapon"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Weapon"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        EVT_BUTTON(self, 10, self.on_remove)
+        EVT_BUTTON(self, 20, self.on_add)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        n_list = handler.master_dom.getElementsByTagName('weapon')
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.handler = handler
+        self.grid.CreateGrid(len(n_list),9,1)
+        self.grid.SetRowLabelSize(0)
+        col_names = ['Name','damage','mod','critical','type','weight','range','size','Total']
+        for i in range(len(col_names)):
+            self.grid.SetColLabelValue(i,col_names[i])
+        self.refresh_data()
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col == 0:
+            self.n_list[row].setAttribute('name',value)
+        elif col == 2:
+            try:
+                int(value)
+                self.n_list[row].setAttribute('mod',value)
+                self.refresh_row(row)
+            except:
+                self.grid.SetCellValue(row,col,"1")
+        else:
+            self.n_list[row].setAttribute(self.grid.GetColLabelValue(col),value)
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+        name = n.getAttribute('name')
+        mod = n.getAttribute('mod')
+        ran = n.getAttribute('range')
+        total = str(int(mod) + self.handler.get_mod(ran))
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetCellValue(i,1,n.getAttribute('damage'))
+        self.grid.SetCellValue(i,2,mod)
+        self.grid.SetCellValue(i,3,n.getAttribute('critical'))
+        self.grid.SetCellValue(i,4,n.getAttribute('type'))
+        self.grid.SetCellValue(i,5,n.getAttribute('weight'))
+        self.grid.SetCellValue(i,6,ran)
+        self.grid.SetCellValue(i,7,n.getAttribute('size') )
+        self.grid.SetCellValue(i,8,total)
+        self.grid.SetReadOnly(i,8)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.n_list = self.master_dom.getElementsByTagName('weapon')
+                self.handler.refresh_weapons()
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["SWd20"]+"d20weapons.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('weapon')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Weapon','Weapon List',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('weapon')
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_weapons()
+        dlg.Destroy()
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols+1)
+        self.grid.SetColSize(0,col_w*2)
+        for i in range(1,cols):
+            self.grid.SetColSize(i,col_w)
+
+    def refresh_data(self):
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+
+class attack_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Melee')
+        wx.Panel.__init__(self, parent, -1)
+
+        self.a_grid = attack_grid(self, handler)
+        self.w_panel = weapon_panel(self, handler)
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.sizer.Add(self.a_grid, 1, wx.EXPAND)
+        self.sizer.Add(self.w_panel, 2, wx.EXPAND)
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+    def refresh_data(self):
+        self.w_panel.refresh_data()
+        self.a_grid.refresh_data()
+
+
+class ac_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Armor')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Armor"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Armor"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        EVT_BUTTON(self, 10, self.on_remove)
+        EVT_BUTTON(self, 20, self.on_add)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.master_dom = handler.master_dom
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        col_names = ['Armor','DR','Max Dex','Check Penalty','Weight','Speed (10)','Speed (6)','type']
+        self.grid.CreateGrid(len(n_list),len(col_names),1)
+        self.grid.SetRowLabelSize(0)
+        for i in range(len(col_names)):
+            self.grid.SetColLabelValue(i,col_names[i])
+        self.atts =['name','bonus','maxdex','checkpenalty','weight','speed','speed6','type']
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col >= 1 and col <= 5:
+            try:
+                int(value)
+                self.n_list[row].setAttribute(self.atts[col],value)
+            except:
+                self.grid.SetCellValue(row,col,"0")
+        else:
+            self.n_list[row].setAttribute(self.atts[col],value)
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+        for y in range(len(self.atts)):
+            self.grid.SetCellValue(i,y,n.getAttribute(self.atts[y]))
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["SWd20"]+"d20armor.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('armor')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Armor:','Armor List',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols+2)
+        self.grid.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.grid.SetColSize(i,col_w)
+
+
+class class_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Class')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Class"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Class"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        EVT_BUTTON(self, 10, self.on_remove)
+        EVT_BUTTON(self, 20, self.on_add)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),2,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"Class")
+        self.grid.SetColLabelValue(1,"Level")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        try:
+            int(value)
+            self.n_list[row].setAttribute('level',value)
+        except:
+            self.grid.SetCellValue(row,col,"1")
+
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+        name = n.getAttribute('name')
+        level = n.getAttribute('level')
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,level)
+        #self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["SWd20"]+"SWd20classes.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('class')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Class','Classes',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+__all__ = ['core']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/chatmacro.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,107 @@
+# 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: chatmacro.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: chatmacro.py,v 1.15 2006/11/15 12:11:23 digitalxero Exp $
+#
+# Description: The file contains code for the form based nodehanlers
+#
+
+__version__ = "$Id: chatmacro.py,v 1.15 2006/11/15 12:11:23 digitalxero Exp $"
+
+from core import *
+
+##########################
+## text node handler
+##########################
+class macro_handler(node_handler):
+    """ A nodehandler for text blocks. Will open text in a text frame
+        <nodehandler name='?' module='chatmacro' class='macro_handler'>
+            <text>some text here</text>
+        </nodehandler >
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.text_elem = self.master_dom.getElementsByTagName('text')[0]
+        self.text = safe_get_text_node(self.text_elem)
+
+    def set_text(self,txt):
+        self.text._set_nodeValue(txt)
+
+    def on_use(self,evt):
+        txt = self.text._get_nodeValue()
+        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)
+        return 1
+
+    def get_design_panel(self,parent):
+        return macro_edit_panel(parent,self)
+
+    def tohtml(self):
+        title = self.master_dom.getAttribute("name")
+        txt = self.text._get_nodeValue()
+        txt = string.replace(txt,'\n',"<br />")
+        return "<P><b>"+title+":</b><br />"+txt
+
+P_TITLE = wx.NewId()
+P_BODY = wx.NewId()
+B_CHAT = wx.NewId()
+
+class macro_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, "Chat Macro"), wx.VERTICAL)
+        self.text = {}
+        self.text[P_TITLE] = wx.TextCtrl(self, P_TITLE, handler.master_dom.getAttribute('name'))
+        self.text[P_BODY] = wx.TextCtrl(self, P_BODY, handler.text._get_nodeValue(), style=wx.TE_MULTILINE)
+
+        #P_BODY : wx.TextCtrl(self, P_BODY,handler.text._get_nodeValue(), style=wx.TE_MULTILINE)
+
+        sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
+        sizer.Add(self.text[P_TITLE], 0, wx.EXPAND)
+        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.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)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        txt = self.text[id].GetValue()
+        if txt == "":
+            return
+        if id == P_TITLE:
+            self.handler.master_dom.setAttribute('name',txt)
+            self.handler.rename(txt)
+        elif id == P_BODY:
+            self.handler.set_text(txt)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/containers.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,377 @@
+# 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: containers.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: containers.py,v 1.43 2007/08/08 19:17:17 digitalxero Exp $
+#
+# Description: The file contains code for the container nodehandlers
+#
+
+
+from core import *
+import wx.lib.splitter
+
+
+##########################
+##  base contiainer
+##########################
+
+class container_handler(node_handler):
+    """ should not be used! only a base class!
+    <nodehandler name='?'  module='core' class='container_handler'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.load_children()
+
+    def load_children(self):
+        children = self.master_dom._get_childNodes()
+        for c in children:
+            self.tree.load_xml(c,self.mytree_node)
+
+    def check_map_aware(self, obj, evt):
+        if hasattr(obj,"map_aware") and obj.map_aware():
+            obj.on_send_to_map(evt)
+
+
+    def on_send_to_map(self, evt):
+        child = self.tree.GetFirstChild(self.mytree_node)
+        if child[0].IsOk():
+            self.traverse(child[0], self.check_map_aware, 0, evt)
+
+
+    def checkChildToMap(self, obj, evt):
+        if hasattr(obj,"map_aware") and obj.map_aware():
+            self.mapcheck = True
+
+    def checkToMapMenu(self):
+        self.mapcheck = False
+        child = self.tree.GetFirstChild(self.mytree_node)
+        if child[0].IsOk():
+            self.traverse(child[0], self.checkChildToMap, 0, self.mapcheck)
+
+        return self.mapcheck
+
+    def on_drop(self,evt):
+        drag_obj = self.tree.drag_obj
+        if drag_obj == self or self.tree.is_parent_node(self.mytree_node,drag_obj.mytree_node):
+            return
+        opt = wx.MessageBox("Add node as child?","Container Node",wx.YES_NO|wx.CANCEL)
+        if opt == wx.YES:
+            xml_dom = self.tree.drag_obj.delete()
+            xml_dom = self.master_dom.insertBefore(xml_dom,None)
+            self.tree.load_xml(xml_dom, self.mytree_node)
+            self.tree.Expand(self.mytree_node)
+        elif opt == wx.NO:
+            node_handler.on_drop(self,evt)
+
+    def gen_html(self, obj, evt):
+        self.html_str += "<p>" + obj.tohtml()
+
+    def tohtml(self):
+        self.html_str = "<table border=\"1\" ><tr><td>"
+        self.html_str += "<b>"+self.master_dom.getAttribute("name") + "</b>"
+        self.html_str += "</td></tr>\n"
+        self.html_str += "<tr><td>"
+
+        child = self.tree.GetFirstChild(self.mytree_node)
+        self.traverse(child[0], self.gen_html, 0, None)
+
+        self.html_str += "</td></tr></table>"
+        return self.html_str
+
+    def get_size_constraint(self):
+        return 2
+
+
+##########################
+## group node handler
+##########################
+class group_handler(container_handler):
+    """ group nodehandler to be used as a placeholder for other nodehandlers.
+        This handler will continue parsing child xml data.
+        <nodehandler name='?'  module='core' class='group_handler'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        container_handler.__init__(self,xml_dom,tree_node)
+
+    def load_children(self):
+        self.atts = None
+        children = self.master_dom._get_childNodes()
+        for c in children:
+            if c._get_tagName() == "group_atts":
+                self.atts = c
+            else:
+                self.tree.load_xml(c,self.mytree_node)
+        if not self.atts:
+            elem = self.xml.minidom.Element('group_atts')
+            elem.setAttribute("cols","1")
+            elem.setAttribute("border","1")
+            self.atts = self.master_dom.appendChild(elem)
+
+    def get_design_panel(self,parent):
+        return group_edit_panel(parent,self)
+
+    def on_use(self,evt):
+        return
+
+    def gen_html(self, obj, evt):
+        if self.i  not in self.tdatas:
+            self.tdatas[self.i] = ''
+        self.tdatas[self.i] += "<P>" + obj.tohtml()
+        self.i += 1
+        if self.i >= self.cols:
+            self.i = 0
+
+    def tohtml(self):
+        cols = self.atts.getAttribute("cols")
+        border = self.atts.getAttribute("border")
+        self.html_str = "<table border=\""+border+"\" ><tr><td colspan=\""+cols+"\">"
+        self.html_str += "<font size=4>"+self.master_dom.getAttribute("name") + "</font>"
+        self.html_str += "</td></tr>\n<tr>"
+
+        self.cols = int(cols)
+        self.i = 0
+        self.tdatas = {}
+
+        child = self.tree.GetFirstChild(self.mytree_node)
+        if child[0].IsOk():
+            self.traverse(child[0], self.gen_html, 0, None)
+
+        for td in self.tdatas:
+            self.html_str += "<td valign=\"top\" >" + self.tdatas[td] + "</td>\n";
+        self.html_str += "</tr></table>"
+        return self.html_str
+
+GROUP_COLS = wx.NewId()
+GROUP_BOR = wx.NewId()
+
+class group_edit_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        self.text = {   P_TITLE : wx.TextCtrl(self, P_TITLE, handler.master_dom.getAttribute('name'))
+                      }
+        sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
+        sizer.Add(self.text[P_TITLE], 0, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+
+        radio_c = wx.RadioBox(self, GROUP_COLS, "Columns", choices=["1","2","3","4"])
+        cols = handler.atts.getAttribute("cols")
+        if cols != "":
+            radio_c.SetSelection(int(cols)-1)
+
+        radio_b = wx.RadioBox(self, GROUP_BOR, "Border", choices=["no","yes"])
+        border = handler.atts.getAttribute("border")
+        if border != "":
+            radio_b.SetSelection(int(border))
+
+        sizer.Add(radio_c, 0, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(radio_b, 0, wx.EXPAND)
+
+        self.sizer = sizer
+        self.outline = wx.StaticBox(self,-1,"Group")
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        parent.SetSize(self.GetBestSize())
+        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+        self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=GROUP_BOR)
+        self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=GROUP_COLS)
+
+    def on_radio_box(self,evt):
+        id = evt.GetId()
+        index = evt.GetInt()
+        if id == GROUP_COLS:
+            self.handler.atts.setAttribute("cols",str(index+1))
+        elif id == GROUP_BOR:
+            self.handler.atts.setAttribute("border",str(index))
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == P_TITLE:
+            txt = self.text[id].GetValue()
+            if txt != "":
+                self.handler.master_dom.setAttribute('name',txt)
+                self.handler.rename(txt)
+
+
+
+##########################
+## tabber node handler
+##########################
+class tabber_handler(container_handler):
+    """ <nodehandler name='?'  module='containers' class='tabber_handler'  />"""
+
+    def __init__(self,xml_dom,tree_node):
+        container_handler.__init__(self,xml_dom,tree_node)
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+
+class tabbed_panel(orpgTabberWnd):
+    def __init__(self, parent, handler, mode):
+        orpgTabberWnd.__init__(self, parent, style=FNB.FNB_NO_X_BUTTON)
+        self.handler = handler
+        self.parent = parent
+        tree = self.handler.tree
+        child = tree.GetFirstChild(handler.mytree_node)
+        if child[0].IsOk():
+            handler.traverse(child[0], self.pick_panel, 0, mode, False)
+
+        parent.SetSize(self.GetBestSize())
+
+    def pick_panel(self, obj, mode):
+        if mode == 1:
+            panel = obj.get_design_panel(self)
+        else:
+            panel = obj.get_use_panel(self)
+
+        name = obj.master_dom.getAttribute("name")
+
+        if panel:
+            self.AddPage(panel, name, False)
+
+#################################
+## Splitter container
+#################################
+
+class splitter_handler(container_handler):
+    """ <nodehandler name='?'  module='containers' class='splitter_handler'  />"""
+
+    def __init__(self,xml_dom,tree_node):
+        container_handler.__init__(self,xml_dom,tree_node)
+
+    def load_children(self):
+        self.atts = None
+        children = self.master_dom._get_childNodes()
+        for c in children:
+            if c._get_tagName() == "splitter_atts":
+                self.atts = c
+            else:
+                self.tree.load_xml(c,self.mytree_node)
+        if not self.atts:
+            elem = self.xml.minidom.Element('splitter_atts')
+            elem.setAttribute("horizontal","0")
+            self.atts = self.master_dom.appendChild(elem)
+
+    def get_design_panel(self,parent):
+        return self.build_splitter_wnd(parent, 1)
+
+    def get_use_panel(self,parent):
+        return self.build_splitter_wnd(parent, 2)
+
+    def on_drop(self,evt):
+        drag_obj = self.tree.drag_obj
+        container_handler.on_drop(self,evt)
+
+    def build_splitter_wnd(self, parent, mode):
+        self.split = self.atts.getAttribute("horizontal")
+
+        self.pane = splitter_panel(parent, self)
+
+        self.splitter = wx.lib.splitter.MultiSplitterWindow(self.pane, -1, style=wx.SP_LIVE_UPDATE|wx.SP_3DSASH|wx.SP_NO_XP_THEME)
+
+        if self.split == '1':
+            self.splitter.SetOrientation(wx.VERTICAL)
+        else:
+            self.splitter.SetOrientation(wx.HORIZONTAL)
+
+        self.bestSizex = -1
+        self.bestSizey = -1
+
+        cookie = 0
+        (child, cookie) = self.tree.GetFirstChild(self.mytree_node)
+        if child.IsOk():
+            self.traverse(child, self.doSplit, 0, mode, False)
+
+        self.pane.sizer.Add(self.splitter, 1, wx.EXPAND)
+
+
+        if mode != 1:
+            self.pane.hozCheck.Hide()
+
+        self.pane.SetSize((self.bestSizex, self.bestSizey))
+        self.pane.Layout()
+        parent.SetSize(self.pane.GetSize())
+        return self.pane
+
+    def doSplit(self, obj, mode):
+        if mode == 1:
+            tmp = obj.get_design_panel(self.splitter)
+        else:
+            tmp = obj.get_use_panel(self.splitter)
+
+        if self.split == '1':
+            sash = tmp.GetBestSize()[1]+1
+            self.bestSizey += sash+11
+            if self.bestSizex < tmp.GetBestSize()[0]:
+                self.bestSizex = tmp.GetBestSize()[0]+10
+        else:
+            sash = tmp.GetBestSize()[0]+1
+            self.bestSizex += sash
+            if self.bestSizey < tmp.GetBestSize()[1]:
+                self.bestSizey = tmp.GetBestSize()[1]+31
+
+        self.splitter.AppendWindow(tmp, sash)
+
+    def get_size_constraint(self):
+        return 1
+
+class splitter_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.VERTICAL)
+
+        self.hozCheck = wx.CheckBox(self, -1, "Horizontal Split")
+        hoz = self.handler.atts.getAttribute("horizontal")
+
+        if hoz == '1':
+            self.hozCheck.SetValue(True)
+            #self.splitsize = wx.BoxSizer(wx.HORIZONTAL)
+        else:
+            self.hozCheck.SetValue(False)
+            #self.splitsize = wx.BoxSizer(wx.VERTICAL)
+
+        sizer.Add(self.hozCheck, 0, wx.EXPAND)
+        sizer.Add(wx.Size(10,0))
+        #sizer.Add(self.splitsize,  1, wx.EXPAND)
+
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+
+        self.Bind(wx.EVT_CHECKBOX, self.on_check_box, id=self.hozCheck.GetId())
+
+    def on_check_box(self,evt):
+        state = self.hozCheck.GetValue()
+        if state:
+            self.handler.atts.setAttribute("horizontal", "1")
+        else:
+            self.handler.atts.setAttribute("horizontal", "0")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/core.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,462 @@
+# 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: core.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: core.py,v 1.49 2007/12/07 20:39:48 digitalxero Exp $
+#
+# Description: The file contains code for the core nodehanlers
+#
+
+__version__ = "$Id: core.py,v 1.49 2007/12/07 20:39:48 digitalxero Exp $"
+
+from nodehandler_version import NODEHANDLER_VERSION
+try:
+    from orpg.orpg_windows import *
+    import orpg.dirpath
+    from orpg.orpg_xml import *
+    from orpg.orpgCore import open_rpg
+    import webbrowser
+    from orpg.mapper import map
+    import os
+except:
+    import wx
+
+
+
+
+#html defaults
+TH_BG = "#E9E9E9"
+##########################
+## base node handler
+##########################
+class node_handler:
+    """ Base nodehandler with virtual functions and standard implmentations """
+    def __init__(self,xml_dom,tree_node):
+        self.master_dom = xml_dom
+        self.mytree_node = tree_node
+        self.tree = open_rpg.get_component('tree')
+        self.frame = open_rpg.get_component('frame')
+        self.chat = open_rpg.get_component('chat')
+        self.xml = open_rpg.get_component('xml')
+        self.drag = True
+        self.myeditor = None # designing
+        self.myviewer = None # prett print
+        self.mywindow = None # using
+        # call version hook
+        self.on_version(self.master_dom.getAttribute("version"))
+        # set to current version
+        self.master_dom.setAttribute("version",NODEHANDLER_VERSION)
+        # null events
+
+    def on_version(self,old_version):
+        ## added version control code here or implement a new on_version in your derived class.
+        ## always call the base class on_version !
+        pass
+
+    def on_rclick(self,evt):
+        self.tree.do_std_menu(evt,self)
+
+    def on_ldclick(self,evt):
+        return 0
+
+    def traverse(self, traverseroot, function, cookie=0, event=None, recursive=True):
+        """ walk tree control """
+        if traverseroot.IsOk():
+            # step in subtree if there are items or ...
+            if self.tree.ItemHasChildren(traverseroot) and recursive:
+                firstchild, cookie = self.tree.GetFirstChild(traverseroot)
+                obj = self.tree.GetPyData(firstchild)
+                function(obj, event)
+                self.traverse(firstchild, function, cookie, event, recursive)
+
+            # ... loop siblings
+            obj = self.tree.GetPyData(traverseroot)
+            function(obj, event)
+
+            child = self.tree.GetNextSibling(traverseroot)
+            if child.IsOk():
+                self.traverse(child, function, cookie, event, recursive)
+
+
+    def usefulness(self,text):
+        if text=="useful":
+            self.master_dom.setAttribute('status',"useful")
+        elif text=="useless":
+            self.master_dom.setAttribute('status',"useless")
+        elif text=="indifferent":
+            self.master_dom.setAttribute('status',"indifferent")
+
+    def on_design(self,evt):
+        try:
+            self.myeditor.Show()
+            self.myeditor.Raise()
+        except:
+            del self.myeditor
+            if self.create_designframe():
+                self.myeditor.Show()
+                self.myeditor.Raise()
+            else:
+                return
+        wx.CallAfter(self.myeditor.Layout)
+
+
+    def create_designframe(self):
+        title = self.master_dom.getAttribute('name') + " Editor"
+        self.myeditor = wx.Frame(None, -1, title)
+        self.myeditor.Freeze()
+        if wx.Platform == '__WXMSW__':
+            icon = wx.Icon(orpg.dirpath.dir_struct["icon"] + 'grid.ico', wx.BITMAP_TYPE_ICO)
+            self.myeditor.SetIcon(icon)
+            del icon
+
+        self.myeditor.panel = self.get_design_panel(self.myeditor)
+        if self.myeditor.panel == None:
+            self.myeditor.Destroy()
+            return False
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.myeditor.panel, 1, wx.EXPAND)
+
+        self.myeditor.SetSizer(sizer)
+        self.myeditor.SetAutoLayout(True)
+
+        (x, y) = self.myeditor.GetSize()
+        if x < 400:
+            x = 400
+        if y < 400:
+            y = 400
+
+        self.myeditor.SetSize((x, y))
+        self.myeditor.Layout()
+        self.myeditor.Thaw()
+
+        return True
+
+    def on_use(self,evt):
+        try:
+            self.mywindow.Show()
+            self.mywindow.Raise()
+        except:
+            del self.mywindow
+            if self.create_useframe():
+                self.mywindow.Show()
+                self.mywindow.Raise()
+            else:
+                return
+        wx.CallAfter(self.mywindow.Layout)
+
+
+    def create_useframe(self):
+        caption = self.master_dom.getAttribute('name')
+        self.mywindow = wx.Frame(None, -1, caption)
+        self.mywindow.Freeze()
+
+        if wx.Platform == '__WXMSW__':
+            icon = wx.Icon(orpg.dirpath.dir_struct["icon"] + 'note.ico', wx.BITMAP_TYPE_ICO)
+            self.mywindow.SetIcon(icon)
+            del icon
+        self.mywindow.panel = self.get_use_panel(self.mywindow)
+        if self.mywindow.panel == None:
+            self.mywindow.Destroy()
+            return False
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.mywindow.panel, 2, wx.EXPAND)
+
+        self.mywindow.SetSizer(sizer)
+        self.mywindow.SetAutoLayout(True)
+
+        (x, y) = self.mywindow.GetSize()
+        if x < 400:
+            x = 400
+        if y < 400:
+            y = 400
+
+        self.mywindow.SetSize((x, y))
+        self.mywindow.Layout()
+        self.mywindow.Thaw()
+
+        return True
+
+
+    def on_html_view(self,evt):
+        try:
+            self.myviewer.Raise()
+        except:
+            caption = self.master_dom.getAttribute('name')
+            self.myviewer = wx.Frame(None, -1, caption)
+            if wx.Platform == '__WXMSW__':
+                icon = wx.Icon(orpg.dirpath.dir_struct["icon"] + 'grid.ico', wx.BITMAP_TYPE_ICO)
+                self.myviewer.SetIcon(icon)
+                del icon
+            self.myviewer.panel = self.get_html_panel(self.myviewer)
+            self.myviewer.Show()
+
+    def map_aware(self):
+        return 0
+
+    def can_clone(self):
+        return 1;
+
+    def on_del(self,evt):
+        print "on del"
+
+    def on_new_data(self,xml_dom):
+        pass
+
+    def get_scaled_bitmap(self,x,y):
+        return None
+
+    def on_send_to_map(self,evt):
+        pass
+
+    def on_send_to_chat(self,evt):
+        self.chat.ParsePost(self.tohtml(),True,True)
+
+    def on_drop(self,evt):
+        drag_obj = self.tree.drag_obj
+        if drag_obj == self or self.tree.is_parent_node(self.mytree_node,drag_obj.mytree_node):
+            return
+        #if self.is_my_child(self.mytree_node,drag_obj.mytree_node):
+        #    return
+        xml_dom = self.tree.drag_obj.delete()
+        parent = self.master_dom._get_parentNode()
+        xml_dom = parent.insertBefore(xml_dom,self.master_dom)
+        parent_node = self.tree.GetItemParent(self.mytree_node)
+        prev_sib = self.tree.GetPrevSibling(self.mytree_node)
+        if not prev_sib.IsOk():
+            prev_sib = parent_node
+        self.tree.load_xml(xml_dom, parent_node, prev_sib)
+
+    def toxml(self,pretty=0):
+        return toxml(self.master_dom,pretty)
+
+    def tohtml(self):
+        return self.master_dom.getAttribute("name")
+
+    def delete(self):
+        """ removes the tree_node and xml_node, and returns the removed xml_node """
+
+        self.tree.Delete(self.mytree_node)
+        parent = self.master_dom._get_parentNode()
+        return parent.removeChild(self.master_dom)
+
+    def rename(self,name):
+        if len(name):
+            self.tree.SetItemText(self.mytree_node,name)
+            self.master_dom.setAttribute('name', name)
+
+    def change_icon(self,icon):
+        self.master_dom.setAttribute("icon",icon)
+        self.tree.SetItemImage(self.mytree_node, self.tree.icons[icon])
+        self.tree.SetItemImage(self.mytree_node, self.tree.icons[icon], wx.TreeItemIcon_Selected)
+        self.tree.Refresh()
+
+    def on_save(self,evt):
+        f = wx.FileDialog(self.tree,"Select a file", orpg.dirpath.dir_struct["user"],"","XML files (*.xml)|*.xml",wx.SAVE)
+        if f.ShowModal() == wx.ID_OK:
+            type = f.GetFilterIndex()
+            file = open(f.GetPath(),"w")
+            file.write(self.toxml(1))
+            file.close()
+        f.Destroy()
+
+    def get_design_panel(self,parent):
+        return None
+
+    def get_use_panel(self,parent):
+        return None
+
+    def get_html_panel(self,parent):
+        html_str = "<html><body bgcolor=\"#FFFFFF\" >"+self.tohtml()+"</body></html>"
+        wnd = wx.html.HtmlWindow(parent,-1)
+        html_str = self.chat.ParseDice(html_str)
+	wnd.SetPage(html_str)
+        return wnd
+
+    def get_size_constraint(self):
+        return 0
+
+    def about(self):
+        html_str = "<b>"+ self.master_dom.getAttribute('class')
+        html_str += " Applet</b><br />by Chris Davis<br />chris@rpgarchive.com"
+        return html_str
+
+P_TITLE = 10
+P_BODY = 20
+class text_edit_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        self.text = {   P_TITLE : wx.TextCtrl(self, P_TITLE, handler.master_dom.getAttribute('name')),
+                        P_BODY : html_text_edit(self,P_BODY,handler.text._get_nodeValue(),self.on_text)
+                      }
+        #P_BODY : wx.TextCtrl(self, P_BODY,handler.text._get_nodeValue(), style=wx.TE_MULTILINE)
+
+        sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
+        sizer.Add(self.text[P_TITLE], 0, wx.EXPAND)
+        sizer.Add(wx.StaticText(self, -1, "Text Body:"), 0, wx.EXPAND)
+        sizer.Add(self.text[P_BODY], 1, wx.EXPAND)
+        self.sizer = sizer
+        self.outline = wx.StaticBox(self,-1,"Text Block")
+        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == P_TITLE:
+            txt = self.text[id].GetValue()
+            #  The following block strips out 8-bit characters
+            u_txt = ""
+            bad_txt_found = 0
+            for c in txt:
+                if ord(c) < 128:
+                    u_txt += c
+                else:
+                    bad_txt_found = 1
+            if bad_txt_found:
+                wx.MessageBox("Some non 7-bit ASCII characters found and stripped","Warning!")
+            txt = u_txt
+            if txt != "":
+                self.handler.master_dom.setAttribute('name',txt)
+                self.handler.rename(txt)
+        elif id == P_BODY:
+            txt = self.text[id].get_text()
+            u_txt = ""
+            bad_txt_found = 0
+            for c in txt:
+                if ord(c) < 128:
+                    u_txt += c
+                else:
+                    bad_txt_found = 1
+
+            if bad_txt_found:
+                wx.MessageBox("Some non 7-bit ASCII characters found and stripped","Warning!")
+            txt = u_txt
+            self.handler.text._set_nodeValue(txt)
+
+
+
+##########################
+## node loader
+##########################
+class node_loader(node_handler):
+    """ clones childe node and insert it at top of tree
+        <nodehandler name='?'  module='core' class='node_loader'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        title = self.master_dom.getAttribute('name')
+        new_node = self.master_dom._get_firstChild()
+        new_node = new_node.cloneNode(True)
+        child = self.tree.master_dom._get_firstChild()
+        new_node = self.tree.master_dom.insertBefore(new_node,child)
+        tree_node = self.tree.load_xml(new_node,self.tree.root,self.tree.root)
+        obj = self.tree.GetPyData(tree_node)
+        return 1
+        #obj.on_design(None)
+
+##########################
+## file loader
+##########################
+
+class file_loader(node_handler):
+    """ loads file and insert into game tree
+        <nodehandler name='?'  module='core' class='file_loader'  >
+        <file name="file_name.xml" />
+        </nodehandler>
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.file_node = self.master_dom._get_firstChild()
+        self.frame = open_rpg.get_component('frame')
+
+    def on_ldclick(self,evt):
+        file_name = self.file_node.getAttribute("name")
+        self.tree.insert_xml(open(orpg.dirpath.dir_struct["nodes"] + file_name,"r").read())
+        return 1
+
+    def on_design(self,evt):
+        tlist = ['Title','File Name']
+        vlist = [self.master_dom.getAttribute("name"),
+                  self.file_node.getAttribute("name")]
+        dlg = orpgMultiTextEntry(self.tree.GetParent(),tlist,vlist,"File Loader Edit")
+        if dlg.ShowModal() == wx.ID_OK:
+            vlist = dlg.get_values()
+            self.file_node.setAttribute('name', vlist[1])
+            self.master_dom.setAttribute('name', vlist[0])
+            self.tree.SetItemText(self.mytree_node,vlist[0])
+        dlg.Destroy()
+
+##########################
+## URL loader
+##########################
+
+class url_loader(node_handler):
+    """ loads file from url and insert into game tree
+        <nodehandler name='?'  module='core' class='url_loader'  >
+        <file name="http://file_name.xml" />
+        </nodehandler>
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.file_node = self.master_dom._get_firstChild()
+        self.frame = open_rpg.get_component('frame')
+
+    def on_ldclick(self,evt):
+        file_name = self.file_node.getAttribute("url")
+        file = urllib.urlopen(file_name)
+        self.tree.insert_xml(file.read())
+        return 1
+
+    def on_design(self,evt):
+        tlist = ['Title','URL']
+        print "design filename",self.master_dom.getAttribute('name')
+        vlist = [self.master_dom.getAttribute("name"),
+                 self.file_node.getAttribute("url")]
+        dlg = orpgMultiTextEntry(self.tree.GetParent(),tlist,vlist,"File Loader Edit")
+        if dlg.ShowModal() == wx.ID_OK:
+            vlist = dlg.get_values()
+            self.file_node.setAttribute('url', vlist[1])
+            self.master_dom.setAttribute('name', vlist[0])
+            self.tree.SetItemText(self.mytree_node,vlist[0])
+        dlg.Destroy()
+
+
+##########################
+## minature map loader
+##########################
+class min_map(node_handler):
+    """ clones childe node and insert it at top of tree
+        <nodehandler name='?'  module='core' class='min_map'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.map = open_rpg.get_component('map')
+        self.mapdata = self.master_dom._get_firstChild()
+
+    def on_ldclick(self,evt):
+        self.map.new_data(toxml(self.mapdata))
+        return 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/core.py~	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,462 @@
+# 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: core.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: core.py,v 1.49 2007/12/07 20:39:48 digitalxero Exp $
+#
+# Description: The file contains code for the core nodehanlers
+#
+
+__version__ = "$Id: core.py,v 1.49 2007/12/07 20:39:48 digitalxero Exp $"
+
+from nodehandler_version import NODEHANDLER_VERSION
+try:
+    from orpg.orpg_windows import *
+    import orpg.dirpath
+    from orpg.orpg_xml import *
+    from orpg.orpgCore import open_rpg
+    import webbrowser
+    from orpg.mapper import map
+    import os
+except:
+    import wx
+
+
+
+
+#html defaults
+TH_BG = "#E9E9E9"
+##########################
+## base node handler
+##########################
+class node_handler:
+    """ Base nodehandler with virtual functions and standard implmentations """
+    def __init__(self,xml_dom,tree_node):
+        self.master_dom = xml_dom
+        self.mytree_node = tree_node
+        self.tree = open_rpg.get_component('tree')
+        self.frame = open_rpg.get_component('frame')
+        self.chat = open_rpg.get_component('chat')
+        self.xml = open_rpg.get_component('xml')
+        self.drag = True
+        self.myeditor = None # designing
+        self.myviewer = None # prett print
+        self.mywindow = None # using
+        # call version hook
+        self.on_version(self.master_dom.getAttribute("version"))
+        # set to current version
+        self.master_dom.setAttribute("version",NODEHANDLER_VERSION)
+        # null events
+
+    def on_version(self,old_version):
+        ## added version control code here or implement a new on_version in your derived class.
+        ## always call the base class on_version !
+        pass
+
+    def on_rclick(self,evt):
+        self.tree.do_std_menu(evt,self)
+
+    def on_ldclick(self,evt):
+        return 0
+
+    def traverse(self, traverseroot, function, cookie=0, event=None, recursive=True):
+        """ walk tree control """
+        if traverseroot.IsOk():
+            # step in subtree if there are items or ...
+            if self.tree.ItemHasChildren(traverseroot) and recursive:
+                firstchild, cookie = self.tree.GetFirstChild(traverseroot)
+                obj = self.tree.GetPyData(firstchild)
+                function(obj, event)
+                self.traverse(firstchild, function, cookie, event, recursive)
+
+            # ... loop siblings
+            obj = self.tree.GetPyData(traverseroot)
+            function(obj, event)
+
+            child = self.tree.GetNextSibling(traverseroot)
+            if child.IsOk():
+                self.traverse(child, function, cookie, event, recursive)
+
+
+    def usefulness(self,text):
+        if text=="useful":
+            self.master_dom.setAttribute('status',"useful")
+        elif text=="useless":
+            self.master_dom.setAttribute('status',"useless")
+        elif text=="indifferent":
+            self.master_dom.setAttribute('status',"indifferent")
+
+    def on_design(self,evt):
+        try:
+            self.myeditor.Show()
+            self.myeditor.Raise()
+        except:
+            del self.myeditor
+            if self.create_designframe():
+                self.myeditor.Show()
+                self.myeditor.Raise()
+            else:
+                return
+        wx.CallAfter(self.myeditor.Layout)
+
+
+    def create_designframe(self):
+        title = self.master_dom.getAttribute('name') + " Editor"
+        self.myeditor = wx.Frame(None, -1, title)
+        self.myeditor.Freeze()
+        if wx.Platform == '__WXMSW__':
+            icon = wx.Icon(orpg.dirpath.dir_struct["icon"] + 'grid.ico', wx.BITMAP_TYPE_ICO)
+            self.myeditor.SetIcon(icon)
+            del icon
+
+        self.myeditor.panel = self.get_design_panel(self.myeditor)
+        if self.myeditor.panel == None:
+            self.myeditor.Destroy()
+            return False
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.myeditor.panel, 1, wx.EXPAND)
+
+        self.myeditor.SetSizer(sizer)
+        self.myeditor.SetAutoLayout(True)
+
+        (x, y) = self.myeditor.GetSize()
+        if x < 400:
+            x = 400
+        if y < 400:
+            y = 400
+
+        self.myeditor.SetSize((x, y))
+        self.myeditor.Layout()
+        self.myeditor.Thaw()
+
+        return True
+
+    def on_use(self,evt):
+        try:
+            self.mywindow.Show()
+            self.mywindow.Raise()
+        except:
+            del self.mywindow
+            if self.create_useframe():
+                self.mywindow.Show()
+                self.mywindow.Raise()
+            else:
+                return
+        wx.CallAfter(self.mywindow.Layout)
+
+
+    def create_useframe(self):
+        caption = self.master_dom.getAttribute('name')
+        self.mywindow = wx.Frame(None, -1, caption)
+        self.mywindow.Freeze()
+
+        if wx.Platform == '__WXMSW__':
+            icon = wx.Icon(orpg.dirpath.dir_struct["icon"] + 'note.ico', wx.BITMAP_TYPE_ICO)
+            self.mywindow.SetIcon(icon)
+            del icon
+        self.mywindow.panel = self.get_use_panel(self.mywindow)
+        if self.mywindow.panel == None:
+            self.mywindow.Destroy()
+            return False
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.mywindow.panel, 2, wx.EXPAND)
+
+        self.mywindow.SetSizer(sizer)
+        self.mywindow.SetAutoLayout(True)
+
+        (x, y) = self.mywindow.GetSize()
+        if x < 400:
+            x = 400
+        if y < 400:
+            y = 400
+
+        self.mywindow.SetSize((x, y))
+        self.mywindow.Layout()
+        self.mywindow.Thaw()
+
+        return True
+
+
+    def on_html_view(self,evt):
+        try:
+            self.myviewer.Raise()
+        except:
+            caption = self.master_dom.getAttribute('name')
+            self.myviewer = wx.Frame(None, -1, caption)
+            if wx.Platform == '__WXMSW__':
+                icon = wx.Icon(orpg.dirpath.dir_struct["icon"] + 'grid.ico', wx.BITMAP_TYPE_ICO)
+                self.myviewer.SetIcon(icon)
+                del icon
+            self.myviewer.panel = self.get_html_panel(self.myviewer)
+            self.myviewer.Show()
+
+    def map_aware(self):
+        return 0
+
+    def can_clone(self):
+        return 1;
+
+    def on_del(self,evt):
+        print "on del"
+
+    def on_new_data(self,xml_dom):
+        pass
+
+    def get_scaled_bitmap(self,x,y):
+        return None
+
+    def on_send_to_map(self,evt):
+        pass
+
+    def on_send_to_chat(self,evt):
+        self.chat.ParsePost(self.tohtml(),True,True)
+
+    def on_drop(self,evt):
+        drag_obj = self.tree.drag_obj
+        if drag_obj == self or self.tree.is_parent_node(self.mytree_node,drag_obj.mytree_node):
+            return
+        #if self.is_my_child(self.mytree_node,drag_obj.mytree_node):
+        #    return
+        xml_dom = self.tree.drag_obj.delete()
+        parent = self.master_dom._get_parentNode()
+        xml_dom = parent.insertBefore(xml_dom,self.master_dom)
+        parent_node = self.tree.GetItemParent(self.mytree_node)
+        prev_sib = self.tree.GetPrevSibling(self.mytree_node)
+        if not prev_sib.IsOk():
+            prev_sib = parent_node
+        self.tree.load_xml(xml_dom, parent_node, prev_sib)
+
+    def toxml(self,pretty=0):
+        return toxml(self.master_dom,pretty)
+
+    def tohtml(self):
+        return self.master_dom.getAttribute("name")
+
+    def delete(self):
+        """ removes the tree_node and xml_node, and returns the removed xml_node """
+
+        self.tree.Delete(self.mytree_node)
+        parent = self.master_dom._get_parentNode()
+        return parent.removeChild(self.master_dom)
+
+    def rename(self,name):
+        if len(name):
+            self.tree.SetItemText(self.mytree_node,name)
+            self.master_dom.setAttribute('name', name)
+
+    def change_icon(self,icon):
+        self.master_dom.setAttribute("icon",icon)
+        self.tree.SetItemImage(self.mytree_node, self.tree.icons[icon])
+        self.tree.SetItemImage(self.mytree_node, self.tree.icons[icon], wx.TreeItemIcon_Selected)
+        self.tree.Refresh()
+
+    def on_save(self,evt):
+        f = wx.FileDialog(self.tree,"Select a file", orpg.dirpath.dir_struct["user"],"","XML files (*.xml)|*.xml",wx.SAVE)
+        if f.ShowModal() == wx.ID_OK:
+            type = f.GetFilterIndex()
+            file = open(f.GetPath(),"w")
+            file.write(self.toxml(1))
+            file.close()
+        f.Destroy()
+
+    def get_design_panel(self,parent):
+        return None
+
+    def get_use_panel(self,parent):
+        return None
+
+    def get_html_panel(self,parent):
+        html_str = "<html><body bgcolor=\"#FFFFFF\" >"+self.tohtml()+"</body></html>"
+        wnd = wx.HTMLpanel(parent,-1)
+        html_str = self.chat.ParseDice(html_str)
+        wnd.load_text(html_str)
+        return wnd
+
+    def get_size_constraint(self):
+        return 0
+
+    def about(self):
+        html_str = "<b>"+ self.master_dom.getAttribute('class')
+        html_str += " Applet</b><br />by Chris Davis<br />chris@rpgarchive.com"
+        return html_str
+
+P_TITLE = 10
+P_BODY = 20
+class text_edit_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        self.text = {   P_TITLE : wx.TextCtrl(self, P_TITLE, handler.master_dom.getAttribute('name')),
+                        P_BODY : html_text_edit(self,P_BODY,handler.text._get_nodeValue(),self.on_text)
+                      }
+        #P_BODY : wx.TextCtrl(self, P_BODY,handler.text._get_nodeValue(), style=wx.TE_MULTILINE)
+
+        sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
+        sizer.Add(self.text[P_TITLE], 0, wx.EXPAND)
+        sizer.Add(wx.StaticText(self, -1, "Text Body:"), 0, wx.EXPAND)
+        sizer.Add(self.text[P_BODY], 1, wx.EXPAND)
+        self.sizer = sizer
+        self.outline = wx.StaticBox(self,-1,"Text Block")
+        self.Bind(wx.EVT_TEXT, self.on_text, id=P_TITLE)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == P_TITLE:
+            txt = self.text[id].GetValue()
+            #  The following block strips out 8-bit characters
+            u_txt = ""
+            bad_txt_found = 0
+            for c in txt:
+                if ord(c) < 128:
+                    u_txt += c
+                else:
+                    bad_txt_found = 1
+            if bad_txt_found:
+                wx.MessageBox("Some non 7-bit ASCII characters found and stripped","Warning!")
+            txt = u_txt
+            if txt != "":
+                self.handler.master_dom.setAttribute('name',txt)
+                self.handler.rename(txt)
+        elif id == P_BODY:
+            txt = self.text[id].get_text()
+            u_txt = ""
+            bad_txt_found = 0
+            for c in txt:
+                if ord(c) < 128:
+                    u_txt += c
+                else:
+                    bad_txt_found = 1
+
+            if bad_txt_found:
+                wx.MessageBox("Some non 7-bit ASCII characters found and stripped","Warning!")
+            txt = u_txt
+            self.handler.text._set_nodeValue(txt)
+
+
+
+##########################
+## node loader
+##########################
+class node_loader(node_handler):
+    """ clones childe node and insert it at top of tree
+        <nodehandler name='?'  module='core' class='node_loader'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        title = self.master_dom.getAttribute('name')
+        new_node = self.master_dom._get_firstChild()
+        new_node = new_node.cloneNode(True)
+        child = self.tree.master_dom._get_firstChild()
+        new_node = self.tree.master_dom.insertBefore(new_node,child)
+        tree_node = self.tree.load_xml(new_node,self.tree.root,self.tree.root)
+        obj = self.tree.GetPyData(tree_node)
+        return 1
+        #obj.on_design(None)
+
+##########################
+## file loader
+##########################
+
+class file_loader(node_handler):
+    """ loads file and insert into game tree
+        <nodehandler name='?'  module='core' class='file_loader'  >
+        <file name="file_name.xml" />
+        </nodehandler>
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.file_node = self.master_dom._get_firstChild()
+        self.frame = open_rpg.get_component('frame')
+
+    def on_ldclick(self,evt):
+        file_name = self.file_node.getAttribute("name")
+        self.tree.insert_xml(open(orpg.dirpath.dir_struct["nodes"] + file_name,"r").read())
+        return 1
+
+    def on_design(self,evt):
+        tlist = ['Title','File Name']
+        vlist = [self.master_dom.getAttribute("name"),
+                  self.file_node.getAttribute("name")]
+        dlg = orpgMultiTextEntry(self.tree.GetParent(),tlist,vlist,"File Loader Edit")
+        if dlg.ShowModal() == wx.ID_OK:
+            vlist = dlg.get_values()
+            self.file_node.setAttribute('name', vlist[1])
+            self.master_dom.setAttribute('name', vlist[0])
+            self.tree.SetItemText(self.mytree_node,vlist[0])
+        dlg.Destroy()
+
+##########################
+## URL loader
+##########################
+
+class url_loader(node_handler):
+    """ loads file from url and insert into game tree
+        <nodehandler name='?'  module='core' class='url_loader'  >
+        <file name="http://file_name.xml" />
+        </nodehandler>
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.file_node = self.master_dom._get_firstChild()
+        self.frame = open_rpg.get_component('frame')
+
+    def on_ldclick(self,evt):
+        file_name = self.file_node.getAttribute("url")
+        file = urllib.urlopen(file_name)
+        self.tree.insert_xml(file.read())
+        return 1
+
+    def on_design(self,evt):
+        tlist = ['Title','URL']
+        print "design filename",self.master_dom.getAttribute('name')
+        vlist = [self.master_dom.getAttribute("name"),
+                 self.file_node.getAttribute("url")]
+        dlg = orpgMultiTextEntry(self.tree.GetParent(),tlist,vlist,"File Loader Edit")
+        if dlg.ShowModal() == wx.ID_OK:
+            vlist = dlg.get_values()
+            self.file_node.setAttribute('url', vlist[1])
+            self.master_dom.setAttribute('name', vlist[0])
+            self.tree.SetItemText(self.mytree_node,vlist[0])
+        dlg.Destroy()
+
+
+##########################
+## minature map loader
+##########################
+class min_map(node_handler):
+    """ clones childe node and insert it at top of tree
+        <nodehandler name='?'  module='core' class='min_map'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.map = open_rpg.get_component('map')
+        self.mapdata = self.master_dom._get_firstChild()
+
+    def on_ldclick(self,evt):
+        self.map.new_data(toxml(self.mapdata))
+        return 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/d20.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2422 @@
+# 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: d20.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: d20.py,v 1.30 2007/05/22 00:50:57 digitalxero Exp $
+#
+# Description: The file contains code for the d20 nodehanlers
+#
+
+__version__ = "$Id: d20.py,v 1.30 2007/05/22 00:50:57 digitalxero Exp $"
+
+from core import *
+import re
+
+D20_EXPORT = wx.NewId()
+############################
+## d20 character node handler
+############################
+## Spells code - added by Dragonstar
+##Powers, Divine spells, inventory, howto, power points by Digitalxero
+##The whole look and easy of use redone by Digitalxero
+class container_handler(node_handler):
+    """ should not be used! only a base class!
+    <nodehandler name='?'  module='core' class='container_handler'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.load_children()
+
+    def load_children(self):
+        children = self.master_dom._get_childNodes()
+        for c in children:
+            self.tree.load_xml(c,self.mytree_node)
+
+
+    def on_drop(self,evt):
+        drag_obj = self.tree.drag_obj
+        #if self.is_my_child(self.mytree_node,drag_obj.mytree_node):
+        #    return
+        if drag_obj == self:
+            return
+        opt = wx.MessageBox("Add node as child?","Container Node",wx.YES_NO|wx.CANCEL)
+        if opt == wx.YES:
+            xml_dom = self.tree.drag_obj.delete()
+            xml_dom = self.master_dom.insertBefore(xml_dom,None)
+            self.tree.load_xml(xml_dom, self.mytree_node)
+            self.tree.Expand(self.mytree_node)
+        elif opt == wx.NO:
+            node_handler.on_drop(self,evt)
+
+    def tohtml(self):
+        cookie = 0
+        html_str = "<table border=\"1\" ><tr><td>"
+        html_str += "<b>"+self.master_dom.getAttribute("name") + "</b>"
+        html_str += "</td></tr>\n"
+        html_str += "<tr><td>"
+
+        max = tree.GetChildrenCount(handler.mytree_node,0)
+        try:
+            (child,cookie)=self.tree.GetFirstChild(self.mytree_node,cookie)
+        except: # If this happens we probably have a newer version of wxPython
+            (child,cookie)=self.tree.GetFirstChild(self.mytree_node)
+        obj = self.tree.GetPyData(child)
+        for m in range(max):
+            html_str += "<p>" + obj.tohtml()
+            if m < max-1:
+                child = self.tree.GetNextSibling(child)
+                if child.IsOk():
+                    obj = self.tree.GetPyData(child)
+        html_str += "</td></tr></table>"
+        return html_str
+
+    def get_size_constraint(self):
+        return 1
+
+    def get_char_name( self ):
+        return self.child_handlers['general'].get_char_name()
+
+    def set_char_pp(self,attr,evl):
+        return self.child_handlers['pp'].set_char_pp(attr,evl)
+
+    def get_char_pp( self, attr ):
+        return self.child_handlers['pp'].get_char_pp(attr)
+
+    def get_char_lvl( self, attr ):
+        return self.child_handlers['classes'].get_char_lvl(attr)
+
+
+
+class d20char_handler(node_handler):
+    """ Node handler for a d20 charactor
+        <nodehandler name='?'  module='d20' class='d20char_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('howtouse','HowTO use this tool',d20howto,'note')
+        self.new_child_handler('general','General Information',d20general,'gear')
+        self.new_child_handler('inventory','Money and Inventory',d20inventory,'money')
+        self.new_child_handler('abilities','Abilities Scores',d20ability,'gear')
+        self.new_child_handler('classes','Classes',d20classes,'knight')
+        self.new_child_handler('saves','Saves',d20saves,'skull')
+        self.new_child_handler('skills','Skills',d20skill,'book')
+        self.new_child_handler('feats','Feats',d20feats,'book')
+        self.new_child_handler('spells','Spells',d20spells,'book')
+        self.new_child_handler('divine','Divine Spells',d20divine,'book')
+        self.new_child_handler('powers','Powers',d20powers,'questionhead')
+        self.new_child_handler('hp','Hit Points',d20hp,'gear')
+        self.new_child_handler('pp','Power Points',d20pp,'gear')
+        self.new_child_handler('attacks','Attacks',d20attacks,'spears')
+        self.new_child_handler('ac','Armor',d20armor,'spears')
+        #wxMenuItem(self.tree.std_menu, D20_EXPORT, "Export...", "Export")
+        self.myeditor = None
+
+
+    def on_version(self,old_version):
+        node_handler.on_version(self,old_version)
+        if old_version == "":
+            tmp = open(orpg.dirpath.dir_struct["nodes"]+"d20character.xml","r")
+            xml_dom = parseXml_with_dlg(self.tree,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            ## add new nodes
+            for tag in ("howtouse","inventory","powers","divine","pp"):
+                node_list = xml_dom.getElementsByTagName(tag)
+                self.master_dom.appendChild(node_list[0])
+
+            ## add new atts
+            melee_attack = self.master_dom.getElementsByTagName('melee')[0]
+            melee_attack.setAttribute("second","0")
+            melee_attack.setAttribute("third","0")
+            melee_attack.setAttribute("forth","0")
+            melee_attack.setAttribute("fifth","0")
+            melee_attack.setAttribute("sixth","0")
+            range_attack = self.master_dom.getElementsByTagName('ranged')[0]
+            range_attack.setAttribute("second","0")
+            range_attack.setAttribute("third","0")
+            range_attack.setAttribute("forth","0")
+            range_attack.setAttribute("fifth","0")
+            range_attack.setAttribute("sixth","0")
+
+            gen_list = self.master_dom.getElementsByTagName('general')[0]
+
+            for tag in ("currentxp","xptolevel"):
+                node_list = xml_dom.getElementsByTagName(tag)
+                gen_list.appendChild(node_list[0])
+            ## temp fix
+            #parent = self.master_dom._get_parentNode()
+            #old_dom = parent.replaceChild(xml_dom,self.master_dom)
+            #self.master_dom = xml_dom
+        print old_version
+
+
+    def get_char_name( self ):
+        return self.child_handlers['general'].get_char_name()
+
+    def set_char_pp(self,attr,evl):
+        return self.child_handlers['pp'].set_char_pp(attr,evl)
+
+    def get_char_pp( self, attr ):
+        return self.child_handlers['pp'].get_char_pp(attr)
+
+    def get_char_lvl( self, attr ):
+        return self.child_handlers['classes'].get_char_lvl(attr)
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+
+    def tohtml(self):
+        html_str = "<table><tr><td colspan=2 >"+self.child_handlers['general'].tohtml()+"</td></tr>"
+        html_str += "<tr><td width='50%' valign=top >"+self.child_handlers['abilities'].tohtml()
+        html_str += "<P>" + self.child_handlers['saves'].tohtml()
+        html_str += "<P>" + self.child_handlers['attacks'].tohtml()
+        html_str += "<P>" + self.child_handlers['ac'].tohtml()
+        html_str += "<P>" + self.child_handlers['feats'].tohtml()
+        html_str += "<P>" + self.child_handlers['spells'].tohtml()
+        html_str += "<P>" + self.child_handlers['divine'].tohtml()
+        html_str += "<P>" + self.child_handlers['powers'].tohtml()
+        html_str += "<P>" + self.child_handlers['inventory'].tohtml() +"</td>"
+        html_str += "<td width='50%' valign=top >"+self.child_handlers['classes'].tohtml()
+        html_str += "<P>" + self.child_handlers['hp'].tohtml()
+        html_str += "<P>" + self.child_handlers['pp'].tohtml()
+        html_str += "<P>" + self.child_handlers['skills'].tohtml() +"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+    def about(self):
+        html_str = "<img src='" + orpg.dirpath.dir_struct["icon"]+'d20_logo.gif' "><br /><b>d20 Character Tool v0.7 beta</b>"
+        html_str += "<br />by Chris Davis<br />chris@rpgarchive.com"
+        return html_str
+
+    def get_char_name( self ):
+        return self.child_handlers['general'].get_char_name()
+    def get_armor_class( self ):
+        return self.child_handlers['ac'].get_armor_class()
+    def get_max_hp( self ):
+        return self.child_handlers['hp'].get_max_hp()
+    def get_current_hp( self ):
+        return self.child_handlers['hp'].get_current_hp()
+
+    def set_char_pp(self,attr,evl):
+        return self.child_handlers['pp'].set_char_pp(attr,evl)
+
+    def get_char_pp( self, attr ):
+        return self.child_handlers['pp'].get_char_pp(attr)
+
+    def get_char_lvl( self, attr ):
+        return self.child_handlers['classes'].get_char_lvl(attr)
+
+class tabbed_panel(wx.Notebook):
+    def __init__(self, parent, handler, mode):
+        wx.Notebook.__init__(self, parent, -1, size=(1200,800))
+        self.handler = handler
+        self.parent = parent
+        tree = self.handler.tree
+        max = tree.GetChildrenCount(handler.mytree_node)
+
+        cookie = 0
+
+        try:
+            (child,cookie)=tree.GetFirstChild(handler.mytree_node,cookie)
+        except: # If this happens we probably have a newer version of wxPython
+            (child,cookie)=tree.GetFirstChild(handler.mytree_node)
+        if not child.IsOk():
+            return
+        obj = tree.GetPyData(child)
+        for m in range(max):
+            if mode == 1:
+                panel = obj.get_design_panel(self)
+            else:
+                panel = obj.get_use_panel(self)
+            name = obj.master_dom.getAttribute("name")
+
+            if panel:
+                self.AddPage(panel,name)
+            if m < max-1:
+                child = tree.GetNextSibling(child)
+                if child.IsOk():
+                    obj = tree.GetPyData(child)
+                else:
+                    break
+
+
+    def about(self):
+        html_str = "<img src='" + orpg.dirpath.dir_struct["icon"]+'d20_logo.gif' "><br /><b>d20 Character Tool v0.7 beta</b>"
+        html_str += "<br />by Chris Davis<br />chris@rpgarchive.com"
+        return html_str
+
+    def get_char_name( self ):
+        return self.child_handlers['general'].get_char_name()
+
+    def set_char_pp(self,attr,evl):
+        return self.child_handlers['pp'].set_char_pp(attr,evl)
+
+    def get_char_pp( self, attr ):
+        return self.child_handlers['pp'].get_char_pp(attr)
+
+    def get_char_lvl( self, attr ):
+        return self.child_handlers['classes'].get_char_lvl(attr)
+
+class d20_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+        if self.myeditor == None or self.myeditor.destroyed:
+            title = self.master_dom.getAttribute('name') + " Editor"
+            self.myeditor = wx.Frame(self.frame, -1, title)
+            if wx.Platform == '__WXMSW__':
+                icon = wx.Icon(orpg.dirpath.dir_struct["icon"]+'grid.ico', wx.BITMAP_TYPE_ICO)
+                self.myeditor.SetIcon(icon)
+                del icon
+            wnd = self.get_design_panel(self.myeditor)
+            self.myeditor.panel = wnd
+            self.wnd = wnd
+            self.myeditor.Show(1)
+        else:
+            self.myeditor.Raise()
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+
+class d20skill(d20_char_child):
+    """ Node Handler for skill.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+        tree = self.tree
+        icons = self.tree.icons
+        node_list = self.master_dom.getElementsByTagName('skill')
+        self.skills={}
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.skills[name] = n
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+
+    def get_mod(self,name):
+        skill = self.skills[name]
+        stat = skill.getAttribute('stat')
+        ac = int(skill.getAttribute('armorcheck'))
+        if ac:
+            ac = self.char_hander.child_handlers['ac'].get_check_pen()
+        stat_mod = self.char_hander.child_handlers['abilities'].get_mod(stat)
+        rank = int(skill.getAttribute('rank'))
+        misc = int(skill.getAttribute('misc'))
+        total = stat_mod + rank + misc + ac
+        return total
+
+    def on_rclick(self,evt):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            d20_char_child.on_ldclick(self,evt)
+            #wnd = skill_grid(self.frame.note,self)
+            #wnd.title = "Skills
+            #self.frame.add_panel(wnd)
+        else:
+            skill = self.skills[name];
+            untrained = skill.getAttribute('untrained');
+            rank = skill.getAttribute('rank');
+            if untrained == "0" and rank == "0":
+                txt = '%s Skill Check: Untrained' % (name)
+            else:
+                mod = self.get_mod(name)
+                if mod >= 0:
+                    mod1 = "+"
+                else:
+                    mod1 = ""
+                txt = '%s Skill Check: [1d20%s%s]' % (name, mod1, mod)
+            chat = self.chat
+            chat.ParsePost(txt,True,True)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,skill_grid,"Skills")
+        wnd.title = "Skills (edit)"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 ><th width='30%'>Skill</th><th>Key</th>
+                    <th>Rank</th><th>Abil</th><th>Misc</th><th>Total</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('skill')
+        for n in node_list:
+            name = n.getAttribute('name')
+            stat = n.getAttribute('stat')
+            rank = n.getAttribute('rank')
+            html_str = html_str + "<tr ALIGN='center'><td>"+name+"</td><td>"+stat+"</td><td>"+rank+"</td>"
+            stat_mod = str(self.char_hander.child_handlers['abilities'].get_mod(stat))
+            misc = n.getAttribute('misc')
+            mod = str(self.get_mod(name))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str = html_str + "<td>"+stat_mod+"</td><td>"+misc+'</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+
+class d20ability(d20_char_child):
+    """ Node Handler for ability.   This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.abilities = {}
+        node_list = self.master_dom.getElementsByTagName('stat')
+        tree = self.tree
+        icons = tree.icons
+        for n in node_list:
+            name = n.getAttribute('abbr')
+            self.abilities[name] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['gear'], icons['gear'] )
+            tree.SetPyData( new_tree_node, self )
+
+    def on_rclick( self, evt ):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        if item == self.mytree_node:
+            d20_char_child.on_ldclick( self, evt )
+        else:
+            mod = self.get_mod( name )
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s check: [1d20%s%s]' % ( name, mod1, mod )
+            chat.ParsePost( txt, True, True )
+
+    def get_mod(self,abbr):
+        score = int(self.abilities[abbr].getAttribute('base'))
+        mod = (score - 10) / 2
+        return mod
+
+    def set_score(self,abbr,score):
+        if score >= 0:
+            self.abilities[abbr].setAttribute("base",str(score))
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,abil_grid,"Abilities")
+        wnd.title = "Abilities (edit)"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100%><tr BGCOLOR=#E9E9E9 ><th width='50%'>Ability</th>
+                    <th>Base</th><th>Modifier</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('stat')
+        for n in node_list:
+            name = n.getAttribute('name')
+            abbr = n.getAttribute('abbr')
+            base = n.getAttribute('base')
+            mod = str(self.get_mod(abbr))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str = html_str + "<tr ALIGN='center'><td>"+name+"</td><td>"+base+'</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+class d20saves(d20_char_child):
+    """ Node Handler for saves.   This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+        tree = self.tree
+        icons = self.tree.icons
+        node_list = self.master_dom.getElementsByTagName('save')
+        self.saves={}
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.saves[name] = n
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+
+    def get_mod(self,name):
+        save = self.saves[name]
+        stat = save.getAttribute('stat')
+        stat_mod = self.char_hander.child_handlers['abilities'].get_mod(stat)
+        base = int(save.getAttribute('base'))
+        miscmod = int(save.getAttribute('miscmod'))
+        magmod = int(save.getAttribute('magmod'))
+        total = stat_mod + base + miscmod + magmod
+        return total
+
+    def on_rclick(self,evt):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            d20_char_child.on_ldclick(self,evt)
+            #wnd = save_grid(self.frame.note,self)
+            #wnd.title = "Saves"
+            #self.frame.add_panel(wnd)
+        else:
+            mod = self.get_mod(name)
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s save: [1d20%s%s]' % (name, mod1, mod)
+            chat.ParsePost( txt, True, True )
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,save_grid,"Saves")
+        wnd.title = "Saves"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 ><th width='30%'>Save</th>
+                    <th>Key</th><th>Base</th><th>Abil</th><th>Magic</th>
+                    <th>Misc</th><th>Total</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('save')
+        for n in node_list:
+            name = n.getAttribute('name')
+            stat = n.getAttribute('stat')
+            base = n.getAttribute('base')
+            html_str = html_str + "<tr ALIGN='center'><td>"+name+"</td><td>"+stat+"</td><td>"+base+"</td>"
+            stat_mod = str(self.char_hander.child_handlers['abilities'].get_mod(stat))
+            mag = n.getAttribute('magmod')
+            misc = n.getAttribute('miscmod')
+            mod = str(self.get_mod(name))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str = html_str + "<td>"+stat_mod+"</td><td>"+mag+"</td>"
+            html_str = html_str + '<td>'+misc+'</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+
+class d20general(d20_char_child):
+    """ Node Handler for general information.   This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,gen_grid,"General Information")
+        wnd.title = "General Info"
+        return wnd
+
+    def tohtml(self):
+        n_list = self.master_dom._get_childNodes()
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>General Information</th></tr><tr><td>"
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+            html_str += "<B>"+n._get_tagName().capitalize() +":</B> "
+            html_str += t_node._get_nodeValue() + ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def on_name_change(self,name):
+        self.char_hander.rename(name)
+
+    def get_char_name( self ):
+        node = self.master_dom.getElementsByTagName( 'name' )[0]
+        t_node = safe_get_text_node( node )
+        return t_node._get_nodeValue()
+
+
+class d20classes(d20_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,class_panel,"Classes")
+        wnd.title = "Classes"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Classes</th></tr><tr><td>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += n.getAttribute('name') + " ("+n.getAttribute('level')+"), "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        node_list = self.master_dom.getElementsByTagName('class')
+        for n in node_list:
+            lvl = n.getAttribute('level')
+            type = n.getAttribute('name')
+            if attr == "level":
+                return lvl
+            elif attr == "class":
+                return type
+
+
+class d20feats(d20_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,feat_panel,"Feats")
+        wnd.title = "Feats"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Feats</th></tr><tr><td>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+class d20spells(d20_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+        node_list = self.master_dom.getElementsByTagName( 'spell' )
+        self.spells = {}
+        tree = self.tree
+        icons = self.tree.icons
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.spells[ name ] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['gear'], icons['gear'] )
+            tree.SetPyData( new_tree_node, self )
+
+    def on_rclick( self, evt ):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        if item == self.mytree_node:
+            d20_char_child.on_ldclick( self, evt )
+        else:
+            level = self.spells[ name ].getAttribute( 'level' )
+            descr = self.spells[ name ].getAttribute( 'desc' )
+            use = self.spells[ name ].getAttribute( 'used' )
+            memrz = self.spells[ name ].getAttribute( 'memrz' )
+            cname = self.char_hander.get_char_name()
+            use += '+1'
+            left = eval( '%s - ( %s )' % ( memrz, use ) )
+            if left < 0:
+                txt = '%s Tried to cast %s but has used all of them for today, "Please rest so I can cast more."' % ( cname, name )
+                self.chat.ParsePost( txt, True, False )
+            else:
+                txt = '%s casts %s ( level %s, "%s" )' % ( cname, name, level, descr )
+                self.chat.ParsePost( txt, True, False )
+                s = ''
+                if left != 1:
+                    s = 's'
+                txt = '%s can cast %s %d more time%s' % ( cname, name, left, s )
+                self.chat.ParsePost( txt, False, False )
+                self.spells[ name ].setAttribute( 'used', `eval( use )` )
+
+    def refresh_spells(self):
+        self.spells = {}
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('spell')
+        for n in node_list:
+            name = n.getAttribute('name')
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+            self.spells[name]=n
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,spell_panel,"Spells")
+        wnd.title = "Spells"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Arcane Spells</th></tr><tr><td><br />"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += "(" + n.getAttribute('level') + ") " + n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        return self.char_hander.get_char_lvl(attr)
+
+class d20divine(d20_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,openrpg,parent)
+        node_list = self.master_dom.getElementsByTagName( 'gift' )
+        self.spells = {}
+        tree = self.tree
+        icons = self.tree.icons
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.spells[ name ] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['flask'], icons['flask'] )
+            tree.SetPyData( new_tree_node, self )
+
+    def on_rclick( self, evt ):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        if item == self.mytree_node:
+            d20_char_child.on_ldclick( self, evt )
+        else:
+            level = self.spells[ name ].getAttribute( 'level' )
+            descr = self.spells[ name ].getAttribute( 'desc' )
+            use = self.spells[ name ].getAttribute( 'used' )
+            memrz = self.spells[ name ].getAttribute( 'memrz' )
+            cname = self.char_hander.get_char_name()
+            use += '+1'
+            left = eval( '%s - ( %s )' % ( memrz, use ) )
+            if left < 0:
+                txt = '%s Tried to cast %s but has used all of them for today, "Please rest so I can cast more."' % ( cname, name )
+                self.chat.ParsePost( txt, True, False )
+            else:
+                txt = '%s casts %s ( level %s, "%s" )' % ( cname, name, level, descr )
+                self.chat.ParsePost( txt, True, False )
+                s = ''
+                if left != 1:
+                    s = 's'
+                txt = '%s can cast %s %d more time%s' % ( cname, name, left, s )
+                self.chat.ParsePost( txt, False, False )
+                self.spells[ name ].setAttribute( 'used', `eval( use )` )
+
+    def refresh_spells(self):
+        self.spells = {}
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('gift')
+        for n in node_list:
+            name = n.getAttribute('name')
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['flask'],icons['flask'])
+            tree.SetPyData(new_tree_node,self)
+            self.spells[name]=n
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,divine_panel,"Spells")
+        wnd.title = "Spells"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Divine Spells</th></tr><tr><td><br />"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += "(" + n.getAttribute('level') + ") " + n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        return self.char_hander.get_char_lvl(attr)
+
+class d20powers(d20_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+        node_list = self.master_dom.getElementsByTagName( 'power' )
+        #cpp = self.master_dom.getElementsByTagName( 'pp' ).getAttribute('current1')
+        self.powers = {}
+        tree = self.tree
+        icons = self.tree.icons
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.powers[ name ] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['gear'], icons['gear'] )
+            tree.SetPyData( new_tree_node, self )
+
+    def on_rclick( self, evt ):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        if item == self.mytree_node:
+            d20_char_child.on_ldclick( self, evt )
+        else:
+            level = self.powers[ name ].getAttribute( 'level' )
+            descr = self.powers[ name ].getAttribute( 'desc' )
+            use = self.powers[ name ].getAttribute( 'used' )
+            points = self.powers[ name ].getAttribute( 'point' )
+            cpp = self.char_hander.get_char_pp('current1')
+            fre = self.char_hander.get_char_pp('free')
+            cname = self.char_hander.get_char_name()
+            if level == "0" and fre != "0":
+                left = eval('%s - ( %s )' % ( fre, points ))
+                numcast = eval('%s / %s' % (left, points))
+                if left < 0:
+                    txt = '%s doesnt have enough PowerPoints to use %s' % ( cname, name )
+                    self.chat.ParsePost( txt, True, False )
+                else:
+                    txt = '%s uses %s as a Free Talent ( level %s, "%s" )' % ( cname, name, level, descr )
+                    self.chat.ParsePost( txt, True, False )
+                    s = ''
+                    if left != 1:
+                        s = 's'
+                    txt = '%s can use %s %d more time%s' % ( cname, name, numcast, s )
+                    self.chat.ParsePost( txt, False, False )
+                    self.char_hander.set_char_pp('free', left)
+            else:
+                left = eval('%s - ( %s )' % ( cpp, points ))
+                numcast = eval('%s / %s' % (left, points))
+                if left < 0:
+                    txt = '%s doesnt have enough PowerPoints to use %s' % ( cname, name )
+                    self.chat.ParsePost( txt, True, False )
+                else:
+                    txt = '%s uses %s ( level %s, "%s" )' % ( cname, name, level, descr )
+                    self.chat.ParsePost( txt, True, False )
+                    s = ''
+                    if left != 1:
+                        s = 's'
+                    txt = '%s can use %s %d more time%s' % ( cname, name, numcast, s )
+                    txt += ' - And has %d more PowerpointsP left' % (left)
+                    self.chat.ParsePost( txt, False, False )
+                    self.char_hander.set_char_pp('current1', left)
+
+    def refresh_powers(self):
+        self.powers = {}
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('power')
+        for n in node_list:
+            name = n.getAttribute('name')
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['questionhead'],icons['questionhead'])
+            tree.SetPyData(new_tree_node,self)
+            self.powers[name]=n
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,power_panel,"Powers")
+        wnd.title = "Powers"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Powers</th></tr><tr><td><br />"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += "(" + n.getAttribute('level') + ") " + n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        return self.char_hander.get_char_lvl(attr)
+
+    def set_char_pp(self,attr,evl):
+        return self.char_hander.set_char_pp(attr,evl)
+
+    def get_char_pp( self, attr ):
+        return self.char_hander.get_char_pp(attr)
+
+class d20howto(d20_char_child):
+    """ Node Handler for hit points.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,howto_panel,"How To")
+        wnd.title = "How To"
+        return wnd
+
+class d20inventory(d20_char_child):
+    """ Node Handler for general information.   This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,inventory_grid,"Inventory")
+        wnd.title = "General Info"
+        return wnd
+
+    def tohtml(self):
+        n_list = self.master_dom._get_childNodes()
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>General Information</th></tr><tr><td>"
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+            html_str += "<B>"+n._get_tagName().capitalize() +":</B> "
+            html_str += t_node._get_nodeValue() + "<br />"
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def on_name_change(self,name):
+        self.char_hander.rename(name)
+
+    def get_char_name( self ):
+        node = self.master_dom.getElementsByTagName( 'name' )[0]
+        t_node = safe_get_text_node( node )
+        return t_node._get_nodeValue()
+
+class d20hp(d20_char_child):
+    """ Node Handler for hit points.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,hp_panel,"Hit Points")
+        wnd.title = "Hit Points"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th colspan=4>Hit Points</th></tr>"
+        html_str += "<tr><th>Max:</th><td>"+self.master_dom.getAttribute('max')+"</td>"
+        html_str += "<th>Current:</th><td>"+self.master_dom.getAttribute('current')+"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+    def get_max_hp( self ):
+        try:
+            return eval( self.master_dom.getAttribute( 'max' ) )
+        except:
+            return 0
+    def get_current_hp( self ):
+        try:
+            return eval( self.master_dom.getAttribute( 'current' ) )
+        except:
+            return 0
+
+class d20pp(d20_char_child):
+    """ Node Handler for power points.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,pp_panel,"Power Points")
+        wnd.title = "Power Points"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th colspan=8>Power Points</th></tr>"
+        html_str += "<tr><th>Max:</th><td>"+self.master_dom.getAttribute('max1')+"</td>"
+        html_str += "<th>Current:</th><td>"+self.master_dom.getAttribute('current1')+"</td>"
+        html_str += "<th>Current Talents/day:</th><td>"+self.master_dom.getAttribute('free')+"</td>"
+        html_str += "<th>Max Talents/day:</th><td>"+self.master_dom.getAttribute('maxfree')+"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+    def get_char_pp( self, attr ):
+        pp = self.master_dom.getAttribute(attr)
+        return pp
+
+    def set_char_pp( self, attr, evl ):
+        pp = self.master_dom.setAttribute(attr, evl)
+        return pp
+
+class d20attacks(d20_char_child):
+    """ Node Handler for attacks.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+        node_list = self.master_dom.getElementsByTagName('melee')
+        self.melee = node_list[0]
+        node_list = self.master_dom.getElementsByTagName('ranged')
+        self.ranged = node_list[0]
+        self.refresh_weapons()
+
+    def refresh_weapons(self):
+        self.weapons = {}
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('weapon')
+        for n in node_list:
+            name = n.getAttribute('name')
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['sword'],icons['sword'])
+            tree.SetPyData(new_tree_node,self)
+            self.weapons[name]=n
+
+    def get_attack_data(self):
+        temp = self.melee
+        base = int(temp.getAttribute('base'))
+        base2 = int(temp.getAttribute('second'))
+        base3 = int(temp.getAttribute('third'))
+        base4 = int(temp.getAttribute('forth'))
+        base5 = int(temp.getAttribute('fifth'))
+        base6 = int(temp.getAttribute('sixth'))
+        misc = int(temp.getAttribute('misc'))
+        return (base, base2, base3, base4, base5, base6, misc)
+
+    # Replace any 'S' and 'D' in an attack modifier and damage modifier with the
+    # strength bonus or dexterity bonus respectively.
+    def process_mod_codes( self, attack, damage ):
+        str_mod = self.char_hander.child_handlers['abilities'].get_mod( 'Str' )
+        dex_mod = self.char_hander.child_handlers['abilities'].get_mod( 'Dex' )
+        str_re = re.compile('S')
+        dex_re = re.compile('D')
+        attack = str_re.sub( str( str_mod ), attack )
+        attack = dex_re.sub( str( dex_mod ), attack )
+        damage = str_re.sub( str( str_mod ), damage );
+        damage = dex_re.sub( str( dex_mod ), damage );
+        return (attack, damage)
+
+    # Decompose a damage string (e.g. longsword +1 sneak attack "1d8+S+1+1d6")
+    # into it's 4 seperate components <n>d<s>+<mods>+<extra dice>
+    def decompose_damage( self, damage ):
+        m = re.match( r"(?P<n>\d+)d(?P<s>\d+)(?P<mods>(\s*(\+|-|/|\*)\s*(\d+|D|S)*)*)(?P<extra>(\s*(\+|-)\s*\d+d\d+)?)\s*$", damage )
+        return (int(m.group('n')), int(m.group('s')), m.group('mods'), m.group('extra'))
+
+    def on_rclick(self,evt):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            d20_char_child.on_ldclick(self,evt)
+            #self.frame.add_panel(self.get_design_panel(self.frame.note))
+        else:
+            # Weapon/attack specific attack modifier (e.g. "S+1" for a longsword+1).
+            attack_mod_str = self.weapons[name].getAttribute('mod')
+
+            # Weapon/attack specific damage (e.g. "1d8+S+1" for a longsword+1).
+            damage_str = self.weapons[name].getAttribute('damage')
+            (num_damage_dice, damage_die, damage_mods, extra_damage) = self.decompose_damage( damage_str )
+
+            # Replace any 'S' and 'D' in attack_mod_str and damage_str with the
+            # strength bonus or dexterity bonus respectively.
+            (attack_mod_str, damage_mods) = self.process_mod_codes( attack_mod_str, damage_mods )
+
+            # Base attack bonuses for up to six attacks.
+            bab_attributes = ['base', 'second', 'third', 'forth', 'fifth', 'sixth']
+            bab = []
+            for b in bab_attributes:
+                bab.append( int(self.melee.getAttribute( b )) )
+
+            # Misc. attack modifier to be applied to *all* attacks.
+            misc_mod = int(self.melee.getAttribute( 'misc' ));
+
+            # Attack modifier (except BAB)
+            attack_mod = misc_mod + eval( attack_mod_str )
+
+            # Total damage mod (except extra dice)
+            if damage_mods != '':
+                damage_mod = eval( damage_mods )
+            else:
+                damage_mod = 0
+
+            # Determine critical hit range and multiplier.
+            critical_str = self.weapons[name].getAttribute( 'critical' )
+            m = re.match( r"(((?P<min>\d+)-)?\d+/)?x(?P<mult>\d+)", critical_str )
+            crit_min = m.group( 'min' )
+            crit_mult = m.group( 'mult' )
+            if crit_min == None:
+                crit_min = 20
+            else:
+                crit_min = int( crit_min )
+            if crit_mult == None:
+                crit_mult = 2
+            else:
+                crit_mult = int( crit_mult )
+
+            # Simple matter to output all the attack/damage lines to the chat buffer.
+            for i in range( 0, len( bab ) ):
+                if bab[i] > 0 or i == 0:
+                    attack_roll_str = '[1d20%+d]' % (bab[i] + attack_mod)
+                    attack_roll_parsed = self.chat.ParseDice( attack_roll_str )
+                    damage_roll_str = '[%dd%d%+d%s]' % (num_damage_dice, damage_die, damage_mod, extra_damage)
+                    damage_roll_parsed = self.chat.ParseDice( damage_roll_str )
+                    txt = '%s (%s): %s ===> Damage: %s' \
+                          % (name, bab_attributes[i], attack_roll_parsed, damage_roll_parsed)
+                    self.chat.Post( txt, True, True )
+
+                    # Check for a critical hit
+                    d20_roll = int(re.match( r".*\[(\d+),.*", attack_roll_parsed ).group(1));
+                    dmg = damage_str
+                    if d20_roll >= crit_min:
+                        for j in range(1,crit_mult):
+                            dmg += '+%s' % damage_str
+                        txt = 'Critical hit? [1d20%+d] ===> Damage: [%dd%d%+d%s]' \
+                              % (bab[i] + attack_mod, crit_mult*num_damage_dice, \
+                                 damage_die, crit_mult*damage_mod, extra_damage)
+                        self.chat.ParsePost( txt, True, True )
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,attack_panel,"Attacks")
+        wnd.title = "Attacks"
+        return wnd
+
+    def get_char_lvl( self, attr ):
+        return self.char_hander.get_char_lvl(attr)
+
+    def tohtml(self):
+        babs = self.get_attack_data()
+        html_str = "<table width=100% border=1 ><tr ALIGN='center'><th BGCOLOR=#E9E9E9>Base Attack Bonus</th>"
+        html_str += '<td>%+d' % babs[0]
+        for i in range(1,6):
+            if babs[i] > 0:
+                html_str += '/%+d' % babs[i]
+        html_str += "</td></tr><tr ALIGN='center' ><th BGCOLOR=#E9E9E9>Misc. Attack Bonus</th>"
+        html_str += '<td>%+d</td></tr></table>' % babs[6]
+
+        n_list = self.master_dom.getElementsByTagName('weapon')
+        for n in n_list:
+            (attack_mod, damage_mod) = self.process_mod_codes( n.getAttribute( 'mod' ), \
+                                                               n.getAttribute( 'damage' ) )
+            attack_mod = eval( attack_mod )
+            html_str += """<P><table width=100% border=1><tr BGCOLOR=#E9E9E9><th colspan=3>Weapon</th>
+                    <th>Attack</th><th >Damage</th></tr>""" \
+                      + "<tr ALIGN='center'><td colspan=3>" \
+                      + n.getAttribute('name') + "</td><td>"
+            html_str += '%+d</td><td>%s</td></tr>' % (attack_mod, damage_mod)
+            html_str += """<tr BGCOLOR=#E9E9E9 ><th>Critical</th><th>Range</th><th>Weight</th>
+                        <th>Type</th><th>Size</th></tr>""" \
+                      + "<tr ALIGN='center'><td>" \
+                      + n.getAttribute( 'critical' ) + "</td><td>" \
+                      + n.getAttribute( 'range' ) + "</td><td>" \
+                      + n.getAttribute( 'weight' )+"</td><td>" \
+                      + n.getAttribute( 'type' ) + "</td><td>" \
+                      + n.getAttribute( 'size' ) + "</td></tr></table>"
+        return html_str
+
+class d20armor(d20_char_child):
+    """ Node Handler for ac.  This handler will be
+        created by d20char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        d20_char_child.__init__(self,xml_dom,tree_node,parent)
+
+    def get_spell_failure(self):
+        return self.get_total('spellfailure')
+
+    def get_total_weight(self):
+        return self.get_total('weight')
+
+    def get_check_pen(self):
+        return self.get_total('checkpenalty')
+
+    def get_armor_class(self):
+        ac_total = 10
+        ac_total += self.get_total('bonus')
+        dex_mod = self.char_hander.child_handlers['abilities'].get_mod('Dex')
+        max_dex = self.get_max_dex()
+        if dex_mod < max_dex:
+            ac_total += dex_mod
+        else:
+            ac_total += max_dex
+        return ac_total
+
+    def get_max_dex(self):
+        armor_list = self.master_dom.getElementsByTagName('armor')
+        dex = 10
+        for a in armor_list:
+            temp = int(a.getAttribute("maxdex"))
+            if temp < dex:
+                dex = temp
+        return dex
+
+    def get_total(self,attr):
+        armor_list = self.master_dom.getElementsByTagName('armor')
+        total = 0
+        for a in armor_list:
+            total += int(a.getAttribute(attr))
+        return total
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,ac_panel,"Armor")
+        wnd.title = "Armor"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>AC</th>
+                    <th>Check Penalty</th><th >Spell Failure</th><th>Max Dex</th><th>Total Weight</th></tr>"""
+        html_str += "<tr ALIGN='center' ><td>"+str(self.get_armor_class())+"</td>"
+        html_str += "<td>"+str(self.get_check_pen())+"</td>"
+        html_str += "<td>"+str(self.get_spell_failure())+"</td>"
+        html_str += "<td>"+str(self.get_max_dex())+"</td>"
+        html_str += "<td>"+str(self.get_total_weight())+"</td></tr></table>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += """<P><table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th colspan=3>Armor</th>
+                    <th>Type</th><th >Bonus</th></tr>"""
+            html_str += "<tr ALIGN='center' ><td  colspan=3>"+n.getAttribute('name')+"</td>"
+            html_str += "<td>"+n.getAttribute('type')+"</td>"
+            html_str += "<td>"+n.getAttribute('bonus')+"</td></tr>"
+            html_str += """<tr BGCOLOR=#E9E9E9 ><th>Check Penalty</th><th>Spell Failure</th>
+                        <th>Max Dex</th><th>Speed</th><th>Weight</th></tr>"""
+            html_str += "<tr ALIGN='center'><td>"+n.getAttribute('checkpenalty')+"</td>"
+            html_str += "<td>"+n.getAttribute('spellfailure')+"</td>"
+            html_str += "<td>"+n.getAttribute('maxdex')+"</td>"
+            html_str += "<td>"+n.getAttribute('speed')+"</td>"
+            html_str += "<td>"+n.getAttribute('weight')+"</td></tr></table>"
+        return html_str
+
+
+########################
+##  d20 char windows
+########################
+
+class base_panel(wx.Panel):
+    def __init__(self, parent):
+        wx.Panel.__init__(self, parent, -1)
+
+        #self.build_ctrls()
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        #self.splitter.SetDimensions(0,0,s[0],s[1])
+
+class outline_panel(wx.Panel):
+    def __init__(self, parent, handler, wnd, txt,):
+        wx.Panel.__init__(self, parent, -1)
+        self.panel = wnd(self,handler)
+        self.outline = wx.StaticBox(self,-1,txt)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.panel.SetDimensions(20,20,s[0]-40,s[1]-40)
+        self.outline.SetDimensions(5,5,s[0]-10,s[1]-10)
+
+class char_panel(wx.ScrolledWindow):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'TWO')
+        wx.ScrolledWindow.__init__(self, parent, -1,style=wx.VSCROLL | wx.SUNKEN_BORDER  )
+        self.height = 1200
+        self.SetScrollbars(10, 10,80, self.height/10)
+        self.main_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.panels = {}
+        keys = handler.child_handlers.keys()
+        for k in keys:
+            self.panels[k] = handler.child_handlers[k].get_design_panel(self, [k])
+        self.sub_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.sub_sizer2 = wx.BoxSizer(wx.VERTICAL)
+        self.sub_sizer.Add(self.panels['general'], 1, wx.EXPAND)
+        self.sub_sizer.Add(self.panels['abilities'], 1, wx.EXPAND)
+
+        self.sub_sizer.Add(self.panels['attacks'], 2, wx.EXPAND)
+        self.sub_sizer.Add(self.panels['ac'], 1, wx.EXPAND)
+        self.sub_sizer.Add(self.panels['spells'], 1, wx.EXPAND)
+
+        self.sub_sizer2.Add(self.panels['classes'], 2, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['hp'], 1, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['pp'], 1, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['saves'], 2, wx.EXPAND)
+
+        self.sub_sizer2.Add(self.panels['feats'], 2, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['powers'], 2, wx.EXPAND)
+        self.sub_sizer2.Add(self.panels['skills'], 3, wx.EXPAND)
+
+        self.main_sizer.Add(self.sub_sizer,   1, wx.EXPAND)
+        self.main_sizer.Add(self.sub_sizer2,   1, wx.EXPAND)
+        self.panels['abilities'].panel.char_wnd = self
+        self.SetSizer(self.main_sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.SetScrollbars(10, 10,s[0]/10, self.height/10)
+        dc = wx.ClientDC(self)
+        x = dc.DeviceToLogicalX(0)
+        y = dc.DeviceToLogicalY(0)
+        self.main_sizer.SetDimension(x,y,s[0],self.height)
+        evt.Skip()
+
+    def refresh_data(self):
+        self.panels['saves'].panel.refresh_data()
+        self.panels['skills'].panel.refresh_data()
+        self.panels['attacks'].panel.refresh_data()
+        self.panels['powers'].panel.refresh_data()
+        self.panels['spells'].panel.refresh_data()
+
+HOWTO_MAX = wx.NewId()
+
+class howto_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        pname = handler.master_dom.setAttribute("name", 'How To')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+        self.sizer.AddMany([ (wx.StaticText(self, -1, t_node._get_nodeValue()),   0, wx.ALIGN_CENTER_VERTICAL),
+                 ])
+        self.sizer.AddGrowableCol(1)
+        self.SetSizer(self.sizer)
+
+
+HP_CUR = wx.NewId()
+HP_MAX = wx.NewId()
+
+class hp_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        pname = handler.master_dom.setAttribute("name", 'HitPoints')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+        self.sizer.AddMany([ (wx.StaticText(self, -1, "HP Current:"),   0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, HP_CUR, self.master_dom.getAttribute('current')),   0, wx.EXPAND),
+                 (wx.StaticText(self, -1, "HP Max:"), 0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, HP_MAX, self.master_dom.getAttribute('max')),  0, wx.EXPAND),
+                 ])
+        self.sizer.AddGrowableCol(1)
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=HP_MAX)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=HP_CUR)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == HP_CUR:
+            self.master_dom.setAttribute('current',evt.GetString())
+        elif id == HP_MAX:
+            self.master_dom.setAttribute('max',evt.GetString())
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+PP_CUR = wx.NewId()
+PP_MAX = wx.NewId()
+PP_FRE = wx.NewId()
+PP_MFRE = wx.NewId()
+
+class pp_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        pname = handler.master_dom.setAttribute("name", 'PowerPoints')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+
+        self.sizer.AddMany([ (wx.StaticText(self, -1, "PP Current:"),   0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, PP_CUR, self.master_dom.getAttribute('current1')),   0, wx.EXPAND),
+                 (wx.StaticText(self, -1, "PP Max:"), 0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, PP_MAX, self.master_dom.getAttribute('max1')),  0, wx.EXPAND),
+                 (wx.StaticText(self, -1, "Current Free Talants per day:"), 0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, PP_FRE, self.master_dom.getAttribute('free')),  0, wx.EXPAND),
+                 (wx.StaticText(self, -1, "Max Free Talants per day:"), 0, wx.ALIGN_CENTER_VERTICAL),
+                 (wx.TextCtrl(self, PP_MFRE, self.master_dom.getAttribute('maxfree')),  0, wx.EXPAND),
+                 ])
+        self.sizer.AddGrowableCol(1)
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_MAX)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_CUR)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_FRE)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_MFRE)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == PP_CUR:
+            self.master_dom.setAttribute('current1',evt.GetString())
+        elif id == PP_MAX:
+            self.master_dom.setAttribute('max1',evt.GetString())
+        elif id == PP_FRE:
+            self.master_dom.setAttribute('free',evt.GetString())
+        elif id == PP_MFRE:
+            self.master_dom.setAttribute('maxfree',evt.GetString())
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+
+class gen_grid(wx.grid.Grid):
+    """grid for gen info"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'General')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        n_list = handler.master_dom._get_childNodes()
+        self.CreateGrid(len(n_list),2)
+        self.SetRowLabelSize(0)
+        self.SetColLabelSize(0)
+        self.n_list = n_list
+        i = 0
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        t_node = self.n_list[row]._get_firstChild()
+        t_node._set_nodeValue(value)
+        if row==0: self.handler.on_name_change(value)
+
+    def refresh_row(self,rowi):
+        t_node = safe_get_text_node(self.n_list[rowi])
+        self.SetCellValue(rowi,0,self.n_list[rowi]._get_tagName())
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,t_node._get_nodeValue())
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+class inventory_grid(wx.grid.Grid):
+    """grid for gen info"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Money and Inventory')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        n_list = handler.master_dom._get_childNodes()
+        self.CreateGrid(len(n_list),2)
+        self.SetRowLabelSize(0)
+        self.SetColLabelSize(0)
+        self.n_list = n_list
+        i = 0
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        t_node = self.n_list[row]._get_firstChild()
+        t_node._set_nodeValue(value)
+        if row==0: self.handler.on_name_change(value)
+
+    def refresh_row(self,rowi):
+        t_node = safe_get_text_node(self.n_list[rowi])
+        self.SetCellValue(rowi,0,self.n_list[rowi]._get_tagName())
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,t_node._get_nodeValue())
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+class abil_grid(wx.grid.Grid):
+    """grid for abilities"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Stats')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        stats = handler.master_dom.getElementsByTagName('stat')
+        self.CreateGrid(len(stats),3)
+        self.SetRowLabelSize(0)
+        col_names = ['Ability','Score','Modifier']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.stats = stats
+        i = 0
+        for i in range(len(stats)):
+            self.refresh_row(i)
+        self.char_wnd = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            self.stats[row].setAttribute('base',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+        if self.char_wnd:
+            self.char_wnd.refresh_data()
+
+    def refresh_row(self,rowi):
+        s = self.stats[rowi]
+        name = s.getAttribute('name')
+        abbr = s.getAttribute('abbr')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,s.getAttribute('base'))
+        self.SetCellValue(rowi,2,str(self.handler.get_mod(abbr)))
+        self.SetReadOnly(rowi,2)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()-1):
+            self.refresh_row(r)
+
+
+class save_grid(wx.grid.Grid):
+    """grid for saves"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Saves')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        saves = handler.master_dom.getElementsByTagName('save')
+        self.stats = handler.char_hander.child_handlers['abilities']
+        self.CreateGrid(len(saves),7)
+        self.SetRowLabelSize(0)
+        col_names = ['Save','Key','base','Abil','Magic','Misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.saves = saves
+        i = 0
+        for i in range(len(saves)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            if col == 2:
+                self.saves[row].setAttribute('base',value)
+            elif col ==4:
+                self.saves[row].setAttribute('magmod',value)
+            elif col ==4:
+                self.saves[row].setAttribute('miscmod',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_row(self,rowi):
+        s = self.saves[rowi]
+        name = s.getAttribute('name')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        stat = s.getAttribute('stat')
+        self.SetCellValue(rowi,1,stat)
+        self.SetReadOnly(rowi,1)
+        self.SetCellValue(rowi,2,s.getAttribute('base'))
+        self.SetCellValue(rowi,3,str(self.stats.get_mod(stat)))
+        self.SetReadOnly(rowi,3)
+        self.SetCellValue(rowi,4,s.getAttribute('magmod'))
+        self.SetCellValue(rowi,5,s.getAttribute('miscmod'))
+        mod = str(self.handler.get_mod(name))
+        self.SetCellValue(rowi,6,mod)
+        self.SetReadOnly(rowi,6)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()):
+            self.refresh_row(r)
+
+
+class skill_grid(wx.grid.Grid):
+    """ panel for skills """
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Skills')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        skills = handler.master_dom.getElementsByTagName('skill')
+        self.stats = handler.char_hander.child_handlers['abilities']
+        self.CreateGrid(len(skills),6)
+        self.SetRowLabelSize(0)
+        col_names = ['Skill','Key','Rank','Abil','Misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        rowi = 0
+        self.skills = skills
+        for i in range(len(skills)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            if col == 2:
+                self.skills[row].setAttribute('rank',value)
+            elif col ==4:
+                self.skills[row].setAttribute('misc',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_row(self,rowi):
+        s = self.skills[rowi]
+        name = s.getAttribute('name')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        stat = s.getAttribute('stat')
+        self.SetCellValue(rowi,1,stat)
+        self.SetReadOnly(rowi,1)
+        self.SetCellValue(rowi,2,s.getAttribute('rank'))
+        self.SetCellValue(rowi,3,str(self.stats.get_mod(stat)))
+        self.SetReadOnly(rowi,3)
+        self.SetCellValue(rowi,4,s.getAttribute('misc'))
+        mod = str(self.handler.get_mod(name))
+        self.SetCellValue(rowi,5,mod)
+        self.SetReadOnly(rowi,5)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()):
+            self.refresh_row(r)
+
+
+
+class feat_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Feats')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Feat"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Feat"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),2,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"Feat")
+        self.grid.SetColLabelValue(1,"Type")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+        self.SetSizer(self.sizer)
+
+    def refresh_row(self,i):
+        feat = self.n_list[i]
+        name = feat.getAttribute('name')
+        type = feat.getAttribute('type')
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,type)
+        self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["d20"]+"d20feats.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('feat')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Feat','Feats',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+
+class spell_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Arcane Spells')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Spell"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Spell"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 30, "Refresh Spells"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.Bind(wx.EVT_BUTTON, self.on_refresh_spells, id=30)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),4,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"No.")
+        self.grid.SetColLabelValue(1,"Lvl")
+        self.grid.SetColLabelValue(2,"Spell")
+        self.grid.SetColLabelValue(3,"Desc")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col == 0:
+            self.n_list[row].setAttribute('memrz',value)
+
+
+    def refresh_row(self,i):
+        spell = self.n_list[i]
+        memrz = spell.getAttribute('memrz')
+        name = spell.getAttribute('name')
+        type = spell.getAttribute('desc')
+        level = spell.getAttribute('level')
+        self.grid.SetCellValue(i,0,memrz)
+        self.grid.SetCellValue(i,2,name)
+        self.grid.SetReadOnly(i,2)
+        self.grid.SetCellValue(i,3,type)
+        self.grid.SetReadOnly(i,3)
+        self.grid.SetCellValue(i,1,level)
+        self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.handler.refresh_spells()
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["d20"]+"d20spells.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('spell')
+        opts = []
+        # lvl = int(self.handler.get_char_lvl('level'))
+        # castlvl = lvl / 2
+        for f in f_list:
+            opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+            # spelllvl = f.getAttribute('level')
+            # if spelllvl <= "1":
+            #     opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+            # else:
+            #     if eval('%d >= %s' %(castlvl, spelllvl)):
+            #         opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Spell','Spells',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('spell')
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_spells()
+        dlg.Destroy()
+
+    def on_refresh_spells( self, evt ):
+        f_list = self.master_dom.getElementsByTagName('spell')
+        for spell in f_list:
+            spell.setAttribute( 'used', '0' )
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+        self.grid.SetColSize(0,w * 0.10)
+        self.grid.SetColSize(1,w * 0.10)
+        self.grid.SetColSize(2,w * 0.30)
+        self.grid.SetColSize(3,w * 0.50)
+
+    def refresh_data(self):
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+class divine_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Divine Spells')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Spell"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Spell"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 30, "Refresh Spells"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.Bind(wx.EVT_BUTTON, self.on_refresh_spells, id=30)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),4,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"No.")
+        self.grid.SetColLabelValue(1,"Lvl")
+        self.grid.SetColLabelValue(2,"Spell")
+        self.grid.SetColLabelValue(3,"Desc")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col == 0:
+            self.n_list[row].setAttribute('memrz',value)
+
+
+    def refresh_row(self,i):
+        spell = self.n_list[i]
+        memrz = spell.getAttribute('memrz')
+        name = spell.getAttribute('name')
+        type = spell.getAttribute('desc')
+        level = spell.getAttribute('level')
+        self.grid.SetCellValue(i,0,memrz)
+        self.grid.SetCellValue(i,2,name)
+        self.grid.SetReadOnly(i,2)
+        self.grid.SetCellValue(i,3,type)
+        self.grid.SetReadOnly(i,3)
+        self.grid.SetCellValue(i,1,level)
+        self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.handler.refresh_spells()
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["d20"]+"d20divine.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('gift')
+        opts = []
+        # lvl = int(self.handler.get_char_lvl('level'))
+        # castlvl = lvl / 2
+        for f in f_list:
+            opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+            # spelllvl = f.getAttribute('level')
+            # if spelllvl <= "1":
+            #     opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+            # else:
+            #     if eval('%d >= %s' %(castlvl, spelllvl)):
+            #         opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Spell','Spells',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('gift')
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_spells()
+        dlg.Destroy()
+
+    def on_refresh_spells( self, evt ):
+        f_list = self.master_dom.getElementsByTagName('gift')
+        for spell in f_list:
+            spell.setAttribute( 'used', '0' )
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+        self.grid.SetColSize(0,w * 0.10)
+        self.grid.SetColSize(1,w * 0.10)
+        self.grid.SetColSize(2,w * 0.30)
+        self.grid.SetColSize(3,w * 0.50)
+
+    def refresh_data(self):
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+
+class power_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Pionic Powers')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Power"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Power"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 30, "Refresh Powers"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.Bind(wx.EVT_BUTTON, self.on_refresh_powers, id=30)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),5,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"PP")
+        self.grid.SetColLabelValue(1,"Lvl")
+        self.grid.SetColLabelValue(2,"Power")
+        self.grid.SetColLabelValue(3,"Desc")
+        self.grid.SetColLabelValue(4,"Type")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.refresh_data()
+        self.temp_dom = None
+        self.SetSizer(self.sizer)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        """if col == 0:
+            self.n_list[row].setAttribute('memrz',value)"""
+
+
+    def refresh_row(self,i):
+        power = self.n_list[i]
+        point = power.getAttribute('point')
+        name = power.getAttribute('name')
+        type = power.getAttribute('desc')
+        test = power.getAttribute('test')
+        level = power.getAttribute('level')
+        self.grid.SetCellValue(i,0,point)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,level)
+        self.grid.SetReadOnly(i,1)
+        self.grid.SetCellValue(i,2,name)
+        self.grid.SetReadOnly(i,2)
+        self.grid.SetCellValue(i,3,type)
+        self.grid.SetReadOnly(i,3)
+        self.grid.SetCellValue(i,4,test)
+        self.grid.SetReadOnly(i,4)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.handler.refresh_powers()
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["d20"]+"d20powers.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('power')
+        opts = []
+        # lvl = int(self.handler.get_char_lvl('level'))
+        # castlvl = lvl / 2
+        for f in f_list:
+            opts.append("(" + f.getAttribute('level') + ") - " + f.getAttribute('name') + " - " + f.getAttribute('test'))
+            # spelllvl = f.getAttribute('level')
+            # if spelllvl <= "1":
+            #     opts.append("(" + f.getAttribute('level') + ") - " + f.getAttribute('name') + " - " + f.getAttribute('test'))
+            # else:
+            #     if eval('%d >= %s' %(castlvl, spelllvl)):
+            #         opts.append("(" + f.getAttribute('level') + ") - " + f.getAttribute('name') + " - " + f.getAttribute('test'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Power','Powers',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('power')
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_powers()
+        dlg.Destroy()
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.n_list = self.master_dom.getElementsByTagName('weapon')
+                self.handler.refresh_weapons()
+
+
+    def on_refresh_powers( self, evt ):
+        mfre = self.handler.get_char_pp('maxfree')
+        mpp = self.handler.get_char_pp('max1')
+        self.handler.set_char_pp( 'free', mfre )
+        self.handler.set_char_pp( 'current1', mpp )
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+        self.grid.SetColSize(0,w * 0.05)
+        self.grid.SetColSize(1,w * 0.05)
+        self.grid.SetColSize(2,w * 0.30)
+        self.grid.SetColSize(3,w * 0.30)
+        self.grid.SetColSize(4,w * 0.30)
+
+    def refresh_data(self):
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+class attack_grid(wx.grid.Grid):
+    """grid for attacks"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Melee')
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.parent = parent
+        self.handler = handler
+        self.babs = self.handler.melee
+        self.CreateGrid(1,7)
+        self.SetRowLabelSize(0)
+        col_names = ['base','base 2','base 3','base 4','base 5','base 6','misc']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.refresh_data()
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+        except:
+            value = "0"
+            self.SetCellValue( row, col, value )
+        attribs = ['base','second','third','forth','fifth','sixth','misc']
+        self.babs.setAttribute( attribs[col], value )
+        self.parent.refresh_data()
+
+    def refresh_data(self):
+        attack_mods = self.handler.get_attack_data()
+        for i in range(0,7):
+            self.SetCellValue( 0, i, str(attack_mods[i]) )
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/cols
+        for i in range(0,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+class weapon_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Weapons')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Weapon"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Weapon"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        n_list = handler.master_dom.getElementsByTagName('weapon')
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.handler = handler
+        self.grid.CreateGrid(len(n_list),8,1)
+        self.grid.SetRowLabelSize(0)
+        col_names = ['Name','damage','mod','critical','type','weight','range','size']
+        for i in range(len(col_names)):
+            self.grid.SetColLabelValue(i,col_names[i])
+        self.refresh_data()
+        self.temp_dom = None
+        self.SetSizer(self.sizer)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col == 0:
+            self.n_list[row].setAttribute('name',value)
+            self.handler.refresh_weapons();
+        else:
+            self.n_list[row].setAttribute(self.grid.GetColLabelValue(col),value)
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+        name = n.getAttribute('name')
+        mod = n.getAttribute('mod')
+        ran = n.getAttribute('range')
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetCellValue(i,1,n.getAttribute('damage'))
+        self.grid.SetCellValue(i,2,mod)
+        self.grid.SetCellValue(i,3,n.getAttribute('critical'))
+        self.grid.SetCellValue(i,4,n.getAttribute('type'))
+        self.grid.SetCellValue(i,5,n.getAttribute('weight'))
+        self.grid.SetCellValue(i,6,ran)
+        self.grid.SetCellValue(i,7,n.getAttribute('size') )
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.n_list = self.master_dom.getElementsByTagName('weapon')
+                self.handler.refresh_weapons()
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["d20"]+"d20weapons.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('weapon')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Weapon','Weapon List',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('weapon')
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_weapons()
+        dlg.Destroy()
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols+1)
+        self.grid.SetColSize(0,col_w*2)
+        for i in range(1,cols):
+            self.grid.SetColSize(i,col_w)
+
+    def refresh_data(self):
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+
+class attack_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Melee')
+        wx.Panel.__init__(self, parent, -1)
+
+        self.a_grid = attack_grid(self, handler)
+        self.w_panel = weapon_panel(self, handler)
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.sizer.Add(self.a_grid, 1, wx.EXPAND)
+        self.sizer.Add(self.w_panel, 2, wx.EXPAND)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.SetSizer(self.sizer)
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+    def refresh_data(self):
+        self.w_panel.refresh_data()
+        self.a_grid.refresh_data()
+
+
+class ac_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Armor')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Armor"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Armor"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.master_dom = handler.master_dom
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        col_names = ['Armor','bonus','maxdex','cp','sf','weight','speed','type']
+        self.grid.CreateGrid(len(n_list),len(col_names),1)
+        self.grid.SetRowLabelSize(0)
+        for i in range(len(col_names)):
+            self.grid.SetColLabelValue(i,col_names[i])
+        self.atts =['name','bonus','maxdex','checkpenalty','spellfailure','weight','speed','type']
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+        self.SetSizer(self.sizer)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col >= 1 and col <= 5:
+            try:
+                int(value)
+                self.n_list[row].setAttribute(self.atts[col],value)
+            except:
+                self.grid.SetCellValue(row,col,"0")
+        else:
+            self.n_list[row].setAttribute(self.atts[col],value)
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+        for y in range(len(self.atts)):
+            self.grid.SetCellValue(i,y,n.getAttribute(self.atts[y]))
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["d20"]+"d20armor.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('armor')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Armor:','Armor List',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols+2)
+        self.grid.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.grid.SetColSize(i,col_w)
+
+
+class class_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Class')
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, 10, "Remove Class"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, 20, "Add Class"), 1, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),2,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"Class")
+        self.grid.SetColLabelValue(1,"Level")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        try:
+            int(value)
+            self.n_list[row].setAttribute('level',value)
+        except:
+            self.grid.SetCellValue(row,col,"1")
+
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+        name = n.getAttribute('name')
+        level = n.getAttribute('level')
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,level)
+        #self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["d20"]+"d20classes.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('class')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Class','Classes',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/dnd35.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2397 @@
+import orpg.tools.orpg_settings
+import orpg.minidom
+from core import *
+from containers import *
+from string import *  #a 1.6003
+from inspect import *  #a 1.9001
+dnd35_EXPORT = wx.NewId()
+
+############Global Stuff##############
+
+HP_CUR = wx.NewId()
+HP_MAX = wx.NewId()
+
+def getRoot (node): # a 1.5002 this whole function is new.
+    root = None
+    target = node
+    while target != None:
+        root = target
+        target = target.hparent
+    return root
+
+#a 1.6 convinience function added safeGetAttr
+def safeGetAttr(node,lable,defRetV=None):
+    cna=node.attributes
+    for i2 in range(len(cna)):
+        if cna.item(i2).name == lable:
+            return cna.item(i2).value
+    #retV=node.getAttribute(lable) # getAttribute does not distingish between
+    # the attribute not being present vs it having a value of ""
+    # This is bad for this routine, thus not used.
+    return defRetV
+#a 1.6... safeGetAttr end.
+
+########End of My global Stuff########
+########Start of Main Node Handlers#######
+class dnd35char_handler(container_handler):
+    """ Node handler for a dnd35 charactor
+        <nodehandler name='?'  module='dnd35' class='dnd35char_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.Version = "v1.000" #a 1.6000 general documentation, usage.
+
+        print "dnd35char_handler - version:",self.Version #m 1.6000
+
+        self.hparent = None #a 1.5002 allow ability to run up tree, this is the
+
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('general','GeneralInformation',dnd35general,'gear')
+        self.new_child_handler('inventory','MoneyAndInventory',dnd35inventory,'money')
+        self.new_child_handler('character','ClassesAndStats',dnd35classnstats,'knight')
+        self.new_child_handler('snf','SkillsAndFeats',dnd35skillsnfeats,'book')
+        self.new_child_handler('combat','Combat',dnd35combat,'spears')
+
+        self.myeditor = None
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+    def tohtml(self):
+        html_str = "<table><tr><td colspan=2 >"
+        html_str += self.general.tohtml()+"</td></tr>"
+        html_str += "<tr><td width='50%' valign=top >"+self.abilities.tohtml()
+        html_str += "<P>" + self.saves.tohtml()
+        html_str += "<P>" + self.attacks.tohtml()
+        html_str += "<P>" + self.ac.tohtml()
+        html_str += "<P>" + self.feats.tohtml()
+        html_str += "<P>" + self.inventory.tohtml() +"</td>"
+        html_str += "<td width='50%' valign=top >"+self.classes.tohtml()
+        html_str += "<P>" + self.hp.tohtml()
+        html_str += "<P>" + self.skills.tohtml() +"</td>"
+        #a block for 1.6009 end
+
+        html_str += "</tr></table>"
+        return html_str
+
+    def about(self):
+        html_str = "<img src='" + orpg.dirpath.dir_struct["icon"]
+        html_str += "dnd3e_logo.gif' ><br /><b>dnd35 Character Tool "
+        html_str += self.Version+"</b>" #m 1.6000 was hard coded.
+        html_str += "<br />by Dj Gilcrease<br />digitalxero@gmail.com"
+        return html_str
+
+########Core Handlers are done now############
+########Onto the Sub Nodes########
+##Primary Sub Node##
+
+class outline_panel(wx.Panel):
+    def __init__(self, parent, handler, wnd, txt,):
+        self.parent = parent #a 1.9001
+        wx.Panel.__init__(self, parent, -1)
+        self.panel = wnd(self,handler)
+        self.sizer = wx.StaticBoxSizer(wx.StaticBox(self,-1,txt), wx.VERTICAL)
+
+        self.sizer.Add(self.panel, 1, wx.EXPAND)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+class dnd35_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+class dnd35general(dnd35_char_child):
+    """ Node Handler for general information.   This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        dnd35_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.general = self  #a 1.5002
+        self.charName = self.get_char_name() # a 1.5002 make getting name easier.
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,gen_grid,"General Information")
+        wnd.title = "General Info"
+        return wnd
+
+    def tohtml(self):
+        n_list = self.master_dom._get_childNodes()
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>General Information</th></tr><tr><td>"
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+            html_str += "<B>"+n._get_tagName().capitalize() +":</B> "
+            html_str += t_node._get_nodeValue() + ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def on_name_change(self,name):
+        self.char_hander.rename(name)
+        #o 1.5002 self.char_hander = parent in this case.
+        self.charName = name  #a 1.5002 make getting name easier.
+
+
+    def get_char_name( self ):
+        node = self.master_dom.getElementsByTagName( 'name' )[0]
+        t_node = safe_get_text_node( node )
+        return t_node._get_nodeValue()
+
+class gen_grid(wx.grid.Grid):
+    """grid for gen info"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'General')
+        self.hparent = handler #a 1.5002 allow ability to run up tree, needed
+        # a 1.5002 parent is functional parent, not invoking parent.
+
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        n_list = handler.master_dom._get_childNodes()
+        self.CreateGrid(len(n_list),2)
+        self.SetRowLabelSize(0)
+        self.SetColLabelSize(0)
+        self.n_list = n_list
+        i = 0
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        t_node = self.n_list[row]._get_firstChild()
+        t_node._set_nodeValue(value)
+        if row==0:
+            self.handler.on_name_change(value)
+        self.AutoSizeColumn(1)
+
+    def refresh_row(self,rowi):
+        t_node = safe_get_text_node(self.n_list[rowi])
+
+        self.SetCellValue(rowi,0,self.n_list[rowi]._get_tagName())
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,t_node._get_nodeValue())
+        self.AutoSizeColumn(1)
+
+class dnd35inventory(dnd35_char_child):
+    """ Node Handler for general information.   This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        dnd35_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.6009
+        self.root.inventory = self #a 1.6009
+
+    def get_design_panel(self,parent):
+        wnd = inventory_pane(parent, self) #outline_panel(parent,self,inventory_grid,"Inventory")
+        wnd.title = "Inventory"
+        return wnd
+
+    def tohtml(self):
+        n_list = self.master_dom._get_childNodes()
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Inventory</th></tr><tr><td>"
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+            html_str += "<B>"+n._get_tagName().capitalize() +":</B> "
+            html_str += t_node._get_nodeValue() + "<br />"
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+class inventory_pane(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, wx.ID_ANY)
+
+        self.n_list = handler.master_dom._get_childNodes()
+        self.autosize = False
+
+        self.sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, "Inventroy"), wx.VERTICAL)
+
+        self.lang = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_BESTWRAP, name="Languages")
+        self.gear = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_BESTWRAP, name="Gear")
+        self.magic = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_BESTWRAP, name="Magic")
+        self.grid = wx.grid.Grid(self, wx.ID_ANY, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+
+        self.grid.CreateGrid(len(self.n_list)-3,2)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelSize(0)
+
+        for i in xrange(len(self.n_list)):
+            self.refresh_row(i)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(self.grid, 1, wx.EXPAND)
+        sizer1.Add(self.lang, 1, wx.EXPAND)
+
+        self.sizer.Add(sizer1, 0, wx.EXPAND)
+
+        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer2.Add(self.gear, 1, wx.EXPAND)
+        sizer2.Add(self.magic, 1, wx.EXPAND)
+
+        self.sizer.Add(sizer2, 1, wx.EXPAND)
+
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.Bind(wx.EVT_TEXT, self.onTextNodeChange, self.lang)
+        self.Bind(wx.EVT_TEXT, self.onTextNodeChange, self.gear)
+        self.Bind(wx.EVT_TEXT, self.onTextNodeChange, self.magic)
+        self.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self.on_cell_change, self.grid)
+
+
+    def fillTextNode(self, name, value):
+        if name == 'Languages':
+            self.lang.SetValue(value)
+        elif name == 'Gear':
+            self.gear.SetValue(value)
+        elif name == 'Magic':
+            self.magic.SetValue(value)
+
+    def onTextNodeChange(self, event):
+        id = event.GetId()
+
+        if id == self.gear.GetId():
+            nodeName = 'Gear'
+            value = self.gear.GetValue()
+        elif id == self.magic.GetId():
+            nodeName = 'Magic'
+            value = self.magic.GetValue()
+        elif id == self.lang.GetId():
+            nodeName = 'Languages'
+            value = self.lang.GetValue()
+
+        for node in self.n_list:
+            if node._get_tagName() == nodeName:
+                t_node = safe_get_text_node(node)
+                t_node._set_nodeValue(value)
+
+    def saveMoney(self, row, col):
+        value = self.grid.GetCellValue(row, col)
+        t_node = safe_get_text_node(self.n_list[row])
+        t_node._set_nodeValue(value)
+
+    def on_cell_change(self, evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        self.grid.AutoSizeColumn(col)
+        wx.CallAfter(self.saveMoney, row, col)
+
+
+
+    def refresh_row(self, row):
+        t_node = safe_get_text_node(self.n_list[row])
+        tagname = self.n_list[row]._get_tagName()
+        value = t_node._get_nodeValue()
+        if tagname == 'Gear':
+            self.fillTextNode(tagname, value)
+        elif tagname == 'Magic':
+            self.fillTextNode(tagname, value)
+        elif tagname == 'Languages':
+            self.fillTextNode(tagname, value)
+        else:
+            self.grid.SetCellValue(row, 0, tagname)
+            self.grid.SetReadOnly(row, 0)
+            self.grid.SetCellValue(row, 1, value)
+            self.grid.AutoSize()
+
+
+class dnd35classnstats(dnd35_char_child):
+    """ Node handler for a dnd35 charactor
+        <nodehandler name='?'  module='dnd35' class='dnd35char_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        dnd35_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('abilities','Abilities Scores',dnd35ability,'gear')
+        self.new_child_handler('classes','Classes',dnd35classes,'knight')
+        self.new_child_handler('saves','Saves',dnd35saves,'skull')
+        self.myeditor = None
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+class class_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+class dnd35ability(class_char_child):
+    """ Node Handler for ability.   This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        class_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self)  #a 1.5002 get top of our local function tree.
+        self.root.abilities = self #a 1.5002 let other classes find me.
+
+        self.abilities = {}
+        node_list = self.master_dom.getElementsByTagName('stat')
+        tree = self.tree
+        icons = tree.icons
+
+        for n in node_list:
+            name = n.getAttribute('abbr')
+            self.abilities[name] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['gear'], icons['gear'] )
+            tree.SetPyData( new_tree_node, self )
+        #print "dnd35ability - init self.abilities",self.abilities #a (debug) 1.5002
+
+    def on_rclick( self, evt ):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        #if item == self.mytree_node:   #d 1.6016
+        #    dnd35_char_child.on_ldclick( self, evt ) #d 1.6016
+        if not item == self.mytree_node: #a 1.6016
+        #else: #d 1.6016
+            mod = self.get_mod( name )
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s check: [1d20%s%s]' % ( name, mod1, mod )
+            chat.ParsePost( txt, True, True )
+
+    def get_mod(self,abbr):
+        score = int(self.abilities[abbr].getAttribute('base'))
+        mod = (score - 10) / 2
+        mod = int(mod)
+        return mod
+
+    def set_score(self,abbr,score):
+        if score >= 0:
+            self.abilities[abbr].setAttribute("base",str(score))
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,abil_grid,"Abilities")
+        wnd.title = "Abilities (edit)"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100%><tr BGCOLOR=#E9E9E9 ><th width='50%'>Ability</th>
+                    <th>Base</th><th>Modifier</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('stat')
+        for n in node_list:
+            name = n.getAttribute('name')
+            abbr = n.getAttribute('abbr')
+            base = n.getAttribute('base')
+            mod = str(self.get_mod(abbr))
+            if int(mod) >= 0: #m 1.6013 added "int(" and ")"
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str = (html_str + "<tr ALIGN='center'><td>"+
+                name+"</td><td>"+base+'</td><td>%s%s</td></tr>' % (mod1, mod))
+        html_str = html_str + "</table>"
+        return html_str
+
+class abil_grid(wx.grid.Grid):
+    """grid for abilities"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Stats')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self)
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        stats = handler.master_dom.getElementsByTagName('stat')
+        self.CreateGrid(len(stats),3)
+        self.SetRowLabelSize(0)
+        col_names = ['Ability','Score','Modifier']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.stats = stats
+        i = 0
+        for i in range(len(stats)):
+            self.refresh_row(i)
+        self.char_wnd = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        #print value
+        try:
+            int(value)
+            self.stats[row].setAttribute('base',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+        if self.char_wnd:
+            self.char_wnd.refresh_data()
+
+    #mark5
+
+    def refresh_row(self,rowi):
+        s = self.stats[rowi]
+
+        name = s.getAttribute('name')
+        abbr = s.getAttribute('abbr')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,s.getAttribute('base'))
+        self.SetCellValue(rowi,2,str(self.handler.get_mod(abbr)))
+        self.SetReadOnly(rowi,2)
+        #if self.root.saves.saveGrid: #a 1.6018 d 1.9002 whole if clause
+            #print getmembers(self.root.saves.saveGrid)
+            #self.root.saves.saveGrid.refresh_data() #a 1.6018
+            #print "skipping saving throw update, put back in later"
+        self.root.saves.refresh_data() #a 1.9002
+        self.root.attacks.refreshMRdata() #a 1.9001 `
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()-1):
+            self.refresh_row(r)
+
+class dnd35classes(class_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        class_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self)
+        self.root.classes = self
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,class_panel,"Classes")
+        wnd.title = "Classes"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Classes</th></tr><tr><td>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += n.getAttribute('name') + " ("+n.getAttribute('level')+"), "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        node_list = self.master_dom.getElementsByTagName('class')
+        # print "eclasses - get_char_lvl node_list",node_list
+        tot = 0  #a 1.5009 actually, slipping in a quick enhancement ;-)
+        for n in node_list:
+            lvl = n.getAttribute('level') #o 1.5009 not sure of the value of this
+            tot += int(lvl) #a 1.5009
+            type = n.getAttribute('name') #o 1.5009 not sure of the value of this
+            #print type,lvl #a (debug) 1.5009
+            if attr == "level":
+                return lvl #o 1.5009 this returns the level of someone's first class. ???
+            elif attr == "class":
+                return type #o 1.5009 this returns one of the char's classes. ???
+        if attr == "lvl":   #a 1.5009 this has value, adding this.
+            return tot  #a 1.5009 return character's "overall" level.
+
+    def get_class_lvl( self, classN ): #a 1.5009 need to be able to get monk lvl
+        #a 1.5009 this function is new.
+        node_list = self.master_dom.getElementsByTagName('class')
+        #print "eclasses - get_class_lvl node_list",node_list
+        for n in node_list:
+            lvl = n.getAttribute('level')
+            type = n.getAttribute('name')
+            if classN == type:
+                return lvl
+
+class class_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Class')
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Class"), 0, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Class"), 0, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),3,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"Class")
+        self.grid.SetColLabelValue(1,"Level")
+        self.grid.SetColLabelValue(2,"Refrence")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        try:
+            int(value)
+            self.n_list[row].setAttribute('level',value)
+        except:
+            self.grid.SetCellValue(row,col,"1")
+
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+
+        name = n.getAttribute('name')
+        level = n.getAttribute('level')
+        book = n.getAttribute('book')
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,level)
+        self.grid.SetCellValue(i,2,book)
+        self.grid.SetReadOnly(i,0)
+        self.grid.AutoSizeColumn(0)
+        self.grid.AutoSizeColumn(1)
+        self.grid.AutoSizeColumn(2)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd35"]+"dnd35classes.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('class')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Class','Classes',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+
+
+class dnd35saves(class_char_child):
+    """ Node Handler for saves.   This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        class_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        #self.saveGrid = None  #a 1.6018 d 1.9002
+        self.saveGridFrame = []  #a 1.9002 handle list, check frame for close.
+
+        tree = self.tree
+        icons = self.tree.icons
+
+        self.root = getRoot(self) #a 1.5002
+        self.root.saves = self #a 1.6009
+        node_list = self.master_dom.getElementsByTagName('save')
+        self.saves={}
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.saves[name] = n
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+
+    #a 1.9002 this whole method
+    def refresh_data(self): # refresh the data in the melee/ranged section
+        # of the attack chart.
+        # count backwards, maintains context despite "removes"
+        for i in range(len(self.saveGridFrame)-1,-1,-1):
+            x = self.saveGridFrame[i]
+            if x == None:
+                x.refresh_data()
+            else:
+                self.saveGridFrame.remove(x)
+
+    def get_mod(self,name):
+        save = self.saves[name]
+        stat = save.getAttribute('stat')
+        #print "dnd35saves, get_mod: self,root",self,self.root #a (debug) 1.5002
+        #print "and abilities",self.root.abilities      #a (debug) 1.5002
+        stat_mod = self.root.abilities.get_mod(stat)            #a 1.5002
+        base = int(save.getAttribute('base'))
+        miscmod = int(save.getAttribute('miscmod'))
+        magmod = int(save.getAttribute('magmod'))
+        total = stat_mod + base + miscmod + magmod
+        return total
+
+    def on_rclick(self,evt):
+
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            pass #a 1.5003 syntatic place holder
+            return #a 1.5003
+            #print "failure mode!"
+            #dnd35_char_child.on_ldclick(self,evt) #d 1.5003 this busted
+            #wnd = save_grid(self.frame.note,self)
+            #wnd.title = "Saves"
+            #self.frame.add_panel(wnd)
+        else:
+            mod = self.get_mod(name)
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s save: [1d20%s%s]' % (name, mod1, mod)
+            chat.ParsePost( txt, True, True )
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,save_grid,"Saves")
+        wnd.title = "Saves"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 >
+            <th width='30%'>Save</th>
+            <th>Key</th><th>Base</th><th>Abil</th><th>Magic</th>
+            <th>Misc</th><th>Total</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('save')
+        for n in node_list:
+            name = n.getAttribute('name')
+            stat = n.getAttribute('stat')
+            base = n.getAttribute('base')
+            html_str = html_str + "<tr ALIGN='center'><td>"+name+"</td><td>"+stat+"</td><td>"+base+"</td>"
+            #stat_mod = str(dnd_globals["stats"][stat])         #d 1.5002
+            stat_mod = self.root.abilities.get_mod(stat)        #a 1.5002
+
+            mag = n.getAttribute('magmod')
+            misc = n.getAttribute('miscmod')
+            mod = str(self.get_mod(name))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            #m 1.5009 next line.  added str() around stat_mod
+            html_str = html_str + "<td>"+str(stat_mod)+"</td><td>"+mag+"</td>"
+            html_str = html_str + '<td>'+misc+'</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+#mark6
+class save_grid(wx.grid.Grid):
+    """grid for saves"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Saves')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+        self.root = getRoot(self)
+
+        #self.hparent.saveGrid = self #a 1.6018 d 1.9001
+
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        saves = handler.master_dom.getElementsByTagName('save')
+        self.CreateGrid(len(saves),7)
+        self.SetRowLabelSize(0)
+        col_names = ['Save','Key','base','Abil','Magic','Misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.saves = saves
+        i = 0
+        for i in range(len(saves)):
+            self.refresh_row(i)
+
+
+        #a 1.9002 remainder of code in this method.
+        climber = parent
+        nameNode = climber.GetClassName()
+        while nameNode != 'wxFrame':
+            climber = climber.parent
+            nameNode = climber.GetClassName()
+        masterFrame=climber
+        masterFrame.refresh_data=self.refresh_data
+        #print getmembers(masterFrame)
+
+        handler.saveGridFrame.append(masterFrame)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            if col == 2:
+                self.saves[row].setAttribute('base',value)
+            elif col ==4:
+                self.saves[row].setAttribute('magmod',value)
+            elif col ==5:                                       # 1.5001
+                self.saves[row].setAttribute('miscmod',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_row(self,rowi):
+        s = self.saves[rowi]
+
+        name = s.getAttribute('name')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        stat = s.getAttribute('stat')
+        self.SetCellValue(rowi,1,stat)
+        self.SetReadOnly(rowi,1)
+        self.SetCellValue(rowi,2,s.getAttribute('base'))
+        self.SetCellValue(rowi,3,str(self.root.abilities.get_mod(stat)))
+        self.SetReadOnly(rowi,3)
+        self.SetCellValue(rowi,4,s.getAttribute('magmod'))
+        self.SetCellValue(rowi,5,s.getAttribute('miscmod'))
+        mod = str(self.handler.get_mod(name))
+        self.SetCellValue(rowi,6,mod)
+        self.SetReadOnly(rowi,6)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()):
+            self.refresh_row(r)
+
+class dnd35skillsnfeats(dnd35_char_child):
+    """ Node handler for a dnd35 charactor
+        <nodehandler name='?'  module='dnd35' class='dnd35char_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.6009
+
+        node_handler.__init__(self,xml_dom,tree_node)
+        dnd35_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('skills','Skills',dnd35skill,'book')
+        self.new_child_handler('feats','Feats',dnd35feats,'book')
+        self.myeditor = None
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+class skills_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+class dnd35skill(skills_char_child):
+    """ Node Handler for skill.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        #a 1.5002 Need the functional parent, not the invoking parent.
+        self.root = getRoot(self) #a 1.5002
+        self.root.skills = self #a 1.6009
+
+        skills_char_child.__init__(self,xml_dom,tree_node,parent)
+        tree = self.tree
+        icons = self.tree.icons
+        node_list = self.master_dom.getElementsByTagName('skill')
+
+        self.skills={}
+        #Adding code to not display skills you can not use -mgt
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.skills[name] = n
+            skill_check = self.skills[name]
+            ranks = int(skill_check.getAttribute('rank'))
+            trained = int(skill_check.getAttribute('untrained'))
+
+            if ranks > 0 or trained == 1:
+                new_tree_node = tree.AppendItem(self.mytree_node,name,
+                                            icons['gear'],icons['gear'])
+            else:
+                continue
+
+            tree.SetPyData(new_tree_node,self)
+
+
+
+    def refresh_skills(self):
+        #Adding this so when you update the grid the tree will reflect
+        #The change. -mgt
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('skill')
+
+        self.skills={}
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.skills[name] = n
+            skill_check = self.skills[name]
+            ranks = int(skill_check.getAttribute('rank'))
+            trained = int(skill_check.getAttribute('untrained'))
+
+            if ranks > 0 or trained == 1:
+                new_tree_node = tree.AppendItem(self.mytree_node,name,
+                                            icons['gear'],icons['gear'])
+            else:
+                continue
+
+            tree.SetPyData(new_tree_node,self)
+
+    def get_mod(self,name):
+        skill = self.skills[name]
+        stat = skill.getAttribute('stat')
+        #stat_mod = int(dnd_globals["stats"][stat])                 #d 1.5002
+        stat_mod = self.root.abilities.get_mod(stat)                #a 1.5002
+        rank = int(skill.getAttribute('rank'))
+        misc = int(skill.getAttribute('misc'))
+        total = stat_mod + rank + misc
+        return total
+
+    def on_rclick(self,evt):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        #print "skill rc self",self                 #a 1.6004
+        #print "skill rc tree",self.mytree_node     #a 1.6004
+        #print "skill rc item",item                 #a 1.6004
+        if item == self.mytree_node:
+            return
+            # following line fails,
+            #dnd35_char_child.on_ldclick(self,evt) #d 1.6014
+            # it's what it used to try to do.
+        ac = self.root.ac.get_check_pen() #a 1.5002 for 1.5004 verify fix.
+
+        skill = self.skills[name]
+
+        untr = skill.getAttribute('untrained')                         #a 1.6004
+        rank = skill.getAttribute('rank')                              #a 1.6004
+        if eval('%s == 0' % (untr)):                                   #a 1.6004
+            if eval('%s == 0' % (rank)):                               #a 1.6004
+                res = 'You fumble around, accomplishing nothing'       #a 1.6004
+                txt = '%s Skill Check: %s' % (name, res)               #a 1.6004
+                chat = self.chat                                       #a 1.6004
+                chat.Post(txt,True,True)                               #a 1.6004
+                return                                                 #a 1.6004
+
+        armor = ''
+        acCp = ''
+        if ac < 0:  #acCp >= 1 #m 1.5004 this is stored as negatives.
+            armorCheck = int(skill.getAttribute('armorcheck'))
+            #print "ac,armorCheck",ac,armorCheck
+            if armorCheck == 1:
+                acCp=ac
+                armor = '(includes Armor Penalty of %s)' % (acCp)
+        if item == self.mytree_node:
+            dnd35_char_child.on_ldclick(self,evt)
+            #wnd = skill_grid(self.frame.note,self)
+            #wnd.title = "Skills"
+            #self.frame.add_panel(wnd)
+        else:
+            mod = self.get_mod(name)
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s Skill Check: [1d20%s%s%s] %s' % (
+                    name, mod1, mod, acCp, armor)
+            chat.ParsePost(txt,True,True)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,skill_grid,"Skills")
+        wnd.title = "Skills (edit)"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 >
+                    <th width='30%'>Skill</th><th>Key</th>
+                    <th>Rank</th><th>Abil</th><th>Misc</th><th>Total</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('skill')
+
+        for n in node_list:
+            name = n.getAttribute('name')
+            stat = n.getAttribute('stat')
+            rank = n.getAttribute('rank')
+            untr = n.getAttribute('untrained')                              #a 1.6004
+            #Filter unsuable skills out of pretty print -mgt
+            if eval('%s > 0' % (rank)) or eval('%s == 1' % (untr)):
+                if eval('%s >=1' % (rank)):
+                    html_str += "<tr ALIGN='center' bgcolor='#CCCCFF'><td>"     #a 1.6004
+                    #html_str += "<tr ALIGN='center' bgcolor='green'><td>"      #d 1.6004
+                    html_str += name+"</td><td>"+stat+"</td><td>"+rank+"</td>"
+                elif eval('%s == 1' % (untr)):                                  #a 1.6004
+                    html_str += "<tr ALIGN='center' bgcolor='#C0FF40'><td>"     #a 1.6004
+                    html_str += name+"</td><td>"+stat+"</td><td>"+rank+"</td>"  #a 1.6004
+                else:
+                    html_str += "<tr ALIGN='center'><td>"+name+"</td><td>"
+                    html_str += stat+"</td><td>"+rank+"</td>"
+            else:
+                continue
+            stat_mod = self.root.abilities.get_mod(stat)        #a 1.5002
+            #stat_mod = str(dnd_globals["stats"][stat])         #d 1.5002
+            misc = n.getAttribute('misc')
+            mod = str(self.get_mod(name))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str += "<td>"+str(stat_mod)+"</td><td>"+misc #m 1.6009 str()
+            html_str += '</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+
+class skill_grid(wx.grid.Grid):
+    """ panel for skills """
+    def __init__(self, parent, handler):
+        self.hparent = handler    #a 1.5002 need function parent, not invoker
+        self.root = getRoot(self) #a 1.5002
+        pname = handler.master_dom.setAttribute("name", 'Skills')
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        skills = handler.master_dom.getElementsByTagName('skill')
+        #xelf.stats = dnd_globals["stats"]                           #d 1.5002
+
+        self.CreateGrid(len(skills),6)
+        self.SetRowLabelSize(0)
+        col_names = ['Skill','Key','Rank','Abil','Misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        rowi = 0
+        self.skills = skills
+        for i in range(len(skills)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        #print value
+        try:
+            int(value)
+            if col == 2:
+                self.skills[row].setAttribute('rank',value)
+            elif col ==4:
+                self.skills[row].setAttribute('misc',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+
+                #call refresh_skills
+        self.handler.refresh_skills()
+
+    def refresh_row(self,rowi):
+        s = self.skills[rowi]
+        name = s.getAttribute('name')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        stat = s.getAttribute('stat')
+        self.SetCellValue(rowi,1,stat)
+        self.SetReadOnly(rowi,1)
+        self.SetCellValue(rowi,2,s.getAttribute('rank'))
+        #self.SetCellValue(rowi,3,str(dnd_globals["stats"][stat]))  #d 1.5002
+        if self.root.abilities: #a 1.5002 sanity check.
+            stat_mod=self.root.abilities.get_mod(stat)           #a 1.5002
+        else: #a 1.5002
+            stat_mod = -6 #a 1.5002 this can happen if code is changed so
+            #a 1.5002 that abilities are not defined prior invokation of init.
+            print "Please advise dnd35 maintainer alert 1.5002 raised"
+
+        self.SetCellValue(rowi,3,str(stat_mod))         #a 1.5002
+        self.SetReadOnly(rowi,3)
+        self.SetCellValue(rowi,4,s.getAttribute('misc'))
+        mod = str(self.handler.get_mod(name))
+        self.SetCellValue(rowi,5,mod)
+        self.SetReadOnly(rowi,5)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+
+        for r in range(self.GetNumberRows()):
+            self.refresh_row(r)
+
+
+
+
+class dnd35feats(skills_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        skills_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.feats = self #a 1.6009
+
+
+    def get_design_panel(self,parent):
+        setTitle="Feats - " + self.root.general.charName    #a 1.5010
+        wnd = outline_panel(parent,self,feat_panel,setTitle) #a 1.5010
+        #wnd = outline_panel(parent,self,feat_panel,"Feats") #d 1.5010
+        wnd.title = "Feats" #d 1.5010
+        #wnd.title = "Feats - " + self.charName
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Feats</th></tr><tr><td>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+class feat_panel(wx.Panel):
+    def __init__(self, parent, handler):
+
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+        self.root = getRoot(self) #a 1.5002
+        #tempTitle= 'Feats - ' + self.root.general.charName #a 1.5010
+        #pname = handler.master_dom.setAttribute("name", tempTitle) #a 1.5010
+        pname = handler.master_dom.setAttribute("name", 'Feats') #d 1.5010
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid = wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Feat"), 0, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Feat"), 0, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+        self.sizer = sizer
+
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),3,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"Feat")
+        self.grid.SetColLabelValue(1,"Reference")
+        self.grid.SetColLabelValue(2,"Description") #m 1.6 typo correction.
+        wrap = wx.grid.GridCellAutoWrapStringRenderer()
+        attr = wx.grid.GridCellAttr()
+        attr.SetRenderer(wrap)
+        self.grid.SetColAttr(2, attr)
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def refresh_row(self,i):
+        feat = self.n_list[i]
+
+        name = feat.getAttribute('name')
+        type = feat.getAttribute('type')
+        desc = feat.getAttribute('desc') #m 1.6 correct typo
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,type)
+        self.grid.SetReadOnly(i,1)
+        self.grid.SetCellValue(i,2,desc) #m 1.6 correct typo
+        self.grid.SetReadOnly(i,2)
+        self.grid.AutoSizeColumn(0)
+        self.grid.AutoSizeColumn(1)
+        self.grid.AutoSizeColumn(2, False)
+        self.grid.AutoSizeRow(i)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd35"]+"dnd35feats.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('feat')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name') + "  -  [" +
+                     f.getAttribute('type') + "]  -  " + f.getAttribute('desc'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Feat','Feats',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+
+class dnd35combat(dnd35_char_child):
+    """ Node handler for a dnd35 charactor
+        <nodehandler name='?'  module='dnd35' class='dnd35char_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+
+        node_handler.__init__(self,xml_dom,tree_node)
+
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5012
+
+
+
+        #mark3
+        dnd35_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('hp','Hit Points',dnd35hp,'gear')
+        self.new_child_handler('attacks','Attacks',dnd35attacks,'spears')
+        self.new_child_handler('ac','Armor',dnd35armor,'spears')
+        #print "combat",self.child_handlers #a (debug) 1.5002
+        #wxMenuItem(self.tree.std_menu, dnd35_EXPORT, "Export...", "Export")
+        self.myeditor = None
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+
+class combat_char_child(node_handler):
+    """ Node Handler for combat.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+class dnd35hp(combat_char_child):
+    """ Node Handler for hit points.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        combat_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.6009
+        self.root.hp = self #a 1.6009
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,hp_panel,"Hit Points")
+        wnd.title = "Hit Points"
+        return wnd
+
+    def on_rclick( self, evt ):
+        chp = self.master_dom.getAttribute('current')
+        mhp = self.master_dom.getAttribute('max')
+        txt = '((HP: %s / %s))' % ( chp, mhp )
+        self.chat.ParsePost( txt, True, True )
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 >"
+        html_str += "<tr BGCOLOR=#E9E9E9 ><th colspan=4>Hit Points</th></tr>"
+        html_str += "<tr><th>Max:</th>"
+        html_str += "<td>"+self.master_dom.getAttribute('max')+"</td>"
+        html_str += "<th>Current:</th>"
+        html_str += "<td>"+self.master_dom.getAttribute('current')+"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+class hp_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.hparent = handler #a 1.5002 allow ability to run up tree.  In this
+        #a 1.5002 case, we need the functional parent, not the invoking parent.
+
+        pname = handler.master_dom.setAttribute("name", 'HitPoints')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+        self.sizer.AddMany([ (wx.StaticText(self, -1, "HP Current:"),   0,
+           wx.ALIGN_CENTER_VERTICAL),
+          (wx.TextCtrl(self, HP_CUR,
+           self.master_dom.getAttribute('current')),   0, wx.EXPAND),
+          (wx.StaticText(self, -1, "HP Max:"), 0, wx.ALIGN_CENTER_VERTICAL),
+          (wx.TextCtrl(self, HP_MAX, self.master_dom.getAttribute('max')),
+           0, wx.EXPAND),
+         ])
+        self.sizer.AddGrowableCol(1)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=HP_MAX)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=HP_CUR)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == HP_CUR:
+            self.master_dom.setAttribute('current',evt.GetString())
+        elif id == HP_MAX:
+            self.master_dom.setAttribute('max',evt.GetString())
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+class dnd35attacks(combat_char_child):
+    """ Node Handler for attacks.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        combat_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.attacks = self #a 1.6009 so others can find me.
+        self.mrFrame = [] #a 1.9001
+
+        #a 1.5012 start a1b
+
+        self.updateFootNotes = False
+        self.updateFootNotes = False
+        self.html_str = "<html><body>"
+        self.html_str += ("<br />  This character has weapons with no "+
+             "footnotes.  This program will "+
+             "add footnotes to the weapons which have names that still match "+
+             "the orginal names.  If you have changed the weapon name, you "+
+             "will see some weapons with a footnote of 'X', you will have "+
+             "to either delete and re-add the weapon, or research "+
+             "and add the correct footnotes for the weapon.\n"+
+             "<br />  Please be aware, that only the bow/sling footnote is "+
+             "being used to affect changes to rolls; implemenation of other "+
+             "footnotes to automaticly adjust rolls will be completed as "+
+             "soon as time allows." +
+             "<br /><br />Update to character:"+self.root.general.charName+
+             "<br /><br />"+
+             """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 >
+              <th width='80%'>Weapon Name</th><th>Added Footnote</th></tr>\n""")
+        self.temp_dom={}
+        #a 1.5012 end a1b
+
+        node_list = self.master_dom.getElementsByTagName('melee')
+        self.melee = node_list[0]
+        node_list = self.master_dom.getElementsByTagName('ranged')
+        self.ranged = node_list[0]
+        self.refresh_weapons() # this causes self.weapons to be loaded.
+
+        #a 1.5012 this whole if clause.
+        if self.updateFootNotes == True:
+            self.updateFootNotes = False
+            name = self.root.general.charName
+            self.html_str +=  "</table>"
+            self.html_str +=  "</body> </html> "
+            masterFrame = self.root.frame
+
+            title = name+"'s weapons' update to have footnotes"
+            fnFrame = wx.Frame(masterFrame, -1, title)
+            fnFrame.panel = wx.html.HtmlWindow(fnFrame,-1)
+            fnFrame.panel.SetPage(self.html_str)
+            fnFrame.Show()
+
+        #weaponsH = self.master_dom.getElementsByTagName('attacks')
+        #mark7
+
+    #a 1.9001 this whole method
+    def refreshMRdata(self): # refresh the data in the melee/ranged section
+        # of the attack chart.
+        # count backwards, maintains context despite "removes"
+        for i in range(len(self.mrFrame)-1,-1,-1):   #a 1.9001
+            x = self.mrFrame[i]
+            if x == None:
+                x.refreshMRdata() #a 1.9001
+            else:
+                self.mrFrame.remove(x)
+
+    def refresh_weapons(self):
+        self.weapons = {}
+
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('weapon')
+        for n in node_list:
+            name = n.getAttribute('name')
+            fn = safeGetAttr(n,'fn') #a 1.5012 can be removed when
+            #a 1.5012 confident all characters in the world have footnotes.
+            #if self.updateFootNotes:
+            if fn == None:#a 1.5012
+                self.updateFootNotes=True
+                self.updateFootN(n) #a 1.5012
+            new_tree_node = tree.AppendItem(
+                self.mytree_node,name,icons['sword'],icons['sword'])
+            tree.SetPyData(new_tree_node,self)
+            self.weapons[name]=n
+
+    def updateFootN(self,n):#a 1.5012 this whole function
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd35"]+"dnd35weapons.xml","r")
+            #tmp = open("c:\clh\codeSamples\sample1.xml","r") #a (debug) 1.5012
+            self.temp_dom = xml.dom.minidom.parse(tmp)
+
+            #self.temp_dom = parseXml_with_dlg(self,tmp.read())
+            self.temp_dom = self.temp_dom._get_firstChild()
+            tmp.close()
+        nameF = n.getAttribute('name')
+        w_list = self.temp_dom.getElementsByTagName('weapon')
+        found = False
+        for w in w_list:
+            if nameF == w.getAttribute('name'):
+                found = True
+                fnN = safeGetAttr(n,'fn')
+                if fnN == None or fnN == 'None':
+                    fnW = w.getAttribute('fn')
+                    #print "weapon",nameF,"footnotes are updated to",fnW
+                    self.html_str += ("<tr ALIGN='center'><td>"+nameF+"</td>"+
+                                     "<td>"+fnW+"</td></tr>\n")
+                    n.setAttribute('fn',fnW)
+                break
+        if not found:
+            self.html_str += ("<tr ALIGN='center'><td>"+nameF+" - Custom "+
+              "Weapon, research "+
+              "and update manually; setting footnote to indicate custom</td>"+
+                                     "<td>"+'X'+"</td></tr>\n")
+            n.setAttribute('fn','X')
+
+
+    def get_mod(self,type='m'):
+        (base, base2, base3, base4, base5, base6, stat_mod, misc) \
+            = self.get_attack_data(type)
+        return int(base + misc + int(stat_mod))
+
+    def get_attack_data(self,type='m'):
+        if type=='m' or type=='0':
+            stat = 'Str'  #m was dnd_globals["stats"]['Str'] 1.5002
+            temp = self.melee
+        else:
+            stat = 'Dex'  #m was dnd_globals["stats"]['Dex'] 1.5002
+            temp = self.ranged
+        stat_mod = -7
+        stat_mod = self.root.abilities.get_mod(stat)    #a 1.5002
+        #print "Big test - stat_mod",stat_mod           #a (debug) 1.6000
+        base = int(temp.getAttribute('base'))
+        base2 = int(temp.getAttribute('second'))
+        base3 = int(temp.getAttribute('third'))
+        base4 = int(temp.getAttribute('forth'))
+        base5 = int(temp.getAttribute('fifth'))
+        base6 = int(temp.getAttribute('sixth'))
+        misc = int(temp.getAttribute('misc'))
+        return (base, base2, base3, base4, base5, base6, stat_mod ,misc)
+
+    def on_rclick(self,evt):
+        item = self.tree.GetSelection()
+
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            #print "bail due to FUD"
+            return #a 1.6015
+            #dnd35_char_child.on_ldclick(self,evt)#d 1.6015
+            #self.frame.add_panel(self.get_design_panel(self.frame.note))
+        else:
+            #print "entering attack phase"
+            mod = int(self.weapons[name].getAttribute('mod'))
+            wepMod = mod #a 1.5008
+            footNotes = safeGetAttr(self.weapons[name],'fn','')
+            cat = self.weapons[name].getAttribute('category') #a1.6001
+            result = split(cat,"-",2) #a 1.6001
+            if len(result) < 2: #a 1.6021 this if & else
+                print "warning: 1.6002 unable to interpret weapon category"
+                print "format 'type weapon-[Range|Melee]', probably missing"
+                print "the hyphen.  Assuming Melee"
+                print "weapon name: ",name
+                tres="Melee"
+            else:
+                tres=result[1]
+            #print "print FootNotes,tres",footNotes,tres
+            if tres == 'Melee': #a 1.6001   #m 1.6022 use of tres here and...
+            #if self.weapons[name].getAttribute('range') == '0':#d 1.6001
+                rangeOrMelee = 'm' #a 1.5008 code demote for next comment block
+            elif tres == 'Ranged': #m 1.6001 (was just else) #m 1.6022 here
+                rangeOrMelee = 'r' #a 1.5008
+            else:#a 1.6001 add this whole else clause.
+                print "warning: 1.6001 unable to interpret weapon category"
+                print "treating weapon as Melee, please correct xml"
+                print "weapon name:",name
+                rangeOrMelee ='m'
+            mod = mod + self.get_mod(rangeOrMelee) #a 1.5008
+            chat = self.chat
+            dmg = self.weapons[name].getAttribute('damage')
+
+            #a 1.6003 start code fix instance a
+            result = split(dmg,"/",2)
+            dmg = result[0]
+            #print "1.6003 check:dmg",dmg,";result",result
+            #o currently, only picking out dmg; rest are simply ignored.
+            #o May be usefull
+            #o later for two weapon attack correction.
+            #a 1.6003 end code fix instance a
+
+            monkLvl = self.root.classes.get_class_lvl('Monk') # a 1.5002
+            #print "monkLvl",monkLvl #a (debug) 1.5002
+            # monkLvl = dnd_globals["class"]["lvl"] #d 1.5002
+            if find(dmg, "Monk Med") > -1:
+                if monkLvl == None:     #a 1.5009
+                    txt = 'Attempting to use monk attack, but has no monk '
+                    txt += 'levels, please choose a different attack.'
+                    chat.ParsePost( txt, True, True ) #a 1.5009
+                    return #a 1.5009
+                else:   #a 1.5009
+                    lvl=int(monkLvl)
+                    if lvl <= 3:     #m 1.6022 reversed the order of checks.
+                        dmg = dmg.replace("Monk Med", "1d6")
+                    elif lvl <= 7:
+                        dmg = dmg.replace("Monk Med", "1d8")
+                    elif lvl <= 11:
+                        dmg = dmg.replace("Monk Med", "1d10")
+                    elif lvl <= 15:
+                        dmg = dmg.replace("Monk Med", "2d6")
+                    elif lvl <= 19:
+                        dmg = dmg.replace("Monk Med", "2d8")
+                    elif lvl <= 20:
+                        dmg = dmg.replace("Monk Med", "2d10")
+            if find(dmg, "Monk Small") > -1:
+                if monkLvl == None:     #a 1.5009
+                    txt = 'Attempting to use monk attack, but has no monk '
+                    txt += 'levels, please choose a different attack.'
+                    chat.ParsePost( txt, True, True ) #a 1.5009
+                    return #a 1.5009
+                else:   #a 1.5009
+                    lvl=int(monkLvl)
+                    if lvl <= 3:     #m 1.6022 reversed the order of the checks
+                        dmg = dmg.replace("Monk Small", "1d4")
+                    elif lvl <= 7:
+                        dmg = dmg.replace("Monk Small", "1d6")
+                    elif lvl <= 11:
+                        dmg = dmg.replace("Monk Small", "1d8")
+                    elif lvl <= 15:
+                        dmg = dmg.replace("Monk Small", "1d10")
+                    elif lvl <= 19:
+                        dmg = dmg.replace("Monk Small", "2d6")
+                    elif lvl <= 20:
+                        dmg = dmg.replace("Monk Small", "2d8")
+            if find(dmg, "Monk Large") > -1:
+                if monkLvl == None:     #a 1.5009
+                    txt = 'Attempting to use monk attack, but has no monk '
+                    txt += 'levels, please choose a different attack.'
+                    chat.ParsePost( txt, True, True ) #a 1.5009
+                    return #a 1.5009
+                else:   #a 1.5009
+                    lvl=int(monkLvl)
+                    if lvl <= 3:     #m 1.6022 reversed the order of the checks
+                        dmg = dmg.replace("Monk Large", "1d8")
+                    elif lvl <= 7:
+                        dmg = dmg.replace("Monk Large", "2d6")
+                    elif lvl <= 11:
+                        dmg = dmg.replace("Monk Large", "2d8")
+                    elif lvl <= 15:
+                        dmg = dmg.replace("Monk Large", "3d6")
+                    elif lvl <= 19:
+                        dmg = dmg.replace("Monk Large", "3d8")
+                    elif lvl <= 20:
+                        dmg = dmg.replace("Monk Large", "4d8")
+            flurry = False
+            #print "adjusted weapon damage is:",dmg
+            #o 1.5007 str bow
+            #o 1.5011 start looking about here str dam bonus missed for thrown?
+            #o 1.5012 start looking about here str penalty missed for bow/sling?
+            #o 1.5013 off-hand attacks.? dam and all affects?
+            str_mod = self.root.abilities.get_mod('Str') #a 1.5007,11,12,13
+            if rangeOrMelee == 'r':                     #a 1.5008
+                #if off_hand == True then stat_mod = stat_mod/2 #o 1.5013
+                #c 1.5007 ranged weapons normally get no str mod
+                if find(footNotes,'b') > -1:#a 1.5012 if it's a bow
+                    if str_mod >= 0:        #a 1.5012 never a str bonus
+                        str_mod = 0         #a 1.5012 penalty,
+                else:                       #a 1.5012 if appropriate
+                    str_mod = 0
+                #  c 1.5007 (must adjust for str bows later and thown weapons)
+                #o 1.5007 include + for str bows
+                #o 1.5012 include any str penalty for bows/slings.
+            mod2 = ""                                   #a 1.5007,11-13
+            if str_mod >= 0: #1.6 tidy up code.
+                mod2 = "+"   #1.6 tidy up code.
+            aStrengthMod = mod2 + str(str_mod) #a 1.5008 applicable strength mod
+
+            #if name == "Flurry of Blows(Monk Med)": #d 1.6012
+            if find(name ,"Flurry of Blows") > -1: #a 1.6012
+                flurry = True
+
+            (base, base2, base3, base4, base5, base6, stat_mod, misc) = self.get_attack_data(rangeOrMelee)  #a 1.5008
+            name = name.replace('(Monk Med)', '')
+            name = name.replace('(Monk Small)', '')
+            if not flurry:
+                if name == 'Shuriken':
+                    for n in xrange(3):
+                        self.sendRoll(base, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod, rollAnyWay=True)
+                        self.sendRoll(base2, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                        self.sendRoll(base3, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                        self.sendRoll(base4, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                        self.sendRoll(base5, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                        self.sendRoll(base6, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                else:
+                    self.sendRoll(base, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod, rollAnyWay=True)
+                    self.sendRoll(base2, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                    self.sendRoll(base3, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                    self.sendRoll(base4, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                    self.sendRoll(base5, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+                    self.sendRoll(base6, stat_mod, misc, wepMod, name, '', dmg, aStrengthMod)
+            else:
+                if monkLvl == None:
+                    txt = 'Attempting to use monk attack, but has no monk '
+                    txt += 'levels, please choose a different attack.'
+                    chat.ParsePost( txt, True, True ) #a 1.5009
+                    return
+                else:
+                    lvl = int(monkLvl)
+                    if lvl <= 4:
+                        flu = '-2'
+                        atks = False
+                    elif lvl <= 8:
+                        flu = '-1'
+                        atks = False
+                    elif lvl <= 10:
+                        flu = ''
+                        atks = False
+                    elif lvl <= 20:
+                        flu = ''
+                        atks = True
+
+                    self.sendRoll(base, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod, rollAnyWay=True)
+                    self.sendRoll(base, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod, rollAnyWay=True)
+                    if atks:
+                        self.sendRoll(base, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod, rollAnyWay=True)
+                    self.sendRoll(base2, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod)
+                    self.sendRoll(base3, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod)
+                    self.sendRoll(base4, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod)
+                    self.sendRoll(base5, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod)
+                    self.sendRoll(base6, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod)
+
+
+
+    def sendRoll(self, base, stat_mod, misc, wepMod, name, flu, dmg, aStrengthMod, rollAnyWay=False):
+        if base != 0 or rollAnyWay:
+            base = base + int(stat_mod) + misc + wepMod #m 1.5008
+            if base >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            txt = ' %s Attack Roll: <b>[1d20%s%s%s]</b>' % (name, mod1, base, flu)
+            txt += ' ===> Damage: <b>[%s%s]</b>' % (dmg, aStrengthMod)
+            self.chat.ParsePost( txt, True, True )
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,attack_panel,"Attacks")
+        wnd.title = "Attacks"
+        return wnd
+
+    def tohtml(self):
+        melee = self.get_attack_data('m')
+        ranged = self.get_attack_data('r')
+        html_str = ("""<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >"""+
+          "<th>Attack</th><th>Total</th><th >Base</th>"+
+          "<th>Abil</th><th>Misc</th></tr>")
+        html_str += "<tr ALIGN='center' ><th >Melee:</th>"
+        html_str += "<td>"+str(melee[0]+melee[1]+melee[2])+"</td>"
+        html_str += "<td>"+str(melee[0])+"</td>"
+        html_str += "<td>"+str(melee[1])+"</td>"
+        html_str += "<td>"+str(melee[2])+"</td></tr>"
+
+        html_str += "<tr ALIGN='center' ><th >Ranged:</th>"
+        html_str += "<td>"+str(ranged[0]+ranged[1]+ranged[2])+"</td>"
+        html_str += "<td>"+str(ranged[0])+"</td>"
+        html_str += "<td>"+str(ranged[1])+"</td>"
+        html_str += "<td>"+str(ranged[2])+"</td></tr></table>"
+
+        n_list = self.master_dom.getElementsByTagName('weapon')
+        for n in n_list:
+            mod = n.getAttribute('mod')
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            ran = n.getAttribute('range')
+            total = str(int(mod) + self.get_mod(ran))
+            html_str += """<P><table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >
+                    <th colspan=2>Weapon</th>
+                    <th>Attack</th><th >Damage</th><th>Critical</th></tr>"""
+            html_str += "<tr ALIGN='center' ><td  colspan=2>"
+            html_str += n.getAttribute('name')+"</td><td>"+total+"</td>"
+            html_str += "<td>"+n.getAttribute('damage')+"</td><td>"
+            html_str += n.getAttribute('critical')+"</td></tr>"
+            html_str += """<tr BGCOLOR=#E9E9E9 ><th>Range</th><th>Weight</th>
+                        <th>Type</th><th>Size</th><th>Misc Mod</th></tr>"""
+            html_str += "<tr ALIGN='center'><td>"+ran+"</td><td>"
+            html_str += n.getAttribute('weight')+"</td>"
+            html_str += "<td>"+n.getAttribute('type')+"</td><td>"
+            html_str += n.getAttribute('size')+"</td>"
+            html_str += '<td>%s%s</td></tr>'  % (mod1, mod)
+            #a 1.5012 add next two lines to pretty print footnotes.
+            html_str += """<tr><th BGCOLOR=#E9E9E9 colspan=2>Footnotes:</th>"""
+            html_str += "<th colspan=3>"+safeGetAttr(n,'fn','')+"</th></tr>"
+            html_str += '</table>'
+        return html_str
+
+class attack_grid(wx.grid.Grid):
+    """grid for attacks"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Melee')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 we need the functional parent, not the invoking parent.
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+
+        self.root = getRoot(self) #a 1.9001
+        self.parent = parent
+        self.handler = handler
+        self.rows = (self.handler.melee,self.handler.ranged)
+        self.CreateGrid(2,10)
+        self.SetRowLabelSize(0)
+        col_names = ['Type','base','base 2','base 3','base 4','base 5',
+            'base 6','abil','misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.SetCellValue(0,0,"Melee")
+        self.SetCellValue(1,0,"Ranged")
+        self.refresh_data()
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        #print "looking for containing frame"
+
+        #a 1.9001 remainder of code in this method.
+        climber = parent
+        nameNode = climber.GetClassName()
+        while nameNode != 'wxFrame':
+            climber = climber.parent
+            nameNode = climber.GetClassName()
+        masterFrame=climber
+        masterFrame.refreshMRdata=self.refresh_data
+
+        handler.mrFrame.append(masterFrame)
+
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            if col==1:
+                self.rows[row].setAttribute('base',value)
+            elif col==2:
+                self.rows[row].setAttribute('second',value)
+            elif col==3:
+                self.rows[row].setAttribute('third',value)
+            elif col==4:
+                self.rows[row].setAttribute('forth',value)
+            elif col==5:
+                self.rows[row].setAttribute('fifth',value)
+            elif col==6:
+                self.rows[row].setAttribute('sixth',value)
+            elif col==8:
+                self.rows[row].setAttribute('misc',value)
+            self.parent.refresh_data()
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_data(self):
+
+        melee = self.handler.get_attack_data('m')
+        ranged = self.handler.get_attack_data('r')
+        tmelee = int(melee[0]) + int(melee[6]) + int(melee[7])
+        tranged = int(ranged[0]) + int(ranged[6]) + int(ranged[7])
+        for i in range(0,8):    #a 1.5005
+            self.SetCellValue(0,i+1,str(melee[i]))
+            self.SetCellValue(1,i+1,str(ranged[i]))
+        self.SetCellValue(0,9,str(tmelee))
+        self.SetCellValue(1,9,str(tranged))
+        self.SetReadOnly(0,0)
+        self.SetReadOnly(1,0)
+        self.SetReadOnly(0,7)
+        self.SetReadOnly(1,7)
+        self.SetReadOnly(0,9)
+        self.SetReadOnly(1,9)
+
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+1)
+        self.SetColSize(0,col_w*2)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+class weapon_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        self.hparent = handler                          #a 1.5012
+        self.root = getRoot(self)
+
+        pname = handler.master_dom.setAttribute("name", 'Weapons')
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer2.Add(wx.Button(self, 10, "Remove Weapon"), 0, wx.EXPAND)
+        sizer2.Add(wx.Size(10,10))
+        sizer2.Add(wx.Button(self, 20, "Add Weapon"), 0, wx.EXPAND)
+
+        sizer.Add(sizer2, 0, wx.EXPAND)
+        sizer.Add(wx.StaticText(self, -1, "Right click a weapon's footnote to see what the footnotes mean."),0, wx.EXPAND)#a 1.5012
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.sizer2 = sizer2
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.on_gridRclick)#a 1.5012
+
+        n_list = handler.master_dom.getElementsByTagName('weapon')
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.handler = handler
+        #trash=input("weapon panel init colnames")
+        self.colAttr = ['name','damage','mod','critical','type','weight',
+                    'range','size','Total','fn',    'comment'] #a 1.5012
+        col_names = ['Name','Damage','To hit\nmod','Critical','Type','Weight',
+                    'Range','Size','Total','Foot\nnotes','Comment'] #a 1.5012
+        gridColCount=len(col_names)#a 1.5012
+        self.grid.CreateGrid(len(n_list),gridColCount,1) #a 1.5012
+        #self.grid.CreateGrid(len(n_list),9,1) #d 1.5012
+        self.grid.SetRowLabelSize(0)
+
+        for i in range(gridColCount): #a 1.5012
+            self.grid.SetColLabelValue(i,col_names[i])
+        self.refresh_data()
+        self.temp_dom = None
+
+
+    #mark4
+    #a 1.5012 add entire method.
+    def on_gridRclick(self,evt):
+        #print "weapon_panel, on_rclick: self,evt",self,evt
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        #print "wp, on rclick,grid row,col,value",row,col,value
+        if col == 9 and value != 'None':
+            n = self.n_list[row]
+            name = n.getAttribute('name')
+            #print "we want a panel!"
+            handler = self.hparent
+            #print "handler:",handler
+            # A handler is a node, and nodes have a reference to
+            # the master frame
+            masterFrame = handler.frame
+            #print "masterFrame:",masterFrame
+            title = name+"'s Special Weapon Characteristics"
+            fnFrame = wx.Frame(masterFrame, -1, title)
+            fnFrame.panel = wx.html.HtmlWindow(fnFrame,-1)
+            if not self.temp_dom:
+                tmp = open(orpg.dirpath.dir_struct["dnd35"]+
+                            "dnd35weapons.xml","r")
+                #tmp = open("c:\clh\codeSamples\sample1.xml","r")
+                xml_dom = parseXml_with_dlg(self,tmp.read())
+                xml_dom = xml_dom._get_firstChild()
+                tmp.close()
+                self.temp_dom = xml_dom
+            f_list = self.temp_dom.getElementsByTagName('f') # the footnotes
+            #print "weapon_panel - on_rclick f_list",f_list#a 1.6
+            n = self.n_list[row]
+            name = n.getAttribute('name')
+            footnotes = n.getAttribute('fn')
+            html_str = "<html><body>"
+            html_str += """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 >
+                        <th width='10%'>Note</th><th>Description</th></tr>\n"""
+            #print "rclick,name,fn",name,footnotes
+            if footnotes == "":
+                html_str += "<tr ALIGN='center'><td></td>"
+                html_str += "  <td>This weapon has no footnotes</td></tr>"
+            for i in range(len(footnotes)):
+                aNote=footnotes[i]
+                found=False
+                for f in f_list:
+                    if f.getAttribute('mark') == aNote:
+                        found=True
+                        text=f.getAttribute('txt')
+                        html_str += ("<tr ALIGN='center'><td>"+aNote+"</td>"+
+                                     "<td>"+text+"</td></tr>\n")
+                if not found:
+                    html_str += ("<tr ALIGN='center'><td>"+aNote+"</td>"+
+                       "<td>is not a recognized footnote</td></tr>\n")
+
+            html_str +=  "</table>"
+            html_str +=  "</body> </html> "
+
+            #print html_str
+            fnFrame.panel.SetPage(html_str)
+            fnFrame.Show()
+            return
+        pass
+
+
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col == 2 and not int(value): # special case for mod, demoted
+            value = "0" #a 5.012 demoted
+            self.n_list[row].setAttribute('mod',value) # a 5.012 demoted
+        if not (col == 9 and value == "None" and
+                self.n_list[row].getAttribute('fn') == "None"
+                ): #a 5.012 special case for footnotes
+            self.n_list[row].setAttribute(self.colAttr[col],value)#a 5.012
+
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+        fn = n.getAttribute('fn')
+        #print "fn=",fn
+        name = n.getAttribute('name')
+        mod = n.getAttribute('mod')
+        ran = n.getAttribute('range')
+        total = str(int(mod) + self.handler.get_mod(ran))
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetCellValue(i,1,n.getAttribute('damage'))
+        self.grid.SetCellValue(i,2,mod)
+        self.grid.SetCellValue(i,3,n.getAttribute('critical'))
+        self.grid.SetCellValue(i,4,n.getAttribute('type'))
+        self.grid.SetCellValue(i,5,n.getAttribute('weight'))
+        self.grid.SetCellValue(i,6,ran)
+        self.grid.SetCellValue(i,7,n.getAttribute('size') )
+        self.grid.SetCellValue(i,8,total)
+        self.grid.SetCellValue(i,9,safeGetAttr(n,'fn','None')) #a 1.5012
+        self.grid.SetCellValue(i,10,safeGetAttr(n,'comment','')) #a 1.5012
+        #fn=safeGetAttr(n,'fn','None') #a (debug) 1.5012
+        #print "fn ",fn,"<" #a (debug) 1.5012
+        #o 1.5012 original damage vs what someone has changed it to.
+
+        self.grid.SetReadOnly(i,8)
+
+    def on_remove(self,evt): #o 1.6011 correcting wrongful deletion
+        rows = self.grid.GetNumberRows()
+        #for i in range(rows):          #d 1.6011 do it backwards,
+        for i in range(rows-1,-1,-1):   #a 1.6011 or you lose context
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.n_list = self.master_dom.getElementsByTagName('weapon')
+                self.handler.refresh_weapons()
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd35"]+"dnd35weapons.xml","r")
+            #tmp = open("c:\clh\codeSamples\sample1.xml","r") #a (debug) 1.5012
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('weapon')
+        opts = []
+        #print "weapon_panel - on_add f_list",f_list#a 1.6
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Weapon','Weapon List',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            #print f_list[i] # DOM Element: weapon.
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            #print self.grid.AppendRows # a bound method of wxGrid
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('weapon')
+            #print "self.n_list",self.n_list # list of DOM weapons
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_weapons()
+        dlg.Destroy()
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-40)
+        self.sizer.SetDimension(0,s[1]-40,s[0],25)
+        self.sizer2.SetDimension(0,s[1]-15,s[0],15)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols+1)
+        self.grid.SetColSize(0,col_w*2)
+        for i in range(1,cols):
+            self.grid.SetColSize(i,col_w)
+
+    def refresh_data(self):
+
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+
+class attack_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Melee')
+        self.parent = parent #a 1.9001
+
+        wx.Panel.__init__(self, parent, -1)
+
+        self.a_grid = attack_grid(self, handler)
+        self.w_panel = weapon_panel(self, handler)
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.sizer.Add(self.a_grid, 1, wx.EXPAND)
+        self.sizer.Add(self.w_panel, 2, wx.EXPAND)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+    def refresh_data(self):
+
+        self.w_panel.refresh_data()
+        self.a_grid.refresh_data()
+
+
+class dnd35armor(combat_char_child):
+    """ Node Handler for ac.  This handler will be
+        created by dnd35char_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        combat_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.ac = self #a 1.6009
+
+    def get_spell_failure(self):
+        return self.get_total('spellfailure')
+
+    def get_total_weight(self):
+        return self.get_total('weight')
+
+    def get_check_pen(self):
+        return self.get_total('checkpenalty')
+
+    def get_armor_class(self):
+        ac_total = 10
+
+        ac_total += self.get_total('bonus')
+        #m 1.5009 change to hardcode dex, was incorrect gv "stat"
+        dex_mod = self.root.abilities.get_mod('Dex')#m 1.5009 hardcode dex
+        max_dex = self.get_max_dex()
+        if dex_mod < max_dex:
+            ac_total += dex_mod
+        else:
+            ac_total += max_dex
+        return ac_total
+
+    def get_max_dex(self):
+        armor_list = self.master_dom.getElementsByTagName('armor')
+        dex = 10
+        for a in armor_list:
+            temp = int(a.getAttribute("maxdex"))
+            if temp < dex:
+                dex = temp
+        return dex
+
+    def get_total(self,attr):
+        armor_list = self.master_dom.getElementsByTagName('armor')
+        total = 0
+        for a in armor_list:
+            total += int(a.getAttribute(attr))
+        return total
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,ac_panel,"Armor")
+        wnd.title = "Armor"
+        return wnd
+
+    def on_rclick( self, evt ):
+        ac = self.get_armor_class()
+        fac = (int(ac)-(self.root.abilities.get_mod('Dex')))
+
+        txt = '((AC: %s Normal, %s Flatfoot))' % ( ac, fac ) #a 1.5002
+        self.chat.ParsePost( txt, True, True )
+
+    def tohtml(self):
+        html_str = """<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >
+            <th>AC</th><th>Check Penalty</th><th >Spell Failure</th>
+            <th>Max Dex</th><th>Total Weight</th></tr>"""
+        html_str += "<tr ALIGN='center' >"
+        html_str += "<td>"+str(self.get_armor_class())+"</td>"
+        html_str += "<td>"+str(self.get_check_pen())+"</td>"
+        html_str += "<td>"+str(self.get_spell_failure())+"</td>"
+        html_str += "<td>"+str(self.get_max_dex())+"</td>"
+        html_str += "<td>"+str(self.get_total_weight())+"</td></tr></table>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += """<P><table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >
+                <th colspan=3>Armor</th><th>Type</th><th >Bonus</th></tr>"""
+            html_str += "<tr ALIGN='center' >"
+            html_str += "<td  colspan=3>"+n.getAttribute('name')+"</td>"
+            html_str += "<td>"+n.getAttribute('type')+"</td>"
+            html_str += "<td>"+n.getAttribute('bonus')+"</td></tr>"
+            html_str += """<tr BGCOLOR=#E9E9E9 >"""
+            html_str += "<th>Check Penalty</th><th>Spell Failure</th>"
+            html_str += "<th>Max Dex</th><th>Speed</th><th>Weight</th></tr>"
+            html_str += "<tr ALIGN='center'>"
+            html_str += "<td>"+n.getAttribute('checkpenalty')+"</td>"
+            html_str += "<td>"+n.getAttribute('spellfailure')+"</td>"
+            html_str += "<td>"+n.getAttribute('maxdex')+"</td>"
+            html_str += "<td>"+n.getAttribute('speed')+"</td>"
+            html_str += "<td>"+n.getAttribute('weight')+"</td></tr></table>"
+        return html_str
+
+
+class ac_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Armor')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 we need the functional parent, not the invoking parent.
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Armor"), 1, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Armor"), 1, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.master_dom = handler.master_dom
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        col_names = ['Armor','bonus','maxdex','cp','sf','weight','speed','type']
+        self.grid.CreateGrid(len(n_list),len(col_names),1)
+        self.grid.SetRowLabelSize(0)
+        for i in range(len(col_names)):
+            self.grid.SetColLabelValue(i,col_names[i])
+        self.atts =['name','bonus','maxdex','checkpenalty',
+            'spellfailure','weight','speed','type']
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col >= 1 and col <= 5:
+            try:
+                int(value)
+                self.n_list[row].setAttribute(self.atts[col],value)
+            except:
+                self.grid.SetCellValue(row,col,"0")
+        else:
+            self.n_list[row].setAttribute(self.atts[col],value)
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+
+        for y in range(len(self.atts)):
+            self.grid.SetCellValue(i,y,n.getAttribute(self.atts[y]))
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd35"]+"dnd35armor.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('armor')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Armor:','Armor List',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols+2)
+        self.grid.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.grid.SetColSize(i,col_w)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/dnd3e.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,3473 @@
+# 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: dnd3e.py
+# Author: Chris Davis & Digitalxero
+# Maintainer: leadb
+# Version:
+#   $Id: dnd3e.py,v 1.33 2006/11/04 21:24:21 digitalxero Exp $
+#
+# Description: The file contains code for the dnd3e nodehanlers
+#
+# qpatch by lead.b.  All modified lines denoted by # 1.500x[or] as below
+# 1.5001r fix for unable to update misc modifier for saves
+# 1.5002r fix for dnd_globals not scoping 1 character's data from another -or
+#        causing abends or other problems due to lack of intialization from
+#        char abilities.  getCharacterProp function added.
+#           This includes fix for "total on saves", making "rclick" on skill
+#           work to send roll to window,"total on skill rolls",
+# 1.5004r getting ac mod for
+#       those skill which have armour class adjustments was broken.
+# 1.5005r perhaps no lines marked.  Under dnd3eattacks, the misc. value for
+#       both Melee and Ranged do not get re-displayed if the panel is closed
+#       and re-opened, tho the adjustment still seems to be in total... perhap
+#       total just isn't getting recalculated?
+# 1.5006r on rclick on a weapon, on first attack roll is generated, additional
+#       crash,terminating the sequence.
+# 1.5008r extended bonuses for "extra" attacks don't include all items.
+# 1.5009r while 1.5008 is "resolved", not sure it is correct for special monk
+#       attacks.  I'm pretty sure I fixed this.
+# 1.5014r powerpoints are broken.
+# 1.5017r if you bring up the entire character in edit mode (dlclick on
+#       top node of character), and click
+#       "Money and Inventory" tab, then change the amount of "plat" a
+#       character has, the name on the tree will be updated to match
+#       the "plat" entry.
+# 1.5019r if str bonus was 0, forgot + for aStrengthMod.
+# ---v 1.6 cut. above corrected by 1.6.
+# -- items marked 1.5xxx were found prior 1.6 but not added (enhancements)
+#       or corrected (bugs, default if not stated) at that cut point
+# 1.5003r this is currently just a "this is busted" flag, this is an rclick on
+#       on the saves node (above the fort/wil/ref saves) itself.  It throws an
+#       error message into the python window, then nothing... Not even sure what
+#       it is -supposed- to do. set to do nothing, which I think is fine.
+# 1.5011r enhancement.  Damage for thrown weapon should get str adder.
+#       Don't think this is accounted for.  Remember, JUST damage, not to-hit.
+#       included into 1.6002, marking resolved to simplify list.
+# 1.5012r enhancement.  str penalty to dam for bow/slings should be applied.
+#       but it's not. Remember, this is just for damage, not to-hit.
+# 1.5015r if you bring up the entire character in edit mode (dlclick on
+#       top node of character), and click
+#       "Spells and Powers" tab, then "Psionic Powers", push the "Refresh
+#       Powers" button, the powers are refreshed but if you select the
+#       "PowerPoints" tab, the update is not reflected there.  Must close
+#       and reopen edit session for refresh. (Already corrected misspelling of
+#       "Pionic" -> "Psionic")
+# 1.6000r eliminate misc debug items, include things like getCharacterProp which
+#       never got used.  Documenting for completeness.  Minor changes for clarity
+# 1.6001r bug, melee weapons, such as dagger, which have a range are currently
+#       being treated simply as ranged weapons... this mean dex bonuses
+#       are being applied for to-hit Vs strength.  Melee thrown are treated
+#       different than ranged.
+# 1.6003r bug, if double weapon handle damage specified as 1d8/1d6 as 1d8
+#       for single weapon usage.  The correct usage for double weapon will
+#       for short term at least remove requirement to have to update
+#       memorized list.  - med priority
+# 1.6008r C:\Program Files\openrpg1\orpg\templates\nodes\dnd3e.xml minor
+#       typos corrected, added comment on psionics.  Remember to replace!
+# 1.6009r tohtml fails, thus send to chat fails and pretty print fails.
+# 1.6010r tohtml for power points needs some clean up.
+# 1.6011r if multiple weapons are chosen for deletion from character inventory,
+#       at least one wrong weapons will be deleted.
+# 1.6012r flurry attack negative only applied to med monk, applies to all.
+# 1.6013r penalties on stats on tohtml showed +-3, instead of just -3.
+# 1.6014r rclick on "Skills" node above actual skills throws an error msg to
+#       python window.
+# 1.6015r rclick on "Attacks" or "Abilities Score" node throws an error msg to
+#       python window.
+# 1.6016r enhancement add comment to rclick footnotes to get footnote details.
+# 1.6018r resolve saves not updating on panel if open when ability scores change
+#       change
+# 1.6021r didn't roll extra monk attacks if base to it bonus was 0.
+# 1.6022r monks always got d20 damage, reversed order of checks to fix.
+# v1.8 cut.
+# -- items marked 1.6xxx were found prior 1.8 but not added (enhancements)
+#       or corrected (bugs, default if not stated) at that cut point
+# 1.5007o enhancement. str bows not accounted for correctly.
+#       thoughts: use new element tag to indicate strBow(3).
+# 1.5010r proposed enhancement.  Adding character name to frames around stuff.
+#    - marking resolved... determined to not do.
+# 1.5013o enhancement. this is for all "off-hand" attacks stuff. Eg: str bonus
+#       only 1/2 for "off-hand" for both to-hit and damage (unless penalty! ;-)
+#       Probably other things, as I see nothing in here to account for them.
+# 1.5016o enhancement. swim check does not reflect weight check.
+# 1.5018o enhancement. actual psionic abilities list.
+# 1.6002o enhancement.  modify code to take advanage of new footnote field for
+#       indicating which weapons are Thrown, as opposed to improvised thrown
+#       weapons; which are treated differently.  Allow for throwing of melee
+#       weapons (with 1.6001 all melee are unthrowable) recast of 1.5011,
+#       which I'm marking resolved.
+# 1.6004o feature request from 541659 Ability to remove skills, at least those
+#       that can't be used untrained.  This would also require ability to re-add
+#       skills later.  Short term solution may be to ability to clearly mark
+#       skill which can't be used yet. - low priority.
+# 1.6005o feature request from 541659 Custom feats, without the need to edit
+#       data/dnd3e/dnd3efeats.xml  Note, while standard feats may be affecting
+#       how tool will generate rolls, etc; it is unlikely that custom feats will
+#       will be able to do this; but should be able to be include from a
+#       complete "character sheet" perspective. - low priority (since
+#       dnd3efeats can be edited to add feats)
+# 1.6006o feature request from 541659 Do sorcerer and bard spells right;
+#       for short term at least remove requirement to have to update
+#       memorized list.  - med priority
+# 1.6007o feature request from 541659 Make tabs optional to be able to remove
+#       tabs which are unused by a particular character.  Would need ability
+#       to add them back in, since character might later add a class which
+#       would need them. - very low priority
+# 1.6017o enhancement when editing footnotes for weapons,
+#       provide full table of footnotes in companion window to help.
+# 1.6019o enhancement Forum request to add "flatfooted" to ac matrix
+# 1.6020o enh add column to skills to allow tracking of skill points allocated.
+# 1.9000r clean up of excess comments from 1.6 and earlier.
+# 1.9001r if str or dex changes, Melee/Ranged combat ability does not update
+#        until refreshed by a change.
+# 1.9002r depending on what subwindows were open, changing stat scores could
+#        crash out entire orpg environment.
+#
+# r- resolved
+# o- open
+#
+import orpg.tools.orpg_settings
+import orpg.minidom
+from core import *
+from containers import *
+from string import *  #a 1.6003
+from inspect import *  #a 1.9001
+dnd3e_EXPORT = wx.NewId()
+############Global Stuff##############
+
+HP_CUR = wx.NewId()
+HP_MAX = wx.NewId()
+PP_CUR = wx.NewId()
+PP_MAX = wx.NewId()
+PP_FRE = wx.NewId()
+PP_MFRE = wx.NewId()
+HOWTO_MAX = wx.NewId()
+
+def getRoot (node): # a 1.5002 this whole function is new.
+    root = None
+    target = node
+    while target != None:
+        root = target
+        target = target.hparent
+    return root
+#o 1.5002 (this whole comment block)
+# if a method runs getRoot for its instance and assigns the
+# value returned to self.root, you can get to instances X via Y
+# instance handle   via     invocation
+# ---------------   ---     -----------
+# classes           via     self.root.classes
+# abilities         via     self.root.abilities
+# pp                via     self.root.pp
+# general           via     self.root.general
+# saves             via     self.root.saves
+# attacks           via     self.root.attacks
+# ac                via     self.root.ac
+# feats             via     self.root.feats
+# spells            via     self.root.spells
+# divine            via     self.root.divine
+# powers            via     self.root.powers
+# inventory         via     self.root.inventory
+# hp                via     self.root.hp
+# skills            via     self.root.skills
+#... if other instances are needed and the instance exists uniquely,
+# add to the instance you need access to in the __init__ section the following:
+#       self.hparent = parent # or handler if a wx instance, also add up chain
+#       self.root = getRoot(self)
+#       self.root.{instance handle} = self
+#       # replace {instance handle} with your designation
+# then, where you need access to the instance, simply add this to the instance
+# that needs to reference
+#       self.hparent = getRoot(self) # in the init section, if not already there
+#       self.root = getRoot(self)    # also in the init
+#   then to refer to the instance where you need it:
+#       self.root.{instance handle}.{whatever you need}
+#       # replace {instance handle} with your designation
+#       # replace {whatever you need} with the attribute/method u want.
+
+#d 1.6000 not used.
+#def getCharacterProp(forgetIt):
+#    return None
+
+#a 1.6 convinience function added safeGetAttr
+def safeGetAttr(node,lable,defRetV=None):
+    cna=node.attributes
+    for i2 in range(len(cna)):
+        if cna.item(i2).name == lable:
+            return cna.item(i2).value
+    #retV=node.getAttribute(lable) # getAttribute does not distingish between
+    # the attribute not being present vs it having a value of ""
+    # This is bad for this routine, thus not used.
+    return defRetV
+#a 1.6... safeGetAttr end.
+
+########End of My global Stuff########
+########Start of Main Node Handlers#######
+class dnd3echar_handler(container_handler):
+    """ Node handler for a dnd3e charactor
+        <nodehandler name='?'  module='dnd3e' class='dnd3echar_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.Version = "v1.901" #a 1.6000 general documentation, usage.
+
+        print "dnd3echar_handler - version:",self.Version #m 1.6000
+
+        self.hparent = None #a 1.5002 allow ability to run up tree, this is the
+        #a 1.5002 top of the handler tree, this is used to flag where to stop
+        #a 1.5002 on the way up.  Changing this will break getRoot(self)
+
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('howtouse','HowTo use this tool',dnd3ehowto,'note')
+        self.new_child_handler('general','GeneralInformation',dnd3egeneral,'gear')
+        self.new_child_handler('inventory','MoneyAndInventory',dnd3einventory,'money')
+        self.new_child_handler('character','ClassesAndStats',dnd3eclassnstats,'knight')
+        self.new_child_handler('snf','SkillsAndFeats',dnd3eskillsnfeats,'book')
+        self.new_child_handler('combat','Combat',dnd3ecombat,'spears')
+        self.new_child_handler('snp','SpellsAndPowers',dnd3esnp,'flask')
+        #wxMenuItem(self.tree.std_menu, dnd3e_EXPORT, "Export...", "Export")
+        #print "dnd3echar_handler init - "+\
+        # "self.child_handlers:",self.child_handlers # a (debug) 1.5002
+        self.myeditor = None
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+    def tohtml(self):
+        html_str = "<table><tr><td colspan=2 >"
+        #d block for 1.6009 start
+        #html_str += self.child_handlers['general'].tohtml()+"</td></tr>"
+        #html_str += "<tr><td width='50%' valign=top >
+        #        "+self.child_handlers['abilities'].tohtml()
+        #html_str += "<P>" + self.child_handlers['saves'].tohtml()
+        #html_str += "<P>" + self.child_handlers['attacks'].tohtml()
+        #html_str += "<P>" + self.child_handlers['ac'].tohtml()
+        #html_str += "<P>" + self.child_handlers['feats'].tohtml()
+        #html_str += "<P>" + self.child_handlers['spells'].tohtml()
+        #html_str += "<P>" + self.child_handlers['divine'].tohtml()
+        #html_str += "<P>" + self.child_handlers['powers'].tohtml()
+        #html_str += "<P>" + self.child_handlers['inventory'].tohtml() +"</td>"
+        #html_str += "<td width='50%' valign=top >
+        #       "+self.child_handlers['classes'].tohtml()
+        #html_str += "<P>" + self.child_handlers['hp'].tohtml()
+        #html_str += "<P>" + self.child_handlers['pp'].tohtml()
+        #html_str += "<P>" + self.child_handlers['skills'].tohtml() +"</td>"
+        #d block for 1.6009 end
+        #a block for 1.6009 start
+        html_str += self.general.tohtml()+"</td></tr>"
+        html_str += "<tr><td width='50%' valign=top >"+self.abilities.tohtml()
+        html_str += "<P>" + self.saves.tohtml()
+        html_str += "<P>" + self.attacks.tohtml()
+        html_str += "<P>" + self.ac.tohtml()
+        html_str += "<P>" + self.feats.tohtml()
+        html_str += "<P>" + self.spells.tohtml()
+        html_str += "<P>" + self.divine.tohtml()
+        html_str += "<P>" + self.powers.tohtml()
+        html_str += "<P>" + self.inventory.tohtml() +"</td>"
+        html_str += "<td width='50%' valign=top >"+self.classes.tohtml()
+        html_str += "<P>" + self.hp.tohtml()
+        html_str += "<P>" + self.pp.tohtml()
+        html_str += "<P>" + self.skills.tohtml() +"</td>"
+        #a block for 1.6009 end
+
+        html_str += "</tr></table>"
+        return html_str
+
+    def about(self):
+        html_str = "<img src='" + orpg.dirpath.dir_struct["icon"]
+        html_str += "dnd3e_logo.gif' ><br><b>dnd3e Character Tool "
+        html_str += self.Version+"</b>" #m 1.6000 was hard coded.
+        html_str += "<br>by Dj Gilcrease<br>digitalxero@gmail.com"
+        return html_str
+
+########Core Handlers are done now############
+########Onto the Sub Nodes########
+##Primary Sub Node##
+
+class outline_panel(wx.Panel):
+    def __init__(self, parent, handler, wnd, txt,):
+        self.parent = parent #a 1.9001
+        wx.Panel.__init__(self, parent, -1)
+        self.panel = wnd(self,handler)
+        self.sizer = wx.StaticBoxSizer(wx.StaticBox(self,-1,txt), wx.VERTICAL)
+
+        self.sizer.Add(self.panel, 1, wx.EXPAND)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+class dnd3e_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+###Child Nodes Organized the way they are in the XML for easier viewing####  #m 1.5002 corrected typo.
+class dnd3ehowto(dnd3e_char_child):  #m 1.5002 corrected string below to reflect "how to"
+    """ Node Handler for how to instructions.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        dnd3e_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+
+    def get_design_panel(self,parent):
+        wnd = howto_panel(parent, self)
+        wnd.title = "How To"
+        return wnd
+
+class howto_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+
+        pname = handler.master_dom.setAttribute("name", 'How To')
+        self.sizer = wx.StaticBoxSizer(wx.StaticBox(self, -1, 'How To'), wx.VERTICAL)
+        self.master_dom = handler.master_dom
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+        self.sizer.Add(wx.StaticText(self, -1, t_node._get_nodeValue()), 1, wx.EXPAND)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+class dnd3egeneral(dnd3e_char_child):
+    """ Node Handler for general information.   This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        dnd3e_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.general = self  #a 1.5002
+        self.charName = self.get_char_name() # a 1.5002 make getting name easier.
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,gen_grid,"General Information")
+        wnd.title = "General Info"
+        return wnd
+
+    def tohtml(self):
+        n_list = self.master_dom._get_childNodes()
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>General Information</th></tr><tr><td>"
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+            html_str += "<B>"+n._get_tagName().capitalize() +":</B> "
+            html_str += t_node._get_nodeValue() + ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def on_name_change(self,name):
+        self.char_hander.rename(name)
+        #o 1.5002 self.char_hander = parent in this case.
+        self.charName = name  #a 1.5002 make getting name easier.
+
+
+    def get_char_name( self ):
+        node = self.master_dom.getElementsByTagName( 'name' )[0]
+        t_node = safe_get_text_node( node )
+        return t_node._get_nodeValue()
+
+class gen_grid(wx.grid.Grid):
+    """grid for gen info"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'General')
+        self.hparent = handler #a 1.5002 allow ability to run up tree, needed
+        # a 1.5002 parent is functional parent, not invoking parent.
+
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        n_list = handler.master_dom._get_childNodes()
+        self.CreateGrid(len(n_list),2)
+        self.SetRowLabelSize(0)
+        self.SetColLabelSize(0)
+        self.n_list = n_list
+        i = 0
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        t_node = self.n_list[row]._get_firstChild()
+        t_node._set_nodeValue(value)
+        if row==0:
+            self.handler.on_name_change(value)
+        self.AutoSizeColumn(1)
+
+    def refresh_row(self,rowi):
+        t_node = safe_get_text_node(self.n_list[rowi])
+
+        self.SetCellValue(rowi,0,self.n_list[rowi]._get_tagName())
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,t_node._get_nodeValue())
+        self.AutoSizeColumn(1)
+
+class dnd3einventory(dnd3e_char_child):
+    """ Node Handler for general information.   This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        dnd3e_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.6009
+        self.root.inventory = self #a 1.6009
+
+    def get_design_panel(self,parent):
+        wnd = inventory_pane(parent, self) #outline_panel(parent,self,inventory_grid,"Inventory")
+        wnd.title = "General Info"
+        return wnd
+
+    def tohtml(self):
+        n_list = self.master_dom._get_childNodes()
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>General Information</th></tr><tr><td>"
+        for n in n_list:
+            t_node = safe_get_text_node(n)
+            html_str += "<B>"+n._get_tagName().capitalize() +":</B> "
+            html_str += t_node._get_nodeValue() + "<br>"
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+class inventory_pane(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, wx.ID_ANY)
+
+        self.n_list = handler.master_dom._get_childNodes()
+        self.autosize = False
+
+        self.sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, "Inventroy"), wx.VERTICAL)
+
+        self.lang = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_BESTWRAP, name="Languages")
+        self.gear = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_BESTWRAP, name="Gear")
+        self.magic = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_BESTWRAP, name="Magic")
+        self.grid = wx.grid.Grid(self, wx.ID_ANY, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+
+        self.grid.CreateGrid(len(self.n_list)-3,2)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelSize(0)
+
+        for i in xrange(len(self.n_list)):
+            self.refresh_row(i)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(self.grid, 1, wx.EXPAND)
+        sizer1.Add(self.lang, 1, wx.EXPAND)
+
+        self.sizer.Add(sizer1, 0, wx.EXPAND)
+
+        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer2.Add(self.gear, 1, wx.EXPAND)
+        sizer2.Add(self.magic, 1, wx.EXPAND)
+
+        self.sizer.Add(sizer2, 1, wx.EXPAND)
+
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.Bind(wx.EVT_TEXT, self.onTextNodeChange, self.lang)
+        self.Bind(wx.EVT_TEXT, self.onTextNodeChange, self.gear)
+        self.Bind(wx.EVT_TEXT, self.onTextNodeChange, self.magic)
+        self.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self.on_cell_change, self.grid)
+
+
+    def fillTextNode(self, name, value):
+        if name == 'Languages':
+            self.lang.SetValue(value)
+        elif name == 'Gear':
+            self.gear.SetValue(value)
+        elif name == 'Magic':
+            self.magic.SetValue(value)
+
+    def onTextNodeChange(self, event):
+        id = event.GetId()
+
+        if id == self.gear.GetId():
+            nodeName = 'Gear'
+            value = self.gear.GetValue()
+        elif id == self.magic.GetId():
+            nodeName = 'Magic'
+            value = self.magic.GetValue()
+        elif id == self.lang.GetId():
+            nodeName = 'Languages'
+            value = self.lang.GetValue()
+
+        for node in self.n_list:
+            if node._get_tagName() == nodeName:
+                t_node = safe_get_text_node(node)
+                t_node._set_nodeValue(value)
+
+    def saveMoney(self, row, col):
+        value = self.grid.GetCellValue(row, col)
+        t_node = safe_get_text_node(self.n_list[row])
+        t_node._set_nodeValue(value)
+
+    def on_cell_change(self, evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        self.grid.AutoSizeColumn(col)
+        wx.CallAfter(self.saveMoney, row, col)
+
+
+
+    def refresh_row(self, row):
+        t_node = safe_get_text_node(self.n_list[row])
+        tagname = self.n_list[row]._get_tagName()
+        value = t_node._get_nodeValue()
+        if tagname == 'Gear':
+            self.fillTextNode(tagname, value)
+        elif tagname == 'Magic':
+            self.fillTextNode(tagname, value)
+        elif tagname == 'Languages':
+            self.fillTextNode(tagname, value)
+        else:
+            self.grid.SetCellValue(row, 0, tagname)
+            self.grid.SetReadOnly(row, 0)
+            self.grid.SetCellValue(row, 1, value)
+            self.grid.AutoSize()
+
+
+class dnd3eclassnstats(dnd3e_char_child):
+    """ Node handler for a dnd3e charactor
+        <nodehandler name='?'  module='dnd3e' class='dnd3echar_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        dnd3e_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('abilities','Abilities Scores',dnd3eability,'gear')
+        self.new_child_handler('classes','Classes',dnd3eclasses,'knight')
+        self.new_child_handler('saves','Saves',dnd3esaves,'skull')
+        self.myeditor = None
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+class class_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+class dnd3eability(class_char_child):
+    """ Node Handler for ability.   This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        class_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self)  #a 1.5002 get top of our local function tree.
+        self.root.abilities = self #a 1.5002 let other classes find me.
+
+        self.abilities = {}
+        node_list = self.master_dom.getElementsByTagName('stat')
+        tree = self.tree
+        icons = tree.icons
+
+        for n in node_list:
+            name = n.getAttribute('abbr')
+            self.abilities[name] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['gear'], icons['gear'] )
+            tree.SetPyData( new_tree_node, self )
+        #print "dnd3eability - init self.abilities",self.abilities #a (debug) 1.5002
+
+    def on_rclick( self, evt ):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        #if item == self.mytree_node:   #d 1.6016
+        #    dnd3e_char_child.on_ldclick( self, evt ) #d 1.6016
+        if not item == self.mytree_node: #a 1.6016
+        #else: #d 1.6016
+            mod = self.get_mod( name )
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s check: [1d20%s%s]' % ( name, mod1, mod )
+            chat.ParsePost( txt, True, True )
+
+    def get_mod(self,abbr):
+        score = int(self.abilities[abbr].getAttribute('base'))
+        mod = (score - 10) / 2
+        mod = int(mod)
+        return mod
+
+    def set_score(self,abbr,score):
+        if score >= 0:
+            self.abilities[abbr].setAttribute("base",str(score))
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,abil_grid,"Abilities")
+        wnd.title = "Abilities (edit)"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100%><tr BGCOLOR=#E9E9E9 ><th width='50%'>Ability</th>
+                    <th>Base</th><th>Modifier</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('stat')
+        for n in node_list:
+            name = n.getAttribute('name')
+            abbr = n.getAttribute('abbr')
+            base = n.getAttribute('base')
+            mod = str(self.get_mod(abbr))
+            if int(mod) >= 0: #m 1.6013 added "int(" and ")"
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str = (html_str + "<tr ALIGN='center'><td>"+
+                name+"</td><td>"+base+'</td><td>%s%s</td></tr>' % (mod1, mod))
+        html_str = html_str + "</table>"
+        return html_str
+
+class abil_grid(wx.grid.Grid):
+    """grid for abilities"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Stats')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self)
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        stats = handler.master_dom.getElementsByTagName('stat')
+        self.CreateGrid(len(stats),3)
+        self.SetRowLabelSize(0)
+        col_names = ['Ability','Score','Modifier']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.stats = stats
+        i = 0
+        for i in range(len(stats)):
+            self.refresh_row(i)
+        self.char_wnd = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        #print value
+        try:
+            int(value)
+            self.stats[row].setAttribute('base',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+        if self.char_wnd:
+            self.char_wnd.refresh_data()
+
+    #mark5
+
+    def refresh_row(self,rowi):
+        s = self.stats[rowi]
+
+        name = s.getAttribute('name')
+        abbr = s.getAttribute('abbr')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        self.SetCellValue(rowi,1,s.getAttribute('base'))
+        self.SetCellValue(rowi,2,str(self.handler.get_mod(abbr)))
+        self.SetReadOnly(rowi,2)
+        #if self.root.saves.saveGrid: #a 1.6018 d 1.9002 whole if clause
+            #print getmembers(self.root.saves.saveGrid)
+            #self.root.saves.saveGrid.refresh_data() #a 1.6018
+            #print "skipping saving throw update, put back in later"
+        self.root.saves.refresh_data() #a 1.9002
+        self.root.attacks.refreshMRdata() #a 1.9001 `
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()-1):
+            self.refresh_row(r)
+
+class dnd3eclasses(class_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        class_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self)
+        self.root.classes = self
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,class_panel,"Classes")
+        wnd.title = "Classes"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Classes</th></tr><tr><td>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += n.getAttribute('name') + " ("+n.getAttribute('level')+"), "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        node_list = self.master_dom.getElementsByTagName('class')
+        # print "eclasses - get_char_lvl node_list",node_list
+        tot = 0  #a 1.5009 actually, slipping in a quick enhancement ;-)
+        for n in node_list:
+            lvl = n.getAttribute('level') #o 1.5009 not sure of the value of this
+            tot += int(lvl) #a 1.5009
+            type = n.getAttribute('name') #o 1.5009 not sure of the value of this
+            #print type,lvl #a (debug) 1.5009
+            if attr == "level":
+                return lvl #o 1.5009 this returns the level of someone's first class. ???
+            elif attr == "class":
+                return type #o 1.5009 this returns one of the char's classes. ???
+        if attr == "lvl":   #a 1.5009 this has value, adding this.
+            return tot  #a 1.5009 return character's "overall" level.
+
+    def get_class_lvl( self, classN ): #a 1.5009 need to be able to get monk lvl
+        #a 1.5009 this function is new.
+        node_list = self.master_dom.getElementsByTagName('class')
+        #print "eclasses - get_class_lvl node_list",node_list
+        for n in node_list:
+            lvl = n.getAttribute('level')
+            type = n.getAttribute('name')
+            if classN == type:
+                return lvl
+
+class class_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Class')
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Class"), 0, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Class"), 0, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),2,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"Class")
+        self.grid.SetColLabelValue(1,"Level")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        try:
+            int(value)
+            self.n_list[row].setAttribute('level',value)
+        except:
+            self.grid.SetCellValue(row,col,"1")
+
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+
+        name = n.getAttribute('name')
+        level = n.getAttribute('level')
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,level)
+        #self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd3e"]+"dnd3eclasses.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('class')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Class','Classes',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+
+
+class dnd3esaves(class_char_child):
+    """ Node Handler for saves.   This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        class_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        #self.saveGrid = None  #a 1.6018 d 1.9002
+        self.saveGridFrame = []  #a 1.9002 handle list, check frame for close.
+
+        tree = self.tree
+        icons = self.tree.icons
+
+        self.root = getRoot(self) #a 1.5002
+        self.root.saves = self #a 1.6009
+        node_list = self.master_dom.getElementsByTagName('save')
+        self.saves={}
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.saves[name] = n
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+
+    #a 1.9002 this whole method
+    def refresh_data(self): # refresh the data in the melee/ranged section
+        # of the attack chart.
+        # count backwards, maintains context despite "removes"
+        for i in range(len(self.saveGridFrame)-1,-1,-1):
+            x = self.saveGridFrame[i]
+            if x == None:
+                x.refresh_data()
+            else:
+                self.saveGridFrame.remove(x)
+
+    def get_mod(self,name):
+        save = self.saves[name]
+        stat = save.getAttribute('stat')
+        #print "dnd3esaves, get_mod: self,root",self,self.root #a (debug) 1.5002
+        #print "and abilities",self.root.abilities      #a (debug) 1.5002
+        stat_mod = self.root.abilities.get_mod(stat)            #a 1.5002
+        base = int(save.getAttribute('base'))
+        miscmod = int(save.getAttribute('miscmod'))
+        magmod = int(save.getAttribute('magmod'))
+        total = stat_mod + base + miscmod + magmod
+        return total
+
+    def on_rclick(self,evt):
+
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            pass #a 1.5003 syntatic place holder
+            return #a 1.5003
+            #print "failure mode!"
+            #dnd3e_char_child.on_ldclick(self,evt) #d 1.5003 this busted
+            #wnd = save_grid(self.frame.note,self)
+            #wnd.title = "Saves"
+            #self.frame.add_panel(wnd)
+        else:
+            mod = self.get_mod(name)
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s save: [1d20%s%s]' % (name, mod1, mod)
+            chat.ParsePost( txt, True, True )
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,save_grid,"Saves")
+        wnd.title = "Saves"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 >
+            <th width='30%'>Save</th>
+            <th>Key</th><th>Base</th><th>Abil</th><th>Magic</th>
+            <th>Misc</th><th>Total</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('save')
+        for n in node_list:
+            name = n.getAttribute('name')
+            stat = n.getAttribute('stat')
+            base = n.getAttribute('base')
+            html_str = html_str + "<tr ALIGN='center'><td>"+name+"</td><td>"+stat+"</td><td>"+base+"</td>"
+            #stat_mod = str(dnd_globals["stats"][stat])         #d 1.5002
+            stat_mod = self.root.abilities.get_mod(stat)        #a 1.5002
+
+            mag = n.getAttribute('magmod')
+            misc = n.getAttribute('miscmod')
+            mod = str(self.get_mod(name))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            #m 1.5009 next line.  added str() around stat_mod
+            html_str = html_str + "<td>"+str(stat_mod)+"</td><td>"+mag+"</td>"
+            html_str = html_str + '<td>'+misc+'</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+#mark6
+class save_grid(wx.grid.Grid):
+    """grid for saves"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Saves')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+        self.root = getRoot(self)
+
+        #self.hparent.saveGrid = self #a 1.6018 d 1.9001
+
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        saves = handler.master_dom.getElementsByTagName('save')
+        self.CreateGrid(len(saves),7)
+        self.SetRowLabelSize(0)
+        col_names = ['Save','Key','base','Abil','Magic','Misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.saves = saves
+        i = 0
+        for i in range(len(saves)):
+            self.refresh_row(i)
+
+
+        #a 1.9002 remainder of code in this method.
+        climber = parent
+        nameNode = climber.GetClassName()
+        while nameNode != 'wxFrame':
+            climber = climber.parent
+            nameNode = climber.GetClassName()
+        masterFrame=climber
+        masterFrame.refresh_data=self.refresh_data
+        #print getmembers(masterFrame)
+
+        handler.saveGridFrame.append(masterFrame)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            if col == 2:
+                self.saves[row].setAttribute('base',value)
+            elif col ==4:
+                self.saves[row].setAttribute('magmod',value)
+            elif col ==5:                                       # 1.5001
+                self.saves[row].setAttribute('miscmod',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_row(self,rowi):
+        s = self.saves[rowi]
+
+        name = s.getAttribute('name')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        stat = s.getAttribute('stat')
+        self.SetCellValue(rowi,1,stat)
+        self.SetReadOnly(rowi,1)
+        self.SetCellValue(rowi,2,s.getAttribute('base'))
+        self.SetCellValue(rowi,3,str(self.root.abilities.get_mod(stat)))
+        self.SetReadOnly(rowi,3)
+        self.SetCellValue(rowi,4,s.getAttribute('magmod'))
+        self.SetCellValue(rowi,5,s.getAttribute('miscmod'))
+        mod = str(self.handler.get_mod(name))
+        self.SetCellValue(rowi,6,mod)
+        self.SetReadOnly(rowi,6)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+        for r in range(self.GetNumberRows()):
+            self.refresh_row(r)
+
+class dnd3eskillsnfeats(dnd3e_char_child):
+    """ Node handler for a dnd3e charactor
+        <nodehandler name='?'  module='dnd3e' class='dnd3echar_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.6009
+
+        #print "dnd3eskillsnfeats - init, self ",self #a (debug) 1.5002
+        #print "dnd3eskillsnfeats - init, parent ",self.hparent #a (debug) 1.5002
+        #print "dnd3eskillsnfeats - init, parent ",parent.dnd3eclassnstats #a (debug) 1.5002
+
+        node_handler.__init__(self,xml_dom,tree_node)
+        dnd3e_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('skills','Skills',dnd3eskill,'book')
+        self.new_child_handler('feats','Feats',dnd3efeats,'book')
+        #wxMenuItem(self.tree.std_menu, dnd3e_EXPORT, "Export...", "Export")
+        self.myeditor = None
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+class skills_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+class dnd3eskill(skills_char_child):
+    """ Node Handler for skill.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        #a 1.5002 Need the functional parent, not the invoking parent.
+        self.root = getRoot(self) #a 1.5002
+        self.root.skills = self #a 1.6009
+
+        skills_char_child.__init__(self,xml_dom,tree_node,parent)
+        tree = self.tree
+        icons = self.tree.icons
+        node_list = self.master_dom.getElementsByTagName('skill')
+
+        self.skills={}
+        #Adding code to not display skills you can not use -mgt
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.skills[name] = n
+            skill_check = self.skills[name]
+            ranks = int(skill_check.getAttribute('rank'))
+            trained = int(skill_check.getAttribute('untrained'))
+
+            if ranks > 0 or trained == 1:
+                new_tree_node = tree.AppendItem(self.mytree_node,name,
+                                            icons['gear'],icons['gear'])
+            else:
+                continue
+
+            tree.SetPyData(new_tree_node,self)
+
+
+
+    def refresh_skills(self):
+                #Adding this so when you update the grid the tree will reflect
+                #The change. -mgt
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('skill')
+
+        self.skills={}
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.skills[name] = n
+            skill_check = self.skills[name]
+            ranks = int(skill_check.getAttribute('rank'))
+            trained = int(skill_check.getAttribute('untrained'))
+
+            if ranks > 0 or trained == 1:
+                new_tree_node = tree.AppendItem(self.mytree_node,name,
+                                            icons['gear'],icons['gear'])
+            else:
+                continue
+
+            tree.SetPyData(new_tree_node,self)
+
+    def get_mod(self,name):
+        skill = self.skills[name]
+        stat = skill.getAttribute('stat')
+        #stat_mod = int(dnd_globals["stats"][stat])                 #d 1.5002
+        stat_mod = self.root.abilities.get_mod(stat)                #a 1.5002
+        rank = int(skill.getAttribute('rank'))
+        misc = int(skill.getAttribute('misc'))
+        total = stat_mod + rank + misc
+        return total
+
+    def on_rclick(self,evt):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText(item)
+        #print "skill rc self",self                 #a 1.6004
+        #print "skill rc tree",self.mytree_node     #a 1.6004
+        #print "skill rc item",item                 #a 1.6004
+        if item == self.mytree_node:
+            return
+            # following line fails,
+            #dnd3e_char_child.on_ldclick(self,evt) #d 1.6014
+            # it's what it used to try to do.
+        ac = self.root.ac.get_check_pen() #a 1.5002 for 1.5004 verify fix.
+
+        skill = self.skills[name]
+
+        untr = skill.getAttribute('untrained')                         #a 1.6004
+        rank = skill.getAttribute('rank')                              #a 1.6004
+        if eval('%s == 0' % (untr)):                                   #a 1.6004
+            if eval('%s == 0' % (rank)):                               #a 1.6004
+                res = 'You fumble around, accomplishing nothing'       #a 1.6004
+                txt = '%s Skill Check: %s' % (name, res)               #a 1.6004
+                chat = self.chat                                       #a 1.6004
+                chat.Post(txt,True,True)                               #a 1.6004
+                return                                                 #a 1.6004
+
+        armor = ''
+        acCp = ''
+        if ac < 0:  #acCp >= 1 #m 1.5004 this is stored as negatives.
+            armorCheck = int(skill.getAttribute('armorcheck'))
+            #print "ac,armorCheck",ac,armorCheck
+            if armorCheck == 1:
+                acCp=ac
+                armor = '(includes Armor Penalty of %s)' % (acCp)
+        if item == self.mytree_node:
+            dnd3e_char_child.on_ldclick(self,evt)
+            #wnd = skill_grid(self.frame.note,self)
+            #wnd.title = "Skills"
+            #self.frame.add_panel(wnd)
+        else:
+            mod = self.get_mod(name)
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            chat = self.chat
+            txt = '%s Skill Check: [1d20%s%s%s] %s' % (
+                    name, mod1, mod, acCp, armor)
+            chat.ParsePost(txt,True,True)
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,skill_grid,"Skills")
+        wnd.title = "Skills (edit)"
+        return wnd
+
+    def tohtml(self):
+        html_str = """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 >
+                    <th width='30%'>Skill</th><th>Key</th>
+                    <th>Rank</th><th>Abil</th><th>Misc</th><th>Total</th></tr>"""
+        node_list = self.master_dom.getElementsByTagName('skill')
+
+        for n in node_list:
+            name = n.getAttribute('name')
+            stat = n.getAttribute('stat')
+            rank = n.getAttribute('rank')
+            untr = n.getAttribute('untrained')                              #a 1.6004
+            #Filter unsuable skills out of pretty print -mgt
+            if eval('%s > 0' % (rank)) or eval('%s == 1' % (untr)):
+                if eval('%s >=1' % (rank)):
+                    html_str += "<tr ALIGN='center' bgcolor='#CCCCFF'><td>"     #a 1.6004
+                    #html_str += "<tr ALIGN='center' bgcolor='green'><td>"      #d 1.6004
+                    html_str += name+"</td><td>"+stat+"</td><td>"+rank+"</td>"
+                elif eval('%s == 1' % (untr)):                                  #a 1.6004
+                    html_str += "<tr ALIGN='center' bgcolor='#C0FF40'><td>"     #a 1.6004
+                    html_str += name+"</td><td>"+stat+"</td><td>"+rank+"</td>"  #a 1.6004
+                else:
+                    html_str += "<tr ALIGN='center'><td>"+name+"</td><td>"
+                    html_str += stat+"</td><td>"+rank+"</td>"
+            else:
+                continue
+            stat_mod = self.root.abilities.get_mod(stat)        #a 1.5002
+            #stat_mod = str(dnd_globals["stats"][stat])         #d 1.5002
+            misc = n.getAttribute('misc')
+            mod = str(self.get_mod(name))
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            html_str += "<td>"+str(stat_mod)+"</td><td>"+misc #m 1.6009 str()
+            html_str += '</td><td>%s%s</td></tr>' % (mod1, mod)
+        html_str = html_str + "</table>"
+        return html_str
+
+
+class skill_grid(wx.grid.Grid):
+    """ panel for skills """
+    def __init__(self, parent, handler):
+        self.hparent = handler    #a 1.5002 need function parent, not invoker
+        self.root = getRoot(self) #a 1.5002
+        pname = handler.master_dom.setAttribute("name", 'Skills')
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.handler = handler
+        skills = handler.master_dom.getElementsByTagName('skill')
+        #xelf.stats = dnd_globals["stats"]                           #d 1.5002
+
+        self.CreateGrid(len(skills),6)
+        self.SetRowLabelSize(0)
+        col_names = ['Skill','Key','Rank','Abil','Misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        rowi = 0
+        self.skills = skills
+        for i in range(len(skills)):
+            self.refresh_row(i)
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        #print value
+        try:
+            int(value)
+            if col == 2:
+                self.skills[row].setAttribute('rank',value)
+            elif col ==4:
+                self.skills[row].setAttribute('misc',value)
+            self.refresh_row(row)
+        except:
+            self.SetCellValue(row,col,"0")
+
+                #call refresh_skills
+        self.handler.refresh_skills()
+
+    def refresh_row(self,rowi):
+        s = self.skills[rowi]
+        name = s.getAttribute('name')
+        self.SetCellValue(rowi,0,name)
+        self.SetReadOnly(rowi,0)
+        stat = s.getAttribute('stat')
+        self.SetCellValue(rowi,1,stat)
+        self.SetReadOnly(rowi,1)
+        self.SetCellValue(rowi,2,s.getAttribute('rank'))
+        #self.SetCellValue(rowi,3,str(dnd_globals["stats"][stat]))  #d 1.5002
+        if self.root.abilities: #a 1.5002 sanity check.
+            stat_mod=self.root.abilities.get_mod(stat)           #a 1.5002
+        else: #a 1.5002
+            stat_mod = -6 #a 1.5002 this can happen if code is changed so
+            #a 1.5002 that abilities are not defined prior invokation of init.
+            print "Please advise dnd3e maintainer alert 1.5002 raised"
+
+        self.SetCellValue(rowi,3,str(stat_mod))         #a 1.5002
+        self.SetReadOnly(rowi,3)
+        self.SetCellValue(rowi,4,s.getAttribute('misc'))
+        mod = str(self.handler.get_mod(name))
+        self.SetCellValue(rowi,5,mod)
+        self.SetReadOnly(rowi,5)
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+2)
+        self.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+    def refresh_data(self):
+
+        for r in range(self.GetNumberRows()):
+            self.refresh_row(r)
+
+
+
+
+class dnd3efeats(skills_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        skills_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.feats = self #a 1.6009
+
+
+    def get_design_panel(self,parent):
+        setTitle="Feats - " + self.root.general.charName    #a 1.5010
+        wnd = outline_panel(parent,self,feat_panel,setTitle) #a 1.5010
+        #wnd = outline_panel(parent,self,feat_panel,"Feats") #d 1.5010
+        wnd.title = "Feats" #d 1.5010
+        #wnd.title = "Feats - " + self.charName
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Feats</th></tr><tr><td>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+class feat_panel(wx.Panel):
+    def __init__(self, parent, handler):
+
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+        self.root = getRoot(self) #a 1.5002
+        #tempTitle= 'Feats - ' + self.root.general.charName #a 1.5010
+        #pname = handler.master_dom.setAttribute("name", tempTitle) #a 1.5010
+        pname = handler.master_dom.setAttribute("name", 'Feats') #d 1.5010
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Feat"), 0, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Feat"), 0, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+        self.sizer = sizer
+
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),3,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"Feat")
+        self.grid.SetColLabelValue(1,"Type")
+        self.grid.SetColLabelValue(2,"Reference") #m 1.6 typo correction.
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def refresh_row(self,i):
+        feat = self.n_list[i]
+
+        name = feat.getAttribute('name')
+        type = feat.getAttribute('type')
+        desc = feat.getAttribute('desc') #m 1.6 correct typo
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,type)
+        self.grid.SetReadOnly(i,1)
+        self.grid.SetCellValue(i,2,desc) #m 1.6 correct typo
+        self.grid.SetReadOnly(i,2)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd3e"]+"dnd3efeats.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('feat')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name') + "  -  [" +
+                     f.getAttribute('type') + "]  -  " + f.getAttribute('desc'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Feat','Feats',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+
+class dnd3ecombat(dnd3e_char_child):
+    """ Node handler for a dnd3e charactor
+        <nodehandler name='?'  module='dnd3e' class='dnd3echar_handler2'  />
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+
+        node_handler.__init__(self,xml_dom,tree_node)
+
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5012
+
+
+
+        #mark3
+        dnd3e_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('hp','Hit Points',dnd3ehp,'gear')
+        self.new_child_handler('attacks','Attacks',dnd3eattacks,'spears')
+        self.new_child_handler('ac','Armor',dnd3earmor,'spears')
+        #print "combat",self.child_handlers #a (debug) 1.5002
+        #wxMenuItem(self.tree.std_menu, dnd3e_EXPORT, "Export...", "Export")
+        self.myeditor = None
+
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+
+class combat_char_child(node_handler):
+    """ Node Handler for combat.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+class dnd3ehp(combat_char_child):
+    """ Node Handler for hit points.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        combat_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.6009
+        self.root.hp = self #a 1.6009
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,hp_panel,"Hit Points")
+        wnd.title = "Hit Points"
+        return wnd
+
+    def on_rclick( self, evt ):
+        chp = self.master_dom.getAttribute('current')
+        mhp = self.master_dom.getAttribute('max')
+        txt = '((HP: %s / %s))' % ( chp, mhp )
+        self.chat.ParsePost( txt, True, True )
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 >"
+        html_str += "<tr BGCOLOR=#E9E9E9 ><th colspan=4>Hit Points</th></tr>"
+        html_str += "<tr><th>Max:</th>"
+        html_str += "<td>"+self.master_dom.getAttribute('max')+"</td>"
+        html_str += "<th>Current:</th>"
+        html_str += "<td>"+self.master_dom.getAttribute('current')+"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+class hp_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.hparent = handler #a 1.5002 allow ability to run up tree.  In this
+        #a 1.5002 case, we need the functional parent, not the invoking parent.
+
+        pname = handler.master_dom.setAttribute("name", 'HitPoints')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+        self.sizer.AddMany([ (wx.StaticText(self, -1, "HP Current:"),   0,
+           wx.ALIGN_CENTER_VERTICAL),
+          (wx.TextCtrl(self, HP_CUR,
+           self.master_dom.getAttribute('current')),   0, wx.EXPAND),
+          (wx.StaticText(self, -1, "HP Max:"), 0, wx.ALIGN_CENTER_VERTICAL),
+          (wx.TextCtrl(self, HP_MAX, self.master_dom.getAttribute('max')),
+           0, wx.EXPAND),
+         ])
+        self.sizer.AddGrowableCol(1)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=HP_MAX)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=HP_CUR)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == HP_CUR:
+            self.master_dom.setAttribute('current',evt.GetString())
+        elif id == HP_MAX:
+            self.master_dom.setAttribute('max',evt.GetString())
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+class dnd3eattacks(combat_char_child):
+    """ Node Handler for attacks.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        combat_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.attacks = self #a 1.6009 so others can find me.
+        self.mrFrame = [] #a 1.9001
+
+        #a 1.5012 start a1b
+
+        self.updateFootNotes = False
+        self.updateFootNotes = False
+        self.html_str = "<html><body>"
+        self.html_str += ("<br>  This character has weapons with no "+
+             "footnotes.  This program will "+
+             "add footnotes to the weapons which have names that still match "+
+             "the orginal names.  If you have changed the weapon name, you "+
+             "will see some weapons with a footnote of 'X', you will have "+
+             "to either delete and re-add the weapon, or research "+
+             "and add the correct footnotes for the weapon.\n"+
+             "<br>  Please be aware, that only the bow/sling footnote is "+
+             "being used to affect changes to rolls; implemenation of other "+
+             "footnotes to automaticly adjust rolls will be completed as "+
+             "soon as time allows." +
+             "<br><br>Update to character:"+self.root.general.charName+
+             "<br><br>"+
+             """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 >
+              <th width='80%'>Weapon Name</th><th>Added Footnote</th></tr>\n""")
+        self.temp_dom={}
+        #a 1.5012 end a1b
+
+        node_list = self.master_dom.getElementsByTagName('melee')
+        self.melee = node_list[0]
+        node_list = self.master_dom.getElementsByTagName('ranged')
+        self.ranged = node_list[0]
+        self.refresh_weapons() # this causes self.weapons to be loaded.
+
+        #a 1.5012 this whole if clause.
+        if self.updateFootNotes == True:
+            self.updateFootNotes = False
+            name = self.root.general.charName
+            self.html_str +=  "</table>"
+            self.html_str +=  "</body> </html> "
+            masterFrame = self.root.frame
+
+            title = name+"'s weapons' update to have footnotes"
+            fnFrame = wx.Frame(masterFrame, -1, title)
+            fnFrame.panel = wx.html.HtmlWindow(fnFrame,-1)
+            fnFrame.panel.SetPage(self.html_str)
+            fnFrame.Show()
+
+        #weaponsH = self.master_dom.getElementsByTagName('attacks')
+        #mark7
+
+    #a 1.9001 this whole method
+    def refreshMRdata(self): # refresh the data in the melee/ranged section
+        # of the attack chart.
+        # count backwards, maintains context despite "removes"
+        for i in range(len(self.mrFrame)-1,-1,-1):   #a 1.9001
+            x = self.mrFrame[i]
+            if x == None:
+                x.refreshMRdata() #a 1.9001
+            else:
+                self.mrFrame.remove(x)
+
+    def refresh_weapons(self):
+        self.weapons = {}
+
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('weapon')
+        for n in node_list:
+            name = n.getAttribute('name')
+            fn = safeGetAttr(n,'fn') #a 1.5012 can be removed when
+            #a 1.5012 confident all characters in the world have footnotes.
+            #if self.updateFootNotes:
+            if fn == None:#a 1.5012
+                self.updateFootNotes=True
+                self.updateFootN(n) #a 1.5012
+            new_tree_node = tree.AppendItem(
+                self.mytree_node,name,icons['sword'],icons['sword'])
+            tree.SetPyData(new_tree_node,self)
+            self.weapons[name]=n
+
+    def updateFootN(self,n):#a 1.5012 this whole function
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd3e"]+"dnd3eweapons.xml","r")
+            #tmp = open("c:\clh\codeSamples\sample1.xml","r") #a (debug) 1.5012
+            self.temp_dom = xml.dom.minidom.parse(tmp)
+
+            #self.temp_dom = parseXml_with_dlg(self,tmp.read())
+            self.temp_dom = self.temp_dom._get_firstChild()
+            tmp.close()
+        nameF = n.getAttribute('name')
+        w_list = self.temp_dom.getElementsByTagName('weapon')
+        found = False
+        for w in w_list:
+            if nameF == w.getAttribute('name'):
+                found = True
+                fnN = safeGetAttr(n,'fn')
+                if fnN == None or fnN == 'None':
+                    fnW = w.getAttribute('fn')
+                    #print "weapon",nameF,"footnotes are updated to",fnW
+                    self.html_str += ("<tr ALIGN='center'><td>"+nameF+"</td>"+
+                                     "<td>"+fnW+"</td></tr>\n")
+                    n.setAttribute('fn',fnW)
+                break
+        if not found:
+            self.html_str += ("<tr ALIGN='center'><td>"+nameF+" - Custom "+
+              "Weapon, research "+
+              "and update manually; setting footnote to indicate custom</td>"+
+                                     "<td>"+'X'+"</td></tr>\n")
+            n.setAttribute('fn','X')
+
+
+    def get_mod(self,type='m'):
+        (base, base2, base3, base4, base5, base6, stat_mod, misc) \
+            = self.get_attack_data(type)
+        return int(base + misc + int(stat_mod))
+
+    def get_attack_data(self,type='m'):
+        if type=='m' or type=='0':
+            stat = 'Str'  #m was dnd_globals["stats"]['Str'] 1.5002
+            temp = self.melee
+        else:
+            stat = 'Dex'  #m was dnd_globals["stats"]['Dex'] 1.5002
+            temp = self.ranged
+        stat_mod = -7
+        stat_mod = self.root.abilities.get_mod(stat)    #a 1.5002
+        #print "Big test - stat_mod",stat_mod           #a (debug) 1.6000
+        base = int(temp.getAttribute('base'))
+        base2 = int(temp.getAttribute('second'))
+        base3 = int(temp.getAttribute('third'))
+        base4 = int(temp.getAttribute('forth'))
+        base5 = int(temp.getAttribute('fifth'))
+        base6 = int(temp.getAttribute('sixth'))
+        misc = int(temp.getAttribute('misc'))
+        return (base, base2, base3, base4, base5, base6, stat_mod ,misc)
+
+    def on_rclick(self,evt):
+        item = self.tree.GetSelection()
+
+        name = self.tree.GetItemText(item)
+        if item == self.mytree_node:
+            #print "bail due to FUD"
+            return #a 1.6015
+            #dnd3e_char_child.on_ldclick(self,evt)#d 1.6015
+            #self.frame.add_panel(self.get_design_panel(self.frame.note))
+        else:
+            #print "entering attack phase"
+            mod = int(self.weapons[name].getAttribute('mod'))
+            wepMod = mod #a 1.5008
+            footNotes = safeGetAttr(self.weapons[name],'fn','')
+            cat = self.weapons[name].getAttribute('category') #a1.6001
+            result = split(cat,"-",2) #a 1.6001
+            if len(result) < 2: #a 1.6021 this if & else
+                print "warning: 1.6002 unable to interpret weapon category"
+                print "format 'type weapon-[Range|Melee]', probably missing"
+                print "the hyphen.  Assuming Melee"
+                print "weapon name: ",name
+                tres="Melee"
+            else:
+                tres=result[1]
+            #print "print FootNotes,tres",footNotes,tres
+            if tres == 'Melee': #a 1.6001   #m 1.6022 use of tres here and...
+            #if self.weapons[name].getAttribute('range') == '0':#d 1.6001
+                rangeOrMelee = 'm' #a 1.5008 code demote for next comment block
+            elif tres == 'Ranged': #m 1.6001 (was just else) #m 1.6022 here
+                rangeOrMelee = 'r' #a 1.5008
+            else:#a 1.6001 add this whole else clause.
+                print "warning: 1.6001 unable to interpret weapon category"
+                print "treating weapon as Melee, please correct xml"
+                print "weapon name:",name
+                rangeOrMelee ='m'
+            mod = mod + self.get_mod(rangeOrMelee) #a 1.5008
+            chat = self.chat
+            dmg = self.weapons[name].getAttribute('damage')
+
+            #a 1.6003 start code fix instance a
+            result = split(dmg,"/",2)
+            dmg = result[0]
+            #print "1.6003 check:dmg",dmg,";result",result
+            #o currently, only picking out dmg; rest are simply ignored.
+            #o May be usefull
+            #o later for two weapon attack correction.
+            #a 1.6003 end code fix instance a
+
+            monkLvl = self.root.classes.get_class_lvl('Monk') # a 1.5002
+            #print "monkLvl",monkLvl #a (debug) 1.5002
+            # monkLvl = dnd_globals["class"]["lvl"] #d 1.5002
+            if dmg == "Monk Med":
+                if monkLvl == None:     #a 1.5009
+                    txt = 'Attempting to use monk attack, but has no monk '
+                    txt += 'levels, please choose a different attack.'
+                    chat.ParsePost( txt, True, True ) #a 1.5009
+                    return #a 1.5009
+                else:   #a 1.5009
+                    lvl=int(monkLvl)
+                    if lvl <= 3:     #m 1.6022 reversed the order of checks.
+                        dmg = "1d6"
+                    elif lvl <= 7:
+                        dmg = "1d8"
+                    elif lvl <= 11:
+                        dmg = "1d10"
+                    elif lvl <= 15:
+                        dmg = "2d6"
+                    elif lvl <= 19:
+                        dmg = "2d8"
+                    elif lvl <= 20:
+                        dmg = "2d10"
+            if dmg == "Monk Small":
+                if monkLvl == None:     #a 1.5009
+                    txt = 'Attempting to use monk attack, but has no monk '
+                    txt += 'levels, please choose a different attack.'
+                    chat.ParsePost( txt, True, True ) #a 1.5009
+                    return #a 1.5009
+                else:   #a 1.5009
+                    lvl=int(monkLvl)
+                    if lvl <= 3:     #m 1.6022 reversed the order of the checks
+                        dmg = "1d4"
+                    elif lvl <= 7:
+                        dmg = "1d6"
+                    elif lvl <= 11:
+                        dmg = "1d8"
+                    elif lvl <= 15:
+                        dmg = "1d10"
+                    elif lvl <= 20:
+                        dmg = "2d6"
+            flu = ''
+            #print "adjusted weapon damage is:",dmg
+            #o 1.5007 str bow
+            #o 1.5011 start looking about here str dam bonus missed for thrown?
+            #o 1.5012 start looking about here str penalty missed for bow/sling?
+            #o 1.5013 off-hand attacks.? dam and all affects?
+            str_mod = self.root.abilities.get_mod('Str') #a 1.5007,11,12,13
+            if rangeOrMelee == 'r':                     #a 1.5008
+                #if off_hand == True then stat_mod = stat_mod/2 #o 1.5013
+                #c 1.5007 ranged weapons normally get no str mod
+                if find(footNotes,'b') > -1:#a 1.5012 if it's a bow
+                    if str_mod >= 0:        #a 1.5012 never a str bonus
+                        str_mod = 0         #a 1.5012 penalty,
+                else:                       #a 1.5012 if appropriate
+                    str_mod = 0
+                #  c 1.5007 (must adjust for str bows later and thown weapons)
+                #o 1.5007 include + for str bows
+                #o 1.5012 include any str penalty for bows/slings.
+            mod2 = ""                                   #a 1.5007,11-13
+            if str_mod >= 0: #1.6 tidy up code.
+                mod2 = "+"   #1.6 tidy up code.
+            aStrengthMod = mod2 + str(str_mod) #a 1.5008 applicable strength mod
+
+            #if name == "Flurry of Blows(Monk Med)": #d 1.6012
+            if find(name ,"Flurry of Blows") > -1: #a 1.6012
+                flu = '-2'
+
+            (base, base2, base3, base4, base5, base6, stat_mod, misc)\
+                = self.get_attack_data(rangeOrMelee)  #a 1.5008
+            self.sendRoll(base ,stat_mod,misc,wepMod,name,flu,dmg,
+                aStrengthMod,'',True,rollAnyWay=True)
+            if flu != '':
+                self.sendRoll(base ,stat_mod,misc,wepMod,name,flu,dmg,
+                    aStrengthMod) #a 1.6021
+
+            self.sendRoll(base2,stat_mod,misc,wepMod,name,flu,dmg,aStrengthMod)
+            self.sendRoll(base3,stat_mod,misc,wepMod,name,flu,dmg,aStrengthMod)
+            self.sendRoll(base4,stat_mod,misc,wepMod,name,flu,dmg,aStrengthMod)
+            self.sendRoll(base5,stat_mod,misc,wepMod,name,flu,dmg,aStrengthMod)
+            self.sendRoll(base6,stat_mod,misc,wepMod,name,flu,dmg,aStrengthMod)
+
+
+    def sendRoll(self,base,stat_mod,misc,wepMod,name,flu,dmg,aStrengthMod,
+        spacer="",pname=False,rollAnyWay=False):
+        if base != 0 or rollAnyWay:
+            base = base + int(stat_mod) + misc + wepMod #m 1.5008
+            if base >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            txt = '%s ' % (spacer)
+            txt += '%s Attack Roll: <b>[1d20%s%s%s]</b>' % (name, mod1, base, flu)
+            txt += ' ===> Damage: <b>[%s%s]</b>' % (dmg, aStrengthMod)
+            self.chat.ParsePost( txt, True, True )
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,attack_panel,"Attacks")
+        wnd.title = "Attacks"
+        return wnd
+
+    def tohtml(self):
+        melee = self.get_attack_data('m')
+        ranged = self.get_attack_data('r')
+        html_str = ("""<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >"""+
+          "<th>Attack</th><th>Total</th><th >Base</th>"+
+          "<th>Abil</th><th>Misc</th></tr>")
+        html_str += "<tr ALIGN='center' ><th >Melee:</th>"
+        html_str += "<td>"+str(melee[0]+melee[1]+melee[2])+"</td>"
+        html_str += "<td>"+str(melee[0])+"</td>"
+        html_str += "<td>"+str(melee[1])+"</td>"
+        html_str += "<td>"+str(melee[2])+"</td></tr>"
+
+        html_str += "<tr ALIGN='center' ><th >Ranged:</th>"
+        html_str += "<td>"+str(ranged[0]+ranged[1]+ranged[2])+"</td>"
+        html_str += "<td>"+str(ranged[0])+"</td>"
+        html_str += "<td>"+str(ranged[1])+"</td>"
+        html_str += "<td>"+str(ranged[2])+"</td></tr></table>"
+
+        n_list = self.master_dom.getElementsByTagName('weapon')
+        for n in n_list:
+            mod = n.getAttribute('mod')
+            if mod >= 0:
+                mod1 = "+"
+            else:
+                mod1 = ""
+            ran = n.getAttribute('range')
+            total = str(int(mod) + self.get_mod(ran))
+            html_str += """<P><table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >
+                    <th colspan=2>Weapon</th>
+                    <th>Attack</th><th >Damage</th><th>Critical</th></tr>"""
+            html_str += "<tr ALIGN='center' ><td  colspan=2>"
+            html_str += n.getAttribute('name')+"</td><td>"+total+"</td>"
+            html_str += "<td>"+n.getAttribute('damage')+"</td><td>"
+            html_str += n.getAttribute('critical')+"</td></tr>"
+            html_str += """<tr BGCOLOR=#E9E9E9 ><th>Range</th><th>Weight</th>
+                        <th>Type</th><th>Size</th><th>Misc Mod</th></tr>"""
+            html_str += "<tr ALIGN='center'><td>"+ran+"</td><td>"
+            html_str += n.getAttribute('weight')+"</td>"
+            html_str += "<td>"+n.getAttribute('type')+"</td><td>"
+            html_str += n.getAttribute('size')+"</td>"
+            html_str += '<td>%s%s</td></tr>'  % (mod1, mod)
+            #a 1.5012 add next two lines to pretty print footnotes.
+            html_str += """<tr><th BGCOLOR=#E9E9E9 colspan=2>Footnotes:</th>"""
+            html_str += "<th colspan=3>"+safeGetAttr(n,'fn','')+"</th></tr>"
+            html_str += '</table>'
+        return html_str
+
+class attack_grid(wx.grid.Grid):
+    """grid for attacks"""
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Melee')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 we need the functional parent, not the invoking parent.
+
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+
+        self.root = getRoot(self) #a 1.9001
+        self.parent = parent
+        self.handler = handler
+        self.rows = (self.handler.melee,self.handler.ranged)
+        self.CreateGrid(2,10)
+        self.SetRowLabelSize(0)
+        col_names = ['Type','base','base 2','base 3','base 4','base 5',
+            'base 6','abil','misc','Total']
+        for i in range(len(col_names)):
+            self.SetColLabelValue(i,col_names[i])
+        self.SetCellValue(0,0,"Melee")
+        self.SetCellValue(1,0,"Ranged")
+        self.refresh_data()
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        #print "looking for containing frame"
+
+        #a 1.9001 remainder of code in this method.
+        climber = parent
+        nameNode = climber.GetClassName()
+        while nameNode != 'wxFrame':
+            climber = climber.parent
+            nameNode = climber.GetClassName()
+        masterFrame=climber
+        masterFrame.refreshMRdata=self.refresh_data
+        #print getmembers(masterFrame)
+
+        handler.mrFrame.append(masterFrame)
+
+        #print "masterFrame=",masterFrame
+        #print "mr.Show=",masterFrame.Show()
+        #print "mf.GetName",masterFrame.GetName()
+        #print "mf.GetClassName",masterFrame.GetClassName()
+        #print "mf.GetId",masterFrame.GetId()
+        #print "mf.GetLabel",masterFrame.GetLabel()
+        #print "mf.GetHandle",masterFrame.GetHandle()
+        #print "mf.GetParent",masterFrame.GetParent()
+        # here, GetParent consistent returns the master frame of the app.
+        #print "mf.GetGParent",masterFrame.GetGrandParent() #here, always None
+        #print "mf.GetTitle",masterFrame.GetTitle()
+        #print "mf.IsEnabled",masterFrame.IsEnabled()
+        #print "mf.IsShown",masterFrame.IsShown()
+        #print "mf.IsTopLevel",masterFrame.IsTopLevel()
+        #print "self.frame=",self.frame
+
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        try:
+            int(value)
+            if col==1:
+                self.rows[row].setAttribute('base',value)
+            elif col==2:
+                self.rows[row].setAttribute('second',value)
+            elif col==3:
+                self.rows[row].setAttribute('third',value)
+            elif col==4:
+                self.rows[row].setAttribute('forth',value)
+            elif col==5:
+                self.rows[row].setAttribute('fifth',value)
+            elif col==6:
+                self.rows[row].setAttribute('sixth',value)
+            elif col==8:
+                self.rows[row].setAttribute('misc',value)
+                #print "row:",row,"value",value,self.rows[row]
+            self.parent.refresh_data()
+        except:
+            self.SetCellValue(row,col,"0")
+
+    def refresh_data(self):
+
+        melee = self.handler.get_attack_data('m')
+        ranged = self.handler.get_attack_data('r')
+        tmelee = int(melee[0]) + int(melee[6]) + int(melee[7])
+        tranged = int(ranged[0]) + int(ranged[6]) + int(ranged[7])
+        # for i in range(0,7):  #d 1.5005
+        for i in range(0,8):    #a 1.5005
+            self.SetCellValue(0,i+1,str(melee[i]))
+            self.SetCellValue(1,i+1,str(ranged[i]))
+        self.SetCellValue(0,9,str(tmelee))
+        self.SetCellValue(1,9,str(tranged))
+        self.SetReadOnly(0,0)
+        self.SetReadOnly(1,0)
+        self.SetReadOnly(0,7)
+        self.SetReadOnly(1,7)
+        self.SetReadOnly(0,9)
+        self.SetReadOnly(1,9)
+
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols+1)
+        self.SetColSize(0,col_w*2)
+        for i in range(1,cols):
+            self.SetColSize(i,col_w)
+        evt.Skip()
+        self.Refresh()
+
+class weapon_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        self.hparent = handler                          #a 1.5012
+        self.root = getRoot(self)
+
+        pname = handler.master_dom.setAttribute("name", 'Weapons')
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer2.Add(wx.Button(self, 10, "Remove Weapon"), 0, wx.EXPAND)
+        sizer2.Add(wx.Size(10,10))
+        sizer2.Add(wx.Button(self, 20, "Add Weapon"), 0, wx.EXPAND)
+
+        sizer.Add(sizer2, 0, wx.EXPAND)
+        sizer.Add(wx.StaticText(self, -1, "Right click a weapon's footnote to see what the footnotes mean."),0, wx.EXPAND)#a 1.5012
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.sizer2 = sizer2
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.on_gridRclick)#a 1.5012
+
+        n_list = handler.master_dom.getElementsByTagName('weapon')
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.handler = handler
+        #trash=input("weapon panel init colnames")
+        self.colAttr = ['name','damage','mod','critical','type','weight',
+                    'range','size','Total','fn',    'comment'] #a 1.5012
+        col_names = ['Name','Damage','To hit\nmod','Critical','Type','Weight',
+                    'Range','Size','Total','Foot\nnotes','Comment'] #a 1.5012
+        gridColCount=len(col_names)#a 1.5012
+        self.grid.CreateGrid(len(n_list),gridColCount,1) #a 1.5012
+        #self.grid.CreateGrid(len(n_list),9,1) #d 1.5012
+        self.grid.SetRowLabelSize(0)
+        #col_names = ['Name','damage','mod','critical','type','weight','range',             'size','Total'] #d 1.5012
+        #for i in range(len(col_names)):   #d 1.5012
+        for i in range(gridColCount): #a 1.5012
+            self.grid.SetColLabelValue(i,col_names[i])
+        self.refresh_data()
+        self.temp_dom = None
+
+
+    #mark4
+    #a 1.5012 add entire method.
+    def on_gridRclick(self,evt):
+        #print "weapon_panel, on_rclick: self,evt",self,evt
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        #print "wp, on rclick,grid row,col,value",row,col,value
+        if col == 9 and value != 'None':
+            n = self.n_list[row]
+            name = n.getAttribute('name')
+            #print "we want a panel!"
+            handler = self.hparent
+            #print "handler:",handler
+            # A handler is a node, and nodes have a reference to
+            # the master frame
+            masterFrame = handler.frame
+            #print "masterFrame:",masterFrame
+            title = name+"'s Special Weapon Characteristics"
+            fnFrame = wx.Frame(masterFrame, -1, title)
+            fnFrame.panel = wx.html.HtmlWindow(fnFrame,-1)
+            if not self.temp_dom:
+                tmp = open(orpg.dirpath.dir_struct["dnd3e"]+
+                            "dnd3eweapons.xml","r")
+                #tmp = open("c:\clh\codeSamples\sample1.xml","r")
+                xml_dom = parseXml_with_dlg(self,tmp.read())
+                xml_dom = xml_dom._get_firstChild()
+                tmp.close()
+                self.temp_dom = xml_dom
+            f_list = self.temp_dom.getElementsByTagName('f') # the footnotes
+            #print "weapon_panel - on_rclick f_list",f_list#a 1.6
+            n = self.n_list[row]
+            name = n.getAttribute('name')
+            footnotes = n.getAttribute('fn')
+            html_str = "<html><body>"
+            html_str += """<table border='1' width=100% ><tr BGCOLOR=#E9E9E9 >
+                        <th width='10%'>Note</th><th>Description</th></tr>\n"""
+            #print "rclick,name,fn",name,footnotes
+            if footnotes == "":
+                html_str += "<tr ALIGN='center'><td></td>"
+                html_str += "  <td>This weapon has no footnotes</td></tr>"
+            for i in range(len(footnotes)):
+                aNote=footnotes[i]
+                found=False
+                for f in f_list:
+                    if f.getAttribute('mark') == aNote:
+                        found=True
+                        text=f.getAttribute('txt')
+                        html_str += ("<tr ALIGN='center'><td>"+aNote+"</td>"+
+                                     "<td>"+text+"</td></tr>\n")
+                if not found:
+                    html_str += ("<tr ALIGN='center'><td>"+aNote+"</td>"+
+                       "<td>is not a recognized footnote</td></tr>\n")
+
+            html_str +=  "</table>"
+            html_str +=  "</body> </html> "
+
+            #print html_str
+            fnFrame.panel.SetPage(html_str)
+            fnFrame.Show()
+            return
+        pass
+
+
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col == 2 and not int(value): # special case for mod, demoted
+            value = "0" #a 5.012 demoted
+            self.n_list[row].setAttribute('mod',value) # a 5.012 demoted
+        if not (col == 9 and value == "None" and
+                self.n_list[row].getAttribute('fn') == "None"
+                ): #a 5.012 special case for footnotes
+            self.n_list[row].setAttribute(self.colAttr[col],value)#a 5.012
+        #print "cell change",row,col,value
+        #if col == 0:#d 5.012 use of colAttr removed need for this.
+        #    self.n_list[row].setAttribute('name',value) #d 5.012
+        #elif col == 2: #d 5.012
+        #    try:#d 5.012 simplifying... remove this block.
+        #        int(value)
+        #        self.n_list[row].setAttribute('mod',value)
+        #        #self.refresh_row(row) #d 5.012 did nothing.
+        #    except:
+        #       value = "0"
+        #       self.n_list[row].setAttribute('mod',value)
+        #else: #d 5.012 demoted self.n set.
+        #   self.n_list[row].setAttribute(self.grid.GetColLabelValue(col),value)
+
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+        fn = n.getAttribute('fn')
+        #print "fn=",fn
+        name = n.getAttribute('name')
+        mod = n.getAttribute('mod')
+        ran = n.getAttribute('range')
+        total = str(int(mod) + self.handler.get_mod(ran))
+        self.grid.SetCellValue(i,0,name)
+        self.grid.SetCellValue(i,1,n.getAttribute('damage'))
+        self.grid.SetCellValue(i,2,mod)
+        self.grid.SetCellValue(i,3,n.getAttribute('critical'))
+        self.grid.SetCellValue(i,4,n.getAttribute('type'))
+        self.grid.SetCellValue(i,5,n.getAttribute('weight'))
+        self.grid.SetCellValue(i,6,ran)
+        self.grid.SetCellValue(i,7,n.getAttribute('size') )
+        self.grid.SetCellValue(i,8,total)
+        self.grid.SetCellValue(i,9,safeGetAttr(n,'fn','None')) #a 1.5012
+        self.grid.SetCellValue(i,10,safeGetAttr(n,'comment','')) #a 1.5012
+        #fn=safeGetAttr(n,'fn','None') #a (debug) 1.5012
+        #print "fn ",fn,"<" #a (debug) 1.5012
+        #o 1.5012 original damage vs what someone has changed it to.
+
+        self.grid.SetReadOnly(i,8)
+
+    def on_remove(self,evt): #o 1.6011 correcting wrongful deletion
+        rows = self.grid.GetNumberRows()
+        #for i in range(rows):          #d 1.6011 do it backwards,
+        for i in range(rows-1,-1,-1):   #a 1.6011 or you lose context
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.n_list = self.master_dom.getElementsByTagName('weapon')
+                self.handler.refresh_weapons()
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd3e"]+"dnd3eweapons.xml","r")
+            #tmp = open("c:\clh\codeSamples\sample1.xml","r") #a (debug) 1.5012
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('weapon')
+        opts = []
+        #print "weapon_panel - on_add f_list",f_list#a 1.6
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Weapon','Weapon List',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            #print f_list[i] # DOM Element: weapon.
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            #print self.grid.AppendRows # a bound method of wxGrid
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('weapon')
+            #print "self.n_list",self.n_list # list of DOM weapons
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_weapons()
+        dlg.Destroy()
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-40)
+        self.sizer.SetDimension(0,s[1]-40,s[0],25)
+        self.sizer2.SetDimension(0,s[1]-15,s[0],15)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols+1)
+        self.grid.SetColSize(0,col_w*2)
+        for i in range(1,cols):
+            self.grid.SetColSize(i,col_w)
+
+    def refresh_data(self):
+
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+
+class attack_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Melee')
+        self.parent = parent #a 1.9001
+
+        wx.Panel.__init__(self, parent, -1)
+
+        self.a_grid = attack_grid(self, handler)
+        self.w_panel = weapon_panel(self, handler)
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.sizer.Add(self.a_grid, 1, wx.EXPAND)
+        self.sizer.Add(self.w_panel, 2, wx.EXPAND)
+        self.Bind(wx.EVT_SIZE, self.on_size)
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+    def refresh_data(self):
+
+        self.w_panel.refresh_data()
+        self.a_grid.refresh_data()
+
+
+class dnd3earmor(combat_char_child):
+    """ Node Handler for ac.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        combat_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.ac = self #a 1.6009
+
+    def get_spell_failure(self):
+        return self.get_total('spellfailure')
+
+    def get_total_weight(self):
+        return self.get_total('weight')
+
+    def get_check_pen(self):
+        return self.get_total('checkpenalty')
+
+    def get_armor_class(self):
+        ac_total = 10
+
+        ac_total += self.get_total('bonus')
+        #m 1.5009 change to hardcode dex, was incorrect gv "stat"
+        dex_mod = self.root.abilities.get_mod('Dex')#m 1.5009 hardcode dex
+        max_dex = self.get_max_dex()
+        if dex_mod < max_dex:
+            ac_total += dex_mod
+        else:
+            ac_total += max_dex
+        return ac_total
+
+    def get_max_dex(self):
+        armor_list = self.master_dom.getElementsByTagName('armor')
+        dex = 10
+        for a in armor_list:
+            temp = int(a.getAttribute("maxdex"))
+            if temp < dex:
+                dex = temp
+        return dex
+
+    def get_total(self,attr):
+        armor_list = self.master_dom.getElementsByTagName('armor')
+        total = 0
+        for a in armor_list:
+            total += int(a.getAttribute(attr))
+        return total
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,ac_panel,"Armor")
+        wnd.title = "Armor"
+        return wnd
+
+    def on_rclick( self, evt ):
+        ac = self.get_armor_class()
+        fac = (int(ac)-(self.root.abilities.get_mod('Dex')))
+
+        txt = '((AC: %s Normal, %s Flatfoot))' % ( ac, fac ) #a 1.5002
+        self.chat.ParsePost( txt, True, True )
+
+    def tohtml(self):
+        html_str = """<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >
+            <th>AC</th><th>Check Penalty</th><th >Spell Failure</th>
+            <th>Max Dex</th><th>Total Weight</th></tr>"""
+        html_str += "<tr ALIGN='center' >"
+        html_str += "<td>"+str(self.get_armor_class())+"</td>"
+        html_str += "<td>"+str(self.get_check_pen())+"</td>"
+        html_str += "<td>"+str(self.get_spell_failure())+"</td>"
+        html_str += "<td>"+str(self.get_max_dex())+"</td>"
+        html_str += "<td>"+str(self.get_total_weight())+"</td></tr></table>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += """<P><table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >
+                <th colspan=3>Armor</th><th>Type</th><th >Bonus</th></tr>"""
+            html_str += "<tr ALIGN='center' >"
+            html_str += "<td  colspan=3>"+n.getAttribute('name')+"</td>"
+            html_str += "<td>"+n.getAttribute('type')+"</td>"
+            html_str += "<td>"+n.getAttribute('bonus')+"</td></tr>"
+            html_str += """<tr BGCOLOR=#E9E9E9 >"""
+            html_str += "<th>Check Penalty</th><th>Spell Failure</th>"
+            html_str += "<th>Max Dex</th><th>Speed</th><th>Weight</th></tr>"
+            html_str += "<tr ALIGN='center'>"
+            html_str += "<td>"+n.getAttribute('checkpenalty')+"</td>"
+            html_str += "<td>"+n.getAttribute('spellfailure')+"</td>"
+            html_str += "<td>"+n.getAttribute('maxdex')+"</td>"
+            html_str += "<td>"+n.getAttribute('speed')+"</td>"
+            html_str += "<td>"+n.getAttribute('weight')+"</td></tr></table>"
+        return html_str
+
+
+class ac_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Armor')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 we need the functional parent, not the invoking parent.
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Armor"), 1, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Armor"), 1, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.master_dom = handler.master_dom
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        col_names = ['Armor','bonus','maxdex','cp','sf','weight','speed','type']
+        self.grid.CreateGrid(len(n_list),len(col_names),1)
+        self.grid.SetRowLabelSize(0)
+        for i in range(len(col_names)):
+            self.grid.SetColLabelValue(i,col_names[i])
+        self.atts =['name','bonus','maxdex','checkpenalty',
+            'spellfailure','weight','speed','type']
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col >= 1 and col <= 5:
+            try:
+                int(value)
+                self.n_list[row].setAttribute(self.atts[col],value)
+            except:
+                self.grid.SetCellValue(row,col,"0")
+        else:
+            self.n_list[row].setAttribute(self.atts[col],value)
+
+    def refresh_row(self,i):
+        n = self.n_list[i]
+
+        for y in range(len(self.atts)):
+            self.grid.SetCellValue(i,y,n.getAttribute(self.atts[y]))
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd3e"]+"dnd3earmor.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('armor')
+        opts = []
+        for f in f_list:
+            opts.append(f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Armor:','Armor List',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.refresh_row(self.grid.GetNumberRows()-1)
+        dlg.Destroy()
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols+2)
+        self.grid.SetColSize(0,col_w*3)
+        for i in range(1,cols):
+            self.grid.SetColSize(i,col_w)
+
+
+class dnd3esnp(dnd3e_char_child):
+    """ Node Handler for power points.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        dnd3e_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+
+
+        self.frame = open_rpg.get_component('frame')
+        self.child_handlers = {}
+        self.new_child_handler('spells','Spells',dnd3espells,'book')
+        self.new_child_handler('divine','Divine Spells',dnd3edivine,'book')
+        self.new_child_handler('powers','Powers',dnd3epowers,'book')
+        self.new_child_handler('pp','Power Points',dnd3epp,'gear')
+        self.myeditor = None
+
+    def new_child_handler(self,tag,text,handler_class,icon='gear'):
+        node_list = self.master_dom.getElementsByTagName(tag)
+        tree = self.tree
+        i = self.tree.icons[icon]
+        new_tree_node = tree.AppendItem(self.mytree_node,text,i,i)
+        handler = handler_class(node_list[0],new_tree_node,self)
+        tree.SetPyData(new_tree_node,handler)
+        self.child_handlers[tag] = handler
+
+    def get_design_panel(self,parent):
+        return tabbed_panel(parent,self,1)
+
+    def get_use_panel(self,parent):
+        return tabbed_panel(parent,self,2)
+
+#    def set_char_pp(self,attr,evl):     #d 1.5002 doesn't seem to be used, but
+#        dnd_globals["pp"][attr] = evl  #d 1.5002 uses dnd_globals, so tossing.
+
+
+#    def get_char_pp( self, attr ):     #d 1.5002 doesn't seem to be used, but
+#        return dnd_globals["pp"][attr]     #d 1.5002 doesn't seem to be used, but
+
+class snp_char_child(node_handler):
+    """ Node Handler for skill.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.char_hander = parent
+        self.drag = False
+        self.frame = open_rpg.get_component('frame')
+        self.myeditor = None
+
+
+
+    def on_drop(self,evt):
+        pass
+
+    def on_rclick(self,evt):
+        pass
+
+    def on_ldclick(self,evt):
+        return
+
+    def on_html(self,evt):
+        html_str = self.tohtml()
+        wnd = http_html_window(self.frame.note,-1)
+        wnd.title = self.master_dom.getAttribute('name')
+        self.frame.add_panel(wnd)
+        wnd.SetPage(html_str)
+
+    def get_design_panel(self,parent):
+        pass
+
+    def get_use_panel(self,parent):
+        return self.get_design_panel(parent)
+
+    def delete(self):
+        pass
+
+
+class dnd3espells(snp_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        snp_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.spells = self #a 1.6009
+
+
+        node_list = self.master_dom.getElementsByTagName( 'spell' )
+        self.spells = {}
+        tree = self.tree
+        icons = self.tree.icons
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.spells[ name ] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['gear'], icons['gear'] )
+            tree.SetPyData( new_tree_node, self )
+
+    def on_rclick( self, evt ):
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        if item == self.mytree_node:
+            dnd3e_char_child.on_ldclick( self, evt )
+        else:
+            level = self.spells[ name ].getAttribute( 'level' )
+            descr = self.spells[ name ].getAttribute( 'desc' )
+            use = self.spells[ name ].getAttribute( 'used' )
+            memrz = self.spells[ name ].getAttribute( 'memrz' )
+            use += '+1'
+            charNameL=self.root.general.charName #a  1.5002
+            left = eval( '%s - ( %s )' % ( memrz, use ) )
+            if left < 0:
+                txt = '%s Tried to cast %s but has used all of them for today,'
+                #txt +='"Please rest so I can cast more."' % ( dnd_globals["gen"]["Name"], name )##d 1.5002
+                txt +='"Please rest so I can cast more."' % ( charNameL, name ) #a 1.5002
+                self.chat.ParsePost( txt, True, False )
+            else:
+                #txt = '%s casts %s ( level %s, "%s" )' % ( dnd_globals["gen"]["Name"], name, level, descr )#d 1.5002
+                txt = '%s casts %s ( level %s, "%s" )' % ( charNameL, name, level, descr )#a f 1.5002
+                self.chat.ParsePost( txt, True, False )
+                s = ''
+                if left != 1:
+                    s = 's'
+                #txt = '%s can cast %s %d more time%s' % ( dnd_globals["gen"]["Name"], name, left, s )#d 1.5002
+                txt = '%s can cast %s %d more time%s' % ( charNameL, name, left, s ) #a 1.5002
+                self.chat.ParsePost( txt, False, False )
+                self.spells[ name ].setAttribute( 'used', `eval( use )` )
+
+    def refresh_spells(self):
+        self.spells = {}
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('spell')
+        for n in node_list:
+            name = n.getAttribute('name')
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+            self.spells[name]=n
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,spells_panel,"Spells")
+        wnd.title = "Spells"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Arcane Spells</th></tr><tr><td><br>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += "(" + n.getAttribute('level') + ") " + n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        return self.char_hander.get_char_lvl(attr)
+
+class spells_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Arcane Spells')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Spell"), 1, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Spell"), 1, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 30, "Refresh Spells"), 1, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.Bind(wx.EVT_BUTTON, self.on_refresh_spells, id=30)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),4,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"No.")
+        self.grid.SetColLabelValue(1,"Lvl")
+        self.grid.SetColLabelValue(2,"Spell")
+        self.grid.SetColLabelValue(3,"Desc")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col == 0:
+            self.n_list[row].setAttribute('memrz',value)
+
+
+    def refresh_row(self,i):
+        spell = self.n_list[i]
+
+        memrz = spell.getAttribute('memrz')
+        name = spell.getAttribute('name')
+        type = spell.getAttribute('desc')
+        level = spell.getAttribute('level')
+        self.grid.SetCellValue(i,0,memrz)
+        self.grid.SetCellValue(i,2,name)
+        self.grid.SetReadOnly(i,2)
+        self.grid.SetCellValue(i,3,type)
+        self.grid.SetReadOnly(i,3)
+        self.grid.SetCellValue(i,1,level)
+        self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd3e"]+"dnd3espells.xml","r")
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('spell')
+        opts = []
+        #lvl = int(dnd3e_char_child.get_char_lvl('level'))
+        #castlvl = eval('%s/2' % (lvl))
+        for f in f_list:
+            spelllvl = f.getAttribute('level')
+            #if spelllvl <= "1":
+            #    opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+            #else:
+            #    if eval('%d >= %s' %(castlvl, spelllvl)):
+            opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Spell','Spells',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('spell')
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_spells()
+        dlg.Destroy()
+
+    def on_refresh_spells( self, evt ):
+        f_list = self.master_dom.getElementsByTagName('spell')
+
+        for spell in f_list:
+            spell.setAttribute( 'used', '0' )
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+        self.grid.SetColSize(0,w * 0.10)
+        self.grid.SetColSize(1,w * 0.10)
+        self.grid.SetColSize(2,w * 0.30)
+        self.grid.SetColSize(3,w * 0.50)
+
+    def refresh_data(self):
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+class dnd3edivine(snp_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        snp_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.divine = self #a 1.6009
+
+
+        node_list = self.master_dom.getElementsByTagName( 'gift' )
+        self.spells = {}
+        tree = self.tree
+        icons = self.tree.icons
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.spells[ name ] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name, icons['flask'], icons['flask'] )
+            tree.SetPyData( new_tree_node, self )
+
+    def on_rclick( self, evt ):
+        charNameL=self.root.general.charName #a f 1.5002
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        if item == self.mytree_node:
+            dnd3e_char_child.on_ldclick( self, evt )
+        else:
+            level = self.spells[ name ].getAttribute( 'level' )
+            descr = self.spells[ name ].getAttribute( 'desc' )
+            use = self.spells[ name ].getAttribute( 'used' )
+            memrz = self.spells[ name ].getAttribute( 'memrz' )
+            use += '+1'
+            left = eval( '%s - ( %s )' % ( memrz, use ) )
+            if left < 0:
+                txt = '%s Tried to cast %s but has used all of them for today,' #m 1.5002 break in 2.
+                txt += "Please rest so I can cast more."' % ( charNameL, name )' #a 1.5002
+                #txt += "Please rest so I can cast more."' % ( dnd_globals["gen"]["Name"], name ) #d 1.5002
+                self.chat.ParsePost( txt, True, False )
+            else:
+                #txt = '%s casts %s ( level %s, "%s" )' % ( dnd_globals["gen"]["Name"], name, level, descr ) #d 1.5002
+                txt = '%s casts %s ( level %s, "%s" )' % ( charNameL, name, level, descr ) #a 5002
+                self.chat.ParsePost( txt, True, False )
+                s = ''
+                if left != 1:
+                    s = 's'
+                #txt = '%s can cast %s %d more time%s' % ( dnd_globals["gen"]["Name"], name, left, s ) #d 1.5002
+                txt = '%s can cast %s %d more time%s' % ( charNameL, name, left, s ) #a 1.5002
+                self.chat.ParsePost( txt, False, False )
+                self.spells[ name ].setAttribute( 'used', `eval( use )` )
+
+    def refresh_spells(self):
+        self.spells = {}
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+
+        node_list = self.master_dom.getElementsByTagName('gift')
+        for n in node_list:
+            name = n.getAttribute('name')
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['flask'],icons['flask'])
+            tree.SetPyData(new_tree_node,self)
+            self.spells[name]=n
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,divine_panel,"Spells")
+        wnd.title = "Spells"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Divine Spells</th></tr><tr><td><br>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += "(" + n.getAttribute('level') + ") " + n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+    def get_char_lvl( self, attr ):
+        return self.char_hander.get_char_lvl(attr)
+
+class divine_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        pname = handler.master_dom.setAttribute("name", 'Divine Spells')
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 in this case, we need the functional parent, not the invoking parent.
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid =wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Spell"), 1, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Spell"), 1, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 30, "Refresh Spells"), 1, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.Bind(wx.EVT_BUTTON, self.on_refresh_spells, id=30)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),4,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"No.")
+        self.grid.SetColLabelValue(1,"Lvl")
+        self.grid.SetColLabelValue(2,"Spell")
+        self.grid.SetColLabelValue(3,"Desc")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        if col == 0:
+            self.n_list[row].setAttribute('memrz',value)
+
+
+    def refresh_row(self,i):
+        spell = self.n_list[i]
+
+        memrz = spell.getAttribute('memrz')
+        name = spell.getAttribute('name')
+        type = spell.getAttribute('desc')
+        level = spell.getAttribute('level')
+        self.grid.SetCellValue(i,0,memrz)
+        self.grid.SetCellValue(i,2,name)
+        self.grid.SetReadOnly(i,2)
+        self.grid.SetCellValue(i,3,type)
+        self.grid.SetReadOnly(i,3)
+        self.grid.SetCellValue(i,1,level)
+        self.grid.SetReadOnly(i,1)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd3e"]+"dnd3edivine.xml","r")
+
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('gift')
+        opts = []
+        #lvl = int(dnd3e_char_child.get_char_lvl('level'))
+        #castlvl = lvl / 2
+        for f in f_list:
+            spelllvl = f.getAttribute('level')
+            #if spelllvl <= "1":
+            #    opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+            #else:
+            #    if eval('%d >= %s' %(castlvl, spelllvl)):
+            opts.append("(" + f.getAttribute('level') + ")" + f.getAttribute('name'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Spell','Spells',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('gift')
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_spells()
+        dlg.Destroy()
+
+    def on_refresh_spells( self, evt ):
+        f_list = self.master_dom.getElementsByTagName('gift')
+        for spell in f_list:
+            spell.setAttribute( 'used', '0' )
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+        self.grid.SetColSize(0,w * 0.10)
+        self.grid.SetColSize(1,w * 0.10)
+        self.grid.SetColSize(2,w * 0.30)
+        self.grid.SetColSize(3,w * 0.50)
+
+    def refresh_data(self):
+
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+
+class dnd3epowers(snp_char_child):
+    """ Node Handler for classes.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        snp_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self) #a 1.5002
+        self.root.powers = self #a 1.6009
+
+        node_list = self.master_dom.getElementsByTagName( 'power' )
+        self.powers = {}
+        tree = self.tree
+        icons = self.tree.icons
+        for n in node_list:
+            name = n.getAttribute('name')
+            self.powers[ name ] = n
+            new_tree_node = tree.AppendItem( self.mytree_node, name,
+                                             icons['gear'], icons['gear'] )
+            tree.SetPyData( new_tree_node, self )
+
+    def on_rclick( self, evt ):
+        charNameL = self.root.general.charName                  #a f 1.5002
+
+        item = self.tree.GetSelection()
+        name = self.tree.GetItemText( item )
+        charNameL = self.root.general.charName                  #a 1.5002
+        if item == self.mytree_node:
+            dnd3e_char_child.on_ldclick( self, evt )
+        else:
+            level = int(self.powers[ name ].getAttribute( 'level' ))
+            descr = self.powers[ name ].getAttribute( 'desc' )
+            #use can be removed -mgt
+            #use = self.powers[ name ].getAttribute( 'used' )
+            points = self.powers[ name ].getAttribute( 'point' )
+            #cpp and fre are strings without the eval -mgt
+            cpp = eval(self.root.pp.get_char_pp('current1'))          #a 1.5002
+            fre = eval(self.root.pp.get_char_pp('free'))              #a 1.5002
+            if level == 0 and fre > 0:
+                left = eval('%s - ( %s )' % ( fre, points ))
+                numcast = eval('%s / %s' % (left, points))
+                if left < 0:
+                    #In theory you should never see this -mgt
+                    txt = ('%s doesnt have enough PowerPoints to use %s'
+                        % ( charNameL, name )) #a 1.5002
+                    self.chat.ParsePost( txt, True, False )
+                else:
+                    txt = ('%s uses %s as a Free Talent ( level %s, "%s" )'
+                        % ( charNameL, name, level, descr )) #a 1.5002
+                    self.chat.ParsePost( txt, True, False )
+                    s = ''
+                    if left != 1:
+                        s = 's'
+                    txt = '%s has %d Free Talent%s left' % ( charNameL, numcast, s ) #a 1.5002
+                    self.chat.ParsePost( txt, False, False )
+                    self.root.pp.set_char_pp('free',left)       #a 1.5002
+            else:
+                left = eval('%s - ( %s )' % ( cpp, points ))
+                #numcast = eval('%s / %s' % (left, points))
+                if left < 0:
+                    txt = '%s doesnt have enough PowerPoints to use %s' % ( charNameL, name ) #m 1.5002
+                    self.chat.ParsePost( txt, True, False )
+                else:
+                    txt = '%s uses %s ( level %s, "%s" )' % ( charNameL, name, level, descr ) #m 1.5002
+                    self.chat.ParsePost( txt, True, False )
+                    s = ''
+                    if left != 1:
+                        s = 's'
+                    #numcast is meaningless here -mgt
+                    #txt = '%s can use %s %d more time%s' % ( charNameL, name, numcast, s ) #m 1.5002
+                    #txt += ' - And has %d more PowerpointsP left' % (left)
+                    txt = '%s has %d more Powerpoint%s' % ( charNameL, left, s ) #m 1.5002
+                    self.chat.ParsePost( txt, False, False )
+                    self.root.pp.set_char_pp('current1',left)   #a 1.5002
+
+    def refresh_powers(self):
+        self.powers = {}
+
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('power')
+        for n in node_list:
+            name = n.getAttribute('name')
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            tree.SetPyData(new_tree_node,self)
+            self.powers[name]=n
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,power_panel,"Powers")
+        wnd.title = "Powers"
+        return wnd
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 ><th>Powers</th></tr><tr><td><br>"
+        n_list = self.master_dom._get_childNodes()
+        for n in n_list:
+            html_str += "(" + n.getAttribute('level') + ") " + n.getAttribute('name')+ ", "
+        html_str = html_str[:len(html_str)-2] + "</td></tr></table>"
+        return html_str
+
+
+class power_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        #m 1.5015 corrected typo, was Pionic.
+        pname = handler.master_dom.setAttribute("name", 'Psionic Powers')
+        self.hparent = handler #a 1.5002 allow ability to run up tree. In this
+        #a 1.5002 case, we need the functional parent, not the invoking parent.
+        self.root = getRoot(self)               #a (debug) 1.5002,1.5014
+
+        wx.Panel.__init__(self, parent, -1)
+        self.grid = wx.grid.Grid(self, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.handler = handler
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        sizer.Add(self.grid, 1, wx.EXPAND)
+
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        sizer1.Add(wx.Button(self, 10, "Remove Power"), 1, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 20, "Add Power"), 1, wx.EXPAND)
+        sizer1.Add(wx.Size(10,10))
+        sizer1.Add(wx.Button(self, 30, "Refresh Power"), 1, wx.EXPAND)
+
+        sizer.Add(sizer1, 0, wx.EXPAND)
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+
+        self.Bind(wx.EVT_BUTTON, self.on_remove, id=10)
+        self.Bind(wx.EVT_BUTTON, self.on_add, id=20)
+        self.Bind(wx.EVT_BUTTON, self.on_refresh_powers, id=30)
+        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        n_list = handler.master_dom._get_childNodes()
+        self.n_list = n_list
+        self.master_dom = handler.master_dom
+        self.grid.CreateGrid(len(n_list),5,1)
+        self.grid.SetRowLabelSize(0)
+        self.grid.SetColLabelValue(0,"PP")
+        self.grid.SetColLabelValue(1,"Lvl")
+        self.grid.SetColLabelValue(2,"Power")
+        self.grid.SetColLabelValue(3,"Desc")
+        self.grid.SetColLabelValue(4,"Type")
+        for i in range(len(n_list)):
+            self.refresh_row(i)
+        self.refresh_data()
+        self.temp_dom = None
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.grid.GetCellValue(row,col)
+        """if col == 0:
+            self.n_list[row].setAttribute('memrz',value)"""
+
+
+    def refresh_row(self,i):
+        power = self.n_list[i]
+
+        point = power.getAttribute('point')
+        name = power.getAttribute('name')
+        type = power.getAttribute('desc')
+        test = power.getAttribute('test')
+        level = power.getAttribute('level')
+        self.grid.SetCellValue(i,0,point)
+        self.grid.SetReadOnly(i,0)
+        self.grid.SetCellValue(i,1,level)
+        self.grid.SetReadOnly(i,1)
+        self.grid.SetCellValue(i,2,name)
+        self.grid.SetReadOnly(i,2)
+        self.grid.SetCellValue(i,3,type)
+        self.grid.SetReadOnly(i,3)
+        self.grid.SetCellValue(i,4,test)
+        self.grid.SetReadOnly(i,4)
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+
+    def on_add(self,evt):
+        if not self.temp_dom:
+            tmp = open(orpg.dirpath.dir_struct["dnd3e"]+"dnd3epowers.xml","r")
+
+            xml_dom = parseXml_with_dlg(self,tmp.read())
+            xml_dom = xml_dom._get_firstChild()
+            tmp.close()
+            self.temp_dom = xml_dom
+        f_list = self.temp_dom.getElementsByTagName('power')
+        opts = []
+        #lvl = int(dnd3e_char_child.get_char_lvl('level'))
+        #castlvl = lvl / 2
+        for f in f_list:
+            spelllvl = f.getAttribute('level')
+            #if spelllvl <= "1":
+            #    opts.append("(" + f.getAttribute('level') + ") - " + f.getAttribute('name') + " - " + f.getAttribute('test'))
+            #else:
+            #    if eval('%d >= %s' %(castlvl, spelllvl)):
+            opts.append("(" + f.getAttribute('level') + ") - " +
+                        f.getAttribute('name') + " - " + f.getAttribute('test'))
+        dlg = wx.SingleChoiceDialog(self,'Choose Power','Powers',opts)
+        if dlg.ShowModal() == wx.ID_OK:
+            i = dlg.GetSelection()
+            new_node = self.master_dom.appendChild(f_list[i].cloneNode(False))
+            self.grid.AppendRows(1)
+            self.n_list = self.master_dom.getElementsByTagName('power')
+            self.refresh_row(self.grid.GetNumberRows()-1)
+            self.handler.refresh_powers()
+        dlg.Destroy()
+
+    def on_remove(self,evt):
+        rows = self.grid.GetNumberRows()
+        for i in range(rows):
+            if self.grid.IsInSelection(i,0):
+                self.grid.DeleteRows(i)
+                self.master_dom.removeChild(self.n_list[i])
+                self.n_list = self.master_dom.getElementsByTagName('weapon')
+                self.handler.refresh_powers()
+
+    def on_refresh_powers( self, evt ):
+        #a 1.5002,1.5014 s
+        self.root.pp.set_char_pp('current1',self.root.pp.get_char_pp('max1'))
+        self.root.pp.set_char_pp('free',self.root.pp.get_char_pp('maxfree'))
+        #a 1.5002,1.5014 e
+
+
+
+    def on_size(self,event):
+        s = self.GetClientSizeTuple()
+        self.grid.SetDimensions(0,0,s[0],s[1]-25)
+        self.sizer.SetDimension(0,s[1]-25,s[0],25)
+        (w,h) = self.grid.GetClientSizeTuple()
+        cols = self.grid.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.grid.SetColSize(i,col_w)
+        self.grid.SetColSize(0,w * 0.05)
+        self.grid.SetColSize(1,w * 0.05)
+        self.grid.SetColSize(2,w * 0.30)
+        self.grid.SetColSize(3,w * 0.30)
+        self.grid.SetColSize(4,w * 0.30)
+
+    def refresh_data(self):
+
+        for i in range(len(self.n_list)):
+            self.refresh_row(i)
+
+class dnd3epp(snp_char_child):
+    """ Node Handler for power points.  This handler will be
+        created by dnd3echar_handler.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        snp_char_child.__init__(self,xml_dom,tree_node,parent)
+        self.hparent = parent #a 1.5002 allow ability to run up tree.
+        self.root = getRoot(self)
+        self.root.pp = self
+        self.ppPanel=None
+
+
+    def get_design_panel(self,parent):
+        wnd = outline_panel(parent,self,pp_panel,"Power Points")
+        wnd.title = "Power Points"
+        return wnd
+
+
+    def tohtml(self):
+        html_str = "<table width=100% border=1 ><tr BGCOLOR=#E9E9E9 >"
+        #html_str += "<th colspan=8>Power Points</th></tr>" #d 1.6010
+        html_str += "<th colspan=7>Power Points</th>" #a 1.6010
+        #m 1.6010 rearanged everything below to "return html_str"
+        html_str += "</tr><tr>"
+        html_str += "<th colspan=2>Max:</th>"
+        html_str += "<td>"+self.master_dom.getAttribute('max1')+"</td>"
+        html_str += "<th colspan=3>Max Talents/day:</th>"
+        html_str += "<td>"+self.master_dom.getAttribute('maxfree')+"</td>"
+        html_str += "</tr><tr>"
+        html_str += "<th colspan=2>Current:</th>"
+        html_str += "<td>"+self.master_dom.getAttribute('current1')+"</td>"
+        html_str += "<th colspan=3>Current Talents/day:</th>"
+        html_str += "<td>"+self.master_dom.getAttribute('free')+"</td>"
+        html_str += "</tr></table>"
+        return html_str
+
+    def get_char_pp( self, attr ):
+        pp = self.master_dom.getAttribute(attr)
+        #print "dnd3epp -get_char_pp: attr,pp",attr,pp
+        return pp
+
+    def set_char_pp( self, attr, evl ):
+        qSub = str(evl) #a 1.5014 must force it to be a string for next call.
+        self.master_dom.setAttribute(attr, qSub)
+        #This function needs to be looked at the idea is to refresh the power panel
+        #But it causes a seg fault when you refresh from powers -mgt
+        #if self.ppPanel:                #a 1.5015
+        #    self.ppPanel.on_refresh(attr,qSub)   #a 1.5015
+
+
+class pp_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.hparent = handler #a 1.5002 allow ability to run up tree.
+        #a 1.5002 we need the functional parent, not the invoking parent.
+        self.hparent.ppPanel=self #a 1.5xx
+
+        pname = handler.master_dom.setAttribute("name", 'PowerPoints')
+        self.sizer = wx.FlexGridSizer(2, 4, 2, 2)  # rows, cols, hgap, vgap
+        self.master_dom = handler.master_dom
+
+        self.static1= wx.StaticText(self, -1, "PP Current:")  #a 1.5015
+        self.dyn1= wx.TextCtrl(self, PP_CUR,
+                self.master_dom.getAttribute('current1'))   #a 1.5015
+        self.dyn3= wx.TextCtrl(self, PP_FRE,
+                self.master_dom.getAttribute('free'))       #a 1.5015
+#        self.sizer.AddMany([ (wx.StaticText(self, -1, "PP Current:"),  #d 1.5015
+#                                           0, wx.ALIGN_CENTER_VERTICAL),
+#            (wx.TextCtrl(self, PP_CUR,                                 #d 1.5015
+#                self.master_dom.getAttribute('current1')),   0, wx.EXPAND),
+        self.sizer.AddMany([ (self.static1, 0, wx.ALIGN_CENTER_VERTICAL),
+            (self.dyn1,   0, wx.EXPAND),
+            (wx.StaticText(self, -1, "PP Max:"), 0, wx.ALIGN_CENTER_VERTICAL),
+            (wx.TextCtrl(self, PP_MAX,
+                self.master_dom.getAttribute('max1')),  0, wx.EXPAND),
+            (wx.StaticText(self, -1, "Current Free Talants per day:"),
+                          0, wx.ALIGN_CENTER_VERTICAL),
+            (self.dyn3,  0, wx.EXPAND),                          #a 1.5015
+#            (wx.TextCtrl(self, PP_FRE,
+#                self.master_dom.getAttribute('free')),  0, wx.EXPAND),#d 1.5015
+            (wx.StaticText(self, -1, "Max Free Talants per day:"),
+                            0, wx.ALIGN_CENTER_VERTICAL),
+            (wx.TextCtrl(self, PP_MFRE,
+                self.master_dom.getAttribute('maxfree')),  0, wx.EXPAND),
+            ])
+
+        self.sizer.AddGrowableCol(1)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_MAX)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_CUR)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_FRE)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=PP_MFRE)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        if id == PP_CUR:
+            self.master_dom.setAttribute('current1',evt.GetString())
+        elif id == PP_MAX:
+            self.master_dom.setAttribute('max1',evt.GetString())
+        elif id == PP_FRE:
+            self.master_dom.setAttribute('free',evt.GetString())
+        elif id == PP_MFRE:
+            self.master_dom.setAttribute('maxfree',evt.GetString())
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(0,0,s[0],s[1])
+
+    #a 5.015 this whole function.
+    def on_refresh(self,attr,value):
+        if attr == 'current1':
+            self.dyn1.SetValue(value)
+        else:
+            self.dyn3.SetValue(value)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/forms.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,825 @@
+# 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: forms.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: forms.py,v 1.53 2007/04/21 23:00:51 digitalxero Exp $
+#
+# Description: The file contains code for the form based nodehanlers
+#
+
+__version__ = "$Id: forms.py,v 1.53 2007/04/21 23:00:51 digitalxero Exp $"
+
+from containers import *
+import wx.lib.scrolledpanel
+
+def bool2int(b):
+    #in wxPython 2.5+, evt.Checked() returns True or False instead of 1.0 or 0.
+    #by running the results of that through this function, we convert it.
+    #if it was an int already, nothing changes. The difference between 1.0
+    #and 1, i.e. between ints and floats, is potentially dangerous when we
+    #use str() on it, but it seems to work fine right now.
+    if b:
+        return 1
+    else:
+        return 0
+
+#################################
+## form container
+#################################
+
+class form_handler(container_handler):
+    """
+            <nodehandler name='?'  module='forms' class='form_handler'  >
+            <form width='100' height='100' />
+            </nodehandler>
+    """
+
+    def __init__(self,xml_dom,tree_node):
+        container_handler.__init__(self,xml_dom,tree_node)
+
+    def load_children(self):
+        self.atts = None
+        children = self.master_dom._get_childNodes()
+        for c in children:
+            if c._get_tagName() == "form":
+                self.atts = c
+            else:
+                self.tree.load_xml(c,self.mytree_node)
+        if not self.atts:
+            elem = self.xml.minidom.Element('form')
+            elem.setAttribute("width","400")
+            elem.setAttribute("height","600")
+            self.atts = self.master_dom.appendChild(elem)
+
+    def get_design_panel(self,parent):
+        return form_edit_panel(parent,self)
+
+    def get_use_panel(self,parent):
+        return form_panel(parent,self)
+
+    def on_drop(self,evt):
+        # make sure its a contorl node
+        container_handler.on_drop(self,evt)
+
+
+class form_panel(wx.lib.scrolledpanel.ScrolledPanel):
+    def __init__(self, parent, handler):
+        wx.lib.scrolledpanel.ScrolledPanel.__init__(self, parent, wx.ID_ANY, style=wx.NO_BORDER|wx.VSCROLL|wx.HSCROLL)
+        self.height = int(handler.atts.getAttribute("height"))
+        self.width = int(handler.atts.getAttribute("width"))
+
+
+        self.SetSize((0,0))
+        self.handler = handler
+        self.parent = parent
+        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
+        tree = self.handler.tree
+        child = tree.GetFirstChild(handler.mytree_node)
+        if child[0].IsOk():
+            handler.traverse(child[0], self.create_child_wnd, 0, None, False)
+
+        self.SetSizer(self.main_sizer)
+        self.SetAutoLayout(True)
+
+        self.SetupScrolling()
+
+        parent.SetSize(self.GetSize())
+        self.Fit()
+
+
+    def SetSize(self, xy):
+        (x, y) = self.GetSize()
+        (nx, ny) = xy
+        if x < nx:
+            x = nx+10
+        y += ny+11
+        wx.lib.scrolledpanel.ScrolledPanel.SetSize(self, (x, y))
+
+
+    def create_child_wnd(self, obj, evt):
+        panel = obj.get_use_panel(self)
+        size = obj.get_size_constraint()
+        if panel:
+            self.main_sizer.Add(panel, size, wx.EXPAND)
+            self.main_sizer.Add(wx.Size(10,10))
+
+
+
+F_HEIGHT = wx.NewId()
+F_WIDTH = wx.NewId()
+class form_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, "Form Properties"), wx.VERTICAL)
+        wh_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.text = {   P_TITLE : wx.TextCtrl(self, P_TITLE, handler.master_dom.getAttribute('name')),
+                        F_HEIGHT : wx.TextCtrl(self, F_HEIGHT, handler.atts.getAttribute('height')),
+                        F_WIDTH : wx.TextCtrl(self, F_WIDTH, handler.atts.getAttribute('width'))
+                      }
+
+        wh_sizer.Add(wx.StaticText(self, -1, "Width:"), 0, wx.ALIGN_CENTER)
+        wh_sizer.Add(wx.Size(10,10))
+        wh_sizer.Add(self.text[F_WIDTH], 0, wx.EXPAND)
+        wh_sizer.Add(wx.Size(10,10))
+        wh_sizer.Add(wx.StaticText(self, -1, "Height:"), 0, wx.ALIGN_CENTER)
+        wh_sizer.Add(wx.Size(10,10))
+        wh_sizer.Add(self.text[F_HEIGHT], 0, wx.EXPAND)
+
+        sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
+        sizer.Add(self.text[P_TITLE], 0, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wh_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.Bind(wx.EVT_TEXT, self.on_text, id=F_HEIGHT)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=F_WIDTH)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        txt = self.text[id].GetValue()
+        if not len(txt): return
+        if id == P_TITLE:
+            self.handler.master_dom.setAttribute('name',txt)
+            self.handler.rename(txt)
+        elif id == F_HEIGHT or id == F_WIDTH:
+            try:
+                int(txt)
+            except:
+                return 0
+            if id == F_HEIGHT:
+                self.handler.atts.setAttribute("height",txt)
+            elif id == F_WIDTH:
+                self.handler.atts.setAttribute("width",txt)
+
+
+
+
+
+##########################
+## control handler
+##########################
+class control_handler(node_handler):
+    """ A nodehandler for form controls.
+        <nodehandler name='?' module='forms' class='control_handler' />
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+
+    def get_size_constraint(self):
+        return 0
+
+
+##########################
+## textctrl handler
+##########################
+    #
+    # Updated by Snowdog (April 2003)
+    #   Now includes Raw Send Mode (like the chat macro uses)
+    #   and an option to remove the title from text when sent
+    #   to the chat in the normal non-chat macro mode.
+    #
+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>
+        </nodehandler>
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.text_elem = self.master_dom.getElementsByTagName('text')[0]
+        self.text = safe_get_text_node(self.text_elem)
+        if self.text_elem.getAttribute("send_button") == "":
+            self.text_elem.setAttribute("send_button","0")
+        if self.text_elem.getAttribute("raw_mode") == "":
+            self.text_elem.setAttribute("raw_mode","0")
+        if self.text_elem.getAttribute("hide_title") == "":
+            self.text_elem.setAttribute("hide_title","0")
+
+    def get_design_panel(self,parent):
+        return textctrl_edit_panel(parent,self)
+
+    def get_use_panel(self,parent):
+        return text_panel(parent,self)
+
+    def get_size_constraint(self):
+        return int(self.text_elem.getAttribute("multiline"))
+
+    def is_multi_line(self):
+        return int(self.text_elem.getAttribute("multiline"))
+
+    def is_raw_send(self):
+        return int(self.text_elem.getAttribute("raw_mode"))
+
+    def is_hide_title(self):
+        return int(self.text_elem.getAttribute("hide_title"))
+
+    def has_send_button(self):
+        return int(self.text_elem.getAttribute("send_button"))
+
+
+    def tohtml(self):
+        txt = self.text._get_nodeValue()
+        txt = string.replace(txt,'\n',"<br />")
+        if not self.is_hide_title():
+            txt = "<b>"+self.master_dom.getAttribute("name")+":</b> "+txt
+        return txt
+
+
+FORM_TEXT_CTRL = wx.NewId()
+FORM_SEND_BUTTON = wx.NewId()
+
+class text_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.chat = handler.chat
+        self.handler = handler
+        if handler.is_multi_line():
+            text_style = wx.TE_MULTILINE
+            sizer_style = wx.EXPAND
+            sizer = wx.BoxSizer(wx.VERTICAL)
+        else:
+            sizer_style = wx.ALIGN_CENTER
+            text_style = 0
+            sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        txt = handler.text._get_nodeValue()
+        self.text = wx.TextCtrl(self, FORM_TEXT_CTRL, txt, style=text_style)
+        sizer.Add(wx.StaticText(self, -1, handler.master_dom.getAttribute('name')+": "), 0, sizer_style)
+        sizer.Add(wx.Size(5,0))
+        sizer.Add(self.text, 1, sizer_style)
+
+        if handler.has_send_button():
+            sizer.Add(wx.Button(self, FORM_SEND_BUTTON, "Send"), 0, sizer_style)
+
+        self.sizer = sizer
+        self.SetSizer(sizer)
+        self.SetAutoLayout(True)
+
+        parent.SetSize(self.GetBestSize())
+        self.Bind(wx.EVT_TEXT, self.on_text, id=FORM_TEXT_CTRL)
+        self.Bind(wx.EVT_BUTTON, self.on_send, id=FORM_SEND_BUTTON)
+
+    def on_text(self,evt):
+        txt = self.text.GetValue()
+        txt = strip_text(txt)
+        self.handler.text._set_nodeValue(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)
+        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()
+
+class textctrl_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, "Text Properties"), wx.VERTICAL)
+
+        self.title = wx.TextCtrl(self, P_TITLE, handler.master_dom.getAttribute('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")
+        self.send_button.SetValue(handler.has_send_button())
+
+        sizer.Add(wx.StaticText(self, P_TITLE, "Title:"), 0, wx.EXPAND)
+        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))
+        if handler.is_multi_line():
+            sizer_style = wx.EXPAND
+            text_style = wx.TE_MULTILINE
+            multi = 1
+        else:
+            sizer_style=wx.EXPAND
+            text_style = 0
+            multi = 0
+        self.text = wx.TextCtrl(self, F_TEXT, handler.text._get_nodeValue(),style=text_style)
+        sizer.Add(wx.Size(5,0))
+        sizer.Add(self.text, multi, sizer_style)
+        self.SetSizer(sizer)
+        self.SetAutoLayout(True)
+
+        self.Bind(wx.EVT_TEXT, 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.master_dom.setAttribute('name',txt)
+            self.handler.rename(txt)
+        if id == F_TEXT:
+            txt = self.text.GetValue()
+            txt = strip_text(txt)
+            self.handler.text._set_nodeValue(txt)
+
+    def on_button(self,evt):
+        self.handler.text_elem.setAttribute("multiline",str(bool2int(evt.Checked())))
+
+    def on_raw_button(self,evt):
+        self.handler.text_elem.setAttribute("raw_mode",str(bool2int(evt.Checked())))
+
+    def on_hide_button(self,evt):
+        self.handler.text_elem.setAttribute("hide_title",str(bool2int(evt.Checked())))
+
+    def on_send_button(self,evt):
+        self.handler.text_elem.setAttribute("send_button",str(bool2int(evt.Checked())))
+
+
+
+
+
+
+#######################
+## listbox handler
+#######################
+    #
+    # Updated by Snowdog (April 2003)
+    #   Now includesan option to remove the title from
+    #   text when sent to the chat.
+    #
+L_DROP = 0
+L_LIST = 1
+L_RADIO = 2
+L_CHECK = 3
+L_ROLLER = 4
+
+class listbox_handler(node_handler):
+    """
+    <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>
+        </list>
+    </nodehandler>
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.list = self.master_dom.getElementsByTagName('list')[0]
+        self.options = self.list.getElementsByTagName('option')
+        if self.list.getAttribute("send_button") == "":
+            self.list.setAttribute("send_button","0")
+        if self.list.getAttribute("hide_title") == "":
+            self.list.setAttribute("hide_title","0")
+
+    def get_design_panel(self,parent):
+        return listbox_edit_panel(parent,self)
+
+    def get_use_panel(self,parent):
+        return listbox_panel(parent,self)
+
+    def get_type(self):
+        return int(self.list.getAttribute("type"))
+
+    def set_type(self,type):
+        self.list.setAttribute("type",str(type))
+
+    def is_hide_title(self):
+        return int(self.list.getAttribute("hide_title"))
+
+    # single selection methods
+    def get_selected_node(self):
+        for opt in self.options:
+            if opt.getAttribute("selected") == "1": return opt
+        return None
+
+    def get_selected_index(self):
+        i = 0
+        for opt in self.options:
+            if opt.getAttribute("selected") == "1":
+                return i
+            i += 1
+        return 0
+
+    def get_selected_text(self):
+        node = self.get_selected_node()
+        if node:
+            return safe_get_text_node(node)._get_nodeValue()
+        else:
+            return ""
+
+
+    # mult selection methods
+
+    def get_selections(self):
+        opts = []
+        for opt in self.options:
+            if opt.getAttribute("selected") == "1":
+                opts.append(opt)
+        return opts
+
+    def get_selections_text(self):
+        opts = []
+        for opt in self.options:
+            if opt.getAttribute("selected") == "1":
+                opts.append(safe_get_text_node(opt)._get_nodeValue())
+        return opts
+
+    def get_selections_index(self):
+        opts = []
+        i = 0
+        for opt in self.options:
+            if opt.getAttribute("selected") == "1":
+                opts.append(i)
+            i += 1
+        return opts
+
+    # setting selection method
+
+    def set_selected_node(self,index,selected=1):
+        if self.get_type() != L_CHECK:
+            self.clear_selections()
+        self.options[index].setAttribute("selected", str(bool2int(selected)))
+
+    def clear_selections(self):
+        for opt in self.options:
+            opt.setAttribute("selected","0")
+
+    # misc methods
+
+    def get_options(self):
+        opts = []
+        for opt in self.options:
+            opts.append(safe_get_text_node(opt)._get_nodeValue())
+        return opts
+
+    def get_option(self,index):
+        return safe_get_text_node(self.options[index])._get_nodeValue()
+
+    def add_option(self,opt):
+        elem = minidom.Element('option')
+        elem.setAttribute("value","0")
+        elem.setAttribute("selected","0")
+        t_node = minidom.Text(opt)
+        t_node = elem.appendChild(t_node)
+        self.list.appendChild(elem)
+        self.options = self.list.getElementsByTagName('option')
+
+    def remove_option(self,index):
+        self.list.removeChild(self.options[index])
+        self.options = self.list.getElementsByTagName('option')
+
+    def edit_option(self,index,value):
+        safe_get_text_node(self.options[index])._set_nodeValue(value)
+
+    def has_send_button(self):
+        if self.list.getAttribute("send_button") == '0':
+            return False
+        else:
+            return True
+
+    def get_size_constraint(self):
+        if self.get_type() == L_DROP:
+            return 0
+        else:
+            return 1
+
+    def tohtml(self):
+        opts = self.get_selections_text()
+        text = ""
+        if not self.is_hide_title():
+            text = "<b>"+self.master_dom.getAttribute("name")+":</b> "
+        comma = ", "
+        text += comma.join(opts)
+        return text
+
+
+
+F_LIST = wx.NewId()
+F_SEND = wx.NewId()
+
+
+class listbox_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.handler = handler
+        self.chat = handler.chat
+        opts = handler.get_options()
+        cur_opt = handler.get_selected_text()
+        type = handler.get_type()
+        label = handler.master_dom.getAttribute('name')
+
+        if type == L_DROP:
+            self.list = wx.ComboBox(self, F_LIST, cur_opt, 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)
+        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 )
+            else:
+                self.list.SetStringSelection( i )
+
+        if type == L_DROP:
+            sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        else:
+            sizer = wx.BoxSizer(wx.VERTICAL)
+
+        if type != L_RADIO:
+            sizer.Add(wx.StaticText(self, -1, label+": "), 0, wx.EXPAND)
+            sizer.Add(wx.Size(5,0))
+
+        sizer.Add(self.list, 1, wx.EXPAND)
+
+        if handler.has_send_button():
+            sizer.Add(wx.Button(self, F_SEND, "Send"), 0, wx.EXPAND)
+            self.Bind(wx.EVT_BUTTON, self.handler.on_send_to_chat, id=F_SEND)
+
+        self.sizer = sizer
+        self.SetSizer(sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        parent.SetSize(self.GetBestSize())
+
+        if type == L_DROP:
+            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)
+        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_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_REM = wx.NewId()
+BUT_EDIT = wx.NewId()
+F_TYPE = wx.NewId()
+F_NO_TITLE = 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)
+
+        self.text = wx.TextCtrl(self, P_TITLE, handler.master_dom.getAttribute('name'))
+
+        opts = handler.get_options()
+        self.listbox = wx.ListBox(self, F_LIST, choices=opts, style=wx.LB_HSCROLL|wx.LB_SINGLE|wx.LB_NEEDED_SB)
+        opts = ['Drop Down', 'List Box', 'Radio Box', 'Check List']
+        self.type_radios = wx.RadioBox(self,F_TYPE,"List Type",choices=opts)
+        self.type_radios.SetSelection(handler.get_type())
+
+        self.send_button = wx.CheckBox(self, F_SEND_BUTTON, " Send Button")
+        self.send_button.SetValue(handler.has_send_button())
+
+        self.hide_title = wx.CheckBox(self, F_NO_TITLE, " Hide Title")
+        self.hide_title.SetValue(handler.is_hide_title())
+
+        but_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        but_sizer.Add(wx.Button(self, BUT_ADD, "Add"), 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.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(self.listbox,1,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.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_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)
+
+    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()
+        self.reload_options()
+
+    def on_remove(self,evt):
+        index = self.listbox.GetSelection()
+        if index >= 0:
+            self.handler.remove_option(index)
+            self.reload_options()
+
+    def on_edit(self,evt):
+        index = self.listbox.GetSelection()
+        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.reload_options()
+
+    def reload_options(self):
+        self.listbox.Clear()
+        for opt in self.handler.get_options():
+            self.listbox.Append(opt)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        txt = self.text.GetValue()
+        if not len(txt): return
+        if id == P_TITLE:
+            self.handler.master_dom.setAttribute('name',txt)
+            self.handler.rename(txt)
+
+    def on_send_button(self,evt):
+        self.handler.list.setAttribute("send_button", str( bool2int(evt.Checked()) ))
+
+    def on_hide_button(self,evt):
+        print "hide_title, " + str(bool2int(evt.Checked()))
+        self.handler.list.setAttribute("hide_title", str( bool2int(evt.Checked()) ))
+
+
+###############################
+## link image handlers
+###############################
+
+class link_handler(node_handler):
+    """ A nodehandler for URLs. Will open URL in a wxHTMLFrame
+        <nodehandler name='?' module='forms' class='link_handler' >
+                <link  href='http//??.??'  />
+        </nodehandler >
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.link = self.master_dom._get_firstChild()
+
+    def on_use(self,evt):
+        href = self.link.getAttribute("href")
+        wb = webbrowser.get()
+        wb.open(href)
+
+    def get_design_panel(self,parent):
+        return link_edit_panel(parent,self)
+
+    def get_use_panel(self,parent):
+        return link_panel(parent,self)
+
+    def tohtml(self):
+        href = self.link.getAttribute("href")
+        title = self.master_dom.getAttribute("name")
+        return "<a href=\""+href+"\" >"+title+"</a>"
+
+class link_panel(wx.StaticText):
+    def __init__(self,parent,handler):
+        self.handler = handler
+        label = handler.master_dom.getAttribute('name')
+        wx.StaticText.__init__(self,parent,-1,label)
+        self.SetForegroundColour(wx.BLUE)
+        self.Bind(wx.EVT_LEFT_DOWN, self.handler.on_use)
+
+
+P_URL = wx.NewId()
+
+class link_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, "Link Properties"), wx.VERTICAL)
+
+        self.text = {}
+        self.text[P_TITLE] = wx.TextCtrl(self, P_TITLE, handler.master_dom.getAttribute('name'))
+        self.text[P_URL] = wx.TextCtrl(self, P_URL, handler.link.getAttribute('href'))
+
+        sizer.Add(wx.StaticText(self, -1, "Title:"), 0, wx.EXPAND)
+        sizer.Add(self.text[P_TITLE], 0, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        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.Bind(wx.EVT_TEXT, self.on_text, id=P_URL)
+
+    def on_text(self,evt):
+        id = evt.GetId()
+        txt = self.text[id].GetValue()
+        if not len(txt): return
+        if id == P_TITLE:
+            self.handler.master_dom.setAttribute('name',txt)
+            self.handler.rename(txt)
+        elif id == P_URL:
+            self.handler.link.setAttribute('href',txt)
+
+##########################
+## webimg node handler
+##########################
+class webimg_handler(node_handler):
+    """ A nodehandler for URLs. Will open URL in a wxHTMLFrame
+        <nodehandler name='?' module='forms' class='webimg_handler' >
+                <link  href='http//??.??'  />
+        </nodehandler >
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.link = self.master_dom._get_firstChild()
+
+    def get_design_panel(self,parent):
+        return link_edit_panel(parent,self)
+
+    def get_use_panel(self,parent):
+        img = img_helper().load_url(self.link.getAttribute("href"))
+        #print img
+        if not img is None:
+            return wx.StaticBitmap(parent,-1,img,size= wx.Size(img.GetWidth(),img.GetHeight()))
+        return wx.EmptyBitmap(1, 1)
+
+    def tohtml(self):
+        href = self.link.getAttribute("href")
+        title = self.master_dom.getAttribute("name")
+        return "<img src=\""+href+"\" alt="+title+" >"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/map_miniature_nodehandler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,140 @@
+# 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: map_miniature_nodehandler.py
+# Author: Andrew Bennett
+# Maintainer:
+# Version:
+#   $Id: map_miniature_nodehandler.py,v 1.17 2007/12/07 20:39:48 digitalxero Exp $
+#
+# Description: nodehandler for miniature images
+#
+
+#from nodehandlers.core import *
+from core import *
+from orpg.gametree import *
+from orpg.mapper.miniatures_msg import mini_msg
+from orpg.mapper.images import ImageHandler
+import urllib
+
+
+class map_miniature_handler(node_handler):
+
+    """ A node handler for miniatures
+        <nodehandler name='Elf-1' module='map_miniature_nodehandler' class='map_miniature_handler' >
+                <miniature id='' label='Elf-1' posx='' posy='' path='' ...  />
+        </nodehandler >
+    """
+
+
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.mapper = open_rpg.get_component("map")
+        self.session = open_rpg.get_component("session")
+        self.miniature_dom = self.master_dom.getElementsByTagName("miniature")
+        if self.miniature_dom:
+            self.miniature_dom = self.miniature_dom[0]   # convert to scalar
+
+    def get_scaled_bitmap(self,x,y):
+        my_mini_msg = mini_msg()
+        my_mini_msg.init_from_dom(self.miniature_dom)
+        bmp = None
+        path = my_mini_msg.get_prop("path")
+
+        if path:
+            path = urllib.unquote(path)
+            if ImageHandler.Cache.has_key(path):
+                bmp = ImageHandler.Cache[path]
+            else:
+                #bmp = ImageHandler.directLoad(path, 'miniature', id)
+                bmp = ImageHandler.directLoad(path)# Old Code TaS.
+
+            if bmp:
+                img = wx.ImageFromMime(ImageHandler.Cache[path][1], ImageHandler.Cache[path][2])
+                #img = wx.ImageFromBitmap(bmp)
+                scaled_img = img.Scale(x,y)
+                scaled_bmp = scaled_img.ConvertToBitmap()
+                scratch = scaled_img.ConvertToBitmap()
+                memDC = wx.MemoryDC()
+                memDC.BeginDrawing()
+                memDC.SelectObject(scaled_bmp)
+                memDC.SetBrush(wx.WHITE_BRUSH)
+                memDC.SetPen(wx.WHITE_PEN)
+                memDC.DrawRectangle(0,0,x,y)
+                memDC.SetPen(wx.NullPen)
+                memDC.SetBrush(wx.NullBrush)
+                memDC.DrawBitmap(scratch,0,0,1)
+                memDC.SelectObject(wx.NullBitmap)
+                memDC.EndDrawing()
+                del memDC
+                return scaled_bmp
+
+    def map_aware(self):
+        return 1
+
+    def get_miniature_XML(self):
+        my_mini_msg = mini_msg()
+        my_mini_msg.init_from_dom(self.miniature_dom)
+        my_mini_msg.init_prop("id",self.session.get_next_id())
+        label = self.master_dom.getAttribute("name")
+        my_mini_msg.init_prop("label",label)
+        new_xml = my_mini_msg.get_all_xml()
+        return new_xml
+
+    def get_to_map_XML(self):
+        new_xml = self.get_miniature_XML()
+        new_xml = str("<map action='update'><miniatures>" + new_xml + "</miniatures></map>")
+        return new_xml
+
+    def on_send_to_map(self,evt):
+        if isinstance(evt, wx.MouseEvent) and evt.LeftUp():# as opposed to a menu event
+            dc = wx.ClientDC(self.mapper.canvas)
+            self.mapper.canvas.PrepareDC(dc)
+            grid = self.mapper.canvas.layers['grid']
+            dc.SetUserScale(grid.mapscale, grid.mapscale)
+            pos = evt.GetLogicalPosition(dc)
+            try:
+                align = int(self.miniature_dom.getAttribute("align"))
+                width = int(self.miniature_dom.getAttribute("width"))
+                height = int(self.miniature_dom.getAttribute("height"))
+                pos = grid.get_snapped_to_pos(pos, align, width, height)
+            except:
+                pass
+            self.miniature_dom.setAttribute("posx", str(pos.x))
+            self.miniature_dom.setAttribute("posy", str(pos.y))
+        new_xml = self.get_to_map_XML()
+        if (self.session.my_role() != self.session.ROLE_GM) and (self.session.my_role() != self.session.ROLE_PLAYER):
+            open_rpg.get_component("chat").InfoPost("You must be either a player or GM to use the miniature Layer")
+            return
+
+        if new_xml:
+            self.mapper.new_data(new_xml)
+            self.session.send(new_xml)
+        else:
+            print "problem converting old mini xml to new mini xml"
+
+    def about(self):
+        return "Miniature node by Andrew Bennett"
+
+    def tohtml(self):
+        html_str = "<table><tr><td>"
+        html_str += "<center><img src='" + self.miniature_dom.getAttribute("path") + "'>"
+        html_str += "</center></td></tr>\n"
+        html_str += "<tr><td><center>" + self.master_dom.getAttribute("name") + "</center></td></tr></table>"
+        return html_str
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/minilib.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,575 @@
+# 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: minilib.py
+# Author: Ted Berg
+# Maintainer:
+# Version:
+#   $Id: minilib.py,v 1.28 2007/04/22 22:00:18 digitalxero Exp $
+#
+# Description: nodehandler for a collection of miniatures.
+#
+
+__version__ = "$Id: minilib.py,v 1.28 2007/04/22 22:00:18 digitalxero Exp $"
+
+"""Nodehandler for collections of miniatures.  User can add, delete, edit
+miniatures as sending them to the map singly or in batches.
+"""
+from core import *
+import orpg.dirpath
+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}
+FROM_MINILIB_MAP = {'url':'path', 'name':'label', 'unique':None}
+CORE_ATTRIBUTES = ['name', 'url', 'unique', 'posy', 'posx', 'hide', 'face', 'heading', 'align', 'locked', 'width', 'height']
+
+ATTRIBUTE_NAME = 'name'
+ATTRIBUTE_URL = 'url'
+ATTRIBUTE_UNIQUE = 'unique'
+ATTRIBUTE_ID = 'id'
+ATTRIBUTE_POSX = 'posx'
+ATTRIBUTE_POSY = 'posy'
+
+TAG_MINIATURE = 'miniature'
+
+COMPONENT_MAP = 'map'
+COMPONENT_SESSION = 'session'
+# <nodehandler name='?' module='minilib' class='minilib_handler'>
+#     <miniature name='?' url='?' unique='?'></miniature>
+# </nodehandler>
+
+class minilib_handler( node_handler ):
+    """A nodehandler that manages a collection of miniatures for the
+    map.
+    <pre>
+        &lt;nodehandler name='?' module='minilib' class='minilib_handler'&gt;
+            &lt;miniature name='?' url='?' unique='?'&gt;&lt;/miniature&gt;
+        &lt;/nodehandler&gt;
+    </pre>
+    """
+    def __init__(self, xml_dom, tree_node):
+        """Instantiates the class, and sets all vars to their default state
+        """
+        node_handler.__init__(self, xml_dom, tree_node)
+
+        self.myeditor = None
+        self.mywindow = None
+        self.tree_node = tree_node
+        # self.xml_dom = xml_dom
+        self.update_leaves()
+        self.sanity_check_nodes()
+
+    def get_design_panel( self, parent ):
+        """returns an instance of the miniature library edit control ( see
+        on_design ).  This is for use with the the 'edit multiple nodes in a
+        single frame' code.
+        """
+        return minpedit( parent, self )
+
+    def get_use_panel( self, parent ):
+        """returns an instance of the miniature library view control ( see
+        on_use ).  This is for use with the the 'view multiple nodes in a
+        single frame' code.
+        """
+        return minilib_use_panel( parent, self )
+
+    def tohtml( self ):
+        """Returns an HTML representation of this node in string format.
+        The table columnwidths are currently being forced, as the wxHTML
+        widgets being used don't handle cells wider than the widgets are
+        expecting for a given column.
+        """
+        str = '<table border="2" >'
+        list = self.master_dom.getElementsByTagName(TAG_MINIATURE)
+        str += "<tr><th width='20%'>Label</th><th>Image</th><th width='65%'>URL</th><th>Unique</th></t>"
+        for mini in list:
+            url = mini.getAttribute(ATTRIBUTE_URL)
+            label = mini.getAttribute(ATTRIBUTE_NAME)
+            flag = 0
+            try:
+                flag = eval( mini.getAttribute(ATTRIBUTE_UNIQUE) )
+            except:
+                pass
+
+            show = 'yes'
+            if flag:
+                show = 'no'
+
+            str += """<tr>
+                <td> %s </td>
+                <td><img src="%s"></td>
+                <td> %s </td>
+                <td> %s </td>
+            </tr>""" % ( label, url, url, show )
+
+        str += "</table>"
+        print str
+        return str
+
+    def html_view( self ):
+        """see to_html
+        """
+        return self.tohtml()
+
+    def on_drop(self, evt):
+        drag_obj = self.tree.drag_obj
+        if drag_obj == self or self.tree.is_parent_node( self.mytree_node, drag_obj.mytree_node ):
+            return
+        if isinstance( drag_obj, minilib_handler ):
+            item = self.tree.GetSelection()
+            name = self.tree.GetItemText( item )
+        if isinstance( drag_obj, map_miniature_nodehandler.map_miniature_handler ):
+            xml_dom = self.tree.drag_obj.master_dom#.delete()
+            obj = xml_dom.firstChild
+            print obj.getAttributeKeys()
+            dict = {}
+            unique = ''
+            for attrib in obj.getAttributeKeys():
+                key = TO_MINILIB_MAP.get( attrib, attrib )
+                if key != None:
+                    dict[ key ] = obj.getAttribute( attrib )
+            dict[ ATTRIBUTE_UNIQUE ] = unique
+            self.new_mini( dict )
+
+
+    def new_mini( self, data={}, add=1 ):
+        mini = minidom.Element( TAG_MINIATURE )
+        for key in data.keys():
+            mini.setAttribute( key, data[ key ] )
+        for key in CORE_ATTRIBUTES:
+            if mini.getAttribute( key ) == '':
+                mini.setAttribute( key, '0' )
+        if add:
+            self.add_mini( mini )
+            self.add_leaf( mini )
+        return mini
+
+    def add_mini( self, mini ):
+        self.master_dom.appendChild( mini )
+
+    def add_leaf( self, mini, icon='gear' ):
+        tree = self.tree
+        icons = tree.icons
+        key = mini.getAttribute( ATTRIBUTE_NAME )
+        self.mydata.append( mini )
+
+    def update_leaves( self ):
+        self.mydata = []
+        nl = self.master_dom.getElementsByTagName( TAG_MINIATURE )
+        for n in nl:
+            self.add_leaf( n )
+
+
+    def on_drag( self, evt ):
+        print 'drag event caught'
+
+    def send_mini_to_map( self, mini, count=1, addName=True ):
+        if mini == None:
+            return
+        if mini.getAttribute( ATTRIBUTE_URL ) == '' or mini.getAttribute( 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.getAttribute( ATTRIBUTE_URL ), mini.getAttribute( ATTRIBUTE_NAME ) )) )
+            return
+        session = open_rpg.get_component( 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 )
+
+    def get_miniature_XML( self, mini, 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() )
+        for k in mini.getAttributeKeys():
+            # translate our attributes to map attributes
+            key = FROM_MINILIB_MAP.get( k, k )
+            if key != None:
+                if not addName and k == 'name':
+                    pass
+                else:
+                    msg.init_prop( key, mini.getAttribute( k ) )
+        unique = self.is_unique( mini )
+        if addName:
+            label = mini.getAttribute( ATTRIBUTE_NAME )
+        else:
+            label = ''
+        return msg.get_all_xml()
+
+    def is_unique( self, mini ):
+        unique = mini.getAttribute( ATTRIBUTE_UNIQUE )
+        val = 0
+        try:
+            val = eval( unique )
+        except:
+            val = len( unique )
+        return val
+
+    def sanity_check_nodes( self ):
+        nl = self.master_dom.getElementsByTagName( TAG_MINIATURE )
+        for node in nl:
+            if node.getAttribute( ATTRIBUTE_POSX ) == '':
+                node.setAttribute( ATTRIBUTE_POSX, '0' )
+            if node.getAttribute( ATTRIBUTE_POSY ) == '':
+                node.setAttribute( ATTRIBUTE_POSY, '0' )
+
+    def get_mini( self, index ):
+        try:
+            nl = self.master_dom.getElementsByTagName( TAG_MINIATURE )
+            return nl[ index ]
+        except:
+            return None
+
+class mini_handler( node_handler ):
+    def __init__( self, xml_dom, tree_node, handler ):
+        node_handler.__init__( self, xml_dom, tree_node)
+        self.handler = handler
+
+    def on_ldclick( self, evt ):
+        self.handler.send_mini_to_map( self.master_dom )
+
+    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,
+    a text field for entering a count ( for batch adds ) and 'add'/'done'
+    buttons.
+    """
+    def __init__( self, frame, handler ):
+        """Constructor.
+        """
+        wx.Panel.__init__( self, frame, -1 )
+        self.handler = handler
+        self.frame = frame
+
+        self.map = open_rpg.get_component('map')
+        names = self.buildList()
+        # self.keys = self.list.keys()
+        # self.keys.sort()
+
+
+        s = self.GetClientSizeTuple()
+
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        box = wx.BoxSizer(wx.HORIZONTAL)
+        self.listbox = wx.ListBox(self, wx.ID_ANY, (10, 10), (s[0] - 10, s[1] - 30 ), names, wx.LB_EXTENDED)
+        self.count = wx.TextCtrl(self, wx.ID_ANY, '1')
+
+        box.Add( wx.StaticText( self, -1, 'Minis to add' ), 0, wx.EXPAND )
+        box.Add(wx.Size(10,10))
+        box.Add(self.count, 1, wx.EXPAND)
+
+        self.sizer.Add( self.listbox, 1, wx.EXPAND )
+        self.sizer.Add( box, 0, wx.EXPAND )
+
+        box = wx.BoxSizer( wx.HORIZONTAL )
+        self.okBtn = wx.Button(self, wx.ID_ANY, 'Add')
+        box.Add(self.okBtn, 0, wx.EXPAND)
+        self.addBtn = wx.Button(self, wx.ID_ANY, 'Add No Label')
+        box.Add(self.addBtn, 0, wx.EXPAND)
+        self.cancleBtn = wx.Button(self, wx.ID_ANY, 'Done')
+        box.Add(self.cancleBtn, 0, wx.EXPAND)
+
+        self.sizer.Add(wx.Size(10,10))
+        self.sizer.Add(box, 0, wx.EXPAND)
+        self.Bind(wx.EVT_BUTTON, self.on_ok, self.okBtn)
+        self.Bind(wx.EVT_BUTTON, self.on_ok, self.addBtn)
+        self.Bind(wx.EVT_BUTTON, self.on_close, self.cancleBtn)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+    def buildList( self ):
+        """Returns a dictionary of label => game tree miniature DOM node mappings.
+        """
+        list = self.handler.master_dom.getElementsByTagName(TAG_MINIATURE)
+        self.list = []
+        for mini in list:
+            self.list.append( mini.getAttribute( ATTRIBUTE_NAME ) )
+        return self.list
+        # self.list = {}
+        # for mini in list:
+        #     name = mini.getAttribute( ATTRIBUTE_NAME )
+        #     if name == '':
+        #         name = self.map.canvas.get_label_from_url( mini.getAttribute( ATTRIBUTE_URL ) )
+        #     self.list[ name ] = mini
+
+    def on_close(self, evt):
+        self.frame.Close()
+
+    def on_ok( self, evt ):
+        """Event handler for the 'add' button.
+        """
+        btn = self.FindWindowById(evt.GetId())
+        sendName = True
+        try:
+            count = eval( self.count.GetValue() )
+        except:
+            count = 1
+
+        try:
+            if eval( unique ):
+                count = 1
+            unique = eval( unique )
+        except:
+            pass
+
+        if btn.GetLabel() == 'Add No Label':
+            sendName = False
+        for index in self.listbox.GetSelections():
+            self.handler.send_mini_to_map( self.handler.get_mini( index ), count, sendName )
+
+
+class minpedit(wx.Panel):
+    """Panel for editing game tree miniature nodes.  Node information
+    is displayed in a grid, and buttons are provided for adding, deleting
+    nodes, and for sending minis to the map ( singly and in batches ).
+    """
+    def __init__( self, frame, handler ):
+        """Constructor.
+        """
+        wx.Panel.__init__( self, frame, -1 )
+        self.handler = handler
+        self.frame = frame
+
+        self.sizer = wx.BoxSizer( wx.VERTICAL )
+        self.grid = minilib_grid( self, handler )
+
+        bbox = wx.BoxSizer( wx.HORIZONTAL )
+        newMiniBtn = wx.Button( self, wx.ID_ANY, "New mini" )
+        delMiniBtn = wx.Button( self, wx.ID_ANY, "Del mini" )
+        addMiniBtn = wx.Button( self, wx.ID_ANY, "Add 1" )
+        addBatchBtn = wx.Button( self, wx.ID_ANY, "Add Batch" )
+        bbox.Add(newMiniBtn, 0, wx.EXPAND )
+        bbox.Add(delMiniBtn, 0, wx.EXPAND )
+        bbox.Add(wx.Size(10,10))
+        bbox.Add(addMiniBtn, 0, wx.EXPAND )
+        bbox.Add(addBatchBtn, 0, wx.EXPAND )
+
+        self.sizer.Add( self.grid, 1, wx.EXPAND)
+        self.sizer.Add( bbox, 0)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.Bind(wx.EVT_BUTTON, self.add_mini, newMiniBtn)
+        self.Bind(wx.EVT_BUTTON, self.del_mini, delMiniBtn)
+        self.Bind(wx.EVT_BUTTON, self.send_to_map, addMiniBtn)
+        self.Bind(wx.EVT_BUTTON, self.send_group_to_map, addBatchBtn)
+
+    def add_mini( self, evt=None ):
+        """Event handler for the 'New mini' button.  It calls
+        minilib_grid.add_row
+        """
+        self.grid.add_row()
+
+    def del_mini( self, evt=None ):
+        """Event handler for the 'Del mini' button.  It calls
+        minilib_grid.del_row
+        """
+        self.grid.del_row()
+
+    def send_to_map( self, evt=None ):
+        """Event handler for the 'Add 1' button.  Sends the
+        miniature defined by the currently selected row to the map, once.
+        """
+        index = self.grid.GetGridCursorRow()
+        self.handler.send_mini_to_map( self.handler.get_mini( index ) )
+
+    def send_group_to_map( self, evt=None ):
+        """Event handler for the 'Add batch' button.  Querys the user
+        for a mini count and sends the miniature defined by the currently
+        selected row to the map, the specified number of times.
+        """
+        if self.grid.GetNumberRows() > 0:
+            dlg = wx.TextEntryDialog( self.frame,
+                'How many %s\'s do you want to add?' %
+                ( self.grid.getSelectedLabel() ), 'Batch mini add', '2' )
+            if dlg.ShowModal() == wx.ID_OK:
+                try:
+                    value = eval( dlg.GetValue() )
+                except:
+                    value = 0
+                # for loop in range( 0, value ):
+                #     self.send_to_map()
+                print 'getting selected index for batch send'
+                index = self.grid.GetGridCursorRow()
+                print 'sending batch to map'
+                self.handler.send_mini_to_map( self.handler.get_mini( index ), value )
+
+class minilib_grid(wx.grid.Grid):
+    """A wxGrid subclass designed for editing game tree miniature library
+    nodes.
+    """
+    def __init__( self, parent, handler ):
+        """Constructor.
+        """
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS )
+        self.parent = parent
+        self.handler = handler
+        #self.keys = [ ATTRIBUTE_NAME, ATTRIBUTE_URL, ATTRIBUTE_UNIQUE ]
+        self.keys = CORE_ATTRIBUTES
+        self.CreateGrid( 1, len( self.keys ) )
+        # self.SetColLabelValue( 0, 'Name' )
+        # self.SetColLabelValue( 1, 'URL' )
+        # self.SetColSize( 1, 250 )
+        # self.SetColLabelValue( 2, 'Unique' )
+        for key in self.keys:
+            self.SetColLabelValue( self.keys.index( key ), key )
+        self.update_all()
+        self.selectedRow = 0
+        self.AutoSizeColumns()
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.select_cell)
+
+    def update_cols( self ):
+        nl = self.handler.master_dom.getElementsByTagName( TAG_MINIATURE )
+        for n in nl:
+            for k in n.getAttributeKeys():
+                if k not in self.keys:
+                    self.keys.append( k )
+
+    def select_cell( self, evt ):
+        """Event handler for grid cell selection changes.  It stores the
+        last selected row in a variable for use by the add[*] and del_row
+        operations.
+        """
+        self.BeginBatch()
+        self.selectedRow = evt.GetRow()
+        self.SelectRow( self.selectedRow )
+        self.EndBatch()
+        evt.Skip()
+
+    def getList( self ):
+        """Returns the list of 'miniature' DOM elements associated with this
+        miniature library.
+        """
+        return self.handler.master_dom.getElementsByTagName( TAG_MINIATURE )
+
+    def add_row( self, count = 1 ):
+        """creates a new miniature node, and then adds it to the current
+        miniature library, and to the grid.
+        """
+        self.AppendRows( count )
+        node = self.handler.new_mini( {
+          ATTRIBUTE_NAME :' ',
+          ATTRIBUTE_URL :'http://'} )# minidom.Element( TAG_MINIATURE )
+        self.update_all()
+        #self.handler.master_dom.appendChild( node )
+
+    def del_row( self ):
+        """deletes the miniature associated with the currently selected
+        row. BUG BUG BUG this method should drop a child from the DOM but
+        does not.
+        """
+        if self.selectedRow > -1:
+            pos = self.selectedRow
+            list = self.handler.master_dom.getElementsByTagName(TAG_MINIATURE)
+            self.handler.master_dom.removeChild( list[pos] )
+            self.DeleteRows( pos, 1 )
+            list = self.getList()
+            del list[ pos ]
+
+    def on_cell_change( self, evt ):
+        """Event handler for cell selection changes. selected row is used
+        to update data for that row.
+        """
+        row = evt.GetRow()
+        self.update_data_row( row )
+
+    def update_all( self ):
+        """ensures that the grid is displaying the correct number of
+        rows, and then updates all data displayed by the grid
+        """
+        list = self.getList()
+        count = 0
+        for n in list:
+            for k in n.getAttributeKeys():
+                if k not in self.keys:
+                    self.keys.append( k )
+        count = len( self.keys )
+        if self.GetNumberCols() < count:
+            self.AppendCols( count - self.GetNumberCols() )
+            for k in self.keys:
+                self.SetColLabelValue( self.keys.index( k ), k )
+        count = len( list )
+        rowcount = self.GetNumberRows()
+        if ( count > rowcount ):
+            total = count - rowcount
+            self.AppendRows( total )
+        elif ( count < rowcount ):
+            total = rowcount - count
+            self.DeleteRows( 0, total );
+        for index in range( 0, count ):
+            self.update_grid_row( index )
+
+    def getSelectedLabel( self ):
+        """Returns the label for the selected row
+        """
+        return self.GetTable().GetValue( self.selectedRow, 0 )
+
+    def getSelectedURL( self ):
+        """Returns the URL for the selected row
+        """
+        return self.GetTable().GetValue( self.selectedRow, 1 )
+
+    def getSelectedSerial( self ):
+        """Returns the ATTRIBUTE_UNIQUE value for the selected row
+        """
+        return self.GetTable().GetValue( self.selectedRow, 2 )
+
+    def update_grid_row( self, row ):
+        """Updates the specified grid row with data from the DOM node
+        specified by 'row'
+        """
+        list = self.getList()
+        item = list[ row ]
+        # self.GetTable().SetValue( row, 0, item.getAttribute(ATTRIBUTE_NAME) )
+        # self.GetTable().SetValue( row, 1, item.getAttribute(ATTRIBUTE_URL) )
+        # self.GetTable().SetValue( row, 2, item.getAttribute(ATTRIBUTE_UNIQUE) )
+        for key in self.keys:
+            self.GetTable().SetValue( row, self.keys.index( key ), item.getAttribute( key ) )
+
+    def update_data_row( self, row ):
+        """Updates the DOM nodw 'row' with grid data from 'row'
+        """
+        list = self.getList()
+        item = list[ row ]
+        for key in self.keys:
+            item.setAttribute( key, string.strip( self.GetTable().GetValue( row, self.keys.index( key ) ) ) )
+        # item.setAttribute( ATTRIBUTE_NAME, string.strip( self.GetTable().GetValue( row, 0 ) ) )
+        # item.setAttribute( ATTRIBUTE_URL, string.strip( self.GetTable().GetValue( row, 1 ) ) )
+        # item.setAttribute( ATTRIBUTE_UNIQUE, string.strip( self.GetTable().GetValue( row, 2 ) ) )
+        # self.GetTable().SetValue( row, 0, item.getAttribute(ATTRIBUTE_NAME) )
+        # self.GetTable().SetValue( row, 1, item.getAttribute(ATTRIBUTE_URL) )
+        # self.GetTable().SetValue( row, 2, item.getAttribute(ATTRIBUTE_UNIQUE) )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/minilib.py~	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,578 @@
+# 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: minilib.py
+# Author: Ted Berg
+# Maintainer:
+# Version:
+#   $Id: minilib.py,v 1.28 2007/04/22 22:00:18 digitalxero Exp $
+#
+# Description: nodehandler for a collection of miniatures.
+#
+
+__version__ = "$Id: minilib.py,v 1.28 2007/04/22 22:00:18 digitalxero Exp $"
+
+"""Nodehandler for collections of miniatures.  User can add, delete, edit
+miniatures as sending them to the map singly or in batches.
+"""
+from core import *
+import orpg.dirpath
+import string
+import map_miniature_nodehandler
+import orpg.mapper.map_msg
+from orpg.mapper.images import ImageHandler
+# import scriptkit
+
+# Constants
+TO_MINILIB_MAP = {'path':'url', 'label':'name', 'id':None, 'action':None}
+FROM_MINILIB_MAP = {'url':'path', 'name':'label', 'unique':None}
+CORE_ATTRIBUTES = ['name', 'url', 'unique', 'posy', 'posx', 'hide', 'face', 'heading', 'align', 'locked', 'width', 'height']
+
+ATTRIBUTE_NAME = 'name'
+ATTRIBUTE_URL = 'url'
+ATTRIBUTE_UNIQUE = 'unique'
+ATTRIBUTE_ID = 'id'
+ATTRIBUTE_POSX = 'posx'
+ATTRIBUTE_POSY = 'posy'
+
+TAG_MINIATURE = 'miniature'
+
+COMPONENT_MAP = 'map'
+COMPONENT_SESSION = 'session'
+# <nodehandler name='?' module='minilib' class='minilib_handler'>
+#     <miniature name='?' url='?' unique='?'></miniature>
+# </nodehandler>
+
+class minilib_handler( node_handler ):
+    """A nodehandler that manages a collection of miniatures for the
+    map.
+    <pre>
+        &lt;nodehandler name='?' module='minilib' class='minilib_handler'&gt;
+            &lt;miniature name='?' url='?' unique='?'&gt;&lt;/miniature&gt;
+        &lt;/nodehandler&gt;
+    </pre>
+    """
+    def __init__(self, xml_dom, tree_node):
+        """Instantiates the class, and sets all vars to their default state
+        """
+        node_handler.__init__(self, xml_dom, tree_node)
+
+        self.myeditor = None
+        self.mywindow = None
+        self.tree_node = tree_node
+        # self.xml_dom = xml_dom
+        self.update_leaves()
+        self.sanity_check_nodes()
+
+    def get_design_panel( self, parent ):
+        """returns an instance of the miniature library edit control ( see
+        on_design ).  This is for use with the the 'edit multiple nodes in a
+        single frame' code.
+        """
+        return minpedit( parent, self )
+
+    def get_use_panel( self, parent ):
+        """returns an instance of the miniature library view control ( see
+        on_use ).  This is for use with the the 'view multiple nodes in a
+        single frame' code.
+        """
+        return minilib_use_panel( parent, self )
+
+    def tohtml( self ):
+        """Returns an HTML representation of this node in string format.
+        The table columnwidths are currently being forced, as the wxHTML
+        widgets being used don't handle cells wider than the widgets are
+        expecting for a given column.
+        """
+        str = '<table border="2" >'
+        list = self.master_dom.getElementsByTagName(TAG_MINIATURE)
+        str += "<tr><th width='20%'>Label</th><th>Image</th><th width='65%'>URL</th><th>Unique</th></t>"
+        for mini in list:
+            url = mini.getAttribute(ATTRIBUTE_URL)
+            label = mini.getAttribute(ATTRIBUTE_NAME)
+            flag = 0
+            try:
+                flag = eval( mini.getAttribute(ATTRIBUTE_UNIQUE) )
+            except:
+                pass
+
+            show = 'yes'
+            if flag:
+                show = 'no'
+
+            str += """<tr>
+                <td> %s </td>
+                <td><img src="%s"></td>
+                <td> %s </td>
+                <td> %s </td>
+            </tr>""" % ( label, url, url, show )
+
+        str += "</table>"
+        print str
+        return str
+
+    def html_view( self ):
+        """see to_html
+        """
+        return self.tohtml()
+
+    def on_drop(self, evt):
+        drag_obj = self.tree.drag_obj
+        if drag_obj == self or self.tree.is_parent_node( self.mytree_node, drag_obj.mytree_node ):
+            return
+        if isinstance( drag_obj, minilib_handler ):
+            item = self.tree.GetSelection()
+            name = self.tree.GetItemText( item )
+        if isinstance( drag_obj, map_miniature_nodehandler.map_miniature_handler ):
+            xml_dom = self.tree.drag_obj.master_dom#.delete()
+            obj = xml_dom.firstChild
+            print obj.getAttributeKeys()
+            dict = {}
+            unique = ''
+            for attrib in obj.getAttributeKeys():
+                key = TO_MINILIB_MAP.get( attrib, attrib )
+                if key != None:
+                    dict[ key ] = obj.getAttribute( attrib )
+            dict[ ATTRIBUTE_UNIQUE ] = unique
+            self.new_mini( dict )
+
+
+    def new_mini( self, data={}, add=1 ):
+        mini = minidom.Element( TAG_MINIATURE )
+        for key in data.keys():
+            mini.setAttribute( key, data[ key ] )
+        for key in CORE_ATTRIBUTES:
+            if mini.getAttribute( key ) == '':
+                mini.setAttribute( key, '0' )
+        if add:
+            self.add_mini( mini )
+            self.add_leaf( mini )
+        return mini
+
+    def add_mini( self, mini ):
+        self.master_dom.appendChild( mini )
+
+    def add_leaf( self, mini, icon='gear' ):
+        tree = self.tree
+        icons = tree.icons
+        key = mini.getAttribute( ATTRIBUTE_NAME )
+        self.mydata.append( mini )
+
+    def update_leaves( self ):
+        self.mydata = []
+        nl = self.master_dom.getElementsByTagName( TAG_MINIATURE )
+        for n in nl:
+            self.add_leaf( n )
+
+
+    def on_drag( self, evt ):
+        print 'drag event caught'
+
+    def send_mini_to_map( self, mini, count=1, addName=True ):
+        if mini == None:
+            return
+        if mini.getAttribute( ATTRIBUTE_URL ) == '' or mini.getAttribute( 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.getAttribute( ATTRIBUTE_URL ), mini.getAttribute( ATTRIBUTE_NAME ) )) )
+            return
+        session = open_rpg.get_component( 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 )
+
+    def get_miniature_XML( self, mini, 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() )
+        for k in mini.getAttributeKeys():
+            # translate our attributes to map attributes
+            key = FROM_MINILIB_MAP.get( k, k )
+            if key != None:
+                if not addName and k == 'name':
+                    pass
+                else:
+                    msg.init_prop( key, mini.getAttribute( k ) )
+        unique = self.is_unique( mini )
+        if addName:
+            label = mini.getAttribute( ATTRIBUTE_NAME )
+        else:
+            label = ''
+        return msg.get_all_xml()
+
+    def is_unique( self, mini ):
+        unique = mini.getAttribute( ATTRIBUTE_UNIQUE )
+        val = 0
+        try:
+            val = eval( unique )
+        except:
+            val = len( unique )
+        return val
+
+    def sanity_check_nodes( self ):
+        nl = self.master_dom.getElementsByTagName( TAG_MINIATURE )
+        for node in nl:
+            if node.getAttribute( ATTRIBUTE_POSX ) == '':
+                node.setAttribute( ATTRIBUTE_POSX, '0' )
+            if node.getAttribute( ATTRIBUTE_POSY ) == '':
+                node.setAttribute( ATTRIBUTE_POSY, '0' )
+
+    def get_mini( self, index ):
+        try:
+            nl = self.master_dom.getElementsByTagName( TAG_MINIATURE )
+            return nl[ index ]
+        except:
+            return None
+
+class mini_handler( node_handler ):
+    def __init__( self, xml_dom, tree_node, handler ):
+        node_handler.__init__( self, xml_dom, tree_node)
+        self.handler = handler
+
+    def on_ldclick( self, evt ):
+        self.handler.send_mini_to_map( self.master_dom )
+
+    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,
+    a text field for entering a count ( for batch adds ) and 'add'/'done'
+    buttons.
+    """
+    def __init__( self, frame, handler ):
+        """Constructor.
+        """
+        wx.Panel.__init__( self, frame, -1 )
+        self.handler = handler
+        self.frame = frame
+
+        self.map = open_rpg.get_component('map')
+        names = self.buildList()
+        # self.keys = self.list.keys()
+        # self.keys.sort()
+
+
+        s = self.GetClientSizeTuple()
+
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        box = wx.BoxSizer(wx.HORIZONTAL)
+        self.listbox = wx.BitmapButton(self, wx.ID_ANY, names, wx.LB_EXTENDED)
+        self.count = wx.TextCtrl(self, wx.ID_ANY, '1')
+
+        box.Add( wx.StaticText( self, -1, 'Minis to add' ), 0, wx.EXPAND )
+        box.Add(wx.Size(10,10))
+        box.Add(self.count, 1, wx.EXPAND)
+
+        self.sizer.Add( self.listbox, 1, wx.EXPAND )
+        self.sizer.Add( box, 0, wx.EXPAND )
+
+        box = wx.BoxSizer( wx.HORIZONTAL )
+        self.okBtn = wx.Button(self, wx.ID_ANY, 'Add')
+        box.Add(self.okBtn, 0, wx.EXPAND)
+        self.addBtn = wx.Button(self, wx.ID_ANY, 'Add No Label')
+        box.Add(self.addBtn, 0, wx.EXPAND)
+        self.cancleBtn = wx.Button(self, wx.ID_ANY, 'Done')
+        box.Add(self.cancleBtn, 0, wx.EXPAND)
+
+        self.sizer.Add(wx.Size(10,10))
+        self.sizer.Add(box, 0, wx.EXPAND)
+        self.Bind(wx.EVT_BUTTON, self.on_ok, self.okBtn)
+        self.Bind(wx.EVT_BUTTON, self.on_ok, self.addBtn)
+        self.Bind(wx.EVT_BUTTON, self.on_close, self.cancleBtn)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+    def buildList( self ):
+        """Returns a dictionary of label => game tree miniature DOM node mappings.
+        """
+        list = self.handler.master_dom.getElementsByTagName(TAG_MINIATURE)
+        self.list = []
+        for mini in list:
+	    min_path = mini.getAttribute( ATTRIBUTE_URL )
+	    min_img = ImageHandler.load( min_path, 'miniature', 1)
+            self.list.append( mini.getAttribute( min_img ) )
+        return self.list
+        # self.list = {}
+        # for mini in list:
+        #     name = mini.getAttribute( ATTRIBUTE_NAME )
+        #     if name == '':
+        #         name = self.map.canvas.get_label_from_url( mini.getAttribute( ATTRIBUTE_URL ) )
+        #     self.list[ name ] = mini
+
+    def on_close(self, evt):
+        self.frame.Close()
+
+    def on_ok( self, evt ):
+        """Event handler for the 'add' button.
+        """
+        btn = self.FindWindowById(evt.GetId())
+        sendName = True
+        try:
+            count = eval( self.count.GetValue() )
+        except:
+            count = 1
+
+        try:
+            if eval( unique ):
+                count = 1
+            unique = eval( unique )
+        except:
+            pass
+
+        if btn.GetLabel() == 'Add No Label':
+            sendName = False
+        for index in self.listbox.GetSelections():
+            self.handler.send_mini_to_map( self.handler.get_mini( index ), count, sendName )
+
+
+class minpedit(wx.Panel):
+    """Panel for editing game tree miniature nodes.  Node information
+    is displayed in a grid, and buttons are provided for adding, deleting
+    nodes, and for sending minis to the map ( singly and in batches ).
+    """
+    def __init__( self, frame, handler ):
+        """Constructor.
+        """
+        wx.Panel.__init__( self, frame, -1 )
+        self.handler = handler
+        self.frame = frame
+
+        self.sizer = wx.BoxSizer( wx.VERTICAL )
+        self.grid = minilib_grid( self, handler )
+
+        bbox = wx.BoxSizer( wx.HORIZONTAL )
+        newMiniBtn = wx.Button( self, wx.ID_ANY, "New mini" )
+        delMiniBtn = wx.Button( self, wx.ID_ANY, "Del mini" )
+        addMiniBtn = wx.Button( self, wx.ID_ANY, "Add 1" )
+        addBatchBtn = wx.Button( self, wx.ID_ANY, "Add Batch" )
+        bbox.Add(newMiniBtn, 0, wx.EXPAND )
+        bbox.Add(delMiniBtn, 0, wx.EXPAND )
+        bbox.Add(wx.Size(10,10))
+        bbox.Add(addMiniBtn, 0, wx.EXPAND )
+        bbox.Add(addBatchBtn, 0, wx.EXPAND )
+
+        self.sizer.Add( self.grid, 1, wx.EXPAND)
+        self.sizer.Add( bbox, 0)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.Bind(wx.EVT_BUTTON, self.add_mini, newMiniBtn)
+        self.Bind(wx.EVT_BUTTON, self.del_mini, delMiniBtn)
+        self.Bind(wx.EVT_BUTTON, self.send_to_map, addMiniBtn)
+        self.Bind(wx.EVT_BUTTON, self.send_group_to_map, addBatchBtn)
+
+    def add_mini( self, evt=None ):
+        """Event handler for the 'New mini' button.  It calls
+        minilib_grid.add_row
+        """
+        self.grid.add_row()
+
+    def del_mini( self, evt=None ):
+        """Event handler for the 'Del mini' button.  It calls
+        minilib_grid.del_row
+        """
+        self.grid.del_row()
+
+    def send_to_map( self, evt=None ):
+        """Event handler for the 'Add 1' button.  Sends the
+        miniature defined by the currently selected row to the map, once.
+        """
+        index = self.grid.GetGridCursorRow()
+        self.handler.send_mini_to_map( self.handler.get_mini( index ) )
+
+    def send_group_to_map( self, evt=None ):
+        """Event handler for the 'Add batch' button.  Querys the user
+        for a mini count and sends the miniature defined by the currently
+        selected row to the map, the specified number of times.
+        """
+        if self.grid.GetNumberRows() > 0:
+            dlg = wx.TextEntryDialog( self.frame,
+                'How many %s\'s do you want to add?' %
+                ( self.grid.getSelectedLabel() ), 'Batch mini add', '2' )
+            if dlg.ShowModal() == wx.ID_OK:
+                try:
+                    value = eval( dlg.GetValue() )
+                except:
+                    value = 0
+                # for loop in range( 0, value ):
+                #     self.send_to_map()
+                print 'getting selected index for batch send'
+                index = self.grid.GetGridCursorRow()
+                print 'sending batch to map'
+                self.handler.send_mini_to_map( self.handler.get_mini( index ), value )
+
+class minilib_grid(wx.grid.Grid):
+    """A wxGrid subclass designed for editing game tree miniature library
+    nodes.
+    """
+    def __init__( self, parent, handler ):
+        """Constructor.
+        """
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS )
+        self.parent = parent
+        self.handler = handler
+        #self.keys = [ ATTRIBUTE_NAME, ATTRIBUTE_URL, ATTRIBUTE_UNIQUE ]
+        self.keys = CORE_ATTRIBUTES
+        self.CreateGrid( 1, len( self.keys ) )
+        # self.SetColLabelValue( 0, 'Name' )
+        # self.SetColLabelValue( 1, 'URL' )
+        # self.SetColSize( 1, 250 )
+        # self.SetColLabelValue( 2, 'Unique' )
+        for key in self.keys:
+            self.SetColLabelValue( self.keys.index( key ), key )
+        self.update_all()
+        self.selectedRow = 0
+        self.AutoSizeColumns()
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.select_cell)
+
+    def update_cols( self ):
+        nl = self.handler.master_dom.getElementsByTagName( TAG_MINIATURE )
+        for n in nl:
+            for k in n.getAttributeKeys():
+                if k not in self.keys:
+                    self.keys.append( k )
+
+    def select_cell( self, evt ):
+        """Event handler for grid cell selection changes.  It stores the
+        last selected row in a variable for use by the add[*] and del_row
+        operations.
+        """
+        self.BeginBatch()
+        self.selectedRow = evt.GetRow()
+        self.SelectRow( self.selectedRow )
+        self.EndBatch()
+        evt.Skip()
+
+    def getList( self ):
+        """Returns the list of 'miniature' DOM elements associated with this
+        miniature library.
+        """
+        return self.handler.master_dom.getElementsByTagName( TAG_MINIATURE )
+
+    def add_row( self, count = 1 ):
+        """creates a new miniature node, and then adds it to the current
+        miniature library, and to the grid.
+        """
+        self.AppendRows( count )
+        node = self.handler.new_mini( {
+          ATTRIBUTE_NAME :' ',
+          ATTRIBUTE_URL :'http://'} )# minidom.Element( TAG_MINIATURE )
+        self.update_all()
+        #self.handler.master_dom.appendChild( node )
+
+    def del_row( self ):
+        """deletes the miniature associated with the currently selected
+        row. BUG BUG BUG this method should drop a child from the DOM but
+        does not.
+        """
+        if self.selectedRow > -1:
+            pos = self.selectedRow
+            list = self.handler.master_dom.getElementsByTagName(TAG_MINIATURE)
+            self.handler.master_dom.removeChild( list[pos] )
+            self.DeleteRows( pos, 1 )
+            list = self.getList()
+            del list[ pos ]
+
+    def on_cell_change( self, evt ):
+        """Event handler for cell selection changes. selected row is used
+        to update data for that row.
+        """
+        row = evt.GetRow()
+        self.update_data_row( row )
+
+    def update_all( self ):
+        """ensures that the grid is displaying the correct number of
+        rows, and then updates all data displayed by the grid
+        """
+        list = self.getList()
+        count = 0
+        for n in list:
+            for k in n.getAttributeKeys():
+                if k not in self.keys:
+                    self.keys.append( k )
+        count = len( self.keys )
+        if self.GetNumberCols() < count:
+            self.AppendCols( count - self.GetNumberCols() )
+            for k in self.keys:
+                self.SetColLabelValue( self.keys.index( k ), k )
+        count = len( list )
+        rowcount = self.GetNumberRows()
+        if ( count > rowcount ):
+            total = count - rowcount
+            self.AppendRows( total )
+        elif ( count < rowcount ):
+            total = rowcount - count
+            self.DeleteRows( 0, total );
+        for index in range( 0, count ):
+            self.update_grid_row( index )
+
+    def getSelectedLabel( self ):
+        """Returns the label for the selected row
+        """
+        return self.GetTable().GetValue( self.selectedRow, 0 )
+
+    def getSelectedURL( self ):
+        """Returns the URL for the selected row
+        """
+        return self.GetTable().GetValue( self.selectedRow, 1 )
+
+    def getSelectedSerial( self ):
+        """Returns the ATTRIBUTE_UNIQUE value for the selected row
+        """
+        return self.GetTable().GetValue( self.selectedRow, 2 )
+
+    def update_grid_row( self, row ):
+        """Updates the specified grid row with data from the DOM node
+        specified by 'row'
+        """
+        list = self.getList()
+        item = list[ row ]
+        # self.GetTable().SetValue( row, 0, item.getAttribute(ATTRIBUTE_NAME) )
+        # self.GetTable().SetValue( row, 1, item.getAttribute(ATTRIBUTE_URL) )
+        # self.GetTable().SetValue( row, 2, item.getAttribute(ATTRIBUTE_UNIQUE) )
+        for key in self.keys:
+            self.GetTable().SetValue( row, self.keys.index( key ), item.getAttribute( key ) )
+
+    def update_data_row( self, row ):
+        """Updates the DOM nodw 'row' with grid data from 'row'
+        """
+        list = self.getList()
+        item = list[ row ]
+        for key in self.keys:
+            item.setAttribute( key, string.strip( self.GetTable().GetValue( row, self.keys.index( key ) ) ) )
+        # item.setAttribute( ATTRIBUTE_NAME, string.strip( self.GetTable().GetValue( row, 0 ) ) )
+        # item.setAttribute( ATTRIBUTE_URL, string.strip( self.GetTable().GetValue( row, 1 ) ) )
+        # item.setAttribute( ATTRIBUTE_UNIQUE, string.strip( self.GetTable().GetValue( row, 2 ) ) )
+        # self.GetTable().SetValue( row, 0, item.getAttribute(ATTRIBUTE_NAME) )
+        # self.GetTable().SetValue( row, 1, item.getAttribute(ATTRIBUTE_URL) )
+        # self.GetTable().SetValue( row, 2, item.getAttribute(ATTRIBUTE_UNIQUE) )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/nodehandler_version.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,3 @@
+### this file holds the nodehandler version ###
+
+NODEHANDLER_VERSION = "1.0"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/rpg_grid.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,498 @@
+# 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: rpg_grid.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: rpg_grid.py,v 1.20 2006/11/15 12:11:24 digitalxero Exp $
+#
+# Description: The file contains code for the grid nodehanlers
+#
+
+__version__ = "$Id: rpg_grid.py,v 1.20 2006/11/15 12:11:24 digitalxero Exp $"
+
+from core import *
+from forms import *
+
+class rpg_grid_handler(node_handler):
+    """ Node handler for rpg grid tool
+<nodehandler module='rpg_grid' class='rpg_grid_handler' name='sample'>
+  <grid border='' autosize='1' >
+    <row>
+      <cell size='?'></cell>
+      <cell></cell>
+    </row>
+    <row>
+      <cell></cell>
+      <cell></cell>
+    </row>
+  </grid>
+  <macros>
+    <macro name=''/>
+  </macros>
+</nodehandler>
+    """
+    def __init__(self,xml_dom,tree_node):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.grid = self.master_dom.getElementsByTagName('grid')[0]
+        if self.grid.getAttribute("border") == "":
+            self.grid.setAttribute("border","1")
+        if self.grid.getAttribute("autosize") == "":
+            self.grid.setAttribute("autosize","1")
+        self.macros = self.master_dom.getElementsByTagName('macros')[0]
+        self.myeditor = None
+        self.refresh_rows()
+
+    def refresh_die_macros(self):
+        pass
+
+    def refresh_rows(self):
+        self.rows = {}
+        tree = self.tree
+        icons = self.tree.icons
+        tree.CollapseAndReset(self.mytree_node)
+        node_list = self.master_dom.getElementsByTagName('row')
+        for n in node_list:
+            cells = n.getElementsByTagName('cell')
+            t_node = cells[0]._get_firstChild()
+            if t_node == None:
+                name = "Row"
+            else:
+                name = t_node._get_nodeValue()
+            if name == "":
+                name = "Row"
+            new_tree_node = tree.AppendItem(self.mytree_node,name,icons['gear'],icons['gear'])
+            handler = grid_row_handler(n,new_tree_node,self)
+            tree.SetPyData(new_tree_node,handler)
+
+
+    def tohtml(self):
+        border = self.grid.getAttribute("border")
+        name = self.master_dom.getAttribute('name')
+        rows = self.grid.getElementsByTagName('row')
+        colspan = str(len(rows[0].getElementsByTagName('cell')))
+        html_str = "<table border=\""+border+"\" align=center><tr bgcolor=\""+TH_BG+"\" ><th colspan="+colspan+">"+name+"</th></tr>"
+        for r in rows:
+            cells = r.getElementsByTagName('cell')
+            html_str += "<tr>"
+            for c in cells:
+                #html_str += "<td width='"+c.getAttribute('size')+"' >" bug here
+                html_str += "<td >"
+                t_node = c._get_firstChild()
+                if t_node == None:
+                    html_str += "<br /></td>"
+                else:
+                    html_str += t_node._get_nodeValue() + "</td>"
+            html_str += "</tr>"
+        html_str += "</table>"
+        return html_str
+
+    def get_design_panel(self,parent):
+        return rpg_grid_edit_panel(parent,self)
+
+    def get_use_panel(self,parent):
+        return rpg_grid_panel(parent,self)
+
+    def get_size_constraint(self):
+        return 1
+
+    def is_autosized(self):
+        return self.grid.getAttribute("autosize")
+
+    def set_autosize(self,autosize=1):
+        self.grid.setAttribute("autosize",str(autosize))
+
+class grid_row_handler(node_handler):
+    """ Node Handler grid row.
+    """
+    def __init__(self,xml_dom,tree_node,parent):
+        node_handler.__init__(self,xml_dom,tree_node)
+        self.drag = False
+
+    def on_drop(self,evt):
+        pass
+
+    def can_clone(self):
+        return 0;
+
+    def tohtml(self):
+        cells = self.master_dom.getElementsByTagName('cell')
+        html_str = "<table border=1 align=center><tr >"
+        for c in cells:
+            html_str += "<td >"
+            t_node = c._get_firstChild()
+            if t_node == None:
+                html_str += "<br /></td>"
+            else:
+                html_str += t_node._get_nodeValue() + "</td>"
+            html_str += "</tr>"
+        html_str += "</table>"
+        return html_str
+
+class MyCellEditor(wx.grid.PyGridCellEditor):
+    """
+    This is a sample GridCellEditor that shows you how to make your own custom
+    grid editors.  All the methods that can be overridden are show here.  The
+    ones that must be overridden are marked with "*Must Override*" in the
+    docstring.
+
+    Notice that in order to call the base class version of these special
+    methods we use the method name preceded by "base_".  This is because these
+    methods are "virtual" in C++ so if we try to call wxGridCellEditor.Create
+    for example, then when the wxPython extension module tries to call
+    ptr->Create(...) then it actually calls the derived class version which
+    looks up the method in this class and calls it, causing a recursion loop.
+    If you don't understand any of this, don't worry, just call the "base_"
+    version instead.
+
+    ----------------------------------------------------------------------------
+    This class is copied from the wxPython examples directory and was written by
+    Robin Dunn.
+
+    I have pasted it directly in and removed all references to "log"
+
+    -- Andrew
+
+    """
+    def __init__(self):
+        wx.grid.PyGridCellEditor.__init__(self)
+
+
+    def Create(self, parent, id, evtHandler):
+        """
+        Called to create the control, which must derive from wxControl.
+        *Must Override*
+        """
+        self._tc = wx.TextCtrl(parent, id, "", style=wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB)
+        self._tc.SetInsertionPoint(0)
+        self.SetControl(self._tc)
+        if evtHandler:
+            self._tc.PushEventHandler(evtHandler)
+
+
+    def SetSize(self, rect):
+        """
+        Called to position/size the edit control within the cell rectangle.
+        If you don't fill the cell (the rect) then be sure to override
+        PaintBackground and do something meaningful there.
+        """
+        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
+        to begin editing.  Set the focus to the edit control.
+        *Must Override*
+        """
+        self.startValue = grid.GetTable().GetValue(row, col)
+        self._tc.SetValue(self.startValue)
+        self._tc.SetInsertionPointEnd()
+        self._tc.SetFocus()
+
+        # 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
+        has changed.  If necessary, the control may be destroyed.
+        *Must Override*
+        """
+        changed = False
+
+        val = self._tc.GetValue()
+        if val != self.startValue:
+            changed = True
+            grid.GetTable().SetValue(row, col, val) # update the table
+
+        self.startValue = ''
+        self._tc.SetValue('')
+        return changed
+
+
+    def Reset(self):
+        """
+        Reset the value in the control back to its starting value.
+        *Must Override*
+        """
+        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
+        version only checks that the event has no modifiers.  F2 is special
+        and will always start the editor.
+        """
+
+        ## Oops, there's a bug here, we'll have to do it ourself..
+        ##return self.base_IsAcceptedKey(evt)
+
+        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
+        called to let the editor do something about that first key if desired.
+        """
+        key = evt.GetKeyCode()
+        ch = None
+        if key in [wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3, wx.WXK_NUMPAD4,
+                   wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, wx.WXK_NUMPAD8, wx.WXK_NUMPAD9]:
+            ch = ch = chr(ord('0') + key - wx.WXK_NUMPAD0)
+
+        elif key < 256 and key >= 0 and chr(key) in string.printable:
+            ch = chr(key)
+            if not evt.ShiftDown():
+                ch = string.lower(ch)
+
+        if ch is not None:
+            # For this example, replace the text.  Normally we would append it.
+            self._tc.AppendText(ch)
+        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
+        *Must Override*
+        """
+        return MyCellEditor()
+
+
+
+class rpg_grid(wx.grid.Grid):
+    """grid for attacks"""
+    def __init__(self, parent, handler):
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.parent = parent
+        self.handler = handler
+
+        #  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())
+
+        self.rows = handler.grid.getElementsByTagName('row')
+        rows = len(self.rows)
+        cols = len(self.rows[0].getElementsByTagName('cell'))
+        self.CreateGrid(rows,cols)
+        self.SetRowLabelSize(0)
+        self.SetColLabelSize(0)
+        self.set_col_widths()
+
+        for i in range(0,len(self.rows)):
+            self.refresh_row(i)
+
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.Bind(wx.grid.EVT_GRID_COL_SIZE, self.on_col_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.on_leftdclick)
+
+
+    def on_leftdclick(self,evt):
+        if self.CanEnableCellControl():
+            self.EnableCellEditControl()
+
+    def on_col_size(self, evt):
+        col = evt.GetRowOrCol()
+        cells = self.rows[0].getElementsByTagName('cell')
+        size = self.GetColSize(col)
+        cells[col].setAttribute('size',str(size))
+        evt.Skip()
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        value = self.GetCellValue(row,col)
+        cells = self.rows[row].getElementsByTagName('cell')
+        t_node = cells[col]._get_firstChild()
+        print t_node
+        t_node._set_nodeValue(value)
+        if col == 0:
+            self.handler.refresh_rows()
+
+    def set_col_widths(self):
+        cells = self.rows[0].getElementsByTagName('cell')
+        for i in range(0,len(cells)):
+            try:
+                size = int(cells[i].getAttribute('size'))
+                self.SetColSize(i,size)
+            except:
+                continue
+
+    def refresh_row(self,rowi):
+        cells = self.rows[rowi].getElementsByTagName('cell')
+        for i in range(0,len(cells)):
+            t_node = cells[i]._get_firstChild()
+            if t_node == None:
+                #doc = cells[i].ownerDocument
+                #t_node = doc.createTextNode("")
+                t_node = minidom.Text("")
+                t_node = cells[i].appendChild(t_node)
+            self.SetCellValue(rowi,i,t_node._get_nodeValue())
+
+    def add_row(self,evt=None):
+        cols = self.GetNumberCols()
+        #doc = self.handler.grid.ownerDocument
+        #row = doc.createElement('row')
+        row = minidom.Element('row')
+        for i in range(0,cols):
+            #cell = doc.createElement('cell')
+            cell = minidom.Element('cell')
+            #t_node = doc.createTextNode("")
+            t_node = minidom.Text("")
+            t_node = cell.appendChild(t_node)
+            row.appendChild(cell)
+        self.handler.grid.appendChild(row)
+        self.AppendRows(1)
+        self.rows = self.handler.grid.getElementsByTagName('row')
+        self.handler.refresh_rows()
+
+    def add_col(self,evt=None):
+        #doc = self.handler.grid.ownerDocument
+        for r in self.rows:
+            #cell = doc.createElement('cell')
+            cell = minidom.Element('cell')
+            #t_node = doc.createTextNode("")
+            t_node = minidom.Text("")
+            t_node = cell.appendChild(t_node)
+            r.appendChild(cell)
+        self.AppendCols(1)
+        self.fit_cols()
+
+
+    def del_row(self,evt=None):
+        num = self.GetNumberRows()
+        row = self.rows[num-1]
+        self.handler.grid.removeChild(row)
+        self.DeleteRows(num-1,1)
+        self.rows = self.handler.grid.getElementsByTagName('row')
+        self.handler.refresh_rows()
+
+    def del_col(self,evt=None):
+        num = self.GetNumberCols()
+        for r in self.rows:
+            cells = r.getElementsByTagName('cell')
+            r.removeChild(cells[num-1])
+        self.DeleteCols(num-1,1)
+        self.fit_cols()
+
+
+G_TITLE = wx.NewId()
+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)
+        label = handler.master_dom.getAttribute('name')
+        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.main_sizer.Add(wx.StaticText(self, -1, label+": "), 0, wx.EXPAND)
+        self.main_sizer.Add(self.grid,1,wx.EXPAND)
+        self.SetSizer(self.main_sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        parent.SetSize(self.GetBestSize())
+
+
+G_AUTO_SIZE = wx.NewId()
+G_ADD_ROW = wx.NewId()
+G_ADD_COL = wx.NewId()
+G_DEL_ROW = wx.NewId()
+G_DEL_COL = wx.NewId()
+
+class rpg_grid_edit_panel(wx.Panel):
+    def __init__(self, parent, handler):
+        wx.Panel.__init__(self, parent, -1)
+        self.handler = handler
+        self.grid = rpg_grid(self,handler)
+        self.title = wx.TextCtrl(self, G_TITLE, handler.master_dom.getAttribute('name'))
+
+        radio_b = wx.RadioBox(self, GRID_BOR, "Border (HTML)", choices=["no","yes"])
+        border = handler.grid.getAttribute("border")
+        radio_b.SetSelection(int(border))
+
+        self.auto_size = wx.CheckBox(self, G_AUTO_SIZE, " Auto Size")
+        if handler.is_autosized() == '1':
+            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.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)
+
+        self.SetSizer(self.main_sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.Bind(wx.EVT_TEXT, self.on_text, id=G_TITLE)
+        self.Bind(wx.EVT_BUTTON, self.grid.add_row, id=G_ADD_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.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_auto_size, id=G_AUTO_SIZE)
+
+    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.setAttribute("border",str(index))
+
+    def on_text(self,evt):
+        txt = self.title.GetValue()
+        if txt != "":
+            self.handler.master_dom.setAttribute('name',txt)
+            self.handler.rename(txt)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/gametree/nodehandlers/voxchat.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,68 @@
+# 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: voxchat.py
+# Author: Ted Berg
+# Maintainer:
+# Version:
+#   $Id: voxchat.py,v 1.37 2007/05/06 16:42:55 digitalxero Exp $
+#
+# Description: nodehandler for alias.
+#
+
+__version__ = "$Id: voxchat.py,v 1.37 2007/05/06 16:42:55 digitalxero Exp $"
+
+import re
+import os
+import string
+
+from orpg.orpg_windows import *
+import core
+import orpg.tools.scriptkit
+import orpg.tools.predTextCtrl
+import orpg.tools.rgbhex
+import orpg.tools.inputValidator
+
+import orpg.dirpath
+import orpg.minidom
+import orpg.networking.mplay_client
+from orpg.orpgCore import open_rpg
+
+#
+# Good things to add:
+# 1.u'use filter' per alias  [ this should be done ]
+# 2. make aliases remember which filter they're using  [ lisbox in gtk appaears to ignore SetSelection( <= 0 )
+#
+
+
+class voxchat_handler(core.node_handler):
+    def __init__(self, xml_dom, tree_node):
+        core.node_handler.__init__( self, xml_dom, tree_node)
+        self.node = xml_dom
+        self.xml = open_rpg.get_component('xml')
+
+    def get_design_panel( self, parent ):
+        aliasLib = open_rpg.get_component('alias')
+        aliasLib.ImportFromTree(self.node)
+        return None
+
+    def get_use_panel( self, parent ):
+        aliasLib = open_rpg.get_component('alias')
+        aliasLib.ImportFromTree(self.node)
+        return None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/main.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1241 @@
+#!/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: main.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: main.py,v 1.153 2008/01/24 03:52:03 digitalxero Exp $
+#
+# Description: This is the main entry point of the oprg application
+#
+
+__version__ = "$Id: main.py,v 1.153 2008/01/24 03:52:03 digitalxero Exp $"
+
+from orpg.orpg_wx import *
+from orpg.orpgCore import *
+from orpg_version import *
+from orpg.orpg_windows import *
+import orpg.dirpath
+import orpg.orpg_xml
+import orpg.player_list
+import orpg.tools.pluginui as pluginUI
+import orpg.tools.orpg_settings
+import orpg.tools.orpg_log
+import orpg.tools.aliaslib
+from orpg.tools.metamenus import MenuBarEx
+import orpg.tools.toolBars
+import orpg.tools.passtool
+import orpg.tools.orpg_sound
+import orpg.tools.validate
+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 wx.py
+
+####################################
+## Main Frame
+####################################
+
+
+class orpgFrame(wx.Frame):
+    def __init__(self, parent, id, title):
+        wx.Frame.__init__(self, parent, id, title, wx.Point(100, 100), wx.Size(600,420), style=wx.DEFAULT_FRAME_STYLE)
+        self.log = open_rpg.get_component("log")
+        self.xml = open_rpg.get_component("xml")
+        self.dir_struct = open_rpg.get_component("dir_struct")
+        self.validate = open_rpg.get_component("validate")
+        self.settings = open_rpg.get_component("settings")
+        self.log.log("Enter orpgFrame", ORPG_DEBUG)
+        self.rgbcovert = orpg.tools.rgbhex.RGBHex()
+        self._mgr = AUI.AuiManager(self)
+
+        # Determine which icon format to use
+        icon = None
+        if wx.Platform == '__WXMSW__':
+            icon = wx.Icon(orpg.dirpath.dir_struct["icon"]+'d20.ico', wx.BITMAP_TYPE_ICO)
+        else:
+            icon = wx.Icon(orpg.dirpath.dir_struct["icon"]+"d20.xpm", wx.BITMAP_TYPE_XPM) # create the object, then determine if it needs to be replaced.  It calculates 2 less calculations.
+
+        # Set it if we have it
+        if icon != None:
+            self.SetIcon( icon )
+
+        # create session
+        call_backs = {"on_receive":self.on_receive,
+                "on_mplay_event":self.on_mplay_event,
+                "on_group_event":self.on_group_event,
+                "on_player_event":self.on_player_event,
+                "on_status_event":self.on_status_event,
+                "on_password_signal":self.on_password_signal,
+                "orpgFrame":self}
+        self.session = orpg.networking.mplay_client.mplay_client(self.settings.get_setting("player"), call_backs)
+        self.poll_timer = wx.Timer(self, wx.NewId())
+        self.Bind(wx.EVT_TIMER, self.session.poll, self.poll_timer)
+        self.poll_timer.Start(100)
+        self.ping_timer = wx.Timer(self, wx.NewId())
+        self.Bind(wx.EVT_TIMER, self.session.update, self.ping_timer)
+
+        # create roller manager
+        self.DiceManager = orpg.dieroller.utils.roller_manager(self.settings.get_setting("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('password_manager', self.password_manager)
+
+        # build frame windows
+        self.build_menu()
+        self.build_gui()
+        self.build_hotkeys()
+        self.log.log("GUI Built", ORPG_DEBUG)
+        open_rpg.add_component("chat",self.chat)
+        open_rpg.add_component("map",self.map)
+        open_rpg.add_component("alias", self.aliaslib)
+        self.log.log("openrpg components all added", ORPG_DEBUG)
+        self.tree.load_tree(self.settings.get_setting("gametree"))
+        self.log.log("Tree Loaded", ORPG_DEBUG)
+        self.players.size_cols()
+        self.log.log("player window cols sized", ORPG_DEBUG)
+
+        #Load the Plugins This has to be after the chat component has been added
+        open_rpg.add_component('pluginmenu', self.pluginMenu)
+        self.pluginsFrame.Start()
+        self.log.log("plugins reloaded and startup plugins launched", ORPG_DEBUG)
+        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
+        self.log.log("Exit orpgFrame", ORPG_DEBUG)
+
+    def post_show_init(self):
+        """Some Actions need to be done after the main fram is drawn"""
+        self.log.log("Enter orpgFrame->post_show_init(self)", ORPG_DEBUG)
+        self.players.size_cols()
+        self.log.log("Exit orpgFrame->post_show_init(self)", ORPG_DEBUG)
+
+    def get_activeplugins(self):
+        self.log.log("Enter orpgFrame->get_activeplugins(self)", ORPG_DEBUG)
+        try:
+            tmp = self.pluginsFrame.get_activeplugins()
+        except:
+            tmp = {}
+        self.log.log("Exit orpgFrame->get_activeplugins(self)", ORPG_DEBUG)
+        return tmp
+
+    def get_startplugins(self):
+        self.log.log("Enter orpgFrame->get_startplugins(self)", ORPG_DEBUG)
+        try:
+            tmp = self.pluginsFrame.get_startplugins()
+        except:
+            tmp = {}
+        self.log.log("Exit orpgFrame->get_startplugins(self)", ORPG_DEBUG)
+        return tmp
+
+    def on_password_signal(self,signal,type,id,data):
+        self.log.log("Enter orpgFrame->on_password_signal(self,signal,type,id,data)", ORPG_DEBUG)
+
+        try:
+            self.log.log("DEBUG: password response= "+str(signal)+" (T:"+str(type)+"  #"+str(id)+")", ORPG_DEBUG)
+            id = int(id)
+            type = str(type)
+            data = str(data)
+            signal = str(signal)
+            if signal == "fail":
+                if type == "server":
+                    self.password_manager.ClearPassword("server", 0)
+                elif type == "admin":
+                    self.password_manager.ClearPassword("admin", int(id))
+                elif type == "room":
+                    self.password_manager.ClearPassword("room", int(id))
+                else:
+                    pass
+        except:
+            traceback.print_exc()
+        self.log.log("Exit orpgFrame->on_password_signal(self,signal,type,id,data)", ORPG_DEBUG)
+
+    def build_menu(self):
+        self.log.log("Enter orpgFrame->build_menu()", ORPG_DEBUG)
+        menu = \
+                [[
+                    ['&OpenRPG'],
+                    ['  &Settings\tCtrl-S'],
+                    ['  -'],
+                    ['  Tab Styles'],
+                    ['    Slanted'],
+                    ['      Colorful', "check"],
+                    ['      Black and White', "check"],
+                    ['      Aqua', "check"],
+                    ['      Custom', "check"],
+                    ['    Flat'],
+                    ['      Black and White', "check"],
+                    ['      Aqua', "check"],
+                    ['      Custom', "check"],
+                    ['  NewMap'],
+                    ['  -'],
+                    ['  &Exit']
+                ],
+                [
+                    ['&Game Server'],
+                    ['  &Browse Servers\tCtrl-B'],
+                    ['  -'],
+                    ['  Server Heartbeat', "check"],
+                    ['  -'],
+                    ['  &Start Server']
+                ],
+                [
+                    ['&Tools'],
+                    ['  Logging Level'],
+                    ['    Debug', "check"],
+                    ['    Note', "check"],
+                    ['    Info', "check"],
+                    ['    General', "check"],
+                    ['  -'],
+                    ['  Password Manager', "check"],
+                    ['  -'],
+                    ['  Sound Toolbar', "check"],
+                    ['  Dice Bar\tCtrl-D', "check"],
+                    ['  Map Bar', "check"],
+                    ['  Status Bar\tCtrl-T', "check"],
+                ],
+                [
+                    ['&Help'],
+                    ['  &About'],
+                    ['  Online User Guide'],
+                    ['  Change Log'],
+                    ['  Report a Bug']
+                ]]
+
+        self.mainmenu = MenuBarEx(self, menu)
+        if self.settings.get_setting('Heartbeat') == '1':
+            self.mainmenu.SetMenuState("GameServerServerHeartbeat", True)
+        tabtheme = self.settings.get_setting('TabTheme') 
+
+	#This change is stable. TaS.
+        self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedColorful", tabtheme == 'slanted&colorful')
+        self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedBlackandWhite", tabtheme == 'slanted&bw')
+        self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedAqua", tabtheme == 'slanted&aqua')
+        self.mainmenu.SetMenuState("OpenRPGTabStylesFlatBlackandWhite", tabtheme == 'flat&bw')
+        self.mainmenu.SetMenuState("OpenRPGTabStylesFlatAqua", tabtheme == 'flat&aqua')
+        self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedCustom", tabtheme == 'customslant')
+        self.mainmenu.SetMenuState("OpenRPGTabStylesFlatCustom", tabtheme == 'customflat')
+
+        lvl = int(self.settings.get_setting('LoggingLevel'))
+        if lvl & ORPG_DEBUG:
+            self.mainmenu.SetMenuState("ToolsLoggingLevelDebug", True)
+        if lvl & ORPG_DEBUG:
+            self.mainmenu.SetMenuState("ToolsLoggingLevelNote", True)
+        if lvl & ORPG_INFO:
+            self.mainmenu.SetMenuState("ToolsLoggingLevelInfo", True)
+        if lvl & ORPG_GENERAL:
+            self.mainmenu.SetMenuState("ToolsLoggingLevelGeneral", True)
+        self.pluginMenu = wx.Menu()
+        item = wx.MenuItem(self.pluginMenu, wx.ID_ANY, "Control Panel", "Control Panel")
+        self.Bind(wx.EVT_MENU, self.OnMB_PluginControlPanel, item)
+        self.pluginMenu.AppendItem(item)
+        self.pluginMenu.AppendSeparator()
+        self.mainmenu.Insert(2, self.pluginMenu, "&Plugins")
+        self.log.log("Exit orpgFrame->build_menu()", ORPG_DEBUG)
+
+    #################################
+    ## All Menu Events
+    #################################
+    #Tab Styles Menus
+    def SetTabStyles(self, *args, **kwargs):
+        self.log.log("Enter orpgFrame->SetTabStyles(self, *args, **kwargs)", ORPG_DEBUG)
+        if kwargs.has_key('style'):
+            newstyle = kwargs['style']
+        else:
+            try:
+                newstyle = args[1]
+            except:
+                self.log.log('Invalid Syntax for orpgFrame->SetTabStyles(self, *args, **kwargs)', ORPG_GENERAL)
+                return
+        if kwargs.has_key('menu'):
+            menu = kwargs['menu']
+        else:
+            try:
+                menu = args[0]
+            except:
+                self.log.log('Invalid Syntax for orpgFrame->SetTabStyles(self, *args, **kwargs)', ORPG_GENERAL)
+                return
+        if kwargs.has_key('graidentTo'):
+            graidentTo = kwargs['graidentTo']
+        else:
+            graidentTo = None
+        if kwargs.has_key('graidentFrom'):
+            graidentFrom = kwargs['graidentFrom']
+        else:
+            graidentFrom = None
+        if kwargs.has_key('textColor'):
+            textColor = kwargs['textColor']
+        else:
+            textColor = None
+
+        #Set all menu's to unchecked
+        self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedColorful", False)
+        self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedBlackandWhite", False)
+        self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedAqua", False)
+        self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedCustom", False)
+        self.mainmenu.SetMenuState("OpenRPGTabStylesFlatBlackandWhite", False)
+        self.mainmenu.SetMenuState("OpenRPGTabStylesFlatAqua", False)
+        self.mainmenu.SetMenuState("OpenRPGTabStylesFlatCustom", False)
+
+        #check the proper menu
+        self.mainmenu.SetMenuState(menu, True)
+
+        #Run though the current tabbed window list and remove those that have been closed
+        tabbedwindows = open_rpg.get_component("tabbedWindows")
+        rgbc = orpg.tools.rgbhex.RGBHex()
+        new = []
+        for wnd in tabbedwindows:
+            try:
+                style = wnd.GetWindowStyleFlag()
+                new.append(wnd)
+            except:
+                pass
+        tabbedwindows = new
+        open_rpg.add_component("tabbedWindows", tabbedwindows)
+
+        #Run though the new list and set the proper styles
+        tabbg = self.settings.get_setting('TabBackgroundGradient')
+        rgbc = orpg.tools.rgbhex.RGBHex()
+        (red, green, blue) = rgbc.rgb_tuple(tabbg)
+
+        for wnd in tabbedwindows:
+            style = wnd.GetWindowStyleFlag()
+            # remove old tabs style
+            mirror = ~(FNB.FNB_VC71 | FNB.FNB_VC8 | FNB.FNB_FANCY_TABS | FNB.FNB_COLORFUL_TABS)
+            style &= mirror
+            style |= newstyle
+            wnd.SetWindowStyleFlag(style)
+            wnd.SetTabAreaColour(wx.Color(red, green, blue))
+            if graidentTo != None:
+                wnd.SetGradientColourTo(graidentTo)
+            if graidentFrom != None:
+                wnd.SetGradientColourFrom(graidentFrom)
+            if textColor != None:
+                wnd.SetNonActiveTabTextColour(textColor)
+            wnd.Refresh()
+
+    def OnMB_OpenRPGNewMap(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGNewMap(self)", ORPG_DEBUG)
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGNewMap(self)", ORPG_DEBUG)
+
+    def OnMB_OpenRPGTabStylesSlantedColorful(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGTabStylesSlantedColorful(self)", ORPG_DEBUG)
+        if self.mainmenu.GetMenuState("OpenRPGTabStylesSlantedColorful"):
+            self.settings.set_setting('TabTheme', 'slanted&colorful')
+            self.SetTabStyles("OpenRPGTabStylesSlantedColorful", FNB.FNB_VC8|FNB.FNB_COLORFUL_TABS)
+        else:
+            self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedColorful", True)
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGTabStylesSlantedColorful(self)", ORPG_DEBUG)
+
+    def OnMB_OpenRPGTabStylesSlantedBlackandWhite(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGTabStylesSlantedBlackandWhite(self)", ORPG_DEBUG)
+        if self.mainmenu.GetMenuState("OpenRPGTabStylesSlantedBlackandWhite"):
+            self.settings.set_setting('TabTheme', 'slanted&bw')
+            self.SetTabStyles("OpenRPGTabStylesSlantedBlackandWhite", FNB.FNB_VC8, graidentTo=wx.WHITE, graidentFrom=wx.WHITE, textColor=wx.BLACK)
+        else:
+            self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedBlackandWhite", True)
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGTabStylesSlantedBlackandWhite(self)", ORPG_DEBUG)
+
+    def OnMB_OpenRPGTabStylesSlantedAqua(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGTabStylesSlantedAqua(self)", ORPG_DEBUG)
+        if self.mainmenu.GetMenuState("OpenRPGTabStylesSlantedAqua"):
+            self.settings.set_setting('TabTheme', 'slanted&aqua')
+            self.SetTabStyles("OpenRPGTabStylesSlantedAqua", FNB.FNB_VC8, graidentTo=wx.Color(0, 128, 255), graidentFrom=wx.WHITE, textColor=wx.BLACK)
+        else:
+            self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedAqua", True)
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGTabStylesSlantedBlackandWhite(self)", ORPG_DEBUG)
+
+    def OnMB_OpenRPGTabStylesSlantedCustom(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGTabStylesSlantedCustom(self)", ORPG_DEBUG)
+
+        if self.mainmenu.GetMenuState("OpenRPGTabStylesSlantedCustom"):
+            self.settings.set_setting('TabTheme', 'customslant')
+            rgbc = orpg.tools.rgbhex.RGBHex()
+            gfrom = self.settings.get_setting('TabGradientFrom')
+            (fred, fgreen, fblue) = rgbc.rgb_tuple(gfrom)
+            gto = self.settings.get_setting('TabGradientTo')
+            (tored, togreen, toblue) = rgbc.rgb_tuple(gto)
+            tabtext = self.settings.get_setting('TabTextColor')
+            (tred, tgreen, tblue) = rgbc.rgb_tuple(tabtext)
+            tabbg = self.settings.get_setting('TabBackgroundGradient')
+            (red, green, blue) = rgbc.rgb_tuple(tabbg)
+            self.SetTabStyles("OpenRPGTabStylesSlantedCustom", FNB.FNB_VC8, graidentTo=wx.Color(tored, togreen, toblue), graidentFrom=wx.Color(fred, fgreen, fblue), textColor=wx.Color(tred, tgreen, tblue))
+        else:
+            self.mainmenu.SetMenuState("OpenRPGTabStylesSlantedCustom", True)
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGTabStylesSlantedCustom(self)", ORPG_DEBUG)
+
+    def OnMB_OpenRPGTabStylesFlatBlackandWhite(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGTabStylesFlatBlackandWhite(self)", ORPG_DEBUG)
+        if self.mainmenu.GetMenuState("OpenRPGTabStylesFlatBlackandWhite"):
+            self.settings.set_setting('TabTheme', 'flat&bw')
+            self.SetTabStyles("OpenRPGTabStylesFlatBlackandWhite", FNB.FNB_FANCY_TABS, graidentTo=wx.WHITE, graidentFrom=wx.WHITE, textColor=wx.BLACK)
+        else:
+            self.mainmenu.SetMenuState("OpenRPGTabStylesFlatBlackandWhite", True)
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGTabStylesFlatBlackandWhite(self)", ORPG_DEBUG)
+
+    def OnMB_OpenRPGTabStylesFlatAqua(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGTabStylesFlatAqua(self)", ORPG_DEBUG)
+        if self.mainmenu.GetMenuState("OpenRPGTabStylesFlatAqua"):
+            self.settings.set_setting('TabTheme', 'flat&aqua')
+            self.SetTabStyles("OpenRPGTabStylesFlatAqua", FNB.FNB_FANCY_TABS, graidentTo=wx.Color(0, 128, 255), graidentFrom=wx.WHITE, textColor=wx.BLACK)
+        else:
+            self.mainmenu.SetMenuState("OpenRPGTabStylesFlatAqua", True)
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGTabStylesFlatAqua(self)", ORPG_DEBUG)
+
+    def OnMB_OpenRPGTabStylesFlatCustom(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGTabStylesFlatCustom(self)", ORPG_DEBUG)
+        if self.mainmenu.GetMenuState("OpenRPGTabStylesFlatCustom"):
+            self.settings.set_setting('TabTheme', 'customflat')
+            rgbc = orpg.tools.rgbhex.RGBHex()
+            gfrom = self.settings.get_setting('TabGradientFrom')
+            (fred, fgreen, fblue) = rgbc.rgb_tuple(gfrom)
+            gto = self.settings.get_setting('TabGradientTo')
+            (tored, togreen, toblue) = rgbc.rgb_tuple(gto)
+            tabtext = self.settings.get_setting('TabTextColor')
+            (tred, tgreen, tblue) = rgbc.rgb_tuple(tabtext)
+            tabbg = self.settings.get_setting('TabBackgroundGradient')
+            (red, green, blue) = rgbc.rgb_tuple(tabbg)
+            self.SetTabStyles("OpenRPGTabStylesFlatCustom", FNB.FNB_FANCY_TABS, graidentTo=wx.Color(tored, togreen, toblue), graidentFrom=wx.Color(fred, fgreen, fblue), textColor=wx.Color(tred, tgreen, tblue))
+        else:
+            self.mainmenu.SetMenuState("OpenRPGTabStylesFlatCustom", True)
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGTabStylesFlatCustom(self)", ORPG_DEBUG)
+
+    #Window Menu
+    def OnMB_WindowsMenu(self, event):
+        self.log.log("Enter orpgFrame->OnMB_WindowsMenu(self, event)", ORPG_DEBUG)
+        menuid = event.GetId()
+        name = self.mainwindows[menuid]
+        if name == 'Alias Lib':
+            if self.aliaslib.IsShown() == True:
+                self.aliaslib.Hide()
+            else:
+                self.aliaslib.Show()
+        else:
+            if self._mgr.GetPane(name).IsShown() == True:
+                self._mgr.GetPane(name).Hide()
+            else:
+                self._mgr.GetPane(name).Show()
+            self._mgr.Update()
+        self.log.log("Exit orpgFrame->OnMB_WindowsMenu(self, event)", ORPG_DEBUG)
+
+    #OpenRPG Menu
+    def OnMB_OpenRPGSettings(self):
+        self.log.log("Enter orpgFrame->OnMB_OpenRPGSettings()", ORPG_DEBUG)
+        dlg = orpg.tools.orpg_settings.orpgSettingsWnd(self)
+        dlg.Centre()
+        dlg.ShowModal()
+        self.log.log("Exit orpgFrame->OnMB_OpenRPGSettings()", ORPG_DEBUG)
+
+    def OnMB_OpenRPGExit(self):
+        self.OnCloseWindow(0)
+
+    #Game Server Menu
+    def OnMB_GameServerBrowseServers(self):
+        self.log.log("Enter orpgFrame->OnMB_GameServerBrowseServers(self)", ORPG_DEBUG)
+        if self._mgr.GetPane("Browse Server Window").IsShown() == True:
+            self._mgr.GetPane("Browse Server Window").Hide()
+        else:
+            self._mgr.GetPane("Browse Server Window").Show()
+        self._mgr.Update()
+        self.log.log("Exit orpgFrame->OnMB_GameServerBrowseServers(self)", ORPG_DEBUG)
+
+    def OnMB_GameServerServerHeartbeat(self):
+        self.log.log("Enter orpgFrame->OnMB_GameServerServerHeartbeat(self)", ORPG_DEBUG)
+        if self.mainmenu.GetMenuState("GameServerServerHeartbeat"):
+            self.settings.set_setting('Heartbeat', '1')
+        else:
+            self.settings.set_setting('Heartbeat', '0')
+        self.log.log("Exit orpgFrame->OnMB_GameServerServerHeartbeat(self)", ORPG_DEBUG)
+
+    def OnMB_GameServerStartServer(self):
+        self.log.log("Enter orpgFrame->OnMB_GameServerStartServer(self)", ORPG_DEBUG)
+        start_dialog = wx.ProgressDialog( "Server Loading", "Server Loading, Please Wait...", 1, self )
+        # Spawn the new process and close the stdout handle from it
+        start_dialog.Update( 0 )
+        # Adjusted following code to work with win32, can't test for Unix
+        # as per reported bug 586227
+        if wx.Platform == "__WXMSW__":
+            arg = '\"' + os.path.normpath(orpg.dirpath.dir_struct["home"] + 'start_server_gui.py') + '\"'
+            args = ( sys.executable, arg )
+        else:
+            arg = orpg.dirpath.dir_struct["home"] + 'start_server_gui.py'
+            args = (arg,arg)
+        os.spawnv( os.P_NOWAIT, sys.executable, args )
+        start_dialog.Update( 1 )
+        start_dialog.Show(False)
+        start_dialog.Destroy()
+        self.log.log("Exit orpgFrame->OnMB_GameServerStartServer(self)", ORPG_DEBUG)
+
+    # Tools Menu
+    def OnMB_PluginControlPanel(self, evt):
+        self.log.log("Enter orpgFrame->OnMB_ToolsPlugins(self)", ORPG_DEBUG)
+        if self.pluginsFrame.IsShown() == True:
+            self.pluginsFrame.Hide()
+        else:
+            self.pluginsFrame.Show()
+        self.log.log("Exit orpgFrame->OnMB_ToolsPlugins(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsLoggingLevelDebug(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsLoggingLevelDebug(self)", ORPG_DEBUG)
+        lvl = self.log.getLogLevel()
+        if self.mainmenu.GetMenuState("ToolsLoggingLevelDebug"):
+            lvl |= ORPG_DEBUG
+        else:
+            lvl &= ~ORPG_DEBUG
+        self.log.setLogLevel(lvl)
+        self.settings.set_setting('LoggingLevel', lvl)
+        self.log.log("Exit orpgFrame->OnMB_ToolsLoggingLevelDebug(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsLoggingLevelNote(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsLoggingLevelNote(self)", ORPG_DEBUG)
+        lvl = self.log.getLogLevel()
+        if self.mainmenu.GetMenuState("ToolsLoggingLevelNote"):
+            lvl |= ORPG_DEBUG
+        else:
+            lvl &= ~ORPG_DEBUG
+        self.log.setLogLevel(lvl)
+        self.settings.set_setting('LoggingLevel', lvl)
+        self.log.log("Exit orpgFrame->OnMB_ToolsLoggingLevelNote(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsLoggingLevelInfo(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsLoggingLevelInfo(self)", ORPG_DEBUG)
+        lvl = self.log.getLogLevel()
+        if self.mainmenu.GetMenuState("ToolsLoggingLevelInfo"):
+            lvl |= ORPG_INFO
+        else:
+            lvl &= ~ORPG_INFO
+        self.log.setLogLevel(lvl)
+        self.settings.set_setting('LoggingLevel', lvl)
+        self.log.log("Exit orpgFrame->OnMB_ToolsLoggingLevelInfo(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsLoggingLevelGeneral(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsLoggingLevelGeneral(self)", ORPG_DEBUG)
+        lvl = self.log.getLogLevel()
+        if self.mainmenu.GetMenuState("ToolsLoggingLevelGeneral"):
+            lvl |= ORPG_GENERAL
+        else:
+            lvl &= ~ORPG_GENERAL
+        self.log.setLogLevel(lvl)
+        self.settings.set_setting('LoggingLevel', lvl)
+        self.log.log("Exit orpgFrame->OnMB_ToolsLoggingLevelGeneral(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsPasswordManager(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsPasswordManager(self)", ORPG_DEBUG)
+        if self.mainmenu.GetMenuState("ToolsPasswordManager"):
+            self.password_manager.Enable()
+        else:
+            self.password_manager.Disable()
+        self.log.log("Exit orpgFrame->OnMB_ToolsPasswordManager(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsStatusBar(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsStatusBar(self)", ORPG_DEBUG)
+        if self._mgr.GetPane("Status Window").IsShown() == True:
+            self.mainmenu.SetMenuState("ToolsStatusBar", False)
+            self._mgr.GetPane("Status Window").Hide()
+        else:
+            self.mainmenu.SetMenuState("ToolsStatusBar", True)
+            self._mgr.GetPane("Status Window").Show()
+        self._mgr.Update()
+        self.log.log("Exit orpgFrame->OnMB_ToolsStatusBar(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsSoundToolbar(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsSoundToolbar(self)", ORPG_DEBUG)
+        if self._mgr.GetPane("Sound Control Toolbar").IsShown() == True:
+            self.mainmenu.SetMenuState("ToolsSoundToolbar", False)
+            self._mgr.GetPane("Sound Control Toolbar").Hide()
+        else:
+            self.mainmenu.SetMenuState("ToolsSoundToolbar", True)
+            self._mgr.GetPane("Sound Control Toolbar").Show()
+        self._mgr.Update()
+        self.log.log("Exit orpgFrame->OnMB_ToolsSoundToolbar(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsDiceBar(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsDiceBar(self)", ORPG_DEBUG)
+        if self._mgr.GetPane("Dice Tool Bar").IsShown() == True:
+            self.mainmenu.SetMenuState("ToolsDiceBar", False)
+            self._mgr.GetPane("Dice Tool Bar").Hide()
+        else:
+            self.mainmenu.SetMenuState("ToolsDiceBar", True)
+            self._mgr.GetPane("Dice Tool Bar").Show()
+        self._mgr.Update()
+        self.log.log("Exit orpgFrame->OnMB_ToolsDiceBar(self)", ORPG_DEBUG)
+
+    def OnMB_ToolsMapBar(self):
+        self.log.log("Enter orpgFrame->OnMB_ToolsMapBar(self)", ORPG_DEBUG)
+        if self._mgr.GetPane("Map Tool Bar").IsShown() == True:
+            self.mainmenu.SetMenuState("ToolsMapBar", False)
+            self._mgr.GetPane("Map Tool Bar").Hide()
+        else:
+            self.mainmenu.SetMenuState("ToolsMapBar", True)
+            self._mgr.GetPane("Map Tool Bar").Show()
+        self._mgr.Update()
+        self.log.log("Exit orpgFrame->OnMB_ToolsMapBar(self)", ORPG_DEBUG)
+
+    #Help Menu
+    def OnMB_HelpAbout(self):
+        "The about box.  We're making it n HTML about box because it's pretty cool!"
+        "Inspired by the wxWindows about.cpp sample."
+        topSizer = wx.BoxSizer( wx.VERTICAL )
+        dlg = wx.Dialog( self, -1, "About" )
+        html = AboutHTMLWindow( dlg, -1, wx.DefaultPosition, wx.Size(400, 200), wx.html.HW_SCROLLBAR_NEVER )
+        html.SetBorders( 0 )
+        replace_text = "VeRsIoNrEpLaCeMeNtStRiNg"
+        about_file = open(orpg.dirpath.dir_struct["template"]+"about.html","r")
+        about_text = about_file.read()
+        about_file.close()
+        display_text = string.replace(about_text,replace_text,VERSION)
+        html.SetPage(display_text)
+        html.SetSize( wx.Size(html.GetInternalRepresentation().GetWidth(),
+                             html.GetInternalRepresentation().GetHeight()) )
+        topSizer.Add( html, 1, wx.ALL, 10 )
+        topSizer.Add( wx.StaticLine( dlg, -1), 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10 )
+        Okay = wx.Button( dlg, wx.ID_OK, "Okay" )
+        Okay.SetDefault()
+        topSizer.Add( Okay, 0, wx.ALL | wx.ALIGN_RIGHT, 15 )
+        dlg.SetAutoLayout( True )
+        dlg.SetSizer( topSizer )
+        topSizer.Fit( dlg )
+        dlg.ShowModal()
+        dlg.Destroy()
+
+    def OnMB_HelpOnlineUserGuide(self):
+        wb = webbrowser.get()
+        wb.open("https://www.assembla.com/wiki/show/openrpg/User_Manual")
+
+    def OnMB_HelpChangeLog(self):
+        wb = webbrowser.get()
+        wb.open("http://www.assembla.com/spaces/milestones/index/openrpg?spaces_tool_id=Milestones")
+
+    def OnMB_HelpReportaBug(self):
+        wb = webbrowser.get()
+        wb.open("http://www.assembla.com/spaces/tickets/index/openrpg?spaces_tool_id=Tickets")
+
+
+    #################################
+    ##    Build the GUI
+    #################################
+    def build_gui(self):
+        self.log.log("Enter orpgFrame->build_gui()", ORPG_DEBUG)
+        self.Freeze()
+        self.validate.config_file("layout.xml","default_layout.xml")
+        filename = orpg.dirpath.dir_struct["user"] + "layout.xml"
+        temp_file = open(filename)
+        txt = temp_file.read()
+        xml_dom = self.xml.parseXml(txt)._get_documentElement()
+        temp_file.close()
+
+        #Plugins Window
+        self.pluginsFrame = pluginUI.PluginFrame(self)
+        open_rpg.add_component("plugins", self.get_activeplugins())
+        open_rpg.add_component("startplugs", self.get_startplugins())
+        self.windowsmenu = wx.Menu()
+        self.mainwindows = {}
+        self.log.log("Menu Created", ORPG_DEBUG)
+        h = int(xml_dom.getAttribute("height"))
+        w = int(xml_dom.getAttribute("width"))
+        posx = int(xml_dom.getAttribute("posx"))
+        posy = int(xml_dom.getAttribute("posy"))
+        maximized = int(xml_dom.getAttribute("maximized"))
+        self.SetDimensions(posx, posy, w, h)
+        self.log.log("Dimensions Set", ORPG_DEBUG)
+
+        # Sound Manager
+        self.sound_player = orpg.tools.orpg_sound.orpgSound(self)
+        open_rpg.add_component("sound", self.sound_player)
+        wndinfo = AUI.AuiPaneInfo()
+        wndinfo.DestroyOnClose(False)
+        wndinfo.Name("Sound Control Toolbar")
+        wndinfo.Caption("Sound Control Toolbar")
+        wndinfo.Float()
+        wndinfo.ToolbarPane()
+        wndinfo.Hide()
+        self._mgr.AddPane(self.sound_player, wndinfo)
+        children = xml_dom._get_childNodes()
+        for c in children:
+            self.build_window(c, self)
+
+        # status window
+        self.status = status_bar(self)
+        wndinfo = AUI.AuiPaneInfo()
+        wndinfo.DestroyOnClose(False)
+        wndinfo.Name("Status Window")
+        wndinfo.Caption("Status Window")
+        wndinfo.Float()
+        wndinfo.ToolbarPane()
+        wndinfo.Hide()
+        self._mgr.AddPane(self.status, wndinfo)
+        self.log.log("Status Window Created", ORPG_DEBUG)
+
+        # Create and show the floating dice toolbar
+        self.dieToolBar = orpg.tools.toolBars.DiceToolBar(self, callBack = self.chat.ParsePost)
+        wndinfo = AUI.AuiPaneInfo()
+        wndinfo.DestroyOnClose(False)
+        wndinfo.Name("Dice Tool Bar")
+        wndinfo.Caption("Dice Tool Bar")
+        wndinfo.Float()
+        wndinfo.ToolbarPane()
+        wndinfo.Hide()
+        self._mgr.AddPane(self.dieToolBar, wndinfo)
+        self.log.log("Dice Tool Bar Created", ORPG_DEBUG)
+
+        #Create the Map tool bar
+        self.mapToolBar = orpg.tools.toolBars.MapToolBar(self, callBack = self.map.MapBar)
+        wndinfo = AUI.AuiPaneInfo()
+        wndinfo.DestroyOnClose(False)
+        wndinfo.Name("Map Tool Bar")
+        wndinfo.Caption("Map Tool Bar")
+        wndinfo.Float()
+        wndinfo.ToolbarPane()
+        wndinfo.Hide()
+        self._mgr.AddPane(self.mapToolBar, wndinfo)
+        self.log.log("Map Tool Bar Created", ORPG_DEBUG)
+
+        #Create the Browse Server Window
+        self.gs = orpg.networking.gsclient.game_server_panel(self)
+        wndinfo = AUI.AuiPaneInfo()
+        wndinfo.DestroyOnClose(False)
+        wndinfo.Name("Browse Server Window")
+        wndinfo.Caption("Game Server")
+        wndinfo.Float()
+        wndinfo.Dockable(False)
+        wndinfo.MinSize(wx.Size(640,480))
+        wndinfo.Hide()
+        self._mgr.AddPane(self.gs, wndinfo)
+        self.log.log("Game Server Window Created", ORPG_DEBUG)
+
+        #Create the Alias Lib Window
+        self.aliaslib = orpg.tools.aliaslib.AliasLib()
+        self.aliaslib.Hide()
+        self.log.log("Alias Window Created", ORPG_DEBUG)
+        menuid = wx.NewId()
+        self.windowsmenu.Append(menuid, "Alias Lib", kind=wx.ITEM_CHECK)
+        self.windowsmenu.Check(menuid, False)
+        self.Bind(wx.EVT_MENU, self.OnMB_WindowsMenu, id=menuid)
+        self.mainwindows[menuid] = "Alias Lib"
+        self.mainmenu.Insert(3, self.windowsmenu, 'Windows')
+        self.log.log("Windows Menu Done", ORPG_DEBUG)
+        self._mgr.Update()
+        if wx.VERSION_STRING > "2.8":
+            self.Bind(AUI.EVT_AUI_PANE_CLOSE, self.onPaneClose)
+        else:
+            self.Bind(AUI.EVT_AUI_PANECLOSE, self.onPaneClose)
+        self.log.log("AUI Bindings Done", ORPG_DEBUG)
+
+        #Load the layout if one exists
+        layout = xml_dom.getElementsByTagName("DockLayout")
+        try:
+            textnode = self.xml.safe_get_text_node(layout[0])
+            self._mgr.LoadPerspective(textnode._get_nodeValue())
+        except:
+            pass
+        xml_dom.unlink()
+        self.log.log("Perspective Loaded", ORPG_DEBUG)
+        self._mgr.GetPane("Browse Server Window").Hide()
+        self._mgr.Update()
+        self.Maximize(maximized)
+        self.log.log("GUI is all created", ORPG_DEBUG)
+        self.Thaw()
+        self.log.log("Exit orpgFrame->build_gui()", ORPG_DEBUG)
+
+    def do_tab_window(self,xml_dom,parent_wnd):
+        self.log.log("Enter orpgFrame->do_tab_window(self,xml_dom,parent_wnd)", ORPG_DEBUG)
+
+        # if container window loop through childern and do a recursive call
+        temp_wnd = orpgTabberWnd(parent_wnd, style=FNB.FNB_ALLOW_FOREIGN_DND)
+        children = xml_dom._get_childNodes()
+        for c in children:
+            wnd = self.build_window(c,temp_wnd)
+            name = c.getAttribute("name")
+            temp_wnd.AddPage(wnd, name, False)
+        self.log.log("Exit orpgFrame->do_tab_window(self,xml_dom,parent_wnd)", ORPG_DEBUG)
+        return temp_wnd
+
+    def build_window(self, xml_dom, parent_wnd):
+        name = xml_dom._get_nodeName()
+        self.log.log("Enter orpgFrame->build_window(" + name + ")", ORPG_DEBUG)
+        if name == "DockLayout" or name == "dock":
+            return
+        dir = xml_dom.getAttribute("direction")
+        pos = xml_dom.getAttribute("pos")
+        height = xml_dom.getAttribute("height")
+        width = xml_dom.getAttribute("width")
+        cap = xml_dom.getAttribute("caption")
+        dockable = xml_dom.getAttribute("dockable")
+        layer = xml_dom.getAttribute("layer")
+
+        try:
+            layer = int(layer)
+            dockable = int(dockable)
+        except:
+            layer = 0
+            dockable = 1
+
+        if name == "tab":
+            temp_wnd = self.do_tab_window(xml_dom, parent_wnd)
+        elif name == "map":
+            temp_wnd = orpg.mapper.map.map_wnd(parent_wnd, -1)
+            self.map = temp_wnd
+        elif name == "tree":
+            temp_wnd = orpg.gametree.gametree.game_tree(parent_wnd, -1)
+            self.tree = temp_wnd
+            if self.settings.get_setting('ColorTree') == '1':
+                self.tree.SetBackgroundColour(self.settings.get_setting('bgcolor'))
+                self.tree.SetForegroundColour(self.settings.get_setting('textcolor'))
+            else:
+                self.tree.SetBackgroundColour('white')
+                self.tree.SetForegroundColour('black')
+
+        elif name == "chat":
+            temp_wnd = orpg.chat.chatwnd.chat_notebook(parent_wnd, wx.DefaultSize)
+            self.chattabs = temp_wnd
+            self.chat = temp_wnd.MainChatPanel
+
+        elif name == "player":
+            temp_wnd = orpg.player_list.player_list(parent_wnd)
+            self.players = temp_wnd
+            if self.settings.get_setting('ColorTree') == '1':
+                self.players.SetBackgroundColour(self.settings.get_setting('bgcolor'))
+                self.players.SetForegroundColour(self.settings.get_setting('textcolor'))
+            else:
+                self.players.SetBackgroundColour('white')
+                self.players.SetForegroundColour('black')
+        if parent_wnd != self:
+            #We dont need this if the window are beeing tabed
+            return temp_wnd
+        menuid = wx.NewId()
+        self.windowsmenu.Append(menuid, cap, kind=wx.ITEM_CHECK)
+        self.windowsmenu.Check(menuid, True)
+        self.Bind(wx.EVT_MENU, self.OnMB_WindowsMenu, id=menuid)
+        self.mainwindows[menuid] = cap
+        wndinfo = AUI.AuiPaneInfo()
+        wndinfo.DestroyOnClose(False)
+        wndinfo.Name(cap)
+        wndinfo.FloatingSize(wx.Size(int(width), int(height)))
+        wndinfo.BestSize(wx.Size(int(width), int(height)))
+        wndinfo.Layer(int(layer))
+        wndinfo.Caption(cap)
+
+# Lambda here should work! (future dev)
+        if dir.lower() == 'top':
+            wndinfo.Top()
+        elif dir.lower() == 'bottom':
+            wndinfo.Bottom()
+        elif dir.lower() == 'left':
+            wndinfo.Left()
+        elif dir.lower() == 'right':
+            wndinfo.Right()
+        elif dir.lower() == 'center':
+            wndinfo.Center()
+            wndinfo.CaptionVisible(False)
+
+        if dockable != 1:
+            wndinfo.Dockable(False)
+            wndinfo.Floatable(False)
+        if pos != '' or pos != '0' or pos != None:
+            wndinfo.Position(int(pos))
+        wndinfo.Show()
+        self._mgr.AddPane(temp_wnd, wndinfo)
+        self.log.log("Exit orpgFrame->build_window(" + name + ")", ORPG_DEBUG)
+        return temp_wnd
+
+    def onPaneClose(self, evt):
+        self.log.log("Enter orpgFrame->onPaneClose()", ORPG_DEBUG)
+        pane = evt.GetPane()
+        for wndid, wname in self.mainwindows.iteritems():
+            if pane.name == wname:
+                self.windowsmenu.Check(wndid, False)
+                break
+        evt.Skip()
+        self._mgr.Update()
+        self.log.log("Exit orpgFrame->onPaneClose()", ORPG_DEBUG)
+
+    def saveLayout(self):
+        self.log.log("Enter orpgFrame->saveLayout()", ORPG_DEBUG)
+        filename = orpg.dirpath.dir_struct["user"] + "layout.xml"
+        temp_file = open(filename)
+        txt = temp_file.read()
+        xml_dom = self.xml.parseXml(txt)._get_documentElement()
+        temp_file.close()
+        (x_size,y_size) = self.GetClientSize()
+        (x_pos,y_pos) = self.GetPositionTuple()
+        if self.IsMaximized():
+            max = 1
+        else:
+            max = 0
+        xml_dom.setAttribute("height", str(y_size))
+        xml_dom.setAttribute("width", str(x_size))
+        xml_dom.setAttribute("posx", str(x_pos))
+        xml_dom.setAttribute("posy", str(y_pos))
+        xml_dom.setAttribute("maximized", str(max))
+        layout = xml_dom.getElementsByTagName("DockLayout")
+        try:
+            textnode = self.xml.safe_get_text_node(layout[0])
+            textnode._set_nodeValue(str(self._mgr.SavePerspective()))
+        except:
+            elem = self.xml.minidom.Element('DockLayout')
+            elem.setAttribute("DO_NO_EDIT","True")
+            textnode = self.xml.safe_get_text_node(elem)
+            textnode._set_nodeValue(str(self._mgr.SavePerspective()))
+            xml_dom.appendChild(elem)
+        temp_file = open(filename, "w")
+        temp_file.write(xml_dom.toxml(1))
+        temp_file.close()
+        self.log.log("Exit saveLayout()", ORPG_DEBUG)
+
+    def build_hotkeys(self):
+        self.log.log("Enter orpgFrame->build_hotkeys(self)", ORPG_DEBUG)
+        self.mainmenu.accel.xaccel.extend(self.chat.get_hot_keys())
+        self.mainmenu.accel.xaccel.extend(self.map.get_hot_keys())
+        self.log.log("Exit orpgFrame->build_hotkeys(self)", ORPG_DEBUG)
+
+    def start_timer(self):
+        self.log.log("Enter orpgFrame->start_timer(self)", ORPG_DEBUG)
+        self.poll_timer.Start(100)
+        s = open_rpg.get_component('settings')
+        if s.get_setting("Heartbeat") == "1":
+            self.ping_timer.Start(1000*60)
+            self.log.log("starting heartbeat...", ORPG_DEBUG, True)
+        self.log.log("Exit orpgFrame->start_timer(self)", ORPG_DEBUG)
+
+    def kill_mplay_session(self):
+        self.log.log("Enter orpgFrame->kill_mplay_session(self)", ORPG_DEBUG)
+        self.game_name = ""
+        self.session.start_disconnect()
+        self.log.log("Exit orpgFrame->kill_mplay_session(self)", ORPG_DEBUG)
+
+    def quit_game(self, evt):
+        self.log.log("Enter orpgFrame->quit_game(self, evt)", ORPG_DEBUG)
+        dlg = wx.MessageDialog(self,"Exit gaming session?","Game Session",wx.YES_NO)
+        if dlg.ShowModal() == wx.ID_YES:
+            self.session.exitCondition.notifyAll()
+            dlg.Destroy()
+            self.kill_mplay_session()
+        self.log.log("Exit orpgFrame->quit_game(self, evt)", ORPG_DEBUG)
+
+    def on_status_event(self, evt):
+        self.log.log("Enter orpgFrame->on_status_event(self, evt)", ORPG_DEBUG)
+        id = evt.get_id()
+        status = evt.get_data()
+        if id == orpg.networking.mplay_client.STATUS_SET_URL:
+            self.status.set_url(status)
+        self.log.log("Exit orpgFrame->on_status_event(self, evt)", ORPG_DEBUG)
+
+    def on_player_event(self, evt):
+        self.log.log("Enter orpgFrame->on_player_event(self, evt)", ORPG_DEBUG)
+        id = evt.get_id()
+        player = evt.get_data()
+        display_name = self.chat.chat_display_name(player)
+        time_str = time.strftime("%H:%M", time.localtime())
+        if id == orpg.networking.mplay_client.PLAYER_NEW:
+            self.players.add_player(player)
+            self.chat.InfoPost(display_name + " (enter): " + time_str)
+        elif id == orpg.networking.mplay_client.PLAYER_DEL:
+            self.players.del_player(player)
+            self.chat.InfoPost(display_name + " (exit): " + time_str)
+        elif id == orpg.networking.mplay_client.PLAYER_UPDATE:
+            self.players.update_player(player)
+        self.players.Refresh()
+        self.log.log("Exit orpgFrame->on_player_event(self, evt)", ORPG_DEBUG)
+
+    def on_group_event(self, evt):
+        self.log.log("Enter orpgFrame->on_group_event(self, evt)", ORPG_DEBUG)
+        id = evt.get_id()
+        data = evt.get_data()
+
+        if id == orpg.networking.mplay_client.GROUP_NEW:
+            self.gs.add_room(data)
+        elif id == orpg.networking.mplay_client.GROUP_DEL:
+            self.password_manager.RemoveGroupData(data)
+            self.gs.del_room(data)
+        elif id == orpg.networking.mplay_client.GROUP_UPDATE:
+            self.gs.update_room(data)
+        self.log.log("Exit orpgFrame->on_group_event(self, evt)", ORPG_DEBUG)
+
+    def on_receive(self, data, player):
+        self.log.log("Enter orpgFrame->on_receive(self, data, player)", ORPG_DEBUG)
+
+        # see if we are ignoring this user
+        (ignore_id,ignore_name) = self.session.get_ignore_list()
+        for m in ignore_id:
+            if m == player[2]:
+                # yes we are
+                self.log.log("ignoring message from player:" + player[0], ORPG_INFO, True)
+                return
+
+        # ok we are not ignoring this message
+        #recvSound = "RecvSound"                #  this will be the default sound.  Whisper will change this below
+        if player:
+            display_name = self.chat.chat_display_name(player)
+        else:
+            display_name = "Server Administrator"
+
+        if data[:5] == "<tree":
+            self.tree.on_receive_data(data,player)
+            self.chat.InfoPost(display_name + " has sent you a tree node...")
+            #self.tree.OnNewData(data)
+
+        elif data[:4] == "<map":
+            self.map.new_data(data)
+
+        elif data[:5] == "<chat":
+            msg = orpg.chat.chat_msg.chat_msg(data)
+            self.chat.post_incoming_msg(msg,player)
+        else:
+        ##############################################################################################
+        #  all this below code is for comptiablity with older clients and can be removed after a bit #
+        ##############################################################################################
+            if data[:3] == "/me":
+                # This fixes the emote coloring to comply with what has been asked for by the user
+                # population, not to mention, what I committed to many moons ago.
+                #  In doing so, Woody's scheme has been tossed out.  I'm sure Woody won't be
+                # happy but I'm invoking developer priveledge to satisfy user request, not to mention,
+                # this scheme actually makes more sense.  In Woody's scheme, a user could over-ride another
+                # users emote color.  This doesn't make sense, rather, people dictate their OWN colors...which is as
+                # it should be in the first place and is as it has been with normal text.  In short, this makes
+                # sense and is consistent.
+                data = data.replace( "/me", "" )
+
+                # Check to see if we find the closing ">" for the font within the first 22 values
+                index = data[:22].find(  ">" )
+                if index == -1:
+                    data = "** " + self.chat.colorize( self.chat.infocolor, display_name + data ) + " **"
+
+                else:
+                    # This means that we found a valid font string, so we can simply plug the name into
+                    # the string between the start and stop font delimiter
+                    print "pre data = " + data
+                    data = data[:22] + "** " + display_name + " " + data[22:] + " **"
+                    print "post data = " + data
+
+            elif data[:2] == "/w":
+                data = data.replace("/w","")
+                data = "<b>" + display_name + "</b> (whispering): " + data
+
+            else:
+                # Normal text
+                if player:
+                    data = "<b>" + display_name + "</b>: " + data
+                else:
+                    data = "<b><i><u>" + display_name + "</u>-></i></b> " + data
+            self.chat.Post(data)
+        self.log.log("Exit orpgFrame->on_receive(self, data, player)", ORPG_DEBUG)
+
+    def on_mplay_event(self, evt):
+        self.log.log("Enter orpgFrame->on_mplay_event(self, evt)", ORPG_DEBUG)
+
+        id = evt.get_id()
+        if id == orpg.networking.mplay_client.MPLAY_CONNECTED:
+            self.chat.InfoPost("Game connected!")
+            self.gs.set_connected(1)
+            self.password_manager.ClearPassword("ALL")
+
+        elif id == orpg.networking.mplay_client.MPLAY_DISCONNECTED:
+            self.poll_timer.Stop()
+            self.ping_timer.Stop()
+            self.chat.SystemPost("Game disconnected!")
+            self.players.reset()
+            self.gs.set_connected(0)
+            self.status.set_connect_status("Not Connected")
+
+        ####Begin changes for Custom Exit Message by mDuo13######
+        elif id == orpg.networking.mplay_client.MPLAY_DISCONNECTING:
+            settings = open_rpg.get_component('settings')
+            custom_msg = settings.get_setting("dcmsg")
+            custom_msg=custom_msg[:80]
+            if custom_msg[:3]=="/me":
+                self.chat.send_chat_message(custom_msg[3:], 3)
+            else:
+                self.chat.system_message(custom_msg)
+        #####End Changes for Custom Exit Message by mDuo13
+
+        elif id== orpg.networking.mplay_client.MPLAY_GROUP_CHANGE:
+            group = evt.get_data()
+            self.chat.InfoPost("Moving to room '"+group[1]+"'..")
+            if self.gs : self.gs.set_cur_room_text(group[1])
+            self.players.reset()
+        elif id== orpg.networking.mplay_client.MPLAY_GROUP_CHANGE_F:
+            self.chat.SystemPost("Room access denied!")
+        self.log.log("Exit orpgFrame->on_mplay_event(self, evt)", ORPG_DEBUG)
+
+    def OnCloseWindow(self, event):
+        self.log.log("Enter orpgFrame->OnCloseWindow(self, event)", ORPG_DEBUG)
+        dlg = wx.MessageDialog(self, "Quit OpenRPG?", "OpenRPG", wx.YES_NO)
+        if dlg.ShowModal() == wx.ID_YES:
+            dlg.Destroy()
+            self.closed_confirmed()
+        self.log.log("Exit orpgFrame->OnCloseWindow(self, event)", ORPG_DEBUG)
+
+    def closed_confirmed(self):
+        self.log.log("Enter orpgFrame->closed_confirmed(self)", ORPG_DEBUG)
+        self.activeplugins = open_rpg.get_component('plugins')
+        self.aliaslib.OnMB_FileSave(None)
+
+        #following lines added by mDuo13
+        #########plugin_disabled()#########
+        for plugin_fname in self.activeplugins.keys():
+            plugin = self.activeplugins[plugin_fname]
+            try:
+                plugin.plugin_disabled()
+            except Exception, e:
+                if str(e) != "'module' object has no attribute 'plugin_disabled'":
+                    #print e
+                    traceback.print_exc()
+        #end mDuo13 added code
+        self.saveLayout()
+        try:
+            self.settings.save()
+        except:
+            self.log.log("[WARNING] Error saving 'settings' component", ORPG_GENERAL, True)
+
+        try:
+            self.map.pre_exit_cleanup()
+        except:
+            self.log.log("[WARNING] Map error pre_exit_cleanup()", ORPG_GENERAL, True)
+
+        try:
+            save_tree = string.upper(self.settings.get_setting("SaveGameTreeOnExit"))
+            if  (save_tree != "0") and (save_tree != "False") and (save_tree != "NO"):
+                self.tree.save_tree(self.settings.get_setting("gametree"))
+        except:
+            self.log.log("[WARNING] Error saving gametree", ORPG_GENERAL, True)
+
+        if self.session.get_status() == orpg.networking.mplay_client.MPLAY_CONNECTED:
+            self.kill_mplay_session()
+
+        try:
+            #Kill all the damn timers
+            self.sound_player.timer.Stop()
+            del self.sound_player.timer
+        except:
+            self.log.log("sound didn't die properly.",ORPG_GENERAL, True)
+
+        try:
+            self.poll_timer.Stop()
+            self.ping_timer.Stop()
+            self.chat.parent.chat_timer.Stop()
+            self.map.canvas.zoom_display_timer.Stop()
+            self.map.canvas.image_timer.Stop()
+            self.status.timer.Stop()
+            del self.ping_timer
+            del self.poll_timer
+            del self.chat.parent.chat_timer
+            del self.map.canvas.zoom_display_timer
+            del self.map.canvas.image_timer
+            del self.status.timer
+        except:
+            self.log.log("some timer didn't die properly.",ORPG_GENERAL, True)
+        self._mgr.UnInit()
+        mainapp = wx.GetApp()
+        mainapp.ExitMainLoop()
+        self.Destroy()
+
+        try:
+            if self.server_pipe != None:
+                dlg = wx.ProgressDialog("Exit","Stoping server",2,self)
+                dlg.Update(2)
+                dlg.Show(True)
+                self.server_pipe.write("\nkill\n")
+                self.log.log("Killing Server process:", ORPG_GENERAL, True)
+                time.sleep(5)
+                self.server_stop()
+                self.server_pipe.close()
+                self.std_out.close()
+                self.server_thread.exit()
+                dlg.Destroy()
+                self.log.log("Server killed:", ORPG_GENERAL, True)
+        except:
+            pass
+        self.log.log("Exit orpgFrame->closed_confirmed(self)", ORPG_DEBUG)
+
+
+########################################
+## Application class
+########################################
+class orpgSplashScreen(wx.SplashScreen):
+    def __init__(self, parent, bitmapfile, duration, callback):
+        wx.SplashScreen.__init__(self, wx.Bitmap(bitmapfile), wx.SPLASH_CENTRE_ON_SCREEN | wx.SPLASH_TIMEOUT, duration, None, -1)
+        self.callback = callback
+        self.closing = False
+        self.Bind(wx.EVT_CLOSE, self.callback)
+
+class orpgApp(wx.App):
+    def OnInit(self):
+        self.log = orpg.tools.orpg_log.orpgLog(orpg.dirpath.dir_struct["user"] + "runlogs/")
+        self.log.setLogToConsol(False)
+        self.log.log("Main Application Start", ORPG_DEBUG)
+        #Add the initial global components of the openrpg class
+        #Every class should be passed openrpg
+        open_rpg.add_component("log", self.log)
+        open_rpg.add_component("xml", orpg.orpg_xml)
+        open_rpg.add_component("dir_struct", orpg.dirpath.dir_struct)
+        open_rpg.add_component("tabbedWindows", [])
+        self.validate = orpg.tools.validate.Validate()
+        open_rpg.add_component("validate", self.validate)
+        self.settings = orpg.tools.orpg_settings.orpgSettings()
+        open_rpg.add_component("settings", self.settings)
+        self.log.setLogLevel(int(self.settings.get_setting('LoggingLevel')))
+        self.called = False
+        wx.InitAllImageHandlers()
+        self.splash = orpgSplashScreen(None, orpg.dirpath.dir_struct["icon"] + 'splash13.jpg', 3000, self.AfterSplash)
+        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyPress)
+        self._crust = None
+        wx.Yield()
+        return True
+
+    def OnKeyPress(self, evt):
+        #Event handler
+        if evt.AltDown() and evt.CmdDown() and evt.KeyCode == ord('I'):
+            self.ShowShell()
+        else:
+            evt.Skip()
+
+    def ShowShell(self):
+        #Show the PyCrust window.
+        if not self._crust:
+            self._crust = wx.py.crust.CrustFrame(self.GetTopWindow())
+            self._crust.shell.interp.locals['app'] = self
+        win = wx.FindWindowAtPointer()
+        self._crust.shell.interp.locals['win'] = win
+        self._crust.Show()
+
+    def AfterSplash(self,evt):
+        if not self.called:
+            self.splash.Hide()
+            self.called = True
+            self.frame = orpgFrame(None, wx.ID_ANY, MENU_TITLE)
+            self.frame.Raise()
+            self.frame.Refresh()
+            self.frame.Show(True)
+            self.SetTopWindow(self.frame)
+            #self.frame.show_dlgs()
+            self.frame.post_show_init()
+            wx.CallAfter(self.splash.Close)
+            return True
+
+    def OnExit(self):
+        self.log.log("Main Application Exit\n\n", ORPG_DEBUG)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/map/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,269 @@
+from threading import Lock
+import mimetypes
+import xml.dom.minidom as minidom
+
+import wx
+
+import orpg.dirpath
+from orpg.orpgCore import *
+from orpg.tools.rgbhex import RGBHex
+import orpg.tools.ButtonPanel as BP
+
+from _canvas import MapCanvas
+
+class MapWnd(wx.Panel):
+    def __init__(self, parent, openrpg):
+        wx.Panel.__init__(self, parent, wx.ID_ANY)
+        self.openrpg = openrpg
+        self.log = self.openrpg.get_component("log")
+        self.xml = self.openrpg.get_component("xml")
+        self.dir_struct = self.openrpg.get_component("dir_struct")
+        self.validate = self.openrpg.get_component("validate")
+        self.settings = self.openrpg.get_component("settings")
+
+        self.Freeze()
+        sizer = wx.GridBagSizer(hgap=1, vgap=1)
+        sizer.SetEmptyCellSize((0,0))
+
+        self.canvas = MapCanvas(self, self.openrpg)
+        sizer.Add(self.canvas, (0,0), flag=wx.EXPAND)
+
+        self.gmToolBar = BP.ButtonPanel(self, wx.ID_ANY)
+        sizer.Add(self.gmToolBar, (1,0), flag=wx.EXPAND)
+        self.playerToolBar = BP.ButtonPanel(self, wx.ID_ANY)
+        sizer.Add(self.playerToolBar, (2,0), flag=wx.EXPAND)
+
+        sizer.AddGrowableCol(0)
+        sizer.AddGrowableRow(0)
+
+        self.SetSizer(sizer)
+        self.SetAutoLayout(True)
+
+        self._CreateToolBar()
+
+        self.Bind(wx.EVT_MOUSEWHEEL, self.canvas.OnZoom)
+        self.Bind(wx.EVT_KEY_DOWN, self.canvas.OnKey)
+        self.Bind(wx.EVT_KEY_UP, self.canvas.OnKey)
+        self.Layout()
+        self.Thaw()
+
+        wx.CallAfter(self.PostLoad)
+
+    #Public API
+    def PostLoad(self):
+        self.canvas.Clear()
+        #self.canvas.roleTimer.Start(100)
+        self.canvas.UpdateMap()
+
+    #Events
+
+
+    #Private Methods
+    def _SetColorBtn(self, color, btn):
+        dc = wx.MemoryDC()
+        bmp = wx.EmptyBitmap(16, 16)
+        dc.SelectObject(bmp)
+        dc.SetBrush(wx.Brush(color))
+        dc.DrawRectangle(0,0, 16, 16)
+
+        del dc
+
+        btn.SetBitmap(bmp)
+
+    def _CreateToolBar(self):
+        self.exclusiveToolList = {}
+
+        self.OpenBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'open.bmp', wx.BITMAP_TYPE_BMP), kind=wx.ITEM_NORMAL, shortHelp="Load New Map", longHelp="Load New Map")
+        self.gmToolBar.AddButton(self.OpenBtn)
+
+        self.SaveBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'save.bmp', wx.BITMAP_TYPE_BMP), kind=wx.ITEM_NORMAL, shortHelp="Save Map", longHelp="Save Map")
+        self.gmToolBar.AddButton(self.SaveBtn)
+
+        self.DefaultBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'defaultmap.png', wx.BITMAP_TYPE_PNG), kind=wx.ITEM_NORMAL, shortHelp="Load Default Map", longHelp="Load Default Map")
+        self.gmToolBar.AddButton(self.DefaultBtn)
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnDefaultBtn, id=self.DefaultBtn.GetId())
+
+        self.MapPropsBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'compass.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_NORMAL, shortHelp="Map Properties", longHelp="Map Properties")
+        self.gmToolBar.AddButton(self.MapPropsBtn)
+
+
+        self.gmToolBar.AddSeparator()
+
+        self.BGBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'img.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_NORMAL, shortHelp="Change Background", longHelp="Change Background")
+        self.gmToolBar.AddButton(self.BGBtn)
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnBGBtn, id=self.BGBtn.GetId())
+
+        self.BGColorBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.EmptyBitmap(16,16), kind=wx.ITEM_NORMAL, shortHelp="Map Background Color", longHelp="Map Background Color")
+        self.gmToolBar.AddButton(self.BGColorBtn)
+        self._SetColorBtn(wx.GREEN, self.BGColorBtn)
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnBGColorBtn, id=self.BGColorBtn.GetId())
+
+        self.TileAddBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'chess.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_NORMAL, shortHelp="Add Map Tile", longHelp="Add Map Tile")
+        self.gmToolBar.AddButton(self.TileAddBtn)
+
+        self.TileMoveBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'crosshair.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_CHECK, shortHelp="Edit Tiles", longHelp="Edit Tiles")
+        self.gmToolBar.AddButton(self.TileMoveBtn)
+        self.exclusiveToolList[self.TileMoveBtn.GetId()] = self.TileMoveBtn
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnExlusiveBtn, id=self.TileMoveBtn.GetId())
+
+        self.GridBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'grid.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_NORMAL, shortHelp="Set Grid", longHelp="Set Grid")
+        self.gmToolBar.AddButton(self.GridBtn)
+
+        self.gmToolBar.AddSeparator()
+
+        self.FogBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'fogon.png', wx.BITMAP_TYPE_PNG), kind=wx.ITEM_CHECK, shortHelp="Turn Fog On", longHelp="Turn Fog On")
+        self.gmToolBar.AddButton(self.FogBtn)
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnFogBtn, id=self.FogBtn.GetId())
+
+        self.FogToolBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'showfog.png', wx.BITMAP_TYPE_PNG), kind=wx.ITEM_CHECK, shortHelp="Show Tool", longHelp="Show Tool")
+        self.gmToolBar.AddButton(self.FogToolBtn)
+        self.exclusiveToolList[self.FogToolBtn.GetId()] = self.FogToolBtn
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnExlusiveBtn, id=self.FogToolBtn.GetId())
+        menu = wx.Menu("Fog Tool")
+        item = wx.MenuItem(menu, 1, "Show", "Show")
+        self.Bind(wx.EVT_MENU, self.OnFogSelection, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, 2, "Hide", "Hide")
+        self.Bind(wx.EVT_MENU, self.OnFogSelection, item)
+        menu.AppendItem(item)
+        self.currentFog = 'Show'
+        self.FogToolBtn.SetMenu(menu)
+
+        self.FogColorBtn = BP.ButtonInfo(self.gmToolBar, wx.ID_ANY, wx.EmptyBitmap(16,16), kind=wx.ITEM_NORMAL, shortHelp="Fog Color", longHelp="Fog Color")
+        self.gmToolBar.AddButton(self.FogColorBtn)
+        self._SetColorBtn(wx.BLACK, self.FogColorBtn)
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnFogColorBtn, id=self.FogColorBtn.GetId())
+
+        self.gmToolBar.AddSeparator()
+
+        self.MiniPropsBtn = BP.ButtonInfo(self.playerToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'questionhead.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_NORMAL, shortHelp="Miniture Properties", longHelp="Miniture Properties")
+        self.gmToolBar.AddButton(self.MiniPropsBtn)
+
+        self.gmToolBar.DoLayout()
+
+        self.SelectorBtn = BP.ButtonInfo(self.playerToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'mouse.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_CHECK, shortHelp="Selection Tool", longHelp="Selection Tool")
+        self.playerToolBar.AddButton(self.SelectorBtn)
+        self.exclusiveToolList[self.SelectorBtn.GetId()] = self.SelectorBtn
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnExlusiveBtn, id=self.SelectorBtn.GetId())
+
+        self.MeasureBtn = BP.ButtonInfo(self.playerToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'tape.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_CHECK, shortHelp="Measure Tool", longHelp="Measure Tool")
+        self.playerToolBar.AddButton(self.MeasureBtn)
+        self.exclusiveToolList[self.MeasureBtn.GetId()] = self.MeasureBtn
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnExlusiveBtn, id=self.MeasureBtn.GetId())
+
+        self.ColorBtn = BP.ButtonInfo(self.playerToolBar, wx.ID_ANY, wx.EmptyBitmap(16,16), kind=wx.ITEM_NORMAL, shortHelp="Select a Color", longHelp="Select a Color")
+        self.playerToolBar.AddButton(self.ColorBtn)
+        self._SetColorBtn(wx.BLACK, self.ColorBtn)
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnColorBtn, id=self.ColorBtn.GetId())
+
+        self.DrawBtn = BP.ButtonInfo(self.playerToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'draw.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_CHECK, shortHelp="Freehand Line Tool", longHelp="Freehand Line Tool")
+        self.playerToolBar.AddButton(self.DrawBtn)
+        self.exclusiveToolList[self.DrawBtn.GetId()] = self.DrawBtn
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnExlusiveBtn, id=self.DrawBtn.GetId())
+        menu = wx.Menu("Line Tool")
+        item = wx.MenuItem(menu, 3, "Free Draw", "Free Draw")
+        self.Bind(wx.EVT_MENU, self.OnLineSelection, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, 4, "Poly Draw", "Poly Draw")
+        self.Bind(wx.EVT_MENU, self.OnLineSelection, item)
+        menu.AppendItem(item)
+        self.currentLine = 'Free'
+        self.DrawBtn.SetMenu(menu)
+
+        self.AddTextBtn = BP.ButtonInfo(self.playerToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'text.png', wx.BITMAP_TYPE_PNG), kind=wx.ITEM_CHECK, shortHelp="Add Text Tool", longHelp="Add Text Tool")
+        self.playerToolBar.AddButton(self.AddTextBtn)
+        self.exclusiveToolList[self.AddTextBtn.GetId()] = self.AddTextBtn
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnExlusiveBtn, id=self.AddTextBtn.GetId())
+
+        self.AddShapeBtn = BP.ButtonInfo(self.playerToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'circle.png', wx.BITMAP_TYPE_PNG), kind=wx.ITEM_CHECK, shortHelp="Add Shape Tool", longHelp="Add Shape Tool")
+        self.playerToolBar.AddButton(self.AddShapeBtn)
+        self.exclusiveToolList[self.AddShapeBtn.GetId()] = self.AddShapeBtn
+        self.Bind(wx.EVT_BUTTON, self.canvas.OnExlusiveBtn, id=self.AddShapeBtn.GetId())
+        menu = wx.Menu("Shape Tool")
+        item = wx.MenuItem(menu, 5, "Circle", "Circle")
+        self.Bind(wx.EVT_MENU, self.OnShapeSelection, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, 6, "Rectangle", "Rectangle")
+        self.Bind(wx.EVT_MENU, self.OnShapeSelection, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, 7, "Arc", "Arc")
+        self.Bind(wx.EVT_MENU, self.OnShapeSelection, item)
+        menu.AppendItem(item)
+        self.currentShape = 'Circle'
+        self.AddShapeBtn.SetMenu(menu)
+
+        self.LineWidth = wx.Choice(self.playerToolBar, wx.ID_ANY, choices=["1","2","3","4","5","6","7","8","9","10"])
+        self.LineWidth.SetSelection(0)
+        self.playerToolBar.AddControl(self.LineWidth)
+
+        self.playerToolBar.AddSeparator()
+
+        self.MiniBtn = BP.ButtonInfo(self.playerToolBar, wx.ID_ANY, wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'player.gif', wx.BITMAP_TYPE_GIF), kind=wx.ITEM_NORMAL, shortHelp="Add Mini", longHelp="Add Mini")
+        self.playerToolBar.AddButton(self.MiniBtn)
+
+        self.playerToolBar.DoLayout()
+
+    def OnShapeSelection(self, event):
+        id = event.GetId()
+        if id == 5:
+            self.currentShape = 'Circle'
+            self.AddShapeBtn.Bitmap = wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'circle.png', wx.BITMAP_TYPE_PNG)
+        elif id == 6:
+            self.currentShape = 'Rectangle'
+            self.AddShapeBtn.Bitmap = wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'rectangle.png', wx.BITMAP_TYPE_PNG)
+        elif id == 7:
+            self.currentShape = 'Arc'
+            self.AddShapeBtn.Bitmap = wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'arc.png', wx.BITMAP_TYPE_PNG)
+
+    def OnLineSelection(self, event):
+        id = event.GetId()
+        if id == 3:
+            self.currentLine = 'Free'
+            self.DrawBtn.Bitmap = wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'draw.gif', wx.BITMAP_TYPE_GIF)
+            self.DrawBtn.SetShortHelp("Freehand Line Tool")
+            self.DrawBtn.SetLongHelp("Freehand Line Tool")
+        elif id == 4:
+            self.currentLine = 'Poly'
+            self.DrawBtn.Bitmap = wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'dash.png', wx.BITMAP_TYPE_PNG)
+            self.DrawBtn.SetShortHelp("Poly Line Tool")
+            self.DrawBtn.SetLongHelp("Poly Line Tool")
+
+    def OnFogSelection(self, event):
+        id = event.GetId()
+        if id == 1:
+            self.currentFog = 'Show'
+            self.FogToolBtn.Bitmap = wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'showfog.png', wx.BITMAP_TYPE_PNG)
+            self.FogToolBtn.SetShortHelp("Show Fog Tool")
+            self.FogToolBtn.SetLongHelp("Show Fog Tool")
+        elif id == 2:
+            self.currentFog = 'Hide'
+            self.FogToolBtn.Bitmap = wx.Bitmap(orpg.dirpath.dir_struct["icon"] + 'hidefog.png', wx.BITMAP_TYPE_PNG)
+            self.FogToolBtn.SetShortHelp("Hide Fog Tool")
+            self.FogToolBtn.SetLongHelp("Hide Fog Tool")
+
+### Test Stuff
+class BlankFrame(wx.Frame):
+    def __init__(self, openrpg):
+        wx.Frame.__init__(self, None, title="New Map Test Window", size=(740,480))
+
+        self.map = MapWnd(self, openrpg)
+        self.basesizer = wx.BoxSizer(wx.VERTICAL)
+        self.basesizer.Add(self.map, 1, wx.EXPAND)
+
+        self.SetSizer(self.basesizer)
+        self.SetAutoLayout(True)
+        #self.Fit()
+
+
+class BlankApp(wx.App):
+    def OnInit(self):
+        self.frame = BlankFrame()
+        self.frame.Show()
+        self.SetTopWindow(self.frame)
+
+
+        return True
+
+if __name__ == "__main__":
+    app = BlankApp(0)
+    app.MainLoop()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/map/_canvas.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,994 @@
+from threading import Lock
+import mimetypes
+import xml.dom.minidom as minidom
+
+import wx
+
+import orpg.dirpath
+from orpg.orpgCore import *
+from orpg.tools.rgbhex import RGBHex
+
+from _object import *
+
+from _circles import MapCircle
+from _text import MapText
+from _lines import MapLine
+from _grid import GridLayer
+from _fog import FogLayer
+
+USE_BUFFER = True
+if "wxMAC" in wx.PlatformInfo:
+    USE_BUFFER = False
+
+class MapCanvas(wx.ScrolledWindow):
+    def __init__(self, parent, openrpg):
+        wx.ScrolledWindow.__init__(self, parent, wx.ID_ANY, style=wx.HSCROLL | wx.VSCROLL | wx.NO_FULL_REPAINT_ON_RESIZE | wx.SUNKEN_BORDER)
+
+        self.openrpg = openrpg
+        self.log = self.openrpg.get_component("log")
+        self.xml = self.openrpg.get_component("xml")
+        self.dir_struct = self.openrpg.get_component("dir_struct")
+        self.validate = self.openrpg.get_component("validate")
+        self.settings = self.openrpg.get_component("settings")
+        self.session = self.openrpg.get_component("session")
+        self.chat = self.openrpg.get_component("chat")
+
+        self.lock = Lock()
+
+        self.RGBHex = RGBHex()
+
+        self.toolWnd = parent
+
+        self.shift = False
+        self.ctrl = False
+
+        self.selectedObjects = []
+        self.overObjects = []
+        self._objectId = 0
+
+        self.gridLayer = GridLayer(self)
+        self.circleLayer = MapCircle(self)
+        self.textLayer = MapText(self)
+        self.lineLayer = MapLine(self)
+        self.fogLayer = FogLayer(self)
+
+        self.zOrder = {}
+        self.zOrder['tiles'] = []
+        self.zOrder["back"] = []
+        self.zOrder["front"] = []
+
+        self.bgImage = None
+        self.bgType = 'Image'
+        self.bgPath = None
+        self.backgroundColor = '#008040'
+
+        self.gridType = 'Square'
+        self.gridLines = wx.SOLID
+        self.gridSnap = True
+        self.gridSize = 60
+        self.gridColor = "#000000"
+
+        self.whiteboardColor = "#000000"
+
+        self.zoomScale = 1.0
+        self.lastZoomTime = time.time()
+        self.lastZoomScale = 1.0
+
+        self.useFog = False
+        self.fogRegion = []
+        self.fogColor = "#000000"
+
+        self.zoomScale = 1.0
+        self.lastZoomTime = time.time()
+        self.lastZoomScale = 1.0
+        self.zoomTimer = wx.Timer(self)
+        self.Bind(wx.EVT_TIMER, self.OnZoomTimer, self.zoomTimer)
+        #self.zoomTimer.Start(1000)
+
+        self.imageCache = {}
+
+        self._SetSize((1000,1000))
+
+        self.Bind(wx.EVT_PAINT, self.OnPaint)
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+        self.Bind(wx.EVT_MOUSEWHEEL, self.OnZoom)
+        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
+        self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
+        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
+        self.Bind(wx.EVT_MOTION, self.OnMotion)
+        self.Bind(wx.EVT_SCROLLWIN, self.OnScroll)
+        self.Bind(wx.EVT_CLOSE, self.OnClose)
+        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnBackground)
+        self.Bind(wx.EVT_KEY_DOWN, self.OnKey)
+        self.Bind(wx.EVT_KEY_UP, self.OnKey)
+
+        self.Bind(EVT_ENTER_OBJECT, self.EnterObject)
+        self.Bind(EVT_LEAVE_OBJECT, self.LeaveObject)
+        self.Bind(EVT_SELECT_OBJECT, self.ObjectSelected)
+        self.Bind(EVT_DESELECT_OBJECT, self.ObjectDeselected)
+
+        self.roleTimer = wx.Timer(self)
+        self.Bind(wx.EVT_TIMER, self.OnRoleTimer, self.roleTimer)
+
+        wx.CallAfter(self.OnSize, None)
+
+
+    #Public API
+    def UpdateMap(self, send=True):
+        cdc = wx.ClientDC(self)
+        self.PrepareDC(cdc)
+        cdc.SetBackgroundMode(wx.TRANSPARENT)
+        if USE_BUFFER:
+            bdc = wx.BufferedDC(cdc, self._buffer)
+            bdc.Clear()
+            dc = wx.GraphicsContext.Create(bdc)
+        else:
+            cdc.Clear()
+            dc = wx.GraphicsContext.Create(cdc)
+
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        #Draw BG Color
+        r,g,b = self.RGBHex.rgb_tuple(self.backgroundColor)
+        brush = wx.Brush(wx.Color(r,g,b,255))
+        dc.SetBrush(brush)
+
+        path = dc.CreatePath()
+
+        dc.PushState()
+        path.AddRectangle(0, 0, self.size[0]+2, self.size[1]+2)
+        dc.DrawPath(path)
+        dc.PopState()
+
+        dc.SetBrush(wx.NullBrush)
+
+        #Set the Zoom
+        dc.Scale(self.zoomScale, self.zoomScale)
+
+        #Draw BG Image
+        if self.bgImage != None:
+            if self.bgType == 'Image':
+                dc.DrawBitmap(self.bgImage, self.offset[0], self.offset[1], self.bgImage.GetWidth(), self.bgImage.GetHeight())
+            else:
+                bmpW = self.bgImage.GetWidth()
+                bmpH = self.bgImage.GetHeight()
+
+                pos = wx.Point(self.offset[0], self.offset[1])
+                while pos.x < self.size[0]:
+                    dc.DrawBitmap(self.bgImage, pos.x, pos.y, self.bgImage.GetWidth(), self.bgImage.GetHeight())
+                    while pos.y < self.size[1]:
+                        pos.y += bmpH
+                        dc.DrawBitmap(self.bgImage, pos.x, pos.y, self.bgImage.GetWidth(), self.bgImage.GetHeight())
+                    pos.y = 0
+                    pos.x += bmpW
+
+        #Draw Tiles
+        for tile in self.zOrder['tiles']:
+            tile.Draw(dc)
+
+        #Draw Grid
+        self.gridLayer.Draw(dc)
+
+        #Draw Objects
+        for object in self.zOrder['back']:
+            object.Draw(dc)
+
+        zl = self.zOrder.keys()
+        zl.remove('back')
+        zl.remove('front')
+        zl.remove('tiles')
+        zl.sort()
+
+        for layer in zl:
+            for object in self.zOrder[layer]:
+                object.Draw(dc)
+
+        for object in self.zOrder['front']:
+            object.Draw(dc)
+
+
+        #Draw Fog
+        if self.useFog:
+            self.fogLayer.Draw(dc)
+
+        dc.SetBrush(wx.NullBrush)
+
+        dc.Scale(1/self.zoomScale, 1/self.zoomScale)
+
+        if self.zoomScale != 1.0:
+            pos = self.GetViewStart()
+            unit = self.GetScrollPixelsPerUnit()
+            pos = [pos[0]*unit[0],pos[1]*unit[1]]
+            font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
+            dc.SetFont(font, wx.BLACK)
+
+            dc.DrawText("Zoom Factor: " + str(self.zoomScale), pos[0], pos[1], dc.CreateBrush(wx.WHITE_BRUSH))
+
+    def Clear(self):
+        self._SetSize((1000,1000))
+        self.selectedObjects = []
+        self.overObjects = []
+        self._objectId = 0
+        self.bgImage = None
+        self.bgType = 'Image'
+        self.bgPath = None
+
+        self.backgroundColor = '#008040'
+        r, g, b = self.RGBHex.rgb_tuple(self.backgroundColor)
+        self.toolWnd._SetColorBtn(wx.Color(r, g, b, 255), self.toolWnd.BGColorBtn)
+
+        self.gridType = 'Square'
+        self.gridLines = wx.SOLID
+        self.gridSnap = True
+        self.gridSize = 60
+        self.gridColor = "#000000"
+
+        self.whiteboardColor = "#000000"
+        r, g, b = self.RGBHex.rgb_tuple(self.whiteboardColor)
+        self.toolWnd._SetColorBtn(wx.Color(r, g, b, 255), self.toolWnd.ColorBtn)
+
+        self.zoomScale = 1.0
+        self.lastZoomTime = time.time()
+        self.lastZoomScale = 1.0
+
+        self.useFog = False
+        self.fogRegion = []
+        self.fogColor = "#000000"
+
+        self.OnRemoveAllObjects(None)
+
+        self.toolWnd.Freeze()
+        for btn in self.toolWnd.exclusiveToolList:
+            self.toolWnd.exclusiveToolList[btn].SetToggled(False)
+
+        self.toolWnd.FogBtn.SetToggled(False)
+        self.toolWnd.SelectorBtn.SetToggled(True)
+        self.toolWnd.Thaw()
+
+    def GetNewObjectId(self):
+        return str(self._objectId+1)
+
+    #Map Events
+    def OnBackground(self, event):
+        #Dont do it
+        pass
+
+    def OnPaint(self, event):
+        if USE_BUFFER:
+            dc = wx.PaintDC(self)
+            self.PrepareDC(dc)
+            dc.DrawBitmap(self._buffer, 0, 0)
+        else:
+            event.Skip()
+
+
+    def OnSize(self, event):
+        self._buffer = wx.EmptyBitmap(self.size[0], self.size[1])
+        self._FixScroll()
+        wx.CallAfter(self.UpdateMap)
+
+
+    def OnZoom(self, event):
+        if event.GetWheelRotation() < 0:
+            self.zoomScale -= .1
+            if self.zoomScale < .5:
+                self.zoomScale = .5
+            else:
+                self.lastZoomTime = time.time()
+                self._FixScroll()
+                self.UpdateMap()
+        else:
+            self.zoomScale += .1
+
+            if self.zoomScale > 1.5:
+                self.zoomScale = 1.5
+            else:
+                self.lastZoomTime = time.time()
+                self._FixScroll()
+                self.UpdateMap()
+
+    def OnKey(self, event):
+        self.shift = False
+        self.ctrl = False
+        if event.ShiftDown():
+            self.shift = True
+        elif event.ControlDown():
+            self.ctrl = True
+
+
+    def EnterObject(self, event):
+        obj = event.GetObject()
+        self.overObjects.append(obj)
+        obj.Highlight()
+
+    def LeaveObject(self, event):
+        obj = event.GetObject()
+        try:
+            self.overObjects.remove(obj)
+        except:
+            pass
+        obj.UnHighlight()
+
+    def ObjectSelected(self, event):
+        obj = event.GetObject()
+        self.selectedObjects.append(obj)
+        try:
+            self.overObjects.remove(obj)
+        except:
+            pass
+        obj.UnHighlight()
+
+    def ObjectDeselected(self, event):
+        obj = event.GetObject()
+        try:
+            self.selectedObjects.remove(obj)
+        except:
+            pass
+        obj.Update()
+
+    def OnLeftDown(self, event):
+        dc = wx.ClientDC(self)
+        self.PrepareDC(dc)
+        pos = event.GetLogicalPosition(dc)
+        pos.x /= self.zoomScale
+        pos.y /= self.zoomScale
+
+        if self.toolWnd.AddShapeBtn.GetToggled() and self.toolWnd.currentShape == 'Circle':
+            self.circleLayer.OnLeftDown(pos)
+
+        elif self.toolWnd.AddTextBtn.GetToggled():
+            self.textLayer.OnLeftDown(pos)
+
+        elif self.toolWnd.DrawBtn.GetToggled():
+            self.lineLayer.OnLeftDown(pos)
+
+        elif self.toolWnd.SelectorBtn.GetToggled() and (self.selectedObjects == [] or self.ctrl or self.shift) and not (self.useFog and self.fogLayer.region.Contains(pos.x, pos.y) and not self.toolWnd.gmToolBar.IsShown()):
+            self.initiatPos = pos
+            self.lxd = 0
+            self.lyd = 0
+            if len(self.overObjects) == 0:
+                return
+            elif len(self.overObjects) == 1:
+                self.overObjects[0].Select()
+            else:
+                if not self.shift:
+                    menu = wx.Menu("Object Selection")
+                    id = 0
+                    for obj in self.overObjects:
+                        menu.Append(id, obj.GetName())
+                        id += 1
+
+                    def selectmenu(event):
+                        id = event.GetId()
+                        self.overObjects[id].Select()
+                        self.Unbind(wx.EVT_MENU)
+
+                    self.Bind(wx.EVT_MENU, selectmenu)
+                    self.PopupMenu(menu)
+                else:
+                    for i in xrange(len(self.overObjects)):
+                        self.overObjects[0].Select()
+
+        elif self.toolWnd.SelectorBtn.GetToggled() and not self.selectedObjects == []:
+            xd = (self.initiatPos.x+pos.x)*(self.initiatPos.x+pos.x)
+            yd = (self.initiatPos.y+pos.y)*(self.initiatPos.y+pos.y)
+
+            for i in xrange(len(self.selectedObjects)):
+                self.selectedObjects[0].Deselect()
+
+        elif self.toolWnd.FogToolBtn.GetToggled():
+            self.fogLayer.OnLeftDown(pos)
+
+    def OnLeftDClick(self, event):
+        dc = wx.ClientDC(self)
+        self.PrepareDC(dc)
+        pos = event.GetLogicalPosition(dc)
+        pos.x /= self.zoomScale
+        pos.y /= self.zoomScale
+
+        if self.toolWnd.DrawBtn.GetToggled():
+            self.lineLayer.OnLeftDClick(pos)
+
+    def OnLeftUp(self, event):
+        dc = wx.ClientDC(self)
+        self.PrepareDC(dc)
+        pos = event.GetLogicalPosition(dc)
+        pos.x /= self.zoomScale
+        pos.y /= self.zoomScale
+
+        if self.toolWnd.AddShapeBtn.GetToggled() and self.toolWnd.currentShape == 'Circle':
+            self.circleLayer.OnLeftUp(pos)
+
+        elif self.toolWnd.FogToolBtn.GetToggled():
+            self.fogLayer.OnLeftUp(pos)
+
+        elif self.toolWnd.DrawBtn.GetToggled():
+            self.lineLayer.OnLeftUp(pos)
+
+        elif self.toolWnd.SelectorBtn.GetToggled() and self.selectedObjects == []:
+            rgn = wx.Region(self.initiatPos.x, self.initiatPos.y, self.lxd, self.lyd)
+
+            for object in self.zOrder['back']:
+                if rgn.Contains(object.start.x, object.start.y):
+                    object.Select()
+
+            zl = self.zOrder.keys()
+            zl.remove('back')
+            zl.remove('front')
+            zl.remove('tiles')
+            zl.sort()
+
+            for layer in zl:
+                for object in self.zOrder[layer]:
+                    if rgn.Contains(object.start.x, object.start.y):
+                        object.Select()
+
+            for object in self.zOrder['front']:
+                if rgn.Contains(object.start.x, object.start.y):
+                    object.Select()
+
+            self.lxd = 0
+            self.lyd = 0
+            self.initiatPos = pos
+        self.Refresh()
+
+    def OnMotion(self, event):
+        dc = wx.ClientDC(self)
+        self.PrepareDC(dc)
+        pos = event.GetLogicalPosition(dc)
+        pos.x /= self.zoomScale
+        pos.y /= self.zoomScale
+
+
+        #HitTest
+        for object in self.zOrder['back']:
+            object.HitTest(pos)
+
+        zl = self.zOrder.keys()
+        zl.remove('back')
+        zl.remove('front')
+        zl.remove('tiles')
+        zl.sort()
+
+        for layer in zl:
+            for object in self.zOrder[layer]:
+                object.HitTest(pos)
+
+        for object in self.zOrder['front']:
+            object.HitTest(pos)
+
+        if self.toolWnd.AddShapeBtn.GetToggled() and event.m_leftDown and self.toolWnd.currentShape == 'Circle':
+            self.circleLayer.OnMotion(pos)
+
+        elif self.toolWnd.DrawBtn.GetToggled() and self.lineLayer.start != wx.Point(0,0):
+            self.lineLayer.OnMotion(pos)
+
+        elif self.toolWnd.SelectorBtn.GetToggled() and self.selectedObjects != [] and not (self.ctrl or self.shift):
+            xd = (pos.x-self.initiatPos.x)
+            yd = (pos.y-self.initiatPos.y)
+            for obj in self.selectedObjects:
+                obj.start.x += xd
+                obj.start.y += yd
+                obj.Update()
+                self.initiatPos = pos
+
+
+        elif self.toolWnd.SelectorBtn.GetToggled() and self.selectedObjects == [] and event.m_leftDown:
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+            pen = wx.Pen(wx.BLACK, 3, wx.DOT)
+            dc.SetPen(pen)
+            dc.SetLogicalFunction(wx.INVERT)
+
+            xd = (pos.x-self.initiatPos.x)
+            yd = (pos.y-self.initiatPos.y)
+
+            if self.lxd != 0 and self.lyd != 0:
+                r = wx.Rect(self.initiatPos.x, self.initiatPos.y, self.lxd, self.lyd)
+                dc.DrawRectangleRect(r)
+
+            self.lxd = xd
+            self.lyd = yd
+            r = wx.Rect(self.initiatPos.x, self.initiatPos.y, self.lxd, self.lyd)
+            dc.DrawRectangleRect(r)
+
+        elif (self.toolWnd.FogToolBtn.GetToggled()) and event.m_leftDown:
+            self.fogLayer.OnMotion(pos)
+
+    def OnRightDown(self, event):
+        mapmenu = wx.Menu()
+
+        item = wx.MenuItem(mapmenu, wx.ID_ANY, "Load Map", "Load Map")
+        #self.Bind(wx.EVT_MENU, self.OnOpenBtn, item)
+        mapmenu.AppendItem(item)
+
+        item = wx.MenuItem(mapmenu, wx.ID_ANY, "Save Map", "Save Map")
+        #self.Bind(wx.EVT_MENU, self.OnSaveBtn, item)
+        mapmenu.AppendItem(item)
+
+        item = wx.MenuItem(mapmenu, wx.ID_ANY, "Default Map", "Default Map")
+        self.Bind(wx.EVT_MENU, self.OnDefaultBtn, item)
+        mapmenu.AppendItem(item)
+
+        item = wx.MenuItem(mapmenu, wx.ID_ANY, "Map Properties", "Map Properties")
+        #self.Bind(wx.EVT_MENU, OnMapPropsBtn, item)
+        mapmenu.AppendItem(item)
+
+        bgmenu = wx.Menu()
+
+        item = wx.MenuItem(bgmenu, wx.ID_ANY, "Change Background Image", "Change Background Image")
+        self.Bind(wx.EVT_MENU, self.OnBGBtn, item)
+        bgmenu.AppendItem(item)
+
+        item = wx.MenuItem(bgmenu, wx.ID_ANY, "Change Background Color", "Change Background Color")
+        self.Bind(wx.EVT_MENU, self.OnBGColorBtn, item)
+        bgmenu.AppendItem(item)
+
+        item = wx.MenuItem(bgmenu, wx.ID_ANY, "Grid Properties", "Grid Properties")
+        #self.Bind(wx.EVT_MENU, self.OnGridBtn, item)
+        bgmenu.AppendItem(item)
+
+        fogmenu = wx.Menu()
+
+        item = wx.MenuItem(fogmenu, wx.ID_ANY, "Toggle Fog", "Toggle Fog")
+        self.Bind(wx.EVT_MENU, self.OnFogBtn, item)
+        fogmenu.AppendItem(item)
+
+        item = wx.MenuItem(fogmenu, wx.ID_ANY, "Fog Color", "Fog Color")
+        self.Bind(wx.EVT_MENU, self.OnFogColorBtn, item)
+        fogmenu.AppendItem(item)
+
+        menu = wx.Menu()
+
+        if self.toolWnd.gmToolBar.IsShown():
+            menu.AppendMenu(wx.ID_ANY, "Map", mapmenu)
+            menu.AppendMenu(wx.ID_ANY, "Background", bgmenu)
+            menu.AppendMenu(wx.ID_ANY, "Fog", fogmenu)
+            menu.AppendSeparator()
+            item = wx.MenuItem(menu, wx.ID_ANY, "Miniture Properties", "Miniture Properties")
+            #self.Bind(wx.EVT_MENU, self.OnColorBtn, item)
+            menu.AppendItem(item)
+            menu.AppendSeparator()
+
+        item = wx.MenuItem(menu, wx.ID_ANY, "Whiteboard Color", "Whiteboard Color")
+        self.Bind(wx.EVT_MENU, self.OnColorBtn, item)
+        menu.AppendItem(item)
+
+
+        def ObjectMenu(event):
+            id = event.GetId()
+            objid = int(menu.GetHelpString(id))
+            menuname = menu.GetLabel(id)
+            obj = self.overObjects[objid]
+
+            if menuname == "Move To Back":
+                self.MoveToBack(obj)
+
+            elif menuname == "Move Back":
+                self.MoveBack(obj)
+
+            elif menuname == "Move Forward":
+                self.MoveForward(obj)
+
+            elif menuname == "Move To Front":
+                self.MoveToFront(obj)
+
+            elif menuname == "Remove":
+                self.zOrder[obj.zOrder].remove(obj)
+                obj.Update()
+
+            self.Unbind(wx.EVT_MENU)
+            self.overObjects.remove(obj)
+
+
+        if len(self.overObjects):
+            menu.AppendSeparator()
+
+        id = 0
+        for obj in self.overObjects:
+            if obj.IsShown() or self.toolWnd.gmToolBar.IsShown():
+                objmenu = wx.Menu()
+                item = wx.MenuItem(objmenu, wx.ID_ANY, "Move To Back", str(id))
+                self.Bind(wx.EVT_MENU, ObjectMenu, item)
+                objmenu.AppendItem(item)
+                item = wx.MenuItem(objmenu, wx.ID_ANY, "Move Back", str(id))
+                self.Bind(wx.EVT_MENU, ObjectMenu, item)
+                objmenu.AppendItem(item)
+                item = wx.MenuItem(objmenu, wx.ID_ANY, "Move Forward", str(id))
+                self.Bind(wx.EVT_MENU, ObjectMenu, item)
+                objmenu.AppendItem(item)
+                item = wx.MenuItem(objmenu, wx.ID_ANY, "Move To Front", str(id))
+                self.Bind(wx.EVT_MENU, ObjectMenu, item)
+                objmenu.AppendItem(item)
+                objmenu.AppendSeparator()
+                if obj.IsShown():
+                    item = wx.MenuItem(objmenu, wx.ID_ANY, "Hide", str(id))
+                    self.Bind(wx.EVT_MENU, obj.Hide, item)
+                    objmenu.AppendItem(item)
+                    objmenu.AppendSeparator()
+                elif self.toolWnd.gmToolBar.IsShown():
+                    item = wx.MenuItem(objmenu, wx.ID_ANY, "Show", str(id))
+                    self.Bind(wx.EVT_MENU, obj.Show, item)
+                    objmenu.AppendItem(item)
+                    objmenu.AppendSeparator()
+                item = wx.MenuItem(objmenu, wx.ID_ANY, "Remove", str(id))
+                self.Bind(wx.EVT_MENU, ObjectMenu, item)
+                objmenu.AppendItem(item)
+                item = wx.MenuItem(objmenu, wx.ID_ANY, "Properties", str(id))
+                self.Bind(wx.EVT_MENU, obj.ShowProperties, item)
+                objmenu.AppendItem(item)
+                menu.AppendMenu(wx.ID_ANY, obj.GetName(), objmenu)
+
+        menu.AppendSeparator()
+        item = wx.MenuItem(menu, wx.ID_ANY, "Remove All Objects", "Remove All Whiteboard Items")
+        self.Bind(wx.EVT_MENU, self.OnRemoveAllObjects, item)
+        menu.AppendItem(item)
+
+        self.PopupMenu(menu)
+
+
+    def OnRemoveAllObjects(self, event):
+        for layer in self.zOrder:
+            for i in xrange(len(self.zOrder[layer])):
+                del self.zOrder[layer][0]
+
+        self.zOrder = {}
+        self.zOrder['tiles'] = []
+        self.zOrder["back"] = []
+        self.zOrder["front"] = []
+        if event != None:
+            self.UpdateMap()
+
+    def MoveToBack(self, object):
+        self.zOrder[object.zOrder].remove(object)
+        self.zOrder['back'].append(object)
+        object.zOrder = 'back'
+        self.UpdateMap()
+
+    def MoveToFront(self, object):
+        self.zOrder[object.zOrder].remove(object)
+        self.zOrder['front'].append(object)
+        object.zOrder = 'front'
+        self.UpdateMap()
+
+    def MoveBack(self, object):
+        self.zOrder[object.zOrder].remove(object)
+
+        zl = self.zOrder.keys()
+        zl.remove('back')
+        zl.remove('front')
+        zl.remove('tiles')
+        zl.sort()
+        lzo = 1
+        if len(zl):
+            lzo = zl.pop()
+
+        if object.zOrder == 'back' or object.zOrder == 1:
+            self.zOrder['back'].append(object)
+            object.zOrder = 'back'
+        elif object.zOrder == 'front':
+            if not self.zOrder.has_key(lzo):
+                self.zOrder[lzo] = []
+            self.zOrder[lzo].append(object)
+            object.zOrder = lzo
+        else:
+            object.zOrder -= 1
+            if not self.zOrder.has_key(object.zOrder):
+                self.zOrder[object.zOrder] = []
+            self.zOrder[object.zOrder].append(object)
+        self.UpdateMap()
+
+    def MoveForward(self, object):
+        self.zOrder[object.zOrder].remove(object)
+
+        zl = self.zOrder.keys()
+        zl.remove('back')
+        zl.remove('front')
+        zl.remove('tiles')
+        zl.sort()
+        lzo = 1
+        if len(zl):
+            lzo = zl.pop()
+
+        if object.zOrder == 'back':
+            if not self.zOrder.has_key(1):
+                self.zOrder[1] = []
+            self.zOrder[1].append(object)
+            object.zOrder = 1
+        elif z == 'front':
+            self.zOrder['front'].append(object)
+            object.zOrder = 'front'
+        else:
+            object.zOrder += 1
+            if not self.zOrder.has_key(object.zOrder):
+                self.zOrder[object.zOrder] = []
+            self.zOrder[object.zOrder].append(object)
+        self.UpdateMap()
+
+    def OnScroll(self, event):
+        event.Skip()
+        self.Refresh()
+
+    def OnZoomTimer(self, event):
+        if (time.time() - self.lastZoomTime) >= 3 and self.lastZoomScale != self.zoomScale:
+            #Send Zoome Notice to other clients
+            self.lastZoomTime = time.time()
+            self.lastZoomScale = self.zoomScale
+
+    def OnRoleTimer(self, event):
+        #Figure out the users role
+        if self.session.my_role() == self.session.ROLE_GM:
+            self.role = 'GM'
+        elif self.session.my_role() == self.session.ROLE_PLAYER:
+            self.role = 'Player'
+        else:
+            self.role = 'Lurker'
+
+        if self.role == 'GM' and not self.toolWnd.gmToolBar.IsShown() and not (str(self.session.group_id) == '0' and str(self.session.status) == '1'):
+            self.toolWnd.Freeze()
+            self.toolWnd.gmToolBar.Show()
+            self.toolWnd.Thaw()
+        elif self.role == 'Player' and not (str(self.session.group_id) == '0' and str(self.session.status) == '1'):
+            if self.toolWnd.gmToolBar.IsShown():
+                self.toolWnd.Freeze()
+                self.toolWnd.gmToolBar.Hide()
+                self.toolWnd.Thaw()
+
+            if not self.toolWnd.playerToolBar.IsShown():
+                self.toolWnd.Freeze()
+                self.toolWnd.playerToolBar.Show()
+                self.toolWnd.Thaw()
+        elif self.role == 'Lurker' or (str(self.session.group_id) == '0' and str(self.session.status) == '1'):
+            if self.toolWnd.playerToolBar.IsShown():
+                self.toolWnd.Freeze()
+                self.toolWnd.gmToolBar.Hide()
+                self.toolWnd.playerToolBar.Hide()
+                self.toolWnd.Thaw()
+
+        try:
+            self.toolWnd.Layout()
+        except:
+            pass
+
+    def OnClose(self, event):
+        self.zoomTimer.Stop()
+        self.roleTimer.Stop()
+        event.Skip()
+
+    #Toolbar Events
+    def OnDefaultBtn(self, event):
+        self.Clear()
+        wx.CallAfter(self.UpdateMap)
+
+    def OnColorBtn(self, event):
+        newcolor = self.RGBHex.do_hex_color_dlg(self.toolWnd)
+        if newcolor == None:
+            return
+
+        self.whiteboardColor = newcolor
+        r, g, b = self.RGBHex.rgb_tuple(self.whiteboardColor)
+        self.toolWnd._SetColorBtn(wx.Color(r, g, b, 255), self.toolWnd.ColorBtn)
+
+    def OnBGColorBtn(self, event):
+        newcolor = self.RGBHex.do_hex_color_dlg(self.toolWnd)
+        if newcolor == None:
+            return
+
+        self.backgroundColor = newcolor
+        r, g, b = self.RGBHex.rgb_tuple(self.backgroundColor)
+        self.toolWnd._SetColorBtn(wx.Color(r, g, b, 255), self.toolWnd.BGColorBtn)
+        self.UpdateMap()
+
+    def OnFogColorBtn(self, event):
+        newcolor = self.RGBHex.do_hex_color_dlg(self.toolWnd)
+        if newcolor == None:
+            return
+
+        self.fogColor = newcolor
+        r, g, b = self.RGBHex.rgb_tuple(self.fogColor)
+        self.toolWnd._SetColorBtn(wx.Color(r, g, b, 255), self.toolWnd.FogColorBtn)
+        self.UpdateMap()
+
+    def OnExlusiveBtn(self, event):
+        id = event.GetId()
+        #This is backwards because the Toggle Switch does not get set until AFTER The mouse gets released
+        if not self.toolWnd.exclusiveToolList[id].GetToggled():
+            self.toolWnd.Freeze()
+            #Disable all mutualy exclusive tools
+            for btn in self.toolWnd.exclusiveToolList:
+                if self.toolWnd.exclusiveToolList[btn].GetId() != id:
+                    self.toolWnd.exclusiveToolList[btn].SetToggled(False)
+            self.toolWnd.Thaw()
+        else:
+            wx.CallAfter(self.toolWnd.SelectorBtn.SetToggled, True)
+
+    def OnFogBtn(self, event):
+        if not self.toolWnd.FogBtn.GetToggled():
+            self.useFog = True
+        else:
+            self.useFog = False
+            self.toolWnd.Freeze()
+            self.toolWnd.SelectorBtn.SetToggled(True)
+            self.toolWnd.FogToolBtn.SetToggled(False)
+            self.toolWnd.Thaw()
+        self.fogRegion = []
+        self.UpdateMap()
+
+    def OnBGBtn(self, event):
+        dlg = wx.Dialog(self.toolWnd, wx.ID_ANY, title="Background Properties")
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        filename = wx.TextCtrl(dlg, wx.ID_ANY)
+        filename.Hide()
+
+        bgpath = wx.TextCtrl(dlg, wx.ID_ANY)
+        if self.bgPath != None:
+            bgpath.SetValue(self.bgPath)
+
+        bgtype = wx.Choice(dlg, wx.ID_ANY, choices=['Image', 'Texture'])
+        bgtype.SetStringSelection(self.bgType)
+
+        browsebtn = wx.Button(dlg, wx.ID_ANY, "Browse")
+        okbtn = wx.Button(dlg, wx.ID_OK)
+        cancelbtn = wx.Button(dlg, wx.ID_CANCEL)
+
+        sizer.Add(wx.StaticText(dlg, wx.ID_ANY, "Image Path"), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2)
+        sizer.Add(bgpath, 0, wx.EXPAND|wx.ALL, 3)
+        sizer.Add(wx.StaticText(dlg, wx.ID_ANY, "Image Type"), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2)
+        sizer.Add(bgtype, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 3)
+        sizer.Add(browsebtn, 0, wx.EXPAND|wx.ALL, 2)
+        sizer.Add(okbtn, 0, wx.EXPAND|wx.ALL, 3)
+        sizer.Add(cancelbtn, 0, wx.EXPAND|wx.ALL, 2)
+
+        dlg.SetSizer(sizer)
+        dlg.SetAutoLayout(True)
+        dlg.Fit()
+
+        def OnBrowse(event):
+            filedlg = wx.FileDialog(self, "Select an Image File", self.dir_struct["user"], wildcard="Image files (*.bmp, *.gif, *.jpg, *.png)|*.bmp;*.gif;*.jpg;*.png", style=wx.HIDE_READONLY|wx.OPEN)
+            if filedlg.ShowModal() != wx.ID_OK:
+                filedlg.Destroy()
+                return
+
+            bgpath.SetValue(filedlg.GetPath())
+            filename.SetValue(filedlg.GetFilename())
+
+        dlg.Bind(wx.EVT_BUTTON, OnBrowse, browsebtn)
+        dlg.Show()
+
+        if not dlg.ShowModal() == wx.ID_OK:
+            dlg.Destroy()
+            return
+
+        self.bgType = bgtype.GetStringSelection()
+
+        if bgpath.GetValue().lower().find('http:') == -1:
+            file = open(bgpath.GetValue(), "rb")
+            imgdata = file.read()
+            file.close()
+
+            (imgtype,j) = mimetypes.guess_type(filename.GetValue())
+
+            postdata = urllib.urlencode({'filename':filename.GetValue(), 'imgdata':imgdata, 'imgtype':imgtype})
+
+            thread.start_new_thread(self.__Upload, (postdata, bgpath.GetValue(), "Background"))
+        else:
+            self.bgImage = self._LoadImage(bgpath.GetValue())
+            self.UpdateMap()
+
+
+    #Private Methods
+    def _SetSize(self, size):
+        if size[0] == -1:
+            size[0] = self.size[0]
+        if size[1] == -1:
+            size[1] = self.size[1]
+
+        if size[0] < 300:
+            size = (300, size[1])
+        if size[1] < 300:
+            size = (size[0], 300)
+
+        size1  = self.GetClientSizeTuple()
+
+        if size[0] < size1[0]:
+            size = (size1[0], size[1])
+        if size[1] < size1[1]:
+            size = (size[0], size1[1])
+
+        self.sizeChanged = 1
+        self.size = size
+        self._FixScroll()
+
+    def _FixScroll(self):
+        scale = self.zoomScale
+        pos = self.GetViewStart()
+        unit = self.GetScrollPixelsPerUnit()
+        pos = [pos[0]*unit[0],pos[1]*unit[1]]
+        size = self.GetClientSize()
+        unit = [10*scale,10*scale]
+        if (unit[0] == 0 or unit[1] == 0):
+            return
+        pos[0] /= unit[0]
+        pos[1] /= unit[1]
+        mx = [int(self.size[0]*scale/unit[0])+1, int(self.size[1]*scale/unit[1]+1)]
+        self.SetScrollbars(unit[0], unit[1], mx[0], mx[1], pos[0], pos[1])
+
+    def _LoadImage(self, path, miniId=None):
+        if self.imageCache.has_key(path):
+            return self.imageCache[path]
+
+        while len(self.imageCache) > int(self.settings.get_setting("ImageCacheSize")):
+            keys = self.imageCache.keys()
+            del self.imageCache[keys[0]]
+
+
+        thread.start_new_thread(self.__DownloadImage, (path, miniId))
+
+        return wx.Bitmap(orpg.dirpath.dir_struct["icon"] + "fetching.png", wx.BITMAP_TYPE_PNG)
+
+    def _ClearCache(self):
+        for key in self.imageCache:
+            del self.imageCache[key]
+
+    #Threads
+    def __Upload(self, postdata, filename, type="Background"):
+        self.lock.acquire()
+
+        url = self.settings.get_setting('ImageServerBaseURL')
+        file = urllib.urlopen(url, postdata)
+        recvdata = file.read()
+        file.close()
+        try:
+            xml_dom = minidom.parseString(recvdata)._get_documentElement()
+
+            if xml_dom.nodeName == 'path':
+                path = xml_dom.getAttribute('url')
+                path = urllib.unquote(path)
+
+                if type == 'Background':
+                    self.bgImage = self._LoadImage(path)
+                    self.bgPath = path
+
+                else:
+                    self.minis.append(self.mapLayer.AddMiniture(path))
+
+                self.UpdateMap()
+
+            else:
+                self.chat.InfoPost(xml_dom.getAttribute('msg'))
+        except Exception, e:
+            print e
+            print recvdata
+
+        self.lock.release()
+
+    def __DownloadImage(self, path, miniId):
+        self.lock.acquire()
+
+        uriPath = urllib.unquote(path)
+        try:
+            data = urllib.urlretrieve(uriPath)
+
+            if data[0] and data[1].getmaintype() == "image":
+                imageType = data[1].gettype()
+                img = wx.ImageFromMime(data[0], imageType).ConvertToBitmap()
+                self.imageCache[path] = img
+
+                if miniId == None:
+                    self.bgImage = img
+                    if self.bgType == 'Image':
+                        self._SetSize((img.GetHeight(), img.GetWidth()))
+
+                else:
+                    mini = self.GetMiniById(miniId)
+                    mini.image = img
+
+                self.UpdateMap()
+        except Exception, e:
+            self.chat.InfoPost("Unable to resolve/open the specified URI; image was NOT laoded:" + path)
+
+        urllib.urlcleanup()
+        self.lock.release()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/map/_circles.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,168 @@
+from math import sqrt
+
+import wx
+
+import orpg.dirpath
+from orpg.orpgCore import *
+
+from _object import MapObject
+
+class MapCircle(MapObject):
+    def __init__(self, canvas, center=wx.Point(0,0), radius=0, color="#000000"):
+        MapObject.__init__(self, canvas=canvas)
+        self.start = center
+        self.radius = int(radius)
+        self.color = color
+
+        r, g, b = self.RGBHex.rgb_tuple(self.color)
+        self.hcolor = self.RGBHex.hexstring(r^255, g^255, b^255)
+
+        self.id = 'circle-' + self.canvas.GetNewObjectId()
+
+
+    def Draw(self, dc):
+        path = dc.CreatePath()
+
+        if not self.highlighed:
+            c = self.color
+        else:
+            c = self.hcolor
+        r, g, b = self.RGBHex.rgb_tuple(c)
+
+        pen = wx.TRANSPARENT_PEN
+        brush = wx.TRANSPARENT_BRUSH
+        if self.IsShown():
+            brush = wx.Brush(wx.Color(r, g, b, 128))
+            pen = wx.Pen(wx.Color(r, g, b, 128))
+        elif self.canvas.toolWnd.gmToolBar.IsShown():
+            brush = wx.Brush(wx.Color(r, g, b, 40))
+            pen = wx.Pen(wx.Color(r, g, b, 40))
+            font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
+            dc.SetFont(font, wx.RED)
+            w, h = dc.GetTextExtent("Hidden")
+            dc.DrawText("Hidden", self.start.x-(w/2), self.start.y-(h/2), dc.CreateBrush(wx.WHITE_BRUSH))
+
+        dc.SetBrush(brush)
+        dc.SetPen(pen)
+
+        path.AddCircle(self.start.x, self.start.y, self.radius)
+        path.CloseSubpath()
+        dc.DrawPath(path)
+
+        dc.SetBrush(wx.NullBrush)
+        dc.SetPen(wx.NullPen)
+
+        if self.selected:
+            self.DrawSelection(dc)
+
+    def DrawSelection(self, dc):
+        dc.SetBrush(wx.GREEN_BRUSH)
+        dc.SetPen(wx.GREEN_PEN)
+        path = dc.CreatePath()
+
+        path.AddRectangle(self.start.x-self.radius, self.start.y-self.radius, 5, 5)
+        path.AddRectangle(self.start.x-self.radius, self.start.y+self.radius, 5, 5)
+        path.AddRectangle(self.start.x+self.radius, self.start.y-self.radius, 5, 5)
+        path.AddRectangle(self.start.x+self.radius, self.start.y+self.radius, 5, 5)
+
+        path.MoveToPoint(self.start.x, self.start.y)
+        path.AddLineToPoint(self.start.x-10, self.start.y)
+        path.MoveToPoint(self.start.x, self.start.y)
+        path.AddLineToPoint(self.start.x, self.start.y+10)
+        path.MoveToPoint(self.start.x, self.start.y)
+        path.AddLineToPoint(self.start.x+10, self.start.y)
+        path.MoveToPoint(self.start.x, self.start.y)
+        path.AddLineToPoint(self.start.x, self.start.y-10)
+
+        dc.DrawPath(path)
+
+        dc.SetBrush(wx.NullBrush)
+        dc.SetPen(wx.NullPen)
+
+    def InObject(self, pos):
+        xd = (self.start.x-pos.x)*(self.start.x-pos.x)
+        yd = (self.start.y-pos.y)*(self.start.y-pos.y)
+        distance = sqrt(xd+yd)
+
+        if distance <= self.radius:
+            return True
+
+        return False
+
+    def GetName(self):
+        return 'Circle: ' + str(self.id) + ' Radius:' + str(self.radius) + ' Color:' + self.color
+
+    def ShowProperties(self, event):
+        dlg = wx.Dialog(self.canvas, wx.ID_ANY, "Circle Properties")
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        radius = wx.TextCtrl(dlg, wx.ID_ANY)
+        radius.SetValue(str(self.radius))
+
+        colorbtn = wx.Button(dlg, wx.ID_ANY, "Color")
+        colorbtn.SetForegroundColour(self.hcolor)
+
+        def ColorBtn(event):
+            newcolor = self.RGBHex.do_hex_color_dlg(self.canvas)
+            if newcolor == None:
+                return
+
+            colorbtn.SetForegroundColour(newcolor)
+            dlg.Unbind(wx.EVT_BUTTON)
+
+        dlg.Bind(wx.EVT_BUTTON, ColorBtn, colorbtn)
+
+        sizer.Add(wx.StaticText(dlg, wx.ID_ANY, "Radius:"), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2)
+        sizer.Add(radius, 0, wx.EXPAND|wx.ALL, 3)
+        sizer.Add(colorbtn, 0, wx.ALL, 2)
+        sizer.Add(wx.Button(dlg, wx.ID_OK), 0, wx.ALL, 3)
+
+        dlg.SetSizer(sizer)
+        dlg.SetAutoLayout(True)
+        dlg.Fit()
+        dlg.Show()
+
+        if dlg.ShowModal() == wx.ID_OK:
+            self.radius = int(radius.GetValue())
+            r,g,b = colorbtn.GetForegroundColour().Get()
+            self.color = self.RGBHex.hexstring(r, g, b)
+            self.hcolor = self.RGBHex.hexstring(r^255, g^255, b^255)
+            self.Update(send=True, action="update")
+
+
+    def OnLeftDown(self, pos):
+        self.start = pos
+        self.lastRadius = 0
+        self.radius = 0
+
+    def OnMotion(self, pos):
+        dc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(dc)
+        dc.SetLogicalFunction(wx.EQUIV)
+        dc.SetUserScale(self.canvas.zoomScale, self.canvas.zoomScale)
+
+
+        if self.radius > 0:
+            dc.DrawCircle(self.start.x, self.start.y, self.radius)
+
+        xd = (self.start.x-pos.x)*(self.start.x-pos.x)
+        yd = (self.start.y-pos.y)*(self.start.y-pos.y)
+        self.radius = sqrt(xd+yd)
+
+        #self.lastRadius = self.radius
+        dc.DrawCircle(self.start.x, self.start.y, self.radius)
+
+    def OnLeftUp(self, pos):
+        xd = (self.start.x-pos.x)*(self.start.x-pos.x)
+        yd = (self.start.y-pos.y)*(self.start.y-pos.y)
+        radius = sqrt(xd+yd)
+
+        if radius > 15:
+            self.canvas.zOrder['front'].append(MapCircle(self.canvas, self.start, radius, self.canvas.whiteboardColor))
+            self.Update(send=True, action='new')
+        self.lastRadius = 0
+        self.start = wx.Point(0,0)
+        self.radius = 0
+
+    def _toxml(self, action="update"):
+        return ''
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/map/_fog.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,92 @@
+from math import sqrt
+
+import wx
+
+import orpg.dirpath
+from orpg.orpgCore import *
+
+from _object import MapObject
+
+class FogLayer(MapObject):
+    def __init__(self, canvas):
+        MapObject.__init__(self, canvas=canvas)
+
+    def Draw(self, dc):
+        path = dc.CreatePath()
+        r, g, b = self.RGBHex.rgb_tuple(self.canvas.fogColor)
+        if self.canvas.toolWnd.gmToolBar.IsShown():
+            brush = wx.Brush(wx.Color(r, g, b, 128))
+        else:
+            brush = wx.Brush(wx.Color(r, g, b, 255))
+        dc.SetBrush(brush)
+
+        self.region = wx.Region(0, 0, self.canvas.size[0]+2, self.canvas.size[1]+2)
+
+        points = []
+        lp = 's'
+        for point in self.canvas.fogRegion:
+            if point == 's' or point == 'h':
+                if lp == 's' and len(points) > 0:
+                    self.region.XorRegion(wx.RegionFromPoints(points))
+                    self.region.SubtractRegion(wx.RegionFromPoints(points))
+                elif len(points) > 0:
+                    self.region.UnionRegion(wx.RegionFromPoints(points))
+                lp = point
+                points = []
+            else:
+                points.append((point.x, point.y))
+
+        if len(points) > 0:
+            if lp == 's':
+                self.region.XorRegion(wx.RegionFromPoints(points))
+                self.region.SubtractRegion(wx.RegionFromPoints(points))
+            else:
+                self.region.UnionRegion(wx.RegionFromPoints(points))
+
+        dc.ClipRegion(self.region)
+
+        dc.DrawRectangle(0, 0, self.canvas.size[0]+2, self.canvas.size[1]+2)
+
+        dc.SetBrush(wx.NullBrush)
+
+    def OnLeftDown(self, pos):
+        self.start = pos
+        self.lastPoint = pos
+        if self.canvas.toolWnd.currentFog == 'Show':
+            self.canvas.fogRegion.append('s')
+        else:
+            self.canvas.fogRegion.append('h')
+        self.canvas.fogRegion.append(pos)
+
+    def OnMotion(self, pos):
+        cdc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(cdc)
+
+        dc = wx.GraphicsContext.Create(cdc)
+        dc.Scale(self.canvas.zoomScale, self.canvas.zoomScale)
+
+        dc.SetPen(wx.WHITE_PEN)
+
+        path = dc.CreatePath()
+
+        xd = (self.lastPoint.x-pos.x)*(self.lastPoint.x-pos.x)
+        yd = (self.lastPoint.y-pos.y)*(self.lastPoint.y-pos.y)
+        distance = sqrt(xd+yd)
+
+        if distance > 5:
+            path.MoveToPoint(self.lastPoint.x, self.lastPoint.y)
+            path.AddLineToPoint(pos.x, pos.y)
+
+            self.canvas.fogRegion.append(pos)
+            self.lastPoint = pos
+
+        path.CloseSubpath()
+        dc.StrokePath(path)
+
+        dc.SetPen(wx.NullPen)
+
+    def OnLeftUp(self, pos):
+        self.canvas.fogRegion.append(pos)
+        self.canvas.fogRegion.append(self.start)
+        self.canvas.UpdateMap()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/map/_grid.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,39 @@
+import wx
+
+import orpg.dirpath
+from orpg.orpgCore import *
+from orpg.tools.rgbhex import RGBHex
+
+class GridLayer:
+    def __init__(self, canvas):
+        self.canvas = canvas
+        self.RGBHex = RGBHex()
+
+    def Draw(self, dc):
+        r, g, b = self.RGBHex.rgb_tuple(self.canvas.gridColor)
+        pen = wx.Pen(wx.Color(r, g, b, 255), 1, self.canvas.gridLines)
+        dc.SetPen(pen)
+
+        path = dc.CreatePath()
+
+        if self.canvas.gridType == 'Square':
+            self._DrawSquare(dc, path)
+
+        dc.SetPen(wx.NullPen)
+
+    def _DrawSquare(self, dc, path):
+        path.MoveToPoint(0, 0)
+        y = 0
+        while y < self.canvas.size[1]:
+            path.AddLineToPoint(self.canvas.size[0], y)
+            y += self.canvas.gridSize
+            path.MoveToPoint(0, y)
+
+        path.MoveToPoint(0, 0)
+        x = 0
+        while x < self.canvas.size[0]:
+            path.AddLineToPoint(x, self.canvas.size[0])
+            x += self.canvas.gridSize
+            path.MoveToPoint(x, 0)
+
+        dc.StrokePath(path)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/map/_lines.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,157 @@
+from math import sqrt
+
+import wx
+
+import orpg.dirpath
+from orpg.orpgCore import *
+
+from _object import MapObject
+
+class MapLine(MapObject):
+    def __init__(self, canvas, start=wx.Point(0,0), width=1, color="#000000", points=[]):
+        MapObject.__init__(self, canvas=canvas)
+        self.start = wx.Point(start[0], start[1])
+        self.color = color
+        self.points = points
+        self.width = width
+
+        r, g, b = self.RGBHex.rgb_tuple(self.color)
+        self.hcolor = self.RGBHex.hexstring(r^255, g^255, b^255)
+
+        self.id = 'line-' + self.canvas.GetNewObjectId()
+
+
+    def Draw(self, dc):
+        path = dc.CreatePath()
+
+        if not self.highlighed:
+            c = self.color
+        else:
+            c = self.hcolor
+        r, g, b = self.RGBHex.rgb_tuple(c)
+
+        pen = wx.Pen(wx.Color(r, g, b, 0), self.width)
+        if self.IsShown():
+            pen = wx.Pen(wx.Color(r, g, b, 255), self.width)
+        elif self.canvas.toolWnd.gmToolBar.IsShown():
+            pen = wx.Pen(wx.Color(r, g, b, 40), self.width)
+        dc.SetPen(pen)
+
+        dc.DrawLines(self.points)
+
+        dc.SetBrush(wx.NullBrush)
+        dc.SetPen(wx.NullPen)
+
+        if self.selected:
+            self.DrawSelection(dc)
+
+    def DrawSelection(self, dc):
+        dc.SetBrush(wx.GREEN_BRUSH)
+        dc.SetPen(wx.GREEN_PEN)
+        path = dc.CreatePath()
+
+        dc.DrawPath(path)
+
+        dc.SetBrush(wx.NullBrush)
+        dc.SetPen(wx.NullPen)
+
+    def InObject(self, pos):
+        for point in self.points:
+            xd = (point[0]-pos.x)*(point[0]-pos.x)
+            yd = (point[1]-pos.y)*(point[1]-pos.y)
+            distance = sqrt(xd+yd)
+
+            if distance <= self.width+1:
+                return True
+
+        return False
+
+    def GetName(self):
+        return self.id + ' Color:' + self.color
+
+    def ShowProperties(self, event):
+        dlg = wx.Dialog(self.canvas, wx.ID_ANY, "Circle Properties")
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        radius = wx.TextCtrl(dlg, wx.ID_ANY)
+        radius.SetValue(str(self.radius))
+
+        colorbtn = wx.Button(dlg, wx.ID_ANY, "Color")
+        colorbtn.SetForegroundColour(self.hcolor)
+
+        def ColorBtn(event):
+            newcolor = self.RGBHex.do_hex_color_dlg(self.canvas)
+            if newcolor == None:
+                return
+
+            colorbtn.SetForegroundColour(newcolor)
+            dlg.Unbind(wx.EVT_BUTTON)
+
+        dlg.Bind(wx.EVT_BUTTON, ColorBtn, colorbtn)
+
+        sizer.Add(wx.StaticText(dlg, wx.ID_ANY, "Radius:"), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2)
+        sizer.Add(radius, 0, wx.EXPAND|wx.ALL, 3)
+        sizer.Add(colorbtn, 0, wx.ALL, 2)
+        sizer.Add(wx.Button(dlg, wx.ID_OK), 0, wx.ALL, 3)
+
+        dlg.SetSizer(sizer)
+        dlg.SetAutoLayout(True)
+        dlg.Fit()
+        dlg.Show()
+
+        if dlg.ShowModal() == wx.ID_OK:
+            self.radius = int(radius.GetValue())
+            r,g,b = colorbtn.GetForegroundColour().Get()
+            self.color = self.RGBHex.hexstring(r, g, b)
+            self.hcolor = self.RGBHex.hexstring(r^255, g^255, b^255)
+            self.Update(send=True, action="update")
+
+
+    def OnLeftDown(self, pos):
+        self.lastPoint = pos
+        self.start = pos
+        self.points.append((pos.x, pos.y))
+
+    def OnMotion(self, pos):
+        dc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(dc)
+
+        r,g,b = self.RGBHex.rgb_tuple(self.canvas.whiteboardColor)
+        pen = wx.Pen(wx.Color(r,g,b,255), int(self.canvas.toolWnd.LineWidth.GetStringSelection()))
+        dc.SetPen(pen)
+
+        xd = (self.lastPoint.x-pos.x)*(self.lastPoint.x-pos.x)
+        yd = (self.lastPoint.y-pos.y)*(self.lastPoint.y-pos.y)
+        distance = sqrt(xd+yd)
+
+        if distance > 5:
+            if self.canvas.toolWnd.currentLine == 'Free':
+                self.points.append((pos.x, pos.y))
+                self.lastPoint = pos
+                dc.DrawLines(self.points)
+            else:
+                dc.SetLogicalFunction(wx.INVERT)
+                dc.DrawLine(self.start.x, self.start.y, self.lastPoint.x, self.lastPoint.y)
+                dc.DrawLine(self.start.x, self.start.y, pos.x, pos.y)
+                dc.SetLogicalFunction(wx.COPY)
+                dc.DrawLines(self.points)
+                self.lastPoint = pos
+
+        dc.SetPen(wx.NullPen)
+
+    def OnLeftUp(self, pos):
+        if self.canvas.toolWnd.currentLine == 'Free' and len(self.points) > 2:
+            self.points.append((pos.x, pos.y))
+            self.canvas.zOrder['front'].append(MapLine(self.canvas, self.points[0], int(self.canvas.toolWnd.LineWidth.GetStringSelection()), self.canvas.whiteboardColor, self.points))
+            self.start = wx.Point(0,0)
+            self.points = []
+
+    def OnLeftDClick(self, pos):
+        if self.canvas.toolWnd.currentLine == 'Poly' and len(self.points) > 2:
+            self.points.append((pos.x, pos.y))
+            self.canvas.zOrder['front'].append(MapLine(self.canvas, self.points[0], int(self.canvas.toolWnd.LineWidth.GetStringSelection()), self.canvas.whiteboardColor, self.points))
+            self.points = []
+            self.start = wx.Point(0,0)
+
+    def _toxml(self, action="update"):
+        return ''
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/map/_object.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,165 @@
+from math import sqrt
+
+import wx
+
+import orpg.dirpath
+from orpg.orpgCore import *
+from orpg.tools.rgbhex import RGBHex
+
+wxEVT_ENTER_OBJECT = wx.NewEventType()
+wxEVT_LEAVE_OBJECT = wx.NewEventType()
+wxEVT_SELECT_OBJECT = wx.NewEventType()
+wxEVT_DESELECT_OBJECT = wx.NewEventType()
+EVT_ENTER_OBJECT = wx.PyEventBinder(wxEVT_ENTER_OBJECT)
+EVT_LEAVE_OBJECT = wx.PyEventBinder(wxEVT_LEAVE_OBJECT)
+EVT_SELECT_OBJECT = wx.PyEventBinder(wxEVT_SELECT_OBJECT)
+EVT_DESELECT_OBJECT = wx.PyEventBinder(wxEVT_DESELECT_OBJECT)
+
+class ObjectEvent(wx.PyCommandEvent):
+    def __init__(self, eventType, object):
+        wx.PyCommandEvent.__init__(self, eventType)
+
+        self._object = object
+
+        self._eventType = eventType
+        self.notify = wx.NotifyEvent(eventType, -1)
+
+    def GetNotifyEvent(self):
+        return self.notify
+
+    def GetObject(self):
+        return self._object
+
+    def GetId(self):
+        return self._object.GetId()
+
+class MapObject:
+    def __init__(self, **kwargs):
+        self.id = -1
+        self.start = wx.Point(0,0)
+        self.color = "#000000"
+        self.hcolor = "#ffffff"
+        self.lineWidth = 1
+        self.zOrder = 'front'
+        self.selected = False
+        self.inObject = False
+        self.highlighed = False
+        self.isshown = True
+        self.canvas = None
+        self.RGBHex = RGBHex()
+        self.trans = 1
+
+        for atter, value in kwargs.iteritems():
+            setattr(self, atter, value)
+
+        try:
+            if self.id == wx.ID_ANY:
+                self.id = wx.NewId()
+        except:
+            self.id = wx.NewId()
+
+    #Public Methods
+    def HitTest(self, pos):
+        if self.InObject(pos) and not self.inObject and not self.selected:
+            self.inObject = True
+            evt = ObjectEvent(wxEVT_ENTER_OBJECT, self)
+            self.canvas.GetEventHandler().ProcessEvent(evt)
+        elif not self.InObject(pos) and self.inObject and not self.selected:
+            self.inObject = False
+            evt = ObjectEvent(wxEVT_LEAVE_OBJECT, self)
+            self.canvas.GetEventHandler().ProcessEvent(evt)
+
+    def GetId(self):
+        return self.id
+
+    def IsShown(self):
+        return self.isshown
+
+    def Show(self, event=None, show=True):
+        self.isshown = show
+        self.Update(send=True, action="update")
+
+    def Hide(self, event=None):
+        self.isshown = False
+        self.Update(send=True, action="update")
+
+    def IsSelected(self):
+        return self.selected
+
+    def Select(self, select=True):
+        self.selected = select
+
+        if select:
+            evt = ObjectEvent(wxEVT_SELECT_OBJECT, self)
+        else:
+            evt = ObjectEvent(wxEVT_DESELECT_OBJECT, self)
+
+        self.canvas.GetEventHandler().ProcessEvent(evt)
+
+    def Deselect(self):
+        self.selected = False
+        evt = ObjectEvent(wxEVT_DESELECT_OBJECT, self)
+        self.canvas.GetEventHandler().ProcessEvent(evt)
+
+    def Update(self, send=False, action="update"):
+        self.canvas.UpdateMap()
+        if send:
+            self.canvas.session.send(self._toxml(action))
+
+    def GetName(self):
+        return 'ID: ' + str(self.id) + ' Color: ' + self.color
+
+    def InObject(self, pos):
+        pass
+
+    def Draw(self, dc):
+        if self.selected:
+            self.DrawSelected(dc)
+
+    def DrawSelected(self, dc):
+        pass
+
+    def Highlight(self):
+        self.highlighed = True
+        self.Update()
+
+    def UnHighlight(self):
+        self.highlighed = False
+        self.Update()
+
+    def OnLeftDown(self, pos):
+        if self.inObject and self.selected:
+            self.start = pos
+            self.Deselect()
+
+        elif self.inObject and not self.selected:
+            self.Select()
+
+        else:
+            self.start = pos
+
+        self.Update()
+
+    def OnMotion(self, pos):
+        cdc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(cdc)
+        dc = wx.GraphicsContext.Create(cdc)
+
+        if self.selected:
+            self.start = pos
+            self.Draw(dc)
+
+    def OnLeftUp(self, pos):
+        pass
+
+    def OnRightDown(self, pos):
+        pass
+
+    def OnLeftDClick(self, pos):
+        pass
+
+    def ShowProperties(self, event):
+        pass
+
+    def _toxml(self, action="update"):
+        return ''
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/map/_text.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,161 @@
+from math import sqrt
+
+import wx
+
+import orpg.dirpath
+from orpg.orpgCore import *
+
+from _object import MapObject
+
+class MapText(MapObject):
+    def __init__(self, canvas, start=wx.Point(0,0), text='', size=12, weight=wx.NORMAL, style=wx.NORMAL, color="#000000"):
+        MapObject.__init__(self, canvas=canvas)
+        self.start = start
+        self.color = color
+        self.text = text
+        self.weight = weight
+        self.style = style
+        self.size = size
+
+        r, g, b = self.RGBHex.rgb_tuple(self.color)
+        self.hcolor = self.RGBHex.hexstring(r^255, g^255, b^255)
+
+        self.id = 'text-' + self.canvas.GetNewObjectId()
+
+
+    def Draw(self, dc):
+        if not self.highlighed:
+            c = self.color
+        else:
+            c = self.hcolor
+
+        font = wx.Font(self.size, wx.DEFAULT, self.weight, self.style)
+        dc.SetFont(font, c)
+        w, h = dc.GetTextExtent(self.text)
+
+
+        if self.IsShown():
+            dc.DrawText(self.text, self.start.x-(w/2), self.start.y-(h/2))
+        elif self.canvas.toolWnd.gmToolBar.IsShown():
+            r, g, b = self.RGBHex.rgb_tuple(c)
+            dc.SetFont(font, wx.Color(r, g, b, 40))
+            dc.DrawText(self.text, self.start.x-(w/2), self.start.y-(h/2))
+
+
+        if self.selected:
+            self.DrawSelection(dc)
+
+    def DrawSelection(self, dc):
+        w, h = dc.GetTextExtent(self.text)
+        dc.SetBrush(wx.GREEN_BRUSH)
+        dc.SetPen(wx.GREEN_PEN)
+        path = dc.CreatePath()
+
+        path.AddRectangle(self.start.x-((w/2)+1), self.start.y-((h/2)+1), 5, 5)
+        path.AddRectangle(self.start.x-((w/2)+1), self.start.y+((h/2)+1), 5, 5)
+        path.AddRectangle(self.start.x+((w/2)+1), self.start.y-((h/2)+1), 5, 5)
+        path.AddRectangle(self.start.x+((w/2)+1), self.start.y+((h/2)+1), 5, 5)
+
+        dc.DrawPath(path)
+
+        dc.SetBrush(wx.NullBrush)
+        dc.SetPen(wx.NullPen)
+
+    def InObject(self, pos):
+        dc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(dc)
+        font = wx.Font(self.size, wx.DEFAULT, self.weight, self.style)
+        w, h = dc.GetTextExtent(self.text)
+        rgn = wx.RegionFromPoints([(self.start.x-(w/2), self.start.y-(h/2)), (self.start.x-(w/2), self.start.y+(h/2)), (self.start.x+(w/2), self.start.y-(h/2)), (self.start.x+(w/2), self.start.y+(h/2))])
+
+        if rgn.Contains(pos.x, pos.y):
+            return True
+
+        return False
+
+    def GetName(self):
+        return self.text + ' Color:' + self.color
+
+    def ShowProperties(self, event):
+        dlg = wx.Dialog(self.canvas, wx.ID_ANY, "Circle Properties")
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        text = wx.TextCtrl(dlg, wx.ID_ANY)
+        text.SetValue(self.text)
+
+        colorbtn = wx.Button(dlg, wx.ID_ANY, "Color")
+        colorbtn.SetForegroundColour(self.color)
+
+        size = wx.SpinCtrl(dlg, wx.ID_ANY, value=str(self.size), min=7, initial=12, name="Font Size: ")
+
+        weight = wx.Choice(dlg, wx.ID_ANY, choices=["Normal", "Bold"])
+        if self.weight == wx.NORMAL:
+            weight.SetSelection(0)
+        else:
+            weight.SetSelection(1)
+
+        style = wx.Choice(dlg, wx.ID_ANY, choices=["Normal", "Italic"])
+        if self.weight == wx.NORMAL:
+            style.SetSelection(0)
+        else:
+            style.SetSelection(1)
+
+        def ColorBtn(event):
+            newcolor = self.RGBHex.do_hex_color_dlg(self.canvas)
+            if newcolor == None:
+                return
+
+            colorbtn.SetForegroundColour(newcolor)
+            dlg.Unbind(wx.EVT_BUTTON)
+
+        dlg.Bind(wx.EVT_BUTTON, ColorBtn, colorbtn)
+
+        sizer.Add(wx.StaticText(dlg, wx.ID_ANY, "Text:"), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2)
+        sizer.Add(text, 0, wx.EXPAND|wx.ALL, 3)
+        sizer.Add(size, 0, wx.ALL, 2)
+        sizer.Add(weight, 0, wx.ALL, 3)
+        sizer.Add(style, 0, wx.ALL, 2)
+        sizer.Add(colorbtn, 0, wx.ALL, 3)
+        sizer.Add(wx.Button(dlg, wx.ID_OK), 0, wx.ALL, 2)
+
+        dlg.SetSizer(sizer)
+        dlg.SetAutoLayout(True)
+        dlg.Fit()
+        dlg.Show()
+
+        if dlg.ShowModal() == wx.ID_OK:
+            self.text = text.GetValue()
+            r,g,b = colorbtn.GetForegroundColour().Get()
+            self.color = self.RGBHex.hexstring(r, g, b)
+            self.hcolor = self.RGBHex.hexstring(r^255, g^255, b^255)
+            self.size = int(size.GetValue())
+            if weight.GetSelection() == 0:
+                self.weight = wx.NORMAL
+            else:
+                self.weight = wx.BOLD
+
+            if style.GetSelection() == 0:
+                self.style = wx.NORMAL
+            else:
+                self.style = wx.ITALIC
+
+            if event != None:
+                self.Update(send=True, action="update")
+
+
+    def OnLeftDown(self, pos):
+        self.ShowProperties(None)
+        self.color = self.canvas.whiteboardColor
+        if self.text != '':
+            self.canvas.zOrder['front'].append(MapText(self.canvas, pos, self.text, self.size, self.weight, self.style, self.color))
+            self.Update(send=True, action='new')
+
+        self.text = ''
+        self.weight = wx.NORMAL
+        self.size = 12
+        self.style = wx.NORMAL
+        self.color = self.canvas.whiteboardColor
+        self.hcolor = self.canvas.whiteboardColor
+
+    def _toxml(self, action="update"):
+        return ''
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,7 @@
+__all__ = [ 'background_handler',  'fog_msg', 'map_msg',
+            'background_msg', 'map_prop_dialog', 'background', 'map',
+            'base_handler', 'map_version', 'base_msg', 'min_dialogs',
+            'base', 'miniatures_handler', 'grid_handler',
+            'miniatures_msg', 'grid_msg', 'miniatures', 'grid',
+            'whiteboard_handler', 'images', 'whiteboard_msg',
+            'whiteboard', 'map_handler' ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/background.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,327 @@
+# 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: background.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: background.py,v 1.29 2007/03/09 14:11:55 digitalxero Exp $
+#
+# 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 base import *
+import thread
+import urllib
+import os.path
+import time
+
+##-----------------------------
+## background layer
+##-----------------------------
+
+BG_NONE = 0
+BG_TEXTURE = 1
+BG_IMAGE = 2
+BG_COLOR = 3
+
+class layer_back_ground(layer_base):
+    def __init__(self, canvas):
+        self.canvas = canvas
+        self.log = self.canvas.log
+        self.log.log("Enter layer_back_ground", ORPG_DEBUG)
+        self.settings = self.canvas.settings
+        layer_base.__init__(self)
+        self.canvas = canvas
+        self.r_h = RGBHex()
+        self.clear()
+        self.log.log("Exit layer_back_ground", ORPG_DEBUG)
+
+    def error_loading_image(self, image):
+        self.log.log("Enter layer_back_ground->error_loading_image(self, image)", ORPG_DEBUG)
+        msg = "Unable to load image:" + `image`
+        dlg = wx.MessageDialog(self.canvas,msg,'File not Found',wx.ICON_EXCLAMATION)
+        dlg.ShowModal()
+        dlg.Destroy()
+        self.log.log("Exit layer_back_ground->error_loading_image(self, image)", ORPG_DEBUG)
+
+    def clear(self):
+        self.log.log("Enter layer_back_ground->clear(self)", ORPG_DEBUG)
+        self.type = BG_NONE
+        self.bg_bmp = None
+        self.bg_color = None
+        self.img_path = None
+        self.local = False
+        self.localPath = ''
+        self.localTime = -1
+        self.isUpdated = True
+        self.log.log("Exit layer_back_ground->clear(self)", ORPG_DEBUG)
+
+    def get_type(self):
+        self.log.log("Enter layer_back_ground->get_type(self)", ORPG_DEBUG)
+        self.log.log("Exit layer_back_ground->get_type(self)", ORPG_DEBUG)
+        return self.type
+
+    def get_img_path(self):
+        self.log.log("Enter layer_back_ground->get_type(self)", ORPG_DEBUG)
+        if self.img_path:
+            self.log.log("Exit layer_back_ground->get_type(self) return " + self.img_path, ORPG_DEBUG)
+            return self.img_path
+        else:
+            self.log.log("Exit layer_back_ground->get_type(self) return None", ORPG_DEBUG)
+            return ""
+
+    def get_color(self):
+        self.log.log("Enter layer_back_ground->get_color(self)", ORPG_DEBUG)
+        hexcolor = "#FFFFFF"
+        if self.bg_color:
+            (red,green,blue) = self.bg_color.Get()
+            hexcolor = self.r_h.hexstring(red, green, blue)
+        self.log.log("Exit layer_back_ground->get_color(self)", ORPG_DEBUG)
+        return hexcolor
+
+    def set_texture(self, path):
+        self.log.log("Enter layer_back_ground->set_texture(self, path)", ORPG_DEBUG)
+        self.isUpdated = True
+
+        self.type = BG_TEXTURE
+        if self.img_path != path:
+            try:
+                self.bg_bmp = ImageHandler.load(path, "texture", 0)
+                if self.bg_bmp == None:
+                    self.log.log("Invalid image type!", ORPG_GENERAL)
+                    raise Exception, "Invalid image type!"
+            except:
+                self.error_loading_image(path)
+        self.img_path = path
+        self.log.log("Enter layer_back_ground->set_texture(self, path)", ORPG_DEBUG)
+
+    def set_image(self, path, scale):
+        self.log.log("Enter layer_back_ground->set_image(self, path, scale)", ORPG_DEBUG)
+        self.isUpdated = True
+
+        self.type = BG_IMAGE
+        if self.img_path != path:
+            self.bg_bmp = ImageHandler.load(path, "background", 0)
+            try:
+                if self.bg_bmp == None:
+                    self.log.log("Invalid image type!", ORPG_GENERAL)
+                    raise Exception, "Invalid image type!"
+            except:
+                self.error_loading_image(path)
+        self.img_path = path
+        self.log.log("Exit layer_back_ground->set_image(self, path, scale)", ORPG_DEBUG)
+        return (self.bg_bmp.GetWidth(),self.bg_bmp.GetHeight())
+
+    def set_color(self, color):
+        self.log.log("Enter layer_back_ground->set_color(self, color)", ORPG_DEBUG)
+        self.isUpdated = True
+        self.type = BG_COLOR
+        (r,g,b) = color.Get()
+        self.bg_color = cmpColour(r,g,b)
+        self.canvas.SetBackgroundColour(self.bg_color)
+        self.log.log("Exit layer_back_ground->set_color(self, color)", ORPG_DEBUG)
+
+    def layerDraw(self, dc, scale, topleft, size):
+        self.log.log("Enter layer_back_ground->layerDraw(self, dc, scale, topleft, size)", ORPG_DEBUG)
+        if self.bg_bmp == None or not self.bg_bmp.Ok() or ((self.type != BG_TEXTURE) and (self.type != BG_IMAGE)):
+            self.log.log("Exit layer_back_ground->layerDraw(self, dc, scale, topleft, size) return False", ORPG_DEBUG)
+            return False
+        dc2 = wx.MemoryDC()
+        dc2.SelectObject(self.bg_bmp)
+        topLeft = [int(topleft[0]/scale), int(topleft[1]/scale)]
+        topRight = [int((topleft[0]+size[0]+1)/scale)+1, int((topleft[1]+size[1]+1)/scale)+1]
+        if (topRight[0] > self.canvas.size[0]):
+            topRight[0] = self.canvas.size[0]
+        if (topRight[1] > self.canvas.size[1]):
+            topRight[1] = self.canvas.size[1]
+        bmpW = self.bg_bmp.GetWidth()
+        bmpH = self.bg_bmp.GetHeight()
+        if self.type == BG_TEXTURE:
+            x = (topLeft[0]/bmpW)*bmpW
+            y1 = int(topLeft[1]/bmpH)*bmpH
+            if x < topLeft[0]:
+                posx = topLeft[0]
+                cl = topLeft[0]-x
+            else:
+                cl = 0
+                posx = x
+            while x < topRight[0]:
+                if x+bmpW > topRight[0]:
+                    cr = x+bmpW-topRight[0]
+                else:
+                    cr = 0
+                y = int(topLeft[1]/bmpH)*bmpH
+                if y < topLeft[1]:
+                    posy = topLeft[1]
+                    ct = topLeft[1]-y
+                else:
+                    ct = 0
+                    posy = y
+                while y < topRight[1]:
+                    if y+bmpH > topRight[1]:
+                        cb = y+bmpH-topRight[1]
+                    else:
+                        cb = 0
+                    newW = bmpW-cr-cl
+                    newH = bmpH-cb-ct
+                    if newW < 0:
+                        newW = 0
+                    if newH < 0:
+                        newH = 0
+                    dc.DrawBitmap(self.bg_bmp, posx, posy)
+                    dc.Blit(posx, posy, newW, newH, dc2, cl, ct)
+                    ct = 0
+                    y = y+bmpH
+                    posy = y
+                cl = 0
+                x = x+bmpW
+                posx = x
+        elif self.type == BG_IMAGE:
+            x = 0
+            y = 0
+            if x < topLeft[0]:
+                posx = topLeft[0]
+                cl = topLeft[0]-x
+            else:
+                cl = 0
+                posx = x
+
+            if y < topLeft[1]:
+                posy = topLeft[1]
+                ct = topLeft[1]-y
+            else:
+                ct = 0
+                posy = y
+            if x+bmpW > topRight[0]:
+                cr = x+bmpW-topRight[0]
+            else:
+                cr = 0
+            if y+bmpH > topRight[1]:
+                cb = y+bmpH-topRight[1]
+            else:
+                cb = 0
+            newW = bmpW-cr-cl
+            newH = bmpH-cb-ct
+            if newW < 0:
+                newW = 0
+            if newH < 0:
+                newH = 0
+            dc.DrawBitmap(self.bg_bmp, posx, posy)
+            dc.Blit(posx, posy, newW, newH, dc2, cl, ct)
+        dc2.SelectObject(wx.NullBitmap)
+        del dc2
+        self.log.log("Exit layer_back_ground->layerDraw(self, dc, scale, topleft, size)", ORPG_DEBUG)
+        return True
+
+    def layerToXML(self, action="update"):
+        self.log.log("Enter layer_back_ground->layerToXML(self, " + action + ")", ORPG_DEBUG)
+        xml_str = "<bg"
+        if self.bg_color != None:
+            (red,green,blue) = self.bg_color.Get()
+            hexcolor = self.r_h.hexstring(red, green, blue)
+            xml_str += ' color="' + hexcolor + '"'
+        if self.img_path != None:
+            xml_str += ' path="' + urllib.quote(self.img_path).replace('%3A', ':') + '"'
+        if self.type != None:
+            xml_str += ' type="' + str(self.type) + '"'
+        if self.local and self.img_path != None:
+            xml_str += ' local="True"'
+            xml_str += ' localPath="' + urllib.quote(self.localPath).replace('%3A', ':') + '"'
+            xml_str += ' localTime="' + str(self.localTime) + '"'
+        xml_str += "/>"
+        self.log.log(xml_str, ORPG_DEBUG)
+        self.log.log("Exit layer_back_ground->layerToXML(self, " + action + ")", ORPG_DEBUG)
+        if (action == "update" and self.isUpdated) or action == "new":
+            self.isUpdated = False
+            return xml_str
+        else:
+            return ''
+
+    def layerTakeDOM(self, xml_dom):
+        self.log.log("Enter layer_back_ground->layerTakeDOM(self, xml_dom)", ORPG_DEBUG)
+        type = BG_COLOR
+        color = xml_dom.getAttribute("color")
+        self.log.log("color=" + color, ORPG_DEBUG)
+        path = urllib.unquote(xml_dom.getAttribute("path"))
+        self.log.log("path=" + path, ORPG_DEBUG)
+
+        # Begin ted's map changes
+        if xml_dom.hasAttribute("color"):
+            r,g,b = self.r_h.rgb_tuple(xml_dom.getAttribute("color"))
+            self.set_color(cmpColour(r,g,b))
+        # End ted's map changes
+        if xml_dom.hasAttribute("type"):
+            type = int(xml_dom.getAttribute("type"))
+            self.log.log("type=" + str(type), ORPG_DEBUG)
+
+        if type == BG_TEXTURE:
+            if path != "":
+                self.set_texture(path)
+
+        elif type == BG_IMAGE:
+            if path != "":
+                self.set_image(path, 1)
+
+        elif type == BG_NONE:
+            self.clear()
+
+        if xml_dom.hasAttribute('local') and xml_dom.getAttribute('local') == 'True' and os.path.exists(urllib.unquote(xml_dom.getAttribute('localPath'))):
+            self.localPath = urllib.unquote(xml_dom.getAttribute('localPath'))
+            self.local = True
+            self.localTime = int(xml_dom.getAttribute('localTime'))
+            if self.localTime-time.time() <= 144000:
+                file = open(self.localPath, "rb")
+                imgdata = file.read()
+                file.close()
+                filename = os.path.split(self.localPath)
+                (imgtype,j) = mimetypes.guess_type(filename[1])
+                postdata = urllib.urlencode({'filename':filename[1], 'imgdata':imgdata, 'imgtype':imgtype})
+                thread.start_new_thread(self.upload, (postdata, self.localPath, type))
+        self.log.log("Exit layer_back_ground->layerTakeDOM(self, xml_dom)", ORPG_DEBUG)
+
+    def upload(self, postdata, filename, type):
+        self.lock.acquire()
+        if type == 'Image' or type == 'Texture':
+            url = self.settings.get_setting('ImageServerBaseURL')
+            file = urllib.urlopen(url, postdata)
+            recvdata = file.read()
+            file.close()
+            try:
+                xml_dom = minidom.parseString(recvdata)._get_documentElement()
+                if xml_dom.nodeName == 'path':
+                    path = xml_dom.getAttribute('url')
+                    path = urllib.unquote(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:
+                    print xml_dom.getAttribute('msg')
+            except Exception, e:
+                print e
+                print recvdata
+        self.lock.release()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/background_handler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,150 @@
+# 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: mapper/background_handler.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: background_handler.py,v 1.23 2007/03/09 14:17:15 digitalxero Exp $
+#
+# Description: Background layer handler
+#
+__version__ = "$Id: background_handler.py,v 1.23 2007/03/09 14:17:15 digitalxero Exp $"
+
+import thread
+from threading import Lock
+from background import *
+from base_handler import *
+import mimetypes
+
+from base import *
+
+class background_handler(base_layer_handler):
+    def __init__(self, parent, id, canvas):
+        base_layer_handler.__init__(self, parent, id, canvas)
+        self.settings = self.canvas.settings
+
+    def build_ctrls(self):
+        base_layer_handler.build_ctrls(self)
+        self.lock = Lock()
+        self.bg_type = wx.Choice(self, wx.ID_ANY, choices = ["Texture", "Image", "Color" ])
+        self.bg_type.SetSelection(2)
+        self.localBrowse = wx.Button(self, wx.ID_ANY, 'Browse')
+        self.localBrowse.Hide()
+        self.url_path = wx.TextCtrl(self, wx.ID_ANY,"http://")
+        self.color_button = wx.Button(self, wx.ID_ANY, "Color", style=wx.BU_EXACTFIT)
+        self.color_button.SetBackgroundColour(wx.BLACK)
+        self.color_button.SetForegroundColour(wx.WHITE)
+        self.apply_button = wx.Button(self, wx.ID_ANY, "Apply", style=wx.BU_EXACTFIT)
+        self.sizer.Add(self.bg_type, 0, wx.EXPAND)
+        self.sizer.Add(self.url_path, 1, wx.EXPAND)
+        self.sizer.Add(self.color_button, 0, wx.EXPAND)
+        self.sizer.Add(self.localBrowse, 0, wx.EXPAND)
+        self.sizer.Add(self.apply_button, 0, wx.EXPAND)
+        self.Bind(wx.EVT_BUTTON, self.on_bg_color, self.color_button)
+        self.Bind(wx.EVT_BUTTON, self.on_apply, self.apply_button)
+        self.Bind(wx.EVT_BUTTON, self.on_browse, self.localBrowse)
+        self.Bind(wx.EVT_CHOICE, self.on_bg_type, self.bg_type)
+        self.update_info()
+
+    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)
+            if not dlg.ShowModal() == wx.ID_OK:
+                dlg.Destroy()
+                return
+            file = open(dlg.GetPath(), "rb")
+            imgdata = file.read()
+            file.close()
+            filename = dlg.GetFilename()
+            (imgtype,j) = mimetypes.guess_type(filename)
+            postdata = urllib.urlencode({'filename':filename, 'imgdata':imgdata, 'imgtype':imgtype})
+
+            if self.settings.get_setting('LocalorRemote') == 'Remote':
+                thread.start_new_thread(self.canvas.layers['bg'].upload, (postdata, dlg.GetPath(), self.bg_type.GetStringSelection()))
+            else:
+                url = self.settings.get_setting('LocalImageBaseURL')
+                print dlg.GetDirectory()
+                print orpg.dirpath.dir_struct["user"]
+                if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Textures' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Textures': url = self.settings.get_setting('LocalImageBaseURL') + 'Textures/'
+                if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Maps' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Maps': url = self.settings.get_setting('LocalImageBaseURL') + 'Maps/'
+                if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Miniatures' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Miniatures': url = self.settings.get_setting('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)
+                self.update_info()
+                self.canvas.send_map_data()
+                self.canvas.Refresh(False)
+
+    def update_info(self):
+        bg_type = self.canvas.layers['bg'].get_type()
+        session=self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM):
+            self.url_path.Hide()
+        else:
+            self.url_path.Show()
+            self.url_path.Enable(BG_COLOR!=bg_type)
+        self.color_button.SetBackgroundColour(self.canvas.layers['bg'].get_color())
+        self.url_path.SetValue(self.canvas.layers['bg'].get_img_path())
+
+    def build_menu(self,label = "Background"):
+        base_layer_handler.build_menu(self,label)
+
+    def on_bg_color(self,evt):
+        data = wx.ColourData()
+        data.SetChooseFull(True)
+        dlg = wx.ColourDialog(self.canvas, data)
+        if dlg.ShowModal() == wx.ID_OK:
+            data = dlg.GetColourData()
+            data = data.GetColour()
+            r = data.Red()
+            g = data.Green()
+            b = data.Blue()
+            fgcolor = wx.Colour(r^255, g^255, b^255)
+            bgcolor = wx.Colour(r, g, b)
+            self.color_button.SetBackgroundColour(bgcolor)
+            self.color_button.SetForegroundColour(fgcolor)
+        dlg.Destroy()
+
+    def on_bg_type(self, evt):
+        if self.bg_type.GetStringSelection() == 'Texture' or self.bg_type.GetStringSelection() == 'Image':
+            self.localBrowse.Show()
+            self.url_path.Enable()
+        else:
+            self.localBrowse.Hide()
+            self.url_path.Disable()
+        self.Layout()
+
+    def on_apply(self, evt):
+        session=self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM) and (session.use_roles()):
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            return
+        self.canvas.layers['bg'].set_color(self.color_button.GetBackgroundColour())
+
+        if self.bg_type.GetStringSelection() == '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)
+
+        self.update_info()
+        self.canvas.send_map_data()
+        self.canvas.Refresh(False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/background_msg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,36 @@
+# 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: background_msg.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: background_msg.py,v 1.8 2006/11/04 21:24:21 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: background_msg.py,v 1.8 2006/11/04 21:24:21 digitalxero Exp $"
+
+from base_msg import *
+
+class bg_msg(map_element_msg_base):
+
+    def __init__(self,reentrant_lock_object = None):
+        self.tagname = "bg"
+        map_element_msg_base.__init__(self,reentrant_lock_object)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/base.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,150 @@
+# 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: mapper/base.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: base.py,v 1.18 2007/02/12 02:29:08 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: base.py,v 1.18 2007/02/12 02:29:08 digitalxero Exp $"
+
+from images import ImageHandler
+from orpg.tools.rgbhex import *
+from orpg.orpg_windows import *
+from orpg.orpg_xml import *
+from math import *
+from threading import Lock
+import time
+
+class cmpPoint(wx.Point):
+    def __init__(self,*_args,**_kwargs):
+        wx.Point.__init__(self,*_args,**_kwargs)
+
+    def __cmp__(self,other):
+        try:
+            if self.x < other.x:
+                return -1
+            elif self.x > other.x:
+                return 1
+            elif self.y < other.y:
+                return -1
+            elif self.y > other.y:
+                return 1
+            else:
+                return 0
+        except:
+            return -2
+
+class cmpColour(wx.Colour):
+    def __init__(self,*_args,**_kwargs):
+        wx.Colour.__init__(self,*_args,**_kwargs)
+
+    def __cmp__(self,other):
+        try:
+            (r,g,b) = self.Get()
+            my_value = b*256*256 + g*256 + r
+            (r,g,b) = other.Get()
+            other_value = b*256*256 + g*256 + r
+            if my_value < other_value:
+                return -1
+            elif my_value > other_value:
+                return 1
+            else:  # they're equal
+                return 0
+        except:
+            return -2
+
+class protectable_attributes:
+
+    def __init__(self):
+        self._set("_protected_attr",[])
+
+    def _set(self,name,value):
+        self.__dict__[name] = value
+
+    def __setattr__(self,name,value):
+        if name in self._protected_attr:
+            full_name = "_protect_" + name
+            if hasattr(self,full_name):
+                (p,c) = getattr(self,full_name)
+                if p != value:
+                    self._set(full_name,(value,1))
+            else:
+                self._set(full_name,(value,1))
+        else:
+            self._set(name,value)
+
+    def __getattr__(self,name):
+        if name in self._protected_attr:
+            (p,c) = self.__dict__["_protect_" + name]
+            return p
+        else:
+            raise AttributeError
+
+    def _clean_attr(self,name):
+        if name in self._protected_attr:
+            (p,c) = self.__dict__["_protect_" + name]
+            self._set("_protect_" + name,(p,0))
+
+    def _dirty_attr(self,name):
+        if name in self._protected_attr:
+            (p,c) = self.__dict__["_protect_" + name]
+            self._set("_protect_" + name,(p,1))
+
+    def _changed_attr(self):
+        changed = {}
+        for name in self._protected_attr:
+            (p,c) = self.__dict__["_protect_" + name]
+            if c:
+                changed[name] = p
+        return changed
+
+    def _clean_all_attr(self):
+        for name in self._protected_attr:
+            (p,c) = self.__dict__["_protect_" + name]
+            self._set("_protect_" + name,(p,0))
+
+    def _dirty_all_attr(self):
+        for name in self._protected_attr:
+            (p,c) = self.__dict__["_protect_" + name]
+            self._set("_protect_" + name,(p,1))
+
+##-----------------------------
+## base class for layer objects
+##-----------------------------
+
+class layer_base:
+
+    def __init__(self):
+        self.lock = Lock()
+
+    def layerDraw(self,dc,scale='',topleft='',size=''):
+        pass
+
+    def layerToXML(self, action):
+        pass
+
+    def layerTakeDOM(self, xml_dom):
+        pass
+
+    def hit_test(self,pos):
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/base_handler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,142 @@
+# 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: mapper/whiteboard_hander.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: base_handler.py,v 1.20 2007/11/04 17:32:25 digitalxero Exp $
+#
+# Description: base layer handler.
+#   layer handlers are responsible for the GUI elements of the layer
+#
+__version__ = "$Id: base_handler.py,v 1.20 2007/11/04 17:32:25 digitalxero Exp $"
+
+
+from orpg.orpg_windows import *
+from orpg.orpgCore import open_rpg
+
+class base_layer_handler(wx.Panel):
+
+    def __init__(self, parent, id, canvas):
+        wx.Panel.__init__(self, parent, id)
+        self.canvas = canvas
+        self.map_frame = self.canvas.frame
+        self.top_frame = self.canvas.frame.top_frame
+        self.chat = open_rpg.get_component("chat")
+        self.build_ctrls()
+        self.build_menu()
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_LEFT_DCLICK, self.on_left_dclick)
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.basesizer.SetDimension(0,0,s[0],s[1])
+
+    def build_ctrls(self):
+        self.basesizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.buttonsizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        self.zoom_in_button = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'zoom_in.gif', "Zoom in from x1.0", wx.ID_ANY )
+        self.zoom_out_button = createMaskedButton( self, orpg.dirpath.dir_struct["icon"]+'zoom_out.gif', "Zoom out from x1.0", wx.ID_ANY )
+        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.SetSizer(self.basesizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        self.basesizer.Add( self.sizer, 1, wx.EXPAND)
+        self.basesizer.Add( self.buttonsizer, 0, wx.ALIGN_RIGHT)
+        self.Bind(wx.EVT_BUTTON, self.canvas.on_zoom_in, self.zoom_in_button)
+        self.Bind(wx.EVT_BUTTON, self.canvas.on_zoom_out, self.zoom_out_button)
+        self.Bind(wx.EVT_BUTTON, self.map_frame.on_open, mapopen)
+        self.Bind(wx.EVT_BUTTON, self.map_frame.on_save, mapsave)
+        self.Bind(wx.EVT_BUTTON, self.canvas.on_prop, props)
+
+    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
+        main_menu = wx.Menu(label)                                                       #  create a menu resource
+        main_menu.SetTitle(label)
+        item = wx.MenuItem(main_menu, wx.ID_ANY, "&Load Map", "Load Map")
+        self.canvas.Bind(wx.EVT_MENU, self.map_frame.on_open, item)
+        main_menu.AppendItem(item)
+        item = wx.MenuItem(main_menu, wx.ID_ANY, "&Save Map", "Save Map")
+        self.canvas.Bind(wx.EVT_MENU, self.map_frame.on_save, item)
+        main_menu.AppendItem(item)
+        item = wx.MenuItem(main_menu, wx.ID_ANY, "Save as JPG", "Save as JPG")
+        self.canvas.Bind(wx.EVT_MENU, self.on_save_map_jpg, item)
+        main_menu.AppendItem(item)
+        main_menu.AppendSeparator()
+        item = wx.MenuItem(main_menu, wx.ID_ANY, "&Properties", "Properties")
+        self.canvas.Bind(wx.EVT_MENU, self.canvas.on_prop, item)
+        main_menu.AppendItem(item)
+        self.main_menu = main_menu
+
+    def on_save_map_jpg(self, evt):
+        directory = orpg.dirpath.dir_struct["user"]
+        if directory == None:
+            directory = ""
+        d = wx.FileDialog(self.GetParent(), "Save map as a jpg", directory, "", "*.jpg", wx.SAVE)
+        if d.ShowModal() != wx.ID_OK:
+            return
+        filename = d.GetPath()
+        width = self.canvas.size[0]
+        height = self.canvas.size[1]
+        dc = wx.MemoryDC()
+        bitmap = wx.EmptyBitmap(width+1, height+1)
+        dc.SelectObject(bitmap)
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetBrush(wx.Brush(self.canvas.GetBackgroundColour(), wx.SOLID))
+        dc.DrawRectangle(0,0,width+1, height+1)
+        self.canvas.layers['bg'].layerDraw(dc, 1, [0,0], self.canvas.size)
+        self.canvas.layers['grid'].layerDraw(dc, [0,0], self.canvas.size)
+        self.canvas.layers['miniatures'].layerDraw(dc, [0,0], self.canvas.size)
+        self.canvas.layers['whiteboard'].layerDraw(dc)
+        self.canvas.layers['fog'].layerDraw(dc, [0,0], self.canvas.size)
+        image = bitmap.ConvertToImage()
+        image.SaveFile(filename, wx.BITMAP_TYPE_JPEG)
+
+    def do_map_board_menu(self,pos):
+        self.canvas.PopupMenu(self.main_menu,pos)
+
+    def on_right_down(self,evt):
+        self.do_map_board_menu(evt.GetPosition())
+
+    def on_left_down(self,evt):
+        pass
+
+    def on_left_up(self,evt):
+        pass
+
+    #added to base layer by Snowdog 5/03
+    def on_left_dclick(self,evt):
+        pass
+
+    def on_motion(self,evt):
+        pass
+
+    def update_info(self):
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/base_msg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,259 @@
+# 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: mapper/base_msg.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: base_msg.py,v 1.9 2007/03/09 14:11:55 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: base_msg.py,v 1.9 2007/03/09 14:11:55 digitalxero Exp $"
+
+from threading import RLock
+from orpg.networking.mplay_client import *
+
+class map_element_msg_base:
+#  This is a base class
+
+    def __init__(self,reentrant_lock_object = None):
+
+        if not hasattr(self,"tagname"):
+            raise Exception, "This is a virtual class that cannot be directly instantiated.  Set self.tagname in derived class."
+
+        self._props = {}
+        #  This is a dictionary that holds (value,changed) 2-tuples, indexed by attribute
+        #  Avoid manipulating these values directly.  Instead, use the provided accessor methods.
+        #  If there is not one that suits your needs, please add one to this class and
+        #  use it instead.
+
+        self.children = {}
+        # This next clause will set self.p_lock to a passed-in RLock object, if present.
+        #    Otherwise, it will create it's very own for this instance.
+        #    Using a passed in object is useful to protect an entire <map/> element from
+        #    being changed by another thread when any part of it is being change by another
+        #    thread.  Just pass the map_msg's p_lock object in to it's descendents.
+        if reentrant_lock_object:
+            self.p_lock = reentrant_lock_object
+        else:
+            self.p_lock = RLock()
+
+    def clear(self):
+        self.p_lock.acquire()
+
+        for child in self.children.keys():
+            self.children[child].clear()
+            self.children[child] = None
+            del self.children[child]
+
+        for p in self._props.keys():
+            self._props[p] = None
+
+        self.p_lock.release()
+
+
+    #########################################
+    #  Accessor functions begin
+
+    #  Access single property
+
+    def init_prop(self,prop,value):  # set's the changed flag to False and assigns
+        self.p_lock.acquire()
+        self._props[prop] = (value, 0)
+        self.p_lock.release()
+
+    def set_prop(self,prop,value):  # set's the changed flag to True and assigns
+        self.p_lock.acquire()
+        self._props[prop] = (value, 1)
+        self.p_lock.release()
+
+    def get_prop(self,prop):  # returns None if prop not found
+        self.p_lock.acquire()
+        if prop in self._props.keys():
+            (p,c) = self._props[prop]
+            self.p_lock.release()
+            return p
+        else:
+            self.p_lock.release()
+            return None
+
+    def is_prop_changed(self,prop):  # returns None if prop not found
+        self.p_lock_acquire()
+        if prop in self._props.keys():
+            (p,c) = self._props[prop]
+            self.p_lock.release()
+            return c
+        else:
+            self.p_lock.release()
+            return None
+
+    def get_child(self,key):         # returns None if key not found in children list
+        self.p_lock_acquire()
+        if self.children.has_key(key):
+            self.p_lock.release()
+            return self.children[key]
+        else:
+            self.p_lock.release()
+            return None
+
+    #  Access multiple properties
+
+    def init_props(self,props):               # same as init_prop(), but takes dictionary of props
+        self.p_lock.acquire()
+        for k in props.keys():
+            self._props[k] = (props[k],0)
+        self.p_lock.release()
+
+    def set_props(self,props):                # same as set_prop(), but takes dictionary of props
+        self.p_lock.acquire()
+        for k in props.keys():
+            self._props[k] = (props[k],1)
+        self.p_lock.release()
+
+    def get_all_props(self):                  # returns dictionary of all properties, regardless of change
+        self.p_lock.acquire()
+
+        result = {}
+        for k in self._props.keys():
+            (p,c) = self._props[k]
+            result[k] = p
+
+        self.p_lock.release()
+        return result
+
+    def get_changed_props(self):              # returns dictionary of all properties that have been changed
+        self.p_lock.acquire()
+        result = {}
+        for k in self._props.keys():
+            (p,c) = self._props[k]
+            if c:
+                result[k] = p
+        self.p_lock.release()
+        return result
+
+    def get_children(self):                 # returns dictionary of children
+        return self.children
+
+
+    #  Accessor functions end
+    #########################################
+
+    #########################################
+    #  XML emitters begin
+    def get_all_xml(self,action="new",output_action = 0):    # outputs a tag with all attributes it contains
+        self.p_lock.acquire()
+        xml_str = "<" + self.tagname
+        if action and output_action:
+            xml_str += " action='" + action + "'"
+        for k in self._props.keys():
+            (p,c) = self._props[k]
+            if k != "action" or not action:
+                xml_str += " " + k + "='" + p + "'"
+        if self.children:
+            xml_str += ">"
+            for child in self.children.keys():
+                xml_str += self.children[child].get_all_xml(action)
+            xml_str += "</" + self.tagname + ">"
+        else:
+            xml_str += "/>"
+        self.p_lock.release()
+        return xml_str
+
+    def get_changed_xml(self,action="update",output_action = 0):    # outputs a tag with all changed attributes
+        self.p_lock.acquire()
+        xml_str = "<" + self.tagname
+        if action and output_action:
+            xml_str += " action='" + action + "'"
+
+        # if present, always send the id, even if it didn't change
+        if self._props.has_key("id"):
+            (p,c) = self._props["id"]
+            xml_str += " id='" + p + "'"
+        for k in self._props.keys():
+            (p,c) = self._props[k]
+            if (k != "id" or k != "action") and c == 1:  # don't duplicate the id attribute
+                xml_str += " " + k + "='" + p + "'"
+        if self.children:
+            xml_str += ">"
+            for child in self.children.keys():
+                xml_str += self.children[child].get_changed_xml(action)
+            xml_str += "</" + self.tagname + ">"
+        else:
+            xml_str += "/>"
+        self.p_lock.release()
+        return xml_str
+
+    # convenience method to use if only this tag is modified
+    #   outputs a <map/> element containing only the changes to this tag
+    def standalone_update_text(self,update_id_string):
+        buffer = "<map id='" + update_id_string + "'>"
+        buffer += self.get_changed_xml("update")
+        buffer += "<map/>"
+        return buffer
+
+    # XML emitters end
+    #########################################
+
+    #########################################
+    #  XML importers begin
+
+    def _from_dom(self,xml_dom,prop_func):
+        self.p_lock.acquire()
+        if xml_dom.tagName == self.tagname:
+            if xml_dom.getAttributeKeys():
+                for k in xml_dom.getAttributeKeys():
+                    prop_func(k,xml_dom.getAttribute(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()
+
+    def init_from_dom(self,xml_dom):
+    #  xml_dom must be pointing to an empty tag.  Override in a derived class for <map/> and other similar tags.
+        self._from_dom(xml_dom,self.init_prop)
+
+    def set_from_dom(self,xml_dom):
+    #  xml_dom must be pointing to an empty tag.  Override in a derived class for <map/> and other similar tags
+        self._from_dom(xml_dom,self.set_prop)
+
+    def init_from_xml(self,xml):
+        xml_dom = parseXml(xml)
+        node_list = xml_dom.getElementsByTagName(self.tagname)
+        if len(node_list) < 1:
+            print "Warning: no <" + self.tagname + "/> elements found in DOM."
+        else:
+            while len(node_list):
+                self.init_from_dom(node_list.pop())
+        if xml_dom:
+            xml_dom.unlink()
+
+    def set_from_xml(self,xml):
+        xml_dom = parseXml(xml)
+        node_list = xml_dom.getElementsByTagName(self.tagname)
+        if len(node_list) < 1:
+            print "Warning: no <" + self.tagname + "/> elements found in DOM."
+        else:
+            while len(node_list):
+                self.set_from_dom(node_list.pop())
+        if xml_dom:
+            xml_dom.unlink()
+
+    # XML importers end
+    #########################################
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/fog.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,372 @@
+# 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: mapper/fog.py
+# Author: OpenRPG Team
+#
+# Description: Maintenance of data structures required for FoW
+#
+
+import sys
+from base import *
+from random import Random
+from region import *
+from orpg.minidom import Element
+import traceback
+COURSE = 10
+
+class FogArea:
+    def __init__(self, outline, log):
+        self.log = log
+        self.log.log("Enter FogArea", ORPG_DEBUG)
+        self.outline = outline
+        self.log.log("Exit FogArea", ORPG_DEBUG)
+
+    def set_fog_props(self, str):
+        self.log.log("Enter FogArea->set_fog_props(self, str)", ORPG_DEBUG)
+        self.outline = str
+        self.log.log("Exit FogArea->set_fog_props(self, str)", ORPG_DEBUG)
+
+    def points_to_elements(self, points=None):
+        self.log.log("Enter FogArea->points_to_elements(self, points)", ORPG_DEBUG)
+        result = []
+        if points == None:
+            self.log.log("Exit FogArea->points_to_elements(self, points)", ORPG_DEBUG)
+            return result
+        for pairs in string.split( points, ';' ):
+            pair = string.split( pairs, ',' )
+            p = Element( "point" )
+            p.setAttribute( "x", pair[0] )
+            p.setAttribute( "y", pair[1] )
+            result.append( p )
+        self.log.log("Exit FogArea->points_to_elements(self, points)", ORPG_DEBUG)
+        return result
+
+    def toxml(self, action="update"):
+        self.log.log("Enter FogArea->toxml(self, " + action + ")", ORPG_DEBUG)
+        xml_str = ""
+        localOutline = self.outline
+        if localOutline != None and localOutline != "all" and localOutline != "none":
+            localOutline = "points"
+        elem = Element( "poly" )
+        if action == "del":
+            elem.setAttribute( "action", action )
+            elem.setAttribute( "outline", localOutline )
+            if localOutline == 'points':
+                list = self.points_to_elements( self.outline )
+                for p in list:
+                    elem.appendChild( p )
+            str = elem.toxml()
+            elem.unlink()
+            self.log.log(str, ORPG_DEBUG)
+            self.log.log("Exit FogArea->toxml(self, " + action + ")", ORPG_DEBUG)
+            return str
+        elem.setAttribute( "action", action )
+        if  localOutline != None:
+            elem.setAttribute( "outline", localOutline )
+            if localOutline == 'points':
+                list = self.points_to_elements( self.outline )
+                for p in list:
+                    elem.appendChild( p )
+        xml_str = elem.toxml()
+        elem.unlink()
+        self.log.log(xml_str, ORPG_DEBUG)
+        self.log.log("Exit FogArea->toxml(self, " + action + ")", ORPG_DEBUG)
+        return xml_str
+
+class fog_layer(layer_base):
+    def __init__(self, canvas):
+        self.canvas = canvas
+        self.log = self.canvas.log
+        self.log.log("Enter fog_layer", ORPG_DEBUG)
+        layer_base.__init__(self)
+        self.color = wx.Color(128,128,128)
+        if "__WXGTK__" not in wx.PlatformInfo:
+            self.color = wx.Color(128,128,128, 128)
+        self.fogregion = wx.Region()
+        self.fogregion.Clear()
+        self.fog_bmp = None
+        self.width = 0
+        self.height = 0
+        self.use_fog = False
+        self.last_role = ""
+        self.log.log("Exit fog_layer", ORPG_DEBUG)
+
+    def clear(self):
+        self.log.log("Enter fog_layer->clear(self)", ORPG_DEBUG)
+        self.fogregion.Clear()
+        self.use_fog = True
+        self.del_area("all")
+        self.recompute_fog()
+        self.log.log("Exit fog_layer->clear(self)", ORPG_DEBUG)
+
+    def remove_fog(self):
+        self.log.log("Enter fog_layer->remove_fog(self)", ORPG_DEBUG)
+        self.fogregion.Clear()
+        self.use_fog = False
+        self.del_area("all")
+        self.add_area("none")
+        self.fog_bmp = None
+        self.log.log("Exit fog_layer->remove_fog(self)", ORPG_DEBUG)
+
+    def resize(self, size):
+        self.log.log("Enter fog_layer->resize(self, size)", ORPG_DEBUG)
+        try:
+            if self.width == size[0] and self.height == size[1]:
+                self.log.log("Exit fog_layer->resize(self, size)", ORPG_DEBUG)
+                return
+            self.recompute_fog()
+        except:
+            pass
+        self.log.log("Exit fog_layer->resize(self, size)", ORPG_DEBUG)
+
+    def recompute_fog(self):
+        self.log.log("Enter fog_layer->recompute_fog(self)", ORPG_DEBUG)
+        if not self.use_fog:
+            self.log.log("Exit fog_layer->recompute_fog(self)", ORPG_DEBUG)
+            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.fill_fog()
+        self.log.log("Exit fog_layer->recompute_fog(self)", ORPG_DEBUG)
+
+    def fill_fog(self):
+        self.log.log("Enter fog_layer->fill_fog(self)", ORPG_DEBUG)
+        if not self.use_fog:
+            self.log.log("Exit fog_layer->fill_fog(self)", ORPG_DEBUG)
+            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
+        self.log.log("Exit fog_layer->fill_fog(self)", ORPG_DEBUG)
+
+    def layerDraw(self, dc, topleft, size):
+        self.log.log("Enter fog_layer->layerDraw(self, dc, topleft, size)", ORPG_DEBUG)
+        if self.fog_bmp == None or not self.fog_bmp.Ok() or not self.use_fog:
+            self.log.log("Exit fog_layer->layerDraw(self, dc, topleft, size)", ORPG_DEBUG)
+            return
+        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
+        self.log.log("Exit fog_layer->layerDraw(self, dc, topleft, size)", ORPG_DEBUG)
+
+    def createregn2(self, polyline, mode, show):
+        self.log.log("Enter fog_layer->createregn2(self, polyline, mode, show)", ORPG_DEBUG)
+        regn = self.scanConvert(polyline)
+        area = ""
+        for i in polyline:
+            if (area != ""):
+                area += ";"
+            area += str(i.X) + "," + str(i.Y)
+        if mode == 'new':
+            if self.fogregion.IsEmpty():
+                self.fogregion = regn
+            else:
+                self.fogregion.UnionRegion(regn)
+            self.add_area(area, show)
+        else:
+            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.SubtractRegion(regn)
+            self.del_area(area, show)
+        self.log.log("Exit fog_layer->createregn2(self, polyline, mode, show)", ORPG_DEBUG)
+
+    def createregn(self, polyline, mode, show="Yes"):
+        self.log.log("Enter fog_layer->createregn(self, polyline, mode, show)", ORPG_DEBUG)
+        if not self.use_fog and mode == 'del':
+            self.clear()
+            self.canvas.Refresh(False)
+        if self.use_fog:
+            self.createregn2(polyline, mode, show)
+            self.fill_fog()
+        self.log.log("Exit fog_layer->createregn(self, polyline, mode, show)", ORPG_DEBUG)
+
+    def scanConvert(self, polypt):
+        self.log.log("Enter fog_layer->scanConvert(self, polypt)", ORPG_DEBUG)
+        regn = wx.Region()
+        regn.Clear()
+        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)
+            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)
+        self.log.log("Exit fog_layer->scanConvert(self, polypt)", ORPG_DEBUG)
+        return regn
+
+    def add_area(self, area="", show="Yes"):
+        self.log.log("Enter fog_layer->add_area(self, area, show)", ORPG_DEBUG)
+        poly = FogArea(area, self.log)
+        xml_str = "<map><fog>"
+        xml_str += poly.toxml("new")
+        xml_str += "</fog></map>"
+        if show == "Yes":
+            self.canvas.frame.session.send(xml_str)
+        self.log.log(xml_str, ORPG_DEBUG)
+        self.log.log("Exit fog_layer->add_area(self, area, show)", ORPG_DEBUG)
+
+    def del_area(self, area="", show="Yes"):
+        self.log.log("Enter fog_layer->del_area(self, area, show)", ORPG_DEBUG)
+        poly = FogArea(area, self.log)
+        xml_str = "<map><fog>"
+        xml_str += poly.toxml("del")
+        xml_str += "</fog></map>"
+        if show == "Yes":
+            self.canvas.frame.session.send(xml_str)
+        self.log.log(xml_str, ORPG_DEBUG)
+        self.log.log("Exit fog_layer->del_area(self, area, show)", ORPG_DEBUG)
+
+    def layerToXML(self, action="update"):
+        self.log.log("Enter fog_layer->layerToXML(self, " + action + ")", ORPG_DEBUG)
+        if not self.use_fog:
+            self.log.log("Exit fog_layer->layerToXML(self, " + action + ") return None", ORPG_DEBUG)
+            return ""
+        fog_string = ""
+        ri = wx.RegionIterator(self.fogregion)
+        if not (ri.HaveRects()):
+            fog_string = FogArea("all", self.log).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
+            poly = FogArea(str(x1) + "," + str(y1) + ";" +
+                          str(x2) + "," + str(y1) + ";" +
+                          str(x2) + "," + str(y2) + ";" +
+                          str(x1) + "," + str(y2), self.log)
+            fog_string += poly.toxml(action)
+            ri.Next()
+        if fog_string:
+            s = "<fog"
+            s += ">"
+            s += fog_string
+            s += "</fog>"
+            self.log.log(s, ORPG_DEBUG)
+            self.log.log("Exit fog_layer->layerToXML(self, " + action + ")", ORPG_DEBUG)
+            return s
+        else:
+            self.log.log("Exit fog_layer->layerToXML(self, " + action + ") return None", ORPG_DEBUG)
+            return ""
+
+    def layerTakeDOM(self, xml_dom):
+        self.log.log("Enter fog_layer->layerTakeDOM(self, xml_dom)", ORPG_DEBUG)
+        try:
+            if not self.use_fog:
+                self.use_fog = True
+                self.recompute_fog()
+            if xml_dom.hasAttribute('serial'):
+                self.serial_number = int(xml_dom.getAttribute('serial'))
+            children = xml_dom._get_childNodes()
+            for l in children:
+                action = l.getAttribute("action")
+                outline = l.getAttribute("outline")
+                if (outline == "all"):
+                    polyline = [IPoint().make(0,0), IPoint().make(self.width-1, 0),
+                              IPoint().make(self.width-1, self.height-1),
+                              IPoint().make(0, self.height-1)]
+                elif (outline == "none"):
+                    polyline = []
+                    self.use_fog = 0
+                    self.fog_bmp = None
+                else:
+                    polyline = []
+                    lastx = None
+                    lasty = None
+                    list = l._get_childNodes()
+                    for point in list:
+                        x = point.getAttribute( "x" )
+                        y = point.getAttribute( "y" )
+                        if (x != lastx or y != lasty):
+                            polyline.append(IPoint().make(int(x), int(y)))
+                        lastx = x
+                        lasty = y
+                if (len(polyline) > 1):
+                    self.createregn2(polyline, action, "No")
+            self.fill_fog()
+        except:
+            self.log.log(traceback.format_exc(), ORPG_GENERAL)
+        self.log.log("Exit fog_layer->layerTakeDOM(self, xml_dom)", ORPG_DEBUG)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/fog_handler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,198 @@
+# 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: mapper/fog_handler.py
+# Author: Mark Tarrabain
+#
+# Description: Handler for fog layer
+#
+
+from fog import *
+from base_handler import *
+from region import *
+
+#CTRL_REVEAL = wx.NewId()
+#CTRL_HIDE = wx.NewId()
+#CTRL_REMOVE = wx.NewId()
+#CTRL_SHOWALL = wx.NewId()
+#CTRL_HIDEALL = wx.NewId()
+#CTRL_COLOR = wx.NewId()
+#CTRL_PEN = wx.NewId()
+
+class fog_handler(base_layer_handler):
+    def __init__(self, parent, id, canvas):
+        self.showmode = 1
+        self.drawing = False
+        self.pencolor=wx.WHITE
+        base_layer_handler.__init__(self, parent, id, canvas)
+
+    def build_ctrls(self):
+        foglayer = self.canvas.layers['fog']
+        base_layer_handler.build_ctrls(self)
+        self.f_type_radio = {}
+        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(wx.Size(20,25),1)
+
+
+    def build_menu(self,label = "fog"):
+        base_layer_handler.build_menu(self,label)
+        self.main_menu.AppendSeparator()
+
+        item = wx.MenuItem(self.main_menu, wx.ID_ANY, "&Hide All", "Hide All")
+        self.canvas.Bind(wx.EVT_MENU, self.on_hideall, item)
+        self.main_menu.AppendItem(item)
+
+        item = wx.MenuItem(self.main_menu, wx.ID_ANY, "&Fog Mask", "Fog Mask")
+        self.canvas.Bind(wx.EVT_MENU, self.on_color, item)
+        self.main_menu.AppendItem(item)
+
+        item = wx.MenuItem(self.main_menu, wx.ID_ANY, "&Remove Fog Layer", "Remove Fog Layer")
+        self.canvas.Bind(wx.EVT_MENU, self.on_remove, item)
+        self.main_menu.AppendItem(item)
+
+        item = wx.MenuItem(self.main_menu, wx.ID_ANY, "&Pen Color", "Pen Color")
+        self.canvas.Bind(wx.EVT_MENU, self.on_pen_color, item)
+        self.main_menu.AppendItem(item)
+
+
+
+
+    def on_remove(self,evt):
+        session=self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM):
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            return
+        self.canvas.layers['fog'].remove_fog()
+        self.canvas.Refresh(False)
+
+    def on_showall(self,evt):
+        session=self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM):
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            return
+        foglayer = self.canvas.layers['fog']
+        foglayer.showall()
+        self.canvas.Refresh(False)
+
+    def on_pen_color(self,evt):
+        data = wx.ColourData()
+        data.SetChooseFull(True)
+        data.SetColour(self.pencolor)
+        dlg = wx.ColourDialog(self.canvas, data)
+        if dlg.ShowModal() == wx.ID_OK:
+            data = dlg.GetColourData()
+            color = data.GetColour()
+            self.pencolor=color
+        dlg.Destroy()
+
+    def on_hideall(self,evt):
+        session=self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM):
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            return
+        foglayer=self.canvas.layers['fog']
+        foglayer.clear()
+        self.canvas.Refresh(False)
+
+    def on_color(self,evt):
+        session=self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM):
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            return
+        data = wx.ColourData()
+        data.SetChooseFull(True)
+        data.SetColour(self.canvas.layers['fog'].color)
+        dlg = wx.ColourDialog(self.canvas, data)
+        if dlg.ShowModal() == wx.ID_OK:
+            data = dlg.GetColourData()
+            color = data.GetColour()
+            if "__WXGTK__" not in wx.PlatformInfo:
+                color = wx.Color(color.Red(), color.Green(), color.Blue(), 128)
+            self.canvas.layers['fog'].color = color
+        dlg.Destroy()
+        self.canvas.layers['fog'].fill_fog()
+        self.canvas.Refresh(False)
+
+    def update_info(self):
+        foglayer = self.canvas.layers['fog']
+        pass
+
+
+    def on_motion(self, evt):
+        scale = self.canvas.layers['grid'].mapscale
+        dc = wx.ClientDC(self.canvas)
+        dc.SetUserScale(scale, scale)
+        self.canvas.PrepareDC(dc)
+        pos = evt.GetLogicalPosition(dc)
+        pos.x /= COURSE
+        pos.y /= COURSE
+        pos.x /= scale
+        pos.y /= scale
+
+        if evt.m_leftDown:
+            if not self.drawing:
+                self.line = []
+                self.line.append(IPoint().make(pos.x, pos.y))
+            elif pos.x != self.last.x or pos.y != self.last.y:
+                pen= wx.Pen(self.pencolor)
+                pen.SetWidth(COURSE/2+1)
+                dc.SetPen(pen)
+                multi = scale*COURSE
+                dc.DrawLine(self.last.x*multi, self.last.y*multi, pos.x*multi, pos.y*multi)
+                dc.SetPen(wx.NullPen)
+                self.line.append(IPoint().make(pos.x, pos.y))
+            self.last = pos
+            self.drawing = True
+        del dc
+
+    def on_left_up(self,evt):
+        if self.drawing == True:
+            session=self.canvas.frame.session
+            if (session.my_role() != session.ROLE_GM):
+                open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            else:
+                # This code sets the mode to either new or del depending on the action to function with the updated createregen code.
+                if (self.fogshow.GetValue() == 1):
+                    showmode = 'new'
+                else:
+                    showmode = 'del'
+                scale = self.canvas.layers['grid'].mapscale
+                dc = wx.ClientDC(self.canvas)
+                self.canvas.PrepareDC(dc)
+                dc.SetUserScale(scale,scale)
+                pen= wx.Pen(self.pencolor)
+                pen.SetWidth(COURSE/2+1)
+                dc.SetPen(pen)
+                dc.DrawLine(self.last.x*scale*COURSE,self.last.y*scale*COURSE,self.line[0].X*scale*COURSE,self.line[0].Y*scale*COURSE)
+                dc.SetPen(wx.NullPen)
+                wx.BeginBusyCursor()
+                # This prevents the divide by zero error by not even sending the line to be proccessed if it contains less then 3 points
+                if (len(self.line)>1):
+                    self.canvas.layers['fog'].createregn(self.line, showmode)
+                else:
+                    #print "Error Divide by zero, ignoring this section"
+                    pass
+                wx.EndBusyCursor()
+                del dc
+            self.canvas.Refresh(False)
+            self.drawing = False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/fog_msg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,136 @@
+# 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: mapper/fog_msg.py
+# Author: Mark Tarrabain
+# Maintainer:
+# Version:
+#   $Id: fog_msg.py,v 1.16 2006/11/04 21:24:21 digitalxero Exp $
+#
+__version__ = "$Id: fog_msg.py,v 1.16 2006/11/04 21:24:21 digitalxero Exp $"
+
+from base_msg import *
+from region import *
+from orpg.minidom import Element
+import string
+
+class fog_msg(map_element_msg_base):
+
+    def __init__(self,reentrant_lock_object = None):
+        self.tagname = "fog"
+        map_element_msg_base.__init__(self,reentrant_lock_object)
+        self.use_fog = 0
+        self.fogregion=IRegion()
+        self.fogregion.Clear()
+
+    def get_line(self,outline,action,output_act):
+        elem = Element( "poly" )
+        if ( output_act ):
+            elem.setAttribute( "action", action )
+        if ( outline == 'all' ) or ( outline == 'none' ):
+            elem.setAttribute( "outline", outline )
+        else:
+            elem.setAttribute( "outline", "points" )
+            for pair in string.split( outline, ";" ):
+                p = string.split( pair, "," )
+                point = Element( "point" )
+                point.setAttribute( "x", p[ 0 ] )
+                point.setAttribute( "y", p[ 1 ] )
+                elem.appendChild( point )
+        str = elem.toxml()
+        elem.unlink()
+        return str
+
+    # convenience method to use if only this line is modified
+    #   outputs a <map/> element containing only the changes to this line
+    def standalone_update_text(self,update_id_string):
+        buffer = "<map id='" + update_id_string + "'>"
+        buffer += "<fog>"
+        buffer += self.get_changed_xml()
+        buffer += "</fog></map>"
+        return buffer
+
+    def get_all_xml(self,action="new",output_action=1):
+        return self.toxml(action,output_action)
+
+    def get_changed_xml(self,action="update",output_action=1):
+        return self.toxml(action,output_action)
+
+    def toxml(self,action,output_action):
+        #print "fog_msg.toxml called"
+        #print "use_fog :",self.use_fog
+        #print "output_action :",output_action
+        #print "action :",action
+        if not (self.use_fog):
+            return ""
+        fog_string = ""
+        if self.fogregion.isEmpty():
+            fog_string=self.get_line("all","del",output_action)
+        for ri in self.fogregion.GetRectList():
+            x1=ri.GetX()
+            x2=x1+ri.GetW()-1
+            y1=ri.GetY()
+            y2=y1+ri.GetH()-1
+            fog_string += self.get_line(str(x1)+","+str(y1)+";"+
+                                         str(x2)+","+str(y1)+";"+
+                                         str(x2)+","+str(y2)+";"+
+                                         str(x1)+","+str(y2),action,output_action)
+        s = "<fog"
+        if fog_string:
+            s += ">"
+            s += fog_string
+            s += "</fog>"
+        else:
+            s+="/>"
+        return s
+
+    def interpret_dom(self,xml_dom):
+        self.use_fog=1
+        #print 'fog_msg.interpret_dom called'
+        children = xml_dom._get_childNodes()
+        #print "children",children
+        for l in children:
+            action = l.getAttribute("action")
+            outline = l.getAttribute("outline")
+            #print "action/outline",action, outline
+            if (outline=="all"):
+                polyline=[]
+                self.fogregion.Clear()
+            elif (outline=="none"):
+                polyline=[]
+                self.use_fog=0
+                self.fogregion.Clear()
+            else:
+                polyline=[]
+                list = l._get_childNodes()
+                for node in list:
+                    polyline.append( IPoint().make( int(node.getAttribute("x")), int(node.getAttribute("y")) ) )
+                # pointarray = outline.split(";")
+                # for m in range(len(pointarray)):
+                #     pt=pointarray[m].split(",")
+                #     polyline.append(IPoint().make(int(pt[0]),int(pt[1])))
+            #print "length of polyline", len(polyline)
+            if (len(polyline)>2):
+                if action=="del":
+                    self.fogregion.FromPolygon(polyline,0)
+                else:
+                    self.fogregion.FromPolygon(polyline,1)
+
+    def init_from_dom(self,xml_dom):
+        self.interpret_dom(xml_dom)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/grid.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,461 @@
+# 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: mapper/gird.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: grid.py,v 1.29 2007/12/07 20:39:49 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: grid.py,v 1.29 2007/12/07 20:39:49 digitalxero Exp $"
+
+from base import *
+from isometric import *
+from miniatures import SNAPTO_ALIGN_CENTER
+from miniatures import SNAPTO_ALIGN_TL
+from math import floor
+
+# Grid mode constants
+GRID_RECTANGLE = 0
+GRID_HEXAGON = 1
+GRID_ISOMETRIC = 2
+LINE_NONE = 0
+LINE_DOTTED = 1
+LINE_SOLID = 2
+RATIO_DEFAULT = 2.0
+
+##-----------------------------
+## grid layer
+##-----------------------------
+class grid_layer(layer_base):
+
+    def __init__(self, canvas):
+        layer_base.__init__(self)
+        self.canvas = canvas
+        self.iso_ratio = RATIO_DEFAULT  #2:1 isometric ratio
+        self.mapscale = 1.0
+        self.unit_size = 100
+        self.unit_size_y = 100
+        #unit_widest and unit_offset are for the Hex Grid only. these are mathmatics to figure out the exact center of the hex
+        self.unit_widest = 100
+        self.unit_offset = 100
+        #size_ratio is the size ajustment for Hex and ISO to make them more accurate
+        self.size_ratio = 1.5
+        self.snap = True
+        self.color = wx.BLACK# = color.Get()
+        #self.color = cmpColour(r,g,b)
+        self.r_h = RGBHex()
+        self.mode = GRID_RECTANGLE
+        self.line = LINE_NONE
+        # Keep logic for different modes in different functions
+        self.grid_hit_test = self.grid_hit_test_rect
+        self.get_top_corner = self.get_top_corner_rect
+        self.layerDraw = self.draw_rect
+        self.isUpdated = True
+
+    def get_unit_size(self):
+        return self.unit_size
+
+    def get_iso_ratio(self):
+        return self.iso_ratio
+
+    def get_mode(self):
+        return self.mode
+
+    def get_color(self):
+        return self.color
+
+    def get_line_type(self):
+        return self.line
+
+    def is_snap(self):
+        return self.snap
+
+    def get_snapped_to_pos(self, pos, snap_to_align, mini_width, mini_height):
+        grid_pos = self.grid_hit_test(pos)
+        if grid_pos is not None:
+            topLeft = self.get_top_corner(grid_pos)#  get the top corner for this grid cell
+            if snap_to_align == SNAPTO_ALIGN_CENTER:
+                if self.mode == GRID_HEXAGON:
+                    x = topLeft.x + (((self.unit_size/1.75) - mini_width) /2)
+                    y = topLeft.y + ((self.unit_size - mini_height) /2)
+                elif self.mode == GRID_ISOMETRIC:
+                    x = (topLeft.x)-(mini_width/2)
+                    y = (topLeft.y)-(mini_height)
+                else:# GRID_RECTANGLE
+                    x = topLeft.x + ((self.unit_size - mini_width) / 2)
+                    y = topLeft.y + ((self.unit_size_y - mini_height) /2)
+            else:
+                x = topLeft.x
+                y = topLeft.y
+            return cmpPoint(int(x),int(y))                                           #  Set the pos attribute
+        else:
+            return cmpPoint(int(pos.x),int(pos.y))
+
+    def set_rect_mode(self):
+        "switch grid to rectangular mode"
+        self.mode = GRID_RECTANGLE
+        self.grid_hit_test = self.grid_hit_test_rect
+        self.get_top_corner = self.get_top_corner_rect
+        self.layerDraw = self.draw_rect
+        self.unit_size_y = self.unit_size
+
+    def set_hex_mode(self):
+        "switch grid to hexagonal mode"
+        self.mode = GRID_HEXAGON
+        self.grid_hit_test = self.grid_hit_test_hex
+        self.get_top_corner = self.get_top_corner_hex
+        self.layerDraw = self.draw_hex
+        self.unit_size_y = self.unit_size
+        self.unit_offset = sqrt(pow((self.unit_size/self.size_ratio ),2)-pow((self.unit_size/2),2))
+        self.unit_widest = (self.unit_offset*2)+(self.unit_size/self.size_ratio )
+
+    def set_iso_mode(self):
+        "switch grid to hexagonal mode"
+        self.mode = GRID_ISOMETRIC
+        self.grid_hit_test = self.grid_hit_test_iso
+        self.get_top_corner = self.get_top_corner_iso
+        self.layerDraw = self.draw_iso
+        self.unit_size_y = self.unit_size
+
+    def set_line_none(self):
+        "switch to no line mode for grid"
+        self.line = LINE_NONE
+
+    def set_line_dotted(self):
+        "switch to dotted line mode for grid"
+        self.line = LINE_DOTTED
+
+    def set_line_solid(self):
+        "switch to solid line mode for grid"
+        self.line = LINE_SOLID
+
+    def grid_hit_test_rect(self,pos):
+        "return grid pos (w,h) on rect map from pos"
+        if self.unit_size and self.snap:
+            return cmpPoint(int(pos.x/self.unit_size), int(pos.y/self.unit_size))
+        else:
+            return None
+
+    def grid_hit_test_hex(self,pos):
+        "return grid pos (w,h) on hex map from pos"
+        if self.unit_size and self.snap:
+            # rectangular repeat patern is as follows (unit_size is the height of a hex)
+            hex_side = int(self.unit_size/1.75)
+            half_height = int(self.unit_size/2)
+            height = int(self.unit_size)
+            #_____
+            #     \       /
+            #      \_____/
+            #      /     \
+            #_____/       \
+            col = int(pos.x/(hex_side*1.5))
+            row = int(pos.y/height)
+            (px, py) = (pos.x-(col*(hex_side*1.5)), pos.y-(row*height))
+            # adjust for the odd columns' rows being staggered lower
+            if col % 2 == 1:
+                if py < half_height:
+                    row = row - 1
+                    py = py + half_height
+                else:
+                    py = py - half_height
+            # adjust for top right corner
+            if (px * height - py * hex_side) > height * hex_side:
+                if col % 2 == 0:
+                    row = row - 1
+                col = col + 1
+            # adjust for bottom right corner
+            elif (px * height + py * hex_side) > 2 * height * hex_side:
+                if col%2==1:
+                    row = row + 1
+                col = col + 1
+            return cmpPoint(col, row)
+        else:
+            return None
+
+    def grid_hit_test_iso(self,pos):
+        "return grid pos (w,h) on isometric map from pos"
+        if self.unit_size and self.snap:
+            height = self.unit_size*self.size_ratio/self.iso_ratio
+            width = self.unit_size*self.size_ratio
+            iso_unit_size = height * width
+            # convert to isometric pos which has an origin of cell (0,0)
+            # x-ord increasing as you go up and right, y-ord increasing as you go down and right
+            # this is the transformation from grid co-ord to iso co-ords
+            iso_x = (pos.x*height) - (pos.y*width) + (iso_unit_size/2)
+            iso_y = (pos.x*height) + (pos.y*width) - (iso_unit_size/2)
+            #
+            #  /\
+            # /  \
+            #/    \
+            #\    /
+            # \  /
+            #  \/
+            # so the exact isomorphic (0,0) is the left corner of the first (ie. top left) diamond
+            # this is at grid co-ordinate (0, height/2)
+            # the top corner of the first diamond is grid co-ord (width/2, 0)
+            # and therefore (per transformation above) is at iso co-ord (iso_unit_size, 0)
+            # the bottom corner of the first diamond is grid co-ord (width/2, height)
+            # and therefore (per transformation above) is at iso co-ord (0, iso_unit_size)
+
+            # the calculation is now as simple as the rectangle case, but using iso co-ords
+            return cmpPoint(floor(iso_x/iso_unit_size), floor(iso_y/iso_unit_size))
+        else:
+            return None
+
+    def get_top_corner_iso(self, iso_pos):
+        "return upper left of a iso grid pos"
+        # for whatever reason the iso grid returns the center of the diamond for "top left corner"
+        if self.unit_size:
+            half_height = self.unit_size*self.size_ratio/(2*self.iso_ratio)
+            half_width = self.unit_size*self.size_ratio/2
+            # convert back into grid co-ordinates of center of diamond
+            grid_x = (iso_pos.y*half_width) + (iso_pos.x*half_width) + half_width
+            grid_y = (iso_pos.y*half_height) - (iso_pos.x*half_height) + half_height
+            return cmpPoint(int(grid_x), int(grid_y))
+        else:
+            return None
+
+    def get_top_corner_rect(self,grid_pos):
+        "return upper left of a rect grid pos"
+        if self.unit_size:
+            return cmpPoint(grid_pos[0]*self.unit_size,grid_pos[1]*self.unit_size)
+        else:
+            return None
+
+    def get_top_corner_hex(self,grid_pos):
+        "return upper left of a hex grid pos"
+        if self.unit_size:
+            # We can get our x value directly, y is trickier
+            temp_x = (((self.unit_size/1.75)*1.5)*grid_pos[0])
+            temp_y = self.unit_size*grid_pos[1]
+            # On odd columns we have to slide down slightly
+            if grid_pos[0] % 2:
+                temp_y += self.unit_size/2
+            return cmpPoint(temp_x,temp_y)
+        else:
+            return None
+
+    def set_grid(self, unit_size, snap, color, mode, line, ratio=None):
+        self.unit_size = unit_size
+        if ratio != None:
+            self.iso_ratio = ratio
+        self.snap = snap
+        self.set_color(color)
+        self.SetMode(mode)
+        self.SetLine(line)
+
+    def SetLine(self,line):
+        if line == LINE_NONE:
+            self.set_line_none()
+        elif line == LINE_DOTTED:
+            self.set_line_dotted()
+        elif line == LINE_SOLID:
+            self.set_line_solid()
+
+    def SetMode(self, mode):
+        if mode == GRID_RECTANGLE:
+            self.set_rect_mode()
+        elif mode == GRID_HEXAGON:
+            self.set_hex_mode()
+        elif mode == GRID_ISOMETRIC:
+            self.set_iso_mode()
+
+    def return_grid(self):
+        return self.canvas.size
+
+    def set_color(self,color):
+        (r,g,b) = color.Get()
+        self.color = cmpColour(r,g,b)
+
+    def draw_iso(self,dc,topleft,clientsize):
+        if not self.unit_size: return
+        if self.line == LINE_NONE: return
+        if self.line == LINE_SOLID:
+            dc.SetPen(wx.Pen(self.color,1,wx.SOLID))
+        else:
+            dc.SetPen(wx.Pen(self.color,1,wx.DOT))
+        sz = self.canvas.size
+
+        # Enable DC optimizations if available on a platform
+        dc.BeginDrawing()
+
+        # create IsoGrid helper object
+        IG = IsoGrid(self.unit_size*self.size_ratio)
+        IG.Ratio(self.iso_ratio)
+        rows = int(min(clientsize[1]+topleft[1],sz[1])/IG.height)
+        cols = int(min(clientsize[0]+topleft[0],sz[0])/IG.width)
+        for y in range(rows+1):
+            for x in range(cols+1):
+                IG.BoundPlace((x*IG.width),(y*IG.height))
+                x1,y1 = IG.Top()
+                x2,y2 = IG.Left()
+                dc.DrawLine(x1,y1,x2,y2)
+                x1,y1 = IG.Left()
+                x2,y2 = IG.Bottom()
+                dc.DrawLine(x1,y1,x2,y2)
+                x1,y1 = IG.Bottom()
+                x2,y2 = IG.Right()
+                dc.DrawLine(x1,y1,x2,y2)
+                x1,y1 = IG.Right()
+                x2,y2 = IG.Top()
+                dc.DrawLine(x1,y1,x2,y2)
+        # Enable DC optimizations if available on a platform
+        dc.EndDrawing()
+        dc.SetPen(wx.NullPen)
+        # Disable pen/brush optimizations to prevent any odd effects elsewhere
+
+    def draw_rect(self,dc,topleft,clientsize):
+        if self.unit_size:
+            draw = 1
+            # Enable pen/brush optimizations if available on a platform
+            if self.line == LINE_NONE:
+                draw = 0
+            elif self.line == LINE_SOLID:
+                dc.SetPen(wx.Pen(self.color,1,wx.SOLID))
+            else:
+                dc.SetPen(wx.Pen(self.color,1,wx.DOT))
+            if draw:
+                sz = self.canvas.size
+                # Enable DC optimizations if available on a platform
+                dc.BeginDrawing()
+                # Now, draw the map grid
+                x = 0
+                s = self.unit_size
+                x = int(topleft[0]/s)*s
+                mx = min(clientsize[0]+topleft[0],sz[0])
+                my = min(clientsize[1]+topleft[1],sz[1])
+                while x < mx:
+                    dc.DrawLine(x,topleft[1],x,my)
+                    x += self.unit_size
+                y = 0
+                y = int (topleft[1]/s)*s
+                while y < my:
+                    dc.DrawLine(topleft[0],y,mx,y)
+                    y += self.unit_size
+                # Enable DC optimizations if available on a platform
+                dc.EndDrawing()
+                dc.SetPen(wx.NullPen)
+            # Disable pen/brush optimizations to prevent any odd effects elsewhere
+
+    def draw_hex(self,dc,topleft,clientsize):
+        if self.unit_size:
+            draw = 1
+            # Enable pen/brush optimizations if available on a platform
+            if self.line == LINE_NONE:
+                draw = 0
+            elif self.line == LINE_SOLID:
+                dc.SetPen(wx.Pen(self.color,1,wx.SOLID))
+            else:
+                dc.SetPen(wx.Pen(self.color,1,wx.DOT))
+            if draw:
+                sz = self.canvas.size
+                x = 0
+                A = self.unit_size/1.75 #Side Length
+                B = self.unit_size #The width between any two sides
+                D = self.unit_size/2 #The distance from the top to the middle of the hex
+                C = self.unit_size/3.5 #The distance from the point of the hex to the point where the top line starts
+
+                #   _____
+                #  /     \
+                # /       \
+                # \       /
+                #  \_____/
+
+                startx=int(topleft[0]/(3*A))*(3*A)
+                starty=int(topleft[1]/B)*B
+                y = starty
+                mx = min(clientsize[0]+topleft[0],sz[0])
+                my = min(clientsize[1]+topleft[1],sz[1])
+                while y < my:
+                    x = startx
+                    lineArray = []
+                    while x < mx:
+                        #The top / Bottom of the Hex
+                        lineArray.append((x, y))
+                        lineArray.append((x+A, y))
+                        #The Right Top Side of the Hex
+                        lineArray.append((x+A, y))
+                        lineArray.append((x+A+C, y+D))
+                        #The Right Bottom Side of the Hex
+                        lineArray.append((x+A+C, y+D))
+                        lineArray.append((x+A, y+B))
+                        #The Top / of the Middle Hex
+                        lineArray.append((x+A+C, y+D))
+                        lineArray.append((x+A+C+A, y+D))
+                        #The Left Bottom Side of the Hex
+                        lineArray.append((x+A+C+A, y+D))
+                        lineArray.append((x+A+C+A+C, y+B))
+                        #The left Top Side of the Hex
+                        lineArray.append((x+A+C+A, y+D))
+                        lineArray.append((x+A+C+A+C, y))
+                        x += A*3
+                    y += B
+                    dc.DrawLines(lineArray)
+                dc.SetPen(wx.NullPen)
+            # Disable pen/brush optimizations to prevent any odd effects elsewhere
+
+    def layerToXML(self,action = "update"):
+        xml_str = "<grid"
+        if self.color != None:
+            (red,green,blue) = self.color.Get()
+            hexcolor = self.r_h.hexstring(red, green, blue)
+            xml_str += " color='" + hexcolor + "'"
+        if self.unit_size != None:
+            xml_str += " size='" + str(self.unit_size) + "'"
+        if self.iso_ratio != None:
+            xml_str += " ratio='" + str(self.iso_ratio) + "'"
+        if self.snap != None:
+            if self.snap:
+                xml_str += " snap='1'"
+            else:
+                xml_str += " snap='0'"
+        if self.mode != None:
+            xml_str+= "  mode='" + str(self.mode) + "'"
+        if self.line != None:
+            xml_str+= " line='" + str(self.line) + "'"
+        xml_str += "/>"
+        if (action == "update" and self.isUpdated) or action == "new":
+            self.isUpdated = False
+            return xml_str
+        else:
+            return ''
+
+    def layerTakeDOM(self, xml_dom):
+        if xml_dom.hasAttribute("color"):
+            r,g,b = self.r_h.rgb_tuple(xml_dom.getAttribute("color"))
+            self.set_color(cmpColour(r,g,b))
+        #backwards compatible with non-isometric map formated clients
+        ratio = RATIO_DEFAULT
+        if xml_dom.hasAttribute("ratio"):
+            ratio = xml_dom.getAttribute("ratio")
+        if xml_dom.hasAttribute("mode"):
+            self.SetMode(int(xml_dom.getAttribute("mode")))
+        if xml_dom.hasAttribute("size"):
+            self.unit_size = int(xml_dom.getAttribute("size"))
+            self.unit_size_y = self.unit_size
+        if xml_dom.hasAttribute("snap"):
+            if (xml_dom.getAttribute("snap") == 'True') or (xml_dom.getAttribute("snap") == "1"):
+                self.snap = True
+            else:
+                self.snap = False
+        if xml_dom.hasAttribute("line"):
+            self.SetLine(int(xml_dom.getAttribute("line")))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/grid_handler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,94 @@
+#
+#    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: orpg/mapper/grid_handler.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: grid_handler.py,v 1.20 2007/04/03 00:14:35 digitalxero Exp $
+#
+# Description: grid layer handler
+#
+__version__ = "$Id: grid_handler.py,v 1.20 2007/04/03 00:14:35 digitalxero Exp $"
+
+from grid import *
+from base_handler import *
+
+class grid_handler(base_layer_handler):
+    def __init__(self, parent, id, canvas):
+        base_layer_handler.__init__(self, parent, id, canvas)
+
+    def build_ctrls(self):
+        base_layer_handler.build_ctrls(self)
+        self.line_type = wx.Choice(self, wx.ID_ANY, choices = ["No Lines", "Dotted Lines", "Solid Lines" ])
+        self.grid_mode = wx.Choice(self, wx.ID_ANY, choices = ["Rectangular", "Hexagonal","Isometric"])
+        self.grid_snap = wx.CheckBox(self, wx.ID_ANY, " Snap")
+        self.grid_size = wx.TextCtrl(self, wx.ID_ANY, size=(32,-1) )
+        self.grid_ratio = wx.TextCtrl(self, wx.ID_ANY, size=(32,-1) )
+        self.color_button = wx.Button(self, wx.ID_ANY, "Color", style=wx.BU_EXACTFIT)
+        self.apply_button = wx.Button(self, wx.ID_OK, "Apply", style=wx.BU_EXACTFIT)
+        self.color_button.SetBackgroundColour(wx.BLACK)
+        self.color_button.SetForegroundColour(wx.WHITE)
+        self.sizer.Add(wx.StaticText(self, -1, "Size: "), 0, wx.ALIGN_CENTER|wx.ALL, 3)
+        self.sizer.Add(self.grid_size, 0, wx.EXPAND|wx.ALL, 2)
+        self.sizer.Add(wx.StaticText(self, -1, "Ratio: "), 0, wx.ALIGN_CENTER|wx.ALL, 3)
+        self.sizer.Add(self.grid_ratio, 0, wx.EXPAND|wx.ALL, 2)
+        self.sizer.Add(self.line_type, 0, wx.EXPAND|wx.ALL, 3)
+        self.sizer.Add(self.grid_mode, 0, wx.EXPAND|wx.ALL, 2)
+        self.sizer.Add(self.grid_snap, 0, wx.EXPAND|wx.ALL, 3)
+        self.sizer.Add(self.color_button, 0, wx.EXPAND|wx.ALL, 2)
+        self.sizer.Add(self.apply_button, 0, wx.EXPAND|wx.ALL, 3)
+        self.sizer.Add(wx.Size(20,25),1)
+        self.Bind(wx.EVT_BUTTON, self.on_bg_color, self.color_button)
+        self.Bind(wx.EVT_BUTTON, self.on_apply, self.apply_button)
+        self.update_info()
+
+    def update_info(self):
+        layer = self.canvas.layers['grid']
+        self.grid_size.SetValue(str(layer.get_unit_size()))
+        self.grid_ratio.SetValue(str(layer.get_iso_ratio()))
+        self.grid_mode.SetSelection(layer.get_mode())
+        self.line_type.SetSelection(layer.get_line_type())
+        self.color_button.SetBackgroundColour(layer.get_color())
+        self.grid_snap.SetValue(layer.is_snap())
+        layer.isUpdated = True
+
+    def build_menu(self,label = "Grid"):
+        base_layer_handler.build_menu(self,label)
+
+    def on_bg_color(self,evt):
+        data = wx.ColourData()
+        data.SetChooseFull(True)
+        dlg = wx.ColourDialog(self.canvas, data)
+        if dlg.ShowModal() == wx.ID_OK:
+            data = dlg.GetColourData()
+            color = data.GetColour()
+            self.color_button.SetBackgroundColour(color)
+        dlg.Destroy()
+
+    def on_apply(self, evt):
+        session=self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM):
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            return
+
+        self.canvas.layers['grid'].set_grid(int(self.grid_size.GetValue()),self.grid_snap.GetValue(),
+            self.color_button.GetBackgroundColour(),self.grid_mode.GetSelection(),self.line_type.GetSelection(),float(self.grid_ratio.GetValue()))
+        self.update_info()
+        self.canvas.send_map_data()
+        self.canvas.Refresh()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/grid_msg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,36 @@
+# 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: mapper/gird_msg.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: grid_msg.py,v 1.8 2006/11/04 21:24:21 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: grid_msg.py,v 1.8 2006/11/04 21:24:21 digitalxero Exp $"
+
+from base_msg import *
+
+class grid_msg(map_element_msg_base):
+
+    def __init__(self,reentrant_lock_object = None):
+        self.tagname = "grid"
+        map_element_msg_base.__init__(self,reentrant_lock_object)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/images.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,158 @@
+# 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: mapper/images.py
+# Author: OpenRPG
+# Maintainer:
+# Version:
+#   $Id: images.py,v 1.21 2007/12/11 04:07:15 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: images.py,v 1.21 2007/12/11 04:07:15 digitalxero Exp $"
+
+import urllib
+import Queue
+import thread
+from threading import Lock
+import time
+from orpg.orpg_wx import *
+from orpg.orpgCore import *
+
+def singleton(cls):
+    instances = {}
+    def getinstance():
+        if cls not in instances:
+            instances[cls] = cls()
+        return instances[cls]
+    return getinstance()
+
+class ImageHandlerClass(object):
+    __cache = {}
+    __fetching = {}
+    __queue = Queue.Queue(0)
+    __lock = Lock()
+
+    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):
+            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:
+                thread.start_new_thread(self.__loadCacheThread, (path, image_type, imageId))
+        return wx.Bitmap(open_rpg.get_component("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):
+            return wx.ImageFromMime(self.__cache[path][1], self.__cache[path][2]).ConvertToBitmap()
+        uriPath = urllib.unquote(path)
+        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(), None)
+                return wx.ImageFromMime(self.__cache[path][1], self.__cache[path][2]).ConvertToBitmap()
+            else:
+                open_rpg.get_component('log').log("Image refused to load or URI did not reference a valid image: " + path, ORPG_GENERAL, True)
+                return None
+        except IOError:
+            open_rpg.get_component('log').log("Unable to resolve/open the specified URI; image was NOT loaded: " + path, ORPG_GENERAL, True)
+            return None
+
+    def cleanCache(self):
+        # Shrinks the Cache down to the proper size
+        try:
+            cacheSize = int(open_rpg.get_component('settings').get_setting("ImageCacheSize"))
+        except:
+            cacheSize = 32
+        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:
+            keyList = self.__cache.keys()
+            for key in keyList:
+                del self.__cache[key]
+        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]
+            else:
+                open_rpg.get_component('log').log("Image refused to load or URI did not reference a valid image: " + path, ORPG_GENERAL, True)
+                self.__fetching[path] = False
+        except IOError:
+            self.__fetching[path] = False
+            open_rpg.get_component('log').log("Unable to resolve/open the specified URI; image was NOT laoded: " + path, ORPG_GENERAL, 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:
+                time.sleep(0.025)
+                if (time.time()-st) > 120:
+                    open_rpg.get_component('log').log("Timeout: " + path, ORPG_GENERAL, True)
+                    break
+        except:
+            self.__fetching[path] = False
+            open_rpg.get_component('log').log("Unable to resolve/open the specified URI; image was NOT loaded: " + path, ORPG_GENERAL, True)
+            return 
+        self.__lock.acquire()
+        try:
+            open_rpg.get_component('log').log("Adding Image to Queue from Cache: " + str(self.__cache[path]), ORPG_DEBUG)
+            self.__queue.put((self.__cache[path], image_type, imageId))
+        finally:
+            self.__lock.release()
+
+#Property Methods
+    def _getCache(self):
+        return self.__cache
+
+    def _getQueue(self):
+        return self.__queue
+
+#Properties
+    Cache = property(_getCache)
+    Queue = property(_getQueue)
+
+ImageHandler = singleton(ImageHandlerClass)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/isometric.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,102 @@
+# 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: mapper/isometric.py
+# Author: Todd Faris (Snowdog)
+# Maintainer:
+# Version:
+#
+# Description:
+#   helper class definition for drawing and line collision within an isometric grid
+#   based on a interger based coordinate system where X increase to the right
+#   and Y increases in the downward direction
+
+
+class IsoGrid:
+    def __init__(self,width):
+        self.ratio = 2.0
+        self.width = width
+        self.height = 0
+        self.CalcRatio()
+        self.center_x = 0
+        self.center_y = 0
+
+    def Ratio(self, r ):
+        "changes the height/width ratio of the isometric rectangle with respect to the horizontal (x) axis"
+        self.ratio = float(r)
+        self.CalcRatio()
+
+    def CalcRatio(self):
+        if (self.ratio <= 0): self.ratio = 1
+        self.height = int( float(self.width)/float(self.ratio) )
+
+    def BoundPlace(self, left,top):
+        "Places the isometric rectangle using its upper left bounding corner for positioning"
+        dx,dy = self.CornerOffset()
+        self.Recenter( left+dx, top+dy)
+
+    def Recenter(self,x,y):
+        "Places the isomentric rectangle using the centerpoint"
+        self.center_x = x
+        self.center_y = y
+
+    def Top(self):
+        "Returns the topmost point of the isometric rectangle as a tuple (x,y)"
+        x = int(self.center_x)
+        y = int(self.center_y) - int(self.height/2)
+        return(x,y)
+
+    def Left(self):
+        "Returns the leftmost point of the isometric rectangle as a tuple (x,y)"
+        x = int(self.center_x) - int(self.width/2)
+        y = int(self.center_y)
+        return(x,y)
+
+    def Right(self):
+        "Returns the rightmost point of the isometric rectangle as a tuple (x,y)"
+        x,y = self.Left()
+        x = x + self.width
+        return(x,y)
+
+    def Bottom(self):
+        "Returns the bottommost point of the isometric rectangle as a tuple (x,y)"
+        x,y = self.Top()
+        y = y + self.height
+        return(x,y)
+
+    def Center(self):
+        "Returns the center point of the isometric rectangle as a tuple (x,y)"
+        x = int(self.center_x)
+        y = int(self.center_y)
+        return(x,y)
+
+    def CornerOffset(self):
+        """Returns a tuple (delta_x,delta_y) representing the offset from the centerpoint
+        of the isometric rectangle to the upper left corner of its rectangular bounding box"""
+        dx = float(self.width/2)
+        dy = float(self.height/2)
+        return (dx,dy)
+
+    def Height(self):
+        "Returns the height of the isometric rectangle based on current ratio"
+        return int(self.height)
+
+    def Width(self):
+        "returns the width of the isomentric rectangle"
+        return int(self.width)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/map.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1131 @@
+# 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: mapper/map.py
+# Author: OpenRPG
+# Maintainer:
+# Version:
+#   $Id: map.py,v 1.73 2007/12/07 20:39:49 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: map.py,v 1.73 2007/12/07 20:39:49 digitalxero Exp $"
+
+from map_version import MAP_VERSION
+from map_msg import *
+from min_dialogs import *
+from map_prop_dialog import *
+import orpg.dirpath
+import random
+import os
+import thread
+import gc
+import traceback
+from miniatures_handler import *
+from whiteboard_handler import *
+from background_handler import *
+from fog_handler import *
+from images import ImageHandler
+from grid_handler import *
+from map_handler import *
+from orpg.orpgCore import open_rpg
+
+# Various marker modes for player tools on the map
+MARKER_MODE_NONE = 0
+MARKER_MODE_MEASURE = 1
+MARKER_MODE_TARGET = 2
+MARKER_MODE_AREA_TARGET = 3
+
+class MapCanvas(wx.ScrolledWindow):
+    def __init__(self, parent, ID, isEditor=0):
+        self.parent = parent
+        self.log = open_rpg.get_component("log")
+        self.log.log("Enter MapCanvas", ORPG_DEBUG)
+        self.settings = open_rpg.get_component("settings")
+        self.session = open_rpg.get_component("session")
+        wx.ScrolledWindow.__init__(self, parent, ID, style=wx.HSCROLL | wx.VSCROLL | wx.FULL_REPAINT_ON_RESIZE | wx.SUNKEN_BORDER )
+        self.frame = parent
+        self.MAP_MODE = 1      #Mode 1 = MINI, 2 = DRAW, 3 = TAPE MEASURE
+        self.layers = {}
+        self.layers['bg'] = layer_back_ground(self)
+        self.layers['grid'] = grid_layer(self)
+        self.layers['whiteboard'] = whiteboard_layer(self)
+        self.layers['miniatures'] = miniature_layer(self)
+        self.layers['fog'] = fog_layer(self)
+        self.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
+        self.Bind(wx.EVT_PAINT, self.on_paint)
+        self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
+        self.Bind(wx.EVT_LEFT_DCLICK, self.on_left_dclick)
+        self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.on_right_down)
+        self.Bind(wx.EVT_MOTION, self.on_motion)
+        self.Bind(wx.EVT_SCROLLWIN, self.on_scroll)
+        self.Bind(wx.EVT_CHAR, self.on_char)
+        self.Bind(wx.EVT_SIZE, self.on_resize)
+        self.set_size((1000,1000))
+        self.root_dir = os.getcwd()
+        self.size_change = 0
+        self.isEditor = isEditor
+        self.map_version = MAP_VERSION
+        self.cacheSize = 32
+        # Create the marker mode attributes for the map
+        self.markerMode = MARKER_MODE_NONE
+        self.markerStart = wx.Point( -1, -1 )
+        self.markerStop = wx.Point( -1, -1 )
+        self.markerMidpoint = wx.Point( -1, -1 )
+        self.markerAngle = 0.0
+        # Optimization of map refreshing during busy map load
+        self.lastRefreshValue = 0
+        self.requireRefresh = 0
+        self.lastRefreshTime = 0
+        self.zoom_display_timer = wx.Timer(self, wx.NewId())
+        self.Bind(wx.EVT_TIMER, self.better_refresh, self.zoom_display_timer)
+        random.seed( time.time() )
+        self.image_timer = wx.Timer(self, wx.NewId())
+        self.Bind(wx.EVT_TIMER, self.processImages, self.image_timer)
+        self.image_timer.Start(100)
+        # Used to check if we've used the user cache size value
+        self.cacheSizeSet = False
+        self.inside = 0
+        # miniatures drag
+        self.drag = None
+        self.log.log("Exit MapCanvas", ORPG_DEBUG)
+
+    def better_refresh(self, event=None):
+        self.log.log("Enter MapCanvas->better_refresh(self)", ORPG_DEBUG)
+        self.Refresh(True)
+        self.log.log("Eexit MapCanvas->better_refresh(self)", ORPG_DEBUG)
+
+    def pre_destory_cleanup(self):
+        self.log.log("Enter MapCanvas->pre_destory_cleanup(self)", ORPG_DEBUG)
+        self.layers["miniatures"].del_all_miniatures()
+        self.log.log("Exit MapCanvas->pre_destory_cleanup(self)", ORPG_DEBUG)
+
+    def processImages(self, evt=None):
+        self.log.log("Enter MapCanvas->processImages(self)", ORPG_DEBUG)
+        self.session = open_rpg.get_component("session")
+        if self.session.my_role() == self.session.ROLE_LURKER or (str(self.session.group_id) == '0' and str(self.session.status) == '1'):
+            cidx = self.parent.get_tab_index("Background")
+            self.parent.layer_tabs.EnableTab(cidx, False)
+            cidx = self.parent.get_tab_index("Grid")
+            self.parent.layer_tabs.EnableTab(cidx, False)
+            cidx = self.parent.get_tab_index("Miniatures")
+            self.parent.layer_tabs.EnableTab(cidx, False)
+            cidx = self.parent.get_tab_index("Whiteboard")
+            self.parent.layer_tabs.EnableTab(cidx, False)
+            cidx = self.parent.get_tab_index("Fog")
+            self.parent.layer_tabs.EnableTab(cidx, False)
+            cidx = self.parent.get_tab_index("General")
+            self.parent.layer_tabs.EnableTab(cidx, False)
+        else:
+            cidx = self.parent.get_tab_index("Background")
+            if not self.parent.layer_tabs.GetEnabled(cidx):
+                cidx = self.parent.get_tab_index("Miniatures")
+                self.parent.layer_tabs.EnableTab(cidx, True)
+                cidx = self.parent.get_tab_index("Whiteboard")
+                self.parent.layer_tabs.EnableTab(cidx, True)
+                cidx = self.parent.get_tab_index("Background")
+                self.parent.layer_tabs.EnableTab(cidx, False)
+                cidx = self.parent.get_tab_index("Grid")
+                self.parent.layer_tabs.EnableTab(cidx, False)
+                cidx = self.parent.get_tab_index("Fog")
+                self.parent.layer_tabs.EnableTab(cidx, False)
+                cidx = self.parent.get_tab_index("General")
+                self.parent.layer_tabs.EnableTab(cidx, False)
+                if self.session.my_role() == self.session.ROLE_GM:
+                    cidx = self.parent.get_tab_index("Background")
+                    self.parent.layer_tabs.EnableTab(cidx, True)
+                    cidx = self.parent.get_tab_index("Grid")
+                    self.parent.layer_tabs.EnableTab(cidx, True)
+                    cidx = self.parent.get_tab_index("Fog")
+                    self.parent.layer_tabs.EnableTab(cidx, True)
+                    cidx = self.parent.get_tab_index("General")
+                    self.parent.layer_tabs.EnableTab(cidx, True)
+        if not self.cacheSizeSet:
+            self.cacheSizeSet = True
+            cacheSize = self.settings.get_setting("ImageCacheSize")
+            if len(cacheSize):
+                self.cacheSize = int(cacheSize)
+            else:
+                self.log.log("Default cache size being used.", ORPG_GENERAL)
+            self.log.log("Current image cache size is set at " + str(self.cacheSize) + " images, using random purge.", ORPG_GENERAL)
+        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
+            # Flag that we now need to refresh!
+            self.requireRefresh += 1
+
+            # Randomly purge an item from the cache, while this is lamo, it does
+            # keep the cache from growing without bounds, which is pretty important!
+            if len(ImageHandler.Cache) >= self.cacheSize:
+                ImageHandler.cleanCache()
+        else:
+            # Now, make sure not only that we require a refresh, but that enough time has
+            # gone by since our last refresh.  This keeps back to back refreshing occuring during
+            # large map loads.  Of course, we are now trying to pack as many image refreshes as
+            # we can into a single cycle.
+            if self.requireRefresh and (self.requireRefresh == self.lastRefreshValue):
+                if (self.lastRefreshTime) < time.time():
+                    self.requireRefresh = 0
+                    self.lastRefreshValue = 0
+                    self.lastRefreshTime = time.time()
+                    self.Refresh(True)
+            else:
+                self.lastRefreshValue = self.requireRefresh
+        self.log.log("Exit MapCanvas->processImages(self)", ORPG_DEBUG)
+
+    def on_scroll(self, evt):
+        self.log.log("Enter MapCanvas->on_scroll(self, evt)", ORPG_DEBUG)
+        if self.drag:
+            self.drag.Hide()
+        if self.settings.get_setting("AlwaysShowMapScale") == "1":
+            self.printscale()
+        evt.Skip()
+        self.log.log("Exit MapCanvas->on_scroll(self, evt)", ORPG_DEBUG)
+
+    def on_char(self, evt):
+        self.log.log("Enter MapCanvas->on_char(self, evt)", ORPG_DEBUG)
+        if self.settings.get_setting("AlwaysShowMapScale") == "1":
+            self.printscale()
+        evt.Skip()
+        self.log.log("Exit MapCanvas->on_char(self, evt)", ORPG_DEBUG)
+
+    def printscale(self):
+        self.log.log("Enter MapCanvas->printscale(self)", ORPG_DEBUG)
+        wx.BeginBusyCursor()
+        dc = wx.ClientDC(self)
+        self.PrepareDC(dc)
+        self.showmapscale(dc)
+        self.Refresh(True)
+        wx.EndBusyCursor()
+        self.log.log("Exit MapCanvas->printscale(self)", ORPG_DEBUG)
+
+    def send_map_data(self, action="update"):
+        self.log.log("Enter MapCanvas->send_map_data(self, " + action +")", ORPG_DEBUG)
+        wx.BeginBusyCursor()
+        send_text = self.toxml(action)
+        if send_text:
+            if not self.isEditor:
+                self.frame.session.send(send_text)
+        wx.EndBusyCursor()
+        self.log.log("Exit MapCanvas->send_map_data(self, " + action +")", ORPG_DEBUG)
+
+    def get_size(self):
+        self.log.log("Enter MapCanvas->get_size(self)", ORPG_DEBUG)
+        self.log.log("Exit MapCanvas->get_size(self) return " + str(self.size), ORPG_DEBUG)
+        return self.size
+
+    def set_size(self, size):
+        self.log.log("Enter MapCanvas->set_size(self, size)", ORPG_DEBUG)
+        if size[0] < 300:
+            size = (300, size[1])
+        if size[1] < 300:
+            size = (size[0], 300)
+        self.size_changed = 1
+        self.size = size
+        self.fix_scroll()
+        self.layers['fog'].resize(size)
+        self.log.log("Exit MapCanvas->set_size(self, size)", ORPG_DEBUG)
+
+    def fix_scroll(self):
+        self.log.log("Enter MapCanvas->fix_scroll(self)", ORPG_DEBUG)
+        scale = self.layers['grid'].mapscale
+        pos = self.GetViewStart()
+        unit = self.GetScrollPixelsPerUnit()
+        pos = [pos[0]*unit[0],pos[1]*unit[1]]
+        size = self.GetClientSize()
+        unit = [10*scale,10*scale]
+        if (unit[0] == 0 or unit[1] == 0):
+            self.log.log("Exit MapCanvas->fix_scroll(self)", ORPG_DEBUG)
+            return
+        pos[0] /= unit[0]
+        pos[1] /= unit[1]
+        mx = [int(self.size[0]*scale/unit[0])+1, int(self.size[1]*scale/unit[1]+1)]
+        self.SetScrollbars(unit[0], unit[1], mx[0], mx[1], pos[0], pos[1])
+        self.log.log("Exit MapCanvas->fix_scroll(self)", ORPG_DEBUG)
+
+    def on_resize(self, evt):
+        self.log.log("Enter MapCanvas->on_resize(self, evt)", ORPG_DEBUG)
+        self.fix_scroll()
+        wx.CallAfter(self.Refresh, True)
+        evt.Skip()
+        self.log.log("Exit MapCanvas->on_resize(self, evt)", ORPG_DEBUG)
+
+    def on_erase_background(self, evt):
+        self.log.log("Enter MapCanvas->on_erase_background(self, evt)", ORPG_DEBUG)
+        evt.Skip()
+        self.log.log("Exit MapCanvas->on_erase_background(self, evt)", ORPG_DEBUG)
+
+    def on_paint(self, evt):
+        self.log.log("Enter MapCanvas->on_paint(self, evt)", ORPG_DEBUG)
+        scale = self.layers['grid'].mapscale
+        scrollsize = self.GetScrollPixelsPerUnit()
+        clientsize = self.GetClientSize()
+        topleft1 = self.GetViewStart()
+        topleft = [topleft1[0]*scrollsize[0], topleft1[1]*scrollsize[1]]
+        if (clientsize[0] > 1) and (clientsize[1] > 1):
+            dc = wx.MemoryDC()
+            bmp = wx.EmptyBitmap(clientsize[0]+1, clientsize[1]+1)
+            dc.SelectObject(bmp)
+            dc.SetPen(wx.TRANSPARENT_PEN)
+            dc.SetBrush(wx.Brush(self.GetBackgroundColour(), wx.SOLID))
+            dc.DrawRectangle(0,0,clientsize[0]+1,clientsize[1]+1)
+            dc.SetDeviceOrigin(-topleft[0], -topleft[1])
+            dc.SetUserScale(scale, scale)
+            self.layers['bg'].layerDraw(dc, scale, topleft, clientsize)
+            self.layers['grid'].layerDraw(dc, [topleft[0]/scale, topleft[1]/scale], [clientsize[0]/scale, clientsize[1]/scale])
+            self.layers['miniatures'].layerDraw(dc, [topleft[0]/scale, topleft[1]/scale], [clientsize[0]/scale, clientsize[1]/scale])
+            self.layers['whiteboard'].layerDraw(dc)
+            self.layers['fog'].layerDraw(dc, topleft, clientsize)
+            dc.SetPen(wx.NullPen)
+            dc.SetBrush(wx.NullBrush)
+            dc.SelectObject(wx.NullBitmap)
+            del dc
+            wdc = self.preppaint()
+            wdc.DrawBitmap(bmp, topleft[0], topleft[1])
+            if self.frame.settings.get_setting("AlwaysShowMapScale") == "1":
+                self.showmapscale(wdc)
+        try:
+            evt.Skip()
+        except:
+            pass
+        self.log.log("Exit MapCanvas->on_paint(self, evt)", ORPG_DEBUG)
+
+    def preppaint(self):
+        self.log.log("Enter MapCanvas->preppaint(self)", ORPG_DEBUG)
+        dc = wx.PaintDC(self)
+        self.PrepareDC(dc)
+        self.log.log("Exit MapCanvas->preppaint(self)", ORPG_DEBUG)
+        return (dc)
+
+    def showmapscale(self, dc):
+        self.log.log("Enter MapCanvas->showmapscale(self, dc)", ORPG_DEBUG)
+        scalestring = "Scale x" + `self.layers['grid'].mapscale`[:3]
+        (textWidth, textHeight) = dc.GetTextExtent(scalestring)
+        dc.SetUserScale(1, 1)
+        dc.SetPen(wx.LIGHT_GREY_PEN)
+        dc.SetBrush(wx.LIGHT_GREY_BRUSH)
+        x = dc.DeviceToLogicalX(0)
+        y = dc.DeviceToLogicalY(0)
+        dc.DrawRectangle(x, y, textWidth+2, textHeight+2)
+        dc.SetPen(wx.RED_PEN)
+        dc.DrawText(scalestring, x+1, y+1)
+        dc.SetPen(wx.NullPen)
+        dc.SetBrush(wx.NullBrush)
+        self.log.log("Exit MapCanvas->showmapscale(self, dc)", ORPG_DEBUG)
+
+    def snapMarker(self, snapPoint):
+        """Based on the position and unit size, figure out where we need to snap to.  As is, on
+        a square grid, there are four possible places to snap.  On a hex gid, there are 6 or 12 snap
+        points."""
+        self.log.log("Enter MapCanvas->snapMarker(self, snapPoint)", ORPG_DEBUG)
+
+        # If snap to grid is disabled, simply return snapPoint unmodified
+        if self.layers['grid'].snap:
+            # This means we need to determine where to snap our line.  We will support
+            # snapping to four different snapPoints per square for now.
+            # TODO!!!
+            if self.layers['grid'].mode == GRID_HEXAGON:
+                size = self.layers['grid'].unit_size_y
+            else:
+                size = int(self.layers['grid'].unit_size)
+                # Find the uppper left hand corner of the grid we are to snap to
+                offsetX = (snapPoint.x / size) * size
+                offsetY = (snapPoint.y / size) * size
+                # Calculate the delta value between where we clicked and the square it is near
+                deltaX = snapPoint.x - offsetX
+                deltaY = snapPoint.y - offsetY
+                # Now, figure our what quadrant (x, y) we need to snap to
+                snapSize = size / 2
+                # Figure out the X snap placement
+                if deltaX <= snapSize:
+                    quadXPos = offsetX
+                else:
+                    quadXPos = offsetX + size
+                # Now, figure out the Y snap placement
+                if deltaY <= snapSize:
+                    quadYPos = offsetY
+                else:
+                    quadYPos = offsetY + size
+                # Create our snap snapPoint and return it
+                snapPoint = wx.Point( quadXPos, quadYPos )
+        self.log.log("Exit MapCanvas->snapMarker(self, snapPoint)", ORPG_DEBUG)
+        return snapPoint
+
+    # Bunch of math stuff for marking and measuring
+    def calcSlope(self, start, stop):
+        """Calculates the slop of a line and returns it."""
+        self.log.log("Enter MapCanvas->calcSlope(self, start, stop)", ORPG_DEBUG)
+        if start.x == stop.x:
+            s = 0.0001
+        else:
+            s = float((stop.y - start.y)) / float((stop.x - start.x))
+        self.log.log("Exit MapCanvas->calcSlope(self, start, stop)", ORPG_DEBUG)
+        return s
+
+    def calcSlopeToAngle(self, slope):
+        """Based on the input slope, the angle (in degrees) will be returned."""
+        self.log.log("Enter MapCanvas->calcSlopeToAngle(self, slope)", ORPG_DEBUG)
+        # See if the slope is neg or positive
+        if slope == abs(slope):
+            # Slope is positive, so make sure it's not zero
+            if slope == 0:
+                a = 0
+            else:
+                # We are positive and NOT zero
+                a = 360 - atan(slope) * (180.0/pi)
+        else:
+            # Slope is negative so work on the abs of it
+            a = atan(abs(slope)) * (180.0/pi)
+        self.log.log("Exit MapCanvas->calcSlopeToAngle(self, slope)", ORPG_DEBUG)
+        return a
+
+    def calcLineAngle(self, start, stop):
+        """Based on two points that are on a line, return the angle of that line."""
+        self.log.log("Enter MapCanvas->calcLineAngle(self, start, stop)", ORPG_DEBUG)
+        a = self.calcSlopeToAngle( self.calcSlope( start, stop ) )
+        self.log.log("Exit MapCanvas->calcLineAngle(self, start, stop)", ORPG_DEBUG)
+        return a
+
+    def calcPixelDistance(self, start, stop):
+        """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 )"""
+        self.log.log("Enter MapCanvas->calcPixelDistance(self, start, stop)", ORPG_DEBUG)
+        d = sqrt( abs((stop.x - start.x)**2 - (stop.y - start.y)**2) )
+        self.log.log("Exit MapCanvas->calcPixelDistance(self, start, stop)", ORPG_DEBUG)
+        return d
+
+    def calcUnitDistance(self, start, stop, lineAngle):
+        self.log.log("Enter MapCanvas->calcUnitDistance(self, start, stop, lineAngle)", ORPG_DEBUG)
+        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
+        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 = sqrt( abs((stop.x - start.x)**2 - (stop.y - start.y)**2) )
+        self.log.log("Exit MapCanvas->calcUnitDistance(self, start, stop, lineAngle)", ORPG_DEBUG)
+        return ud
+
+    def on_tape_motion(self, evt):
+        """Track mouse motion so we can update the marker visual every time it's moved"""
+        self.log.log("Enter MapCanvas->on_tape_motion(self, evt)", ORPG_DEBUG)
+        # Make sure we have a mode to do anything, otherwise, we ignore this
+        if self.markerMode:
+            # Grap the current DC for all of the marker modes
+            dc = wx.ClientDC( self )
+            self.PrepareDC( dc )
+            dc.SetUserScale(self.layers['grid'].mapscale,self.layers['grid'].mapscale)
+            # Grab the current map position
+            pos = self.snapMarker( evt.GetLogicalPosition( dc ) )
+            # Enable brush optimizations
+            #dc.SetOptimization( True )
+            # Set up the pen used for drawing our marker
+            dc.SetPen( wx.Pen(wx.RED, 1, wx.LONG_DASH) )
+            # Now, based on the marker mode, draw the right thing
+            if self.markerMode == MARKER_MODE_MEASURE:
+                if self.markerStop.x != -1 and self.markerStop.y != -1:
+                    # Set the DC function that we need
+                    dc.SetLogicalFunction(wx.INVERT)
+                    # Erase old and Draw new marker line
+                    dc.BeginDrawing()
+                    dc.DrawLine( self.markerStart.x, self.markerStart.y, self.markerStop.x, self.markerStop.y )
+                    dc.DrawLine( self.markerStart.x, self.markerStart.y, pos.x, pos.y )
+                    dc.EndDrawing()
+                    # Restore the default DC function
+                    dc.SetLogicalFunction(wx.COPY)
+                # As long as we are in marker mode, we ned to update the stop point
+                self.markerStop = pos
+            dc.SetPen(wx.NullPen)
+            # Disable brush optimizations
+            #dc.SetOptimization( False )
+            del dc
+        self.log.log("Exit MapCanvas->on_tape_motion(self, evt)", ORPG_DEBUG)
+
+    def on_tape_down(self, evt):
+        """Greg's experimental tape measure code.  Hopefully, when this is done, it will all be
+        modal based on a toolbar."""
+        self.log.log("Enter MapCanvas->on_tape_down(self, evt)", ORPG_DEBUG)
+        dc = wx.ClientDC( self )
+        self.PrepareDC( dc )
+        dc.SetUserScale(self.layers['grid'].mapscale,self.layers['grid'].mapscale)
+        pos = evt.GetLogicalPosition( dc )
+        # If grid snap is enabled, then snap the tool to a proper position
+        pos = self.snapMarker( evt.GetLogicalPosition( dc ) )
+        # Maker mode should really be set by a toolbar
+        self.markerMode = MARKER_MODE_MEASURE
+        # Erase the old line if her have one
+        if self.markerStart.x != -1 and self.markerStart.y != -1:
+            # Enable brush optimizations
+            #dc.SetOptimization( True )
+            # Set up the pen used for drawing our marker
+            dc.SetPen( wx.Pen(wx.RED, 1, wx.LONG_DASH) )
+            # Set the DC function that we need
+            dc.SetLogicalFunction(wx.INVERT)
+            # Draw the marker line
+            dc.BeginDrawing()
+            dc.DrawLine( self.markerStart.x, self.markerStart.y, self.markerStop.x, self.markerStop.y )
+            dc.EndDrawing()
+            # Restore the default DC function and pen
+            dc.SetLogicalFunction(wx.COPY)
+            dc.SetPen(wx.NullPen)
+            # Disable brush optimizations
+            #dc.SetOptimization( False )
+        # Save our current start and reset the stop value
+        self.markerStart = pos
+        self.markerStop = pos
+        del dc
+        self.log.log("Exit MapCanvas->on_tape_down(self, evt)", ORPG_DEBUG)
+
+    def on_tape_up(self, evt):
+        """When we release the middle button, disable any marking updates that we have been doing."""
+        self.log.log("Enter MapCanvas->on_tape_up(self, evt)", ORPG_DEBUG)
+        # If we are in measure mode, draw the actual UNIT distance
+        if self.markerMode == MARKER_MODE_MEASURE:
+            dc = wx.ClientDC( self )
+            self.PrepareDC( dc )
+            dc.SetUserScale(self.layers['grid'].mapscale,self.layers['grid'].mapscale)
+            # Draw the measured distance on the DC.  Since we want
+            # 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 )
+            midPoint = (self.markerStart + self.markerStop)
+            midPoint.x /= 2
+            midPoint.y /= 2
+            # Adjust out font to be bigger & scaled
+            font = dc.GetFont()
+            # Set the DC function that we need
+            dc.SetLogicalFunction(wx.INVERT)
+            # Set the pen we want to use
+            dc.SetPen(wx.BLACK_PEN)
+            # Now, draw the text at the proper angle on the canvas
+            self.markerMidpoint = midPoint
+            self.markerAngle = lineAngle
+            dText = "%0.2f Units" % (distance)
+            dc.BeginDrawing()
+            dc.DrawRotatedText( dText, midPoint.x, midPoint.y, lineAngle )
+            dc.EndDrawing()
+            # Restore the default font and DC
+            dc.SetFont(wx.NullFont)
+            dc.SetLogicalFunction(wx.COPY)
+            del font
+            del dc
+        self.markerMode = MARKER_MODE_NONE
+        self.log.log("Exit MapCanvas->on_tape_up(self, evt)", ORPG_DEBUG)
+
+    # MODE 1 = MOVE, MODE 2 = whiteboard, MODE 3 = Tape measure
+    def on_left_down(self, evt):
+        self.log.log("Enter MapCanvas->on_left_down(self, evt)", ORPG_DEBUG)
+        if evt.ShiftDown():
+            self.on_tape_down (evt)
+        else:
+            self.frame.on_left_down(evt)
+        self.log.log("Exit MapCanvas->on_left_down(self, evt)", ORPG_DEBUG)
+
+    def on_right_down(self, evt):
+        self.log.log("Enter MapCanvas->on_right_down(self, evt)", ORPG_DEBUG)
+        if evt.ShiftDown():
+            pass
+        else:
+            self.frame.on_right_down(evt)
+        self.log.log("Exit MapCanvas->on_right_down(self, evt)", ORPG_DEBUG)
+
+    def on_left_dclick(self, evt):
+        self.log.log("Enter MapCanvas->on_left_dclick(self, evt)", ORPG_DEBUG)
+        if evt.ShiftDown():
+            pass
+        else:
+            self.frame.on_left_dclick(evt)
+        self.log.log("Exit MapCanvas->on_left_dclick(self, evt)", ORPG_DEBUG)
+
+    def on_left_up(self, evt):
+        self.log.log("Enter MapCanvas->on_left_up(self, evt)", ORPG_DEBUG)
+        if evt.ShiftDown():
+            self.on_tape_up(evt)
+        elif open_rpg.get_component("tree").dragging:
+            tree = open_rpg.get_component("tree")
+            if tree.drag_obj.map_aware():
+                tree.drag_obj.on_send_to_map(evt)
+                tree.dragging = False
+                tree.drag_obj = None
+        else:
+            self.frame.on_left_up(evt)
+        self.log.log("Exit MapCanvas->on_left_up(self, evt)", ORPG_DEBUG)
+
+    def on_motion(self, evt):
+        self.log.log("Enter MapCanvas->on_motion(self, evt)", ORPG_DEBUG)
+        if evt.ShiftDown():
+            self.on_tape_motion(evt)
+        elif evt.LeftIsDown() and open_rpg.get_component("tree").dragging:
+            pass
+        else:
+            self.frame.on_motion(evt)
+        self.log.log("Exit MapCanvas->on_motion(self, evt)", ORPG_DEBUG)
+
+    def on_zoom_out(self, evt):
+        self.log.log("Enter MapCanvas->on_zoom_out(self, evt)", ORPG_DEBUG)
+        if self.layers['grid'].mapscale > 0.2:
+            # attempt to keep same logical point at center of screen
+            scale = self.layers['grid'].mapscale
+            scrollsize = self.GetScrollPixelsPerUnit()
+            clientsize = self.GetClientSize()
+            topleft1 = self.GetViewStart()
+            topleft = [topleft1[0]*scrollsize[0], topleft1[1]*scrollsize[1]]
+            scroll_x = (((topleft[0]+clientsize[0]/2)*(scale-.1)/scale)-clientsize[0]/2)/scrollsize[0]
+            scroll_y = (((topleft[1]+clientsize[1]/2)*(scale-.1)/scale)-clientsize[1]/2)/scrollsize[1]
+            self.Scroll(scroll_x, scroll_y)
+            self.layers['grid'].mapscale -= .1
+            scalestring = "x" + `self.layers['grid'].mapscale`[:3]
+            self.frame.get_current_layer_handler().zoom_out_button.SetToolTip(wx.ToolTip("Zoom out from " + scalestring) )
+            self.frame.get_current_layer_handler().zoom_in_button.SetToolTip(wx.ToolTip("Zoom in from " + scalestring) )
+            self.set_size(self.size)
+            dc = wx.ClientDC(self)
+            dc.BeginDrawing()
+            scalestring = "Scale x" + `self.layers['grid'].mapscale`[:3]
+            (textWidth,textHeight) = dc.GetTextExtent(scalestring)
+            dc.SetPen(wx.LIGHT_GREY_PEN)
+            dc.SetBrush(wx.LIGHT_GREY_BRUSH)
+            dc.DrawRectangle(dc.DeviceToLogicalX(0),dc.DeviceToLogicalY(0),textWidth,textHeight)
+            dc.SetPen(wx.RED_PEN)
+            dc.DrawText(scalestring,dc.DeviceToLogicalX(0),dc.DeviceToLogicalY(0))
+            dc.SetPen(wx.NullPen)
+            dc.SetBrush(wx.NullBrush)
+            dc.EndDrawing()
+            del dc
+            self.zoom_display_timer.Start(500,1)
+        self.log.log("Exit MapCanvas->on_zoom_out(self, evt)", ORPG_DEBUG)
+
+    def on_zoom_in(self, evt):
+        self.log.log("Enter MapCanvas->on_zoom_in(self, evt)", ORPG_DEBUG)
+        # attempt to keep same logical point at center of screen
+        scale = self.layers['grid'].mapscale
+        scrollsize = self.GetScrollPixelsPerUnit()
+        clientsize = self.GetClientSize()
+        topleft1 = self.GetViewStart()
+        topleft = [topleft1[0]*scrollsize[0], topleft1[1]*scrollsize[1]]
+        scroll_x = (((topleft[0]+clientsize[0]/2)*(scale+.1)/scale)-clientsize[0]/2)/scrollsize[0]
+        scroll_y = (((topleft[1]+clientsize[1]/2)*(scale+.1)/scale)-clientsize[1]/2)/scrollsize[1]
+        self.Scroll(scroll_x, scroll_y)
+        self.layers['grid'].mapscale += .1
+        scalestring = "x" + `self.layers['grid'].mapscale`[:3]
+        self.frame.get_current_layer_handler().zoom_out_button.SetToolTip(wx.ToolTip("Zoom out from " + scalestring) )
+        self.frame.get_current_layer_handler().zoom_in_button.SetToolTip(wx.ToolTip("Zoom in from " + scalestring) )
+        self.set_size(self.size)
+        dc = wx.ClientDC(self)
+        dc.BeginDrawing()
+        scalestring = "Scale x" + `self.layers['grid'].mapscale`[:3]
+        (textWidth,textHeight) = dc.GetTextExtent(scalestring)
+        dc.SetPen(wx.LIGHT_GREY_PEN)
+        dc.SetBrush(wx.LIGHT_GREY_BRUSH)
+        dc.DrawRectangle(dc.DeviceToLogicalX(0), dc.DeviceToLogicalY(0), textWidth,textHeight)
+        dc.SetPen(wx.RED_PEN)
+        dc.DrawText(scalestring, dc.DeviceToLogicalX(0), dc.DeviceToLogicalY(0))
+        dc.SetPen(wx.NullPen)
+        dc.SetBrush(wx.NullBrush)
+        dc.EndDrawing()
+        del dc
+        self.zoom_display_timer.Start(500, 1)
+        self.log.log("Exit MapCanvas->on_zoom_in(self, evt)", ORPG_DEBUG)
+
+    def on_prop(self, evt):
+        self.log.log("Enter MapCanvas->on_prop(self, evt)", ORPG_DEBUG)
+        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")
+            self.log.log("Exit MapCanvas->on_prop(self, evt)", ORPG_DEBUG)
+            return
+        dlg = general_map_prop_dialog(self.frame.GetParent(),self.size,self.layers['bg'],self.layers['grid'])
+        if dlg.ShowModal() == wx.ID_OK:
+            self.set_size(dlg.size)
+            self.send_map_data()
+            self.Refresh(False)
+        dlg.Destroy()
+        os.chdir(self.root_dir)
+        self.log.log("Exit MapCanvas->on_prop(self, evt)", ORPG_DEBUG)
+
+    def add_miniature(self, min_url, min_label='', min_unique=-1):
+        self.log.log("Enter MapCanvas->add_miniature(self, min_url, min_label, min_unique)", ORPG_DEBUG)
+        if min_unique == -1:
+            min_unique = not self.use_serial
+        if min_url == "" or min_url == "http://":
+            return
+        if min_url[:7] != "http://" :
+            min_url = "http://" + min_url
+        # make label
+        wx.BeginBusyCursor()
+        if self.auto_label:
+            if min_label == '':
+                min_label = self.get_label_from_url( min_url )
+            if not min_unique and self.use_serial:
+                min_label = '%s %d' % ( min_label, self.layers['miniatures'].next_serial() )
+        else:
+            min_label = ""
+        if self.frame.min_url.FindString(min_url) == -1:
+            self.frame.min_url.Append(min_url)
+        try:
+            id = 'mini-' + self.frame.session.get_next_id()
+            self.layers['miniatures'].add_miniature(id, min_url, label=min_label)
+        except Exception, e:
+            self.log.log(traceback.format_exc(), ORPG_GENERAL)
+            self.log.log("Unable to load/resolve URL: " + min_url + " on resource ' + min_label + ' !!!", ORPG_GENERAL)
+            self.layers['miniatures'].rollback_serial()
+        wx.EndBusyCursor()
+        self.send_map_data()
+        self.Refresh(False)
+        self.log.log("Exit MapCanvas->add_miniature(self, min_url, min_label, min_unique)", ORPG_DEBUG)
+
+    def get_label_from_url(self, url=''):
+        self.log.log("Enter MapCanvas->get_label_from_url(self, url)", ORPG_DEBUG)
+        if url == '':
+            self.log.log("Exit MapCanvas->get_label_from_url(self, url)", ORPG_DEBUG)
+            return ''
+        start = url.rfind("/")+1
+        label = url[start:len(url)-4]
+        self.log.log("Exit MapCanvas->get_label_from_url(self, url)", ORPG_DEBUG)
+        return label
+
+    def toxml(self, action="update"):
+        self.log.log("Enter MapCanvas->toxml(self, " + action + ")", ORPG_DEBUG)
+        if action == "new":
+            self.size_changed = 1
+        xml_str = "<map version='" + self.map_version + "'"
+        changed = self.size_changed
+        if self.size_changed:
+            xml_str += " sizex='" + str(self.size[0]) + "'"
+            xml_str += " sizey='" + str(self.size[1]) + "'"
+        s = ""
+        keys = self.layers.keys()
+        for k in keys:
+            if (k != "fog" or action != "update"):
+                s += self.layers[k].layerToXML(action)
+        self.size_changed = 0
+        if s:
+            self.log.log("Exit MapCanvas->toxml(self, " + action + ")", ORPG_DEBUG)
+            return xml_str + " action='" + action + "'>" + s + "</map>"
+        else:
+            if changed:
+                self.log.log("Exit MapCanvas->toxml(self, " + action + ")", ORPG_DEBUG)
+                return xml_str + " action='" + action + "'/>"
+            else:
+                self.log.log("Exit MapCanvas->toxml(self, " + action + ")", ORPG_DEBUG)
+                return ""
+
+    def takexml(self, xml):
+        #
+        # Added Process Dialog to display during long map parsings
+        # as well as a try block with an exception traceback to try
+        # and isolate some of the map related problems users have been
+        # experiencing --Snowdog 5/15/03
+        #
+        # Apparently Process Dialog causes problems with linux.. commenting it out. sheez.
+        #  --Snowdog 5/27/03
+        self.log.log("Enter MapCanvas->takexml(self, xml)", ORPG_DEBUG)
+        try:
+            #parse the map DOM
+            xml_dom = parseXml(xml)
+            if xml_dom == None:
+                self.log.log("xml_dom == None\n" + xml, ORPG_INFO)
+                self.log.log("Exit MapCanvas->takexml(self, xml)", ORPG_DEBUG)
+                return
+            node_list = xml_dom.getElementsByTagName("map")
+            if len(node_list) < 1:
+                self.log.log("Invalid XML format for mapper", ORPG_INFO)
+            else:
+                # set map version to incoming data so layers can convert
+                self.map_version = node_list[0].getAttribute("version")
+                action = node_list[0].getAttribute("action")
+                if action == "new":
+                    self.layers = {}
+                    try:
+                        self.layers['bg'] = layer_back_ground(self)
+                    except:
+                        pass
+                    try:
+                        self.layers['grid'] = grid_layer(self)
+                    except:
+                        pass
+                    try:
+                        self.layers['miniatures'] = miniature_layer(self)
+                    except:
+                        pass
+                    try:
+                        self.layers['whiteboard'] = whiteboard_layer(self)
+                    except:
+                        pass
+                    try:
+                        self.layers['fog'] = fog_layer(self)
+                    except:
+                        pass
+                sizex = node_list[0].getAttribute("sizex")
+                if sizex != "":
+                    sizex = int(float(sizex))
+                    sizey = self.size[1]
+                    self.set_size((sizex,sizey))
+                    self.size_changed = 0
+                sizey = node_list[0].getAttribute("sizey")
+                if sizey != "":
+                    sizey = int(float(sizey))
+                    sizex = self.size[0]
+                    self.set_size((sizex,sizey))
+                    self.size_changed = 0
+                children = node_list[0]._get_childNodes()
+                #fog layer must be computed first, so that no data is inadvertently revealed
+                for c in children:
+                    name = c._get_nodeName()
+                    if name == "fog":
+                        self.layers[name].layerTakeDOM(c)
+                for c in children:
+                    name = c._get_nodeName()
+                    if name != "fog":
+                        self.layers[name].layerTakeDOM(c)
+                # all map data should be converted, set map version to current version
+                self.map_version = MAP_VERSION
+                self.Refresh(False)
+            xml_dom.unlink()  # eliminate circular refs
+        except:
+            self.log.log(traceback.format_exc(), ORPG_GENERAL)
+            self.log.log("EXCEPTION: Critical Error Loading Map!!!", ORPG_GENERAL)
+        self.log.log("Exit MapCanvas->takexml(self, xml)", ORPG_DEBUG)
+
+    def re_ids_in_xml(self, xml):
+        self.log.log("Enter MapCanvas->re_ids_in_xml(self, xml)", ORPG_DEBUG)
+        new_xml = ""
+        tmp_map = map_msg()
+        xml_dom = parseXml(str(xml))
+        node_list = xml_dom.getElementsByTagName("map")
+        if len(node_list) < 1:
+            self.log.log("Invalid XML format for mapper", ORPG_INFO)
+        else:
+            tmp_map.init_from_dom(node_list[0])
+            if tmp_map.children.has_key("miniatures"):
+                miniatures_layer = tmp_map.children["miniatures"]
+                if miniatures_layer:
+                    minis = miniatures_layer.get_children().keys()
+                    if minis:
+                        for mini in minis:
+                            m = miniatures_layer.children[mini]
+                            id = 'mini-' + self.frame.session.get_next_id()
+                            m.init_prop("id", id)
+            # This allows for backward compatibility with older maps which do not
+            # have a whiteboard node.  As such, if it's not there, we'll just happily
+            # move on and process as always.
+            if tmp_map.children.has_key("whiteboard"):
+                whiteboard_layer = tmp_map.children["whiteboard"]
+                if whiteboard_layer:
+                    lines = whiteboard_layer.get_children().keys()
+                    if lines:
+                        for line in lines:
+                            l = whiteboard_layer.children[line]
+                            if l.tagname == 'line':
+                                id = 'line-' + self.frame.session.get_next_id()
+                            elif l.tagname == 'text':
+                                id = 'text-' + self.frame.session.get_next_id()
+                            elif l.tagname == 'circle':
+                                id = 'circle-' + self.frame.session.get_next_id()
+                            l.init_prop("id", id)
+            new_xml = tmp_map.get_all_xml()
+        if xml_dom:
+            xml_dom.unlink()
+        self.log.log("Exit MapCanvas->re_ids_in_xml(self, xml)", ORPG_DEBUG)
+        return str(new_xml)
+
+class map_wnd(wx.Panel):
+    def __init__(self, parent, id):
+        self.log = open_rpg.get_component('log')
+        self.log.log("Enter map_wnd", ORPG_DEBUG)
+        wx.Panel.__init__(self, parent, id)
+        self.canvas = MapCanvas(self, -1)
+        self.session = open_rpg.get_component('session')
+        self.settings = open_rpg.get_component('settings')
+        self.chat = open_rpg.get_component('chat')
+        self.top_frame = open_rpg.get_component('frame')
+        self.root_dir = os.getcwd()
+        self.current_layer = 2
+        self.layer_tabs = orpgTabberWnd(self, style=FNB.FNB_NO_X_BUTTON|FNB.FNB_BOTTOM|FNB.FNB_NO_NAV_BUTTONS)
+        self.layer_handlers = []
+        self.layer_handlers.append(background_handler(self.layer_tabs,-1,self.canvas))
+        self.layer_tabs.AddPage(self.layer_handlers[0],"Background")
+        self.layer_handlers.append(grid_handler(self.layer_tabs,-1,self.canvas))
+        self.layer_tabs.AddPage(self.layer_handlers[1],"Grid")
+        self.layer_handlers.append(miniatures_handler(self.layer_tabs,-1,self.canvas))
+        self.layer_tabs.AddPage(self.layer_handlers[2],"Miniatures", True)
+        self.layer_handlers.append(whiteboard_handler(self.layer_tabs,-1,self.canvas))
+        self.layer_tabs.AddPage(self.layer_handlers[3],"Whiteboard")
+        self.layer_handlers.append(fog_handler(self.layer_tabs,-1,self.canvas))
+        self.layer_tabs.AddPage(self.layer_handlers[4],"Fog")
+        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.Bind(FNB.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.on_layer_change)
+        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
+        self.log.log("Exit map_wnd", ORPG_DEBUG)
+
+    def OnLeave(self, evt):
+        if "__WXGTK__" in wx.PlatformInfo:
+            wx.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))
+
+    def load_default(self):
+        self.log.log("Enter map_wnd->load_default(self)", ORPG_DEBUG)
+        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")
+            self.log.log("Exit map_wnd->load_default(self)", ORPG_DEBUG)
+            return
+        f = open(orpg.dirpath.dir_struct["template"] + "default_map.xml")
+        self.new_data(f.read())
+        f.close()
+        self.canvas.send_map_data("new")
+        if not self.session.is_connected() and (self.session.my_role() != self.session.ROLE_GM):
+            self.session.update_role("GM")
+        self.log.log("Exit map_wnd->load_default(self)", ORPG_DEBUG)
+
+    def new_data(self, data):
+        self.log.log("Enter map_wnd->new_data(self, data)", ORPG_DEBUG)
+        self.canvas.takexml(data)
+        self.update_tools()
+        self.log.log("Exit map_wnd->new_data(self, data)", ORPG_DEBUG)
+
+    def on_save(self,evt):
+        self.log.log("Enter map_wnd->new_data(self, data)", ORPG_DEBUG)
+        if (self.session.my_role() != self.session.ROLE_GM):
+            self.chat.InfoPost("You must be a GM to use this feature")
+            self.log.log("Exit map_wnd->new_data(self, data)", ORPG_DEBUG)
+            return
+        d = wx.FileDialog(self.GetParent(), "Save map data", orpg.dirpath.dir_struct["user"], "", "*.xml", wx.SAVE)
+        if d.ShowModal() == wx.ID_OK:
+            f = open(d.GetPath(), "w")
+            data = '<nodehandler class="min_map" icon="compass" module="core" name="miniature Map">'
+            data += self.canvas.toxml("new")
+            data += "</nodehandler>"
+            data = data.replace(">",">\n")
+            f.write(data)
+            f.close()
+        d.Destroy()
+        os.chdir(self.root_dir)
+        self.log.log("Exit map_wnd->new_data(self, data)", ORPG_DEBUG)
+
+    def on_open(self, evt):
+        self.log.log("Enter map_wnd->on_open(self, evt)", ORPG_DEBUG)
+        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")
+            self.log.log("Exit map_wnd->on_open(self, evt)", ORPG_DEBUG)
+            return
+        d = wx.FileDialog(self.GetParent(), "Select a file", orpg.dirpath.dir_struct["user"], "", "*.xml", wx.OPEN)
+        if d.ShowModal() == wx.ID_OK:
+            f = open(d.GetPath())
+            map_string = f.read()
+            new_xml = self.canvas.re_ids_in_xml(map_string)
+            if new_xml:
+                self.canvas.takexml(new_xml)
+                self.canvas.send_map_data("new")
+                self.update_tools()
+                if not self.session.is_connected() and (self.session.my_role() != self.session.ROLE_GM):
+                    self.session.update_role("GM")
+        d.Destroy()
+        os.chdir(self.root_dir)
+        self.log.log("Exit map_wnd->on_open(self, evt)", ORPG_DEBUG)
+
+    def get_current_layer_handler(self):
+        self.log.log("Enter map_wnd->get_current_layer_handler(self)", ORPG_DEBUG)
+        self.log.log("Exit map_wnd->get_current_layer_handler(self)", ORPG_DEBUG)
+        return self.layer_handlers[self.current_layer]
+
+    def get_tab_index(self, layer):
+        """Return the index of a chatpanel in the wxNotebook."""
+        self.log.log("Enter map_wnd->get_tab_index(self, layer)", ORPG_DEBUG)
+        for i in xrange(self.layer_tabs.GetPageCount()):
+            if (self.layer_tabs.GetPageText(i) == layer):
+                self.log.log("Exit map_wnd->get_tab_index(self, layer) return " + str(i), ORPG_DEBUG)
+                return i
+        self.log.log("Exit map_wnd->get_tab_index(self, layer) return 0", ORPG_DEBUG)
+        return 0
+
+    def on_layer_change(self, evt):
+        self.log.log("Enter map_wnd->on_layer_change(self, evt)", ORPG_DEBUG)
+        layer = self.layer_tabs.GetPage(evt.GetSelection())
+        for i in xrange(0, len(self.layer_handlers)):
+            if layer == self.layer_handlers[i]:
+                self.current_layer = i
+        if self.current_layer == 0:
+            bg = self.layer_handlers[0]
+            if (self.session.my_role() != self.session.ROLE_GM):
+                bg.url_path.Show(False)
+            else:
+                bg.url_path.Show(True)
+        self.canvas.Refresh(False)
+        evt.Skip()
+        self.log.log("Exit map_wnd->on_layer_change(self, evt)", ORPG_DEBUG)
+
+    def on_left_down(self, evt):
+        self.log.log("Enter map_wnd->on_left_down(self, evt)", ORPG_DEBUG)
+        self.log.log("Exit map_wnd->on_left_down(self, evt)", ORPG_DEBUG)
+        self.layer_handlers[self.current_layer].on_left_down(evt)
+
+    #double click handler added by Snowdog 5/03
+    def on_left_dclick(self, evt):
+        self.log.log("Enter map_wnd->on_left_dclick(self, evt)", ORPG_DEBUG)
+        self.log.log("Exit map_wnd->on_left_dclick(self, evt)", ORPG_DEBUG)
+        self.layer_handlers[self.current_layer].on_left_dclick(evt)
+
+    def on_right_down(self, evt):
+        self.log.log("Enter map_wnd->on_right_down(self, evt)", ORPG_DEBUG)
+        self.log.log("Exit map_wnd->on_right_down(self, evt)", ORPG_DEBUG)
+        self.layer_handlers[self.current_layer].on_right_down(evt)
+
+    def on_left_up(self, evt):
+        self.log.log("Enter map_wnd->on_left_up(self, evt)", ORPG_DEBUG)
+        self.log.log("Exit map_wnd->on_left_up(self, evt)", ORPG_DEBUG)
+        self.layer_handlers[self.current_layer].on_left_up(evt)
+
+    def on_motion(self, evt):
+        self.log.log("Enter map_wnd->on_motion(self, evt)", ORPG_DEBUG)
+        self.log.log("Exit map_wnd->on_motion(self, evt)", ORPG_DEBUG)
+        self.layer_handlers[self.current_layer].on_motion(evt)
+
+    def MapBar(self, id, data):
+        self.log.log("Enter map_wnd->MapBar(self, id, data)", ORPG_DEBUG)
+        self.canvas.MAP_MODE = data
+        if id == 1:
+            self.canvas.MAP_MODE = data
+        self.log.log("Exit map_wnd->MapBar(self, id, data)", ORPG_DEBUG)
+
+    def set_map_focus(self, evt):
+        self.log.log("Enter map_wnd->set_map_focus(self, evt)", ORPG_DEBUG)
+        self.canvas.SetFocus()
+        self.log.log("Exit map_wnd->set_map_focus(self, evt)", ORPG_DEBUG)
+
+    def pre_exit_cleanup(self):
+        self.log.log("Enter map_wnd->pre_exit_cleanup(self)", ORPG_DEBUG)
+        # do some pre exit clean up for bitmaps or other objects
+        try:
+            ImageHandler.flushCache()
+            self.canvas.pre_destory_cleanup()
+        except Exception, e:
+            self.log.log(traceback.format_exc(), ORPG_CRITICAL)
+            self.log.log("EXCEPTION: " + str(e), ORPG_CRITICAL)
+        self.log.log("Exit map_wnd->pre_exit_cleanup(self)", ORPG_DEBUG)
+
+    def update_tools(self):
+        self.log.log("Enter map_wnd->update_tools(self)", ORPG_DEBUG)
+        for h in self.layer_handlers:
+            h.update_info()
+        self.log.log("Exit map_wnd->update_tools(self)", ORPG_DEBUG)
+
+    def on_hk_map_layer(self, evt):
+        self.log.log("Enter map_wnd->on_hk_map_layer(self, evt)", ORPG_DEBUG)
+        id = self.top_frame.mainmenu.GetHelpString(evt.GetId())
+        #print evt.GetMenu().GetTitle()
+        if id == "Background Layer":
+            self.current_layer = self.get_tab_index("Background")
+        if id == "Grid Layer":
+            self.current_layer = self.get_tab_index("Grid")
+        if id == "Miniature Layer":
+            self.current_layer = self.get_tab_index("Miniatures")
+        elif id == "Whiteboard Layer":
+            self.current_layer = self.get_tab_index("Whiteboard")
+        elif id == "Fog Layer":
+            self.current_layer = self.get_tab_index("Fog")
+        elif id == "General Properties":
+            self.current_layer = self.get_tab_index("General")
+        self.layer_tabs.SetSelection(self.current_layer)
+        self.log.log("Exit map_wnd->on_hk_map_layer(self, evt)", ORPG_DEBUG)
+
+    def on_flush_cache(self, evt):
+        self.log.log("Enter map_wnd->on_flush_cache(self, evt)", ORPG_DEBUG)
+        ImageHandler.flushCache()
+        self.log.log("Exit map_wnd->on_flush_cache(self, evt)", ORPG_DEBUG)
+
+    def build_menu(self):
+        self.log.log("Enter map_wnd->build_menu(self)", ORPG_DEBUG)
+        # temp menu
+        menu = wx.Menu()
+        item = wx.MenuItem(menu, wx.ID_ANY, "&Load Map", "Load Map")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_open, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "&Save Map", "Save Map")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_save, item)
+        menu.AppendItem(item)
+        menu.AppendSeparator()
+        item = wx.MenuItem(menu, wx.ID_ANY, "Background Layer\tCtrl+1", "Background Layer")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_hk_map_layer, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "Grid Layer\tCtrl+2", "Grid Layer")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_hk_map_layer, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "Miniature Layer\tCtrl+3", "Miniature Layer")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_hk_map_layer, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "Whiteboard Layer\tCtrl+4", "Whiteboard Layer")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_hk_map_layer, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "Fog Layer\tCtrl+5", "Fog Layer")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_hk_map_layer, item)
+        menu.AppendItem(item)
+        item = wx.MenuItem(menu, wx.ID_ANY, "General Properties\tCtrl+6", "General Properties")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_hk_map_layer, item)
+        menu.AppendItem(item)
+        menu.AppendSeparator()
+        item = wx.MenuItem(menu, wx.ID_ANY, "&Flush Image Cache\tCtrl+F", "Flush Image Cache")
+        self.top_frame.Bind(wx.EVT_MENU, self.on_flush_cache, item)
+        menu.AppendItem(item)
+        menu.AppendSeparator()
+        item = wx.MenuItem(menu, wx.ID_ANY, "&Properties", "Properties")
+        self.top_frame.Bind(wx.EVT_MENU, self.canvas.on_prop, item)
+        menu.AppendItem(item)
+        self.top_frame.mainmenu.Insert(2, menu, '&Map')
+        self.log.log("Exit map_wnd->build_menu(self)", ORPG_DEBUG)
+
+    def get_hot_keys(self):
+        self.log.log("Enter map_wnd->get_hot_keys(self)", ORPG_DEBUG)
+        self.build_menu()
+        self.log.log("Exit map_wnd->get_hot_keys(self)", ORPG_DEBUG)
+        return []
+
+    def on_size(self, evt):
+        self.log.log("Enter map_wnd->on_size(self, evt)", ORPG_DEBUG)
+        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.log.log("Exit map_wnd->on_size(self, evt)", ORPG_DEBUG)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/map_handler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,80 @@
+#
+#    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: orpg/mapper/map_handler.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: map_handler.py,v 1.14 2007/04/03 00:14:35 digitalxero Exp $
+#
+# Description: map layer handler
+#
+__version__ = "$Id: map_handler.py,v 1.14 2007/04/03 00:14:35 digitalxero Exp $"
+
+from base_handler import *
+
+class map_handler(base_layer_handler):
+    def __init__(self, parent, id, canvas):
+        base_layer_handler.__init__(self, parent, id, canvas)
+
+    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.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.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()
+
+    def update_info(self):
+        size = self.canvas.get_size()
+        self.width.SetValue(str(size[0]))
+        self.height.SetValue(str(size[1]))
+
+    def build_menu(self,label = "Grid"):
+        base_layer_handler.build_menu(self,label)
+
+    def on_load_default(self, evt):
+        self.map_frame.load_default()
+
+    def on_apply(self, evt):
+        session=self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM):
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            return
+        try:
+            size = (int(self.width.GetValue()),int(self.height.GetValue()))
+        except:
+            wx.MessageBox("Invalide Map Size!","Map Properties")
+            return
+        self.canvas.set_size(size)
+        self.update_info()
+        self.canvas.send_map_data()
+        self.canvas.Refresh()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/map_msg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,133 @@
+# 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: mapper/map_msg.py
+# Author: OpenRPG
+# Maintainer:
+# Version:
+#   $Id: map_msg.py,v 1.16 2007/03/09 14:11:55 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: map_msg.py,v 1.16 2007/03/09 14:11:55 digitalxero Exp $"
+
+#from base import *
+from base_msg import *
+from background_msg import *
+from grid_msg import *
+from miniatures_msg import *
+from whiteboard_msg import *
+from fog_msg import *
+
+"""
+<map name=? id=? >
+    <bg type=? file=? color=? />
+    <grid size=? snap=? />
+    <miniatures serial=? >
+        <miniature  path=?  posx=?  posy=?  heading=? face=? owner=? label=? locked=? width=? height=? />
+    </miniatures>
+</map>
+
+"""
+
+class map_msg(map_element_msg_base):
+
+    def __init__(self,reentrant_lock_object = None):
+        self.tagname = "map"
+        map_element_msg_base.__init__(self,reentrant_lock_object)
+
+    def init_from_dom(self,xml_dom):
+        self.p_lock.acquire()
+        if xml_dom.tagName == 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 xml_dom.hasAttribute("action") and xml_dom.getAttribute("action") == "new":
+                self.clear()
+            # Process all of the properties in each tag
+            if xml_dom.getAttributeKeys():
+                for k in xml_dom.getAttributeKeys():
+                    self.init_prop(k,xml_dom.getAttribute(k))
+            for c in xml_dom._get_childNodes():
+                name = c._get_nodeName()
+                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
+        else:
+            self.p_lock.release()
+            raise Exception, "Error attempting to initialize a " + self.tagname + " from a non-<" + self.tagname + "/> element"
+        self.p_lock.release()
+
+    def set_from_dom(self,xml_dom):
+        self.p_lock.acquire()
+        if xml_dom.tagName == 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 xml_dom.hasAttribute("action") and xml_dom.getAttribute("action") == "new":
+                self.clear()
+            # Process all of the properties in each tag
+            if xml_dom.getAttributeKeys():
+                for k in xml_dom.getAttributeKeys():
+                    self.set_prop(k,xml_dom.getAttribute(k))
+            for c in xml_dom._get_childNodes():
+                name = c._get_nodeName()
+                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
+        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()
+
+    def get_all_xml(self, action="new", output_action=1):
+        return map_element_msg_base.get_all_xml(self, action, output_action)
+
+    def get_changed_xml(self, action="update", output_action=1):
+        return map_element_msg_base.get_changed_xml(self, action, output_action)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/map_prop_dialog.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,240 @@
+# 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: mapper/map_prop_dialog.py
+# Author: OpenRPG
+# Maintainer:
+# Version:
+#   $Id: map_prop_dialog.py,v 1.16 2006/11/04 21:24:21 digitalxero Exp $
+#
+# Description:
+#
+__version__ = "$Id: map_prop_dialog.py,v 1.16 2006/11/04 21:24:21 digitalxero Exp $"
+
+from orpg.orpg_windows import *
+from background import *
+from grid import *
+from miniatures import *
+from whiteboard import *
+
+##-----------------------------
+## map prop dialog
+##-----------------------------
+
+CTRL_WIDTH = wx.NewId()
+CTRL_HEIGHT = wx.NewId()
+CTRL_BG_COLOR = wx.NewId()
+CTRL_BG_COLOR_VALUE = wx.NewId()
+CTRL_TEXTURE = wx.NewId()
+CTRL_TEXTURE_PATH = wx.NewId()
+CTRL_IMAGE = wx.NewId()
+CTRL_IMAGE_PATH  = wx.NewId()
+CTRL_GRID = wx.NewId()
+CTRL_GRID_SNAP = wx.NewId()
+CTRL_GRID_COLOR = wx.NewId()
+CTRL_GRID_MODE_RECT = wx.NewId()
+CTRL_GRID_MODE_HEX = wx.NewId()
+CTRL_GRID_LINE_NONE = wx.NewId()
+CTRL_GRID_LINE_DOTTED = wx.NewId()
+CTRL_GRID_LINE_SOLID = wx.NewId()
+
+class general_map_prop_dialog(wx.Dialog):
+    def __init__(self,parent,size,bg_layer,grid_layer):
+        wx.Dialog.__init__(self,parent,-1,"General Map Properties",wx.DefaultPosition,wx.Size(425,405))
+        self.size = size
+        self.bg_layer = bg_layer
+        self.grid_layer = grid_layer
+        #build controls
+        self.ctrls = {  CTRL_WIDTH : wx.TextCtrl(self, CTRL_WIDTH, str(size[0])),
+                        CTRL_HEIGHT : wx.TextCtrl(self, CTRL_HEIGHT, str(size[1])),
+                        CTRL_BG_COLOR : wx.RadioButton(self, CTRL_BG_COLOR, "Color", style=wx.RB_GROUP),
+                        CTRL_BG_COLOR_VALUE : wx.Button(self, CTRL_BG_COLOR_VALUE, "Color"),
+                        CTRL_TEXTURE : wx.RadioButton(self, CTRL_TEXTURE, "Texture"),
+                        CTRL_TEXTURE_PATH: wx.TextCtrl(self, CTRL_TEXTURE_PATH,"http://"),
+                        CTRL_IMAGE : wx.RadioButton(self, CTRL_IMAGE, "Image"),
+                        CTRL_IMAGE_PATH :wx.TextCtrl(self, CTRL_IMAGE_PATH,"http://"),
+                        CTRL_GRID : wx.TextCtrl(self, CTRL_GRID),
+                        CTRL_GRID_SNAP : wx.CheckBox(self, CTRL_GRID_SNAP, " Snap to grid"),
+                        CTRL_GRID_COLOR : wx.Button(self, CTRL_GRID_COLOR, "Grid Color"),
+                        CTRL_GRID_MODE_RECT : wx.RadioButton(self, CTRL_GRID_MODE_RECT, "Rectangular", style=wx.RB_GROUP),
+                        CTRL_GRID_MODE_HEX : wx.RadioButton(self, CTRL_GRID_MODE_HEX, "Hexagonal"),
+                        CTRL_GRID_LINE_NONE : wx.RadioButton(self, CTRL_GRID_LINE_NONE, "No Lines", style=wx.RB_GROUP),
+                        CTRL_GRID_LINE_DOTTED : wx.RadioButton(self, CTRL_GRID_LINE_DOTTED, "Dotted Lines"),
+                        CTRL_GRID_LINE_SOLID : wx.RadioButton(self, CTRL_GRID_LINE_SOLID, "Solid Lines")
+                     }
+        # set values of bg controls
+        self.ctrls[CTRL_BG_COLOR].SetValue(False)
+        self.ctrls[CTRL_TEXTURE].SetValue(False)
+        self.ctrls[CTRL_IMAGE].SetValue(False)
+
+        # Begin ted's changes for map bg persistency.
+        if bg_layer.bg_color != None:
+            self.ctrls[CTRL_BG_COLOR_VALUE].SetBackgroundColour(bg_layer.bg_color)
+        if bg_layer.img_path != None:
+            self.ctrls[CTRL_TEXTURE_PATH].SetValue(bg_layer.img_path)
+            self.ctrls[CTRL_IMAGE_PATH].SetValue(bg_layer.img_path)
+        # End ted's changes
+
+        if bg_layer.type == BG_COLOR:
+            self.ctrls[CTRL_BG_COLOR].SetValue(True)
+            # self.ctrls[CTRL_BG_COLOR_VALUE].SetBackgroundColour(bg_layer.bg_color)
+        elif bg_layer.type == BG_TEXTURE:
+            self.ctrls[CTRL_TEXTURE].SetValue(True)
+            # self.ctrls[CTRL_TEXTURE_PATH].SetValue(bg_layer.img_path)
+        elif bg_layer.type == BG_IMAGE:
+            self.ctrls[CTRL_WIDTH].Enable(False)
+            self.ctrls[CTRL_HEIGHT].Enable(False)
+            self.ctrls[CTRL_IMAGE].SetValue(True)
+            # self.ctrls[CTRL_IMAGE_PATH].SetValue(bg_layer.img_path)
+
+        # set grid layer control values
+        self.ctrls[CTRL_GRID].SetValue(str(grid_layer.unit_size))
+        self.ctrls[CTRL_GRID_SNAP].SetValue(grid_layer.snap)
+        self.ctrls[CTRL_GRID_COLOR].SetBackgroundColour(grid_layer.color)
+        self.ctrls[CTRL_GRID_MODE_RECT].SetValue(grid_layer.mode == GRID_RECTANGLE)
+        self.ctrls[CTRL_GRID_MODE_HEX].SetValue(grid_layer.mode == GRID_HEXAGON)
+        self.ctrls[CTRL_GRID_LINE_NONE].SetValue(grid_layer.line == LINE_NONE)
+        self.ctrls[CTRL_GRID_LINE_DOTTED].SetValue(grid_layer.line == LINE_DOTTED)
+        self.ctrls[CTRL_GRID_LINE_SOLID].SetValue(grid_layer.line == LINE_SOLID)
+
+        #create sizers
+        sizers = {}
+        sizers['main'] = wx.BoxSizer(wx.VERTICAL)
+
+        #size
+        sizers['size'] = wx.StaticBoxSizer(wx.StaticBox(self,-1,"Size"), wx.HORIZONTAL)
+        sizers['size'].Add(wx.StaticText(self, -1, "Width: "), 0, wx.ALIGN_CENTER)
+        sizers['size'].Add(self.ctrls[CTRL_WIDTH], 0, wx.ALIGN_CENTER)
+        sizers['size'].Add(wx.Size(20,25))
+        sizers['size'].Add(wx.StaticText(self, -1, "Height: "), 0, wx.ALIGN_CENTER)
+        sizers['size'].Add(self.ctrls[CTRL_HEIGHT], 0, wx.ALIGN_CENTER)
+
+        #bg
+        sizers['bg'] = wx.StaticBoxSizer(wx.StaticBox(self,-1,"Background"), wx.HORIZONTAL)
+        sizers['bg_layout'] = wx.FlexGridSizer(3, 2,10,10)
+        sizers['bg_layout'].AddMany([(self.ctrls[CTRL_BG_COLOR],0,wx.EXPAND),
+                              (self.ctrls[CTRL_BG_COLOR_VALUE],1,wx.EXPAND),
+                              (self.ctrls[CTRL_TEXTURE],0,wx.EXPAND),
+                              (self.ctrls[CTRL_TEXTURE_PATH],1,wx.EXPAND),
+                              (self.ctrls[CTRL_IMAGE],0,wx.EXPAND),
+                              (self.ctrls[CTRL_IMAGE_PATH],1,wx.EXPAND)
+                            ])
+        sizers['bg_layout'].AddGrowableCol(1)
+        sizers['bg'].Add(sizers['bg_layout'], 0, wx.EXPAND)
+
+        #grid
+        sizers['grid'] = wx.StaticBoxSizer(wx.StaticBox(self,-1,"Grid"), wx.HORIZONTAL)
+        sizers['grid_layout'] = wx.FlexGridSizer(2, 3,10,10)
+        sizers['grid_layout'].AddMany([(wx.StaticText(self, -1, "Pixels per Square: "),2,wx.ALIGN_CENTER),
+                              (self.ctrls[CTRL_GRID],1,wx.EXPAND),
+                              (self.ctrls[CTRL_GRID_COLOR],1,wx.EXPAND),
+                              (self.ctrls[CTRL_GRID_SNAP],2,wx.EXPAND),
+                              (self.ctrls[CTRL_GRID_MODE_RECT],1,wx.EXPAND),
+                              (self.ctrls[CTRL_GRID_MODE_HEX],1,wx.EXPAND),
+                              (self.ctrls[CTRL_GRID_LINE_NONE],1,wx.EXPAND),
+                              (self.ctrls[CTRL_GRID_LINE_DOTTED],1,wx.EXPAND),
+                              (self.ctrls[CTRL_GRID_LINE_SOLID],1,wx.EXPAND)
+                            ])
+        sizers['grid'].Add(sizers['grid_layout'], 0, wx.EXPAND)
+
+        # buttons
+        sizers['but'] = wx.BoxSizer(wx.HORIZONTAL)
+        sizers['but'].Add(wx.Button(self, wx.ID_OK, "Apply"), 1, wx.EXPAND)
+        sizers['but'].Add(wx.Size(10,10))
+        sizers['but'].Add(wx.Button(self, wx.ID_CANCEL, "Cancel"), 1, wx.EXPAND)
+        self.sizers = sizers
+
+        #main sizer
+        self.sizers['main'].Add(sizers['size'],1, wx.EXPAND)
+        self.sizers['main'].Add(sizers['bg'], 1, wx.EXPAND)
+        self.sizers['main'].Add(sizers['grid'], 1, wx.EXPAND)
+        self.sizers['main'].Add(sizers['but'], 0, wx.EXPAND)
+        self.SetSizer(self.sizers['main'])
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #event handlers
+        self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)
+        self.Bind(wx.EVT_RADIOBUTTON, self.on_click, id=CTRL_BG_COLOR)
+        self.Bind(wx.EVT_RADIOBUTTON, self.on_click, id=CTRL_TEXTURE)
+        self.Bind(wx.EVT_RADIOBUTTON, self.on_click, id=CTRL_IMAGE)
+        self.Bind(wx.EVT_BUTTON, self.on_click, id=CTRL_BG_COLOR_VALUE)
+        self.Bind(wx.EVT_BUTTON, self.on_click, id=CTRL_GRID_COLOR)
+        self.Bind(wx.EVT_RADIOBUTTON, self.on_click, id=CTRL_GRID_MODE_RECT)
+        self.Bind(wx.EVT_RADIOBUTTON, self.on_click, id=CTRL_GRID_MODE_HEX)
+        self.Bind(wx.EVT_RADIOBUTTON, self.on_click, id=CTRL_GRID_LINE_NONE)
+        self.Bind(wx.EVT_RADIOBUTTON, self.on_click, id=CTRL_GRID_LINE_DOTTED)
+        self.Bind(wx.EVT_RADIOBUTTON, self.on_click, id=CTRL_GRID_LINE_SOLID)
+
+    def on_click(self,evt):
+        id = evt.GetId()
+        if id == CTRL_BG_COLOR or id == CTRL_TEXTURE:
+            self.ctrls[CTRL_WIDTH].Enable(True)
+            self.ctrls[CTRL_HEIGHT].Enable(True)
+        elif id == CTRL_IMAGE:
+            self.ctrls[CTRL_WIDTH].Enable(False)
+            self.ctrls[CTRL_HEIGHT].Enable(False)
+        elif id == CTRL_BG_COLOR_VALUE:
+            data = wx.ColourData()
+            data.SetChooseFull(True)
+            dlg = wx.ColourDialog(self, data)
+            if dlg.ShowModal() == wx.ID_OK:
+                data = dlg.GetColourData()
+                self.ctrls[CTRL_BG_COLOR_VALUE].SetBackgroundColour(data.GetColour())
+            dlg.Destroy()
+        elif id == CTRL_GRID_COLOR:
+            data = wx.ColourData()
+            data.SetChooseFull(True)
+            dlg = wx.ColourDialog(self, data)
+            if dlg.ShowModal() == wx.ID_OK:
+                data = dlg.GetColourData()
+                self.ctrls[CTRL_GRID_COLOR].SetBackgroundColour(data.GetColour())
+            dlg.Destroy()
+    def on_ok(self,evt):
+        try:
+            self.size = (int(self.ctrls[CTRL_WIDTH].GetValue()),int(self.ctrls[CTRL_HEIGHT].GetValue()))
+        except:
+            pass
+#            dlg = wx.MessageDialog(frame, 'Invalid Size',"Error")
+#            dlg.ShowModal()
+#            dlg.Destroy()
+        if self.ctrls[CTRL_BG_COLOR].GetValue() == True:
+            self.bg_layer.set_color(self.ctrls[CTRL_BG_COLOR_VALUE].GetBackgroundColour())
+        elif self.ctrls[CTRL_TEXTURE].GetValue() == True:
+            self.bg_layer.set_texture(self.ctrls[CTRL_TEXTURE_PATH].GetValue())
+        elif self.ctrls[CTRL_IMAGE].GetValue() == True:
+            self.size = self.bg_layer.set_image(self.ctrls[CTRL_IMAGE_PATH].GetValue(),self.grid_layer.mapscale)
+        else:
+            self.bg_layer.clear()
+        if self.ctrls[CTRL_GRID_MODE_RECT].GetValue() == True:
+            grid_mode = GRID_RECTANGLE
+        else:
+            grid_mode = GRID_HEXAGON
+        if self.ctrls[CTRL_GRID_LINE_NONE].GetValue() == True:
+            grid_line = LINE_NONE
+        elif self.ctrls[CTRL_GRID_LINE_DOTTED].GetValue() == True:
+            grid_line = LINE_DOTTED
+        else:
+            grid_line = LINE_SOLID
+        self.grid_layer.set_grid(int(self.ctrls[CTRL_GRID].GetValue()),
+                                 self.ctrls[CTRL_GRID_SNAP].GetValue(),
+                                 self.ctrls[CTRL_GRID_COLOR].GetBackgroundColour(),
+                                 grid_mode,
+                                 grid_line)
+        self.EndModal(wx.ID_OK)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/map_utils.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,61 @@
+#------------------------------------------------
+# file: map_utils.py
+#
+# This file contains generic utility functions
+# for use in the openrpg mapping system
+# -----------------------------------------------
+
+import math
+
+#-----------------------------------------------------------------------
+# distance_between()
+# 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 )
+
+#-----------------------------------------------------------------------
+# proximity_test()
+# Tests if 'test_point' (T) is close (within 'threshold' units) to the
+# line segment 'start_point' to 'end_point' (PQ).
+#
+# The closest point (R) to T on the line PQ is given by:
+#    R = P + u (Q - P)
+# TR is perpendicular to PQ so:
+#    (T - R) dot (Q - P) = 0
+# Solving these two equations gives the equation for u (see below).
+#
+# If u < 0 or u > 1 then R is not within the line segment and we simply
+# test against point P or Q.
+#-----------------------------------------------------------------------
+def proximity_test( start_point, end_point, test_point, threshold ):
+   "Test if a point is close to a line segment"
+   x1,y1 = start_point
+   x2,y2 = end_point
+   xt,yt = test_point
+   x1 = float(x1)
+   x2 = float(x2)
+   y1 = float(y1)
+   y2 = float(y2)
+   xt = float(xt)
+   yt = float(yt)
+
+   # Coincident points?
+   if x1 == x2 and y1 == y2:
+       d = distance_between(xt, yt, x1, y1)
+   else:
+       dx = x2 - x1
+       dy = y2 - y1
+       u = ((xt - x1) * dx + (yt - y1) * dy) / (dx*dx + dy*dy)
+       if u < 0:
+           d = distance_between(xt, yt, x1, y1)
+       elif u > 1:
+           d = distance_between(xt, yt, x2, y2)
+       else:
+           xr = x1 + u * dx
+           yr = y1 + u * dy
+           d = distance_between(xt, yt, xr, yr)
+   return d <= threshold
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/map_version.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,3 @@
+## this gile old the map version ###
+
+MAP_VERSION = "2.1"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/min_dialogs.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,539 @@
+# 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: mapper/min_dialogs.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: min_dialogs.py,v 1.27 2006/11/13 02:23:16 digitalxero Exp $
+#
+# Description: This file contains some of the basic definitions for the chat
+# utilities in the orpg project.
+
+##-----------------------------
+## Miniature List Panel
+##-----------------------------
+
+from miniatures import *
+
+class min_list_panel(wx.Dialog):
+
+    def __init__(self, parent,layers, log, pos =(-1,-1)):
+        wx.Dialog.__init__(self,  parent,-1, log,pos = (-1,-1), size = (785,175), style=wx.RESIZE_BORDER)
+        listID = wx.NewId()
+        self.parent = parent
+        self.min = layers['miniatures'].miniatures
+        self.grid = layers['grid']
+        self.layers = layers
+        self.listID = listID
+        list_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.list_sizer = list_sizer
+        listctrl = wx.ListCtrl(self, listID, style=wx.LC_REPORT | wx.SUNKEN_BORDER)
+        self.listctrl = listctrl
+        self.Centre(wx.BOTH)
+        self.log = log
+        self.list_sizer.Add(self.listctrl,1,wx.EXPAND)
+        self.listctrl.InsertColumn(0,"POS    ")
+        self.listctrl.InsertColumn(0,"LOCKED")
+        self.listctrl.InsertColumn(0,"HEADING")
+        self.listctrl.InsertColumn(0,"FACING")
+        self.listctrl.InsertColumn(0,"LABEL")
+        self.listctrl.InsertColumn(0,"PATH")
+        self.listctrl.SetColumnWidth(1, wx.LIST_AUTOSIZE_USEHEADER)
+        self.listctrl.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
+        self.listctrl.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
+        self.listctrl.SetColumnWidth(4, wx.LIST_AUTOSIZE_USEHEADER)
+        self.listctrl.SetColumnWidth(5, wx.LIST_AUTOSIZE_USEHEADER)
+        self.list_sizer.Add(wx.Button(self, wx.ID_OK, "DONE"),0,wx.ALIGN_CENTER)
+        self.refresh()
+        self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)
+        self.listctrl.Bind(wx.EVT_COMMAND_RIGHT_CLICK, self.OnRightClick, id=listID)
+        self.listctrl.Bind(wx.EVT_RIGHT_UP, self.OnRightClick)
+        self.listctrl.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
+        self.SetSizer(self.list_sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+    def OnRightClick(self,event):
+        if self.listctrl.GetSelectedItemCount() > 0:
+            menu = wx.Menu()
+            lPopupID1 = wx.NewId()
+            lPopupID2 = wx.NewId()
+            lPopupID3 = wx.NewId()
+            menu.Append(lPopupID1, "&Edit")
+            menu.Append(lPopupID2, "&Delete")
+            menu.Append(lPopupID3, "To &Gametree")
+            self.Bind(wx.EVT_MENU, self.onEdit, id=lPopupID1)
+            self.Bind(wx.EVT_MENU, self.onDelete, id=lPopupID2)
+            self.Bind(wx.EVT_MENU, self.onToGametree, id=lPopupID3)
+            self.PopupMenu(menu, cmpPoint(self.x, self.y))
+            menu.Destroy()
+        event.Skip()
+
+    def refresh(self):
+        self.SetMinSize((600,175));
+        for m in self.min:
+            self.listctrl.InsertStringItem(self.min.index(m),self.min[self.min.index(m)].path)
+            self.listctrl.SetStringItem(self.min.index(m),1,self.min[self.min.index(m)].label)
+            self.listctrl.SetStringItem(self.min.index(m),2,`self.min[self.min.index(m)].heading`)
+            self.listctrl.SetStringItem(self.min.index(m),3,`self.min[self.min.index(m)].face`)
+            self.listctrl.SetStringItem(self.min.index(m),4,`self.min[self.min.index(m)].locked`)
+            self.listctrl.SetStringItem(self.min.index(m),5,`self.min[self.min.index(m)].pos`)
+            oldcolumnwidth = self.listctrl.GetColumnWidth(0)
+            self.listctrl.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+            if oldcolumnwidth < self.listctrl.GetColumnWidth(0):
+                self.listctrl.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+            else:
+                self.listctrl.SetColumnWidth(0, oldcolumnwidth)
+        self.list_sizer=self.list_sizer
+
+    def onEdit(self,event):
+        min_list = []
+        min_index = []
+        loop_count = 0
+        item =-1
+        while True:
+            loop_count += 1
+            item = self.listctrl.GetNextItem(item,wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
+            if item == -1:
+                break
+            min_list.append(self.min[item])
+            min_index.append(item-loop_count+1)
+        if len(min_list) > 0:
+            dlg = min_list_edit_dialog(self.parent,min_index, min_list,self.layers)
+        if dlg.ShowModal() == wx.ID_OK:
+            pass
+        self.listctrl.DeleteAllItems()
+        self.refresh()
+        event.Skip()
+
+    def onDelete(self,event):
+        loop_count = 0
+        item = -1
+        while True:
+            loop_count += 1
+            item = self.listctrl.GetNextItem(item,wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
+            if item == -1:
+                break
+            #self.min.remove(self.min[item-loop_count+1])
+            self.layers["miniatures"].del_miniature(self.min[item-loop_count+1])
+        self.listctrl.DeleteAllItems()
+        self.refresh()
+        event.Skip()
+
+    def onToGametree(self,event):
+        min_list = []
+        min_index = []
+        loop_count = 0
+        item =-1
+        while True:
+            loop_count += 1
+            item = self.listctrl.GetNextItem(item,wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
+            if item == -1:
+                break
+            min_list.append(self.min[item])
+            min_index.append(item-loop_count+1)
+        if len(min_list) > 0:
+            for sel_rmin in min_list:
+#############
+                min_xml = sel_rmin.toxml(action="new")
+                node_begin = "<nodehandler module='map_miniature_nodehandler' class='map_miniature_handler' name='"
+
+                if sel_rmin.label:
+                    node_begin += sel_rmin.label + "'"
+                else:
+                    node_begin += "Unnamed Miniature'"
+
+                node_begin += ">"
+                gametree = open_rpg.get_component('tree')
+                node_xml = node_begin + min_xml + '</nodehandler>'
+                print "Sending this XML to insert_xml:" + node_xml
+                gametree.insert_xml(node_xml)
+##########
+        self.listctrl.DeleteAllItems()
+        self.refresh()
+        event.Skip()
+
+    def OnRightDown(self,event):
+        self.x = event.GetX()
+        self.y = event.GetY()
+        event.Skip()
+
+    def on_ok(self,evt):
+        self.EndModal(wx.ID_OK)
+
+class min_list_edit_dialog(wx.Dialog):
+    def __init__(self,parent,min_index, min_list, layers):
+        wx.Dialog.__init__(self,parent,-1,"Miniature List",wx.DefaultPosition,wx.Size(600,530))
+        self.layers = layers
+        grid = layers['grid']
+        min = layers['miniatures']
+        self.min_list = min_list
+        self.min_index = min_index
+        self.min = min
+        sizer1 = wx.BoxSizer(wx.VERTICAL)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.grid = grid
+        editor = min_list_edit_panel(self, min_index, min_list,layers)
+        sizer1.Add(editor, 1, wx.EXPAND)
+        sizer.Add(wx.Button(self, wx.ID_OK, "OK"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, wx.ID_CANCEL, "Cancel"), 1, wx.EXPAND)
+        sizer1.Add(sizer, 0, wx.EXPAND)
+        self.editor = editor
+        self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)
+        self.SetSizer(sizer1)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+    def on_revert(self,evt):
+        pass
+
+    def on_ok(self,evt):
+        self.editor.on_ok(self.layers)
+        self.EndModal(wx.ID_OK)
+
+class min_list_edit_panel(wx.Panel):
+    def __init__(self, parent, min_index, min_list,layers):
+        LABEL_COMBO = wx.NewId()
+        PATH_COMBO = wx.NewId()
+        POS_COMB = wx.NewId()
+        MIN_POS = wx.NewId()
+        POS_SPIN = wx.NewId()
+        self.grid = layers['grid']
+        self.min = layers['miniatures'].miniatures
+        self.min_list = min_list
+        self.min_index = min_index
+        self.layers = layers
+        wx.Panel.__init__(self, parent, -1)
+        self.min=min
+        listsizer = wx.StaticBoxSizer(wx.StaticBox(self,-1,"Miniature list properties"), wx.VERTICAL)
+        labelsizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.labelcheck = wx.CheckBox(self,-1,"Serialize")
+        labelsizer.Add(wx.StaticText(self, -1, "Label:          "), 0, wx.EXPAND)
+        labelsizer.Add(self.labelcheck,wx.ALIGN_RIGHT,wx.EXPAND)
+        listsizer.Add(labelsizer,0, wx.EXPAND)
+        self.labelcombo = wx.ComboBox(self, LABEL_COMBO,"no change",style=wx.CB_DROPDOWN)
+        listsizer.Add(self.labelcombo,0, wx.EXPAND)
+        self.pathcombo = wx.ComboBox(self, PATH_COMBO, "no change",style=wx.CB_DROPDOWN)
+        self.positioncombo = wx.ComboBox(self, POS_COMB, "no change", choices=["no change"], style=wx.CB_READONLY)
+        #self.positioncombo.SetValue(`min_list[0].pos`)
+        self.labelcombo.Append("no change")
+        self.pathcombo.Append("no change")
+        for m in min_list:
+            self.labelcombo.Append(min_list[min_list.index(m)].label)
+            self.pathcombo.Append(min_list[min_list.index(m)].path)
+            self.positioncombo.Append(`min_list[min_list.index(m)].pos`)
+        listsizer.Add(wx.StaticText(self, -1, "Path:"), 0, wx.EXPAND)
+        listsizer.Add(self.pathcombo, 0, wx.EXPAND)
+        listsizer.Add(wx.Size(10,10))
+        self.heading = wx.RadioBox(self, MIN_HEADING, "Heading", choices=["None","N","NE","E","SE","S","SW","W","NW","no change"], majorDimension=5, style=wx.RA_SPECIFY_COLS)
+        self.heading.SetSelection( 9 )
+        listsizer.Add( self.heading, 0, wx.EXPAND )
+        listsizer.Add(wx.Size(10,10))
+        self.face = wx.RadioBox(self, MIN_FACE, "Facing", choices=["None","N","NE","E","SE","S","SW","W","NW","no change"], majorDimension=5, style=wx.RA_SPECIFY_COLS)
+        self.face.SetSelection(9)
+        listsizer.Add(self.face, 0, wx.EXPAND)
+###
+###    Group together locked, Hide, and snap radioboxes in group2 box
+###
+        group2 = wx.BoxSizer(wx.HORIZONTAL)
+        self.locked = wx.RadioBox(self, MIN_LOCK, "Lock", choices=["Don't lock","Lock","no change"],majorDimension=1,style=wx.RA_SPECIFY_COLS)
+        self.locked.SetSelection(2)
+        self.hide = wx.RadioBox(self, MIN_HIDE, "Hide", choices=["Don't hide", "Hide", "no change"],majorDimension=1,style=wx.RA_SPECIFY_COLS)
+        self.hide.SetSelection(2)
+        self.snap = wx.RadioBox(self,MIN_ALIGN,"Snap",choices=["Center","Top left","no change"],majorDimension=1,style=wx.RA_SPECIFY_COLS)
+        self.snap.SetSelection(2)
+        group2.Add(self.locked, 0, wx.EXPAND)
+        group2.Add(wx.Size(10,0))
+        group2.Add(self.hide, 0, wx.EXPAND)
+        group2.Add(wx.Size(10,0))
+        group2.Add(self.snap, 0, wx.EXPAND)
+        group2.Add(wx.Size(10,0))
+        listsizer.Add(group2,0,0)
+###
+###     group together the postion radiobox and the and its selection elements
+###
+        xpos = int(min_list[0].pos[0])
+        #xpos = int(`min_list[0].pos`[1:`min_list[0].pos`.index(',')])
+        ypos = int(min_list[0].pos[1])
+        #ypos = int(`min_list[0].pos`[`min_list[0].pos`.rfind(',')+1:len(`min_list[0].pos`)-1])
+        self.scx = wx.SpinCtrl(self, POS_SPIN, "", (-1,-1), wx.Size(75,25))
+        self.scx.SetRange(0,self.grid.return_grid()[0])
+        self.scx.SetValue(xpos)
+        self.scy = wx.SpinCtrl(self, POS_SPIN, "", (-1,-1), wx.Size(75,25))
+        self.scy.SetRange(0,self.grid.return_grid()[1])
+        self.scy.SetValue(1)
+        self.scy.SetValue(ypos)
+        positionbox = wx.BoxSizer(wx.HORIZONTAL)
+        self.poschoice = wx.RadioBox(self,MIN_POS,"Position",choices=["Manual", "Existing", "no change"],majorDimension=1,style=wx.RA_SPECIFY_COLS)
+        self.poschoice.SetSelection(2)
+        positionbox.Add(self.poschoice,0,0)
+        ###
+        ### group together choices under position choice boxsizer
+        ###
+        poschoicebox = wx.BoxSizer(wx.VERTICAL)
+            ###
+            ### spinbox contains the x and y spinctrls
+            ###
+        spinbox = wx.BoxSizer(wx.HORIZONTAL)
+        group2.Add(positionbox,0, wx.EXPAND)
+        xpos = wx.StaticText(self, -1,"XPOS:  ")
+        spinbox.Add(xpos,0, 0)
+        spinbox.Add(self.scx, 0, 0)
+        ypos = wx.StaticText(self, -1,"YPOS:  ")
+        spinbox.Add(ypos,0, 0)
+        spinbox.Add(self.scy, 0, 0)
+        poschoicebox.Add(wx.Size(0,15))
+        poschoicebox.Add(spinbox,0,0)
+            ###
+            ### kludge is just a way to horizontaly position text.  .Add doesn't seem to work.
+            ###
+        kluge = wx.BoxSizer(wx.HORIZONTAL)
+        klugetext = wx.StaticText(self, -1, "            ")
+        kluge.Add(klugetext,0,0)
+        kluge.Add(self.positioncombo,0,0)
+        poschoicebox.Add(wx.Size(0,1))
+        poschoicebox.Add(kluge,0,0)
+        positionbox.Add(poschoicebox,0,0)
+        listsizer.Add(positionbox,0, 0)
+        self.listsizer = listsizer
+        #self.outline = wx.StaticBox(self,-1,"Miniature list properties")
+        #listsizer.Add(self.outline,0, wx.EXPAND)
+        self.SetSizer(listsizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        self.Bind(wx.EVT_SPINCTRL, self.on_spin, id=POS_SPIN)
+        self.Bind(wx.EVT_TEXT, self.on_combo_box, id=POS_COMB)
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=MIN_LABEL)
+        self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=MIN_HEADING)
+        self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=MIN_FACE)
+
+    def on_ok(self,min):
+        self.min = min
+        for m in self.min_list:
+            if self.hide.GetSelection() !=2:
+                m.hide = self.hide.GetSelection()
+            if self.heading.GetSelection() !=9:
+                m.heading = self.heading.GetSelection()
+            if self.face.GetSelection() !=9:
+                m.face = self.face.GetSelection()
+            if self.locked.GetSelection() !=2:
+                m.locked = self.locked.GetSelection()
+            if self.snap.GetSelection() !=2:
+                m.snap_to_align = self.snap.GetSelection()
+            if self.labelcombo.GetValue() != "no change":
+                m.label = self.labelcombo.GetValue()
+                if self.labelcheck.GetValue():
+                    m.label += " " + `self.layers['miniatures'].next_serial()`
+                    # self.layers['miniatures'].serial_number +=1
+                    # m.label += " " + `self.layers['miniatures'].serial_number`
+            if self.pathcombo.GetValue() != "no change":
+                path = self.pathcombo.GetValue()
+                image = self.evaluate(path)
+                if str(image[1]) != '-1':
+                    m.path = image[0]
+                    m.bmp = image[1]
+                else:
+                    image[-1] = -1
+                    while image[1] == -1:
+                        image = 0
+                        self.dlg = wx.TextEntryDialog(self, 'You entered an invalid URL for the image path.  Please Enter a valid URL or cancel to leave the old url unchanged')
+                        if self.dlg.ShowModal() == wx.ID_OK:
+                            path = self.dlg.GetValue()
+                            image = self.evaluate(path)
+                            if image[1] != -1:
+                                m.path = image[0]
+                                m.bmp = image[1]
+                            self.dlg.Destroy()
+                        else:
+                            break
+            if self.poschoice.GetSelection() !=2:
+                if self.poschoice.GetSelection() == 0:
+                    m.pos = cmpPoint(self.scx.GetValue(),self.scy.GetValue())
+                else:
+                    pos = self.positioncombo.GetValue()
+                    m.pos = cmpPoint(int(`pos`[2:`pos`.index(",")]),int(`pos`[`pos`.rfind(',')+1:len(`pos`)-2]))
+        self.layers["miniatures"].canvas.send_map_data()
+
+    def evaluate(self, ckpath):
+        path = []
+        if ckpath[:7] != "http://":
+            ckpath = "http://" + ckpath
+        path = self.check_path(ckpath)
+        return [ckpath, path]
+
+    def check_path(self, path):
+        if ImageHandler.Cache.has_key(path):
+            return ImageHandler.Cache[path]
+        img = ImageHandler.directLoad(path)
+        if img is None:
+            return -1
+        return img
+
+    def on_text(self,evt):
+        id=evt.GetId()
+
+    def on_spin(self,evt):
+        self.poschoice.SetSelection(0)
+
+    def on_combo_box(self,evt):
+        self.poschoice.SetSelection(1)
+
+    def on_radio_box(self,evt):
+        id=evt.GetId()
+        index = evt.GetInt()
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.listsizer.SetDimension(20,20,s[0]-40,s[1]-40)
+        self.outline.SetDimensions(5,5,s[0]-10,s[1]-10)
+
+##-----------------------------
+## Miniature Prop Panel
+##-----------------------------
+
+MIN_LABEL = wx.NewId()
+MIN_HEADING = wx.NewId()
+MIN_FACE = wx.NewId()
+MIN_HIDE = wx.NewId()
+MIN_LOCK = wx.NewId()
+MIN_ALIGN = wx.NewId()
+wxID_MIN_WIDTH = wx.NewId()
+wxID_MIN_HEIGHT = wx.NewId()
+wxID_MIN_SCALING = wx.NewId()
+
+class min_edit_panel(wx.Panel):
+    def __init__(self, parent, min):
+        wx.Panel.__init__(self, parent, -1)
+        self.min = min
+        sizer = wx.StaticBoxSizer(wx.StaticBox(self,-1,"Miniature"), wx.VERTICAL)
+        sizerSize = wx.BoxSizer(wx.HORIZONTAL)
+        hSizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.label = wx.TextCtrl(self, MIN_LABEL, min.label)
+        sizer.Add(wx.StaticText(self, -1, "Label:"), 0, wx.EXPAND)
+        sizer.Add(self.label, 0, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        self.heading = wx.RadioBox(self, MIN_HEADING, "Heading", choices=["None","N","NE","E","SE","S","SW","W","NW"],majorDimension=5,style=wx.RA_SPECIFY_COLS)
+        self.heading.SetSelection(min.heading)
+        self.face = wx.RadioBox(self, MIN_FACE, "Facing", choices=["None","N","NE","E","SE","S","SW","W","NW"],majorDimension=5,style=wx.RA_SPECIFY_COLS)
+        self.face.SetSelection(min.face)
+        self.locked = wx.CheckBox(self, MIN_LOCK, " Lock")
+        self.locked.SetValue(min.locked)
+        self.hide = wx.CheckBox(self, MIN_HIDE, " Hide")
+        self.hide.SetValue(min.hide)
+        sizer.Add(self.heading, 0, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(self.face, 0, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        #
+        #image resizing
+        #
+        self.min_width_old_value = str(self.min.bmp.GetWidth())
+        self.min_width = wx.TextCtrl(self, wxID_MIN_WIDTH, self.min_width_old_value)
+        sizerSize.Add(wx.StaticText(self, -1, "Width: "), 0, wx.ALIGN_CENTER)
+        sizerSize.Add(self.min_width, 1, wx.EXPAND)
+        sizerSize.Add(wx.Size(20, 25))
+
+        #TODO:keep in mind that self.min is a local copy???
+        self.min_height_old_value = str(self.min.bmp.GetHeight())
+        self.min_height = wx.TextCtrl(self, wxID_MIN_HEIGHT, self.min_height_old_value)
+        sizerSize.Add(wx.StaticText(self, -1, "Height: "),0,wx.ALIGN_CENTER)
+        sizerSize.Add(self.min_height, 1, wx.EXPAND)
+        self.min_scaling = wx.CheckBox(self, wxID_MIN_SCALING, "Lock scaling")
+        self.min_scaling.SetValue(True)
+        sizerSize.Add(self.min_scaling, 1, wx.EXPAND)
+        sizer.Add(sizerSize, 0, wx.EXPAND)
+        sizer.Add(wx.Size(10, 10))
+
+        # Now, add the last items on in their own sizer
+        hSizer.Add(self.locked, 0, wx.EXPAND)
+        hSizer.Add(wx.Size(10,10))
+        hSizer.Add(self.hide, 0, wx.EXPAND)
+
+        # Add the hSizer to the main sizer
+        sizer.Add( hSizer )
+        self.sizer = sizer
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        #self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.EVT_TEXT, self.on_text, id=MIN_LABEL)
+        self.Bind(wx.EVT_TEXT, self.on_scaling, id=wxID_MIN_WIDTH)
+        self.Bind(wx.EVT_TEXT, self.on_scaling, id=wxID_MIN_HEIGHT)
+        self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=MIN_HEADING)
+        self.Bind(wx.EVT_RADIOBOX, self.on_radio_box, id=MIN_FACE)
+
+    def on_scaling(self, evt):
+        if self.min_scaling.GetValue() == False:
+            return
+        elif self.min_width.GetValue() and wxID_MIN_WIDTH == evt.GetId() and self.min_width.GetInsertionPoint():
+            self.min_height.SetValue ( str(int((float(self.min_width.GetValue()) / float(self.min_width_old_value)) * float(self.min_height_old_value))) )
+        elif self.min_height.GetValue() and wxID_MIN_HEIGHT == evt.GetId() and self.min_height.GetInsertionPoint():
+            self.min_width.SetValue ( str(int((float(self.min_height.GetValue()) / float(self.min_height_old_value)) * float(self.min_width_old_value))) )
+
+    def update_min(self):
+        self.min.set_min_props(self.heading.GetSelection(),
+                               self.face.GetSelection(),
+                               self.label.GetValue(),
+                               self.locked.GetValue(),
+                               self.hide.GetValue(),
+                               self.min_width.GetValue(),
+                               self.min_height.GetValue())
+
+    def on_radio_box(self,evt):
+        id = evt.GetId()
+        index = evt.GetInt()
+
+    def on_text(self,evt):
+        id = evt.GetId()
+
+    def on_size(self,evt):
+        s = self.GetClientSizeTuple()
+        self.sizer.SetDimension(20,20,s[0]-40,s[1]-40)
+        self.outline.SetDimensions(5,5,s[0]-10,s[1]-10)
+
+class min_edit_dialog(wx.Dialog):
+    def __init__(self,parent,min):
+#520,265
+        wx.Dialog.__init__(self,parent,-1,"Miniature",wx.DefaultPosition,wx.Size(520,350))
+        (w,h) = self.GetClientSizeTuple()
+        mastersizer = wx.BoxSizer(wx.VERTICAL)
+        editor = min_edit_panel(self,min)
+        #editor.SetDimensions(0,0,w,h-25)
+        self.editor = editor
+        mastersizer.Add(editor, 1, wx.EXPAND)
+        mastersizer.Add(wx.Size(10,10))
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, wx.ID_OK, "OK"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, wx.ID_CANCEL, "Cancel"), 1, wx.EXPAND)
+        #sizer.SetDimension(0,h-25,w,25)
+        mastersizer.Add(sizer, 0, wx.EXPAND)
+        self.SetSizer(mastersizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)
+
+    def on_ok(self,evt):
+        self.editor.update_min()
+        self.EndModal(wx.ID_OK)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/miniatures.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,741 @@
+# 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: mapper/miniatures.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: miniatures.py,v 1.46 2007/12/07 20:39:50 digitalxero Exp $
+#
+# Description: This file contains some of the basic definitions for the chat
+# utilities in the orpg project.
+#
+__version__ = "$Id: miniatures.py,v 1.46 2007/12/07 20:39:50 digitalxero Exp $"
+
+from base import *
+import thread
+import time
+import urllib
+import os.path
+
+MIN_STICKY_BACK = -0XFFFFFF
+MIN_STICKY_FRONT = 0xFFFFFF
+
+##----------------------------------------
+##  miniature object
+##----------------------------------------
+
+FACE_NONE = 0
+FACE_NORTH = 1
+FACE_NORTHEAST = 2
+FACE_EAST = 3
+FACE_SOUTHEAST = 4
+FACE_SOUTH = 5
+FACE_SOUTHWEST = 6
+FACE_WEST = 7
+FACE_NORTHWEST = 8
+SNAPTO_ALIGN_CENTER = 0
+SNAPTO_ALIGN_TL = 1
+
+def cmp_zorder(first,second):
+    f = first.zorder
+    s = second.zorder
+    if f == None:
+        f = 0
+    if s == None:
+        s = 0
+    if f == s:
+        value = 0
+    elif f < s:
+        value = -1
+    else:
+        value = 1
+    return value
+
+class BmpMiniature:
+    def __init__(self, id,path, bmp, 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, log=None, local=False, localPath='', localTime=-1):
+        self.log = log
+        self.log.log("Enter BmpMiniature", ORPG_DEBUG)
+        self.heading = heading
+        self.face = face
+        self.label = label
+        self.path = path
+        self.bmp = bmp
+        self.pos = pos
+        self.selected = False
+        self.locked = locked
+        self.snap_to_align = snap_to_align
+        self.hide = hide
+        self.id = id
+        self.zorder = zorder
+        self.left = 0
+        self.local = local
+        self.localPath = localPath
+        self.localTime = localTime
+        if not width:
+            self.width = 0
+        else:
+            self.width = width
+        if not height:
+            self.height = 0
+        else:
+            self.height = height
+        self.right = bmp.GetWidth()
+        self.top = 0
+        self.bottom = bmp.GetHeight()
+        self.isUpdated = False
+        self.gray = False
+        self.log.log("Exit BmpMiniature", ORPG_DEBUG)
+
+    def __del__(self):
+        self.log.log("Enter BmpMiniature->__del__(self)", ORPG_DEBUG)
+        del self.bmp
+        self.bmp = None
+        self.log.log("Exit BmpMiniature->__del__(self)", ORPG_DEBUG)
+
+    def set_bmp(self, bmp):
+        self.log.log("Enter BmpMiniature->set_bmp(self, bmp)", ORPG_DEBUG)
+        self.bmp = bmp
+        self.log.log("Exit BmpMiniature->set_bmp(self, bmp)", ORPG_DEBUG)
+
+    def set_min_props(self, heading=FACE_NONE, face=FACE_NONE, label="", locked=False, hide=False, width=0, height=0):
+        self.log.log("Enter BmpMiniature->set_min_props(self, heading, face, label, locked, hide, width, height)", ORPG_DEBUG)
+        self.heading = heading
+        self.face = face
+        self.label = label
+        if locked:
+            self.locked = True
+        else:
+            self.locked = False
+        if hide:
+            self.hide = True
+        else:
+            self.hide = False
+        self.width = int(width)
+        self.height = int(height)
+        self.isUpdated = True
+        self.log.log("Exit BmpMiniature->set_min_props(self, heading, face, label, locked, hide, width, height)", ORPG_DEBUG)
+
+    def hit_test(self, pt):
+        self.log.log("Enter BmpMiniature->hit_test(self, pt)", ORPG_DEBUG)
+        rect = self.get_rect()
+        result = None
+        result = rect.InsideXY(pt.x, pt.y)
+        self.log.log("Exit BmpMiniature->hit_test(self, pt)", ORPG_DEBUG)
+        return result
+
+    def get_rect(self):
+        self.log.log("Enter BmpMiniature->get_rect(self)", ORPG_DEBUG)
+        ret = wx.Rect(self.pos.x, self.pos.y, self.bmp.GetWidth(), self.bmp.GetHeight())
+        self.log.log("Exit BmpMiniature->get_rect(self)", ORPG_DEBUG)
+        return ret
+
+    def draw(self, dc, mini_layer, op=wx.COPY):
+        self.log.log("Enter BmpMiniature->draw(self, dc, mini_layer, op)", ORPG_DEBUG)
+        if isinstance(self.bmp, tuple):
+            self.log.log("bmp is a tuple, it shouldnt be!", ORPG_INFO)
+            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:
+                self.log.log("Enter BmpMiniature->draw->Draw Hidden", ORPG_DEBUG)
+                # 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)
+                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.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)
+                    dc.DrawRectangle(self.pos.x, self.pos.y, self.bmp.GetWidth(), self.bmp.GetHeight())
+                    dc.SetBrush(wx.NullBrush)
+                    dc.SetPen(wx.NullPen)
+                self.log.log("Exit BmpMiniature->draw->Draw Hidden", ORPG_DEBUG)
+                return True
+            elif self.hide:
+                self.log.log("Enter/Exit BmpMiniature->draw->Skip Hidden", ORPG_DEBUG)
+                return True
+
+            else:
+                self.log.log("Enter BmpMiniature->draw->Not Hidden", ORPG_DEBUG)
+                # 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)
+
+                # 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 = []
+
+                    # 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
+                self.log.log("Exit BmpMiniature->draw->Not Hidden", ORPG_DEBUG)
+                return True
+        else:
+            self.log.log("Exit BmpMiniature->draw(self, dc, mini_layer, op) return False", ORPG_DEBUG)
+            return False
+        self.log.log("Exit BmpMiniature->draw(self, dc, mini_layer, op)", ORPG_DEBUG)
+
+    def toxml(self, action="update"):
+        self.log.log("Enter BmpMiniature->toxml(self, " + action + ")", ORPG_DEBUG)
+        if action == "del":
+            xml_str = "<miniature action='del' id='" + self.id + "'/>"
+            self.log.log(xml_str, ORPG_DEBUG)
+            self.log.log("Exit BmpMiniature->toxml(self, " + action + ")", ORPG_DEBUG)
+            return xml_str
+        xml_str = "<miniature"
+        xml_str += " action='" + action + "'"
+        xml_str += " label='" + self.label + "'"
+        xml_str+= " id='" + self.id + "'"
+        if self.pos != None:
+            xml_str += " posx='" + str(self.pos.x) + "'"
+            xml_str += " posy='" + str(self.pos.y) + "'"
+        if self.heading != None:
+            xml_str += " heading='" + str(self.heading) + "'"
+        if self.face != None:
+            xml_str += " face='" + str(self.face) + "'"
+        if self.path != None:
+            xml_str += " path='" + urllib.quote(self.path).replace('%3A', ':') + "'"
+        if self.locked:
+            xml_str += "  locked='1'"
+        else:
+            xml_str += "  locked='0'"
+        if self.hide:
+            xml_str += " hide='1'"
+        else:
+            xml_str += " hide='0'"
+        if self.snap_to_align != None:
+            xml_str += " align='" + str(self.snap_to_align) + "'"
+        if self.id != None:
+            xml_str += " zorder='" + str(self.zorder) + "'"
+        if self.width != None:
+            xml_str += " width='" + str(self.width) + "'"
+        if self.height != None:
+            xml_str += " height='" + str(self.height) + "'"
+        if self.local:
+            xml_str += ' local="' + str(self.local) + '"'
+            xml_str += ' localPath="' + str(urllib.quote(self.localPath).replace('%3A', ':')) + '"'
+            xml_str += ' localTime="' + str(self.localTime) + '"'
+        xml_str += " />"
+        self.log.log(xml_str, ORPG_DEBUG)
+        self.log.log("Exit BmpMiniature->toxml(self, " + action + ")", ORPG_DEBUG)
+        if (action == "update" and self.isUpdated) or action == "new":
+            self.isUpdated = False
+            return xml_str
+        else:
+            return ''
+
+    def takedom(self, xml_dom):
+        self.log.log("Enter BmpMiniature->takedom(self, xml_dom)", ORPG_DEBUG)
+        self.id = xml_dom.getAttribute("id")
+        self.log.log("self.id=" + str(self.id), ORPG_DEBUG)
+        if xml_dom.hasAttribute("posx"):
+            self.pos.x = int(xml_dom.getAttribute("posx"))
+            self.log.log("self.pos.x=" + str(self.pos.x), ORPG_DEBUG)
+        if xml_dom.hasAttribute("posy"):
+            self.pos.y = int(xml_dom.getAttribute("posy"))
+            self.log.log("self.pos.y=" + str(self.pos.y), ORPG_DEBUG)
+        if xml_dom.hasAttribute("heading"):
+            self.heading = int(xml_dom.getAttribute("heading"))
+            self.log.log("self.heading=" + str(self.heading), ORPG_DEBUG)
+        if xml_dom.hasAttribute("face"):
+            self.face = int(xml_dom.getAttribute("face"))
+            self.log.log("self.face=" + str(self.face), ORPG_DEBUG)
+        if xml_dom.hasAttribute("path"):
+            self.path = urllib.unquote(xml_dom.getAttribute("path"))
+            self.set_bmp(ImageHandler.load(self.path, 'miniature', self.id))
+            self.log.log("self.path=" + self.path, ORPG_DEBUG)
+        if xml_dom.hasAttribute("locked"):
+            if xml_dom.getAttribute("locked") == '1' or xml_dom.getAttribute("locked") == 'True':
+                self.locked = True
+            else:
+                self.locked = False
+            self.log.log("self.locked=" + str(self.locked), ORPG_DEBUG)
+        if xml_dom.hasAttribute("hide"):
+            if xml_dom.getAttribute("hide") == '1' or xml_dom.getAttribute("hide") == 'True':
+                self.hide = True
+            else:
+                self.hide = False
+            self.log.log("self.hide=" + str(self.hide), ORPG_DEBUG)
+        if xml_dom.hasAttribute("label"):
+            self.label = xml_dom.getAttribute("label")
+            self.log.log("self.label=" + self.label, ORPG_DEBUG)
+        if xml_dom.hasAttribute("zorder"):
+            self.zorder = int(xml_dom.getAttribute("zorder"))
+            self.log.log("self.zorder=" + str(self.zorder), ORPG_DEBUG)
+        if xml_dom.hasAttribute("align"):
+            if xml_dom.getAttribute("align") == '1' or xml_dom.getAttribute("align") == 'True':
+                self.snap_to_align = 1
+            else:
+                self.snap_to_align = 0
+            self.log.log("self.snap_to_align=" + str(self.snap_to_align), ORPG_DEBUG)
+        if xml_dom.hasAttribute("width"):
+            self.width = int(xml_dom.getAttribute("width"))
+            self.log.log("self.width=" + str(self.width), ORPG_DEBUG)
+        if xml_dom.hasAttribute("height"):
+            self.height = int(xml_dom.getAttribute("height"))
+            self.log.log("self.height=" + str(self.height), ORPG_DEBUG)
+        self.log.log("Exit BmpMiniature->takedom(self, xml_dom)", ORPG_DEBUG)
+
+##-----------------------------
+## miniature layer
+##-----------------------------
+class miniature_layer(layer_base):
+    def __init__(self, canvas):
+        self.canvas = canvas
+        self.log = self.canvas.log
+        self.log.log("Enter miniature_layer", ORPG_DEBUG)
+        self.settings = self.canvas.settings
+        layer_base.__init__(self)
+        self.miniatures = []
+        self.serial_number = 0
+        self.log.log("Exit miniature_layer", ORPG_DEBUG)
+
+    def next_serial(self):
+        self.log.log("Enter miniature_layer->next_serial(self)", ORPG_DEBUG)
+        self.serial_number += 1
+        self.log.log("Exit miniature_layer->next_serial(self)", ORPG_DEBUG)
+        return self.serial_number
+
+    def get_next_highest_z(self):
+        self.log.log("Enter miniature_layer->get_next_highest_z(self)", ORPG_DEBUG)
+        z = len(self.miniatures)+1
+        self.log.log("Exit miniature_layer->get_next_highest_z(self)", ORPG_DEBUG)
+        return z
+
+    def cleanly_collapse_zorder(self):
+        self.log.log("Enter miniature_layer->cleanly_collapse_zorder(self)", ORPG_DEBUG)
+        #  lock the zorder stuff
+        sorted_miniatures = self.miniatures[:]
+        sorted_miniatures.sort(cmp_zorder)
+        i = 0
+        for mini in sorted_miniatures:
+            mini.zorder = i
+            i = i + 1
+        self.log.log("Exit miniature_layer->cleanly_collapse_zorder(self)", ORPG_DEBUG)
+        #  unlock the zorder stuff
+
+    def collapse_zorder(self):
+        self.log.log("Enter miniature_layer->collapse_zorder(self)", ORPG_DEBUG)
+        #  lock the zorder stuff
+        sorted_miniatures = self.miniatures[:]
+        sorted_miniatures.sort(cmp_zorder)
+        i = 0
+        for mini in sorted_miniatures:
+            if (mini.zorder != MIN_STICKY_BACK) and (mini.zorder != MIN_STICKY_FRONT):
+                mini.zorder = i
+            else:
+                pass
+            i = i + 1
+        self.log.log("Exit miniature_layer->collapse_zorder(self)", ORPG_DEBUG)
+        #  unlock the zorder stuff
+
+    def rollback_serial(self):
+        self.log.log("Enter miniature_layer->rollback_serial(self)", ORPG_DEBUG)
+        self.serial_number -= 1
+        self.log.log("Exit miniature_layer->rollback_serial(self)", ORPG_DEBUG)
+
+    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):
+        self.log.log("Enter miniature_layer->add_miniature(self, id, path, pos, label, heading, face, width, height)", ORPG_DEBUG)
+        self.log.log("Before mini creation: " + str(self.get_next_highest_z()), ORPG_DEBUG)
+        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, log=self.log, local=local, localPath=localPath, localTime=localTime)
+            self.log.log("After mini creation:" + str(self.get_next_highest_z()), ORPG_DEBUG)
+            self.miniatures.append(mini)
+            self.log.log("After mini addition:" + str(self.get_next_highest_z()), ORPG_DEBUG)
+            xml_str = "<map><miniatures>"
+            xml_str += mini.toxml("new")
+            xml_str += "</miniatures></map>"
+            self.canvas.frame.session.send(xml_str)
+        else:
+            self.log.log("Invalid image " + path + " has been ignored!", ORPG_DEBUG)
+        self.log.log("Exit miniature_layer->add_miniature(self, id, path, pos, label, heading, face, width, height)", ORPG_DEBUG)
+
+    def get_miniature_by_id(self, id):
+        self.log.log("Enter miniature_layer->get_miniature_by_id(self, id)", ORPG_DEBUG)
+        for mini in self.miniatures:
+            if str(mini.id) == str(id):
+                self.log.log("Exit miniature_layer->get_miniature_by_id(self, id) return miniID: " + str(id), ORPG_DEBUG)
+                return mini
+        self.log.log("Exit miniature_layer->get_miniature_by_id(self, id) return None", ORPG_DEBUG)
+        return None
+
+    def del_miniature(self, min):
+        self.log.log("Enter miniature_layer->del_miniature(self, min)", ORPG_DEBUG)
+        xml_str = "<map><miniatures>"
+        xml_str += min.toxml("del")
+        xml_str += "</miniatures></map>"
+        self.canvas.frame.session.send(xml_str)
+        self.miniatures.remove(min)
+        del min
+        self.collapse_zorder()
+        self.log.log("Exit miniature_layer->del_miniature(self, min)", ORPG_DEBUG)
+
+    def del_all_miniatures(self):
+        self.log.log("Enter miniature_layer->del_all_miniatures(self)", ORPG_DEBUG)
+        while len(self.miniatures):
+            min = self.miniatures.pop()
+            del min
+        self.collapse_zorder()
+        self.log.log("Exit miniature_layer->del_all_miniatures(self)", ORPG_DEBUG)
+
+    def layerDraw(self, dc, topleft, size):
+        self.log.log("Enter miniature_layer->layerDraw(self, dc, topleft, size)", ORPG_DEBUG)
+        sorted_miniatures = self.miniatures[:]
+        sorted_miniatures.sort(cmp_zorder)
+        for m in sorted_miniatures:
+            if (m.pos.x>topleft[0]-m.right and
+                m.pos.y>topleft[1]-m.bottom and
+                m.pos.x<topleft[0]+size[0]-m.left and
+                m.pos.y<topleft[1]+size[1]-m.top):
+                m.draw(dc, self)
+        self.log.log("Exit miniature_layer->layerDraw(self, dc, topleft, size)", ORPG_DEBUG)
+
+    def find_miniature(self, pt, only_unlocked=False):
+        self.log.log("Enter miniature_layer->find_miniature(self, pt, only_unlocked)", ORPG_DEBUG)
+        min_list = []
+        for m in self.miniatures:
+            if m.hit_test(pt):
+                if m.hide and self.canvas.frame.session.my_role() != self.canvas.frame.session.ROLE_GM:
+                    continue
+                if only_unlocked and not m.locked:
+                    min_list.append(m)
+                elif not only_unlocked and m.locked:
+                    min_list.append(m)
+                else:
+                    continue
+        if len(min_list) > 0:
+            self.log.log("Exit miniature_layer->find_miniature(self, pt, only_unlocked)", ORPG_DEBUG)
+            return min_list
+        else:
+            self.log.log("Exit miniature_layer->find_miniature(self, pt, only_unlocked)", ORPG_DEBUG)
+            return None
+
+    def layerToXML(self, action="update"):
+        """ format  """
+        self.log.log("Enter miniature_layer->layerToXML(self, " + action + ")", ORPG_DEBUG)
+        minis_string = ""
+        if self.miniatures:
+            for m in self.miniatures:
+                minis_string += m.toxml(action)
+        if minis_string != '':
+            s = "<miniatures"
+            s += " serial='" + str(self.serial_number) + "'"
+            s += ">"
+            s += minis_string
+            s += "</miniatures>"
+            self.log.log("Exit miniature_layer->layerToXML(self, " + action + ")", ORPG_DEBUG)
+            return s
+        else:
+            self.log.log("Exit miniature_layer->layerToXML(self, " + action + ") return None", ORPG_DEBUG)
+            return ""
+
+    def layerTakeDOM(self, xml_dom):
+        self.log.log("Enter miniature_layer->layerTakeDOM(self, xml_dom)", ORPG_DEBUG)
+        if xml_dom.hasAttribute('serial'):
+            self.serial_number = int(xml_dom.getAttribute('serial'))
+        children = xml_dom._get_childNodes()
+        for c in children:
+            action = c.getAttribute("action")
+            id = c.getAttribute('id')
+            if action == "del":
+                mini = self.get_miniature_by_id(id)
+                if mini:
+                    self.miniatures.remove(mini)
+                    del mini
+                else:
+                    self.log.log("Map Synchronization Error :: Update of unknown mini attempted", ORPG_DEBUG)
+                    #wx.MessageBox("Deletion of unknown mini attempted","Map Synchronization Error")
+            elif action == "new":
+                pos = cmpPoint(int(c.getAttribute('posx')),int(c.getAttribute('posy')))
+                path = urllib.unquote(c.getAttribute('path'))
+                label = c.getAttribute('label')
+                height = width = heading = face = snap_to_align = zorder = 0
+                locked = hide = False
+                if c.hasAttribute('height'):
+                    height = int(c.getAttribute('height'))
+                if c.hasAttribute('width'):
+                    width = int(c.getAttribute('width'))
+                if c.getAttribute('locked') == 'True' or c.getAttribute('locked') == '1':
+                    locked = True
+                if c.getAttribute('hide') == 'True' or c.getAttribute('hide') == '1':
+                    hide = True
+                if c.getAttribute('heading'):
+                    heading = int(c.getAttribute('heading'))
+                if c.hasAttribute('face'):
+                    face = int(c.getAttribute('face'))
+                if c.hasAttribute('align'):
+                    snap_to_align = int(c.getAttribute('align'))
+                if c.getAttribute('zorder'):
+                    zorder = int(c.getAttribute('zorder'))
+                min = BmpMiniature(id, path, ImageHandler.load(path, 'miniature', id), pos, heading, face, label, locked, hide, snap_to_align, zorder, width, height, self.log)
+                self.miniatures.append(min)
+                if c.hasAttribute('local') and c.getAttribute('local') == 'True' and os.path.exists(urllib.unquote(c.getAttribute('localPath'))):
+                    localPath = urllib.unquote(c.getAttribute('localPath'))
+                    local = True
+                    localTime = float(c.getAttribute('localTime'))
+                    if localTime-time.time() <= 144000:
+                        file = open(localPath, "rb")
+                        imgdata = file.read()
+                        file.close()
+                        filename = os.path.split(localPath)
+                        (imgtype,j) = mimetypes.guess_type(filename[1])
+                        postdata = urllib.urlencode({'filename':filename[1], 'imgdata':imgdata, 'imgtype':imgtype})
+                        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
+                self.collapse_zorder()
+            else:
+                mini = self.get_miniature_by_id(id)
+                if mini:
+                    mini.takedom(c)
+                else:
+                    self.log.log("Map Synchronization Error :: Update of unknown mini attempted", ORPG_DEBUG)
+                    #wx.MessageBox("Update of unknown mini attempted","Map Synchronization Error")
+        self.log.log("Exit miniature_layer->layerTakeDOM(self, xml_dom)", ORPG_DEBUG)
+
+    def upload(self, postdata, filename, modify=False, pos=cmpPoint(0,0)):
+        self.lock.acquire()
+        url = self.settings.get_setting('ImageServerBaseURL')
+        file = urllib.urlopen(url, postdata)
+        recvdata = file.read()
+        file.close()
+        try:
+            xml_dom = minidom.parseString(recvdata)._get_documentElement()
+            if xml_dom.nodeName == 'path':
+                path = xml_dom.getAttribute('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:
+                    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_dom.getAttribute('msg')
+        except Exception, e:
+            print e
+            print recvdata
+        urllib.urlcleanup()
+        self.lock.release()
+####################################################################
+        ## helper function
+
+    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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/miniatures_handler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,860 @@
+# 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: mapper/whiteboard_hander.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: miniatures_handler.py,v 1.43 2007/12/07 20:39:50 digitalxero Exp $
+#
+# Description: Miniature layer handler
+#
+__version__ = "$Id: miniatures_handler.py,v 1.43 2007/12/07 20:39:50 digitalxero Exp $"
+
+from base_handler import *
+from min_dialogs import *
+import thread
+import time
+import mimetypes
+import urllib
+import xml.dom.minidom as minidom
+import wx
+from grid import GRID_RECTANGLE
+from grid import GRID_HEXAGON
+from grid import GRID_ISOMETRIC
+LABEL_TOOL = wx.NewId()
+LAYER_TOOL = wx.NewId()
+MIN_LIST_TOOL = wx.NewId()
+MIN_TOOL = wx.NewId()
+MIN_URL = wx.NewId()
+SERIAL_TOOL = wx.NewId()
+MIN_MOVE = wx.NewId()
+MIN_REMOVE = wx.NewId()
+MIN_PROP_DLG = wx.NewId()
+MIN_FACING_NONE = wx.NewId()
+MIN_FACING_MATCH = wx.NewId()
+MIN_FACING_EAST = wx.NewId()
+MIN_FACING_WEST = wx.NewId()
+MIN_FACING_NORTH = wx.NewId()
+MIN_FACING_SOUTH = wx.NewId()
+MIN_FACING_NORTHEAST = wx.NewId()
+MIN_FACING_SOUTHEAST = wx.NewId()
+MIN_FACING_SOUTHWEST = wx.NewId()
+MIN_FACING_NORTHWEST = wx.NewId()
+MIN_HEADING_NONE = wx.NewId()
+MIN_HEADING_MATCH = wx.NewId()
+MIN_HEADING_EAST = wx.NewId()
+MIN_HEADING_WEST = wx.NewId()
+MIN_HEADING_NORTH = wx.NewId()
+MIN_HEADING_SOUTH = wx.NewId()
+MIN_HEADING_NORTHEAST = wx.NewId()
+MIN_HEADING_SOUTHEAST = wx.NewId()
+MIN_HEADING_SOUTHWEST = wx.NewId()
+MIN_HEADING_NORTHWEST = wx.NewId()
+MIN_HEADING_SUBMENU = wx.NewId()
+MIN_FACING_SUBMENU = wx.NewId()
+MIN_ALIGN_SUBMENU = wx.NewId()
+MIN_ALIGN_GRID_CENTER = wx.NewId()
+MIN_ALIGN_GRID_TL = wx.NewId()
+MIN_TITLE_HACK = wx.NewId()
+MIN_TO_GAMETREE = wx.NewId()
+MIN_BACK_ONE = wx.NewId()
+MIN_FORWARD_ONE = wx.NewId()
+MIN_TO_BACK = wx.NewId()
+MIN_TO_FRONT = wx.NewId()
+MIN_LOCK_BACK = wx.NewId()
+MIN_LOCK_FRONT = wx.NewId()
+MIN_FRONTBACK_UNLOCK = wx.NewId()
+MIN_ZORDER_SUBMENU = wx.NewId()
+MIN_SHOW_HIDE = wx.NewId()
+MIN_LOCK_UNLOCK = wx.NewId()
+MAP_REFRESH_MINI_URLS = wx.NewId()
+
+class myFileDropTarget(wx.FileDropTarget):
+    def __init__(self, handler):
+        wx.FileDropTarget.__init__(self)
+        self.m_handler = handler
+    def OnDropFiles(self, x, y, filenames):
+        self.m_handler.on_drop_files(x, y, filenames)
+
+class miniatures_handler(base_layer_handler):
+
+    def __init__(self, parent, id, canvas):
+        self.sel_min = None
+        self.auto_label = 1
+        self.use_serial = 1
+        self.auto_label_cb = None
+        self.canvas = canvas
+        self.settings = self.canvas.settings
+        self.mini_rclick_menu_extra_items = {}
+        self.background_rclick_menu_extra_items = {}
+        base_layer_handler.__init__(self, parent, id, canvas)
+        # id is the index of the last good menu choice or 'None'
+        # if the last menu was left without making a choice
+        # should be -1 at other times to prevent events overlapping
+        self.lastMenuChoice = None
+        self.drag_mini = None
+        self.tooltip_delay_miliseconds = 500
+        self.tooltip_timer = wx.CallLater(self.tooltip_delay_miliseconds, self.on_tooltip_timer)
+        self.tooltip_timer.Stop()
+        dt = myFileDropTarget(self)
+        self.canvas.SetDropTarget(dt)
+   #     wxInitAllImageHandlers()
+
+    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')
+        minilist = createMaskedButton( self, orpg.dirpath.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)
+        self.sizer.Add(wx.Size(20,25))
+        self.sizer.Add(minilist, 0, wx.EXPAND )
+        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)
+
+    def on_browse(self, evt):
+        if not self.role_is_gm_or_player():
+            return
+        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)
+        if not dlg.ShowModal() == wx.ID_OK:
+            dlg.Destroy()
+            return
+        file = open(dlg.GetPath(), "rb")
+        imgdata = file.read()
+        file.close()
+        filename = dlg.GetFilename()
+        (imgtype,j) = mimetypes.guess_type(filename)
+        postdata = urllib.urlencode({'filename':filename, 'imgdata':imgdata, 'imgtype':imgtype})
+        if self.settings.get_setting('LocalorRemote') == 'Remote':
+            # make the new mini appear in top left of current viewable map
+            dc = wx.ClientDC(self.canvas)
+            self.canvas.PrepareDC(dc)
+            dc.SetUserScale(self.canvas.layers['grid'].mapscale,self.canvas.layers['grid'].mapscale)
+            x = dc.DeviceToLogicalX(0)
+            y = dc.DeviceToLogicalY(0)
+            thread.start_new_thread(self.canvas.layers['miniatures'].upload, (postdata, dlg.GetPath()), {'pos':cmpPoint(x,y)})
+        else:
+            min_url = self.settings.get_setting('LocalImageBaseURL') + filename
+            if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Textures' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Textures': min_url = self.settings.get_setting('LocalImageBaseURL') + 'Textures/' + filename
+            if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Maps' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Maps': min_url = self.settings.get_setting('ImageServerBaseURL') + 'Maps/' + filename
+            if dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles/Miniatures' or dlg.GetDirectory() == orpg.dirpath.dir_struct["user"]+'webfiles\Miniatures': min_url = self.settings.get_setting('LocalImageBaseURL') + 'Miniatures/' + filename
+            # build url
+            if min_url == "" or min_url == "http://":
+                return
+            if min_url[:7] != "http://" :
+                min_url = "http://" + min_url
+            # make label
+            if self.auto_label and min_url[-4:-3] == '.':
+                start = min_url.rfind("/") + 1
+                min_label = min_url[start:len(min_url)-4]
+                if self.use_serial:
+                    min_label = '%s %d' % ( min_label, self.canvas.layers['miniatures'].next_serial() )
+            else:
+                min_label = ""
+            if self.min_url.FindString(min_url) == -1:
+                self.min_url.Append(min_url)
+            try:
+                id = 'mini-' + self.canvas.frame.session.get_next_id()
+                # make the new mini appear in top left of current viewable map
+                dc = wx.ClientDC(self.canvas)
+                self.canvas.PrepareDC(dc)
+                dc.SetUserScale(self.canvas.layers['grid'].mapscale,self.canvas.layers['grid'].mapscale)
+                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:
+                # 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
+                dlg = wx.MessageDialog(self,unablemsg, 'Url not found',wx.ICON_EXCLAMATION)
+                dlg.ShowModal()
+                dlg.Destroy()
+                self.canvas.layers['miniatures'].rollback_serial()
+            self.canvas.send_map_data()
+            self.canvas.Refresh(False)
+
+
+    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(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_serial, id=SERIAL_TOOL)
+        # build miniature meenu
+        self.min_menu = wx.Menu()
+        # Rectangles and hexagons require slightly different menus because of
+        # facing and heading possibilities.
+        heading_menu = wx.Menu()
+        face_menu = wx.Menu()
+        face_menu.Append(MIN_FACING_NONE,"&None")
+        face_menu.Append(MIN_FACING_NORTH,"&North")
+        face_menu.Append(MIN_FACING_NORTHEAST,"Northeast")
+        face_menu.Append(MIN_FACING_EAST,"East")
+        face_menu.Append(MIN_FACING_SOUTHEAST,"Southeast")
+        face_menu.Append(MIN_FACING_SOUTH,"&South")
+        face_menu.Append(MIN_FACING_SOUTHWEST,"Southwest")
+        face_menu.Append(MIN_FACING_WEST,"West")
+        face_menu.Append(MIN_FACING_NORTHWEST,"Northwest")
+        heading_menu.Append(MIN_HEADING_NONE,"&None")
+        heading_menu.Append(MIN_HEADING_NORTH,"&North")
+        heading_menu.Append(MIN_HEADING_NORTHEAST,"Northeast")
+        heading_menu.Append(MIN_HEADING_EAST,"East")
+        heading_menu.Append(MIN_HEADING_SOUTHEAST,"Southeast")
+        heading_menu.Append(MIN_HEADING_SOUTH,"&South")
+        heading_menu.Append(MIN_HEADING_SOUTHWEST,"Southwest")
+        heading_menu.Append(MIN_HEADING_WEST,"West")
+        heading_menu.Append(MIN_HEADING_NORTHWEST,"Northwest")
+        align_menu = wx.Menu()
+        align_menu.Append(MIN_ALIGN_GRID_CENTER,"&Center")
+        align_menu.Append(MIN_ALIGN_GRID_TL,"&Top-Left")
+        #  This is a hack to simulate a menu title, due to problem in Linux
+        if wx.Platform == '__WXMSW__':
+            self.min_menu.SetTitle(label)
+        else:
+            self.min_menu.Append(MIN_TITLE_HACK,label)
+            self.min_menu.AppendSeparator()
+        self.min_menu.Append(MIN_SHOW_HIDE,"Show / Hide")
+        self.min_menu.Append(MIN_LOCK_UNLOCK, "Lock / Unlock")
+        self.min_menu.Append(MIN_REMOVE,"&Remove")
+        self.min_menu.Append(MIN_TO_GAMETREE,"To &Gametree")
+        self.min_menu.AppendMenu(MIN_HEADING_SUBMENU,"Set &Heading",heading_menu)
+        self.min_menu.AppendMenu(MIN_FACING_SUBMENU,"Set &Facing",face_menu)
+        self.min_menu.AppendMenu(MIN_ALIGN_SUBMENU,"Snap-to &Alignment",align_menu)
+        self.min_menu.AppendSeparator()
+        zorder_menu = wx.Menu()
+        zorder_menu.Append(MIN_BACK_ONE,"Back one")
+        zorder_menu.Append(MIN_FORWARD_ONE,"Forward one")
+        zorder_menu.Append(MIN_TO_BACK,"To back")
+        zorder_menu.Append(MIN_TO_FRONT,"To front")
+        zorder_menu.AppendSeparator()
+        zorder_menu.Append(MIN_LOCK_BACK,"Lock to back")
+        zorder_menu.Append(MIN_LOCK_FRONT,"Lock to front")
+        zorder_menu.Append(MIN_FRONTBACK_UNLOCK,"Unlock Front/Back")
+        self.min_menu.AppendMenu(MIN_ZORDER_SUBMENU, "Miniature Z-Order",zorder_menu)
+        #self.min_menu.Append(MIN_LOCK,"&Lock")
+        self.min_menu.AppendSeparator()
+        self.min_menu.Append(MIN_PROP_DLG,"&Properties")
+        self.min_menu.AppendSeparator()
+        self.min_menu.Append(MIN_MOVE, "Move")
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_MOVE)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_SHOW_HIDE)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_LOCK_UNLOCK)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_REMOVE)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_TO_GAMETREE)
+        #self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_LOCK)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_PROP_DLG)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_NONE)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_EAST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_WEST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_NORTH)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_SOUTH)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_NORTHEAST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_SOUTHEAST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_SOUTHWEST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FACING_NORTHWEST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_NONE)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_EAST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_WEST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_NORTH)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_SOUTH)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_NORTHEAST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_SOUTHEAST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_SOUTHWEST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_HEADING_NORTHWEST)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_ALIGN_GRID_CENTER)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_ALIGN_GRID_TL)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_BACK_ONE)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FORWARD_ONE)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_TO_BACK)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_TO_FRONT)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_LOCK_BACK)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_LOCK_FRONT)
+        self.canvas.Bind(wx.EVT_MENU, self.on_min_menu_item, id=MIN_FRONTBACK_UNLOCK)
+        ######### add plugin added menu items #########
+        if len(self.mini_rclick_menu_extra_items)>0:
+            self.min_menu.AppendSeparator()
+            for item in self.mini_rclick_menu_extra_items.items():
+                self.min_menu.Append(item[1], item[0])
+        if len(self.background_rclick_menu_extra_items)>0:
+            self.main_menu.AppendSeparator()
+            for item in self.background_rclick_menu_extra_items.items():
+                self.main_menu.Append(item[1], item[0])
+
+    def do_min_menu(self,pos):
+        self.canvas.PopupMenu(self.min_menu,pos)
+
+    def do_min_select_menu(self, min_list, pos):
+        # to prevent another event being processed
+        self.lastMenuChoice = None
+        self.min_select_menu = wx.Menu()
+        self.min_select_menu.SetTitle("Select Miniature")
+        loop_count = 1
+        try:
+            for m in min_list:
+                # Either use the miniatures label for the selection list
+                if m.label:
+                    self.min_select_menu.Append(loop_count, m.label)
+                # Or use part of the images filename as an identifier
+                else:
+                    string_split = string.split(m.path,"/",)
+                    last_string = string_split[len(string_split)-1]
+                    self.min_select_menu.Append(loop_count, 'Unlabeled - ' + last_string[:len(last_string)-4])
+                self.canvas.Bind(wx.EVT_MENU, self.min_selected, id=loop_count)
+                loop_count += 1
+            self.canvas.PopupMenu(self.min_select_menu,pos)
+        except:
+            pass
+
+    def min_selected(self,evt):
+        # this is the callback function for the menu that is used to choose
+        # between minis when you right click, left click or left double click
+        # on a stack of two or more
+        self.canvas.Refresh(False)
+        self.canvas.send_map_data()
+        self.lastMenuChoice = evt.GetId()-1
+
+    def on_min_menu_item(self,evt):
+        id = evt.GetId()
+        if id == MIN_MOVE:
+            if self.sel_min:
+                self.moveSelectedMini(self.last_rclick_pos)
+                self.deselectAndRefresh()
+            return
+        elif id == MIN_REMOVE:
+            self.canvas.layers['miniatures'].del_miniature(self.sel_rmin)
+        elif id == MIN_TO_GAMETREE:
+            min_xml = self.sel_rmin.toxml(action="new")
+            node_begin = "<nodehandler module='map_miniature_nodehandler' class='map_miniature_handler' name='"
+            if self.sel_rmin.label:
+                node_begin += self.sel_rmin.label + "'"
+            else:
+                node_begin += "Unnamed Miniature'"
+            node_begin += ">"
+	    gametree = open_rpg.get_component('tree')
+            node_xml = node_begin + min_xml + '</nodehandler>'
+            #print "Sending this XML to insert_xml:" + node_xml
+            gametree.insert_xml(node_xml)
+        elif id == MIN_SHOW_HIDE:
+            if self.sel_rmin.hide:
+                self.sel_rmin.hide = 0
+            else:
+                self.sel_rmin.hide = 1
+        elif id == MIN_LOCK_UNLOCK:
+            if self.sel_rmin.locked:
+                self.sel_rmin.locked = False
+            else:
+                self.sel_rmin.locked = True
+            if self.sel_rmin == self.sel_min:
+                # when we lock / unlock the selected mini make sure it isn't still selected
+                # or it might easily get moved by accident and be hard to move back
+                self.sel_min.selected = False
+                self.sel_min.isUpdated = True
+                self.sel_min = None
+	recycle_bin = {MIN_HEADING_NONE: FACE_NONE, MIN_HEADING_NORTH: FACE_NORTH, MIN_HEADING_NORTHWEST: FACE_NORTHWEST, MIN_HEADING_NORTHEAST: FACE_NORTHEAST, MIN_HEADING_EAST: FACE_EAST, MIN_HEADING_SOUTHEAST: FACE_SOUTHEAST, MIN_HEADING_SOUTHWEST: FACE_SOUTHWEST, MIN_HEADING_SOUTH: FACE_SOUTH, MIN_HEADING_WEST: FACE_WEST}
+	if recycle_bin.has_key(id):
+	    self.sel_rmin.heading = recycle_bin[id]
+	    recycle_bin = {}
+	recycle_bin = {MIN_FACING_NONE: FACE_NONE, MIN_FACING_NORTH: FACE_NORTH, MIN_FACING_NORTHWEST: FACE_NORTHWEST, MIN_FACING_NORTHEAST: FACE_NORTHEAST, MIN_FACING_EAST: FACE_EAST, MIN_FACING_SOUTHEAST: FACE_SOUTHEAST, MIN_FACING_SOUTHWEST: FACE_SOUTHWEST, MIN_FACING_SOUTH: FACE_SOUTH, MIN_FACING_WEST: FACE_WEST}
+	if recycle_bin.has_key(id):
+	    self.sel_rmin.face = recycle_bin[id]
+	    recycle_bin = {}
+        elif id == MIN_ALIGN_GRID_CENTER:
+            self.sel_rmin.snap_to_align = SNAPTO_ALIGN_CENTER
+        elif id == MIN_ALIGN_GRID_TL:
+            self.sel_rmin.snap_to_align = SNAPTO_ALIGN_TL
+        elif id == MIN_PROP_DLG:
+            old_lock_value = self.sel_rmin.locked
+            dlg = min_edit_dialog(self.canvas.frame.GetParent(),self.sel_rmin)
+            if dlg.ShowModal() == wx.ID_OK:
+                if self.sel_rmin == self.sel_min and self.sel_rmin.locked != old_lock_value:
+                    # when we lock / unlock the selected mini make sure it isn't still selected
+                    # or it might easily get moved by accident and be hard to move back
+                    self.sel_min.selected = False
+                    self.sel_min.isUpdated = True
+                    self.sel_min = None
+                self.canvas.Refresh(False)
+                self.canvas.send_map_data()
+                return
+
+        elif id == MIN_BACK_ONE:
+            #  This assumes that we always start out with a z-order
+            #     that starts at 0 and goes up to the number of
+            #     minis - 1.  If this isn't the case, then execute
+            #     a self.canvas.layers['miniatures'].collapse_zorder()
+            #     before getting the oldz to test
+            #  Save the selected minis current z-order
+            oldz = self.sel_rmin.zorder
+            # Make sure the mini isn't sticky front or back
+            if (oldz != MIN_STICKY_BACK) and (oldz != MIN_STICKY_FRONT):
+		##   print "old z-order = " + str(oldz)
+                self.sel_rmin.zorder -= 1
+                #  Re-collapse to normalize
+                #  Note:  only one update (with the final values) will be sent
+                self.canvas.layers['miniatures'].collapse_zorder()
+
+        elif id == MIN_FORWARD_ONE:
+            #  This assumes that we always start out with a z-order
+            #     that starts at 0 and goes up to the number of
+            #     minis - 1.  If this isn't the case, then execute
+            #     a self.canvas.layers['miniatures'].collapse_zorder()
+            #     before getting the oldz to test
+            #  Save the selected minis current z-order
+            oldz = self.sel_rmin.zorder
+	    ##  print "old z-order = " + str(oldz)
+            self.sel_rmin.zorder += 1
+
+            #  Re-collapse to normalize
+            #  Note:  only one update (with the final values) will be sent
+            self.canvas.layers['miniatures'].collapse_zorder()
+
+        elif id == MIN_TO_FRONT:
+            #  This assumes that we always start out with a z-order
+            #     that starts at 0 and goes up to the number of
+            #     minis - 1.  If this isn't the case, then execute
+            #     a self.canvas.layers['miniatures'].collapse_zorder()
+            #     before getting the oldz to test
+            #  Save the selected minis current z-order
+            oldz = self.sel_rmin.zorder
+
+            # Make sure the mini isn't sticky front or back
+            if (oldz != MIN_STICKY_BACK) and (oldz != MIN_STICKY_FRONT):
+	    ##  print "old z-order = " + str(oldz)
+                #  The new z-order will be one more than the last index
+                newz = len(self.canvas.layers['miniatures'].miniatures)
+	    ##  print "new z-order = " + str(newz)
+                self.sel_rmin.zorder = newz
+                #  Re-collapse to normalize
+                #  Note:  only one update (with the final values) will be sent
+                self.canvas.layers['miniatures'].collapse_zorder()
+
+        elif id == MIN_TO_BACK:
+            #  This assumes that we always start out with a z-order
+            #     that starts at 0 and goes up to the number of
+            #     minis - 1.  If this isn't the case, then execute
+            #     a self.canvas.layers['miniatures'].collapse_zorder()
+            #     before getting the oldz to test
+            #  Save the selected minis current z-order
+            oldz = self.sel_rmin.zorder
+            # Make sure the mini isn't sticky front or back
+            if (oldz != MIN_STICKY_BACK) and (oldz != MIN_STICKY_FRONT):
+	    ##  print "old z-order = " + str(oldz)
+
+                #  Since 0 is the lowest in a normalized order, be one less
+                newz = -1
+	    ##  print "new z-order = " + str(newz)
+                self.sel_rmin.zorder = newz
+                #  Re-collapse to normalize
+                #  Note:  only one update (with the final values) will be sent
+                self.canvas.layers['miniatures'].collapse_zorder()
+
+        elif id == MIN_FRONTBACK_UNLOCK:
+            #print "Unlocked/ unstickified..."
+            if self.sel_rmin.zorder == MIN_STICKY_BACK:
+                self.sel_rmin.zorder = MIN_STICKY_BACK + 1
+            elif self.sel_rmin.zorder == MIN_STICKY_FRONT:
+                self.sel_rmin.zorder = MIN_STICKY_FRONT - 1
+        elif id == MIN_LOCK_BACK:
+            #print "lock back"
+            self.sel_rmin.zorder = MIN_STICKY_BACK
+        elif id == MIN_LOCK_FRONT:
+            #print "lock front"
+            self.sel_rmin.zorder = MIN_STICKY_FRONT
+        # Pretty much, we always want to refresh when we go through here
+        # This helps us remove the redundant self.Refresh() on EVERY menu event
+        # that we process above.
+        self.sel_rmin.isUpdated = True
+        self.canvas.Refresh(False)
+        self.canvas.send_map_data()
+
+    def on_miniature(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()):
+            self.infoPost("You must be either a player or GM to use the miniature Layer")
+            return
+        min_url = self.min_url.GetValue()
+        # build url
+        if min_url == "" or min_url == "http://":
+            return
+        if min_url[:7] != "http://" :
+            min_url = "http://" + min_url
+        # make label
+        if self.auto_label and min_url[-4:-3] == '.':
+            start = min_url.rfind("/") + 1
+            min_label = min_url[start:len(min_url)-4]
+            if self.use_serial:
+                min_label = '%s %d' % ( min_label, self.canvas.layers['miniatures'].next_serial() )
+        else:
+            min_label = ""
+        if self.min_url.FindString(min_url) == -1:
+            self.min_url.Append(min_url)
+        try:
+            id = 'mini-' + self.canvas.frame.session.get_next_id()
+            # make the new mini appear in top left of current viewable map
+            dc = wx.ClientDC(self.canvas)
+            self.canvas.PrepareDC(dc)
+            dc.SetUserScale(self.canvas.layers['grid'].mapscale,self.canvas.layers['grid'].mapscale)
+            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:
+            # 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
+            dlg = wx.MessageDialog(self,unablemsg, 'Url not found',wx.ICON_EXCLAMATION)
+            dlg.ShowModal()
+            dlg.Destroy()
+            self.canvas.layers['miniatures'].rollback_serial()
+        self.canvas.send_map_data()
+        self.canvas.Refresh(False)
+        #except Exception, e:
+            #wx.MessageBox(str(e),"Miniature Error")
+
+    def on_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
+        if (session.my_role() != session.ROLE_GM):
+            self.infoPost("You must be a GM to use this feature")
+            return
+        #d = min_list_panel(self.frame.GetParent(),self.canvas.layers,"Miniature list")
+        d = min_list_panel(self.canvas.frame,self.canvas.layers,"Miniature list")
+        if d.ShowModal() == wx.ID_OK:
+            d.Destroy()
+        self.canvas.Refresh(False)
+
+    def on_serial(self, evt):
+        self.use_serial = not self.use_serial
+
+    def on_map_board_menu_item(self,evt):
+        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
+            self.canvas.Refresh(False)
+
+####################################################################
+    ## old functions, changed an awful lot
+
+    def on_left_down(self, evt):
+        if not self.role_is_gm_or_player() or self.alreadyDealingWithMenu():
+            return
+        mini = self.find_mini(evt, evt.ControlDown() and self.role_is_gm())
+        if mini:
+            deselecting_selected_mini = (mini == self.sel_min) #clicked on the selected mini
+            self.deselectAndRefresh()
+            self.drag_mini = mini
+            if deselecting_selected_mini:
+                return
+            self.sel_min = mini
+            self.sel_min.selected = True
+            dc = wx.ClientDC(self.canvas)
+            self.canvas.PrepareDC(dc)
+            dc.SetUserScale(self.canvas.layers['grid'].mapscale,self.canvas.layers['grid'].mapscale)
+            self.sel_min.draw(dc, self.canvas.layers['miniatures'])
+        else:
+            self.drag_mini = None
+            pos = self.getLogicalPosition(evt)
+            self.moveSelectedMini(pos)
+            self.deselectAndRefresh()
+
+    def on_right_down(self, evt):
+        if not self.role_is_gm_or_player() or self.alreadyDealingWithMenu():
+            return
+        self.last_rclick_pos = self.getLogicalPosition(evt)
+        mini = self.find_mini(evt, evt.ControlDown() and self.role_is_gm())
+        if mini:
+            self.sel_rmin = mini
+            if self.sel_min:
+                self.min_menu.Enable(MIN_MOVE, True)
+            else:
+                self.min_menu.Enable(MIN_MOVE, False)
+            self.prepare_mini_rclick_menu(evt)
+            self.do_min_menu(evt.GetPosition())
+        else:# pass it on
+            if self.sel_min:
+                self.main_menu.Enable(MIN_MOVE, True)
+            else:
+                self.main_menu.Enable(MIN_MOVE, False)
+            self.prepare_background_rclick_menu(evt)
+            base_layer_handler.on_right_down(self, evt)
+
+####################################################################
+    ## new functions
+
+    def on_drop_files(self, x, y, filepaths):
+        # currently we ignore multiple files
+        filepath = filepaths[0]
+        start1 = filepath.rfind("\\") + 1 # check for both slashes in path to be on the safe side
+        start2 = filepath.rfind("/") + 1
+        if start1 < start2:
+            start1 = start2
+        filename = filepath[start1:]
+        pos = filename.rfind('.')
+        ext = filename[pos:].lower()
+   	# ext = filename[-4:].lower()
+        if(ext != ".bmp" and ext != ".gif" and ext != ".jpg" and ext != ".jpeg" and ext != ".png"):
+            self.infoPost("Supported file extensions are: *.bmp, *.gif, *.jpg, *.jpeg, *.png")
+            return
+        file = open(filepath, "rb")
+        imgdata = file.read()
+        file.close()
+        dc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(dc)
+        dc.SetUserScale(self.canvas.layers['grid'].mapscale,self.canvas.layers['grid'].mapscale)
+        x = dc.DeviceToLogicalX(x)
+        y = dc.DeviceToLogicalY(y)
+        (imgtype,j) = mimetypes.guess_type(filename)
+        postdata = urllib.urlencode({'filename':filename, 'imgdata':imgdata, 'imgtype':imgtype})
+        thread.start_new_thread(self.canvas.layers['miniatures'].upload, (postdata, filepath), {'pos':cmpPoint(x,y)})
+
+    def on_tooltip_timer(self, *args):
+        pos = args[0]
+        dc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(dc)
+        dc.SetUserScale(self.canvas.layers['grid'].mapscale,self.canvas.layers['grid'].mapscale)
+        pos = wx.Point(dc.DeviceToLogicalX(pos.x), dc.DeviceToLogicalY(pos.y))
+        mini_list = self.getMiniListOrSelectedMini(pos)
+        if len(mini_list) > 0:
+            tooltip = self.get_mini_tooltip(mini_list)
+            self.canvas.SetToolTipString(tooltip)
+        else:
+            self.canvas.SetToolTipString("")
+
+    def on_motion(self,evt):
+        if evt.Dragging() and evt.LeftIsDown():
+            if self.canvas.drag is None and self.drag_mini is not None:
+                drag_bmp = self.drag_mini.bmp
+                if self.drag_mini.width and self.drag_mini.height:
+                    tmp_image = drag_bmp.ConvertToImage()
+                    tmp_image.Rescale(int(self.drag_mini.width * self.canvas.layers['grid'].mapscale), int(self.drag_mini.height * self.canvas.layers['grid'].mapscale))
+                    tmp_image.ConvertAlphaToMask()
+                    drag_bmp = tmp_image.ConvertToBitmap()
+                    mask = wx.Mask(drag_bmp, wx.Colour(tmp_image.GetMaskRed(), tmp_image.GetMaskGreen(), tmp_image.GetMaskBlue()))
+                    drag_bmp.SetMask(mask)
+                    tmp_image = tmp_image.ConvertToGreyscale()
+                    self.drag_mini.gray = True
+                    self.drag_mini.isUpdated = True
+                    def refresh():
+                        self.canvas.drag.Hide()
+                        self.canvas.Refresh(False)
+                    wx.CallAfter(refresh)
+                self.canvas.drag = wx.DragImage(drag_bmp)
+                self.drag_offset = self.getLogicalPosition(evt)- self.drag_mini.pos
+                self.canvas.drag.BeginDrag((int(self.drag_offset.x * self.canvas.layers['grid'].mapscale), int(self.drag_offset.y * self.canvas.layers['grid'].mapscale)), self.canvas, False)
+            elif self.canvas.drag is not None:
+                self.canvas.drag.Move(evt.GetPosition())
+                self.canvas.drag.Show()
+        # reset tool tip timer
+        self.canvas.SetToolTipString("")
+        self.tooltip_timer.Restart(self.tooltip_delay_miliseconds, evt.GetPosition())
+
+    def on_left_up(self,evt):
+        if self.canvas.drag:
+            self.canvas.drag.Hide()
+            self.canvas.drag.EndDrag()
+            self.canvas.drag = None
+            pos = self.getLogicalPosition(evt)
+            pos = pos - self.drag_offset
+            if self.canvas.layers['grid'].snap:
+                nudge = int(self.canvas.layers['grid'].unit_size/2)
+                if self.canvas.layers['grid'].mode != GRID_ISOMETRIC:
+                    if self.drag_mini.snap_to_align == SNAPTO_ALIGN_CENTER:
+                        pos = pos + (int(self.drag_mini.bmp.GetWidth()/2),int(self.drag_mini.bmp.GetHeight()/2))
+                    else:
+                        pos = pos + (nudge, nudge)
+                else:# GRID_ISOMETRIC
+                    if self.drag_mini.snap_to_align == SNAPTO_ALIGN_CENTER:
+                        pos = pos + (int(self.drag_mini.bmp.GetWidth()/2), self.drag_mini.bmp.GetHeight())
+                    else:
+                        pass # no nudge for the isomorphic / top-left
+            self.sel_min = self.drag_mini
+            # check to see if the mouse is inside the window still
+            w = self.canvas.GetClientSizeTuple() # this is the window size, minus any scrollbars
+            p = evt.GetPosition() # compare the window size, w with the non-logical position
+            c = self.canvas.size # this is the grid size, compare with the logical position, pos
+            # both are [width, height]
+            if p.x>=0 and pos.x<c[0] and p.x<w[0] and p.y>=0 and pos.y<c[1] and p.y<w[1]:
+                self.moveSelectedMini(pos)
+            self.sel_min.gray = False
+            self.sel_min.selected = False
+            self.sel_min.isUpdated = True
+            self.canvas.Refresh(False)
+            self.canvas.send_map_data()
+            self.sel_min = None
+        self.drag_mini = None
+
+    def on_left_dclick(self,evt):
+        if not self.role_is_gm_or_player() or self.alreadyDealingWithMenu():
+            return
+        mini = self.find_mini(evt, evt.ControlDown() and self.role_is_gm())
+        if mini:
+            self.on_mini_dclick(evt, mini)
+        else:# pass it on
+            base_layer_handler.on_left_dclick(self, evt)
+
+
+####################################################################
+    ## hook functions (although with python you can override any of the functions)
+
+    def prepare_mini_rclick_menu(self, evt):
+        # override the entire right-click on a mini menu
+        pass
+
+    def prepare_background_rclick_menu(self, evt):
+        # override the entire right-click on the map menu
+        pass
+
+    def get_mini_tooltip(self, mini_list):
+        # override to create a tooltip
+        return ""
+
+    def on_mini_dclick(self, evt, mini):
+        # do something after the mini was left double clicked
+        pass
+
+####################################################################
+    ## easy way to add a single menu item
+
+    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:
+            del self.mini_rclick_menu_extra_items[label]
+        else:
+            if not self.mini_rclick_menu_extra_items.has_key(label):
+                self.mini_rclick_menu_extra_items[label]=wx.NewId()
+            menu_id = self.mini_rclick_menu_extra_items[label]
+            self.canvas.Bind(wx.EVT_MENU, callback_function, id=menu_id)
+        self.build_menu()
+
+    def set_background_rclick_menu_item(self, label, callback_function):
+        if callback_function == None:
+            del self.background_rclick_menu_extra_items[label]
+        else:
+            if not self.background_rclick_menu_extra_items.has_key(label):
+                self.background_rclick_menu_extra_items[label]=wx.NewId()
+            menu_id = self.background_rclick_menu_extra_items[label]
+            self.canvas.Bind(wx.EVT_MENU, callback_function, id=menu_id)
+        self.build_menu()
+
+
+####################################################################
+    ## helper functions
+
+    def infoPost(self, message):
+        open_rpg.get_component("chat").InfoPost(message)
+
+    def role_is_gm_or_player(self):
+        session = self.canvas.frame.session
+        if (session.my_role() <> session.ROLE_GM) and (session.my_role() <> session.ROLE_PLAYER) and (session.use_roles()):
+            self.infoPost("You must be either a player or GM to use the miniature Layer")
+            return False
+        return True
+
+    def role_is_gm(self):
+        session = self.canvas.frame.session
+        if (session.my_role() <> session.ROLE_GM) and (session.use_roles()):
+            return False
+        return True
+
+    def alreadyDealingWithMenu(self):
+        return self.lastMenuChoice is not None
+
+    def getLastMenuChoice(self):
+        choice = self.lastMenuChoice
+        self.lastMenuChoice = None
+        return choice
+
+    def getLogicalPosition(self, evt):
+        dc = wx.ClientDC(self.canvas)
+        self.canvas.PrepareDC(dc)
+        dc.SetUserScale(self.canvas.layers['grid'].mapscale,self.canvas.layers['grid'].mapscale)
+        pos = evt.GetLogicalPosition(dc)
+        return pos
+
+    def getMiniListOrSelectedMini(self, pos, include_locked=False):
+        if self.sel_min and self.sel_min.hit_test(pos):
+            # clicked on the selected mini - assume that is the intended target
+            # and don't give a choice of it and any other minis stacked with it
+            mini_list = []
+            mini_list.append(self.sel_min)
+            return mini_list
+        mini_list = self.canvas.layers['miniatures'].find_miniature(pos, (not include_locked))
+        if mini_list:
+            return mini_list
+        mini_list = []
+        return mini_list
+
+    def deselectAndRefresh(self):
+        if self.sel_min:
+            self.sel_min.selected = False
+            self.sel_min.isUpdated = True
+            self.canvas.Refresh(False)
+            self.canvas.send_map_data()
+            self.sel_min = None
+
+    def moveSelectedMini(self, pos):
+        if self.sel_min:
+            self.moveMini(pos, self.sel_min)
+
+    def moveMini(self, pos, mini):
+        grid = self.canvas.layers['grid']
+        mini.pos = grid.get_snapped_to_pos(pos, mini.snap_to_align, mini.bmp.GetWidth(), mini.bmp.GetHeight())
+
+    def find_mini(self, evt, include_locked):
+        if not self.role_is_gm_or_player() or self.alreadyDealingWithMenu():
+            return
+        pos = self.getLogicalPosition(evt)
+        mini_list = self.getMiniListOrSelectedMini(pos, include_locked)
+        mini = None
+        if len(mini_list) > 1:
+            try:
+                self.do_min_select_menu(mini_list, evt.GetPosition())
+            except:
+                pass
+            choice = self.getLastMenuChoice()
+            if choice == None:
+                return None # left menu without making a choice, eg by clicking outside menu
+            mini = mini_list[choice]
+        elif len(mini_list) == 1:
+            mini = mini_list[0]
+        return mini
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/miniatures_msg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,159 @@
+# 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: mapper/miniatures_msg.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: miniatures_msg.py,v 1.8 2006/11/04 21:24:21 digitalxero Exp $
+#
+# Description: This file contains some of the basic definitions for the chat
+# utilities in the orpg project.
+#
+__version__ = "$Id: miniatures_msg.py,v 1.8 2006/11/04 21:24:21 digitalxero Exp $"
+
+from base_msg import *
+
+class mini_msg(map_element_msg_base):
+
+    def __init__(self,reentrant_lock_object = None):
+        self.tagname = "miniature"   # set this to be for minis.  Tagname gets used in some base class functions.
+        map_element_msg_base.__init__(self,reentrant_lock_object)   # call base class
+
+
+
+    # convenience method to use if only this mini is modified
+    #   outputs a <map/> element containing only the changes to this mini
+    def standalone_update_text(self,update_id_string):
+        buffer = "<map id='" + update_id_string + "'>"
+        buffer += "<miniatures>"
+        buffer += self.get_changed_xml()
+        buffer += "</miniatures></map>"
+        return buffer
+
+    # convenience method to use if only this mini is modified
+    #   outputs a <map/> element that deletes this mini
+    def standalone_delete_text(self,update_id_string):
+        buffer = None
+
+        if self._props.has_key("id"):
+            buffer = "<map id='" + update_id_string + "'>"
+            buffer += "<miniatures>"
+            buffer += "<miniature action='del' id='" + self._props("id") + "'/>"
+            buffer += "</miniatures></map>"
+
+        return buffer
+
+    # convenience method to use if only this mini is modified
+    #   outputs a <map/> element to add this mini
+    def standalone_add_text(self,update_id_string):
+        buffer = "<map id='" + update_id_string + "'>"
+        buffer += "<miniatures>"
+        buffer += self.get_all_xml()
+        buffer += "</miniatures></map>"
+        return buffer
+
+    def get_all_xml(self,action="new",output_action=1):
+        return map_element_msg_base.get_all_xml(self,action,output_action)
+
+    def get_changed_xml(self,action="update",output_action=1):
+        return map_element_msg_base.get_changed_xml(self,action,output_action)
+
+
+
+class minis_msg(map_element_msg_base):
+
+    def __init__(self,reentrant_lock_object = None):
+        self.tagname = "miniatures"
+        map_element_msg_base.__init__(self,reentrant_lock_object)
+
+    def init_from_dom(self,xml_dom):
+        self.p_lock.acquire()
+        if xml_dom.tagName == self.tagname:
+            if xml_dom.getAttributeKeys():
+                for k in xml_dom.getAttributeKeys():
+                    self.init_prop(k,xml_dom.getAttribute(k))
+
+            for c in xml_dom._get_childNodes():
+                mini = mini_msg(self.p_lock)
+
+                try:
+                    mini.init_from_dom(c)
+                except Exception, e:
+                    print e
+                    continue
+
+                id = mini.get_prop("id")
+                action = mini.get_prop("action")
+
+
+                if action == "new":
+                    self.children[id] = mini
+
+                elif action == "del":
+                    if self.children.has_key(id):
+                        self.children[id] = None
+                        del self.children[id]
+
+                elif action == "update":
+                    if self.children.has_key(id):
+                        self.children[id].init_props(mini.get_all_props())
+
+        else:
+            self.p_lock.release()
+            raise Exception, "Error attempting to initialize a " + self.tagname + " from a non-<" + self.tagname + "/> element"
+        self.p_lock.release()
+
+
+
+    def set_from_dom(self,xml_dom):
+        self.p_lock.acquire()
+        if xml_dom.tagName == self.tagname:
+            if xml_dom.getAttributeKeys():
+                for k in xml_dom.getAttributeKeys():
+                    self.set_prop(k,xml_dom.getAttribute(k))
+
+            for c in xml_dom._get_childNodes():
+                mini = mini_msg(self.p_lock)
+
+                try:
+                    mini.set_from_dom(c)
+                except Exception, e:
+                    print e
+                    continue
+
+                id = mini.get_prop("id")
+                action = mini.get_prop("action")
+
+                if action == "new":
+                    self.children[id] = mini
+
+                elif action == "del":
+                    if self.children.has_key(id):
+                        self.children[id] = None
+                        del self.children[id]
+
+                elif action == "update":
+                    if self.children.has_key(id):
+                        self.children[id].set_props(mini.get_all_props())
+
+        else:
+            self.p_lock.release()
+            raise Exception, "Error attempting to set a " + self.tagname + " from a non-<" + self.tagname + "/> element"
+        self.p_lock.release()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/region.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,645 @@
+# 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: mapper/region.py
+# Author: Mark Tarrabain
+# Maintainer:
+# Version:
+#   $Id: region.py,v 1.10 2006/11/04 21:24:21 digitalxero Exp $
+#
+__version__ = "$Id: region.py,v 1.10 2006/11/04 21:24:21 digitalxero Exp $"
+
+import sys
+
+# This class manages the edge of a polygon
+# it is employed by scan_convert to trace each edge of the polygon as the
+# scan converter advances past each Y coordinate
+# A bresenham-type of algorithm is used to avoid expensive computation during
+# use - no floating point arithmetic is needed.
+class Edge:
+    def __init__(self,start,end):
+        self.startx=start.X
+        self.starty=start.Y
+        self.endx=end.X
+        self.endy=end.Y
+        self.dy=self.endy-self.starty
+        self.cx=self.startx
+        self.bottom=0
+        self.dir = 0
+        if self.endx>self.startx:
+            self.dx = int((self.endx-self.startx)/self.dy)
+        else:
+            self.dx = -int((self.startx-self.endx)/self.dy)
+        self.error = 0
+        if self.endx >= self.startx:
+            self.inc = (self.endx-self.startx)%self.dy
+        else:
+            self.inc = (self.startx-self.endx)%self.dy
+
+    def advance(self):
+        self.cx += self.dx
+        self.error += self.inc
+        if (self.error>=self.dy):
+            if (self.endx<self.startx):
+                self.cx -= 1
+            else:
+                self.cx += 1
+            self.error -= self.dy
+
+# Utilitarian class for describing a coordinate in 2D space
+class IPoint:
+    def __init__(self):
+        X=0
+        Y=0
+
+    def __str__(self):
+        return "(X:" + str(self.X) + ", Y:" + str(self.Y) + ")"
+
+    def pluseq(self,b):
+        self.X += b.X
+        self.Y += b.Y
+
+    def minuseq(self,b):
+        self.X -= b.X
+        self.Y -= b.Y
+
+    def make(self,x,y):
+        self.X=x
+        self.Y=y
+        return self
+
+    def equals(self,b):
+        if self.X==b.X and self.Y==b.Y:
+            return 1
+        return 0
+
+    def less(self,b):
+        if self.Y<b.Y or (self.Y==b.Y and self.X<b.X):
+            return 1
+        return 0
+
+    def greater(self,b):
+        if self.Y>b.Y or (self.Y==b.Y and self.X>b.X):
+            return 1
+        return 0
+
+# generic rectangle class
+class IRect:
+    def __init__(self): #initializes to an invalid rectangle
+        self.left=999999
+        self.top=999999
+        self.right=-999999
+        self.bottom=-999999
+
+    def __str__(self):
+        return "[ left:"+str(self.left)+", top:"+str(self.top)+", right:"+str(self.right)+", bottom:"+str(self.bottom)+" ]"
+
+    def ToPoly(self):
+        thelist = []
+        thelist.append(IPoint().make(self.left,self.top))
+        thelist.append(IPoint().make(self.right,self.top))
+        thelist.append(IPoint().make(self.right,self.bottom))
+        thelist.append(IPoint().make(self.left,self.bottom))
+        return thelist
+
+    def Width(self):
+        return self.right-self.left+1
+
+    def Height(self):
+        return self.bottom-self.top+1
+
+    def GetX(self):
+        return self.left
+
+    def GetY(self):
+        return self.top
+
+    def GetW(self):
+        return self.right-self.left+1
+
+    def GetH(self):
+        return self.bottom-self.top+1
+
+    def Size(self):
+        return IPoint().make(self.right-self.left+1,self.bottom-self.top+1)
+
+    def TopLeft(self):
+        return IPoint().make(self.left,self.top)
+
+    def BottomRight(self):
+        return IPoint().make(self.right,self.bottom)
+
+    def Bounds(self):
+        return IRect().make(0,0,self.right-self.left,self.bottom-self.top)
+
+    def Resize(self,nL,nT,nR,nB):
+        self.left+=nL
+        self.top+=nT
+        self.right+=nR
+        sel.bottom+=nB
+
+    def IsValid(self):
+        if (self.left<=self.right and self.top<=self.bottom):
+            return 1
+        return 0
+
+    def add(self,pt):
+        return IRect().make(self.left+pt.X,self.top+pt.Y,self.right+pt.X,self.bottom+pt.Y)
+
+    def subtract(self,pt):
+        return IRect().make(self.left+pt.X,self.top+pt.Y,self.right+pt.X,self.bottom+pt.Y)
+
+    def intersect(self,rect):
+        return IRect().make(max(self.left,rect.left),max(self.top,rect.top),min(self.right,rect.right),min(self.bottom,rect.bottom))
+
+    def union(self,rect):
+        return IRect().make(min(self.left,rect.left),min(self.top,rect.top),max(self.right,rect.right),max(self.bottom,rect.bottom))
+
+    def equals(self,rect):
+        if (self.top==rect.top and self.bottom==rect.bottom and self.left==rect.left and self.right==rect.right):
+            return 1
+        return 0
+
+    def make(self,l,t,r,b):
+        self.left=l
+        self.right=r
+        self.top=t
+        self.bottom=b
+        return self
+
+# A span is a single, contiguous horizontal line.  The routine scan_convert
+# returns a list of these that describe the polygon
+class ISpan:
+    def __init__(self,x1,x2,ypos):
+        self.left=x1
+        self.right=x2
+        self.y=ypos
+
+    def __str__(self):
+        return "(" + str(self.left) + " to " + str(self.right) + " at " + str(self.y) + ")"
+
+# clipping rectangle class -- this class is a single node in a linked list
+# of rectangles that describe a region
+
+class ClipRect:
+    def __init__(self):
+        self.next=None
+        self.prev=None
+        self.bounds=IRect().make(0,0,0,0)
+
+
+# cliprectlist -- the container class that manages a list of cliprects
+class ClipRectList:
+    def __init__(self):
+        self.first=None
+        self.last=None
+        self.count=0
+
+    def __str__(self):
+        x="["
+        for y in self.GetList():
+            x+=" "+str(y.bounds)+" "
+        x+="]"
+        return x
+
+#remove all rectangles from list
+    def Clear(self):
+        while(self.first):
+            rect=self.first
+            self.first=self.first.next
+            del rect
+        self.last=None
+        self.count=0
+
+#add a new clipping rectangle to list
+    def AddRect(self,rect):
+        rect.prev=None
+        rect.next=self.first
+        if not (self.first is None):
+            self.first.prev=rect
+        self.first=rect
+        if self.last is None:
+            self.last=rect
+        self.count += 1
+
+#removes the passed clipping rectangle from the list
+    def RemoveRect(self,rect):
+        if not (rect.prev is None):
+            rect.prev.next=rect.next
+        else:
+            self.first=rect.next
+        if not (rect.next is None):
+            rect.next.prev=rect.prev
+        else:
+            self.last=rect.prev
+        self.count -= 1
+
+# find the clipping rectangle at the the beginning of the list, remove it,
+# and return it
+    def RemoveHead(self):
+        if self.count==0:
+            return None
+        rect=self.first
+        self.first=rect.next
+        if (self.first is None):
+            self.last=None
+        self.count -= 1
+        return rect
+
+# stealrects -- appends the list of clipping rectangles in pclist to the current
+# list.  removes the entries from pclist
+    def StealRects(self,pclist):
+        if pclist.first is None:
+            return
+        if self.first is None:
+            self.first=pclist.first
+            self.last=pclist.last
+        else:
+            self.last.next=pclist.first
+            pclist.first.prev=self.last
+            self.last=pclist.last
+        self.count += pclist.count
+        pclist.first = None
+        pclist.last = None
+        pclist.count = 0
+
+# utilitarian procedure to return all clipping rectangles as a Python list
+    def GetList(self):
+        result=[]
+        f = self.first
+        while f:
+            result.append(f)
+            f = f.next
+        return result
+
+# some utility procedures, defined outside the scope of any class to ensure
+# efficiency
+
+def _hSortCmp(rect1,rect2):
+    if (rect1.bounds.left<rect2.bounds.left):
+        return -1
+    if (rect1.bounds.left>rect2.bounds.left):
+        return 1
+    if (rect1.bounds.top<rect2.bounds.top):
+        return -1
+    if (rect1.bounds.top>rect2.bounds.top):
+        return 1
+    return 0
+
+def _vSortCmp(rect1,rect2):
+    if (rect1.bounds.top<rect2.bounds.top):
+        return -1
+    if (rect1.bounds.top>rect2.bounds.top):
+        return 1
+    if (rect1.bounds.left<rect2.bounds.left):
+        return -1
+    if (rect1.bounds.left>rect2.bounds.left):
+        return 1
+    return 0
+
+# this is the class for which this whole source file is designed!
+# a Region is maintained as an optimal set of non-intersecting rectangles
+# whose union is the 2D area of interest.  At the end of each of the public
+# procedures that may alter the region's structure, the set of rectangles is
+# passed through an optimization phase, that joins any rectangles that are found
+# to be adjacent and can be combined into a single, larger rectangle
+
+class IRegion:
+    def __init__(self):
+        self.crects=ClipRectList()
+        self.firstclip=None
+
+    def __AllocClipRect(self):
+        return ClipRect()
+
+    def __FreeClipRect(self,p):
+        del p
+
+    def Clear(self):
+        self.crects.Clear()
+
+    def isEmpty(self):
+        if self.crects.first:
+            return 0
+        return 1
+
+    def Copy(self,dest):
+        dest.Clear()
+        p=self.crects.first
+        while p:
+            dest.__AddRect(p.bounds)
+            p=p.next
+
+    def __AddRect(self,rect):
+        cr=self.__AllocClipRect()
+        cr.bounds=IRect().make(rect.left,rect.top,rect.right,rect.bottom)
+        self.crects.AddRect(cr)
+        return cr
+
+# This is the magic procedure that always makes it possible to merge adjacent
+# rectangles later.   It is called once for each rectangle to be combined
+# with the current region.  Basically, it dices the current region into
+# horizontal bands where any rectangle is found to be beside the one being
+# examined.  This tends to leave more rectangles than necessary in the region,
+# but they can be culled during the optimization phase when they are combined
+# with adjacent rectangles.
+
+    def __Examine(self,rect):
+        newlist = ClipRectList()
+        while not (self.crects.first is None):
+            p = self.crects.RemoveHead()
+            if (p.bounds.right+1==rect.left or p.bounds.left==rect.right+1):
+                if (p.bounds.top<rect.top and p.bounds.bottom>rect.bottom):
+                    cnew=[IRect().make(p.bounds.left,p.bounds.top,p.bounds.right,rect.top-1),
+                          IRect().make(p.bounds.left,rect.top,p.bounds.right,rect.bottom),
+                          IRect().make(p.bounds.left,rect.bottom+1,p.bounds.right,p.bounds.bottom)]
+                elif (p.bounds.top<rect.top and p.bounds.bottom>rect.top):
+                    cnew=[IRect().make(p.bounds.left,p.bounds.top,p.bounds.right,rect.top-1),
+                          IRect().make(p.bounds.left,rect.top,p.bounds.right,p.bounds.bottom)]
+                elif (p.bounds.top<rect.bottom and p.bounds.bottom>rect.bottom):
+                    cnew=[IRect().make(p.bounds.left,p.bounds.top,p.bounds.right,rect.bottom),
+                          IRect().make(p.bounds.left,rect.bottom+1,p.bounds.right,p.bounds.bottom)]
+                else:
+                    cnew=[IRect().make(p.bounds.left,p.bounds.top,p.bounds.right,p.bounds.bottom)]
+                self.__FreeClipRect(p)
+                for i in cnew:
+                    if (i.IsValid()):
+                        newclip=self.__AllocClipRect()
+                        newclip.bounds=i
+                        newlist.AddRect(newclip)
+            else:
+                newlist.AddRect(p)
+        self.crects.StealRects(newlist)
+
+    def __IncludeRect(self,rect):
+        tmprg=IRegion()
+        tmprg.__AddRect(rect)
+        p = self.crects.first
+        while p:
+            tmprg.__ExcludeRect(p.bounds)
+            p = p.next
+        self.crects.StealRects(tmprg.crects)
+
+    def IncludeRect(self,rect):
+        tmprg = IRegion()
+        tmprg.Clear()
+        tmprg.__AddRect(rect)
+        self.__Examine(rect)
+        p= self.crects.first
+        while p:
+            tmprg.__Examine(p.bounds)
+            p=p.next
+        p=tmprg.crects.first
+        while p:
+            self.__IncludeRect(p.bounds)
+            p=p.next
+        self.Optimize()
+
+    def IncludeRegion(self,regn):
+        tmprg = IRegion()
+        regn.Copy(tmprg)
+        p = self.crects.first
+        while p:
+            tmprg.__Examine(p.bounds)
+            p=p.next
+        p = tmprg.crects.first
+        while p:
+            self.__Examine(p.bounds)
+            self.__IncludeRect(p.bounds)
+            p = p.next
+        self.Optimize()
+
+    def __ExcludeRect(self,rect):
+        newlist=ClipRectList()
+        while not (self.crects.first is None):
+            pcclip=self.crects.RemoveHead()
+            hide=rect.intersect(pcclip.bounds)
+            if not hide.IsValid():
+                newlist.AddRect(pcclip)
+            else:
+                #make 4 new rectangles to replace the one taken out
+                #
+                # AAAAAAAAAAAA
+                # AAAAAAAAAAAA
+                # BBBB    CCCC
+                # BBBB    CCCC
+                # DDDDDDDDDDDD
+                # DDDDDDDDDDDD
+                # the center rectangle is the one to remove, rectangles A,B,C and D
+                # get created.  If the rectangle is not in the middle, then the
+                # corresponding rectangle that should have been on "that side" of
+                # the current rectangle will have IsValid==False, so it will be
+                # rejected.
+                cnew=[IRect().make(pcclip.bounds.left,hide.top,hide.left-1,hide.bottom),
+                      IRect().make(hide.right+1,hide.top,pcclip.bounds.right,hide.bottom),
+                      IRect().make(pcclip.bounds.left,hide.bottom+1,pcclip.bounds.right,pcclip.bounds.bottom),
+                      IRect().make(pcclip.bounds.left,pcclip.bounds.top,pcclip.bounds.right,hide.top-1) ]
+                self.__FreeClipRect(pcclip)
+                for i in cnew:
+                    if (i.IsValid()):
+                        newclip=self.__AllocClipRect()
+                        newclip.bounds=i
+                        newlist.AddRect(newclip)
+        self.crects.last=None
+        self.crects.StealRects(newlist)
+        self.Optimize()
+
+    def ExcludeRect(self,rect):
+        self.__ExcludeRect(rect)
+        self.Optimize()
+
+    def ExcludeRegion(self,reg):
+        pclist = reg.crects.first
+        while pclist:
+            self.__ExcludeRect(pclist.bounds)
+            pclist = pclist.next
+        self.Optimize()
+
+    def __IntersectRect(self,rect):
+        newlist=ClipRectList()
+        while not self.crects.first is None:
+            pcclip=self.crects.RemoveHead()
+            hide=rect.intersect(pcclip.bounds)
+            if not hide.IsValid():
+                newlist.AddRect(pcclip)
+            else:
+                cnew=[IRect().make(pcclip.bounds.left,hide.top,hide.left-1,hide.bottom),
+                      IRect().make(hide.right+1,hide.top,pcclip.bounds.right,hide.bottom),
+                      IRect().make(pcclip.bounds.left,hide.bottom+1,pcclip.bounds.right,pcclip.bounds.bottom),
+                      IRect().make(pcclip.bounds.left,pcclip.bounds.top,pcclip.bounds.right,hide.top-1) ]
+                self.__FreeClipRect(pcclip)
+                for i in cnew:
+                    if (i.IsValid()):
+                        newclip=self.__AllocClipRect()
+                        newclip.bounds=i
+                        newlist.AddRect(newclip)
+        self.crects.last = None
+        self.crects.StealRects(newlist)
+
+    def IntersectRect(self,rect):
+        self.__IntersectRect(rect)
+        self.Optimize()
+
+    def IntersectRegion(self,reg):
+        clist=ClipRectList()
+        pcvclip = reg.crects.first
+        while pcvclip:
+            pchclip = self.crects.first
+            while pchclip:
+                crect=pcvclip.bounds.intersect(pchclip.bounds)
+                if crect.IsValid():
+                    newclip=self.__AllocClipRect()
+                    newclip.bounds=crect
+                    clist.AddRect(newclip)
+                pchclip = pchclip.next
+            pcvclip = pcvclip.next
+        self.Clear()
+        self.crects.StealRects(clist)
+        self.Optimize()
+
+    def GetBounds(self):
+        bounds=IRect()
+        pcclip = self.crects.first
+        while pcclip:
+            bounds=bounds.union(pcclip.bounds)
+            pcclip = pcclip.next
+        return bounds
+
+    def Optimize(self):
+        clist=self.crects.GetList()
+        removed=[]
+        keepgoing = 1
+        while len(clist)>1 and keepgoing:
+            keepgoing=0
+            clist.sort(lambda a,b:_vSortCmp(a,b))
+            keys=range(len(clist)-1)
+            keys.reverse()
+            for i in keys:
+                if (clist[i].bounds.right==clist[i+1].bounds.left-1 and
+                    clist[i].bounds.top==clist[i+1].bounds.top and
+                    clist[i].bounds.bottom==clist[i+1].bounds.bottom):
+                    clist[i].bounds.right=clist[i+1].bounds.right
+                    x=clist[i+1]
+                    del clist[i+1]
+                    self.crects.RemoveRect(x)
+                    self.__FreeClipRect(x)
+                    keepgoing=1
+            if len(clist)<=1:
+                break
+            clist.sort(lambda a,b:_hSortCmp(a,b))
+            keys=range(len(clist)-1)
+            keys.reverse()
+            for i in keys:
+                if (clist[i].bounds.bottom==clist[i+1].bounds.top-1 and
+                    clist[i].bounds.left==clist[i+1].bounds.left and
+                    clist[i].bounds.right==clist[i+1].bounds.right):
+                    clist[i].bounds.bottom=clist[i+1].bounds.bottom
+                    x=clist[i+1]
+                    del clist[i+1]
+                    self.crects.RemoveRect(x)
+                    self.__FreeClipRect(x)
+                    keepgoing=1
+
+    def FromPolygon(self,polypt,mode):
+        thelist=self.scan_Convert(polypt)
+        for i in thelist:
+            r=IRect().make(i.left,i.y,i.right,i.y)
+            if mode:
+                self.__Examine(r)
+                self.__IncludeRect(r)
+            else:
+                self.__ExcludeRect(r)
+        self.Optimize()
+
+    def GetRectList(self):
+        result=[]
+        i = self.crects.first
+        while i:
+            result.append(i.bounds)
+            i = i.next
+        return result
+
+#Portable implementation to scan-convert a polygon
+#from algoritm described in Section 3.6.3 of Foley, et al
+#returns a (possibly redundant) list of spans that comprise the polygon
+#this routine does not manipulate the current region in any way, but
+#is enclosed in this class to isolate its name from the global namespace
+#invoke via IRegion().scan_Convert(polypt)
+
+    def scan_Convert(self,polypt):
+        result=[]
+        ET = {}  # edge table
+        i = 0
+        size = len(polypt)
+        polylines=[] # list of lines in polygon
+        y = -1
+        for i in xrange(size):
+            n = (i+1) % size
+            if polypt[i].Y < polypt[n].Y:
+                e = Edge(polypt[i],polypt[n])
+                e.dir = 1 #moving top to bottom
+                polylines.append(e)
+            elif polypt[i].Y > polypt[n].Y:
+                e = Edge(polypt[n],polypt[i])
+                e.dir = -1 #moving bottom to top
+                polylines.append(e)
+            elif polypt[i].X != polypt[n].X:  # any horizontal lines just get added directly
+                sx = min(polypt[i].X,polypt[n].X)
+                ex = max(polypt[i].X,polypt[n].X)
+                result.append(ISpan(sx,ex,polypt[i].Y))
+        size = len(polylines)
+        for i in xrange(size):
+            if i == 0 and polylines[i].dir == -1:
+                n = size-1
+            elif (polylines[i].dir == -1):
+                n = i-1
+            else:
+                n = (i+1) % size
+            if (polylines[i].dir != polylines[n].dir): #find out if at bottom end
+                polylines[i].bottom = 1
+            if i == 0 or polylines[i].starty < y:
+                y = polylines[i].starty
+            if not ET.has_key(polylines[i].starty):
+                ET[polylines[i].starty] = []
+            ET[polylines[i].starty].append(polylines[i]) #add to edge table, indexed by smaller y coordinate
+        AET = [] #active edge table
+        while len(AET) > 0 or len(ET) > 0:
+            if ET.has_key(y): #if current y value has entries in edge tabe
+                AET.extend(ET[y]) # add them to active edge table
+                del ET[y] #and delete the entries in the edge table
+            i = len(AET)-1
+            vals = []
+            while i >= 0:
+                if (AET[i].endy == y): #if at the end of this edge's run
+                    if (AET[i].bottom): #check and see if we need one more point
+                        vals.append(AET[i].cx) #we do, add it
+                    del AET[i] # y=end of edge, so remove it
+                i -= 1
+            i = 0
+            while i < len(AET): # for every edge in AET
+                vals.append(AET[i].cx) # add x intercept
+                AET[i].advance() #and advance the edge one down
+                i += 1
+            vals.sort()
+            i = 0
+            while i < len(vals)-1:  #split vals[] array into pairs and output them
+                result.append(ISpan(vals[i],vals[i+1],y))
+                i += 2
+            y += 1
+        return result
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/whiteboard.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,664 @@
+# 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: mapper/whiteboard.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: whiteboard.py,v 1.47 2007/03/09 14:11:55 digitalxero Exp $
+#
+# Description: This file contains some of the basic definitions for the chat
+# utilities in the orpg project.
+#
+__version__ = "$Id: whiteboard.py,v 1.47 2007/03/09 14:11:55 digitalxero Exp $"
+
+from base import *
+from orpg.mapper.map_utils import *
+
+def cmp_zorder(first,second):
+    f = first.zorder
+    s = second.zorder
+    if f == None:
+        f = 0
+    if s == None:
+        s = 0
+    if f == s:
+        value = 0
+    elif f < s:
+        value = -1
+    else:
+        value = 1
+    return value
+
+class WhiteboardText:
+    def __init__(self, id, text_string, pos, style, pointsize, weight, color="#000000", log=None):
+        self.log = log
+        self.log.log("Enter WhiteboardText", ORPG_DEBUG)
+        self.scale = 1
+        self.r_h = RGBHex()
+        self.selected = False
+        self.text_string = text_string
+        self.id = id
+        self.weight = int(weight)
+        self.pointsize = int(pointsize)
+        self.style = int(style)
+        self.textcolor = color
+        self.posx = pos.x
+        self.posy = pos.y
+        self.font = wx.Font(self.pointsize, wx.DEFAULT, self.style, self.weight)
+        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)
+        self.isUpdated = False
+        self.log.log("Exit WhiteboardText", ORPG_DEBUG)
+
+    def highlight(self, highlight=True):
+        self.log.log("Enter WhiteboardText->highlight(self, highlight)", ORPG_DEBUG)
+        self.highlighted = highlight
+        self.log.log("Exit WhiteboardText->highlight(self, highlight)", ORPG_DEBUG)
+
+    def set_text_props(self, text_string, style, point, weight, color="#000000"):
+        self.log.log("Enter WhiteboardText->set_text_props(self, text_string, style, point, weight, color)", ORPG_DEBUG)
+        self.text_string = text_string
+        self.textcolor = color
+        self.style = int(style)
+        self.font.SetStyle(self.style)
+        self.pointsize = int(point)
+        self.font.SetPointSize(self.pointsize)
+        self.weight = int(weight)
+        self.font.SetWeight(self.weight)
+        self.isUpdated = True
+        self.log.log("Exit WhiteboardText->set_text_props(self, text_string, style, point, weight, color)", ORPG_DEBUG)
+
+    def hit_test(self, pt, dc):
+        self.log.log("Enter WhiteboardText->hit_test(self, pt, dc)", ORPG_DEBUG)
+        rect = self.get_rect(dc)
+        result = rect.InsideXY(pt.x, pt.y)
+        self.log.log("Exit WhiteboardText->hit_test(self, pt, dc)", ORPG_DEBUG)
+        return result
+
+    def get_rect(self, dc):
+        self.log.log("Enter WhiteboardText->get_rect(self, dc)", ORPG_DEBUG)
+        dc.SetFont(self.font)
+        (w,x,y,z) = dc.GetFullTextExtent(self.text_string)
+        self.log.log("Exit WhiteboardText->get_rect(self, dc)", ORPG_DEBUG)
+        return wx.Rect(self.posx,self.posy,w,(x+y+z))
+
+    def draw(self, parent, dc, op=wx.COPY):
+        self.log.log("Enter WhiteboardText->draw(self, parent, dc, op)", ORPG_DEBUG)
+        self.scale = parent.canvas.layers['grid'].mapscale
+        if self.highlighted:
+            textcolor = self.highlight_color
+        else:
+            textcolor = self.textcolor
+        try:
+            dc.SetTextForeground(textcolor)
+        except Exception,e:
+            dc.SetTextForeground('#000000')
+        dc.SetUserScale(self.scale, self.scale)
+
+        # Draw text
+        (w,x,y,z) = self.get_rect(dc)
+        dc.SetFont(self.font)
+        dc.DrawText(self.text_string, self.posx, self.posy)
+        dc.SetTextForeground(wx.Colour(0,0,0))
+        self.log.log("Exit WhiteboardText->draw(self, parent, dc, op)", ORPG_DEBUG)
+
+    def toxml(self, action="update"):
+        self.log.log("Enter WhiteboardText->toxml(self, " + action + ")", ORPG_DEBUG)
+        if action == "del":
+            xml_str = "<text action='del' id='" + str(self.id) + "'/>"
+            self.log.log(xml_str, ORPG_DEBUG)
+            self.log.log("Exit WhiteboardText->toxml(self, " + action + ")", ORPG_DEBUG)
+            return xml_str
+        xml_str = "<text"
+        xml_str += " action='" + action + "'"
+        xml_str += " id='" + str(self.id) + "'"
+        if self.pointsize != None:
+            xml_str += " pointsize='" + str(self.pointsize) + "'"
+        if self.style != None:
+            xml_str += " style='" + str(self.style) + "'"
+        if self.weight != None:
+            xml_str += " weight='" + str(self.weight) + "'"
+        if self.posx != None:
+            xml_str+= " posx='" + str(self.posx) + "'"
+        if not (self.posy is None):
+            xml_str += " posy='" + str(self.posy) + "'"
+        if self.text_string != None:
+            xml_str+= " text_string='" + self.text_string + "'"
+        if self.textcolor != None:
+            xml_str += " color='" + self.textcolor + "'"
+        xml_str += "/>"
+        self.log.log(xml_str, ORPG_DEBUG)
+        self.log.log("Exit WhiteboardText->toxml(self, " + action + ")", ORPG_DEBUG)
+        if (action == "update" and self.isUpdated) or action == "new":
+            self.isUpdated = False
+            return xml_str
+        else:
+            return ''
+
+    def takedom(self, xml_dom):
+        self.log.log("Enter WhiteboardText->takedom(self, xml_dom)", ORPG_DEBUG)
+        self.text_string = xml_dom.getAttribute("text_string")
+        self.log.log("self.text_string=" + self.text_string, ORPG_DEBUG)
+        self.id = xml_dom.getAttribute("id")
+        self.log.log("self.id=" + str(self.id), ORPG_DEBUG)
+        if xml_dom.hasAttribute("posy"):
+            self.posy = int(xml_dom.getAttribute("posy"))
+            self.log.log("self.posy=" + str(self.posy), ORPG_DEBUG)
+        if xml_dom.hasAttribute("posx"):
+            self.posx = int(xml_dom.getAttribute("posx"))
+            self.log.log("self.posx=" + str(self.posx), ORPG_DEBUG)
+        if xml_dom.hasAttribute("weight"):
+            self.weight = int(xml_dom.getAttribute("weight"))
+            self.font.SetWeight(self.weight)
+            self.log.log("self.weight=" + str(self.weight), ORPG_DEBUG)
+        if xml_dom.hasAttribute("style"):
+            self.style = int(xml_dom.getAttribute("style"))
+            self.font.SetStyle(self.style)
+            self.log.log("self.style=" + str(self.style), ORPG_DEBUG)
+        if xml_dom.hasAttribute("pointsize"):
+            self.pointsize = int(xml_dom.getAttribute("pointsize"))
+            self.font.SetPointSize(self.pointsize)
+            self.log.log("self.pointsize=" + str(self.pointsize), ORPG_DEBUG)
+        if xml_dom.hasAttribute("color") and xml_dom.getAttribute("color") != '':
+            self.textcolor = xml_dom.getAttribute("color")
+            if self.textcolor == '#0000000':
+                self.textcolor = '#000000'
+            self.log.log("self.textcolor=" + self.textcolor, ORPG_DEBUG)
+        self.log.log("Exit WhiteboardText->takedom(self, xml_dom)", ORPG_DEBUG)
+
+class WhiteboardLine:
+    def __init__(self, id, line_string, upperleft, lowerright, color="#000000", width=1, log=None):
+        self.log = log
+        self.log.log("Enter WhiteboardLine", ORPG_DEBUG)
+        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.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)
+        self.log.log("Exit WhiteboardLine", ORPG_DEBUG)
+
+    def highlight(self, highlight=True):
+        self.log.log("Enter WhiteboardLine->highlight(self, highlight)", ORPG_DEBUG)
+        self.highlighted = highlight
+        self.log.log("Enter WhiteboardLine->highlight(self, highlight)", ORPG_DEBUG)
+
+    def set_line_props(self, line_string="", upperleftx=0, upperlefty=0, lowerrightx=0, lowerrighty=0, color="#000000", width=1):
+        self.log.log("Enter WhiteboardLine->set_line_props(self, line_string, upperleftx, upperlefty, lowerrightx, lowerrighty, color, width)", ORPG_DEBUG)
+        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
+        self.log.log("Exit WhiteboardLine->set_line_props(self, line_string, upperleftx, upperlefty, lowerrightx, lowerrighty, color, width)", ORPG_DEBUG)
+
+    def hit_test(self, pt):
+        self.log.log("Enter WhiteboardLine->hit_test(self, pt)", ORPG_DEBUG)
+        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] == "":
+                self.log.log("Exit WhiteboardLine->hit_test(self, pt) return False", ORPG_DEBUG)
+                return False
+            icords = (int(stcords[0]),int(stcords[1]))
+            if orpg.mapper.map_utils.proximity_test(oldicords,icords,pt,12):
+                self.log.log("Exit WhiteboardLine->hit_test(self, pt) return True", ORPG_DEBUG)
+                return True
+            oldicords = icords
+        self.log.log("Exit WhiteboardLine->hit_test(self, pt) return False", ORPG_DEBUG)
+        return False
+
+    def draw(self, parent, dc, op=wx.COPY):
+        self.log.log("Enter WhiteboardLine->draw(self, parent, dc, op=wx.COPY)", ORPG_DEBUG)
+        self.scale = parent.canvas.layers['grid'].mapscale
+        if self.highlighted:
+            linecolor = self.highlight_color
+        else:
+            linecolor = self.linecolor
+        pen = wx.BLACK_PEN
+        try:
+            pen.SetColour(linecolor)
+        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 = -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
+        pen.SetColour(wx.Colour(0,0,0))
+        dc.SetPen(pen)
+        dc.SetPen(wx.NullPen)
+        dc.SetBrush(wx.NullBrush)
+        #selected outline
+        self.log.log("Exit WhiteboardLine->draw(self, parent, dc, op=wx.COPY)", ORPG_DEBUG)
+
+    def toxml(self, action="update"):
+        self.log.log("Enter WhiteboardLine->toxml(self, " + action + ")", ORPG_DEBUG)
+        if action == "del":
+            xml_str = "<line action='del' id='" + str(self.id) + "'/>"
+            self.log.log(xml_str, ORPG_DEBUG)
+            self.log.log("Exit WhiteboardLine->toxml(self, " + action + ")", ORPG_DEBUG)
+            return xml_str
+        #  if there are any changes, make sure id is one of them
+        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) + "'"
+        if self.linecolor != None:
+            xml_str += " color='" + str(self.linecolor) + "'"
+        if self.linewidth != None:
+            xml_str += " width='" + str(self.linewidth) + "'"
+        xml_str += "/>"
+        self.log.log(xml_str, ORPG_DEBUG)
+        self.log.log("Exit WhiteboardLine->toxml(self, " + action + ")", ORPG_DEBUG)
+        if action == "new":
+            return xml_str
+        return ''
+
+    def takedom(self, xml_dom):
+        self.log.log("Enter WhiteboardLine->takedom(self, xml_dom)", ORPG_DEBUG)
+        self.line_string = xml_dom.getAttribute("line_string")
+        self.log.log("self.line_string=" + self.line_string, ORPG_DEBUG)
+        self.id = xml_dom.getAttribute("id")
+        self.log.log("self.id=" + str(self.id), ORPG_DEBUG)
+        if xml_dom.hasAttribute("upperleftx"):
+            self.upperleft.x = int(xml_dom.getAttribute("upperleftx"))
+            self.log.log("self.upperleft.x=" + str(self.upperleft.x), ORPG_DEBUG)
+        if xml_dom.hasAttribute("upperlefty"):
+            self.upperleft.y = int(xml_dom.getAttribute("upperlefty"))
+            self.log.log("self.upperleft.y=" + str(self.upperleft.y), ORPG_DEBUG)
+        if xml_dom.hasAttribute("lowerrightx"):
+            self.lowerright.x = int(xml_dom.getAttribute("lowerrightx"))
+            self.log.log("self.lowerright.x=" + str(self.lowerright.x), ORPG_DEBUG)
+        if xml_dom.hasAttribute("lowerrighty"):
+            self.lowerright.y = int(xml_dom.getAttribute("lowerrighty"))
+            self.log.log("self.lowerright.y=" + str(self.lowerright.y), ORPG_DEBUG)
+        if xml_dom.hasAttribute("color") and xml_dom.getAttribute("color") != '':
+            self.linecolor = xml_dom.getAttribute("color")
+            if self.linecolor == '#0000000':
+                self.linecolor = '#000000'
+            self.log.log("self.linecolor=" + self.linecolor, ORPG_DEBUG)
+        if xml_dom.hasAttribute("width"):
+            self.linewidth = int(xml_dom.getAttribute("width"))
+            self.log.log("self.linewidth=" + str(self.linewidth), ORPG_DEBUG)
+        self.log.log("Exit WhiteboardLine->takedom(self, xml_dom)", ORPG_DEBUG)
+
+##-----------------------------
+## whiteboard layer
+##-----------------------------
+class whiteboard_layer(layer_base):
+
+    def __init__(self, canvas):
+        self.canvas = canvas
+        self.log = self.canvas.log
+        self.log.log("Enter whiteboard_layer", ORPG_DEBUG)
+        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 = []
+        self.log.log("Exit whiteboard_layer", ORPG_DEBUG)
+
+    def next_serial(self):
+        self.log.log("Enter whiteboard_layer->next_serial(self)", ORPG_DEBUG)
+        self.serial_number += 1
+        self.log.log("Exit whiteboard_layer->next_serial(self)", ORPG_DEBUG)
+        return self.serial_number
+
+    def get_next_highest_z(self):
+        self.log.log("Enter whiteboard_layer->get_next_highest_z(self)", ORPG_DEBUG)
+        z = len(self.lines)+1
+        self.log.log("Exit whiteboard_layer->get_next_highest_z(self)", ORPG_DEBUG)
+        return z
+
+    def cleanly_collapse_zorder(self):
+        self.log.log("Enter/Exit whiteboard_layer->cleanly_collapse_zorder(self)", ORPG_DEBUG)
+
+    def collapse_zorder(self):
+        self.log.log("Enter/Exit whiteboard_layer->collapse_zorder(self)", ORPG_DEBUG)
+
+    def rollback_serial(self):
+        self.log.log("Enter whiteboard_layer->rollback_serial(self)", ORPG_DEBUG)
+        self.serial_number -= 1
+        self.log.log("Exit whiteboard_layer->rollback_serial(self)", ORPG_DEBUG)
+
+    def add_line(self, line_string="", upperleft=cmpPoint(0,0), lowerright=cmpPoint(0,0), color="#000000", width=1):
+        self.log.log("Enter whiteboard_layer->add_line(self, line_string, upperleft, lowerright, color, width)", ORPG_DEBUG)
+        id = 'line-' + str(self.next_serial())
+        line = WhiteboardLine(id, line_string, upperleft, lowerright, color=self.color, width=self.width, log=self.log)
+        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)
+        self.log.log("Exit whiteboard_layer->add_line(self, line_string, upperleft, lowerright, color, width)", ORPG_DEBUG)
+        return line
+
+    def get_line_by_id(self, id):
+        self.log.log("Enter whiteboard_layer->get_line_by_id(self, id)", ORPG_DEBUG)
+        for line in self.lines:
+            if str(line.id) == str(id):
+                self.log.log("Exit whiteboard_layer->get_line_by_id(self, id) return LineID: " + str(id), ORPG_DEBUG)
+                return line
+        self.log.log("Exit whiteboard_layer->get_line_by_id(self, id) return None", ORPG_DEBUG)
+        return None
+
+    def get_text_by_id(self, id):
+        self.log.log("Enter whiteboard_layer->get_text_by_id(self, id)", ORPG_DEBUG)
+        for text in self.texts:
+            if str(text.id) == str(id):
+                self.log.log("Exit whiteboard_layer->get_text_by_id(self, id) return textID: " + str(id), ORPG_DEBUG)
+                return text
+        self.log.log("Enter whiteboard_layer->get_text_by_id(self, id) return None", ORPG_DEBUG)
+        return None
+
+    def del_line(self, line):
+        self.log.log("Enter whiteboard_layer->del_line(self, line)", ORPG_DEBUG)
+        xml_str = "<map><whiteboard>"
+        xml_str += line.toxml("del")
+        xml_str += "</whiteboard></map>"
+        self.canvas.frame.session.send(xml_str)
+        if line:
+            self.lines.remove(line)
+            self.removedLines.append(line)
+        self.canvas.Refresh(True)
+        self.log.log("Exit whiteboard_layer->del_line(self, line)", ORPG_DEBUG)
+
+    def undo_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)
+            self.canvas.Refresh(True)
+
+    def del_all_lines(self):
+        self.log.log("Enter whiteboard_layer->del_all_lines(self)", ORPG_DEBUG)
+        for i in xrange(len(self.lines)):
+            self.del_line(self.lines[0])
+        print self.lines
+        self.log.log("Exit whiteboard_layer->del_all_lines(self)", ORPG_DEBUG)
+
+    def del_text(self, text):
+        self.log.log("Enter whiteboard_layer->del_text(self, text)", ORPG_DEBUG)
+        xml_str = "<map><whiteboard>"
+        xml_str += text.toxml("del")
+        xml_str += "</whiteboard></map>"
+        self.canvas.frame.session.send(xml_str)
+        if text:
+            self.texts.remove(text)
+        self.canvas.Refresh(True)
+        self.log.log("Exit whiteboard_layer->del_text(self, text)", ORPG_DEBUG)
+
+    def layerDraw(self, dc):
+        self.log.log("Enter whiteboard_layer->layerDraw(self, dc)", ORPG_DEBUG)
+        for m in self.lines:
+            m.draw(self, dc)
+        for m in self.texts:
+            m.draw(self,dc)
+        self.log.log("Exit whiteboard_layer->layerDraw(self, dc)", ORPG_DEBUG)
+
+    def hit_test_text(self, pos, dc):
+        self.log.log("Enter whiteboard_layer->hit_test_text(self, pos, dc)", ORPG_DEBUG)
+        list_of_texts_matching = []
+        if self.canvas.layers['fog'].use_fog == 1:
+            if self.canvas.frame.session.role != "GM":
+                self.log.log("Exit whiteboard_layer->hit_test_text(self, pos, dc)", ORPG_DEBUG)
+                return list_of_texts_matching
+        for m in self.texts:
+            if m.hit_test(pos,dc):
+                list_of_texts_matching.append(m)
+        self.log.log("Exit whiteboard_layer->hit_test_text(self, pos, dc)", ORPG_DEBUG)
+        return list_of_texts_matching
+
+    def hit_test_lines(self, pos, dc):
+        self.log.log("Enter whiteboard_layer->hit_test_lines(self, pos, dc)", ORPG_DEBUG)
+        list_of_lines_matching = []
+        if self.canvas.layers['fog'].use_fog == 1:
+            if self.canvas.frame.session.role != "GM":
+                self.log.log("Exit whiteboard_layer->hit_test_lines(self, pos, dc)", ORPG_DEBUG)
+                return list_of_lines_matching
+        for m in self.lines:
+            if m.hit_test(pos):
+                list_of_lines_matching.append(m)
+        self.log.log("Exit whiteboard_layer->hit_test_lines(self, pos, dc)", ORPG_DEBUG)
+        return list_of_lines_matching
+
+    def find_line(self, pt):
+        self.log.log("Enter whiteboard_layer->find_line(self, pt)", ORPG_DEBUG)
+        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)
+        if line_list:
+            self.log.log("Exit whiteboard_layer->find_line(self, pt)", ORPG_DEBUG)
+            return line_list[0]
+        else:
+            self.log.log("Exit whiteboard_layer->find_line(self, pt) return None", ORPG_DEBUG)
+            return None
+
+    def setcolor(self, color):
+        self.log.log("Enter whiteboard_layer->setcolor(self, color)", ORPG_DEBUG)
+        r,g,b = color.Get()
+        self.color = self.r_h.hexstring(r,g,b)
+        self.log.log("Exit whiteboard_layer->setcolor(self, color)", ORPG_DEBUG)
+
+    def sethexcolor(self, hexcolor):
+        self.log.log("Enter whiteboard_layer->sethexcolor(self, hexcolor)", ORPG_DEBUG)
+        self.color = hexcolor
+        self.log.log("Exit whiteboard_layer->sethexcolor(self, hexcolor)", ORPG_DEBUG)
+
+    def setwidth(self, width):
+        self.log.log("Enter whiteboard_layer->setwidth(self, width)", ORPG_DEBUG)
+        self.width = int(width)
+        self.log.log("Exit whiteboard_layer->setwidth(self, width)", ORPG_DEBUG)
+
+    def set_font(self, font):
+        self.log.log("Enter whiteboard_layer->set_font(self, font)", ORPG_DEBUG)
+        self.font = font
+        self.log.log("Exit whiteboard_layer->set_font(self, font)", ORPG_DEBUG)
+
+    def add_text(self, text_string, pos, style, pointsize, weight, color="#000000"):
+        self.log.log("Enter whiteboard_layer->add_text(self, text_string, pos, style, pointsize, weight, color)", ORPG_DEBUG)
+        id = 'text-' + str(self.next_serial())
+        text = WhiteboardText(id,text_string, pos, style, pointsize, weight, color, self.log)
+        self.texts.append(text)
+        xml_str = "<map><whiteboard>"
+        xml_str += text.toxml("new")
+        xml_str += "</whiteboard></map>"
+        self.canvas.frame.session.send(xml_str)
+        self.canvas.Refresh(True)
+        self.log.log("Exit whiteboard_layer->add_text(self, text_string, pos, style, pointsize, weight, color)", ORPG_DEBUG)
+
+    def draw_working_line(self, dc, line_string):
+        self.log.log("Enter whiteboard_layer->draw_working_line(self, dc, line_string)", ORPG_DEBUG)
+        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)
+        self.log.log("Exit whiteboard_layer->draw_working_line(self, dc, line_string)", ORPG_DEBUG)
+
+    def layerToXML(self, action="update"):
+        """ format  """
+        self.log.log("Enter whiteboard_layer->layerToXML(self, " + action + ")", ORPG_DEBUG)
+        white_string = ""
+        if self.lines:
+            for l in self.lines:
+                white_string += l.toxml(action)
+        if self.texts:
+            for l in self.texts:
+                white_string += l.toxml(action)
+        if len(white_string):
+            s = "<whiteboard"
+            s += " serial='" + str(self.serial_number) + "'"
+            s += ">"
+            s += white_string
+            s += "</whiteboard>"
+            self.log.log(s, ORPG_DEBUG)
+            self.log.log("Exit whiteboard_layer->layerToXML(self, " + action + ")", ORPG_DEBUG)
+            return s
+        else:
+            self.log.log("Exit whiteboard_layer->layerToXML(self, " + action + ")", ORPG_DEBUG)
+            return ""
+
+    def layerTakeDOM(self, xml_dom):
+        self.log.log("Enter whiteboard_layer->layerTakeDOM(self, xml_dom)", ORPG_DEBUG)
+        serial_number = xml_dom.getAttribute('serial')
+        if serial_number != "":
+            self.serial_number = int(serial_number)
+        children = xml_dom._get_childNodes()
+        for l in children:
+            nodename = l._get_nodeName()
+            action = l.getAttribute("action")
+            id = l.getAttribute('id')
+            if action == "del":
+                if nodename == 'line':
+                    line = self.get_line_by_id(id)
+                    if line != None:
+                        self.lines.remove(line)
+                    else:
+                        self.log.log("Whiteboard error: Deletion of unknown line object attempted.", ORPG_GENERAL)
+                elif nodename == 'text':
+                    text = self.get_text_by_id(id)
+                    if text != None:
+                        self.texts.remove(text)
+                    else:
+                        self.log.log("Whiteboard error: Deletion of unknown text object attempted.", ORPG_GENERAL)
+                else:
+                    self.log.log("Whiteboard error: Deletion of unknown whiteboard object attempted.", ORPG_GENERAL)
+            elif action == "new":
+                if nodename == "line":
+                    try:
+                        line_string = l.getAttribute('line_string')
+                        upperleftx = l.getAttribute('upperleftx')
+                        upperlefty = l.getAttribute('upperlefty')
+                        lowerrightx = l.getAttribute('lowerrightx')
+                        lowerrighty = l.getAttribute('lowerrighty')
+                        upperleft = wx.Point(int(upperleftx),int(upperlefty))
+                        lowerright = wx.Point(int(lowerrightx),int(lowerrighty))
+                        color = l.getAttribute('color')
+                        if color == '#0000000':
+                            color = '#000000'
+                        id = l.getAttribute('id')
+                        width = int(l.getAttribute('width'))
+                    except:
+                        line_string = upperleftx = upperlefty = lowerrightx = lowerrighty = color = 0
+                        self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                        self.log.log("invalid line", ORPG_GENERAL)
+                        continue
+                    line = WhiteboardLine(id, line_string, upperleft, lowerright, color, width, self.log)
+                    self.lines.append(line)
+                elif nodename == "text":
+                    try:
+                        text_string = l.getAttribute('text_string')
+                        style = l.getAttribute('style')
+                        pointsize = l.getAttribute('pointsize')
+                        weight = l.getAttribute('weight')
+                        color = l.getAttribute('color')
+                        if color == '#0000000':
+                            color = '#000000'
+                        id = l.getAttribute('id')
+                        posx = l.getAttribute('posx')
+                        posy = l.getAttribute('posy')
+                        pos = wx.Point(0,0)
+                        pos.x = int(posx)
+                        pos.y = int(posy)
+                    except:
+                        self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                        self.log.log("invalid line", ORPG_GENERAL)
+                        continue
+                    text = WhiteboardText(id, text_string, pos, style, pointsize, weight, color, self.log)
+                    self.texts.append(text)
+            else:
+                if nodename == "line":
+                    line = self.get_line_by_id(id)
+                    if line:
+                        line.takedom(l)
+                    else:
+                        self.log.log("Whiteboard error: Update of unknown line attempted.", ORPG_GENERAL)
+                if nodename == "text":
+                    text = self.get_text_by_id(id)
+                    if text:
+                        text.takedom(l)
+                    else:
+                        self.log.log("Whiteboard error: Update of unknown text attempted.", ORPG_GENERAL)
+        self.log.log("Enter whiteboard_layer->layerTakeDOM(self, xml_dom)", ORPG_DEBUG)
+        #self.canvas.send_map_data()
+
+    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, log=self.log)
+        self.lines.append(line)
+        return line
+
+    def del_temp_line(self, line):
+        if line:
+            self.lines.remove(line)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/whiteboard_handler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,868 @@
+# 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: mapper/whiteboard_hander.py
+# Author: OpenRPG Team
+# Maintainer:
+# Version:
+#   $Id: whiteboard_handler.py,v 1.37 2007/03/09 14:11:56 digitalxero Exp $
+#
+# Description: Whiteboard layer handler
+#
+__version__ = "$Id: whiteboard_handler.py,v 1.37 2007/03/09 14:11:56 digitalxero Exp $"
+
+from base_handler import *
+from math import floor
+from math import sqrt
+
+class whiteboard_handler(base_layer_handler):
+    def __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
+        #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.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)
+        self.color_button = wx.Button(self, wx.ID_ANY, "Pen Color", style=wx.BU_EXACTFIT)
+        self.color_button.SetBackgroundColour(wx.BLACK)
+        self.color_button.SetForegroundColour(wx.WHITE)
+        self.drawmode_ctrl = wx.Choice(self, wx.ID_ANY, choices = ["Freeform", "Polyline","Text", "Cone", "Circle"])
+        self.drawmode_ctrl.SetSelection(0) #always start showing "Freeform"
+        self.radius = wx.TextCtrl(self, wx.ID_ANY, size=(32,-1) )
+        self.radius.SetValue("15")
+        self.live_refresh = wx.CheckBox(self, wx.ID_ANY, " Live Refresh")
+        self.live_refresh.SetValue(True)
+        self.widthList= wx.Choice(self, wx.ID_ANY, size= wx.Size(40, 20), choices=['1','2','3','4','5','6','7','8','9','10'])
+        self.widthList.SetSelection(0) #always start showing "1"
+        self.sizer.Add(wx.StaticText(self, wx.ID_ANY, "Line Width: "),0,wx.ALIGN_CENTER)
+        self.sizer.Add(self.widthList, 0, wx.EXPAND)
+        self.sizer.Add(wx.Size(10,25))
+        self.sizer.Add(wx.StaticText(self, wx.ID_ANY, "Drawing Mode: "),0,wx.ALIGN_CENTER)
+        self.sizer.Add(self.drawmode_ctrl, 0, wx.EXPAND)
+        self.sizer.Add(wx.StaticText(self, -1, " Radius: "), 0, wx.ALIGN_CENTER|wx.ALL, 3)
+        self.sizer.Add(self.radius, 0, wx.EXPAND|wx.ALL, 2)
+        self.sizer.Add(wx.Size(10,25))
+        self.sizer.Add(self.live_refresh, 0, wx.EXPAND)
+        self.sizer.Add(wx.Size(20,25))
+        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_BUTTON, self.on_pen_color, self.color_button)
+        self.Bind(wx.EVT_CHOICE, self.on_pen_width, self.widthList)
+
+    def build_text_properties_menu(self, label="Text Properties"):
+        self.text_properties_dialog = wx.Dialog(self, -1, "Text Properties",  name = "Text Properties")
+        self.text_props_sizer = wx.BoxSizer(wx.VERTICAL)
+        okay_boxer = wx.BoxSizer(wx.HORIZONTAL)
+        okay_button = wx.Button(self.text_properties_dialog, wx.ID_OK, "APPLY")
+        cancel_button = wx.Button(self.text_properties_dialog, wx.ID_CANCEL,"CANCEL")
+        okay_boxer.Add(okay_button, 1, wx.ALIGN_LEFT)
+        okay_boxer.Add(wx.Size(10,10))
+        okay_boxer.Add(cancel_button, 1, wx.ALIGN_RIGHT)
+        self.txt_boxer = wx.BoxSizer(wx.HORIZONTAL)
+        self.txt_static = wx.StaticText(self.text_properties_dialog, -1, "Text: ")
+        self.text_control = wx.TextCtrl(self.text_properties_dialog, wx.ID_ANY, "", name = "Text: ")
+        self.txt_boxer.Add(self.txt_static,0,wx.EXPAND)
+        self.txt_boxer.Add(wx.Size(10,10))
+        self.txt_boxer.Add(self.text_control,1,wx.EXPAND)
+        self.point_boxer = wx.BoxSizer(wx.HORIZONTAL)
+        self.point_static = wx.StaticText(self.text_properties_dialog, -1, "Text Size: ")
+        self.point_control = wx.SpinCtrl(self.text_properties_dialog, wx.ID_ANY, value = "12",min = 1, initial = 12, name = "Font Size: ")
+        self.point_boxer.Add(self.point_static,1,wx.EXPAND)
+        self.point_boxer.Add(wx.Size(10,10))
+        self.point_boxer.Add(self.point_control,0,wx.EXPAND)
+        self.text_color_control = wx.Button(self.text_properties_dialog, wx.ID_ANY, "TEXT COLOR",style=wx.BU_EXACTFIT)
+        self.weight_control = wx.RadioBox(self.text_properties_dialog, wx.ID_ANY, "Weight", choices = ["Normal","Bold"])
+        self.style_control = wx.RadioBox(self.text_properties_dialog, wx.ID_ANY, "Style", choices = ["Normal", "Italic"])
+        self.text_props_sizer.Add(self.txt_boxer,0,wx.EXPAND)
+        self.text_props_sizer.Add(self.point_boxer,0, wx.EXPAND)
+        self.text_props_sizer.Add(self.weight_control,0, wx.EXPAND)
+        self.text_props_sizer.Add(self.style_control,0, wx.EXPAND)
+        self.text_props_sizer.Add(self.text_color_control, 0, wx.EXPAND)
+        self.text_props_sizer.Add(wx.Size(10,10))
+        self.text_props_sizer.Add(okay_boxer,0, wx.EXPAND)
+        self.text_props_sizer.Fit(self)
+        self.text_properties_dialog.SetSizer(self.text_props_sizer)
+        self.text_properties_dialog.Fit()
+        self.text_properties_dialog.Bind(wx.EVT_BUTTON, self.on_text_color, self.text_color_control)
+        self.text_properties_dialog.Bind(wx.EVT_BUTTON, self.on_text_properties, okay_button)
+        #self.text_properties_dialog.Destroy()
+
+    def build_menu(self, label = "Whiteboard"):
+        base_layer_handler.build_menu(self,label)
+        self.main_menu.AppendSeparator()
+        item = wx.MenuItem(self.main_menu, wx.ID_ANY, "&Change Pen Color", "Change Pen Color")
+        self.canvas.Bind(wx.EVT_MENU, self.on_pen_color, item)
+        self.main_menu.AppendItem(item)
+        item = wx.MenuItem(self.main_menu, wx.ID_ANY, "Delete &All Lines", "Delete All Lines")
+        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.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.line_menu.AppendItem(item)
+        self.text_menu = wx.Menu("Whiteboard Text")
+        self.text_menu.SetTitle("Whiteboard Text")
+        item = wx.MenuItem(self.text_menu, wx.ID_ANY, "&Properties", "Properties")
+        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.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':
+            style = wx.NORMAL
+        else:
+            style = wx.ITALIC
+        if self.weight_control.GetStringSelection() == 'Normal':
+            weight = wx.NORMAL
+        else:
+            weight = wx.BOLD
+        point = str(self.point_control.GetValue())
+        c = self.text_color_control.GetForegroundColour()
+        color = self.canvas.layers['whiteboard'].r_h.hexstring(c.Red(), c.Green(), c.Blue())
+        self.text_selected_item.set_text_props(text_string, style, point, weight, color)
+        self.text_to_xml()
+        self.text_properties_dialog.Show(False)
+        self.text_selected_item.selected = False
+        self.text_selected_item.isUpdated = True
+        self.text_selected_item = None
+
+    def on_text_color(self,evt):
+        dlg = wx.ColourDialog(self)
+        if dlg.ShowModal() == wx.ID_OK:
+            c = dlg.GetColourData()
+            self.text_color_control.SetForegroundColour(c.GetColour())
+        dlg.Destroy()
+
+    def text_to_xml(self):
+        xml_str = "<map><whiteboard>"
+        xml_str += self.text_selected_item.toxml('update')
+        xml_str += "</whiteboard></map>"
+        self.canvas.frame.session.send(xml_str)
+        self.canvas.Refresh(False)
+
+    def get_text_properties(self, event=None):
+        self.text_color_control.SetForegroundColour(self.text_selected_item.textcolor)
+        self.text_control.SetValue(self.text_selected_item.text_string)
+        self.point_control.SetValue(int(self.text_selected_item.pointsize))
+        if int(self.text_selected_item.weight) == wx.NORMAL:
+            self.weight_control.SetSelection(0)
+        else:
+            self.weight_control.SetSelection(1)
+
+        if int(self.text_selected_item.style) == wx.NORMAL:
+            self.style_control.SetSelection(0)
+        else:
+            self.style_control.SetSelection(1)
+        self.text_properties_dialog.Center()
+        self.text_properties_dialog.Show(True)
+
+    def do_text_menu(self, pos, items=None):
+        if items == None:
+            self.canvas.PopupMenu(self.text_menu)
+        else:
+            menu = wx.Menu()
+            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)
+            self.canvas.PopupMenu(menu)
+        return
+
+    def onItemSelect(self, evt):
+        id = evt.GetId()-1
+        self.text_selected_item = self.ItemList[id]
+        self.text_selected_item.selected = True
+        if self.tmpPos == 'right':
+            self.canvas.PopupMenu(self.text_menu)
+        self.ItemList = None
+        self.tmpPos = None
+
+    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)
+        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)
+            if line_list:
+                self.sel_rline = self.canvas.layers['whiteboard'].get_line_by_id(line_list.id)
+                if self.sel_rline:
+                    self.do_line_menu(evt.GetPosition())
+                    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):
+        data = wx.ColourData()
+        data.SetChooseFull(True)
+        dlg = wx.ColourDialog(self.canvas, data)
+        if dlg.ShowModal() == wx.ID_OK:
+            data = dlg.GetColourData()
+            color = data.GetColour()
+            self.canvas.layers['whiteboard'].setcolor(color)
+            self.color_button.SetBackgroundColour(color)
+        dlg.Destroy()
+
+    def on_pen_width(self,evt):
+        width = int(self.widthList.GetStringSelection())
+        self.canvas.layers['whiteboard'].setwidth(width)
+
+    def undo_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  
+
+    def delete_all_lines(self,evt):
+        session = self.canvas.frame.session
+        if (session.my_role() != session.ROLE_GM) and (session.use_roles()):
+            open_rpg.get_component("chat").InfoPost("You must be a GM to use this feature")
+            return
+        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.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)
+        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
+
+    # Check Draw Mode
+    # Queries the GUI to see what mode to draw in
+    # 05-09-2003 Snowdog
+
+    def check_draw_mode(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.cone_start  = None
+
+    # Altered on_left_up to toggle between
+    # drawing modes freeform vs polyline
+    # 05-09-2003  Snowdog
+    def on_left_down(self,evt):
+        if not self.drawing:
+            self.check_draw_mode()
+        if self.drawing_mode == 'Freeform':
+            #Freeform mode ignores the inital down click
+            pass
+        elif self.drawing_mode == 'Polyline':
+            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:
+                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
+            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
+
+    # Altered on_left_up to toggle between
+    # drawing modes freeform vs polyline
+    # 05-09-2003  Snowdog
+    def on_motion(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()):
+            return
+        if self.drawing_mode == 'Freeform':
+            if 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)
+        else:
+            self.un_highlight(dc)
+            hit = self.canvas.layers['whiteboard'].hit_test_text(pos,dc)
+            if hit:
+                self.highlight(hit,dc)
+            else:
+                self.un_highlight(dc)
+        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)
+        return dc
+
+    def highlight(self,hit,dc):
+        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)
+
+    def un_highlight(self,dc):
+        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;"
+
+    # 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;"
+
+    # 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)
+        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
+
+        #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)
+
+    # 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)
+        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
+
+    # 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 on_text_left_down(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")
+            self.canvas.Refresh(False)
+            return
+        scale = self.canvas.layers['grid'].mapscale
+        dc = wx.ClientDC( self.canvas )
+        self.canvas.PrepareDC( dc )
+        dc.SetUserScale(scale,scale)
+        pos = evt.GetLogicalPosition(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)
+            else:
+                self.text_selected_item = test_text[0]
+                self.text_selected_item.selected = True
+            self.canvas.Refresh(True)
+        else:
+            if self.text_selected_item == None:
+                dlg = wx.TextEntryDialog(self,"Text to add to whiteboard", caption="Enter text",defaultValue=" ")
+                if dlg.ShowModal() == wx.ID_OK:
+                    text_string = dlg.GetValue()
+                    self.canvas.layers['whiteboard'].add_text(text_string,pos, self.style, self.pointsize, self.weight, self.canvas.layers['whiteboard'].color)
+            else:
+                self.text_selected_item.posx = pos.x
+                self.text_selected_item.posy = pos.y
+                self.text_selected_item.isUpdated = True
+                self.text_to_xml()
+                self.text_selected_item.selected = False
+                self.text_selected_item = None
+        del dc
+
+    def on_text_right_down(self, evt, dc):
+        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
+        pos = evt.GetLogicalPosition(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('right', test_text)
+            else:
+                self.text_selected_item = test_text[0]
+                self.do_text_menu('right')
+
+    def on_start_cone(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")
+            self.canvas.Refresh(False)
+            return
+        self.cone_start = self.get_snapped_to_logical_pos(evt)
+        self.drawing = True
+
+    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)
+        pos = evt.GetLogicalPosition(dc)
+        if self.canvas.layers['grid'].snap:
+            size = self.canvas.layers['grid'].unit_size
+            pos.x = int((pos.x+size/2)/size)*size
+            pos.y = int((pos.y+size/2)/size)*size
+        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)
+        pos = evt.GetLogicalPosition(dc)
+        pos2 = self.get_snapped_to_logical_pos(evt)
+        size = self.canvas.layers['grid'].unit_size #60
+        if abs(pos.x-pos2.x)<=size/10 and abs(pos.y-pos2.y)<=size/10:
+            pos = pos2
+        radius = int(int(self.radius.GetValue())/5) 
+        curve  = self.calculate_circle(self.cone_start, radius, size)
+        edge1 = []
+        edge2 = []
+        horizontal_inc = wx.Point(size,0)
+        if pos.x <= self.cone_start.x:
+            horizontal_inc = wx.Point(-size,0)
+        vertical_inc = wx.Point(0,size)
+        if pos.y <= self.cone_start.y:
+            vertical_inc = wx.Point(0,-size)
+        x_diff = float(pos.x - self.cone_start.x)
+        y_diff = float(pos.y - self.cone_start.y)
+        ratio = float(1)
+        if abs(x_diff) <= abs(y_diff):
+            ratio = x_diff / y_diff
+        elif abs(y_diff) < abs(x_diff):
+            ratio = -(y_diff / x_diff)
+            horizontal_inc,vertical_inc = vertical_inc,horizontal_inc #swap
+        horizontal_rotated = wx.Point(-horizontal_inc.y, horizontal_inc.x)
+        vertical_rotated   = wx.Point(-vertical_inc.y,   vertical_inc.x)
+        on_diagonal = True
+        for v in range(radius):
+            x = int(floor((v+1)*ratio)) - int(floor(v*ratio))
+            if x < 0 and on_diagonal:
+                edge1 += [vertical_inc]
+                edge1 += [horizontal_inc]
+            elif x != 0:
+                edge1 += [horizontal_inc]
+                edge1 += [vertical_inc]
+            else:
+                edge1 += [vertical_inc]
+                on_diagonal = False
+        on_diagonal = True
+        for v in range(radius):
+            x = int(floor((v+1)*(-ratio))) - int(floor(v*(-ratio)))
+            if x < 0 and on_diagonal:
+                edge2 += [vertical_rotated]
+                edge2 += [horizontal_rotated]
+            elif x != 0:
+                edge2 += [horizontal_rotated]
+                edge2 += [vertical_rotated]
+            else:
+                edge2 += [vertical_rotated]
+                on_diagonal = False
+        p = wx.Point(0,0)
+        string1 =  self.point_to_string(p, [self.cone_start])
+        string1 += self.point_to_string(p, edge1)
+        p = wx.Point(0,0)
+        string2 =  self.point_to_string(p, [self.cone_start])
+        string2 += self.point_to_string(p, edge2)
+
+        # truncate the edges where they meet the curve
+        pointArray = string1.split(";")
+        string1 = ""
+        for p in pointArray:
+            p += ";"
+            index = (";"+curve).find(";"+p)
+            if index >= 0:
+                # found intersection, start circle at intersection
+                curve = curve[index:]+curve[:index]
+                break
+            string1 += p
+
+        # truncate the edges where they meet the curve
+        pointArray = string2.split(";")
+        string2 = ""
+        for p in pointArray:
+            p += ";"
+            string2 = p + string2 #backwards
+            index = (";"+curve).find(";"+p)
+            if index >= 0:
+                # found intersection, end circle at intersection
+                curve = curve[:index]
+                break
+        curve = string1 + curve + string2
+
+        # add the lines that define the real cone edges
+        sz = sqrt(x_diff*x_diff + y_diff*y_diff)
+        x_diff = radius*size*x_diff/sz
+        y_diff = radius*size*y_diff/sz
+        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.canvas.Refresh(True)
+
+    def draw_temporary_circle(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")
+            self.canvas.Refresh(False)
+            return
+        pos = self.get_snapped_to_logical_pos(evt)
+        size = self.canvas.layers['grid'].unit_size #60
+        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.canvas.Refresh(True)
+
+    def calculate_circle(self, center, radius, size):
+        pos = wx.Point(center.x, center.y-size*radius)
+        r = int(radius+2/3)
+        right = wx.Point(size,0)
+        left  = wx.Point(-size,0)
+        up    = wx.Point(0,-size)
+        down  = wx.Point(0,size)
+        v1 = ([right, down, right]*r)[:radius]
+        v2 = ([down, right, down]*r)[r*3-radius:]
+        v3 = ([down, left, down]*r)[:radius]
+        v4 = ([left, down, left]*r)[r*3-radius:]
+        v5 = ([left, up, left]*r)[:radius]
+        v6 = ([up, left, up]*r)[r*3-radius:]
+        v7 = ([up, right, up]*r)[:radius]
+        v8 = ([right, up, right]*r)[r*3-radius:]
+        p = wx.Point(0,0)
+        temp_string =  self.point_to_string(p, [pos])
+        temp_string += self.point_to_string(p, v1+v2+v3+v4+v5+v6+v7+v8)
+        return temp_string
+        
+    def point_to_string(self, pos, vec):
+        str = ""
+        for i in range(len(vec)):
+            pos.x = pos.x + vec[i].x
+            pos.y = pos.y + vec[i].y
+            str += `pos.x` + "," + `pos.y` + ";"
+        return str
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/mapper/whiteboard_msg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,136 @@
+# 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: mapper/whiteboard_msg.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: whiteboard_msg.py,v 1.12 2007/03/09 14:11:56 digitalxero Exp $
+#
+# Description: This file contains some of the basic definitions for the chat
+# utilities in the orpg project.
+#
+__version__ = "$Id: whiteboard_msg.py,v 1.12 2007/03/09 14:11:56 digitalxero Exp $"
+
+from base_msg import *
+
+class item_msg(map_element_msg_base):
+
+    def __init__(self,reentrant_lock_object = None, tagname = "line"):
+        self.tagname = tagname   # set this to be for items.  Tagname gets used in some base class functions.
+        map_element_msg_base.__init__(self,reentrant_lock_object)   # call base class
+
+    # convenience method to use if only this item is modified
+    #   outputs a <map/> element containing only the changes to this item
+    def standalone_update_text(self,update_id_string):
+        buffer = "<map id='" + update_id_string + "'>"
+        buffer += "<whiteboard>"
+        buffer += self.get_changed_xml()
+        buffer += "</whiteboad></map>"
+        return buffer
+
+    # convenience method to use if only this item is modified
+    #   outputs a <map/> element that deletes this item
+    def standalone_delete_text(self,update_id_string):
+        buffer = None
+        if self._props.has_key("id"):
+            buffer = "<map id='" + update_id_string + "'>"
+            buffer += "<whiteboard>"
+            buffer += "<"+self.tagname+" action='del' id='" + self._props("id") + "'/>"
+            buffer += "</whiteboard></map>"
+        return buffer
+
+    # convenience method to use if only this item is modified
+    #   outputs a <map/> element to add this item
+    def standalone_add_text(self,update_id_string):
+        buffer = "<map id='" + update_id_string + "'>"
+        buffer += "<whiteboard>"
+        buffer += self.get_all_xml()
+        buffer += "</whiteboard></map>"
+        return buffer
+
+    def get_all_xml(self,action="new",output_action=1):
+        return map_element_msg_base.get_all_xml(self,action,output_action)
+
+    def get_changed_xml(self,action="update",output_action=1):
+        return map_element_msg_base.get_changed_xml(self,action,output_action)
+
+class whiteboard_msg(map_element_msg_base):
+
+    def __init__(self,reentrant_lock_object = None):
+        self.tagname = "whiteboard"
+        map_element_msg_base.__init__(self,reentrant_lock_object)
+
+    def init_from_dom(self,xml_dom):
+        self.p_lock.acquire()
+        if xml_dom.tagName == self.tagname:
+            if xml_dom.getAttributeKeys():
+                for k in xml_dom.getAttributeKeys():
+                    self.init_prop(k,xml_dom.getAttribute(k))
+            for c in xml_dom._get_childNodes():
+                item = item_msg(self.p_lock,c._get_nodeName())
+                try:
+                    item.init_from_dom(c)
+                except Exception, e:
+                    print e
+                    continue
+                id = item.get_prop("id")
+                action = item.get_prop("action")
+                if action == "new":
+                    self.children[id] = item
+                elif action == "del":
+                    if self.children.has_key(id):
+                        self.children[id] = None
+                        del self.children[id]
+                elif action == "update":
+                    if self.children.has_key(id):
+                        self.children[id].init_props(item.get_all_props())
+        else:
+            self.p_lock.release()
+            raise Exception, "Error attempting to initialize a " + self.tagname + " from a non-<" + self.tagname + "/> element in whiteboard"
+        self.p_lock.release()
+
+    def set_from_dom(self,xml_dom):
+        self.p_lock.acquire()
+        if xml_dom.tagName == self.tagname:
+            if xml_dom.getAttributeKeys():
+                for k in xml_dom.getAttributeKeys():
+                    self.set_prop(k,xml_dom.getAttribute(k))
+            for c in xml_dom._get_childNodes():
+                item = item_msg(self.p_lock, c._get_nodeName())
+                try:
+                    item.set_from_dom(c)
+                except Exception, e:
+                    print e
+                    continue
+                id = item.get_prop("id")
+                action = item.get_prop("action")
+                if action == "new":
+                    self.children[id] = item
+                elif action == "del":
+                    if self.children.has_key(id):
+                        self.children[id] = None
+                        del self.children[id]
+                elif action == "update":
+                    if self.children.has_key(id):
+                        self.children[id].set_props(item.get_all_props())
+        else:
+            self.p_lock.release()
+            raise Exception, "Error attempting to set a " + self.tagname + " from a non-<" + self.tagname + "/> element"
+        self.p_lock.release()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/minidom.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,576 @@
+"""\
+minidom.py -- a lightweight DOM implementation based on SAX.
+
+parse( "foo.xml" )
+
+parseString( "<foo><bar/></foo>" )
+
+Todo:
+=====
+ * convenience methods for getting elements and text.
+ * more testing
+ * bring some of the writer and linearizer code into conformance with this
+        interface
+ * SAX 2 namespaces
+"""
+
+from orpg import pulldom
+import string
+from StringIO import StringIO
+import types
+
+class Node:
+    ELEMENT_NODE                = 1
+    ATTRIBUTE_NODE              = 2
+    TEXT_NODE                   = 3
+    CDATA_SECTION_NODE          = 4
+    ENTITY_REFERENCE_NODE       = 5
+    ENTITY_NODE                 = 6
+    PROCESSING_INSTRUCTION_NODE = 7
+    COMMENT_NODE                = 8
+    DOCUMENT_NODE               = 9
+    DOCUMENT_TYPE_NODE          = 10
+    DOCUMENT_FRAGMENT_NODE      = 11
+    NOTATION_NODE               = 12
+    allnodes = {}
+    _debug = 0
+    _makeParentNodes = 1
+    debug = None
+
+    def __init__(self):
+        self.childNodes = []
+        self.ownerDocument = None
+        if Node._debug:
+            index = repr(id(self)) + repr(self.__class__)
+            Node.allnodes[index] = repr(self.__dict__)
+            if Node.debug is None:
+                Node.debug = StringIO()
+                #open( "debug4.out", "w" )
+            Node.debug.write("create %s\n" % index)
+
+    def __getattr__(self, key):
+        if key[0:2] == "__":
+            raise AttributeError
+        # getattr should never call getattr!
+        if self.__dict__.has_key("inGetAttr"):
+            del self.inGetAttr
+            raise AttributeError, key
+        prefix, attrname = key[:5], key[5:]
+        if prefix == "_get_":
+            self.inGetAttr = 1
+            if hasattr(self, attrname):
+                del self.inGetAttr
+                return (lambda self=self, attrname=attrname:
+                                getattr(self, attrname))
+            else:
+                del self.inGetAttr
+                raise AttributeError, key
+        else:
+            self.inGetAttr = 1
+            try:
+                func = getattr(self, "_get_" + key)
+            except AttributeError:
+                raise AttributeError, key
+            del self.inGetAttr
+            return func()
+
+    def __nonzero__(self):
+        return 1
+
+    def toxml(self,pretty=0):
+        writer = StringIO()
+        self.writexml(writer,pretty)
+        return str(writer.getvalue())
+
+    def hasChildNodes(self):
+        if self.childNodes:
+            return 1
+        else:
+            return 0
+
+    def getChildren(self):
+        return self.childNodes
+
+    def _get_firstChild(self):
+        if self.hasChildNodes():
+            return self.childNodes[0]
+        else:
+            return None
+
+    def _get_lastChild(self):
+        return self.childNodes[-1]
+
+    def insertBefore(self, newChild, refChild):
+        if refChild == None:
+            return self.appendChild(newChild)
+        index = self.childNodes.index(refChild)
+        self.childNodes.insert(index, newChild)
+        if self._makeParentNodes:
+            newChild.parentNode = self
+            newChild.ownerDocument = self.ownerDocument
+        return newChild
+
+    def appendChild(self, node):
+        if self.childNodes:
+            last = self.lastChild
+            node.previousSibling = last
+            last.nextSibling = node
+        else:
+            node.previousSibling = None
+        node.nextSibling = None
+        node.ownerDocument = self.ownerDocument
+        node.parentNode = self
+        self.childNodes.append(node)
+        return node
+
+    def replaceChild(self, newChild, oldChild):
+        index = self.childNodes.index(oldChild)
+        self.childNodes[index] = newChild
+        newChild.ownerDocument = self.ownerDocument
+        return oldChild
+
+    def removeChild(self, oldChild):
+        index = self.childNodes.index(oldChild)
+        del self.childNodes[index]
+        return oldChild
+
+    def cloneNode(self, deep=0):
+        newNode = Node()
+        if deep:
+            self.deep_clone(newNode)
+        return newNode
+
+    def deep_clone(self, newNode):
+        for child in self.childNodes:
+            new_child = child.cloneNode(1)
+            newNode.appendChild(new_child)
+
+    def normalize(self):
+        """Join adjacent Text nodes and delete empty Text nodes
+        in the full depth of the sub-tree underneath this Node.
+        """
+        i = 0
+        while i < len(self.childNodes):
+            cn = self.childNodes[i]
+            if cn.nodeType == Node.TEXT_NODE:
+                i = i + 1
+                # join adjacent Text nodes
+                while i < len(self.childNodes) and self.childNodes[i].nodeType == Node.TEXT_NODE:
+                    cn.nodeValue = cn.data = cn.data + self.childNodes[i].data
+                    del self.childNodes[i]
+                # delete empty nodes
+                if cn.nodeValue.strip() == "":
+                    i = i - 1
+                    del self.childNodes[i]
+                continue
+            elif cn.nodeType == Node.ELEMENT_NODE:
+                cn.normalize()
+            i = i + 1
+
+    def unlink(self):
+        self.parentNode = None
+        while self.childNodes:
+            self.childNodes[-1].unlink()
+            del self.childNodes[-1] # probably not most efficient!
+        self.childNodes = None
+        self.previousSibling = None
+        self.nextSibling = None
+        if self.attributes:
+            for attr in self._attrs.values():
+                self.removeAttributeNode(attr)
+            assert not len(self._attrs)
+            assert not len(self._attrsNS)
+        if Node._debug:
+            index = repr(id(self)) + repr(self.__class__)
+            self.debug.write("Deleting: %s\n" % index)
+            del Node.allnodes[index]
+
+def _write_data(writer, data):
+    "Writes datachars to writer."
+    data = string.replace(data, "&", "&amp;")
+    data = string.replace(data, "<", "&lt;")
+    data = string.replace(data, "\"", "&quot;")
+    data = string.replace(data, ">", "&gt;")
+    writer.write(data)
+
+def _getElementsByTagNameHelper(parent, name, rc):
+    for node in parent.childNodes:
+        if node.nodeType == Node.ELEMENT_NODE and \
+            (name == "*" or node.tagName == name):
+            rc.append(node)
+        _getElementsByTagNameHelper(node, name, rc)
+    return rc
+
+def _getElementsByTagNameNSHelper(parent, nsURI, localName, rc):
+    for node in parent.childNodes:
+        if node.nodeType == Node.ELEMENT_NODE:
+            if ((localName == "*" or node.tagName == localName) and
+                (nsURI == "*" or node.namespaceURI == nsURI)):
+                rc.append(node)
+            _getElementsByTagNameNSHelper(node, name, rc)
+
+class Attr(Node):
+    nodeType = Node.ATTRIBUTE_NODE
+
+    def __init__(self, qName, namespaceURI="", localName=None, prefix=None):
+        # skip setattr for performance
+        self.__dict__["localName"] = localName or qName
+        self.__dict__["nodeName"] = self.__dict__["name"] = qName
+        self.__dict__["namespaceURI"] = namespaceURI
+        self.__dict__["prefix"] = prefix
+        self.attributes = None
+        Node.__init__(self)
+        # nodeValue and value are set elsewhere
+
+    def __setattr__(self, name, value):
+        if name in ("value", "nodeValue"):
+            self.__dict__["value"] = self.__dict__["nodeValue"] = value
+        else:
+            self.__dict__[name] = value
+
+    def cloneNode(self, deep=0):
+        newNode = Attr(self.__dict__["name"],self.__dict__["namespaceURI"],
+                        self.__dict__["localName"],self.__dict__["prefix"])
+        newNode.__dict__["value"] = newNode.__dict__["nodeValue"] = self.value
+        if deep:
+            self.deep_clone(newNode)
+        return newNode
+
+class AttributeList:
+    """the attribute list is a transient interface to the underlying
+    dictionaries.  mutations here will change the underlying element's
+    dictionary"""
+    def __init__(self, attrs, attrsNS):
+        self._attrs = attrs
+        self._attrsNS = attrsNS
+        self.length = len(self._attrs.keys())
+
+    def copy(self):
+        clone = AttributeList(self._attrs.copy(),self._attrsNS.copy())
+        return clone
+
+    def item(self, index):
+        try:
+            return self[self.keys()[index]]
+        except IndexError:
+            return None
+
+    def items(self):
+        return map(lambda node: (node.tagName, node.value),
+                   self._attrs.values())
+
+    def itemsNS(self):
+        return map(lambda node: ((node.URI, node.localName), node.value),
+                   self._attrs.values())
+
+    def keys(self):
+        return self._attrs.keys()
+
+    def keysNS(self):
+        return self._attrsNS.keys()
+
+    def values(self):
+        return self._attrs.values()
+
+    def __len__(self):
+        return self.length
+
+    def __cmp__(self, other):
+        if self._attrs is getattr(other, "_attrs", None):
+            return 0
+        else:
+            return cmp(id(self), id(other))
+
+    #FIXME: is it appropriate to return .value?
+    def __getitem__(self, attname_or_tuple):
+        if type(attname_or_tuple) is types.TupleType:
+            return self._attrsNS[attname_or_tuple]
+        else:
+            return self._attrs[attname_or_tuple]
+
+    # same as set
+    def __setitem__(self, attname, value):
+        if type(value) is types.StringType:
+            node = Attr(attname)
+            node.value=value
+        else:
+            assert isinstance(value, Attr) or type(value) is types.StringType
+            node = value
+        old = self._attrs.get(attname, None)
+        if old:
+            old.unlink()
+        self._attrs[node.name] = node
+        self._attrsNS[(node.namespaceURI, node.localName)] = node
+
+    def __delitem__(self, attname_or_tuple):
+        node = self[attname_or_tuple]
+        node.unlink()
+        del self._attrs[node.name]
+        del self._attrsNS[(node.namespaceURI, node.localName)]
+
+class Element(Node):
+    nodeType = Node.ELEMENT_NODE
+
+    def __init__(self, tagName, namespaceURI="", prefix="",
+                 localName=None):
+        Node.__init__(self)
+        self.tagName = self.nodeName = tagName
+        self.localName = localName or tagName
+        self.prefix = prefix
+        self.namespaceURI = namespaceURI
+        self.nodeValue = None
+        self._attrs={}  # attributes are double-indexed:
+        self._attrsNS={}#    tagName -> Attribute
+                #    URI,localName -> Attribute
+                # in the future: consider lazy generation of attribute objects
+                #                this is too tricky for now because of headaches
+                #                with namespaces.
+
+    def cloneNode(self, deep=0):
+        newNode = Element(self.tagName,self.namespaceURI,self.prefix,self.localName )
+        keys = self._attrs.keys()
+        for k in keys:
+            attr = self._attrs[k].cloneNode(1)
+            newNode.setAttributeNode(attr)
+        if deep:
+            self.deep_clone(newNode)
+        return newNode
+
+    def _get_tagName(self):
+        return str(self.tagName)
+
+    def getAttributeKeys(self):
+        result = []
+        if self._attrs:
+            return self._attrs.keys()
+        else:
+            return None
+
+    def getAttribute(self, attname):
+        if self.hasAttribute(attname):
+            return str(self._attrs[attname].value)
+        else:
+            return ""
+
+    def getAttributeNS(self, namespaceURI, localName):
+        return self._attrsNS[(namespaceURI, localName)].value
+
+    def setAttribute(self, attname, value):
+        attr = Attr(attname)
+        # for performance
+        attr.__dict__["value"] = attr.__dict__["nodeValue"] = value
+        self.setAttributeNode(attr)
+
+    def setAttributeNS(self, namespaceURI, qualifiedName, value):
+        prefix, localname = _nssplit(qualifiedName)
+        # for performance
+        attr = Attr(qualifiedName, namespaceURI, localname, prefix)
+        attr.__dict__["value"] = attr.__dict__["nodeValue"] = value
+        self.setAttributeNode(attr)
+        # FIXME: return original node if something changed.
+
+    def getAttributeNode(self, attrname):
+        return self._attrs.get(attrname)
+
+    def getAttributeNodeNS(self, namespaceURI, localName):
+        return self._attrsNS[(namespaceURI, localName)]
+
+    def setAttributeNode(self, attr):
+        old = self._attrs.get(attr.name, None)
+        if old:
+            old.unlink()
+        self._attrs[attr.name] = attr
+        self._attrsNS[(attr.namespaceURI, attr.localName)] = attr
+        # FIXME: return old value if something changed
+
+    def removeAttribute(self, name):
+        attr = self._attrs[name]
+        self.removeAttributeNode(attr)
+
+    def removeAttributeNS(self, namespaceURI, localName):
+        attr = self._attrsNS[(namespaceURI, localName)]
+        self.removeAttributeNode(attr)
+
+    def removeAttributeNode(self, node):
+        node.unlink()
+        del self._attrs[node.name]
+        del self._attrsNS[(node.namespaceURI, node.localName)]
+
+    def hasAttribute(self, name):
+        return self._attrs.has_key(name)
+
+    def hasAttributeNS(self, namespaceURI, localName):
+        return self._attrsNS.has_key((namespaceURI, localName))
+
+    def getElementsByTagName(self, name):
+        return _getElementsByTagNameHelper(self, name, [])
+
+    def getElementsByTagNameNS(self, namespaceURI, localName):
+        _getElementsByTagNameNSHelper(self, namespaceURI, localName, [])
+
+    def __repr__(self):
+        return "<DOM Element: %s at %s>" % (self.tagName, id(self))
+
+    # undocumented
+    def writexml(self, writer, tabs=0):
+        tab_str = ""
+        if tabs:
+            tab_str = "\n" + ("  "*(tabs-1))
+            tabs += 1
+        writer.write(tab_str + "<" + self.tagName)
+        a_names = self._get_attributes().keys()
+        a_names.sort()
+
+        for a_name in a_names:
+            writer.write(" %s=\"" % a_name)
+            _write_data(writer, self._get_attributes()[a_name].value)
+            writer.write("\"")
+        if self.childNodes:
+            writer.write(">")
+            for node in self.childNodes:
+                node.writexml(writer,tabs)
+            if self.childNodes[0].nodeType == Node.TEXT_NODE:
+                tab_str = ""
+            writer.write(tab_str + "</%s>" % self.tagName)
+        else:
+            writer.write("/>")
+
+    def _get_attributes(self):
+        return AttributeList(self._attrs, self._attrsNS)
+
+class Comment(Node):
+    nodeType = Node.COMMENT_NODE
+
+    def __init__(self, data):
+        Node.__init__(self)
+        self.data = self.nodeValue = data
+        self.nodeName = "#comment"
+        self.attributes = None
+
+    def writexml(self, writer, tabs=0):
+        writer.write("<!--%s-->" % self.data)
+
+    def cloneNode(self, deep=0):
+        newNode = Comment(self.data)
+        if deep:
+            self.deep_clone(newNode)
+        return newNode
+
+class ProcessingInstruction(Node):
+    nodeType = Node.PROCESSING_INSTRUCTION_NODE
+
+    def __init__(self, target, data):
+        Node.__init__(self)
+        self.target = self.nodeName = target
+        self.data = self.nodeValue = data
+        self.attributes = None
+
+    def writexml(self, writer, tabs=0):
+        writer.write("<?%s %s?>" % (self.target, self.data))
+
+    def cloneNode(self, deep=0):
+        newNode = ProcessingInstruction(self.target, self.data)
+        if deep:
+            self.deep_clone(newNode)
+        return newNode
+
+class Text(Node):
+    nodeType = Node.TEXT_NODE
+    nodeName = "#text"
+
+    def __init__(self, data):
+        Node.__init__(self)
+        self.data = self.nodeValue = data
+        self.attributes = None
+
+    def __repr__(self):
+        if len(self.data) > 10:
+            dotdotdot = "..."
+        else:
+            dotdotdot = ""
+        return "<DOM Text node \"%s%s\">" % (self.data[0:10], dotdotdot)
+
+    def writexml(self, writer, tabs=0):
+        _write_data(writer, self.data)
+
+    def _get_nodeValue(self):
+        return str(self.nodeValue)
+
+    def _set_nodeValue(self,data):
+        self.data = self.nodeValue = data
+
+    def cloneNode(self, deep=0):
+        newNode = Text(self.data)
+        if deep:
+            self.deep_clone(newNode)
+        return newNode
+
+def _nssplit(qualifiedName):
+    fields = string.split(qualifiedName,':', 1)
+    if len(fields) == 2:
+        return fields
+    elif len(fields) == 1:
+        return ('', fields[0])
+
+class Document(Node):
+    nodeType = Node.DOCUMENT_NODE
+    documentElement = None
+
+    def __init__(self):
+        Node.__init__(self)
+        self.attributes = None
+        self.nodeName = "#document"
+        self.nodeValue = None
+        self.ownerDocument = self
+
+    def appendChild(self, node):
+        if node.nodeType == Node.ELEMENT_NODE:
+            if self.documentElement:
+                raise TypeError, "Two document elements disallowed"
+            else:
+                self.documentElement = node
+        Node.appendChild(self, node)
+        return node
+    createElement = Element
+    createTextNode = Text
+    createComment = Comment
+    createProcessingInstruction = ProcessingInstruction
+    createAttribute = Attr
+
+    def createElementNS(self, namespaceURI, qualifiedName):
+        prefix,localName = _nssplit(qualifiedName)
+        return Element(qualifiedName, namespaceURI, prefix, localName)
+
+    def createAttributeNS(self, namespaceURI, qualifiedName):
+        prefix,localName = _nssplit(qualifiedName)
+        return Attr(qualifiedName, namespaceURI, localName, prefix)
+
+    def getElementsByTagNameNS(self, namespaceURI, localName):
+        _getElementsByTagNameNSHelper(self, namespaceURI, localName)
+
+    def unlink(self):
+        self.documentElement = None
+        Node.unlink(self)
+
+    def getElementsByTagName(self, name):
+        rc = []
+        _getElementsByTagNameHelper(self, name, rc)
+        return rc
+
+    def writexml(self, writer):
+        for node in self.childNodes:
+            node.writexml(writer)
+
+def _doparse(func, args, kwargs):
+    events = apply(func, args, kwargs)
+    toktype, rootNode = events.getEvent()
+    events.expandNode(rootNode)
+    return rootNode
+
+def parse(*args, **kwargs):
+    "Parse a file into a DOM by filename or file object"
+    return _doparse(pulldom.parse, args, kwargs)
+
+def parseString(*args, **kwargs):
+    "Parse a file into a DOM from a string"
+    return _doparse(pulldom.parseString, args, kwargs)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+__all__ = ['gsclient','meta_server_lib','mplay_client','mplay_server','mplay_server_gui']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/gsclient.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,711 @@
+# 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: gsclient.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: gsclient.py,v 1.53 2007/10/25 21:49:34 digitalxero Exp $
+#
+# Description: The file contains code for the game server browser
+#
+
+__version__ = "$Id: gsclient.py,v 1.53 2007/10/25 21:49:34 digitalxero Exp $"
+
+import orpg.dirpath
+from orpg.orpg_windows import *
+from orpg.orpg_xml import *
+import meta_server_lib
+import orpg.tools.orpg_settings
+import orpg.tools.rgbhex
+from orpg.orpgCore import open_rpg
+import traceback
+
+gs_host = 1
+gs_join = 2
+# constants
+
+LIST_SERVER = wx.NewId()
+LIST_ROOM = wx.NewId()
+ADDRESS = wx.NewId()
+GS_CONNECT = wx.NewId()
+GS_DISCONNECT = wx.NewId()
+GS_SERVER_REFRESH = wx.NewId()
+GS_JOIN = wx.NewId()
+GS_JOINLOBBY = wx.NewId()
+GS_CREATE_ROOM = wx.NewId()
+GS_PWD = wx.NewId()
+GS_CLOSE = wx.NewId()
+OR_CLOSE = wx.NewId()
+
+class server_instance:
+    def __init__(self, id, name="[Unknown]", users="0", address="127.0.0.1", port="9557"):
+        self.id = id
+        self.name = name
+        self.user = users
+        self.addy = address
+        self.port = port
+
+def server_instance_compare(x,y):
+    """compares server insances for list sort"""
+    DEV_SERVER = "OpenRPG DEV"
+    xuser = 0
+    yuser = 0
+    try: xuser = int(x.user)
+    except: pass
+    try: yuser = int(y.user)
+    except: pass
+    if x.name.startswith(DEV_SERVER):
+        if y.name.startswith(DEV_SERVER):
+            if x.name > y.name: return 1
+            elif x.name == y.name:
+                if xuser < yuser: return 1
+                elif xuser > yuser: return -1
+                else: return 0
+            else: return -1
+        else: return -1
+    elif y.name.startswith(DEV_SERVER): return 1
+    elif xuser < yuser: return 1
+    elif xuser == yuser:
+        if x.name > y.name: return 1
+        elif x.name == y.name: return 0
+        else: return -1
+    else: return -1
+
+def roomCmp(room1, room2):
+    if int(room1) > int(room2):
+        return 1
+    elif int(room1) < int(room2):
+        return -1
+    return 0
+
+class game_server_panel(wx.Panel):
+    def __init__(self,parent):
+        wx.Panel.__init__(self, parent, -1)
+        self.parent = parent
+        self.log = open_rpg.get_component('log')
+        self.log.log("Enter game_server_panel", ORPG_DEBUG)
+        self.password_manager = open_rpg.get_component('password_manager') # passtool --SD 8/03
+        self.frame = open_rpg.get_component('frame')
+        self.session = open_rpg.get_component('session')
+        self.settings = open_rpg.get_component('settings')
+        self.xml = open_rpg.get_component('xml')
+        self.serverNameSet = 0
+        self.last_motd = ""
+        self.buttons = {}
+        self.texts = {}
+        self.svrList = []
+        self.build_ctrls()
+        self.refresh_server_list()
+        self.refresh_room_list()
+        self.log.log("Exit game_server_panel", ORPG_DEBUG)
+
+#---------------------------------------------------------
+# [START] Snowdog: Updated Game Server Window 12/02
+#---------------------------------------------------------
+    def build_ctrls(self):
+        self.log.log("Enter game_server_panel->build_ctrls(self)", ORPG_DEBUG)
+
+        ## Section Sizers (with frame edges and text captions)
+        self.box_sizers = {}
+        self.box_sizers["server"] = wx.StaticBox(self, -1, "Server")
+        self.box_sizers["window"] = wx.StaticBox(self, -1, "Exit" )
+        self.box_sizers["room"] = wx.StaticBox(self, -1, "Rooms")
+        self.box_sizers["c_room"] = wx.StaticBox(self, -1, "Create Room")
+
+        ## Layout Sizers
+        self.sizers = {}
+        self.sizers["main"] = wx.GridBagSizer(hgap=1, vgap=1)
+        self.sizers["server"] = wx.StaticBoxSizer(self.box_sizers["server"], wx.VERTICAL)
+        self.sizers["rooms"] = wx.StaticBoxSizer(self.box_sizers["room"], wx.VERTICAL)
+        self.sizers["close"] = wx.StaticBoxSizer(self.box_sizers["window"], wx.HORIZONTAL)
+        self.sizers["c_room"] = wx.StaticBoxSizer(self.box_sizers["c_room"], wx.VERTICAL)
+
+        #Build Server Sizer
+        adder = wx.StaticText(self, -1, "Address:")
+        self.texts["address"] = wx.TextCtrl(self, ADDRESS)
+        servers = wx.StaticText(self, -1, "Servers:")
+        self.server_list = wx.ListCtrl(self, LIST_SERVER, style=wx.LC_REPORT | wx.SUNKEN_BORDER )
+        self.server_list.InsertColumn(0, "Players", wx.LIST_FORMAT_LEFT, 0)
+        self.server_list.InsertColumn(1, "Name", wx.LIST_FORMAT_LEFT, 0)
+        self.buttons[GS_CONNECT] = wx.Button(self, GS_CONNECT, "Connect")
+        self.buttons[GS_SERVER_REFRESH] = wx.Button(self, GS_SERVER_REFRESH, "Refresh")
+        self.buttons[GS_DISCONNECT] = wx.Button(self, GS_DISCONNECT, "Disconnect")
+        self.sizers["svrbtns"] = wx.BoxSizer(wx.HORIZONTAL)
+        self.sizers["svrbtns"].Add(self.buttons[GS_CONNECT], 0, wx.EXPAND)
+        self.sizers["svrbtns"].Add(self.buttons[GS_SERVER_REFRESH], 0, wx.EXPAND)
+        self.sizers["svrbtns"].Add(self.buttons[GS_DISCONNECT], 0, wx.EXPAND)
+        self.sizers["server"].Add(adder, 0, wx.EXPAND)
+        self.sizers["server"].Add(self.texts["address"], 0, wx.EXPAND)
+        self.sizers["server"].Add(servers, 0, wx.EXPAND)
+        self.sizers["server"].Add(self.server_list, 1, wx.EXPAND)
+        self.sizers["server"].Add(self.sizers["svrbtns"], 0, wx.EXPAND)
+
+        #Build Rooms Sizer
+        self.room_list = wx.ListCtrl(self, LIST_ROOM, style=wx.LC_REPORT | wx.SUNKEN_BORDER)
+        self.room_list.InsertColumn(0,"Game", wx.LIST_FORMAT_LEFT,0)
+        self.room_list.InsertColumn(1,"Players", wx.LIST_FORMAT_LEFT,0)
+        self.room_list.InsertColumn(2,"PW", wx.LIST_FORMAT_LEFT,0)
+        self.buttons[GS_JOIN] = wx.Button(self, GS_JOIN, "Join Room")
+        self.buttons[GS_JOINLOBBY] = wx.Button(self, GS_JOINLOBBY, "Lobby")
+        self.sizers["roombtns"] = wx.BoxSizer(wx.HORIZONTAL)
+        self.sizers["roombtns"].Add(self.buttons[GS_JOIN], 0, wx.EXPAND)
+        self.sizers["roombtns"].Add(self.buttons[GS_JOINLOBBY], 0, wx.EXPAND)
+        self.sizers["rooms"].Add(self.room_list, 1, wx.EXPAND)
+        self.sizers["rooms"].Add(self.sizers["roombtns"], 0, wx.EXPAND)
+
+        #Build Close Sizer
+        self.buttons[OR_CLOSE] = wx.Button(self, OR_CLOSE,"Exit OpenRPG")
+        self.buttons[GS_CLOSE] = wx.Button(self, GS_CLOSE,"Close Window")
+        self.sizers["close"].Add(self.buttons[OR_CLOSE], 1, wx.ALIGN_CENTER_VERTICAL)
+        self.sizers["close"].Add(self.buttons[GS_CLOSE], 1, wx.ALIGN_CENTER_VERTICAL)
+
+        #Build Create Room Sizer
+        rname = wx.StaticText(self,-1, "Room Name:")
+        self.texts["room_name"] = wx.TextCtrl(self, -1)
+        rpass = wx.StaticText(self,-1, "Password:")
+        self.buttons[GS_PWD] = wx.CheckBox(self, GS_PWD, "")
+        self.texts["room_pwd"] = wx.TextCtrl(self, -1)
+        self.texts["room_pwd"].Enable(0)
+        pwsizer = wx.BoxSizer(wx.HORIZONTAL)
+        pwsizer.Add(self.buttons[GS_PWD],0,0)
+        pwsizer.Add(self.texts["room_pwd"], 1, wx.EXPAND)
+        apass = wx.StaticText(self,-1, "Admin Password:")
+        self.texts["room_boot_pwd"] = wx.TextCtrl(self, -1)
+        minver = wx.StaticText(self,-1, "Minimum Version:")
+        self.texts["room_min_version"] = wx.TextCtrl(self, -1)
+        self.sizers["c_room_layout"] = wx.FlexGridSizer(rows=8, cols=2, hgap=1, vgap=1)
+        self.sizers["c_room_layout"].Add(rname, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.ALL)
+        self.sizers["c_room_layout"].Add(self.texts["room_name"], 0, wx.EXPAND)
+        self.sizers["c_room_layout"].Add(rpass, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.ALL)
+        self.sizers["c_room_layout"].Add(pwsizer, 0, wx.EXPAND)
+        self.sizers["c_room_layout"].Add(apass, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.ALL)
+        self.sizers["c_room_layout"].Add(self.texts["room_boot_pwd"], 0, wx.EXPAND)
+        self.sizers["c_room_layout"].Add(minver, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.ALL)
+        self.sizers["c_room_layout"].Add(self.texts["room_min_version"], 0, wx.EXPAND)
+        self.sizers["c_room_layout"].AddGrowableCol(1)
+        self.buttons[GS_CREATE_ROOM] = wx.Button(self, GS_CREATE_ROOM, "Create Room")
+        self.sizers["c_room"].Add(self.sizers["c_room_layout"], 1, wx.EXPAND)
+        self.sizers["c_room"].Add(self.buttons[GS_CREATE_ROOM], 0, wx.EXPAND)
+
+        #Build Main Sizer
+        self.sizers["main"].Add(self.sizers["server"], (0,0), span=(2,1), flag=wx.EXPAND)
+        self.sizers["main"].Add(self.sizers["rooms"], (0,1), flag=wx.EXPAND)
+        self.sizers["main"].Add(self.sizers["close"], (2,0), flag=wx.EXPAND)
+        self.sizers["main"].Add(self.sizers["c_room"], (1,1), span=(2,1), flag=wx.EXPAND)
+        self.sizers["main"].AddGrowableCol(0)
+        self.sizers["main"].AddGrowableCol(1)
+        self.sizers["main"].AddGrowableRow(0)
+        self.SetSizer(self.sizers["main"])
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        ## Event Handlers
+        self.Bind(wx.EVT_BUTTON, self.on_button, id=GS_CONNECT)
+        self.Bind(wx.EVT_BUTTON, self.on_button, id=GS_DISCONNECT)
+        self.Bind(wx.EVT_BUTTON, self.on_button, id=GS_CREATE_ROOM)
+        self.Bind(wx.EVT_BUTTON, self.on_button, id=GS_JOIN)
+        self.Bind(wx.EVT_BUTTON, self.on_button, id=GS_JOINLOBBY)
+        self.Bind(wx.EVT_BUTTON, self.on_button, id=GS_SERVER_REFRESH)
+        self.Bind(wx.EVT_BUTTON, self.on_button, id=GS_CLOSE)
+        self.Bind(wx.EVT_BUTTON, self.on_button, id=OR_CLOSE)
+        self.Bind(wx.EVT_CHECKBOX, self.on_button, id=GS_PWD)
+
+        # Added double click handlers 5/05 -- Snowdog
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_server_dbclick, id=LIST_SERVER)
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_room_dbclick, id=LIST_ROOM)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_select, id=LIST_ROOM)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_select, id=LIST_SERVER)
+        self.texts['address'].Bind(wx.EVT_SET_FOCUS, self.on_text)
+        self.set_connected(self.session.is_connected())
+        self.cur_room_index = -1
+        self.cur_server_index = -1
+        self.rmList = {}
+        self.log.log("Exit game_server_panel->build_ctrls(self)", ORPG_DEBUG)
+
+#---------------------------------------------------------
+# [END] Snowdog: Updated Game Server Window 12/02
+#---------------------------------------------------------
+
+
+    #-----------------------------------------------------
+    # on_server_dbclick()
+    # support for double click selection of server.
+    # 5/16/05 -- Snowdog
+    #-----------------------------------------------------
+    def on_server_dbclick(self, evt=None):
+        self.log.log("Enter game_server_panel->on_server_dbclick(self, evt)", ORPG_DEBUG)
+
+        #make sure address is updated just in case list select wasn't done
+        try:
+            self.on_select(evt)
+        except:
+            pass
+        address = self.texts["address"].GetValue()
+        if self.session.is_connected():
+            if self.session.host_server == address :
+                #currently connected to address. Do nothing.
+                return
+            else:
+                #address differs, disconnect.
+                self.frame.kill_mplay_session()
+        self.do_connect(address)
+        self.log.log("Exit game_server_panel->on_server_dbclick(self, evt)", ORPG_DEBUG)
+
+
+    #-----------------------------------------------------
+    # on_room_dbclick()
+    # support for double click selection of server.
+    # 5/16/05 -- Snowdog
+    #-----------------------------------------------------
+
+    def on_room_dbclick(self, evt=None):
+        self.log.log("Enter game_server_panel->on_room_dbclick(self, evt)", ORPG_DEBUG)
+
+        #make sure address is updated just in case list select wasn't done
+        try:
+            self.on_select(evt)
+        except:
+            pass
+        group_id = str(self.room_list.GetItemData(self.cur_room_index))
+
+        if self.NoGroups:
+            self.NoGroups = False
+            self.session.group_id = group_id
+            self.on_server_dbclick()
+            return
+
+        if self.cur_room_index >= 0:
+            if self.cur_room_index != 0:
+                self.set_lobbybutton(1);
+            else:
+                self.set_lobbybutton(0);
+            group = self.session.get_group_info(group_id)
+            pwd = ""
+            if (group[2] == "True") or (group[2] == "1"):
+                pwd = self.password_manager.GetPassword("room", group_id)
+            else:
+                pwd = ""
+            self.session.send_join_group(group_id, pwd)
+        self.log.log("Exit game_server_panel->on_room_dbclick(self, evt)", ORPG_DEBUG)
+
+
+    def on_select(self,evt):
+        self.log.log("Enter game_server_panel->on_select(self,evt)", ORPG_DEBUG)
+        id = evt.GetId()
+        if id == LIST_ROOM:
+            self.cur_room_index = evt.m_itemIndex
+        elif id==LIST_SERVER:
+            self.cur_server_index = evt.m_itemIndex
+            self.name = self.svrList[self.cur_server_index].name
+            address = self.svrList[self.cur_server_index].addy
+            port = self.svrList[self.cur_server_index].port
+            self.texts["address"].SetValue(address+":"+str(port))
+            self.refresh_room_list()
+        self.log.log("Exit game_server_panel->on_select(self,evt)", ORPG_DEBUG)
+
+    def on_text(self,evt):
+        self.log.log("Enter game_server_panel->on_text(self,evt)", ORPG_DEBUG)
+        id = evt.GetId()
+        if (id == ADDRESS) and (self.cur_server_index >= 0):
+            #print "ADDRESS id = ", id, "index = ", self.cur_server_index
+            self.cur_server_index = -1
+        evt.Skip()
+        self.log.log("Exit game_server_panel->on_text(self,evt)", ORPG_DEBUG)
+
+    def add_room(self,data):
+        self.log.log("Enter game_server_panel->add_room(self,data)", ORPG_DEBUG)
+        i = self.room_list.GetItemCount()
+        if (data[2]=="1") or (data[2]=="True"): pwd="yes"
+        else: pwd="no"
+        self.room_list.InsertStringItem(i,data[1])
+        self.room_list.SetStringItem(i,1,data[3])
+        self.room_list.SetStringItem(i,2,pwd)
+        self.room_list.SetItemData(i,int(data[0]))
+        self.refresh_room_list()
+        self.log.log("Exit game_server_panel->add_room(self,data)", ORPG_DEBUG)
+
+    def del_room(self, data):
+        self.log.log("Enter game_server_panel->del_room(self, data)", ORPG_DEBUG)
+        i = self.room_list.FindItemData(-1, int(data[0]))
+        self.room_list.DeleteItem(i)
+        self.refresh_room_list()
+        self.log.log("Exit game_server_panel->del_room(self, data)", ORPG_DEBUG)
+
+#---------------------------------------------------------
+# [START] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+
+    def update_room(self,data):
+        self.log.log("Enter game_server_panel->update_room(self,data)", ORPG_DEBUG)
+
+        #-------------------------------------------------------
+        # Udated 12/02 by Snowdog
+        # allows refresh of all room data not just player counts
+        #-------------------------------------------------------
+        i = self.room_list.FindItemData(-1,int(data[0]))
+        if data[2]=="1" : pwd="yes"
+        else: pwd="no"
+        self.room_list.SetStringItem(i,0,data[1])
+        self.room_list.SetStringItem(i,1,data[3])
+        self.room_list.SetStringItem(i,2,pwd)
+        self.refresh_room_list()
+        self.log.log("Exit game_server_panel->update_room(self,data)", ORPG_DEBUG)
+
+#---------------------------------------------------------
+# [END] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+
+    def set_cur_room_text(self,name):
+        pass
+        #self.texts["cur_room"].SetLabel(name)
+        #self.sizers["room"].Layout()
+
+    def set_lobbybutton(self,allow):
+        self.log.log("Enter game_server_panel->set_lobbybutton(self,allow)", ORPG_DEBUG)
+        self.buttons[GS_JOINLOBBY].Enable(allow)
+        self.log.log("Exit game_server_panel->set_lobbybutton(self,allow)", ORPG_DEBUG)
+
+    def set_connected(self,connected):
+        self.log.log("Enter game_server_panel->set_connected(self,connected)", ORPG_DEBUG)
+        self.buttons[GS_CONNECT].Enable(not connected)
+        self.buttons[GS_DISCONNECT].Enable(connected)
+        self.buttons[GS_JOIN].Enable(connected)
+        self.buttons[GS_CREATE_ROOM].Enable(connected)
+        if not connected:
+            self.buttons[GS_JOINLOBBY].Enable(connected)
+            self.room_list.DeleteAllItems()
+            self.set_cur_room_text("Not Connected!")
+            self.cur_room_index = -1
+            self.frame.status.set_connect_status("Not Connected")
+        else:
+            #data = self.session.get_my_group()
+            self.frame.status.set_connect_status(self.name)
+            self.set_cur_room_text("Lobby")
+        self.log.log("Exit game_server_panel->set_connected(self,connected)", ORPG_DEBUG)
+
+    def on_button(self,evt):
+        self.log.log("Enter game_server_panel->son_button(self,evt)", ORPG_DEBUG)
+        id = evt.GetId()
+        if id == GS_CONNECT:
+            address = self.texts["address"].GetValue()
+            ### check to see if this is a manual entry vs. list entry.
+            try:
+                dummy = self.name
+            except:
+                self.name = `address`
+            self.do_connect(address)
+        elif id == GS_DISCONNECT:
+            self.frame.kill_mplay_session()
+        elif id == GS_CREATE_ROOM:
+            self.do_create_group()
+        elif id == GS_JOIN:
+            self.do_join_group()
+        elif id == GS_JOINLOBBY:
+            self.do_join_lobby()
+        elif id == GS_SERVER_REFRESH:
+            self.refresh_server_list()
+        elif id == GS_PWD:
+            self.texts["room_pwd"].Enable(evt.Checked())
+        elif id == OR_CLOSE:
+            dlg = wx.MessageDialog(self,"Quit OpenRPG?","OpenRPG",wx.YES_NO)
+            if dlg.ShowModal() == wx.ID_YES:
+                dlg.Destroy()
+                self.frame.kill_mplay_session()
+                self.frame.closed_confirmed()
+        elif id == GS_CLOSE:
+            self.parent.OnMB_GameServerBrowseServers()
+        self.log.log("Exit game_server_panel->son_button(self,evt)", ORPG_DEBUG)
+
+    def refresh_room_list(self):
+        self.log.log("Enter game_server_panel->refresh_room_list(self)", ORPG_DEBUG)
+        self.room_list.DeleteAllItems()
+        address = self.texts["address"].GetValue()
+        try:
+            cadder = self.session.host_server
+        except:
+            cadder = ''
+        if self.rmList.has_key(address) and len(self.rmList[address]) > 0 and cadder != address:
+            groups = self.rmList[address]
+            self.NoGroups = True
+        else:
+            self.NoGroups = False
+            groups = self.session.get_groups()
+        for g in groups:
+            i = self.room_list.GetItemCount()
+            if (g[2]=="True") or (g[2]=="1") : pwd="yes"
+            else: pwd="no"
+            self.room_list.InsertStringItem(i, g[1])
+            self.room_list.SetStringItem(i, 1, g[3])
+            self.room_list.SetStringItem(i, 2, pwd)
+            self.room_list.SetItemData(i, int(g[0]))
+        if self.room_list.GetItemCount() > 0:
+            self.colorize_group_list(groups)
+            self.room_list.SortItems(roomCmp)
+            wx.CallAfter(self.autosizeRooms)
+        self.log.log("Exit game_server_panel->refresh_room_list(self)", ORPG_DEBUG)
+
+    def autosizeRooms(self):
+        self.log.log("Enter game_server_panel->autosizeRooms(self)", ORPG_DEBUG)
+        self.room_list.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+        self.room_list.SetColumnWidth(1, wx.LIST_AUTOSIZE)
+        self.room_list.SetColumnWidth(2, wx.LIST_AUTOSIZE)
+        self.log.log("Exit game_server_panel->autosizeRooms(self)", ORPG_DEBUG)
+
+    def refresh_server_list(self):
+        self.log.log("Enter game_server_panel->refresh_server_list(self)", ORPG_DEBUG)
+
+        try:
+            self.svrList = []
+            self.server_list.DeleteAllItems()
+            xml_dom = meta_server_lib.get_server_list(["2"]);
+            node_list = xml_dom.getElementsByTagName('server')
+            hex = orpg.tools.rgbhex.RGBHex()
+            color1 = self.settings.get_setting("RoomColor_Active")
+            color2 = self.settings.get_setting("RoomColor_Locked")
+            color3 = self.settings.get_setting("RoomColor_Empty")
+            color4 = self.settings.get_setting("RoomColor_Lobby")
+
+            if len(node_list):
+                length = len(node_list)
+                part = 0
+                partLength = 1.0/length
+                for n in node_list:
+                    if n.hasAttribute('id') and n.hasAttribute('name') and n.hasAttribute('num_users') and n.hasAttribute('address') and n.hasAttribute('port'):
+                        self.svrList.append( server_instance(n.getAttribute('id'),n.getAttribute('name'), n.getAttribute('num_users'), n.getAttribute('address'),n.getAttribute('port')))
+                        address = n.getAttribute('address') + ':' + n.getAttribute('port')
+                        self.rmList[address] = []
+                        rooms = n.getElementsByTagName('room')
+
+                        for room in rooms:
+                            pwd = room.getAttribute("pwd")
+                            id = room.getAttribute("id")
+                            name = room.getAttribute("name")
+                            nump = room.getAttribute("num_users")
+                            self.rmList[address].append((id, name, pwd, nump))
+                self.svrList.sort(server_instance_compare)
+
+                for n in self.svrList:
+                    i = self.server_list.GetItemCount()
+                    name = n.name
+                    players = n.user
+                    self.server_list.InsertStringItem(i,players)
+                    self.server_list.SetStringItem(i,1,name)
+                    r,g,b = hex.rgb_tuple(color1)
+                    svrcolor = wx.Colour(red=r,green=g,blue=b)
+
+                    if players == "0":
+                        r,g,b = hex.rgb_tuple(color3)
+                        svrcolor = wx.Colour(red=r,green=g,blue=b)
+
+                    if n.name.startswith("OpenRPG DEV"):
+                        r,g,b = hex.rgb_tuple(color2)
+                        svrcolor = wx.Colour(red=r,green=g,blue=b)
+
+                    item = self.server_list.GetItem(i)
+                    item.SetTextColour(svrcolor)
+                    self.server_list.SetItem(item)
+                    part += partLength
+                self.servers = node_list
+
+            # No server is currently selected!!!  Versus the broken and random 0!
+            self.cur_server_index = -1
+            self.server_list.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+            self.server_list.SetColumnWidth(1, wx.LIST_AUTOSIZE)
+
+            if self.serverNameSet == 0:
+                # Pointless to constantly set the address field to random
+                # server.  This way, at least, if someone has their own local
+                # server it can now easily be connected with.  Nor do we want
+                # to reset this every time as it would wipe any previously
+                # entered data or even the previously selected server data!
+                # Localhost should probably be used but some systems are ultra/lame
+                # broken about localhost use.
+                self.texts["address"].SetValue("127.0.0.1:6774")
+                self.serverNameSet = 1
+            else:
+                pass
+
+            #  Allow xml_dom to be collected
+            try:
+                xml_dom.unlink()
+            except:
+                pass
+        except Exception, e:
+            print "Server List not available."
+            traceback.print_exc()
+        self.log.log("Exit game_server_panel->refresh_server_list(self)", ORPG_DEBUG)
+
+    def failed_connection(self):
+        self.log.log("Enter game_server_panel->failed_connection(self)", ORPG_DEBUG)
+        if(self.cur_server_index >= 0):
+            id = self.servers[self.cur_server_index].getAttribute('id')
+            meta = self.servers[self.cur_server_index].getAttribute('meta')
+            address = self.servers[self.cur_server_index].getAttribute('address')
+            port = self.servers[self.cur_server_index].getAttribute('port')
+            #  post_failed_connection will return a non-zero if the server
+            #  was removed.  If it was, refresh the display
+            if(meta_server_lib.post_failed_connection(id,meta=meta,address=address,port=port)):
+                self.refresh_server_list()
+        self.log.log("Exit game_server_panel->failed_connection(self)", ORPG_DEBUG)
+
+    def do_connect(self, address):
+        self.log.log("Enter game_server_panel->do_connect(self, address)", ORPG_DEBUG)
+        chat = open_rpg.get_component('chat')
+        chat.InfoPost("Locating server at " + address + "...")
+        if self.session.connect(address):
+            self.frame.start_timer()
+        else:
+            chat.SystemPost("Failed to connect to game server...")
+            self.failed_connection()
+        self.log.log("Exit game_server_panel->do_connect(self, address)", ORPG_DEBUG)
+
+    def do_join_lobby(self):
+        self.log.log("Enter game_server_panel->do_join_lobby(self)", ORPG_DEBUG)
+        self.cur_room_index = 0
+        self.session.send_join_group("0","")
+        self.set_lobbybutton(0);
+        self.log.log("Exit game_server_panel->do_join_lobby(self)", ORPG_DEBUG)
+
+    def do_join_group(self):
+        self.log.log("Enter game_server_panel->do_join_group(self)", ORPG_DEBUG)
+        if self.cur_room_index >= 0:
+            if self.cur_room_index != 0:
+                self.set_lobbybutton(1);
+            else:
+                self.set_lobbybutton(0);
+            group_id = str(self.room_list.GetItemData(self.cur_room_index))
+            group = self.session.get_group_info(group_id)
+            pwd = ""
+            if (group[2] == "True") or (group[2] == "1"):
+                pwd = self.password_manager.GetPassword("room", group_id)
+                #dlg = wx.TextEntryDialog(self,"Password?","Join Private Room")
+                #if dlg.ShowModal() == wx.ID_OK:
+                #    pwd = dlg.GetValue()
+                #dlg.Destroy()
+            else:
+                pwd = ""
+            if pwd != None: #pwd==None means the user clicked "Cancel"
+                self.session.send_join_group(group_id,pwd)
+        self.log.log("Exit game_server_panel->do_join_group(self)", ORPG_DEBUG)
+
+    def do_create_group(self):
+        self.log.log("Enter game_server_panel->do_create_group(self)", ORPG_DEBUG)
+        name = self.texts["room_name"].GetValue()
+        boot_pwd = self.texts["room_boot_pwd"].GetValue()
+        minversion = self.texts["room_min_version"].GetValue()
+        #
+        # Check for & in name.  We want to allow this becaus of its common use in D&D.
+        #
+        loc = name.find("&")
+        oldloc=0
+        while loc > -1:
+            loc = name.find("&",oldloc)
+            if loc > -1:
+                b = name[:loc]
+                e = name[loc+1:]
+                name = b + "&amp;" + e
+                oldloc = loc+1
+        loc = name.find('"')
+        oldloc=0
+        while loc > -1:
+            loc = name.find('"',oldloc)
+            if loc > -1:
+                b = name[:loc]
+                e = name[loc+1:]
+                name = b + "&quote;" + e
+                oldloc = loc+1
+        loc = name.find("'")
+        oldloc=0
+        while loc > -1:
+            loc = name.find("'",oldloc)
+            if loc > -1:
+                b = name[:loc]
+                e = name[loc+1:]
+                name = b + "&#39;" + e
+                oldloc = loc+1
+        if self.buttons[GS_PWD].GetValue():
+            pwd = self.texts["room_pwd"].GetValue()
+        else:
+            pwd = ""
+        if name == "":
+            wx.MessageBox("Invalid Name","Error");
+        else:
+            msg = "%s is creating room \'%s.\'" % (self.session.name, name)
+            self.session.send( msg )
+            self.session.send_create_group(name,pwd,boot_pwd,minversion)
+            self.set_lobbybutton(1); #enable the Lobby quickbutton
+        self.log.log("Exit game_server_panel->do_create_group(self)", ORPG_DEBUG)
+
+
+#---------------------------------------------------------
+# [START] Snowdog: Updated Game Server Window 12/02
+#---------------------------------------------------------
+
+    def on_size(self,evt):
+        # set column widths for room list
+
+
+        # set column widths for server list
+        pass
+
+
+
+#---------------------------------------------------------
+# [END] Snowdog: Updated Game Server Window 12/02
+#---------------------------------------------------------
+
+
+    def colorize_group_list(self, groups):
+        self.log.log("Enter game_server_panel->colorize_group_list(self, groups)", ORPG_DEBUG)
+
+        try:
+            hex = orpg.tools.rgbhex.RGBHex()
+#            activ = self.settings.get_setting("RoomColor_Active")
+#            lockt = self.settings.get_setting("RoomColor_Locked")
+#            empty = self.settings.get_setting("RoomColor_Empty")
+#            lobby = self.settings.get_setting("RoomColor_Lobby")
+#renamed colors - TaS sirebral
+
+            for gr in groups:
+                item_list_location = self.room_list.FindItemData(-1,int(gr[0]))
+                if item_list_location != -1:
+                    item = self.room_list.GetItem(item_list_location)
+                    if gr[0] == "0":
+#                        active_state = lobby
+			r,g,b = hex.rgb_tuple(self.settings.get_setting("RoomColor_Lobby"))
+                    elif gr[3] <> "0":
+#                        active_state = activ
+                        if gr[2] == "True" or gr[2] == "1":
+#                           active_state = lockt
+			   r,g,b = hex.rgb_tuple(self.settings.get_setting("RoomColor_Locked"))
+			else:
+#			   active_state = activ
+			   r,g,b = hex.rgb_tuple(self.settings.get_setting("RoomColor_Active"))
+                    else:
+#                        active_state = empty
+			r,g,b = hex.rgb_tuple(self.settings.get_setting("RoomColor_Empty"))
+                        
+#                    r,g,b = hex.rgb_tuple(active_state)
+		    color = wx.Colour(red=r,green=g,blue=b)
+                    item.SetTextColour(color)
+                    self.room_list.SetItem(item)
+        except:
+            traceback.print_exc()
+        self.log.log("Exit game_server_panel->colorize_group_list(self, groups)", ORPG_DEBUG)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/meta_server_lib.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,644 @@
+#!/usr/bin/python2.1
+# 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: meta_server_lib.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: meta_server_lib.py,v 1.40 2007/04/04 01:18:42 digitalxero Exp $
+#
+# Description: A collection of functions to communicate with the meta server.
+#
+
+
+#added debug flag for meta messages to cut console server spam --Snowdog
+META_DEBUG = 0
+
+__version__ = "$Id: meta_server_lib.py,v 1.40 2007/04/04 01:18:42 digitalxero Exp $"
+
+from orpg.orpg_version import PROTOCOL_VERSION
+from orpg.orpg_xml import *
+import orpg.dirpath
+import orpg.tools.validate
+import urllib
+import orpg.minidom
+from threading import *
+import time
+import sys
+import random
+import traceback
+import re
+
+metacache_lock = RLock()
+
+def get_server_dom(data=None,path=None):
+    # post data at server and get the resulting DOM
+
+    if path == None:
+        # get meta server URI
+        path = getMetaServerBaseURL()
+
+    # POST the data
+    if META_DEBUG:
+        print
+        print "Sending the following POST info to Meta at " + path + ":"
+        print "=========================================="
+        print data
+        print
+    file = urllib.urlopen(path, data)
+    data = file.read()
+    file.close()
+
+    # Remove any leading or trailing data.  This can happen on some satellite connections
+    p = re.compile('(<servers>.*?</servers>)',re.DOTALL|re.IGNORECASE)
+    mo = p.search(data)
+    if mo:
+        data = mo.group(0)
+
+    if META_DEBUG:
+        print
+        print "Got this string from the Meta at " + path + ":"
+        print "==============================================="
+        print data
+        print
+    # build dom
+    xml_dom = parseXml(data)
+    xml_dom = xml_dom._get_documentElement()
+    return xml_dom
+
+def post_server_data( name, realHostName=None):
+    # build POST data
+##    data = urllib.urlencode( {"server_data[name]":name,
+##                              "server_data[version]":PROTOCOL_VERSION,
+##                              "act":"new"} )
+##
+    if realHostName:
+        data = urllib.urlencode( {"server_data[name]":name,
+                                  "server_data[version]":PROTOCOL_VERSION,
+                                  "act":"new",
+                                  "REMOTE_ADDR": realHostName } )
+
+    else:
+        #print "Letting meta server decide the hostname to list..."
+        data = urllib.urlencode( {"server_data[name]":name,
+                                  "server_data[version]":PROTOCOL_VERSION,
+                                  "act":"new"} )
+
+    xml_dom = get_server_dom( data , "http://openrpg.sf.net/openrpg_servers.php")
+    ret_val = int( xml_dom.getAttribute( "id" ) )
+    return ret_val
+
+def post_failed_connection(id,meta=None,address=None,port=None):
+    #  For now, turning this off.  This needs to be re-vamped for
+    #  handling multiple Metas.
+    return 0
+#    data = urllib.urlencode({"id":id,"act":"failed"});
+#    xml_dom = get_server_dom(data)
+#    ret_val = int(xml_dom.getAttribute("return"))
+#    return ret_val
+
+def remove_server(id):
+    data = urllib.urlencode({"id":id,"act":"del"});
+    xml_dom = get_server_dom(data)
+    ret_val = int(xml_dom.getAttribute("return"))
+    return ret_val
+
+
+def byStartAttribute(first,second):
+    #  This function is used to easily sort a list of nodes
+    #  by their start time
+
+    if first.hasAttribute("start"):
+        first_start = int(first.getAttribute("start"))
+    else:
+        first_start = 0
+
+    if second.hasAttribute("start"):
+        second_start = int(second.getAttribute("start"))
+    else:
+        second_start = 0
+
+    # Return the result of the cmp function on the two strings
+    return cmp(first_start,second_start)
+
+def byNameAttribute(first,second):
+    #  This function is used to easily sort a list of nodes
+    #  by their name attribute
+
+    # Ensure there is something to sort with for each
+
+    if first.hasAttribute("name"):
+        first_name = str(first.getAttribute("name")).lower()
+    else:
+        first_name = ""
+
+    if second.hasAttribute("name"):
+        second_name = str(second.getAttribute("name")).lower()
+    else:
+        second_name = ""
+
+    # Return the result of the cmp function on the two strings
+
+    return cmp(first_name,second_name)
+
+
+def get_server_list(versions = None,sort_by="start"):
+    data = urllib.urlencode({"version":PROTOCOL_VERSION,"ports":"%"})
+    all_metas = getMetaServers(versions,1)  # get the list of metas
+    base_meta = getMetaServerBaseURL()
+
+    #all_metas.reverse()  # The last one checked will take precedence, so reverse the order
+                        #  so that the top one on the actual list is checked last
+
+    return_hash = {}  # this will end up with an amalgamated list of servers
+
+    for meta in all_metas:                  # check all of the metas
+
+        #  get the server's xml from the current meta
+        bad_meta = 0
+        #print "Getting server list from " + meta + "..."
+        try:
+            xml_dom = get_server_dom(data=data,path=meta)
+        except:
+            #print "Trouble getting servers from " + meta + "..."
+            bad_meta = 1
+
+        if bad_meta:
+            continue
+
+        if base_meta == meta:
+            #print "This is our base meta: " + meta
+            updateMetaCache(xml_dom)
+
+        node_list = xml_dom.getElementsByTagName('server')
+
+        if len(node_list):                  # if there are entries in the node list
+                                            #  otherwise, just loop to next meta
+
+            #  for each node found, we're going to check the nodes from prior
+            #  metas in the list.  If a match is found, then use the new values.
+            for n in node_list:
+
+                # set them from current node
+
+                if not n.hasAttribute('name'):
+                    n.setAttribute('name','NO_NAME_GIVEN')
+                name = n.getAttribute('name')
+                if not n.hasAttribute('num_users'):
+                    n.setAttribute('num_users','N/A')
+                num_users = n.getAttribute('num_users')
+                if not n.hasAttribute('address'):
+                    n.setAttribute('address','NO_ADDRESS_GIVEN')
+                address = n.getAttribute('address')
+                if not n.hasAttribute('port'):
+                    n.setAttribute('port','6774')
+                port = n.getAttribute('port')
+                n.setAttribute('meta',meta)
+                end_point = str(address) + ":" + str(port)
+                if return_hash.has_key(end_point):
+                    if META_DEBUG: print "Replacing duplicate server entry at " + end_point
+                return_hash[end_point] = n
+
+    #  At this point, we have an amalgamated list of servers
+    #  Now, we have to construct a new DOM to pass back.
+
+    #  Create a servers element
+    return_dom = orpg.minidom.Element("servers")
+
+    #  get the nodes stored in return_hash
+    return_list = return_hash.values()
+
+    #  sort them by their name attribute.  Uses byNameAttribute()
+    #  defined above as a comparison function
+
+    if sort_by == "start":
+        return_list.sort(byStartAttribute)
+    elif sort_by == "name":
+        return_list.sort(byNameAttribute)
+
+    #  Add each node to the DOM
+    for n in return_list:
+        return_dom.appendChild(n)
+    return return_dom
+
+## List Format:
+## <servers>
+## <server address=? id=? name=? failed_count=? >
+## </servers>
+
+def updateMetaCache(xml_dom):
+    try:
+        if META_DEBUG: print "Updating Meta Server Cache"
+        metaservers = xml_dom.getElementsByTagName( 'metaservers' )   # pull out the metaservers bit
+        if len(metaservers) == 0:
+            cmetalist = getRawMetaList()
+            xml_dom = get_server_dom(cmetalist[0])
+            metaservers = xml_dom.getElementsByTagName( 'metaservers' )
+        authoritative = metaservers[0].getAttribute('auth')
+        if META_DEBUG: print "  Authoritive Meta: "+str(authoritative)
+        metas = metaservers[0].getElementsByTagName("meta")                # get the list of metas
+        if META_DEBUG: print "  Meta List ("+str(len(metas))+" servers)"
+        try:
+            metacache_lock.acquire()
+            ini = open(orpg.dirpath.dir_struct["user"]+"metaservers.cache","w")
+            for meta in metas:
+                if META_DEBUG: print "   Writing: "+str(meta.getAttribute('path'))
+                ini.write(str(meta.getAttribute('path')) + " " + str(meta.getAttribute('versions')) + "\n")
+            ini.close()
+        finally:
+            metacache_lock.release()
+    except Exception, e:
+        if META_DEBUG: traceback.print_exc()
+        print "Meta Server Lib: UpdateMetaCache(): " + str(e)
+
+def getRawMetaList():
+    try:
+        try:
+            metacache_lock.acquire()
+            #  Read in the metas
+            orpg.tools.validate.Validate().config_file("metaservers.cache","metaservers.cache")
+            ini = open(orpg.dirpath.dir_struct["user"]+"metaservers.cache","r")
+            metas = ini.readlines()
+            ini.close()
+            return metas
+        finally:
+            metacache_lock.release()
+    except Exception, e:
+        if META_DEBUG: traceback.print_exc()
+        print "Meta Server Lib: getRawMetaList(): " + str(e)
+        return []
+
+def getMetaServers(versions = None, pick_random=0):
+    # get meta server URLs as a list
+
+    #  versions is a list of acceptable version numbers.
+    #    A False truth value will use getMetaServerBaseURL()
+
+    # set a default if we have weird reading problems
+    # default_url = "http://www.openrpg.com/openrpg_servers.php"
+
+    meta_names = []
+
+    if(versions):  #  If versions are supplied, then look in metaservers.conf
+        try:
+            #  read in the metas from file
+            #  format of file is one meta entry per line
+            #  each entry will be the meta url, followed by one or more version numbers that it
+            #  handle.  Generally, this will be either a 1 for the original Meta format, or
+            #  2 for the new one.
+
+            #  Read in the metas
+            metas = getRawMetaList()
+            #print str(metas)
+
+            # go through each one to check if it should be returned, based on the
+            #   version numbers allowed.
+            for meta in metas:
+
+                # split the line on whitespace
+                #   obviously, your meta servers urls shouldn't contain whitespace.  duh.
+                words = meta.split()
+
+                success = 0         #  init success flag for version check
+
+                for version in versions:    # run through each allowed version from caller
+                    if version in words[1:]:  #  if the allowed version token was found
+                        success += 1          #  then increment the success indicator
+
+                if success:          #  if the meta entry is acceptable to the caller
+                    meta_names.append(words[0])    #  add the entry
+                    if META_DEBUG: print "adding metaserver " + meta
+
+            #  at this point, we should have at least one name from the cache.  If not ...
+            if not meta_names:
+                default_meta = getMetaServerBaseURL()       # grab the meta from ini.xml
+                meta_names.append(default_meta)             # add it to the return list
+#                print "Warning!!\nNo valid metaservers cached."
+#                print "Using meta from MetaServerBaseURL: " + default_meta + "\n"
+            # if we have more than one and want a random one
+            elif pick_random:
+                if META_DEBUG: print "choosing random meta from: " + str(meta_names)
+                i = int(random.uniform(0,len(meta_names)))
+                #meta = meta_names[i]
+                meta_names = [meta_names[i]]
+                if META_DEBUG: print "using: " + str(meta_names)
+            else:
+                if META_DEBUG: print "using all metas: " + str(meta_names)
+            return meta_names
+        except Exception,e:
+            print e
+            #print "using default meta server URI: " + default_url
+            metas = []
+            #metas.append(default_url)
+            return metas   # return an empty list
+    else:        #  otherwise, use MetaServerBaseURL()
+        url = getMetaServerBaseURL()
+        meta_names.append(url)
+        return meta_names
+
+def getMetaServerBaseURL():
+    # get meta server URL
+    url = "http://www.openrpg.com/openrpg_servers.php"
+    try:
+        orpg.tools.validate.Validate().config_file("settings.xml","default_settings.xml")
+        ini = open(orpg.dirpath.dir_struct["user"]+"settings.xml","r")
+        txt = ini.read()
+        tree = parseXml(txt)._get_documentElement()
+        ini.close()
+        node_list = tree.getElementsByTagName("MetaServerBaseURL")
+        if node_list:
+            url = node_list[0].getAttribute("value")
+
+        # allow tree to be collected
+        try:
+            tree.unlink()
+        except:
+            pass
+
+    except Exception,e:
+        print e
+#    print "using meta server URI: " + url
+    return url
+
+#######################################################################################
+#  Beginning of Class registerThread
+#
+#  A Class to Manage Registration with the Meta2
+#  Create an instance and call it's start() method
+#  if you want to be (and stay) registered.  This class
+#  will take care of registering and re-registering as
+#  often as necessary to stay in the Meta list.
+#
+#  You may call register() yourself if you wish to change your
+#  server's name.  It will immediately update the Meta.  There
+#  is no need to unregister first.
+#
+#  Call unregister() when you no longer want to be registered.
+#  This will result in the registerThread dying after
+#  attempting to immediately remove itself from the Meta.
+#
+#  If you need to become registered again after that, you
+#  must create a new instance of class registerThread.  Don't
+#  just try to call register() on the old, dead thread class.
+
+
+class registerThread(Thread):
+#  Originally, I wrote this as a sub-class of wxThread, but
+#       A)  I couldn't get it to import right
+#       B)  I realized that I want this to be used in a server,
+#           which I don't want needing wxWindows to run!
+#
+#   Because of this fact, there are some methods from wxThread
+#   that I implemented to minimize changes to the code I had
+#   just written, i.e. TestDeleteStatus() and Delete()
+
+    def __init__(self,name=None,realHostName=None,num_users = "Hmmm",MetaPath=None,port=6774,register_callback=None):
+
+        Thread.__init__(self,name="registerThread")
+        self.rlock = RLock()                    #  Re-entrant lock used to make this class thread safe
+        self.die_event = Event()                #  The main loop in run() will wait with timeout on this
+        if name:
+            self.name = name                        #  Name that the server want's displayed on the Meta
+        else:
+            self.name = "Unnamed server"             #  But use this if for some crazy reason no name is
+                                                    #  passed to the constructor
+        self.num_users = num_users               #  the number of users currently on this server
+        self.realHostName = realHostName        #  Name to advertise for connection
+        self.id = "0"                           #  id returned from Meta.  Defaults to "0", which
+                                                #  indicates a new registration.
+        self.cookie = "0"                       #  cookie returned from Meta.  Defaults to "0",which
+                                                #  indicates a new registration.
+        self.interval = 0                       #  interval returned from Meta.  Is how often to
+                                                #  re-register, in minutes.
+        self.destroy = 0                        #  Used to flag that this thread should die
+        self.port = str(port)
+        self.register_callback = register_callback               # set a method to call to report result of register
+        #  This thread will communicate with one and only one
+        #  Meta.  If the Meta in ini.xml is changed after
+        #  instantiation, then this instance must be
+        #  unregistered and a new instance instantiated.
+        #
+        #  Also, if MetaPath is specified, then use that.  Makes
+        #  it easier to have multiple registerThreads going to keep the server registered
+        #  on multiple (compatible) Metas.
+
+        if MetaPath == None:
+            self.path = getMetaServerBaseURL()  #  Do this if no Meta specified
+        else:
+            self.path = MetaPath
+
+    def getIdAndCookie(self):
+        return self.id, self.cookie
+
+    def TestDeleteStatus(self):
+        try:
+            self.rlock.acquire()
+            return self.die_event.isSet()
+        finally:
+            self.rlock.release()
+
+    def Delete(self):
+        try:
+            self.rlock.acquire()
+            self.die_event.set()
+        finally:
+            self.rlock.release()
+
+    def run(self):
+    #  This method gets called by Thread implementation
+    #  when self.start() is called to begin the thread's
+    #  execution
+    #
+    #  We will basically enter a loop that continually
+    #  re-registers this server and sleeps Interval
+    #  minutes until the thread is ordered to die in place
+        while(not self.TestDeleteStatus()):         # Loop while until told to die
+            #  Otherwise, call thread safe register().
+            self.register(self.name, self.realHostName, self.num_users)
+            if META_DEBUG: print "Sent Registration Data"
+
+            #  register() will end up setting the state variables
+            #  for us, including self.interval.
+            try:
+                self.rlock.acquire()            #  Serialize access to this state information
+
+                if self.interval >= 3:          #  If the number of minutes is one or greater
+                    self.interval -= 1          #  wake up with 30 seconds left to re-register
+                else:
+                    self.interval = .5           #  Otherwise, we probably experienced some kind
+                                                #  of error from the Meta in register().  Sleep
+                                                #  for 6 seconds and start from scratch.
+
+            finally:                            # no matter what, release the lock
+                self.rlock.release()
+            #  Wait interval minutes for a command to die
+            die_signal = self.die_event.wait(self.interval*60)
+
+        #  If we get past the while loop, it's because we've been asked to die,
+        #  so just let run() end.  Once this occurs, the thread is dead and
+        #  calls to Thread.isAlive() return False.
+
+    def unregister(self):
+        #  This method can (I hope) be called from both within the thread
+        #  and from other threads.  It will attempt to unregister this
+        #  server from the Meta database
+        #  When this is either accomplished or has been tried hard enough
+        #  (after which it just makes sense to let the Meta remove the
+        #  entry itself when we don't re-register using this id),
+        #  this method will either cause the thread to immediately die
+        #  (if called from this thread's context) or set the Destroy flag
+        #  (if called from the main thread), a positive test for which will cause
+        #  the code in Entry() to exit() when the thread wakes up and
+        #  checks TestDeleteStatus().
+        #  lock the critical section.  The unlock will
+        #  automatically occur at the end of the function in the finally clause
+        try:
+            self.rlock.acquire()
+            if not self.isAlive():      #  check to see if this thread is dead
+                return 1                #  If so, return an error result
+            #  Do the actual unregistering here
+            data = urllib.urlencode( {"server_data[id]":self.id,
+                                        "server_data[cookie]":self.cookie,
+                                        "server_data[version]":PROTOCOL_VERSION,
+                                        "act":"unregister"} )
+            try:
+                xml_dom = get_server_dom(data=data, path=self.path)  #  this POSTS the request and returns the result
+                if xml_dom.hasAttribute("errmsg"):
+                    print "Error durring unregistration:  " + xml_dom.getAttribute("errmsg")
+            except:
+                if META_DEBUG: print "Problem talking to Meta.  Will go ahead and die, letting Meta remove us."
+            #  If there's an error, echo it to the console
+
+            #  No special handling is required.  If the de-registration worked we're done.  If
+            #  not, then it's because we've already been removed or have a bad cookie.  Either
+            #  way, we can't do anything else, so die.
+            self.Delete()            #  This will cause the registerThread to die in register()
+            #  prep xml_dom for garbage collection
+            try:
+                xml_dom.unlink()
+            except:
+                pass
+            return 0
+        finally:
+            self.rlock.release()
+
+    def register(self, name=None, realHostName=None, num_users=None):
+        #  Designed to handle the registration, both new and
+        #  repeated.
+        #
+        #  It is intended to be called once every interval
+        #    (or interval - delta) minutes.
+
+        #  lock the critical section.  The unlock will
+        #  automatically occur at the end of the function in the finally clause
+        try:
+            self.rlock.acquire()
+            if not self.isAlive():      #  check to see if this thread is dead
+                return 1                #  If so, return an error result
+
+            #  Set the server's attibutes, if specified.
+            if name:
+                self.name = name
+            if num_users != None:
+                self.num_users = num_users
+            if realHostName:
+                self.realHostName = realHostName
+            # build POST data
+            if self.realHostName:
+                data = urllib.urlencode( {"server_data[id]":self.id,
+                                        "server_data[cookie]":self.cookie,
+                                        "server_data[name]":self.name,
+                                        "server_data[port]":self.port,
+                                        "server_data[version]":PROTOCOL_VERSION,
+                                        "server_data[num_users]":self.num_users,
+                                        "act":"register",
+                                        "server_data[address]": self.realHostName } )
+            else:
+                if META_DEBUG:  print "Letting meta server decide the hostname to list..."
+                data = urllib.urlencode( {"server_data[id]":self.id,
+                                        "server_data[cookie]":self.cookie,
+                                        "server_data[name]":self.name,
+                                        "server_data[port]":self.port,
+                                        "server_data[version]":PROTOCOL_VERSION,
+                                        "server_data[num_users]":self.num_users,
+                                        "act":"register"} )
+            try:
+                xml_dom = get_server_dom(data=data,path=self.path)  #  this POSTS the request and returns the result
+            except:
+                if META_DEBUG: print "Problem talking to server.  Setting interval for retry ..."
+                if META_DEBUG: print data
+                if META_DEBUG: print
+                self.interval = 0
+                #  If we are in the registerThread thread, then setting interval to 0
+                #  will end up causing a retry in about 6 seconds (see self.run())
+                #  If we are in the main thread, then setting interval to 0 will do one
+                #  of two things:
+                #  1)  Do the same as if we were in the registerThread
+                #  2)  Cause the next, normally scheduled register() call to use the values
+                #      provided in this call.
+                #
+                #  Which case occurs depends on where the registerThread thread is when
+                #  the main thread calls register().
+                return 0  # indicates that it was okay to call, not that no errors occurred
+            #  If there is a DOM returned ....
+            if xml_dom:
+                #  If there's an error, echo it to the console
+                if xml_dom.hasAttribute("errmsg"):
+                    print "Error durring registration:  " + xml_dom.getAttribute("errmsg")
+                    if META_DEBUG: print data
+                    if META_DEBUG: print
+                #  No special handling is required.  If the registration worked, id, cookie, and interval
+                #  can be stored and used for the next time.
+                #  If an error occurred, then the Meta will delete us and we need to re-register as
+                #  a new server.  The way to indicate this is with a "0" id and "0" cookie sent to
+                #  the server during the next registration.  Since that's what the server returns to
+                #  us on an error anyway, we just store them and the next registration will
+                #  automatically be set up as a new one.
+                #
+                #  Unless the server calls register() itself in the meantime.  Of course, that's okay
+                #  too, because a success on THAT register() call will set up the next one to use
+                #  the issued id and cookie.
+                #
+                #  The interval is stored unconditionally for similar reasons.  If there's an error,
+                #  the interval will be less than 1, and the main thread's while loop will reset it
+                #  to 6 seconds for the next retry.
+                #  Is it wrong to have a method where there's more comments than code?  :)
+                try:
+                    self.interval = int(xml_dom.getAttribute("interval"))
+                    self.id = xml_dom.getAttribute("id")
+                    self.cookie = xml_dom.getAttribute("cookie")
+                    if not xml_dom.hasAttribute("errmsg"):
+                        updateMetaCache(xml_dom)
+                except:
+                    if META_DEBUG: print
+                    if META_DEBUG: print "OOPS!  Is the Meta okay?  It should be returning an id, cookie, and interval."
+                    if META_DEBUG: print "Check to see what it really returned.\n"
+                #  Let xml_dom get garbage collected
+                try:
+                    xml_dom.unlink()
+                except:
+                    pass
+            else:  #  else if no DOM is returned from get_server_dom()
+                print "Error - no DOM constructed from Meta message!"
+            return 0  # Let caller know it was okay to call us
+        finally:
+            self.rlock.release()
+    #  End of class registerThread
+    ################################################################################
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/mplay_client.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,915 @@
+# 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: mplay_client.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: mplay_client.py,v 1.71 2007/05/12 20:41:54 digitalxero Exp $
+#
+# Description: This file contains the code for the client stubs of the multiplayer
+# features in the orpg project.
+#
+
+__version__ = "$Id: mplay_client.py,v 1.71 2007/05/12 20:41:54 digitalxero Exp $"
+
+import orpg.minidom
+import socket
+import Queue
+import thread
+import traceback
+from threading import Event, Lock
+from xml.sax.saxutils import escape
+from struct import pack, unpack, calcsize
+from string import *
+from orpg.orpg_version import *
+import errno
+import os
+import time
+
+try:
+    import bz2
+    cmpBZ2 = True
+except:
+    cmpBZ2 = False
+
+try:
+    import zlib
+    cmpZLIB = True
+except:
+    cmpZLIB = False
+
+
+# This should be configurable
+OPENRPG_PORT = 9557
+
+# We should be sending a length for each packet
+MPLAY_LENSIZE = calcsize( 'i' )
+MPLAY_DISCONNECTED = 0
+MPLAY_CONNECTED = 1
+MPLAY_DISCONNECTING = 3
+MPLAY_GROUP_CHANGE = 4
+MPLAY_GROUP_CHANGE_F = 5
+PLAYER_NEW = 1
+PLAYER_DEL = 2
+PLAYER_GROUP = 3
+
+#  The next two messages are used to inform others that a player is typing
+PLAYER_TYPING = 4
+PLAYER_NOT_TYPING = 5
+PLAYER_UPDATE = 6
+GROUP_JOIN = 1
+GROUP_NEW = 2
+GROUP_DEL = 3
+GROUP_UPDATE = 4
+STATUS_SET_URL = 1
+
+def parseXml(data):
+    "parse and return doc"
+    #print data
+    doc = orpg.minidom.parseString(data)
+    doc.normalize()
+    return doc
+
+def myescape(data):
+    return escape(data,{"\"":""})
+
+class mplay_event:
+    def __init__(self,id,data=None):
+        self.id = id
+        self.data = data
+
+    def get_id(self):
+        return self.id
+
+    def get_data(self):
+        return self.data
+
+BOOT_MSG = "YoU ArE ThE WeAkEsT LiNk. GoOdByE."
+
+class client_base:
+
+    # Player role definitions
+    def __init__(self):
+        self.outbox = Queue.Queue(0)
+        self.inbox = Queue.Queue(0)
+        self.startedEvent = Event()
+        self.exitEvent = Event()
+        self.sendThreadExitEvent = Event()
+        self.recvThreadExitEvent = Event()
+        self.id = "0"
+        self.group_id = "0"
+        self.name = ""
+        self.role = "GM"
+        self.ROLE_GM = "GM"
+        self.ROLE_PLAYER = "PLAYER"
+        self.ROLE_LURKER = "LURKER"
+        self.ip = socket.gethostbyname(socket.gethostname())
+        self.remote_ip = None
+        self.version = VERSION
+        self.protocol_version = PROTOCOL_VERSION
+        self.client_string = CLIENT_STRING
+        self.status = MPLAY_DISCONNECTED
+        self.useCompression = False
+        self.compressionType = 'Undefined'
+        self.log_console = None
+        self.sock = None
+        self.text_status = "Idle"
+        self.statLock = Lock()
+        self.useroles = 0
+        self.ROLE_GM = "GM"
+        self.ROLE_PLAYER = "PLAYER"
+        self.ROLE_LURKER = "LURKER"
+        self.lastmessagetime = time.time()
+        self.connecttime = time.time()
+
+    def sendThread( self, arg ):
+        "Sending thread.  This thread reads from the data queue and writes to the socket."
+
+        # Wait to be told it's okay to start running
+        self.startedEvent.wait()
+
+        # Loop as long as we have a connection
+        while( self.get_status() == MPLAY_CONNECTED ):
+            try:
+                readMsg = self.outbox.get( block=1 )
+            except Exception, text:
+                self.log_msg( ("outbox.get() got an exception:  ", text) )
+
+            # If we are here, it's because we have data to send, no doubt!
+            if self.status == MPLAY_CONNECTED:
+                try:
+                    # Send the entire message, properly formated/encoded
+                    sent = self.sendMsg( self.sock, readMsg )
+                except Exception, e:
+                    self.log_msg( e )
+            else:
+                # If we are not connected, purge the data queue
+                self.log_msg( "Data queued without a connection, purging data from queue..." )
+        self.sendThreadExitEvent.set()
+        self.log_msg( "sendThread has terminated..." )
+
+    def recvThread( self, arg ):
+        "Receiving thread.  This thread reads from the socket and writes to the data queue."
+
+        # Wait to be told it's okay to start running
+        self.startedEvent.wait()
+
+        while( self.get_status() == MPLAY_CONNECTED ):
+            readMsg = self.recvMsg( self.sock )
+            try:
+                if self.useCompression and self.compressionType != None:
+                    readMsg = self.compressionType.decompress(readMsg)
+            except:
+                pass
+
+            # Check the length of the message
+            bytes = len( readMsg )
+
+            # Make sure we are still connected
+            if bytes == 0:
+                break
+            else:
+                # Pass along the message so it can be processed
+                self.inbox.put( readMsg )
+                self.update_idle_time() #update the last message time
+        if bytes == 0:
+            self.log_msg( "Remote has disconnected!" )
+            self.set_status( MPLAY_DISCONNECTING )
+        self.outbox.put( "" )    # Make sure the other thread is woken up!
+        self.sendThreadExitEvent.set()
+        self.log_msg( "recvThread has terminated..." )
+
+    def sendMsg( self, sock, msg ):
+        """Very simple function that will properly encode and send a message to te
+        remote on the specified socket."""
+
+        if self.useCompression and self.compressionType != None:
+            mpacket = self.compressionType.compress(msg)
+            lpacket = pack('!i', len(mpacket))
+            sock.send(lpacket)
+            offset = 0
+            while offset < len(mpacket):
+                slice = buffer(mpacket, offset, len(mpacket)-offset)
+                sent = sock.send(slice)
+                offset += sent
+            sentm = offset
+        else:
+            # Calculate our message length
+            length = len(msg)
+
+            # Encode the message length into network byte order
+            lp = pack('!i', length)
+
+            try:
+                # Send the encoded length
+                sentl = sock.send( lp )
+
+                # Now, send the message the the length was describing
+                sentm = sock.send( msg )
+                if self.isServer():
+                    self.log_msg(("data_sent", sentl+sentm))
+            except socket.error, e:
+                self.log_msg( e )
+            except Exception, e:
+                self.log_msg( e )
+        return sentm
+
+    def recvData( self, sock, readSize ):
+        """Simple socket receive method.  This method will only return when the exact
+        byte count has been read from the connection, if remote terminates our
+        connection or we get some other socket exception."""
+        data = ""
+        offset = 0
+        try:
+            while offset != readSize:
+                frag = sock.recv( readSize - offset )
+                # See if we've been disconnected
+                rs = len( frag )
+                if rs <= 0:
+                    # Loudly raise an exception because we've been disconnected!
+                    raise IOError, "Remote closed the connection!"
+                else:
+                    # Continue to build complete message
+                    offset += rs
+                    data += frag
+        except socket.error, e:
+            self.log_msg( e )
+            data = ""
+        return data
+
+    def recvMsg( self, sock ):
+        """This method now expects to receive a message having a 4-byte prefix length.  It will ONLY read
+        completed messages.  In the event that the remote's connection is terminated, it will throw an
+        exception which should allow for the caller to more gracefully handle this exception event.
+
+        Because we use strictly reading ONLY based on the length that is told to use, we no longer have to
+        worry about partially adjusting for fragmented buffers starting somewhere within a buffer that we've
+        read.  Rather, it will get ONLY a whole message and nothing more.  Everything else will remain buffered
+        with the OS until we attempt to read the next complete message."""
+
+        msgData = ""
+        try:
+            lenData = self.recvData( sock, MPLAY_LENSIZE )
+            # Now, convert to a usable form
+            (length,) = unpack('!i', lenData)
+            # Read exactly the remaining amount of data
+            msgData = self.recvData( sock, length )
+            if self.isServer():
+                self.log_msg(("data_recv", length+4))
+                # Make the peer IP address available for reference later
+                if self.remote_ip is None:
+                    self.remote_ip = self.sock.getpeername()
+        except IOError, e:
+            self.log_msg( e )
+        except Exception, e:
+            self.log_msg( e )
+        return msgData
+
+    def initialize_threads(self):
+        "Starts up our threads (2) and waits for them to make sure they are running!"
+        self.status = MPLAY_CONNECTED
+        self.sock.setblocking(1)
+        # Confirm that our threads have started
+        thread.start_new_thread( self.sendThread,(0,) )
+        thread.start_new_thread( self.recvThread,(0,) )
+        self.startedEvent.set()
+
+    def disconnect(self):
+        self.set_status(MPLAY_DISCONNECTING)
+        self.log_msg("client stub " + self.ip +" disconnecting...")
+        self.log_msg("closing sockets...")
+        try:
+            self.sock.shutdown( 2 )
+        except Exception, e:
+            print "Caught exception:  " + str(e)
+            print
+            print "Continuing"
+        self.set_status(MPLAY_DISCONNECTED)
+
+    def reset(self,sock):
+        self.disconnect()
+        self.sock = sock
+        self.initialize_threads()
+
+    def update_role(self,role):
+        self.useroles = 1
+        self.role = role
+
+    def use_roles(self):
+        if self.useroles:
+            return 1
+        else:
+            return 0
+    def update_self_from_player(self, player):
+        try:
+            (self.name, self.ip, self.id, self.text_status, self.version, self.protocol_version, self.client_string,role) = player
+        except Exception, e:
+            print e
+
+# The IP field should really be deprecated as too many systems are NAT'd and/or behind firewalls for a
+# client provided IP address to have much value.  As such, we now label it as deprecated.
+    def toxml(self,action):
+        xml_data = '<player name="' + myescape(self.name) + '"'
+        xml_data += ' action="' + action + '" id="' + self.id + '"'
+        xml_data += ' group_id="' + self.group_id + '" ip="' + self.ip + '"'
+        xml_data += ' status="' + self.text_status + '"'
+        xml_data += ' version="' + self.version + '"'
+        xml_data += ' protocol_version="' + self.protocol_version + '"'
+        xml_data += ' client_string="' + self.client_string + '"'
+        xml_data += ' useCompression="' + str(self.useCompression) + '"'
+        if cmpBZ2 and (self.compressionType == 'Undefined' or self.compressionType == bz2):
+            xml_data += ' cmpType="bz2"'
+        elif cmpZLIB and (self.compressionType == 'Undefined' or self.compressionType == zlib):
+            xml_data += ' cmpType="zlib"'
+        else:
+            xml_data += ' cmpType="None"'
+        xml_data += ' />'
+        return xml_data
+
+    def log_msg(self,msg):
+        if self.log_console:
+            self.log_console(msg)
+#        else:
+#            print "message", msg
+
+    def get_status(self):
+        self.statLock.acquire()
+        status = self.status
+        self.statLock.release()
+        return status
+
+    def my_role(self):
+#Why create the three different objects?  Why not just assign a value to self.role and use that? Prof_Ebral ponders.
+        if self.role == "GM":
+            return self.ROLE_GM
+        elif self.role == "Player":
+            return self.ROLE_PLAYER
+        elif self.role == "Lurker":
+            return self.ROLE_LURKER
+        return -1
+
+    def set_status(self,status):
+        self.statLock.acquire()
+        self.status = status
+        self.statLock.release()
+
+    def isServer( self ):
+        # Return 1 if we are running as a server, else, return 0.
+        # This method must be overloaded by whomever derives from us
+        pass
+
+    def __str__(self):
+        return "%s(%s)\nIP:%s\ngroup_id:%s\n" % (self.name, self.id, self.ip, self.group_id)
+
+    # idle time functions added by snowdog 3/31/04
+    def update_idle_time(self):
+        self.lastmessagetime = time.time()
+
+    def idle_time(self):
+        curtime = time.time()
+        idletime = curtime - self.lastmessagetime
+        return idletime
+
+    def idle_status(self):
+        idletime = self.idle_time()
+        idlemins = idletime / 60
+        status = "Unknown"
+        if idlemins < 3:
+            status = "Active"
+        elif idlemins < 10:
+            status = "Idle ("+str(int(idlemins))+" mins)"
+        else:
+            status = "Inactive ("+str(int(idlemins))+" mins)"
+        return status
+
+    def connected_time(self):
+        curtime = time.time()
+        timeoffset = curtime - self.connecttime
+        return timeoffset
+
+    def connected_time_string(self):
+        "returns the time client has been connected as a formated time string"
+        ct = self.connected_time()
+        d = int(ct/86400)
+        h = int( (ct-(86400*d))/3600 )
+        m = int( (ct-(86400*d)-(3600*h))/60)
+        s = int( (ct-(86400*d)-(3600*h)-(60*m)) )
+        cts =  zfill(d,2)+":"+zfill(h,2)+":"+zfill(m,2)+":"+zfill(s,2)
+        return cts
+
+#========================================================================
+#
+#
+#                           MPLAY CLIENT
+#
+#
+#========================================================================
+class mplay_client(client_base):
+    "mplay client"
+    def __init__(self,name,callbacks):
+        client_base.__init__(self)
+        self.set_name(name)
+        self.on_receive = callbacks['on_receive']
+        self.on_mplay_event = callbacks['on_mplay_event']
+        self.on_group_event = callbacks['on_group_event']
+        self.on_player_event = callbacks['on_player_event']
+        self.on_status_event = callbacks['on_status_event']
+        self.on_password_signal = callbacks['on_password_signal']
+        # I know this is a bad thing to do but it has to be
+        # be done to use the unified password manager.
+        # Should really find a better solution. -- SD 8/03
+        self.orpgFrame_callback = callbacks['orpgFrame']
+        self.settings = self.orpgFrame_callback.settings
+        #self.version = VERSION
+        #self.protocol_version = PROTOCOL_VERSION
+        #self.client_string = CLIENT_STRING
+        self.ignore_id = []
+        self.ignore_name = []
+        self.players = {}
+        self.groups = {}
+        self.unique_cookie = 0
+        self.msg_handlers = {}
+        self.core_msg_handlers = []
+        self.load_core_msg_handlers()
+
+    # implement from our base class
+    def isServer(self):
+        return 0
+
+    def get_chat(self):
+        return self.orpgFrame_callback.chat
+
+    def set_name(self,name):
+        self.name =  name
+        self.update()
+
+    def set_text_status(self, status):
+        if self.text_status != status:
+            self.text_status = status
+            self.update()
+
+    def set_status_url(self, url="None"):
+        self.on_status_event(mplay_event(STATUS_SET_URL,url))
+
+    def update(self, evt=None):
+        if self.status == MPLAY_CONNECTED:
+            self.outbox.put(self.toxml('update'))
+            self.inbox.put(self.toxml('update'))
+
+    def get_group_info(self, id=0):
+        self.statLock.acquire()
+        id = self.groups[id]
+        self.statLock.release()
+        return id
+
+    def get_my_group(self):
+        self.statLock.acquire()
+        id = self.groups[self.group_id]
+        self.statLock.release()
+        return id
+
+    def get_groups(self):
+        self.statLock.acquire()
+        groups = self.groups.values()
+        self.statLock.release()
+        return groups
+
+    def get_players(self):
+        self.statLock.acquire()
+        players = self.players.values()
+        self.statLock.release()
+        return players
+
+    def get_player_info(self,id):
+        self.statLock.acquire()
+        player = self.players[id]
+        self.statLock.release()
+        return player
+
+    def get_player_by_player_id(self,player):
+        players = self.get_players()
+        if self.players.has_key(player):
+            for m in players:
+                if player == m[2]:
+                    return m
+        return -1
+
+    def get_id(self):
+        return self.id
+
+    def get_my_info(self):
+        return (self.name, self.ip, self.id, self.text_status, self.version, self.protocol_version, self.client_string, self.role)
+
+    def is_valid_id(self,id):
+        self.statLock.acquire()
+        value = self.players.has_key( id )
+        self.statLock.release()
+        return value
+
+    def clear_players(self,save_self=0):
+        self.statLock.acquire()
+        keys = self.players.keys()
+        for k in keys:
+            del self.players[k]
+        self.statLock.release()
+
+    def clear_groups(self):
+        self.statLock.acquire()
+        keys = self.groups.keys()
+        for k in keys:
+            del self.groups[k]
+        self.statLock.release()
+
+    def find_role(self,id):
+        return self.players[id].role
+
+    def get_ignore_list(self):
+        try:
+            return (self.ignore_id, self.ignore_name)
+        except:
+            return (None, None)
+
+    def toggle_ignore(self, id):
+        for m in self.ignore_id:
+            if `self.ignore_id[self.ignore_id.index(m)]` ==  `id`:
+                name = self.ignore_name[self.ignore_id.index(m)]
+                self.ignore_id.remove(m)
+                self.ignore_name.remove(name)
+                return (0,id,name)
+        self.ignore_name.append(self.players[id][0])
+        self.ignore_id.append(self.players[id][2])
+        return (1,self.players[id][2],self.players[id][0])
+
+    def boot_player(self,id,boot_pwd = ""):
+        #self.send(BOOT_MSG,id)
+        msg = '<boot boot_pwd="' + boot_pwd + '"/>'
+        self.send(msg,id)
+
+#---------------------------------------------------------
+# [START] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+
+    def set_room_pass(self,npwd,pwd=""):
+        self.outbox.put("<alter key=\"pwd\" val=\"" +npwd+ "\" bpw=\"" + pwd + "\" plr=\"" + self.id +"\" gid=\"" + self.group_id + "\" />")
+        self.update()
+
+    def set_room_name(self,name,pwd=""):
+        loc = name.find("&")
+        oldloc=0
+        while loc > -1:
+            loc = name.find("&",oldloc)
+            if loc > -1:
+                b = name[:loc]
+                e = name[loc+1:]
+                name = b + "&amp;" + e
+                oldloc = loc+1
+        loc = name.find('"')
+        oldloc=0
+        while loc > -1:
+            loc = name.find('"',oldloc)
+            if loc > -1:
+                b = name[:loc]
+                e = name[loc+1:]
+                name = b + "&quot;" + e
+                oldloc = loc+1
+        loc = name.find("'")
+        oldloc=0
+        while loc > -1:
+            loc = name.find("'",oldloc)
+            if loc > -1:
+                b = name[:loc]
+                e = name[loc+1:]
+                name = b + "&#39;" + e
+                oldloc = loc+1
+        self.outbox.put("<alter key=\"name\" val=\"" + name + "\" bpw=\"" + pwd + "\" plr=\"" + self.id +"\" gid=\"" + self.group_id + "\" />")
+        self.update()
+
+#---------------------------------------------------------
+# [END] Snowdog Password/Room Name altering code  12/02
+#---------------------------------------------------------
+
+    def display_roles(self):
+        self.outbox.put("<role action=\"display\" player=\"" + self.id +"\" group_id=\""+self.group_id + "\" />")
+
+    def get_role(self):
+        self.outbox.put("<role action=\"get\" player=\"" + self.id +"\" group_id=\""+self.group_id + "\" />")
+
+    def set_role(self,player,role,pwd=""):
+        self.outbox.put("<role action=\"set\" player=\"" + player + "\" role=\"" +role+ "\" boot_pwd=\"" + pwd + "\" group_id=\"" + self.group_id + "\" />")
+        self.update()
+
+    def send(self,msg,player="all"):
+        if self.status == MPLAY_CONNECTED and player != self.id:
+            self.outbox.put("<msg to='"+player+"' from='"+self.id+"' group_id='"+self.group_id+"' />"+msg)
+        self.check_my_status()
+
+    def send_sound(self, snd_xml):
+        if self.status == MPLAY_CONNECTED:
+            self.outbox.put(snd_xml)
+        self.check_my_status()
+
+    def send_create_group(self,name,pwd,boot_pwd,minversion):
+        self.outbox.put("<create_group from=\""+self.id+"\" pwd=\""+pwd+"\" name=\""+
+                        name+"\" boot_pwd=\""+boot_pwd+"\" min_version=\"" + minversion +"\" />")
+
+    def send_join_group(self,group_id,pwd):
+        if (group_id != 0):
+            self.update_role("LURKER")
+        self.outbox.put("<join_group from=\""+self.id+"\" pwd=\""+pwd+"\" group_id=\""+str(group_id)+"\" />")
+
+    def poll(self, evt=None):
+        try:
+            msg = self.inbox.get_nowait()
+        except:
+            if self.get_status() != MPLAY_CONNECTED:
+                self.check_my_status()
+        else:
+            try:
+                self.pretranslate(msg)
+            except Exception, e:
+                print "The following  message: " + str(msg)
+                print "created the following exception: "
+                traceback.print_exc()
+
+    def add_msg_handler(self, tag, function, core=False):
+        if not self.msg_handlers.has_key(tag):
+            self.msg_handlers[tag] = function
+            if core:
+                self.core_msg_handlers.append(tag)
+        else:
+            print 'XML Messages ' + tag + ' already has a handler'
+
+    def remove_msg_handler(self, tag):
+        if self.msg_handlers.has_key(tag) and not tag in self.core_msg_handlers:
+            del self.msg_handlers[tag]
+        else:
+            print 'XML Messages ' + tag + ' already deleted'
+
+    def load_core_msg_handlers(self):
+        self.add_msg_handler('msg', self.on_msg, True)
+        self.add_msg_handler('ping', self.on_ping, True)
+        self.add_msg_handler('group', self.on_group, True)
+        self.add_msg_handler('role', self.on_role, True)
+        self.add_msg_handler('player', self.on_player, True)
+        self.add_msg_handler('password', self.on_password, True)
+        self.add_msg_handler('sound', self.on_sound, True)
+
+    def pretranslate(self,data):
+        # Pre-qualify our data.  If we don't have atleast 5-bytes, then there is
+        # no way we even have a valid message!
+        if len(data) < 5:
+            return
+        end = data.find(">")
+        head = data[:end+1]
+        msg = data[end+1:]
+        xml_dom = parseXml(head)
+        xml_dom = xml_dom._get_documentElement()
+        tag_name = xml_dom._get_tagName()
+        id = xml_dom.getAttribute("from")
+        if id == '':
+            id = xml_dom.getAttribute("id")
+        if self.msg_handlers.has_key(tag_name):
+            self.msg_handlers[tag_name](id, data, xml_dom)
+        else:
+            #Unknown messages recived ignoring
+            #using pass insted or printing an error message
+            #because plugins should now be able to send and proccess messages
+            #if someone is using a plugin to send messages and this user does not
+            #have the plugin they would be getting errors
+            pass
+        if xml_dom:
+            xml_dom.unlink()
+
+    def on_sound(self, id, data, xml_dom):
+        (ignore_id,ignore_name) = self.get_ignore_list()
+        for m in ignore_id:
+            if m == id:
+                # yes we are
+                print "ignoring sound from player:"
+                return
+        chat = self.get_chat()
+        snd = xml_dom.getAttribute("url")
+        loop_sound = xml_dom.getAttribute("loop")
+        chat.sound_player.play(snd, "remote", loop_sound)
+
+    def on_msg(self, id, data, xml_dom):
+        end = data.find(">")
+        head = data[:end+1]
+        msg = data[end+1:]
+        if id == "0":
+            self.on_receive(msg,None)      #  None get's interpreted in on_receive as the sys admin.
+                                           #  Doing it this way makes it harder to impersonate the admin
+        else:
+            if self.is_valid_id(id):
+                self.on_receive(msg,self.players[id])
+        if xml_dom:
+            xml_dom.unlink()
+
+    def on_ping(self, id, msg, xml_dom):
+        #a REAL ping time implementation by Snowdog 8/03
+        # recieves special server <ping time="###" /> command
+        # where ### is a returning time from the clients ping command
+        #get current time, pull old time from object and compare them
+        # the difference is the latency between server and client * 2
+        ct = time.clock()
+        ot = xml_dom.getAttribute("time")
+        latency = float(float(ct) - float(ot))
+        latency = int( latency * 10000.0 )
+        latency = float( latency) / 10.0
+        ping_msg = "Ping Results: " + str(latency) + " ms (parsed message, round trip)"
+        self.on_receive(ping_msg,None)
+        if xml_dom:
+            xml_dom.unlink()
+
+    def on_group(self, id, msg, xml_dom):
+        name = xml_dom.getAttribute("name")
+        players = xml_dom.getAttribute("players")
+        act = xml_dom.getAttribute("action")
+        pwd = xml_dom.getAttribute("pwd")
+        group_data = (id, name, pwd, players)
+
+        if act == 'new':
+            self.groups[id] = group_data
+            self.on_group_event(mplay_event(GROUP_NEW, group_data))
+        elif act == 'del':
+            del self.groups[id]
+            self.on_group_event(mplay_event(GROUP_DEL, group_data))
+        elif act == 'update':
+            self.groups[id] = group_data
+            self.on_group_event(mplay_event(GROUP_UPDATE, group_data))
+        if xml_dom:
+            xml_dom.unlink()
+
+    def on_role(self, id, msg, xml_dom):
+        act = xml_dom.getAttribute("action")
+        role = xml_dom.getAttribute("role")
+        if (act == "set") or (act == "update"):
+            try:
+                (a,b,c,d,e,f,g,h) = self.players[id]
+                if id == self.id:
+                    self.players[id] = (a,b,c,d,e,f,g,role)
+                    self.update_role(role)
+                else:
+                    self.players[id] = (a,b,c,d,e,f,g,role)
+                self.on_player_event(mplay_event(PLAYER_UPDATE,self.players[id]))
+            except:
+                pass
+        if xml_dom:
+            xml_dom.unlink()
+
+    def on_player(self, id, msg, xml_dom):
+        act = xml_dom.getAttribute("action")
+        ip = xml_dom.getAttribute("ip")
+        name = xml_dom.getAttribute("name")
+        status = xml_dom.getAttribute("status")
+        version = xml_dom.getAttribute("version")
+        protocol_version = xml_dom.getAttribute("protocol_version")
+        client_string = xml_dom.getAttribute("client_string")
+        try:
+            player = (name,ip,id,status,version,protocol_version,client_string,self.players[id][7])
+        except Exception, e:
+            player = (name,ip,id,status,version,protocol_version,client_string,"Player")
+        if act == "new":
+            self.players[id] = player
+            self.on_player_event(mplay_event(PLAYER_NEW,self.players[id]))
+        elif act == "group":
+            self.group_id = xml_dom.getAttribute("group_id")
+            self.clear_players()
+            self.on_mplay_event(mplay_event(MPLAY_GROUP_CHANGE,self.groups[self.group_id]))
+            self.players[self.id] = self.get_my_info() #(self.name,self.ip,self.id,self.text_status)
+            self.on_player_event(mplay_event(PLAYER_NEW,self.players[self.id]))
+        elif act == "failed":
+            self.on_mplay_event(mplay_event(MPLAY_GROUP_CHANGE_F))
+        elif act == "del":
+            self.on_player_event(mplay_event(PLAYER_DEL,self.players[id]))
+            if self.players.has_key(id):
+                del self.players[id]
+            if id == self.id:
+                self.do_disconnect()
+        #  the next two cases handle the events that are used to let you know when others are typing
+        elif act == "update":
+            if id == self.id:
+                self.players[id] = player
+                self.update_self_from_player(player)
+            else:
+                self.players[id] = player
+            dont_send = 0
+            for m in self.ignore_id:
+                if m == id:
+                    dont_send=1
+            if dont_send != 1:
+                self.on_player_event(mplay_event(PLAYER_UPDATE,self.players[id]))
+        if xml_dom:
+            xml_dom.unlink()
+
+    def on_password(self, id, msg, xml_dom):
+        signal = type = id = data = None
+        id = xml_dom.getAttribute("id")
+        type = xml_dom.getAttribute("type")
+        signal = xml_dom.getAttribute("signal")
+        data = xml_dom.getAttribute("data")
+        self.on_password_signal( signal,type,id,data )
+        if xml_dom:
+            xml_dom.unlink()
+
+    def check_my_status(self):
+        status = self.get_status()
+        if status == MPLAY_DISCONNECTING:
+            self.do_disconnect()
+
+    def connect(self, addressport):
+        """Establish a connection to a server while still using sendThread & recvThread for its
+        communication."""
+        if self.is_connected():
+            self.log_msg( "Client is already connected to a server?!?  Need to disconnect first." )
+            return 0
+        xml_dom = None
+        self.inbox = Queue.Queue(0)
+        self.outbox = Queue.Queue(0)
+        addressport_ar = addressport.split(":")
+        if len(addressport_ar) == 1:
+            address = addressport_ar[0]
+            port = OPENRPG_PORT
+        else:
+            address = addressport_ar[0]
+            port = int(addressport_ar[1])
+        self.host_server = addressport
+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            self.sock.connect((address,port))
+            # send client into with id=0
+            self.sendMsg( self.sock, self.toxml("new") )
+            data = self.recvMsg( self.sock )
+            # get new id and group_id
+            xml_dom = parseXml(data)
+            xml_dom = xml_dom._get_documentElement()
+            self.id = xml_dom.getAttribute("id")
+            self.group_id = xml_dom.getAttribute("group_id")
+            if xml_dom.hasAttribute('useCompression') and xml_dom.getAttribute('useCompression') == 'True':
+                self.useCompression = True
+                if xml_dom.hasAttribute('cmpType'):
+                    if cmpBZ2 and xml_dom.getAttribute('cmpType') == 'bz2':
+                        self.compressionType = bz2
+                    elif cmpZLIB and xml_dom.getAttribute('cmpType') == 'zlib':
+                        self.compressionType = zlib
+                    else:
+                        self.compressionType = None
+                else:
+                    self.compressionType = bz2
+            #send confirmation
+            self.sendMsg( self.sock, self.toxml("new") )
+        except Exception, e:
+            self.log_msg(e)
+            if xml_dom:
+                xml_dom.unlink()
+            return 0
+
+        # Start things rollings along
+        self.initialize_threads()
+        self.on_mplay_event(mplay_event(MPLAY_CONNECTED))
+        self.players[self.id] = (self.name,self.ip,self.id,self.text_status,self.version,self.protocol_version,self.client_string,self.role)
+        self.on_player_event(mplay_event(PLAYER_NEW,self.players[self.id]))
+        if xml_dom:
+            xml_dom.unlink()
+        return 1
+
+    def start_disconnect(self):
+        self.on_mplay_event(mplay_event(MPLAY_DISCONNECTING))
+        self.outbox.put( self.toxml("del") )
+            ## Client Side Disconect Forced -- Snowdog 10-09-2003
+        #pause to allow GUI events time to sync.
+        time.sleep(1)
+        self.do_disconnect()
+
+    def do_disconnect(self):
+        client_base.disconnect(self)
+        self.clear_players()
+        self.clear_groups()
+        self.useroles = 0
+        self.on_mplay_event(mplay_event(MPLAY_DISCONNECTED))
+        self.useCompression = False
+
+    def is_connected(self):
+        return (self.status == MPLAY_CONNECTED)
+
+    def get_next_id(self):
+        self.unique_cookie += 1
+        return_str = self.id + "-" + str(self.unique_cookie)
+        return return_str
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/mplay_groups.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,81 @@
+from orpg.mapper.map_msg import *
+
+class game_group:
+    def __init__( self, id, name, pwd, desc="", boot_pwd="", minVersion="", mapFile=None, messageFile=None, persist =0 ):
+        self.id = id
+        self.name = name
+        self.desc = desc
+        self.minVersion = minVersion
+        self.messageFile = messageFile
+        self.players = []
+        self.pwd = pwd
+        self.boot_pwd = boot_pwd
+        self.game_map = map_msg()
+        self.lock = Lock()
+        self.moderated = 0
+        self.voice = {}
+        self.persistant = persist
+
+        if mapFile != None:
+            f = open( mapFile )
+            tree = f.read()
+            f.close()
+
+        else:
+            f = open(orpg.dirpath.dir_struct["template"] + "default_map.xml")
+            tree = f.read()
+            f.close()
+
+        self.game_map.init_from_xml(tree)
+
+
+    def add_player(self,id):
+        self.players.append(id)
+
+    def remove_player(self,id):
+        if self.voice.has_key(id):
+            del self.voice[id]
+        self.players.remove(id)
+
+    def get_num_players(self):
+        num =  len(self.players)
+        return num
+
+    def get_player_ids(self):
+        tmp = self.players
+        return tmp
+
+
+    def check_pwd(self,pwd):
+        return (pwd==self.pwd)
+
+    def check_boot_pwd(self,pwd):
+        return (pwd==self.boot_pwd)
+
+    def check_version(self,ver):
+        if (self.minVersion == ""):
+            return 1
+        minVersion=self.minVersion.split('.')
+        version=ver.split('.')
+        for i in range(min(len(minVersion),len(version))):
+            w=max(len(minVersion[i]),len(version[i]))
+            v1=minVersion[i].rjust(w);
+            v2=version[i].rjust(w);
+            if v1<v2:
+                return 1
+            if v1>v2:
+                return 0
+
+        if len(minVersion)>len(version):
+            return 0
+        return 1
+
+    #depreciated - see send_group_list()
+    def toxml(self,act="new"):
+        #  Please don't add the boot_pwd to the xml, as this will give it away to players watching their console
+        xml_data = "<group id=\"" + self.id
+        xml_data += "\" name=\"" + self.name
+        xml_data += "\" pwd=\"" + str(self.pwd!="")
+        xml_data += "\" players=\"" + str(self.get_num_players())
+        xml_data += "\" action=\"" + act + "\" />"
+        return xml_data
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/mplay_messaging.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,475 @@
+# 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: mplay_messaging.py
+# Author: Dj Gilcrease
+# Maintainer:
+# Version:
+#   $Id: mplay_messaging.py,v 1.5 2007/05/06 16:42:59 digitalxero Exp $
+#
+# Description: This file contains the code for the client / server messaging
+#
+
+__version__ = "$Id: mplay_messaging.py,v 1.5 2007/05/06 16:42:59 digitalxero Exp $"
+
+import socket
+import Queue
+import thread
+import traceback
+from threading import Event, Lock
+from xml.sax.saxutils import escape
+from struct import pack, unpack, calcsize
+from string import *
+from orpg.orpg_version import *
+import os
+import time
+
+from orpg.orpgCore import *
+
+def myescape(data):
+    return escape(data,{"\"":""})
+
+class messenger:
+    def __init__(self, *args, **kwargs):
+        self.log = open_rpg.get_component("log")
+        self.xml = open_rpg.get_component("xml")
+        self.dir_struct = open_rpg.get_component("dir_struct")
+        self.validate = open_rpg.get_component("validate")
+        self.settings = open_rpg.get_component("settings")
+        if kwargs.has_key('isServer'):
+            self.isServer = kwargs['isServer']
+        else:
+            self.isServer = False
+        self.listen_event = Event()
+        self.outbox = Queue.Queue(0)
+        self.inbox_event = Event()
+        self.inbox = Queue.Queue(0)
+        self.startedEvent = Event()
+        self.exitEvent = Event()
+        self.sendThreadExitEvent = Event()
+        self.recvThreadExitEvent = Event()
+        self.port = int(self.settings.get_setting("port"))
+        self.ip = socket.gethostbyname(socket.gethostname())
+        self.lensize = calcsize('i')
+        self.mplay_type = ('disconnected', 'connected', 'disconnecting', 'group change', 'group change failed')
+        self.status = self.mplay_type[0]
+        self.alive = False
+        self.sock = None
+        self.version = VERSION
+        self.protocol_version = PROTOCOL_VERSION
+        self.client_string = CLIENT_STRING
+        self.minClientVersion = SERVER_MIN_CLIENT_VERSION
+        self.id = "0"
+        self.group_id = "0"
+        self.name = ""
+        self.role = "GM"
+        self.ROLE_GM = "GM"
+        self.ROLE_PLAYER = "PLAYER"
+        self.ROLE_LURKER = "LURKER"
+        self.text_status = "Idle"
+        self.statLock = Lock()
+        self.useroles = 0
+        self.lastmessagetime = time.time()
+        self.connecttime = time.time()
+        self.timeout_time = None
+        self.ignorelist = {}
+        self.players = {}
+        self.groups = {}
+
+        #Setup Stuff from the Server
+        if kwargs.has_key('inbox'):
+            self.inbox = kwargs['inbox']
+        if kwargs.has_key('sock'):
+            self.sock = kwargs['sock']
+        if kwargs.has_key('ip'):
+            self.ip = kwargs['ip']
+        if kwargs.has_key('role'):
+            self.role = kwargs['role']
+        if kwargs.has_key('id'):
+            self.id = kwargs['id']
+        if kwargs.has_key('group_id'):
+            self.group_id = kwargs['group_id']
+        if kwargs.has_key('name'):
+            self.name = kwargs['name']
+        if kwargs.has_key('version'):
+            self.version = kwargs['version']
+        if kwargs.has_key('protocol_version'):
+            self.protocol_version = kwargs['protocol_version']
+        if kwargs.has_key('client_string'):
+            self.client_string = kwargs['client_string']
+
+    def build_message(self, *args, **kwargs):
+        #print args
+        message = '<' + args[0]
+
+        #Setup the attributes of the message
+        if len(kwargs) > 0:
+            for attrib in kwargs.keys():
+                message += ' ' + attrib + '="' + str(kwargs[attrib]) + '"'
+
+        #Add the actual message if there is one
+        if len(args) > 1:
+            #Close the first part
+            message += '>'
+            message += escape(args[1])
+
+            #Close the whole thing
+            message += '</' + args[0] + '>'
+        else:
+            message += ' />'
+        return message
+
+    def disconnect(self):
+        self.set_status(2)
+        self.log.log("client stub " + self.ip +" disconnecting...", ORPG_DEBUG)
+        self.log.log("closing sockets...", ORPG_DEBUG)
+        try:
+            self.sock.shutdown( 2 )
+        except:
+            self.log.log("Caught exception:\n" + traceback.format_exc(), ORPG_GENERAL)
+        self.set_status(0)
+
+    def reset(self, sock):
+        self.disconnect()
+        self.sock = sock
+        self.initialize_threads()
+
+    def update_role(self, role):
+        self.useroles = 1
+        self.role = role
+
+    def use_roles(self):
+        return self.useroles
+
+    def update_self_from_player(self, player):
+        try:
+            (self.name, self.ip, self.id, self.text_status, self.version, self.protocol_version, self.client_string,role) = player
+        except:
+            self.log.log("Exception:  messenger->update_self_from_player():\n" + traceback.format_exc(), ORPG_GENERAL)
+
+    def toxml(self, act):
+        self.log.log("DEPRECIATED! messenger->toxml()", ORPG_CRITICAL)
+        xml_data = self.build_message('player',
+                                name=myescape(self.name),
+                                action=act,
+                                id=self.id,
+                                group_id=self.group_id,
+                                ip=self.ip,
+                                status=self.text_status,
+                                version=self.version,
+                                protocol_version=self.protocol_version,
+                                client_string=self.client_string
+                                )
+        return xml_data
+
+    def get_status(self):
+        self.statLock.acquire()
+        status = self.status
+        self.statLock.release()
+        return status
+
+    def my_role(self):
+        return self.role
+
+    def set_status(self, status):
+        self.statLock.acquire()
+        self.status = status
+        self.statLock.release()
+
+    def initialize_threads(self):
+        "Starts up our threads (2) and waits for them to make sure they are running!"
+        self.status = 'connected'
+        self.sock.setblocking(1)
+
+        # Confirm that our threads have started
+        thread.start_new_thread( self.sendThread,(0,) )
+        thread.start_new_thread( self.recvThread,(0,) )
+        self.startedEvent.set()
+
+    def __str__(self):
+        return "%s(%s)\nIP:%s\ngroup_id:%s\n%s (%s)" % (self.name, self.id, self.ip, self.group_id, self.idle_time(), self.connected_time())
+
+    # idle time functions added by snowdog 3/31/04
+    def update_idle_time(self):
+        self.lastmessagetime = time.time()
+
+    def idle_time(self):
+        curtime = time.time()
+        idletime = curtime - self.lastmessagetime
+        return idletime
+
+    def idle_status(self):
+        idletime = self.idle_time()
+        idlemins = idletime / 60
+        status = "Unknown"
+        if idlemins < 3:
+            status = "Active"
+        elif idlemins < 10:
+            status = "Idle ("+str(int(idlemins))+" mins)"
+        else:
+            status = "Inactive ("+str(int(idlemins))+" mins)"
+        return status
+
+    def connected_time(self):
+        curtime = time.time()
+        timeoffset = curtime - self.connecttime
+        return timeoffset
+
+    def connected_time_string(self):
+        "returns the time client has been connected as a formated time string"
+        ct = self.connected_time()
+        d = int(ct/86400)
+        h = int( (ct-(86400*d))/3600 )
+        m = int( (ct-(86400*d)-(3600*h))/60)
+        s = int( (ct-(86400*d)-(3600*h)-(60*m)) )
+        cts =  zfill(d,2)+":"+zfill(h,2)+":"+zfill(m,2)+":"+zfill(s,2)
+        return cts
+
+    def clear_timeout(self):
+        self.timeout_time = None
+
+    def check_time_out(self):
+        if self.timeout_time==None:
+            self.timeout_time = time.time()
+        curtime = time.time()
+        diff = curtime - self.timeout_time
+        if diff > 1800:
+            return 1
+        else:
+            return 0
+
+    def send(self, msg):
+        if self.get_status() == 'connected':
+            self.outbox.put(msg)
+
+    def change_group(self, group_id, groups):
+        old_group_id = str(self.group_id)
+        groups[group_id].add_player(self.id)
+        groups[old_group_id].remove_player(self.id)
+        self.group_id = group_id
+        self.outbox.put(self.toxml('group'))
+        msg = groups[group_id].game_map.get_all_xml()
+        self.send(msg)
+        return old_group_id
+
+    def take_dom(self, xml_dom):
+        self.name = xml_dom.getAttribute("name")
+        self.text_status = xml_dom.getAttribute("status")
+
+    def add_msg_handler(self, tag, function, core=False):
+        if not self.msg_handlers.has_key(tag):
+            self.msg_handlers[tag] = function
+            if core:
+                self.core_msg_handlers.append(tag)
+        else:
+            print 'XML Messages ' + tag + ' already has a handler'
+
+    def remove_msg_handler(self, tag):
+        if self.msg_handlers.has_key(tag) and not tag in self.core_msg_handlers:
+            del self.msg_handlers[tag]
+        else:
+            print 'XML Messages ' + tag + ' already deleted'
+
+
+    #Message Handaling
+    def message_handler(self, arg):
+        xml_dom = None
+        self.log.log("message handler thread running...", ORPG_NOTE)
+        while self.alive or self.status == 'connected':
+            data = None
+            try:
+                data = self.inbox.get(0)
+            except Queue.Empty:
+                time.sleep(0.25) #sleep 1/4 second
+                continue
+            bytes = len(data)
+            if bytes < 5:
+                continue
+            try:
+                thread.start_new_thread(self.parse_incoming_dom,(str(data),))
+                #data has been passed... unlink from the variable references
+                #so data in passed objects doesn't change (python passes by reference)
+                del data
+                data = None
+            except Exception, e:
+                self.log.log(traceback.format_exc(), ORPG_GENERAL)
+                if xml_dom: xml_dom.unlink()
+        if xml_dom: xml_dom.unlink()
+        self.log.log("message handler thread exiting...", ORPG_NOTE)
+        self.inbox_event.set()
+
+    def parse_incoming_dom(self, data):
+        #print data
+        xml_dom = None
+        try:
+            xml_dom = self.xml.parseXml(data)
+            xml_dom = xml_dom._get_documentElement()
+            self.message_action(xml_dom, data)
+
+        except Exception, e:
+            self.log.log("Error in parse of inbound message. Ignoring message.", ORPG_GENERAL)
+            self.log.log("\tOffending data(" + str(len(data)) + "bytes)=" + data, ORPG_GENERAL)
+            self.log.log("Exception=" + traceback.format_exc(), ORPG_GENERAL)
+        if xml_dom: xml_dom.unlink()
+
+    def message_action(self, xml_dom, data):
+        tag_name = xml_dom._get_tagName()
+        if self.msg_handlers.has_key(tag_name):
+            self.msg_handlers[tag_name](xml_dom, data)
+        else:
+            self.log.log("Unknown Message Type", ORPG_GENERAL)
+            self.log.log(data, ORPG_GENERAL)
+        #Message Action thread expires and closes here.
+        return
+
+    #Privet functions
+    def sendThread( self, arg ):
+        "Sending thread.  This thread reads from the data queue and writes to the socket."
+        # Wait to be told it's okay to start running
+        self.startedEvent.wait()
+
+        # Loop as long as we have a connection
+        while( self.get_status() == 'connected' ):
+            try:
+                readMsg = self.outbox.get( block=1 )
+
+            except Exception, text:
+                self.log.log("Exception:  messenger->sendThread():  " + str(text), ORPG_CRITICAL)
+
+            # If we are here, it's because we have data to send, no doubt!
+            if self.status == 'connected':
+                try:
+                    # Send the entire message, properly formated/encoded
+                    sent = self.sendMsg( self.sock, readMsg )
+                except:
+                    self.log.log("Exception:  messenger->sendThread():\n" + traceback.format_exc(), ORPG_CRITICAL)
+            else:
+                # If we are not connected, purge the data queue
+                self.log.log("Data queued without a connection, purging data from queue...", ORPG_NOTE)
+        self.sendThreadExitEvent.set()
+        self.log.log( "sendThread has terminated...", ORPG_NOTE)
+
+    def sendMsg( self, sock, msg ):
+        """Very simple function that will properly encode and send a message to te
+        remote on the specified socket."""
+
+        # Calculate our message length
+        length = len( msg )
+
+        # Encode the message length into network byte order
+        lp = pack( 'i', socket.htonl( length ) )
+
+        try:
+            # Send the encoded length
+            sentl = sock.send( lp )
+
+            # Now, send the message the the length was describing
+            sentm = sock.send( msg )
+            if self.isServer:
+                self.log.log("('data_sent', " + str(sentl+sentm) + ")", ORPG_DEBUG)
+            return sentm
+        except socket.error, e:
+            self.log.log("Socket Error: messenger->sendMsg(): " +  traceback.format_exc(), ORPG_CRITICAL)
+        except:
+            self.log.log("Exception:  messenger->sendMsg(): " +  traceback.format_exc(), ORPG_CRITICAL)
+
+    def recvThread( self, arg ):
+        "Receiving thread.  This thread reads from the socket and writes to the data queue."
+
+        # Wait to be told it's okay to start running
+        self.startedEvent.wait()
+        while( self.get_status() == 'connected' ):
+            readMsg = self.recvMsg( self.sock )
+
+            # Make sure we didn't get disconnected
+            bytes = len( readMsg )
+            if bytes == 0:
+                break
+
+            # Check the length of the message
+            bytes = len( readMsg )
+
+            # Make sure we are still connected
+            if bytes == 0:
+                break
+            else:
+                # Pass along the message so it can be processed
+                self.inbox.put( readMsg )
+                self.update_idle_time() #update the last message time
+        if bytes == 0:
+            self.log.log("Remote has disconnected!", ORPG_NOTE)
+            self.set_status(2)
+        self.outbox.put( "" )    # Make sure the other thread is woken up!
+        self.sendThreadExitEvent.set()
+        self.log.log("messenger->recvThread() has terminated...", ORPG_NOTE)
+
+    def recvData( self, sock, readSize ):
+        """Simple socket receive method.  This method will only return when the exact
+        byte count has been read from the connection, if remote terminates our
+        connection or we get some other socket exception."""
+        data = ""
+        offset = 0
+        try:
+            while offset != readSize:
+                frag = sock.recv( readSize - offset )
+
+                # See if we've been disconnected
+                rs = len( frag )
+                if rs <= 0:
+                    # Loudly raise an exception because we've been disconnected!
+                    raise IOError, "Remote closed the connection!"
+                else:
+                    # Continue to build complete message
+                    offset += rs
+                    data += frag
+        except socket.error, e:
+            self.log.log("Socket Error: messenger->recvData(): " +  str(e), ORPG_CRITICAL)
+            data = ""
+        return data
+
+    def recvMsg( self, sock ):
+        """This method now expects to receive a message having a 4-byte prefix length.  It will ONLY read
+        completed messages.  In the event that the remote's connection is terminated, it will throw an
+        exception which should allow for the caller to more gracefully handles this exception event.
+
+        Because we use strictly reading ONLY based on the length that is told to use, we no longer have to
+        worry about partially adjusting for fragmented buffers starting somewhere within a buffer that we've
+        read.  Rather, it will get ONLY a whole message and nothing more.  Everything else will remain buffered
+        with the OS until we attempt to read the next complete message."""
+
+        msgData = ""
+        try:
+            lenData = self.recvData( sock, self.lensize )
+
+            # Now, convert to a usable form
+            (length,) = unpack( 'i', lenData )
+            length = socket.ntohl( length )
+
+            # Read exactly the remaining amount of data
+            msgData = self.recvData( sock, length )
+
+            if self.isServer:
+                self.log.log("('data_recv', " + str(length+4) + ")", ORPG_DEBUG)
+        except:
+            self.log.log("Exception: messenger->recvMsg():\n" + traceback.format_exc(), ORPG_CRITICAL)
+        return msgData
+
+if __name__ == "__main__":
+    test = messenger(None)
+    print test.build_message('hello', "This is a test message", attrib1="hello world", attrib2="hello world2", attrib3="hello world3")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/mplay_server.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2887 @@
+#!/usr/bin/python2.1
+# 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: mplay_server.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: mplay_server.py,v 1.155 2008/01/24 03:52:03 digitalxero Exp $
+#
+# Description: This file contains the code for the server of the multiplayer
+# features in the orpg project.
+#
+
+
+# 04-15-2005 [Snowdog]: Added patch from Brandan Yares (xeriar). Reference: patch tracker id #1182076
+
+__version__ = "$Id: mplay_server.py,v 1.155 2008/01/24 03:52:03 digitalxero Exp $"
+
+#!/usr/bin/env python
+"""
+<msg to='' from='' group_id='' />
+<player id='' ip='' group_id='' name='' action='new,del,group,update' status="" version=""/>
+<group id='' name='' pwd='' players='' action='new,del,update' />
+<create_group from='' pwd='' name='' />
+<join_group from='' pwd='' group_id='' />
+<role action='set,get,display' player='' group_id='' boot_pwd='' role=''/>
+"""
+
+from mplay_client import *
+from mplay_client import MPLAY_LENSIZE
+import orpg.dirpath
+import orpg.tools.validate
+import gc
+import cgi
+import sys
+import string
+import time
+import urllib
+from orpg.mapper.map_msg import *
+from threading import Lock, RLock
+from struct import pack, unpack, calcsize
+from meta_server_lib import *
+import traceback
+import re
+
+# Import the minidom XML module
+from xml.dom import minidom
+
+# Snag the version number
+from orpg.orpg_version import *
+
+#Plugins
+from server_plugins import ServerPlugins
+
+def id_compare(a,b):
+    "converts strings to intergers for list sort comparisons for group and player ids so they end up in numeric order"
+    return cmp(int(a),int(b))
+
+
+class game_group(object):
+    def __init__( self, id, name, pwd, desc="", boot_pwd="", minVersion="", mapFile=None, messageFile=None, persist =0 ):
+        self.id = id
+        self.name = name
+        self.desc = desc
+        self.minVersion = minVersion
+        self.messageFile = messageFile
+        self.players = []
+        self.pwd = pwd
+        self.boot_pwd = boot_pwd
+        self.game_map = map_msg()
+        self.lock = Lock()
+        self.moderated = 0
+        self.voice = {}
+        self.persistant = persist
+        self.mapFile = None
+
+        if mapFile != None:
+            self.mapFile = mapFile
+            f = open( mapFile )
+            tree = f.read()
+            f.close()
+
+        else:
+            f = open(orpg.dirpath.dir_struct["template"] + "default_map.xml")
+            tree = f.read()
+            f.close()
+
+        self.game_map.init_from_xml(tree)
+
+    def save_map(self):
+        if self.mapFile is not None and self.persistant == 1 and self.mapFile.find("default_map.xml") == -1:
+            f = open(self.mapFile, "w")
+            f.write(self.game_map.get_all_xml())
+            f.close()
+
+
+    def add_player(self,id):
+        self.players.append(id)
+
+    def remove_player(self,id):
+        if self.voice.has_key(id):
+            del self.voice[id]
+        self.players.remove(id)
+
+    def get_num_players(self):
+        num =  len(self.players)
+        return num
+
+    def get_player_ids(self):
+        tmp = self.players
+        return tmp
+
+
+    def check_pwd(self,pwd):
+        return (pwd==self.pwd)
+
+    def check_boot_pwd(self,pwd):
+        return (pwd==self.boot_pwd)
+
+    def check_version(self,ver):
+        if (self.minVersion == ""):
+            return 1
+        minVersion=self.minVersion.split('.')
+        version=ver.split('.')
+        for i in xrange(min(len(minVersion),len(version))):
+            w=max(len(minVersion[i]),len(version[i]))
+            v1=minVersion[i].rjust(w);
+            v2=version[i].rjust(w);
+            if v1<v2:
+                return 1
+            if v1>v2:
+                return 0
+
+        if len(minVersion)>len(version):
+            return 0
+        return 1
+
+    #depreciated - see send_group_list()
+    def toxml(self,act="new"):
+        #  Please don't add the boot_pwd to the xml, as this will give it away to players watching their console
+        xml_data = "<group id=\"" + self.id
+        xml_data += "\" name=\"" + self.name
+        xml_data += "\" pwd=\"" + str(self.pwd!="")
+        xml_data += "\" players=\"" + str(self.get_num_players())
+        xml_data += "\" action=\"" + act + "\" />"
+        return xml_data
+
+
+
+class client_stub(client_base):
+    def __init__(self,inbox,sock,props,log):
+        client_base.__init__(self)
+        self.ip = props['ip']
+        self.role = props['role']
+        self.id = props['id']
+        self.group_id = props['group_id']
+        self.name = props['name']
+        self.version = props['version']
+        self.protocol_version = props['protocol_version']
+        self.client_string = props['client_string']
+        self.inbox = inbox
+        self.sock = sock
+        self.timeout_time = None
+        self.log_console = log
+        self.ignorelist = {}
+
+    # implement from our base class
+    def isServer( self ):
+        return 1
+
+    def clear_timeout(self):
+        self.timeout_time = None
+
+    def check_time_out(self):
+        if self.timeout_time==None:
+            self.timeout_time = time.time()
+        curtime = time.time()
+        diff = curtime - self.timeout_time
+        if diff > 1800:
+            return 1
+        else:
+            return 0
+
+    def send(self,msg,player,group):
+        if self.get_status() == MPLAY_CONNECTED:
+            self.outbox.put("<msg to='" + player + "' from='0' group_id='" + group + "' />" + msg)
+
+    def change_group(self,group_id,groups):
+        old_group_id = str(self.group_id)
+        groups[group_id].add_player(self.id)
+        groups[old_group_id].remove_player(self.id)
+        self.group_id = group_id
+        self.outbox.put(self.toxml('group'))
+        msg = groups[group_id].game_map.get_all_xml()
+        self.send(msg,self.id,group_id)
+        return old_group_id
+
+    def self_message(self,act):
+        self.send(act,self.id,self.group_id)
+
+    def take_dom(self,xml_dom):
+        self.name = xml_dom.getAttribute("name")
+        self.text_status = xml_dom.getAttribute("status")
+
+
+######################################################################
+######################################################################
+##
+##
+##   MPLAY SERVER
+##
+##
+######################################################################
+######################################################################
+
+class mplay_server:
+    def __init__(self, log_console=None, name=None):
+        self.log_to_console = 1
+        self.log_console = log_console
+        self.alive = 1
+        self.players = {}
+        self.listen_event = Event()
+        self.incoming_event = Event()
+        self.incoming = Queue.Queue(0)
+        self.p_lock = RLock()
+        self.next_player_id = 1
+        self.plugin_player_id = -1
+        self.next_group_id = 100
+        self.metas = {}              #  This holds the registerThread objects for each meta
+        self.be_registered = 0       #  Status flag for whether we want to be registered.
+        self.serverName = name            #  Name of this server in the metas
+        self.boot_pwd = ""
+        self.server_address = None # IP or Name of server to post to the meta. None means the meta will auto-detect it.
+        self.defaultMessageFile = None
+        self.userPath = orpg.dirpath.dir_struct["user"]
+        self.lobbyMapFile = "Lobby_map.xml"
+        self.lobbyMessageFile = "LobbyMessage.html"
+        self.banFile = "ban_list.xml"
+        self.show_meta_messages = 0
+        self.log_network_messages = 0
+        self.allow_room_passwords = 1
+        self.silent_auto_kick = 0
+        self.zombie_time = 480 #time in minutes before a client is considered a ZOMBIE
+        self.minClientVersion = SERVER_MIN_CLIENT_VERSION
+        self.maxSendSize = 1024
+        self.server_port = OPENRPG_PORT
+        self.allowRemoteKill = False
+        self.allowRemoteAdmin = True
+        self.sendLobbySound = False
+        self.lobbySound = 'http://www.digitalxero.net/music/mus_tavern1.bmu'
+
+    def initServer(self, **kwargs):
+        for atter, value in kwargs.iteritems():
+            setattr(self, atter, value)
+        self.validate = orpg.tools.validate.Validate(self.userPath)
+        self.validate.config_file( self.lobbyMapFile, "default_Lobby_map.xml" )
+        self.validate.config_file( self.lobbyMessageFile, "default_LobbyMessage.html" )
+        self.server_start_time = time.time()
+
+        # Since the server is just starting here, we read in the XML configuration
+        # file.  Notice the lobby is still created here by default.
+        self.groups = { '0': game_group('0','Lobby','','The game lobby', '', '', self.userPath + self.lobbyMapFile, self.userPath + self.lobbyMessageFile, 1)}
+        # Make sure the server's name gets set, in case we are being started from
+        # elsewhere.  Basically, if it's passed in, we'll over ride what we were
+        # prompted for.  This should never really happen at any rate.
+
+        self.initServerConfig()
+        self.listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self.listen_thread = thread.start_new_thread(self.listenAcceptThread, (0,))
+        self.in_thread = thread.start_new_thread(self.message_handler,(0,))
+
+        #  Starts the player reaper thread.  See self.player_reaper_thread_func() for more explanation
+        self.player_reaper_thread = thread.start_new_thread(self.player_reaper_thread_func,(0,))
+        thread.start_new_thread(self.PluginThread,())
+        self.svrcmds = {}
+        self.initsvrcmds()
+        self.ban_list = {}
+        self.initBanList()
+
+    def addsvrcmd(self, cmd, function):
+        if not self.svrcmds.has_key(cmd):
+            self.svrcmds[cmd] = {}
+            self.svrcmds[cmd]['function'] = function
+
+    def initsvrcmds(self):
+        self.addsvrcmd('msg', self.incoming_msg_handler)
+        self.addsvrcmd('player', self.incoming_player_handler)
+        self.addsvrcmd('admin', self.remote_admin_handler)
+        self.addsvrcmd('alter', self.do_alter)
+        self.addsvrcmd('role', self.do_role)
+        self.addsvrcmd('ping', self.do_ping)
+        self.addsvrcmd('system', self.do_system)
+        self.addsvrcmd('join_group', self.join_group)
+        self.addsvrcmd('create_group', self.create_group)
+        self.addsvrcmd('moderate', self.moderate_group)
+        self.addsvrcmd('plugin', self.plugin_msg_handler)
+        self.addsvrcmd('sound', self.sound_msg_handler)
+
+
+    # This method reads in the server's ban list added by Darren
+    def initBanList( self ):
+        self.log_msg("Processing Ban List File...")
+
+        # make sure the server_ini.xml exists!
+        self.validate.config_file(self.banFile, "default_ban_list.xml" )
+
+        # try to use it.
+        try:
+            self.banDom = minidom.parse(self.userPath + 'ban_list.xml')
+            self.banDom.normalize()
+            self.banDoc = self.banDom.documentElement
+
+            for element in self.banDom.getElementsByTagName('banned'):
+                playerName = element.getAttribute( 'name' ).replace("&", "&amp;").replace("<", "&lt;").replace('"', "&quot;").replace(">", "&gt;")
+                playerIP = element.getAttribute('ip')
+                self.ban_list[playerIP] = {}
+                self.ban_list[playerIP]['ip'] = playerIP
+                self.ban_list[playerIP]['name'] = playerName
+                self.log_msg(str(playerName) + " " + str(playerIP) + " is banned.")
+            self.saveBanList()
+        except Exception, e:
+            self.log_msg("Exception in initBanList() " + str(e))
+
+    # This method writes out the server's ban list added by Darren
+    def saveBanList( self ):
+        self.log_msg("Saving Ban List File...")
+
+        # try to use it.
+        try:
+            data = []
+            data.append("<server>\n")
+            for ip in self.ban_list:
+                data.append('    <banned name="' + str(self.ban_list[ip]['name'].replace("&amp;", "&").replace("&lt;", "<").replace("&quot;", '"').replace("&gt;", ">")) + '" ip="' + str(self.ban_list[ip]['ip']) + '" />' + "\n")
+            data.append("</server>")
+            file = open(self.userPath + self.banFile ,"w")
+            file.write("".join(data))
+            file.close()
+        except Exception, e:
+            self.log_msg("Exception in saveBanList() " + str(e))
+
+    # This method reads in the server's configuration file and reconfigs the server
+    # as needed, over-riding any default values as requested.
+    def initServerConfig(self):
+        self.log_msg("Processing Server Configuration File... " + self.userPath)
+        # make sure the server_ini.xml exists!
+        self.validate.config_file( "server_ini.xml", "default_server_ini.xml" )
+        # try to use it.
+        try:
+            self.configDom = minidom.parse(self.userPath + 'server_ini.xml')
+            self.configDom.normalize()
+            self.configDoc = self.configDom.documentElement
+            # Obtain the lobby/server password if it's been specified
+            if self.configDoc.hasAttribute("admin"):
+                self.boot_pwd = self.configDoc.getAttribute("admin")
+            elif self.configDoc.hasAttribute("boot"):
+                self.boot_pwd = self.configDoc.getAttribute("boot")
+            if hasattr(self, 'bootPassword'):
+                self.boot_pwd = self.bootPassword
+            elif len(self.boot_pwd) < 1:
+                self.boot_pwd = raw_input("Enter boot password for the Lobby:  ")
+            if not hasattr(self, 'reg') and self.configDoc.hasAttribute("register"):
+                self.reg = self.configDoc.getAttribute("register")
+            if not len(self.reg) > 0 or self.reg[0].upper() not in ("Y", "N"):
+                opt = raw_input("Do you want to post your server to the OpenRPG Meta Server list? (y,n) ")
+                if len(opt) and (opt[0].upper() == 'Y'):
+                    self.reg = 'Y'
+                else:
+                    self.reg = 'N'
+            LobbyName = 'Lobby'
+            if self.configDoc.hasAttribute("lobbyname"):
+                LobbyName = self.configDoc.getAttribute("lobbyname")
+            map_node = service_node = self.configDoc.getElementsByTagName("map")[0]
+            msg_node = service_node = self.configDoc.getElementsByTagName("message")[0]
+            mapFile = map_node.getAttribute('file')
+            msgFile = msg_node.getAttribute('file')
+            if mapFile == '':
+                mapFile = 'Lobby_map.xml'
+            if msgFile == '':
+                msgFile = 'LobbyMessage.html'
+            # Update the lobby with the passwords if they've been specified
+            if len(self.boot_pwd):
+                self.groups = {'0': game_group( '0', LobbyName, "", 'The game lobby', self.boot_pwd, "",
+                                                 self.userPath + mapFile.replace("myfiles/", ""),
+                                                 self.userPath + msgFile.replace("myfiles/", ""), 1 )
+                                }
+
+            # set ip or dns name to send to meta server
+            service_node = self.configDoc.getElementsByTagName("service")[0]
+            address = service_node.getAttribute("address")
+            address = address.lower()
+            if address == "" or address == "hostname/address" or address == "localhost":
+                self.server_address = None
+            else:
+                self.server_address = address
+            self.server_port = OPENRPG_PORT
+            if service_node.hasAttribute("port"):
+                self.server_port = int(service_node.getAttribute("port"))
+            if self.configDoc.hasAttribute("name") and len(self.configDoc.getAttribute("name")) > 0 :
+                self.name = self.configDoc.getAttribute("name")
+            else:
+                if self.reg[0].upper() == "Y":
+                    if self.name == None:
+                       self.name = raw_input("Server Name? ")
+                    self.register()
+
+            # Get the minimum openrpg version from config if available
+            # if it isn't set min version to internal default.
+            #
+            # server_ini.xml entry for version tag...
+            # <version min="x.x.x">
+            try:
+                mver = self.configDoc.getElementsByTagName("version")[0]
+                self.minClientVersion = mver.getAttribute("min")
+            except:
+                self.minClientVersion = SERVER_MIN_CLIENT_VERSION #from orpg/orpg_version.py
+            self.defaultMessageFile = ""
+            # This try/except bit is to allow older versions of python to continue without a list error.
+
+
+
+            #------------------------[ START <AUTOKICK> TAG PROCESSING ]--------------
+            # Auto-kick option defaults for silent booting and
+            # setting the default zombie-client delay time --Snowdog 9/05
+            #
+            # server_ini.xml entry for autikick tag...
+            # <autokick silent=["no","yes"] delay="(# of seconds)">
+
+            try:
+                ak = self.configDoc.getElementsByTagName("autokick")[0]
+                if ak.hasAttribute("silent"):
+                    if ((ak.getAttribute("silent")).lower() == "yes"):
+                        self.silent_auto_kick = 1
+                    else:
+                        self.silent_auto_kick = 0
+                if ak.hasAttribute("delay"):
+                    try:
+                        delay = int(ak.getAttribute("delay"))
+                        self.zombie_time = delay
+                    except:
+                        #delay value cannot be converted into an int use defaut
+                        self.zombie_time = 480 #(default 8 mins)
+                        self.log_msg("**WARNING** Error with autokick delay string using default (480 sec)")
+
+            except:
+                self.silent_auto_kick = 0 #(default to off)
+                self.zombie_time = 480 #(default 8 mins)
+                self.log_msg("**WARNING** Error loading autokick settings... using defaults")
+
+            alk = ""
+            if (self.silent_auto_kick == 1): alk = "(Silent Mode)"
+            self.log_msg("Auto Kick:  Delay="+str(self.zombie_time) + " " + alk)
+            #------------------------[ END <AUTOKICK> TAG PROCESSING ]--------------
+
+
+
+            #-------------------------------[ START <ROOM_DEFAULT> TAG PROCESSING ]--------------------
+            #
+            # New room_defaults configuration option used to set various defaults
+            # for all user created rooms on the server. Incorporates akomans older
+            # default room message code (from above)      --Snowdog 11/03
+            #
+            # option syntax
+            # <room_defaults passwords="yes" map="myfiles/LobbyMap.xml" message="myfiles/LobbyMessage.html" />
+
+            #default settings for tag options...
+            roomdefault_msg = str(self.defaultMessageFile) #no message is the default
+            roomdefault_map = "" #use lobby map as default
+            roomdefault_pass = 1 #allow passwords
+
+
+            #pull information from config file DOM
+            try:
+                roomdefaults = self.configDom.getElementsByTagName("room_defaults")[0]
+                #rd.normalize()
+                #roomdefaults = self.rd.documentElement
+                try:
+                    setting = roomdefaults.getElementsByTagName('passwords')[0]
+                    rpw = setting.getAttribute('allow')
+                    if rpw == "no" or rpw == "0":
+                        roomdefault_pass = 0
+                        self.log_msg("Room Defaults: Disallowing Passworded Rooms")
+                    else:
+                        self.log_msg("Room Defaults: Allowing Passworded Rooms")
+                except:
+                    self.log_msg("Room Defaults: [Warning] Allowing Passworded Rooms")
+                try:
+                    setting = roomdefaults.getElementsByTagName('map')[0]
+                    map = setting.getAttribute('file')
+                    if map != "":
+                        roomdefault_map = self.userPath + map.replace("myfiles/", "")
+                        self.log_msg("Room Defaults: Using " + str(map) + " for room map")
+                except:
+                    self.log_msg("Room Defaults: [Warning] Using Default Map")
+
+                try:
+                    setting = roomdefaults.getElementsByTagName('message')[0]
+                    msg = setting.getAttribute('file')
+                    if msg != "":
+                        if msg[:4].lower() == 'http':
+                            roomdefault_msg = msg
+                        else:
+                            roomdefault_msg = self.userPath + msg.replace("myfiles/", "")
+                        self.log_msg("Room Defaults: Using " + str(msg) + " for room messages")
+                except:
+                    print ("Room Defaults: [Warning] Using Default Message")
+            except:
+                traceback.print_exc()
+                self.log_msg("**WARNING** Error loading default room settings from configuration file. Using internal defaults.")
+
+
+            #set the defaults
+            if roomdefault_msg != "" or roomdefault_msg != None:
+                self.defaultMessageFile = roomdefault_msg  #<room_defaults> tag superceeds older <newrooms> tag
+            else:
+                self.defaultMessageFile = None
+
+            if roomdefault_map != "" or roomdefault_map != None:
+                self.defaultMapFile = roomdefault_map  #<room_defaults> tag superceeds older <newrooms> tag
+            else:
+                self.defaultMapFile = None
+
+            ##### room default map not handled yet. SETTING IGNORED
+            if roomdefault_pass == 0: self.allow_room_passwords = 0
+            else: self.allow_room_passwords = 1
+
+            #-------------------------------[ END <ROOM_DEFAULT> TAG PROCESSING ]--------------------
+
+
+            ###Server Cheat message
+            try:
+                cheat_node = self.configDoc.getElementsByTagName("cheat")[0]
+                self.cheat_msg = cheat_node.getAttribute("text")
+            except:
+                self.cheat_msg = "**FAKE ROLL**"
+                self.log_msg("**WARNING** <cheat txt=\"\"> tag missing from server configuration file. Using empty string.")
+
+
+
+            # should validate protocal
+            validate_protocol_node = self.configDom.getElementsByTagName("validate_protocol ")
+
+            self.validate_protocol = 1
+
+            if(validate_protocol_node):
+                self.validate_protocol = (validate_protocol_node[0].getAttribute("value") == "True")
+            if(self.validate_protocol != 1):
+                self.log_msg("Protocol Validation: OFF")
+            self.makePersistentRooms()
+
+            self.log_msg("Server Configuration File: Processing Completed.")
+
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg("Exception in initServerConfig() " + str(e))
+
+
+    def makePersistentRooms(self):
+        'Creates rooms on the server as defined in the server config file.'
+
+        for element in self.configDom.getElementsByTagName('room'):
+            roomName = element.getAttribute('name')
+            roomPassword = element.getAttribute('password')
+            bootPassword = element.getAttribute('boot')
+
+            # Conditionally check for minVersion attribute
+            if element.hasAttribute('minVersion'):
+                minVersion = element.getAttribute('minVersion')
+            else:
+                minVersion = ""
+
+            # Extract the map filename attribute from the map node
+            # we only care about the first map element found -- others are ignored
+            mapElement = element.getElementsByTagName('map')[0]
+            mapFile = self.userPath + mapElement.getAttribute('file').replace("myfiles/", "")
+
+            messageElement = element.getElementsByTagName('message')[0]
+            messageFile = messageElement.getAttribute('file')
+
+            if messageFile[:4] != 'http':
+                messageFile = self.userPath + messageFile.replace("myfiles/", "")
+
+            # Make sure we have a message to even mess with
+            if(len(messageFile) == 0):
+                messageFile = self.defaultMessageFile
+
+            if(len(mapFile) == 0):
+                mapFile = self.defaultMapFile
+
+            moderated = 0
+            if element.hasAttribute('moderated') and element.getAttribute('moderated').lower() == "true":
+                moderated = 1
+
+            #create the new persistant group
+            self.new_group(roomName, roomPassword, bootPassword, minVersion, mapFile, messageFile, persist = 1, moderated=moderated)
+
+
+
+    def isPersistentRoom(self, id):
+        'Returns True if the id is a persistent room (other than the lobby), otherwise, False.'
+
+        # altered persistance tracking from simple room id based to per-group setting
+        # allows arbitrary rooms to be marked as persistant without needing the self.persistRoomThreshold
+        # -- Snowdog 4/04
+        try:
+            id = str(id) #just in case someone sends an int instead of a str into the function
+            if id not in self.groups: return 0 #invalid room, can't be persistant
+            pr = (self.groups[id]).persistant
+            return pr
+        except:
+            self.log_msg("Exception occured in isPersistentRoom(self,id)")
+            return 0
+
+
+
+    #-----------------------------------------------------
+    #  Toggle Meta Logging  -- Added by Snowdog 4/03
+    #-----------------------------------------------------
+    def toggleMetaLogging(self):
+        if self.show_meta_messages != 0:
+            self.log_msg("Meta Server Logging: OFF")
+            self.show_meta_messages = 0
+        else:
+            self.log_msg("Meta Server Logging: ON")
+            self.show_meta_messages = 1
+
+
+    #-----------------------------------------------------
+    #  Start/Stop Network Logging to File  -- Added by Snowdog 4/03
+    #-----------------------------------------------------
+    def NetworkLogging(self, mode = 0):
+        if mode == 0:
+            self.log_msg("Network Logging: OFF")
+            self.log_network_messages = 0
+        elif mode == 1:
+            self.log_msg("Network Logging: ON (composite logfile)")
+            self.log_network_messages = 1
+        elif mode == 2:
+            self.log_msg("Network Logging: ON (split logfiles)")
+            self.log_network_messages = 2
+        else: return
+        #when log mode changes update all connection stubs
+        for n in self.players:
+            try:
+                self.players[n].EnableMessageLogging = mode
+            except:
+                self.log_msg("Error changing Message Logging Mode for client #" + str(self.players[n].id))
+    def NetworkLoggingStatus(self):
+        if self.log_network_messages == 0: return "Network Traffic Log: Off"
+        elif self.log_network_messages == 1: return "Network Traffic Log: Logging (composite file)"
+        elif self.log_network_messages == 2: return "Network Traffic Log: Logging (inbound/outbound files)"
+        else: self.log_msg("Network Traffic Log: [Unknown]")
+
+
+
+
+    def register_callback(instance, xml_dom = None,source=None):
+        if xml_dom:    # if we get something
+            if source == getMetaServerBaseURL():    # if the source of this DOM is the authoritative meta
+                try:
+                    metacache_lock.acquire()
+                    curlist = getRawMetaList()      #  read the raw meta cache lines into a list
+                    updateMetaCache(xml_dom)        #  update the cache from the xml
+                    newlist = getRawMetaList()      #  read it into a second list
+                finally:
+                    metacache_lock.release()
+
+                if newlist != curlist:          #  If the two lists aren't identical
+                                               #  then something has changed.
+                    instance.register()             #  Call self.register()
+                                                #  which will force a re-read of the meta cache and
+                                                #  redo the registerThreads
+        else:
+            instance.register()
+
+                # Eventually, reset the MetaServerBaseURL here
+
+    ## Added to help clean up parser errors in the XML on clients
+    ## due to characters that break welformedness of the XML from
+    ## the meta server.
+    ## NOTE: this is a stopgap measure -SD
+    def clean_published_servername(self, name):
+        #clean name of all apostrophes and quotes
+        badchars = "\"\\`><"
+        for c in badchars:
+            name = name.replace(c,"")
+        return name
+
+    def registerRooms(self, args=None):
+        rooms = ''
+        id = '0'
+        time.sleep(500)
+        for rnum in self.groups.keys():
+            rooms += urllib.urlencode( {"room_data[rooms][" + str(rnum) + "][name]":self.groups[rnum].name,
+                                        "room_data[rooms][" + str(rnum) + "][pwd]":str(self.groups[rnum].pwd != "")})+'&'
+
+            for pid in self.groups[rnum].players:
+                rooms += urllib.urlencode( {"room_data[rooms][" + str(rnum) + "][players]["+str(pid)+"]":self.players[pid].name,})+'&'
+
+
+        for meta in self.metas.keys():
+            while id == '0':
+                id, cookie = self.metas[meta].getIdAndCookie()
+                data = urllib.urlencode( {"room_data[server_id]":id,
+                                        "act":'registerrooms'})
+            get_server_dom(data+'&'+rooms, self.metas[meta].path)
+
+
+    def register(self,name_given=None):
+        if name_given == None:
+            name = self.name
+        else:
+            self.name = name = name_given
+
+        name = self.clean_published_servername(name)
+
+        #  Set up the value for num_users
+        if self.players:
+            num_players = len(self.players)
+        else:
+            num_players = 0
+
+        #  request only Meta servers compatible with version 2
+        metalist = getMetaServers(versions=["2"])
+        if self.show_meta_messages != 0:
+            self.log_msg("Found these valid metas:")
+            for meta in metalist:
+                self.log_msg("Meta:" + meta)
+
+        #  Go through the list and see if there is already a running register
+        #  thread for the meta.
+        #  If so, call it's register() method
+        #  If not, start one, implicitly calling the new thread's register() method
+
+
+        #  iterate through the currently running metas and prune any
+        #  not currently listed in the Meta Server list.
+        if self.show_meta_messages != 0: self.log_msg( "Checking running register threads for outdated metas.")
+        for meta in self.metas.keys():
+            if self.show_meta_messages != 0: self.log_msg("meta:" + meta + ": ")
+            if not meta in metalist:  # if the meta entry running is not in the list
+                if self.show_meta_messages != 0: self.log_msg( "Outdated.  Unregistering and removing")
+                self.metas[meta].unregister()
+                del self.metas[meta]
+            else:
+                if self.show_meta_messages != 0: self.log_msg( "Found in current meta list.  Leaving intact.")
+
+        #  Now call register() for alive metas or start one if we need one
+        for meta in metalist:
+            if self.metas.has_key(meta) and self.metas[meta] and self.metas[meta].isAlive():
+                self.metas[meta].register(name=name, realHostName=self.server_address, num_users=num_players)
+            else:
+                self.metas[meta] = registerThread(name=name, realHostName=self.server_address, num_users=num_players, MetaPath=meta, port=self.server_port,register_callback=self.register_callback)
+                self.metas[meta].start()
+
+        #The register Rooms thread
+
+        self.be_registered = 1
+        thread.start_new_thread(self.registerRooms,(0,))
+
+
+
+    def unregister(self):
+        #  loop through all existing meta entries
+        #  Don't rely on getMetaServers(), as a server may have been
+        #  removed since it was started.  In that case, then the meta
+        #  would never get unregistered.
+        #
+        #  Instead, loop through all existing meta threads and unregister them
+
+        for meta in self.metas.values():
+            if meta and meta.isAlive():
+                meta.unregister()
+
+        self.be_registered = 0
+
+
+
+
+    #  This method runs as it's own thread and does the group_member_check every
+    #    sixty seconds.  This should eliminate zombies that linger when no one is
+    #    around to spook them.  GC: Frequency has been reduced as I question how valid
+    #    the implementation is as it will only catch a very small segment of lingering
+    #    connections.
+    def player_reaper_thread_func(self,arg):
+        while self.alive:
+            time.sleep(60)
+
+            self.p_lock.acquire()
+            for group in self.groups.keys():
+                self.check_group_members(group)
+            self.p_lock.release()
+
+    #This thread runs ever 250 miliseconds, and checks various plugin stuff
+    def PluginThread(self):
+        while self.alive:
+            self.p_lock.acquire()
+            players = ServerPlugins.getPlayer()
+
+            for player in players:
+                if player is not None:
+                    #Do something here so they can show up in the chat room for non web users'
+                    pass
+
+            data = ServerPlugins.preParseOutgoing()
+
+            for msg in data:
+                try:
+                    xml_dom = parseXml(msg)
+                    xml_dom = xml_dom._get_documentElement()
+
+                    if xml_dom.hasAttribute('from') and int(xml_dom.getAttribute('from')) > -1:
+                        xml_dom.setAttribute('from', '-1')
+
+                    xml_dom.setAttribute('to', 'all')
+                    self.incoming_msg_handler(xml_dom, msg)
+                    xml_dom.unlink()
+                except:
+                    pass
+
+            self.p_lock.release()
+            time.sleep(0.250)
+
+
+    def sendMsg( self, sock, msg, useCompression=False, cmpType=None):
+        """Very simple function that will properly encode and send a message to te
+        remote on the specified socket."""
+
+        if useCompression and cmpType != None:
+            mpacket = cmpType.compress(msg)
+            lpacket = pack('!i', len(mpacket))
+            sock.send(lpacket)
+
+            offset = 0
+            while offset < len(mpacket):
+                slice = buffer(mpacket, offset, len(mpacket)-offset)
+                sent = sock.send(slice)
+                offset += sent
+            sentm = offset
+        else:
+            # Calculate our message length
+            length = len( msg )
+
+            # Encode the message length into network byte order
+            lp = pack('!i', length)
+
+            try:
+                # Send the encoded length
+                sentl = sock.send( lp )
+
+                # Now, send the message the the length was describing
+                sentm = sock.send( msg )
+
+            except socket.error, e:
+                self.log_msg( e )
+
+            except Exception, e:
+                self.log_msg( e )
+
+
+    def recvData( self, sock, readSize ):
+        """Simple socket receive method.  This method will only return when the exact
+        byte count has been read from the connection, if remote terminates our
+        connection or we get some other socket exception."""
+
+        data = ""
+        offset = 0
+        try:
+            while offset != readSize:
+                frag = sock.recv( readSize - offset )
+
+                # See if we've been disconnected
+                rs = len( frag )
+                if rs <= 0:
+                    # Loudly raise an exception because we've been disconnected!
+                    raise IOError, "Remote closed the connection!"
+
+                else:
+                    # Continue to build complete message
+                    offset += rs
+                    data += frag
+
+        except socket.error, e:
+            self.log_msg("Socket Error: recvData(): " +  e )
+            data = ""
+
+        return data
+
+
+
+    def recvMsg(self, sock, useCompression=False, cmpType=None):
+        """This method now expects to receive a message having a 4-byte prefix length.  It will ONLY read
+        completed messages.  In the event that the remote's connection is terminated, it will throw an
+        exception which should allow for the caller to more gracefully handles this exception event.
+
+        Because we use strictly reading ONLY based on the length that is told to use, we no longer have to
+        worry about partially adjusting for fragmented buffers starting somewhere within a buffer that we've
+        read.  Rather, it will get ONLY a whole message and nothing more.  Everything else will remain buffered
+        with the OS until we attempt to read the next complete message."""
+
+        msgData = ""
+        try:
+            lenData = self.recvData( sock, MPLAY_LENSIZE )
+
+            # Now, convert to a usable form
+            (length,) = unpack('!i', lenData)
+
+            # Read exactly the remaining amount of data
+            msgData = self.recvData( sock, length )
+
+            try:
+                if useCompression and cmpType != None:
+                    msgData = cmpType.decompress(msgData)
+            except:
+                traceback.print_exc()
+
+        except Exception, e:
+            self.log_msg( "Exception: recvMsg(): " + str(e) )
+
+        return msgData
+
+
+
+    def kill_server(self):
+        self.alive = 0
+        self.log_msg("Server stopping...")
+        self.unregister()                    # unregister from the Meta
+        for p in self.players.itervalues():
+            p.disconnect()
+            self.incoming.put("<system/>")
+
+        for g in self.groups.itervalues():
+            g.save_map()
+
+        try:
+            ip = socket.gethostbyname(socket.gethostname())
+            kill = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            kill.connect((ip, self.server_port))
+
+            # Now, send the "system" command using the correct protocol format
+            self.sendMsg( kill, "<system/>" )
+            kill.close()
+        except:
+            pass
+
+        self.listen_sock.close()
+        self.listen_event.wait(10)
+        self.incoming_event.wait(10)
+        self.log_msg("Server stopped!")
+
+
+
+    def log_msg(self,msg):
+        if self.log_to_console:
+            if self.log_console:
+                self.log_console(msg)
+            else:
+                print str(msg)
+
+
+    def print_help(self):
+        print
+        print "Commands: "
+        print "'kill' or 'quit' - to stop the server"
+        print "'broadcast' - broadcast a message to all players"
+        print "'list' - list players and groups"
+        print "'dump' - to dump player data"
+        print "'dump groups' - to list the group names and ids only"
+        print "'group n' - to list details about one group only"
+        print "'register' - To register the server as name.  Also used to change the server's name if registered."
+        print "'unregister' - To remove this server from the list of servers"
+        print "'get lobby boot password' - to show the Lobby's boot password"
+        print "'set lobby boot password' - to set the Lobby's boot password"
+        print "'log' - toggles logging to the console off or on"
+        print "'log meta' - toggles logging of meta server messages on or off"
+        print "'logfile [off|on|split]' - timestamped network traffic log"
+        print "'remove room' - to remove a room from the server"
+        print "'kick' - kick a player from the server"
+        print "'ban' - ban a player from the server"
+        print "'remotekill' - This will toggle the ability to kill the server via the /admin command"
+        print "'monitor (#)' - monitors raw network I/O stream to specific client"
+        print "'purge clients' - boots all connected clients off the server immediately"
+        print "'zombie [set [min]]' - view/set the auto-kick time for zombie clients"
+        #drop any clients that are idle for more than 8 hours
+        #as these are likely dead clientskick' - kick a player from the server"
+        print "'uptime' - reports how long server has been running"
+        print "'roompasswords' - allow/disallow room passwords (toggle)"
+        print "'search' - will prompt for pattern and display results"
+        print "'sendsize' - will ajust the send size limit"
+        print "'remoteadmin' - will toggle remote admin commands"
+        print "'togglelobbysound' - Will turn on or off the Auto sending of a sound to all players who join the loby"
+        print "'lobbysound' - Lets you specify which sound file to send to players joining the lobby"
+        print "'help' or '?' or 'h' - for this help message"
+        print
+
+
+    def broadcast(self,msg):
+        self.send_to_all("0","<msg to='all' from='0' group_id='1'><font color='#FF0000'>" + msg + "</font>")
+
+
+    def console_log(self):
+        if self.log_to_console == 1:
+            print "console logging now off"
+            self.log_to_console = 0
+        else:
+            print "console logging now on"
+            self.log_to_console = 1
+
+
+    def groups_list(self):
+        self.p_lock.acquire()
+        try:
+            keys = self.groups.keys()
+            for k in keys:
+                pw = "-"
+                pr = " -"
+                if self.groups[k].pwd != "":
+                    pw = "P"
+                if self.isPersistentRoom( k ):
+                    pr = " S" #using S for static (P for persistant conflicts with password)
+                print "Group: " + k + pr + pw + '  Name: ' + self.groups[k].name
+            print
+
+        except Exception, e:
+            self.log_msg(str(e))
+
+        self.p_lock.release()
+
+#----------------------------------------------------------------
+#  Monitor Function  -- Added by snowdog 2/05
+#----------------------------------------------------------------
+    def monitor(self, pid, mode=1 ):
+        "allows monitoring of a specific user(s) network i/o"
+        #if mode is not set to 1 then monitor adds toggles the state
+        #of monitoring on the given user
+
+        if (mode == 1):
+            for p in self.players:
+                try: p.monitor("off")
+                except: pass
+        try:
+            r = (self.players[pid]).set_traffic_monitor("toggle")
+            self.log_msg("Monitor: Mode=" + str(r) + " on Player #" + str(pid))
+        except:
+            self.log_msg("Monitor: Invalid Player ID")
+            traceback.print_exc()
+
+
+    def search(self,patern):
+        keys = self.groups.keys()
+        print "Search results:"
+        for k in keys:
+            ids = self.groups[k].get_player_ids()
+            for id in ids:
+                if self.players[id].id.find(patern)>-1:
+                    self.print_player_info(self.players[id])
+
+                elif self.players[id].name.find(patern)>-1:
+                    self.print_player_info(self.players[id])
+
+                elif self.players[id].ip.find(patern)>-1:
+                    self.print_player_info(self.players[id])
+
+                elif self.players[id].group_id.find(patern)>-1:
+                    self.print_player_info(self.players[id])
+
+                elif self.players[id].role.find(patern)>-1:
+                    self.print_player_info(self.players[id])
+
+                elif self.players[id].version.find(patern)>-1:
+                    self.print_player_info(self.players[id])
+
+                elif self.players[id].protocol_version.find(patern)>-1:
+                    self.print_player_info(self.players[id])
+
+                elif self.players[id].client_string.find(patern)>-1:
+                    self.print_player_info(self.players[id])
+
+
+    def print_player_info(self,player):
+        print player.id,player.name,player.ip,player.group_id, player.role,player.version,player.protocol_version,player.client_string
+
+    #----------------------------------------------------------------
+    #  Uptime Function  -- Added by snowdog 4/03
+    #----------------------------------------------------------------
+    def uptime(self , mode = 0):
+        "returns string containing how long server has been in operation"
+        ut = time.time() - self.server_start_time
+        d = int(ut/86400)
+        h = int( (ut-(86400*d))/3600 )
+        m = int( (ut-(86400*d)-(3600*h))/60)
+        s = int( (ut-(86400*d)-(3600*h)-(60*m)) )
+        uts =  str( "This server has been running for:\n " + str(d) + " days  " + str(h) + " hours  " + str(m) + " min. " + str(s) + " sec.  [" + str(int(ut)) + " seconds]")
+        if mode == 0: print uts
+        else: return uts
+
+    #-----------------------------------------------------
+    #  Toggle Room Password Allow  -- Added by Snowdog 11/03
+    #-----------------------------------------------------
+    def RoomPasswords(self):
+        if self.allow_room_passwords != 0:
+            self.allow_room_passwords = 0
+            return "Client Created Room Passwords: Disallowed"
+        else:
+            self.allow_room_passwords = 1
+            return "Client Created Room Passwords: Allowed"
+
+
+    def group_dump(self,k):
+        self.p_lock.acquire()
+        try:
+            print "Group: " + k
+            print "    Name:  %s" % self.groups[k].name
+            print "    Desc:  %s" % self.groups[k].desc
+            print "    Pass:  %s" % self.groups[k].pwd
+            print "    Boot:  %s" % self.groups[k].boot_pwd
+            print "    Moderated:  %s" % self.groups[k].moderated
+            print "    Map:  %s" % self.groups[k].game_map.get_all_xml()
+            print
+        except Exception, e:
+            self.log_msg(str(e))
+        self.p_lock.release()
+
+    #----------------------------------------------------------------
+    #  Player List  -- Added by snowdog 4/03
+    #----------------------------------------------------------------
+    def player_list(self):
+        "display a condensed list of players on the server"
+        self.p_lock.acquire()
+        try:
+            print "------------[ PLAYER LIST ]------------"
+            keys = self.groups.keys()
+            keys.sort(id_compare)
+            for k in keys:
+                groupstring = "Group " + str(k)  + ": " +  self.groups[k].name
+                if self.groups[k].pwd != "":
+                    groupstring += " (Pass: \"" + self.groups[k].pwd + "\" )"
+                print groupstring
+                ids = self.groups[k].get_player_ids()
+                ids.sort(id_compare)
+                for id in ids:
+                    if self.players.has_key(id):
+                        print "  (%s)%s [IP: %s] %s (%s)" % ((self.players[id]).id, (self.players[id]).name, (self.players[id]).ip, (self.players[id]).idle_status(), (self.players[id]).connected_time_string())
+                    else:
+                        self.groups[k].remove_player(id)
+                        print "Bad Player Ref (#" + id + ") in group"
+                if len(ids) > 0: print ""
+            print "--------------------------------------"
+            print "\nStatistics: groups: " + str(len(self.groups)) + "  players: " +  str(len(self.players))
+        except Exception, e:
+            self.log_msg(str(e))
+        self.p_lock.release()
+
+
+    def player_dump(self):
+        self.p_lock.acquire()
+        try:
+            keys = self.groups.keys()
+            for k in keys:
+                print "Group: %s  %s (pass: \"%s\")" % (str(k),self.groups[k].name, self.groups[k].pwd)
+
+                ids = self.groups[k].get_player_ids()
+                for id in ids:
+                    if self.players.has_key(id):
+                        print str(self.players[id])
+                    else:
+                        self.groups[k].remove_player(id)
+                        print "Bad Player Ref (#" + id + ") in group"
+        except Exception, e:
+            self.log_msg(str(e))
+
+        self.p_lock.release()
+
+
+    def update_request(self,newsock,xml_dom):
+        # handle reconnects
+
+        self.log_msg( "update_request() has been called." )
+
+        # get player id
+        id = xml_dom.getAttribute("id")
+        group_id = xml_dom.getAttribute("group_id")
+
+        self.p_lock.acquire()
+        if self.players.has_key(id):
+            self.sendMsg( newsock, self.players[id].toxml("update"), self.players[id].useCompression, self.players[id].compressionType )
+            self.players[id].reset(newsock)
+            self.players[id].clear_timeout()
+            need_new = 0
+        else:
+            need_new = 1
+        self.p_lock.release()
+
+        if need_new:
+            self.new_request(newsock,xml_dom)
+        else:
+            msg = self.groups[group_id].game_map.get_all_xml()
+            self.send(msg,id,group_id)
+
+
+    def new_request(self,newsock,xml_dom,LOBBY_ID='0'):
+        #build client stub
+        props = {}
+        # Don't trust what the client tells us...trust what they connected as!
+        props['ip'] = socket.gethostbyname( newsock.getpeername()[0] )
+
+        try:
+            props['role'] = xml_dom.getAttribute("role")
+        except:
+            props['role'] = "GM"
+
+        props['name'] = xml_dom.getAttribute("name")
+        props['group_id'] = LOBBY_ID
+        props['id'] = str(self.next_player_id)
+        props['version'] = xml_dom.getAttribute("version")
+        props['protocol_version'] = xml_dom.getAttribute("protocol_version")
+        props['client_string'] = xml_dom.getAttribute("client_string")
+        self.next_player_id += 1
+        new_stub = client_stub(self.incoming,newsock,props,self.log_console)
+        if xml_dom.hasAttribute('useCompression'):
+            new_stub.useCompression = True
+
+            if xml_dom.hasAttribute('cmpType'):
+                cmpType = xml_dom.getAttribute('cmpType')
+                if cmpBZ2 and cmpType == 'bz2':
+                    new_stub.compressionType = bz2
+                elif cmpZLIB and cmpType == 'zlib':
+                    new_stub.compressionType = zlib
+                else:
+                    new_stub.compressionType = None
+            else:
+                new_stub.compressionType = bz2
+
+        else:
+            new_stub.useCompression = False
+
+        #update newly create client stub with network logging state
+        new_stub.EnableMessageLogging = self.log_network_messages
+
+        self.sendMsg(newsock, new_stub.toxml("new"), False, None)
+
+        #  try to remove circular refs
+        if xml_dom:
+            xml_dom.unlink()
+
+        # send confirmation
+        data = self.recvMsg(newsock, new_stub.useCompression, new_stub.compressionType)
+        try:
+            xml_dom = parseXml(data)
+            xml_dom = xml_dom._get_documentElement()
+        except Exception, e:
+            print e
+            (remote_host,remote_port) = newsock.getpeername()
+            bad_xml_string =  "Your client sent an illegal message to the server and will be disconnected. "
+            bad_xml_string += "Please report this bug to the development team at:<br /> "
+            bad_xml_string += "<a href=\"http://sourceforge.net/tracker/?group_id=2237&atid=102237\">OpenRPG bugs "
+            bad_xml_string += "(http://sourceforge.net/tracker/?group_id=2237&atid=102237)</a><br />"
+            self.sendMsg( newsock, "<msg to='" + props['id'] + "' from='" + props['id'] + "' group_id='0' />" + bad_xml_string, new_stub.useCompression, new_stub.compressionType)
+
+            time.sleep(2)
+            newsock.close()
+            print "Error in parse found from " + str(remote_host) + ".  Disconnected."
+            print "  Offending data(" + str(len(data)) + "bytes)=" + data
+            print "Exception=" + str(e)
+
+            if xml_dom:
+                xml_dom.unlink()
+            return
+
+        #start threads and store player
+
+        allowed = 1
+        version_string = ""
+
+        if ((props['protocol_version'] != PROTOCOL_VERSION) and self.validate_protocol):
+            version_string = "Sorry, this server can't handle your client version. (Protocol mismatch)<br />"
+            allowed = 0
+
+        if not self.checkClientVersion(props['version']):
+            version_string = "Sorry, your client is out of date. <br />"
+            version_string += "This server requires your client be version " + self.minClientVersion + " or higher to connect.<br />"
+            allowed = 0
+
+        if not allowed:
+            version_string += '  Please go to <a href="http://openrpg.digitalxero.net">http://openrpg.digitalxero.net</a> to find a compatible client.<br />'
+            version_string += "If you can't find a compatible client on the website, chances are that the server is running an unreleased development version for testing purposes.<br />"
+
+            self.sendMsg( newsock, "<msg to='" + props['id'] + "' from='0' group_id='0' />" + version_string, new_stub.useCompression, new_stub.compressionType)
+            #  Give messages time to flow
+            time.sleep(1)
+            self.log_msg("Connection terminating due to version incompatibility with client (ver: " + props['version'] + "  protocol: " + props['protocol_version'] + ")" )
+            newsock.close()
+            if xml_dom:
+                xml_dom.unlink()
+            return None
+
+        ip = props['ip']
+        if self.ban_list.has_key(ip):
+            banmsg = "You have been banned from this server.<br />"
+            cmsg = "Banned Client: (" + str(props['id']) + ") " + str(props['name']) + " [" + str(props['ip']) + "]"
+            self.log_msg(cmsg)
+            allowed = 0
+
+            self.sendMsg( newsock, "<msg to='" + props['id'] + "' from='0' group_id='0' />" + banmsg, new_stub.useCompression, new_stub.compressionType)
+            #  Give messages time to flow
+            time.sleep(1)
+            newsock.close()
+            if xml_dom:
+                xml_dom.unlink()
+            return None
+
+        #---- Connection order changed by Snowdog 1/05
+        #---- Attempt to register player and send group data
+        #---- before displaying lobby message
+        #---- Does not solve the Blackhole bug but under some conditions may
+        #---- allow for a graceful server response. -SD
+
+        #---- changed method of sending group names to user 8/05
+        #---- black hole bug causes the group information to not be sent
+        #---- to clients. Not sure why the group messages were being sent to the
+        #---- incomming message queue, when they should be sent directly to user
+        #---- Does not solve the black hole bug totally -SD
+
+        try:
+            if xml_dom.getAttribute("id") == props['id']:
+                new_stub.initialize_threads()
+                self.p_lock.acquire()
+                self.players[props['id']] = new_stub
+                self.groups[LOBBY_ID].add_player(props['id']) #always add to lobby on connection.
+                self.send_group_list(props['id'])
+                self.send_player_list(props['id'],LOBBY_ID)
+                self.p_lock.release()
+
+                msg = self.groups[LOBBY_ID].game_map.get_all_xml()
+                self.send(msg,props['id'],LOBBY_ID)
+                self.send_to_group(props['id'],LOBBY_ID,self.players[props['id']].toxml('new'))
+                self.return_room_roles(props['id'],LOBBY_ID)
+
+                # Re-initialize the role for this player incase they came from a different server
+                self.handle_role("set",props['id'], "GM",self.groups[LOBBY_ID].boot_pwd, LOBBY_ID)
+
+                cmsg = "Client Connect: (" + str(props['id']) + ") " + str(props['name']) + " [" + str(props['ip']) + "]"
+                self.log_msg(cmsg)
+
+                #  If already registered then re-register, thereby updating the Meta
+                #    on the number of players
+                if self.be_registered:
+                    self.register()
+        except:
+            traceback.print_exc()
+
+            #something didn't go right. Notify client and drop the connection
+            err_string = "<center>"
+            err_string +=  "<hr><b>The server has encountered an error while processing your connection request.</b><hr>"
+            err_string += "<br /><i>You are being disconnected from the server.</i><br />"
+            err_string += "This error may represent a problem with the server. If you continue to get this message "
+            err_string += "please contact the servers administrator to correct the issue.</center> "
+            self.sendMsg( newsock, "<msg to='" + props['id'] + "' from='" + props['id'] + "' group_id='0' />" + err_string, new_stub.useCompression, new_stub.compressionType )
+            time.sleep(2)
+            newsock.close()
+
+
+        #  Display the lobby message
+        self.SendLobbyMessage(newsock,props['id'])
+
+        if xml_dom:
+            xml_dom.unlink()
+
+
+    def checkClientVersion(self, clientversion):
+        minv = self.minClientVersion.split('.')
+        cver = clientversion.split('.')
+        for i in xrange(min(len(minv),len(cver))):
+            w=max(len(minv[i]),len(cver[i]))
+            v1=minv[i].rjust(w);
+            v2=cver[i].rjust(w);
+            if v1<v2:
+                return 1
+            if v1>v2:
+                return 0
+
+        if len(minv)>len(cver):
+            return 0
+        return 1
+
+
+
+    def SendLobbyMessage(self, socket, player_id):
+        #######################################################################
+        #  Display the lobby message
+        #  prepend this server's version string to the the lobby message
+        try:
+            lobbyMsg = "You have connected to an <a href=\"http://www.openrpg.com\">OpenRPG</a> server, version '" + VERSION + "'"
+
+            # See if we have a server name to report!
+
+            if len(self.serverName):
+                lobbyMsg += ", named '" + self.serverName + "'."
+
+            else:
+                lobbyMsg += "."
+
+            # Add extra line spacing
+            lobbyMsg += "\n\n"
+
+            try:
+                self.validate.config_file("LobbyMessage.html","default_LobbyMessage.html")
+            except:
+                pass
+            else:
+                open_msg = open( self.userPath + "LobbyMessage.html", "r" )
+                lobbyMsg += open_msg.read()
+                open_msg.close()
+
+            # Send the server's lobby message to the client no matter what
+            self.sendMsg(socket, "<msg to='" + player_id + "' from='0' group_id='0' />" + lobbyMsg, self.players[player_id].useCompression, self.players[player_id].compressionType)
+            if self.sendLobbySound:
+                self.sendMsg(socket, '<sound url="' + self.lobbySound + '" group_id="0" from="0" loop="True" />', self.players[player_id].useCompression, self.players[player_id].compressionType)
+            return
+        except:
+            traceback.print_exc()
+        #  End of lobby message code
+        #######################################################################
+
+
+    def listenAcceptThread(self,arg):
+        #  Set up the socket to listen on.
+        try:
+            self.log_msg("\nlisten thread running...")
+            adder = ""
+            if self.server_address is not None:
+                adder = self.server_address
+            self.listen_sock.bind(('', self.server_port))
+            self.listen_sock.listen(5)
+
+        except Exception, e:
+            self.log_msg(("Error binding request socket!", e))
+            self.alive = 0
+
+
+        while self.alive:
+
+            #  Block on the socket waiting for a new connection
+            try:
+                (newsock, addr) = self.listen_sock.accept()
+                ## self.log_msg("New connection from " + str(addr)+ ". Interfacing with server...")
+
+                # Now that we've accepted a new connection, we must immediately spawn a new
+                # thread to handle it...otherwise we run the risk of having a DoS shoved into
+                # our face!  :O  After words, this thread is dead ready for another connection
+                # accept to come in.
+                thread.start_new_thread(self.acceptedNewConnectionThread, ( newsock, addr ))
+
+            except:
+                print "The following exception caught accepting new connection:"
+                traceback.print_exc()
+
+        #  At this point, we're done and cleaning up.
+        self.log_msg("server socket listening thread exiting...")
+        self.listen_event.set()
+
+
+
+    def acceptedNewConnectionThread( self, newsock, addr ):
+        """Once a new connection comes in and is accepted, this thread starts up to handle it."""
+
+        # Initialize xml_dom
+        xml_dom = None
+        data = None
+
+        # get client info and send othe client info
+        # If this receive fails, this thread should exit without even attempting to process it
+        self.log_msg("Connection from " + str(addr) + " has been accepted.  Waiting for data...")
+
+        data = self.recvMsg( newsock )
+
+        if data=="" or data == None:
+            self.log_msg("Connection from " + str(addr) + " failed. Closing connection.")
+            try:
+                newsock.close()
+            except Exception, e:
+                self.log_msg( str(e) )
+                print str(e)
+            return #returning causes connection thread instance to terminate
+
+
+        if data == "<system/>":
+            try:
+                newsock.close()
+            except:
+                pass
+            return #returning causes connection thread instance to terminate
+
+        #  Clear out the xml_dom in preparation for new stuff, if necessary
+        try:
+            if xml_dom:
+                xml_dom.unlink()
+
+        except:
+            self.log_msg( "The following exception caught unlinking xml_dom:")
+            self.log_msg("Continuing")
+
+            try:
+                newsock.close()
+            except:
+                pass
+            return #returning causes connection thread instance to terminate
+
+        #  Parse the XML received from the connecting client
+        try:
+            xml_dom = parseXml(data)
+            xml_dom = xml_dom._get_documentElement()
+
+        except:
+            try:
+                newsock.close()
+            except:
+                pass
+            self.log_msg( "Error in parse found from " + str(addr) + ".  Disconnected.")
+            self.log_msg("  Offending data(" + str(len(data)) + "bytes)=" + data)
+            self.log_msg( "Exception:")
+            traceback.print_exc()
+            return #returning causes connection thread instance to terminate
+
+        #  Determine the correct action and execute it
+        try:
+            # get action
+            action = xml_dom.getAttribute("action")
+
+            # Figure out what type of connection we have going on now
+            if action == "new":
+                self.new_request(newsock,xml_dom)
+
+            elif action == "update":
+                self.update_request(newsock,xml_dom)
+
+            else:
+                self.log_msg("Unknown Join Request!")
+
+        except Exception, e:
+            print "The following  message: " + str(data)
+            print "from " + str(addr) + " created the following exception: "
+            traceback.print_exc()
+            return #returning causes connection thread instance to terminate
+
+        #  Again attempt to clean out DOM stuff
+        try:
+            if xml_dom:
+                xml_dom.unlink()
+        except:
+            print "The following exception caught unlinking xml_dom:"
+            traceback.print_exc()
+            return #returning causes connection thread instance to terminate
+
+
+
+    #========================================================
+    #
+    #   Message_handler
+    #
+    #========================================================
+    #
+    # Changed thread organization from one continuous parsing/handling thread
+    # to multiple expiring parsing/handling threads to improve server performance
+    # and player load capacity -- Snowdog 3/04
+
+    def message_handler(self,arg):
+        xml_dom = None
+        self.log_msg( "message handler thread running..." )
+        while self.alive:
+            data = None
+            try:
+                data=self.incoming.get(0)
+            except Queue.Empty:
+                time.sleep(0.5) #sleep 1/2 second
+                continue
+
+            bytes = len(data)
+            if bytes <= 0:
+                continue
+            try:
+                thread.start_new_thread(self.parse_incoming_dom,(str(data),))
+                #data has been passed... unlink from the variable references
+                #so data in passed objects doesn't change (python passes by reference)
+                del data
+                data = None
+            except Exception, e:
+                self.log_msg(str(e))
+                if xml_dom: xml_dom.unlink()
+        if xml_dom: xml_dom.unlink()
+        self.log_msg("message handler thread exiting...")
+        self.incoming_event.set()
+
+    def parse_incoming_dom(self,data):
+        end = data.find(">") #locate end of first element of message
+        head = data[:end+1]
+        #self.log_msg(head)
+        xml_dom = None
+        try:
+            xml_dom = parseXml(head)
+            xml_dom = xml_dom._get_documentElement()
+            self.message_action(xml_dom,data)
+
+        except Exception, e:
+            print "Error in parse of inbound message. Ignoring message."
+            print "  Offending data(" + str(len(data)) + "bytes)=" + data
+            print "Exception=" + str(e)
+
+        if xml_dom: xml_dom.unlink()
+
+
+    def message_action(self, xml_dom, data):
+        tag_name = xml_dom._get_tagName()
+        if self.svrcmds.has_key(tag_name):
+            self.svrcmds[tag_name]['function'](xml_dom,data)
+        else:
+            raise Exception, "Not a valid header!"
+        #Message Action thread expires and closes here.
+        return
+
+
+    def do_alter(self, xml_dom, data):
+        target = xml_dom.getAttribute("key")
+        value = xml_dom.getAttribute("val")
+        player = xml_dom.getAttribute("plr")
+        group_id = xml_dom.getAttribute("gid")
+        boot_pwd = xml_dom.getAttribute("bpw")
+        actual_boot_pwd = self.groups[group_id].boot_pwd
+
+        if self.allow_room_passwords == 0:
+            msg ="<msg to='" + player + "' from='0' group_id='0' /> Room passwords have been disabled by the server administrator."
+            self.players[player].outbox.put(msg)
+            return
+        elif boot_pwd == actual_boot_pwd:
+            if target == "pwd":
+                lmessage = "Room password changed to from \"" + self.groups[group_id].pwd + "\" to \"" + value  + "\" by " + player
+                self.groups[group_id].pwd = value
+                msg ="<msg to='" + player + "' from='0' group_id='0' /> Room password changed to \"" +  value + "\"."
+                self.players[player].outbox.put(msg)
+                self.log_msg(lmessage)
+                self.send_to_all('0',self.groups[group_id].toxml('update'))
+            elif target == "name":
+                # Check for & in name.  We want to allow this because of its common
+                # use in d&d games
+                result = self.change_group_name(group_id,value,player)
+                msg ="<msg to='" + player + "' from='0' group_id='0' />" + result
+                self.players[player].outbox.put(msg)
+        else:
+            msg ="<msg to='" + player + "' from='0' group_id='0'>Invalid Administrator Password."
+            self.players[player].outbox.put(msg)
+
+
+    def do_role(self, xml_dom, data):
+        role = ""
+        boot_pwd = ""
+        act = xml_dom.getAttribute("action")
+        player = xml_dom.getAttribute("player")
+        group_id = xml_dom.getAttribute("group_id")
+        if act == "set":
+            role = xml_dom.getAttribute("role")
+            boot_pwd = xml_dom.getAttribute("boot_pwd")
+        xml_dom.unlink()
+        if group_id != "0":
+            self.handle_role(act, player, role, boot_pwd, group_id)
+            self.log_msg(("role", (player, role)))
+
+    def do_ping(self, xml_dom, data):
+        player = xml_dom.getAttribute("player")
+        group_id = xml_dom.getAttribute("group_id")
+        sent_time = ""
+        msg = ""
+        try:
+            sent_time = xml_dom.getAttribute("time")
+        except:
+            pass
+
+        if sent_time != "":
+            #because a time was sent return a ping response
+            msg ="<ping time='" + str(sent_time) + "' />"
+        else:
+            msg ="<msg to='" + player + "' from='" + player + "' group_id='" + group_id + "'><font color='#FF0000'>PONG!?!</font>"
+
+        self.players[player].outbox.put(msg)
+        xml_dom.unlink()
+
+    def do_system(self, xml_dom, data):
+        pass
+
+    def moderate_group(self,xml_dom,data):
+        try:
+            action = xml_dom.getAttribute("action")
+            from_id = xml_dom.getAttribute("from")
+            if xml_dom.hasAttribute("pwd"):
+                pwd=xml_dom.getAttribute("pwd")
+            else:
+                pwd=""
+            group_id=self.players[from_id].group_id
+
+            if action == "list":
+                if (self.groups[group_id].moderated):
+                    msg = ""
+                    for i in self.groups[group_id].voice.keys():
+                        if msg != "":
+                            msg +=", "
+                        if self.players.has_key(i):
+                            msg += '('+i+') '+self.players[i].name
+                        else:
+                            del self.groups[group_id].voice[i]
+                    if (msg != ""):
+                        msg = "The following users may speak in this room: " + msg
+                    else:
+                        msg = "No people are currently in this room with the ability to chat"
+                    self.players[from_id].self_message(msg)
+                else:
+                    self.players[from_id].self_message("This room is currently unmoderated")
+            elif action == "enable":
+                if not self.groups[group_id].check_boot_pwd(pwd):
+                    self.players[from_id].self_message("Failed - incorrect admin password")
+                    return
+                self.groups[group_id].moderated = 1
+                self.players[from_id].self_message("This channel is now moderated")
+            elif action == "disable":
+                if not self.groups[group_id].check_boot_pwd(pwd):
+                    self.players[from_id].self_message("Failed - incorrect admin password")
+                    return
+                self.groups[group_id].moderated = 0
+                self.players[from_id].self_message("This channel is now unmoderated")
+            elif action == "addvoice":
+                if not self.groups[group_id].check_boot_pwd(pwd):
+                    self.players[from_id].self_message("Failed - incorrect admin password")
+                    return
+                users = xml_dom.getAttribute("users").split(',')
+                for i in users:
+                    self.groups[group_id].voice[i.strip()]=1
+            elif action == "delvoice":
+                if not self.groups[group_id].check_boot_pwd(pwd):
+                    self.players[from_id].self_message("Failed - incorrect admin password")
+                    return
+                users = xml_dom.getAttribute("users").split(',')
+                for i in users:
+                    if self.groups[group_id].voice.has_key(i.strip()):
+                        del self.groups[group_id].voice[i.strip()]
+            else:
+                print "Bad input: " + data
+
+        except Exception,e:
+            self.log_msg(str(e))
+
+
+
+
+    def join_group(self,xml_dom,data):
+        try:
+            from_id = xml_dom.getAttribute("from")
+            pwd = xml_dom.getAttribute("pwd")
+            group_id = xml_dom.getAttribute("group_id")
+            ver = self.players[from_id].version
+            allowed = 1
+
+            if not self.groups[group_id].check_version(ver):
+                allowed = 0
+                msg = 'failed - invalid client version ('+self.groups[group_id].minVersion+' or later required)'
+
+            if not self.groups[group_id].check_pwd(pwd):
+                allowed = 0
+
+                #tell the clients password manager the password failed -- SD 8/03
+                pm = "<password signal=\"fail\" type=\"room\" id=\"" +  group_id  + "\" data=\"\"/>"
+                self.players[from_id].outbox.put(pm)
+
+                msg = 'failed - incorrect room password'
+
+            if not allowed:
+                self.players[from_id].self_message(msg)
+                #the following line makes sure that their role is reset to normal,
+                #since it is briefly set to lurker when they even TRY to change
+                #rooms
+                msg = "<role action=\"update\" id=\"" + from_id  + "\" role=\"" + self.players[from_id].role + "\" />"
+                self.players[from_id].outbox.put(msg)
+                return
+
+            #move the player into their new group.
+            self.move_player(from_id, group_id)
+
+        except Exception, e:
+            self.log_msg(str(e))
+
+
+
+
+    #----------------------------------------------------------------------------
+    # move_player function -- added by Snowdog 4/03
+    #
+    # Split join_group function in half. separating the player validation checks
+    # from the actual group changing code. Done primarily to impliment
+    # boot-from-room-to-lobby behavior in the server.
+
+    def move_player(self, from_id, group_id ):
+        "move a player from one group to another"
+        try:
+            try:
+                if group_id == "0":
+                    self.players[from_id].role = "GM"
+                else:
+                    self.players[from_id].role = "Lurker"
+            except Exception, e:
+                print "exception in move_player() "
+                traceback.print_exc()
+
+            old_group_id = self.players[from_id].change_group(group_id,self.groups)
+            self.send_to_group(from_id,old_group_id,self.players[from_id].toxml('del'))
+            self.send_to_group(from_id,group_id,self.players[from_id].toxml('new'))
+            self.check_group(from_id, old_group_id)
+
+            # Here, if we have a group specific lobby message to send, push it on
+            # out the door!  Make it put the message then announce the player...just
+            # like in the lobby during a new connection.
+            # -- only do this check if the room id is within range of known persistent id thresholds
+            #also goes ahead if there is a defaultRoomMessage --akoman
+
+            if self.isPersistentRoom(group_id) or self.defaultMessageFile != None:
+                try:
+                    if self.groups[group_id].messageFile[:4] == 'http':
+                        data = urllib.urlretrieve(self.groups[group_id].messageFile)
+                        roomMsgFile = open(data[0])
+                    else:
+                        roomMsgFile = open(self.groups[group_id].messageFile, "r")
+                    roomMsg = roomMsgFile.read()
+                    roomMsgFile.close()
+                    urllib.urlcleanup()
+
+                except Exception, e:
+                    roomMsg = ""
+                    self.log_msg(str(e))
+
+                # Spit that darn message out now!
+                self.players[from_id].outbox.put("<msg to='" + from_id + "' from='0' group_id='" + group_id + "' />" + roomMsg)
+
+            if self.sendLobbySound and group_id == '0':
+                self.players[from_id].outbox.put('<sound url="' + self.lobbySound + '" group_id="0" from="0" loop="True" />')
+
+            # Now, tell everyone that we've arrived
+            self.send_to_all('0', self.groups[group_id].toxml('update'))
+
+            # this line sends a handle role message to change the players role
+            self.send_player_list(from_id,group_id)
+
+            #notify user about others in the room
+            self.return_room_roles(from_id,group_id)
+            self.log_msg(("join_group", (from_id, group_id)))
+            self.handle_role("set", from_id, self.players[from_id].role, self.groups[group_id].boot_pwd, group_id)
+
+        except Exception, e:
+            self.log_msg(str(e))
+
+        thread.start_new_thread(self.registerRooms,(0,))
+
+    def return_room_roles(self,from_id,group_id):
+        for m in self.players.keys():
+            if self.players[m].group_id == group_id:
+                msg = "<role action=\"update\" id=\"" + self.players[m].id  + "\" role=\"" + self.players[m].role + "\" />"
+                self.players[from_id].outbox.put(msg)
+
+
+    # This is pretty much the same thing as the create_group method, however,
+    # it's much more generic whereas the create_group method is tied to a specific
+    # xml message.  Ack!  This version simply creates the groups, it does not
+    # send them to players.  Also note, both these methods have race
+    # conditions written all over them.  Ack! Ack!
+    def new_group( self, name, pwd, boot, minVersion, mapFile, messageFile, persist = 0, moderated=0 ):
+        group_id = str( self.next_group_id )
+        self.next_group_id += 1
+
+        self.groups[group_id] = game_group( group_id, name, pwd, "", boot, minVersion, mapFile, messageFile, persist )
+        self.groups[group_id].moderated = moderated
+        ins = ""
+        if persist !=0: ins="Persistant "
+        lmsg = "Creating " + ins + "Group... (" + str(group_id) + ") " + str(name)
+        self.log_msg( lmsg )
+
+
+    def change_group_name(self,gid,name,pid):
+        "Change the name of a group"
+        # Check for & in name.  We want to allow this because of its common
+        # use in d&d games.
+        try:
+            loc = name.find("&")
+            oldloc = 0
+            while loc > -1:
+                loc = name.find("&",oldloc)
+                if loc > -1:
+                    b = name[:loc]
+                    e = name[loc+1:]
+                    value = b + "&amp;" + e
+                    oldloc = loc+1
+
+            loc = name.find("'")
+            oldloc = 0
+            while loc > -1:
+                loc = name.find("'",oldloc)
+                if loc > -1:
+                    b = name[:loc]
+                    e = name[loc+1:]
+                    name = b + "&#39;" + e
+                    oldloc = loc+1
+
+            loc = name.find('"')
+            oldloc = 0
+            while loc > -1:
+                loc = name.find('"',oldloc)
+                if loc > -1:
+                    b = name[:loc]
+                    e = name[loc+1:]
+                    name = b + "&quot;" + e
+                    oldloc = loc+1
+
+            oldroomname = self.groups[gid].name
+            self.groups[gid].name = str(name)
+            lmessage = "Room name changed to from \"" + oldroomname + "\" to \"" + name + "\""
+            self.log_msg(lmessage  + " by " + str(pid) )
+            self.send_to_all('0',self.groups[gid].toxml('update'))
+            return lmessage
+        except:
+            return "An error occured during rename of room!"
+
+        thread.start_new_thread(self.registerRooms,(0,))
+
+
+
+    def create_group(self,xml_dom,data):
+        try:
+            from_id = xml_dom.getAttribute("from")
+            pwd = xml_dom.getAttribute("pwd")
+            name = xml_dom.getAttribute("name")
+            boot_pwd = xml_dom.getAttribute("boot_pwd")
+            minVersion = xml_dom.getAttribute("min_version")
+            #added var reassign -- akoman
+            messageFile = self.defaultMessageFile
+
+            # see if passwords are allowed on this server and null password if not
+            if self.allow_room_passwords != 1: pwd = ""
+
+
+            #
+            # Check for & in name.  We want to allow this because of its common
+            # use in d&d games.
+
+            loc = name.find("&")
+            oldloc = 0
+            while loc > -1:
+                loc = name.find("&",oldloc)
+                if loc > -1:
+                    b = name[:loc]
+                    e = name[loc+1:]
+                    name = b + "&amp;" + e
+                    oldloc = loc+1
+
+            loc = name.find("'")
+            oldloc = 0
+            while loc > -1:
+                loc = name.find("'",oldloc)
+                if loc > -1:
+                    b = name[:loc]
+                    e = name[loc+1:]
+                    name = b + "&#39;" + e
+                    oldloc = loc+1
+
+            loc = name.find('"')
+            oldloc = 0
+            while loc > -1:
+                loc = name.find('"',oldloc)
+                if loc > -1:
+                    b = name[:loc]
+                    e = name[loc+1:]
+                    name = b + "&quot;" + e
+                    oldloc = loc+1
+
+
+            group_id = str(self.next_group_id)
+            self.next_group_id += 1
+            self.groups[group_id] = game_group(group_id,name,pwd,"",boot_pwd, minVersion, None, messageFile )
+            self.groups[group_id].voice[from_id]=1
+            self.players[from_id].outbox.put(self.groups[group_id].toxml('new'))
+            old_group_id = self.players[from_id].change_group(group_id,self.groups)
+            self.send_to_group(from_id,old_group_id,self.players[from_id].toxml('del'))
+            self.check_group(from_id, old_group_id)
+            self.send_to_all(from_id,self.groups[group_id].toxml('new'))
+            self.send_to_all('0',self.groups[group_id].toxml('update'))
+            self.handle_role("set",from_id,"GM",boot_pwd, group_id)
+            lmsg = "Creating Group... (" + str(group_id) + ") " + str(name)
+            self.log_msg( lmsg )
+            jmsg = "moving to room " + str(group_id) + "."
+            self.log_msg( jmsg )
+            #even creators of the room should see the HTML --akoman
+            #edit: jan10/03 - was placed in the except statement. Silly me.
+            if self.defaultMessageFile != None:
+                if self.defaultMessageFile[:4] == 'http':
+                    data = urllib.urlretrieve(self.defaultMessageFile)
+                    open_msg = open(data[0])
+                    urllib.urlcleanup()
+                else:
+                    open_msg = open( self.defaultMessageFile, "r" )
+
+                roomMsg = open_msg.read()
+                open_msg.close()
+                # Send the rooms message to the client no matter what
+                self.players[from_id].outbox.put( "<msg to='" + from_id + "' from='0' group_id='" + group_id + "' />" + roomMsg )
+
+        except Exception, e:
+            self.log_msg( "Exception: create_group(): " + str(e))
+
+        thread.start_new_thread(self.registerRooms,(0,))
+
+
+    def check_group(self, from_id, group_id):
+        try:
+            if group_id not in self.groups: return
+            if group_id == '0':
+                self.send_to_all("0",self.groups[group_id].toxml('update'))
+                return #never remove lobby *sanity check*
+            if not self.isPersistentRoom(group_id)  and self.groups[group_id].get_num_players() == 0:
+                self.send_to_all("0",self.groups[group_id].toxml('del'))
+                del self.groups[group_id]
+                self.log_msg(("delete_group", (from_id, group_id)))
+
+            else:
+                self.send_to_all("0",self.groups[group_id].toxml('update'))
+
+            #The register Rooms thread
+            thread.start_new_thread(self.registerRooms,(0,))
+
+        except Exception, e:
+            self.log_msg(str(e))
+
+    def del_player(self,id,group_id):
+        try:
+            dmsg = "Client Disconnect: (" + str(id) + ") " + str(self.players[id].name)
+            self.players[id].disconnect()
+            self.groups[group_id].remove_player(id)
+            del self.players[id]
+            self.log_msg(dmsg)
+
+
+            #  If already registered then re-register, thereby updating the Meta
+            #    on the number of players
+            #  Note:  Upon server shutdown, the server is first unregistered, so
+            #           this code won't be repeated for each player being deleted.
+            if self.be_registered:
+                self.register()
+
+
+        except Exception, e:
+            self.log_msg(str(e))
+
+        self.log_msg("Explicit garbage collection shows %s undeletable items." % str(gc.collect()))
+
+
+
+    def incoming_player_handler(self,xml_dom,data):
+        id = xml_dom.getAttribute("id")
+        act = xml_dom.getAttribute("action")
+        #group_id = xml_dom.getAttribute("group_id")
+        group_id = self.players[id].group_id
+        ip = self.players[id].ip
+        self.log_msg("Player with IP: " + str(ip) + " joined.")
+
+        ServerPlugins.setPlayer(self.players[id])
+
+        self.send_to_group(id,group_id,data)
+        if act=="new":
+            try:
+                self.send_player_list(id,group_id)
+                self.send_group_list(id)
+            except Exception, e:
+                traceback.print_exc()
+        elif act=="del":
+            #print "del player"
+            self.del_player(id,group_id)
+            self.check_group(id, group_id)
+        elif act=="update":
+            self.players[id].take_dom(xml_dom)
+            self.log_msg(("update", {"id": id,
+                                     "name": xml_dom.getAttribute("name"),
+                                     "status": xml_dom.getAttribute("status"),
+                                     "role": xml_dom.getAttribute("role"),
+				     "ip":  str(ip),
+				     "group": xml_dom.getAttribute("group_id"),
+				     "room": xml_dom.getAttribute("name"),
+				     "boot": xml_dom.getAttribute("rm_boot"),
+				     "version": xml_dom.getAttribute("version"),
+				     "ping": xml_dom.getAttribute("time") \
+                                     }))
+
+
+    def strip_cheat_roll(self, string):
+        try:
+            cheat_regex = re.compile('&amp;#91;(.*?)&amp;#93;')
+            string = cheat_regex.sub( r'[ ' + self.cheat_msg + " \\1 " + self.cheat_msg + ' ]', string)
+        except:
+            pass
+        return string
+
+    def strip_body_tags(self, string):
+        try:
+            bodytag_regex = re.compile('&lt;\/?body(.*?)&gt;')
+            string = bodytag_regex.sub('', string)
+        except:
+            pass
+        return string
+
+    def msgTooLong(self, length):
+        if length > self.maxSendSize and not self.maxSendSize == 0:
+            return True
+        return False
+
+    def incoming_msg_handler(self,xml_dom,data):
+        xml_dom, data = ServerPlugins.preParseIncoming(xml_dom, data)
+
+        to_id = xml_dom.getAttribute("to")
+        from_id = xml_dom.getAttribute("from")
+        group_id = xml_dom.getAttribute("group_id")
+        end = data.find(">")
+        msg = data[end+1:]
+
+        if from_id == "0" or len(from_id) == 0:
+            print "WARNING!! Message received with an invalid from_id.  Message dropped."
+            return None
+
+        #
+        # check for < body to prevent someone from changing the background
+        #
+
+        data = self.strip_body_tags(data)
+
+        #
+        # check for &#91 and &#93  codes which are often used to cheat with dice.
+        #
+        if self.players[from_id].role != "GM":
+            data = self.strip_cheat_roll(data)
+
+        if group_id == '0' and self.msgTooLong(len(msg) and msg[:5] == '<chat'):
+            self.send("Your message was too long, break it up into smaller parts please", from_id, group_id)
+            self.log_msg('Message Blocked from Player: ' + self.players[from_id].name + ' attempting to send a message longer then ' + str(self.maxSendSize))
+            return
+
+        if msg[:4] == '<map':
+            if group_id == '0':
+                #attempt to change lobby map. Illegal operation.
+                self.players[from_id].self_message('The lobby map may not be altered.')
+            elif to_id.lower() == 'all':
+                #valid map for all players that is not the lobby.
+                self.send_to_group(from_id,group_id,data)
+                self.groups[group_id].game_map.init_from_xml(msg)
+            else:
+                #attempting to send map to specific individuals which is not supported.
+                self.players[from_id].self_message('Invalid map message. Message not sent to others.')
+
+        elif msg[:6] == '<boot ':
+            self.handle_boot(from_id,to_id,group_id,msg)
+
+        else:
+            if to_id == 'all':
+                if self.groups[group_id].moderated and not self.groups[group_id].voice.has_key(from_id):
+                    self.players[from_id].self_message('This room is moderated - message not sent to others')
+                else:
+                    self.send_to_group(from_id,group_id,data)
+            else:
+                self.players[to_id].outbox.put(data)
+
+        self.check_group_members(group_id)
+        return
+
+    def sound_msg_handler(self, xml_dom, data):
+        from_id = xml_dom.getAttribute("from")
+        group_id = xml_dom.getAttribute("group_id")
+        if group_id != 0:
+            self.send_to_group(from_id, group_id, data)
+
+    def plugin_msg_handler(self,xml_dom,data):
+        to_id = xml_dom.getAttribute("to")
+        from_id = xml_dom.getAttribute("from")
+        group_id = xml_dom.getAttribute("group_id")
+        end = data.find(">")
+        msg = data[end+1:]
+
+        if from_id == "0" or len(from_id) == 0:
+            print "WARNING!! Message received with an invalid from_id.  Message dropped."
+            return None
+
+
+        if to_id == 'all':
+            if self.groups[group_id].moderated and not self.groups[group_id].voice.has_key(from_id):
+                self.players[from_id].self_message('This room is moderated - message not sent to others')
+            else:
+                self.send_to_group(from_id, group_id, msg)
+        else:
+            self.players[to_id].outbox.put(msg)
+
+        self.check_group_members(group_id)
+        return
+
+    def handle_role(self, act, player, role, given_boot_pwd, group_id):
+        if act == "display":
+            msg = "<msg to=\"" + player + "\" from=\"0\" group_id=\"" + group_id + "\" />"
+            msg += "Displaying Roles<br /><br /><u>Role</u>&nbsp&nbsp&nbsp<u>Player</u><br />"
+            keys = self.players.keys()
+            for m in keys:
+                if self.players[m].group_id == group_id:
+                    msg += self.players[m].role + " " + self.players[m].name + "<br />"
+            self.send(msg,player,group_id)
+        elif act == "set":
+            try:
+                actual_boot_pwd = self.groups[group_id].boot_pwd
+                if self.players[player].group_id == group_id:
+                    if actual_boot_pwd == given_boot_pwd:
+                        self.log_msg( "Administrator passwords match -- changing role")
+
+                        #  Send update role event to all
+                        msg = "<role action=\"update\" id=\"" + player  + "\" role=\"" + role + "\" />"
+                        self.send_to_group("0", group_id, msg)
+                        self.players[player].role = role
+                        if (role.lower() == "gm" or role.lower() == "player"):
+                            self.groups[group_id].voice[player]=1
+                    else:
+                        #tell the clients password manager the password failed -- SD 8/03
+                        pm = "<password signal=\"fail\" type=\"admin\" id=\"" + group_id + "\" data=\"\"/>"
+                        self.players[player].outbox.put(pm)
+                        self.log_msg( "Administrator passwords did not match")
+            except Exception, e:
+                print e
+                print "Error executing the role change"
+                print "due to the following exception:"
+                traceback.print_exc()
+                print "Ignoring boot message"
+
+    def handle_boot(self,from_id,to_id,group_id,msg):
+        xml_dom = None
+        try:
+            given_boot_pwd = None
+            try:
+                xml_dom = parseXml(msg)
+                xml_dom = xml_dom._get_documentElement()
+                given_boot_pwd = xml_dom.getAttribute("boot_pwd")
+
+            except:
+                print "Error in parse of boot message, Ignoring."
+                print "Exception: "
+                traceback.print_exc()
+
+            try:
+                actual_boot_pwd = self.groups[group_id].boot_pwd
+                server_admin_pwd = self.groups["0"].boot_pwd
+
+                self.log_msg("Actual boot pwd = " + actual_boot_pwd)
+                self.log_msg("Given boot pwd = " + given_boot_pwd)
+
+                if self.players[to_id].group_id == group_id:
+
+                    ### ---CHANGES BY SNOWDOG 4/03 ---
+                    ### added boot to lobby code.
+                    ### if boot comes from lobby dump player from the server
+                    ### any user in-room boot will dump to lobby instead
+                    if given_boot_pwd == server_admin_pwd:
+                        # Send a message to everyone in the room, letting them know someone has been booted
+                        boot_msg = "<msg to='all' from='%s' group_id='%s'/><font color='#FF0000'>Booting '(%s) %s' from server...</font>" % (from_id, group_id, to_id, self.players[to_id].name)
+
+                        self.log_msg("boot_msg:" + boot_msg)
+
+                        self.send_to_group( "0", group_id, boot_msg )
+                        time.sleep( 1 )
+
+                        self.log_msg("Booting player " + str(to_id) + " from server.")
+
+                        #  Send delete player event to all
+                        self.send_to_group("0",group_id,self.players[to_id].toxml("del"))
+
+                        #  Remove the player from local data structures
+                        self.del_player(to_id,group_id)
+
+                        #  Refresh the group data
+                        self.check_group(to_id, group_id)
+
+                    elif actual_boot_pwd == given_boot_pwd:
+                        # Send a message to everyone in the room, letting them know someone has been booted
+                        boot_msg = "<msg to='all' from='%s' group_id='%s'/><font color='#FF0000'>Booting '(%s) %s' from room...</font>" % (from_id, group_id, to_id, self.players[to_id].name)
+
+                        self.log_msg("boot_msg:" + boot_msg)
+
+                        self.send_to_group( "0", group_id, boot_msg )
+                        time.sleep( 1 )
+
+                        #dump player into the lobby
+                        self.move_player(to_id,"0")
+
+                        #  Refresh the group data
+                        self.check_group(to_id, group_id)
+                    else:
+                        #tell the clients password manager the password failed -- SD 8/03
+                        pm = "<password signal=\"fail\" type=\"admin\" id=\"" + group_id + "\" data=\"\"/>"
+                        self.players[from_id].outbox.put(pm)
+                        print "boot passwords did not match"
+
+            except Exception, e:
+                traceback.print_exc()
+                self.log_msg('Exception in handle_boot() ' + str(e))
+
+        finally:
+            try:
+                if xml_dom:
+                    xml_dom.unlink()
+            except Exception, e:
+                traceback.print_exc()
+                self.log_msg('Exception in xml_dom.unlink() ' + str(e))
+
+
+    #---------------------------------------------------------------
+    # admin_kick function -- by Snowdog 4/03
+    # 9/17/05 updated to allow stealth boots (no client chat announce) -SD
+    #---------------------------------------------------------------
+    def admin_kick(self, id, message="", silent = 0 ):
+        "Kick a player from a server from the console"
+
+        try:
+            group_id = self.players[id].group_id
+            # Send a message to everyone in the victim's room, letting them know someone has been booted
+            boot_msg = "<msg to='all' from='0' group_id='%s'/><font color='#FF0000'>Kicking '(%s) %s' from server... %s</font>" % ( group_id, id, self.players[id].name, str(message))
+            self.log_msg("boot_msg:" + boot_msg)
+            if (silent == 0):
+                self.send_to_group( "0", group_id, boot_msg )
+            time.sleep( 1 )
+
+            self.log_msg("kicking player " + str(id) + " from server.")
+            #  Send delete player event to all
+            self.send_to_group("0",group_id,self.players[id].toxml("del"))
+
+            #  Remove the player from local data structures
+            self.del_player(id,group_id)
+
+            #  Refresh the group data
+            self.check_group(id, group_id)
+
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg('Exception in admin_kick() ' + str(e))
+
+
+    def admin_banip(self, ip, name="", silent = 0):
+        "Ban a player from a server from the console"
+        try:
+            self.ban_list[ip] = {}
+            self.ban_list[ip]['ip'] = ip
+            self.ban_list[ip]['name'] = name
+            self.saveBanList()
+
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg('Exception in admin_banip() ' + str(e))
+
+    def admin_ban(self, id, message="", silent = 0):
+        "Ban a player from a server from the console"
+        try:
+            id = str(id)
+            group_id = self.players[id].group_id
+            ip = self.players[id].ip
+            self.ban_list[ip] = {}
+            self.ban_list[ip]['ip'] = ip
+            self.ban_list[ip]['name'] = self.players[id].name
+            self.saveBanList()
+
+            # Send a message to everyone in the victim's room, letting them know someone has been booted
+            ban_msg = "<msg to='all' from='0' group_id='%s'/><font color='#FF0000'>Banning '(%s) %s' from server... %s</font>" % ( group_id, id, self.players[id].name, str(message))
+            self.log_msg("ban_msg:" + ban_msg)
+            if (silent == 0):
+                self.send_to_group("0", group_id, ban_msg)
+            time.sleep( .1 )
+
+            self.log_msg("baning player " + str(id) + " from server.")
+            #  Send delete player event to all
+            self.send_to_group("0", group_id, self.players[id].toxml("del"))
+
+            #  Remove the player from local data structures
+            self.del_player(id, group_id)
+
+            #  Refresh the group data
+            self.check_group(id, group_id)
+
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg('Exception in admin_ban() ' + str(e))
+
+    def admin_unban(self, ip):
+        try:
+            if self.ban_list.has_key(ip):
+                del self.ban_list[ip]
+
+            self.saveBanList()
+
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg('Exception in admin_unban() ' + str(e))
+
+    def admin_banlist(self):
+        msg = []
+        msg.append('<table border="1"><tr><td><b>Name</b></td><td><b>IP</b></td></tr>')
+        for ip in self.ban_list.keys():
+            msg.append("<tr><td>")
+            msg.append(self.ban_list[ip]['name'])
+            msg.append("</td><td>")
+            msg.append(self.ban_list[ip]['ip'])
+            msg.append("</td></tr>")
+        msg.append("</table>")
+
+        return "".join(msg)
+
+    def admin_toggleSound(self):
+        if self.sendLobbySound:
+            self.sendLobbySound = False
+        else:
+            self.sendLobbySound = True
+
+        return self.sendLobbySound
+
+    def admin_soundFile(self, file):
+        self.lobbySound = file
+
+    def admin_setSendSize(self, sendlen):
+        self.maxSendSize = sendlen
+        self.log_msg('Max Send Size was set to ' + str(sendlen))
+
+    def remove_room(self, group):
+        "removes a group and boots all occupants"
+        #check that group id exists
+        if group not in self.groups:
+            return "Invalid Room Id. Ignoring remove request."
+
+        self.groups[group].persistant = 0
+        try:
+            keys = self.groups[group].get_player_ids()
+            for k in keys:
+                self.del_player(k, str(group))
+            self.check_group("0", str(group))
+        except:
+            pass
+
+    def send(self,msg,player,group):
+        self.players[player].send(msg,player,group)
+
+
+    def send_to_all(self,from_id,data):
+        try:
+            self.p_lock.acquire()
+            keys = self.players.keys()
+            self.p_lock.release()
+            for k in keys:
+                if k != from_id:
+                    self.players[k].outbox.put(data)
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg("Exception: send_to_all(): " + str(e))
+
+
+
+    def send_to_group(self, from_id, group_id, data):
+        data = ServerPlugins.postParseIncoming(data)
+        try:
+            keys = self.groups[group_id].get_player_ids()
+            for k in keys:
+                if k != from_id:
+                    self.players[k].outbox.put(data)
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg("Exception: send_to_group(): " + str(e))
+
+    def send_player_list(self,to_id,group_id):
+        try:
+            keys = self.groups[group_id].get_player_ids()
+            for k in keys:
+                if k != to_id:
+                    data = self.players[k].toxml('new')
+                    self.players[to_id].outbox.put(data)
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg("Exception: send_player_list(): " + str(e))
+
+    def send_group_list(self, to_id, action="new"):
+        try:
+            for key in self.groups:
+                xml = self.groups[key].toxml(action)
+                self.players[to_id].outbox.put(xml)
+        except Exception, e:
+            self.log_msg("Exception: send_group_list(): (client #"+to_id+") : " + str(e))
+            traceback.print_exc()
+
+    #--------------------------------------------------------------------------
+    # KICK_ALL_CLIENTS()
+    #
+    # Convience method for booting all clients off the server at once.
+    # used while troubleshooting mysterious "black hole" server bug
+    # Added by Snowdog 11-19-04
+    def kick_all_clients(self):
+        try:
+            keys = self.groups.keys()
+            for k in keys:
+                pl = self.groups[k].get_player_ids()
+                for p in pl:
+                    self.admin_kick(p,"Purged from server")
+        except Exception, e:
+            traceback.print_exc()
+            self.log_msg("Exception: kick_all_clients(): " + str(e))
+
+
+    # This really has little value as it will only catch people that are hung
+    # on a disconnect which didn't complete.  Other idle connections which are
+    # really dead go undeterred.
+    #
+    # UPDATED 11-29-04: Changed remove XML send to forced admin_kick for 'dead clients'
+    #                   Dead clients now removed more effeciently as soon as they are detected
+    #                        --Snowdog
+    def check_group_members(self, group_id):
+        try:
+            keys = self.groups[group_id].get_player_ids()
+            for k in keys:
+                #drop any clients that are idle for more than 8 hours
+                #as these are likely dead clients
+                idlemins = self.players[k].idle_time()
+                idlemins = idlemins/60
+                if (idlemins > self.zombie_time):
+                    self.admin_kick(k,"Removing zombie client", self.silent_auto_kick)
+                elif self.players[k].get_status() != MPLAY_CONNECTED:
+                    if self.players[k].check_time_out():
+                        self.log_msg("Player #" + k + " Lost connection!")
+                        self.admin_kick(k,"Removing dead client", self.silent_auto_kick)
+        except Exception, e:
+            self.log_msg("Exception: check_group_members(): " + str(e))
+
+
+    def remote_admin_handler(self,xml_dom,data):
+        # handle incoming remove server admin messages
+        # (allows basic administration of server from a remote client)
+        # base message format: <admin id="" pwd="" cmd="" [data for command]>
+
+        if not self.allowRemoteAdmin:
+            return
+
+        try:
+            pid = xml_dom.getAttribute("id")
+            gid = ""
+            given_pwd = xml_dom.getAttribute("pwd")
+            cmd = xml_dom.getAttribute("cmd")
+            server_admin_pwd = self.groups["0"].boot_pwd
+            p_id = ""
+            p_name= ""
+            p_ip = ""
+
+
+            #verify that the message came from the proper ID/Socket and get IP address for logging
+            if self.players.has_key(pid):
+                p_name=(self.players[pid]).name
+                p_ip=(self.players[pid]).ip
+                gid=(self.players[pid]).group_id
+            else:
+                #invalid ID.. report fraud and log
+                m = "Invalid Remote Server Control Message (invalid id) #" + str(pid) + " does not exist."
+                self.log_msg( m )
+                return
+
+            #log receipt of admin command   added by Darren
+            m = "Remote Server Control Message ( "+ str(cmd) +" ) from #" + str(pid) + " (" + str(p_name) + ") " + str(p_ip)
+            self.log_msg ( m )
+
+            #check the admin password(boot password) against the supplied one in message
+            #dump and log any attempts to control server remotely with invalid password
+            if server_admin_pwd != given_pwd:
+                #tell the clients password manager the password failed -- SD 8/03
+                pm = "<password signal=\"fail\" type=\"server\" id=\"" + str(self.players[pid].group_id) + "\" data=\"\"/>"
+                self.players[pid].outbox.put(pm)
+                m = "Invalid Remote Server Control Message (bad password) from #" + str(pid) + " (" + str(p_name) + ") " + str(p_ip)
+                self.log_msg( m )
+                return
+
+            #message now deemed 'authentic'
+            #determine action to take based on command (cmd)
+
+            if cmd == "list":
+                #return player list to this user.
+                msg ="<msg to='" + pid + "' from='0' group_id='" + gid + "'>" + self.player_list_remote()
+                self.players[pid].outbox.put(msg)
+
+            elif cmd == "banip":
+                ip = xml_dom.getAttribute("bip")
+                name = xml_dom.getAttribute("bname")
+                msg = "<msg to='" + pid + "' from='0' group_id='" + gid + "'> Banned: " + str(ip)
+                self.admin_banip(ip, name)
+
+            elif cmd == "ban":
+                id = xml_dom.getAttribute("bid")
+                msg = "<msg to='" + id + "' from='0' group_id='" + gid + "'> Banned!"
+                self.players[pid].outbox.put(msg)
+                self.admin_ban(id, "")
+
+            elif cmd == "unban":
+                ip = xml_dom.getAttribute("ip")
+                self.admin_unban(ip)
+                msg = "<msg to='" + pid + "' from='0' group_id='" + gid + "'> Unbaned: " + str(ip)
+                self.players[pid].outbox.put(msg)
+
+            elif cmd == "banlist":
+                msg = "<msg to='" + pid + "' from='0' group_id='" + gid + "'>" + self.admin_banlist()
+                self.players[pid].outbox.put(msg)
+
+            elif cmd == "killgroup":
+                ugid = xml_dom.getAttribute("gid")
+                if ugid == "0":
+                    m = "<msg to='" + pid + "' from='0' group_id='" + gid + "'>Cannot Remove Lobby! Remote administrator request denied!"
+                    self.players[pid].outbox.put(m)
+                else:
+                    result = self.prune_room(ugid)
+                    msg = "<msg to='" + pid + "' from='0' group_id='" + gid + "'>" + str(result)
+                    self.players[pid].outbox.put(msg)
+
+            elif cmd == "message":
+                tuid = xml_dom.getAttribute("to_id")
+                msg = xml_dom.getAttribute("msg")
+                pmsg = "<msg to='" + tuid + "' from='0' group_id='" + self.players[tuid].group_id + "' >" + msg
+                try: self.players[tuid].outbox.put(pmsg)
+                except:
+                    msg = "<msg to='" + pid + "' from='0' group_id='" + gid + ">Unknown Player ID: No message sent."
+                    self.players[pid].outbox.put(msg)
+
+            elif cmd == "broadcast":
+                bmsg = xml_dom.getAttribute("msg")
+                self.broadcast(bmsg)
+
+            elif cmd == "killserver" and self.allowRemoteKill:
+                #dangerous command..once server stopped it must be restarted manually
+                self.kill_server()
+
+            elif cmd == "uptime":
+                msg ="<msg to='" + pid + "' from='0' group_id='" + gid + "'>" + self.uptime(1)
+                self.players[pid].outbox.put(msg)
+
+            elif cmd == "help":
+                msg = "<msg to='" + pid + "' from='0' group_id='" + gid + "'>"
+                msg += self.AdminHelpMessage()
+                self.players[pid].outbox.put( msg)
+
+            elif cmd == "roompasswords":
+                # Toggle if room passwords are allowed on this server
+                msg = "<msg to='" + pid + "' from='0' group_id='" + gid + "'>"
+                msg += self.RoomPasswords()
+                self.players[pid].outbox.put( msg)
+
+            elif cmd == "createroom":
+                rm_name = xml_dom.getAttribute("name")
+                rm_pass = xml_dom.getAttribute("pass")
+                rm_boot = xml_dom.getAttribute("boot")
+                result = self.create_temporary_persistant_room(rm_name, rm_boot, rm_pass)
+                msg = "<msg to='" + pid + "' from='0' group_id='" + gid + "'>" + result
+                self.players[pid].outbox.put(msg)
+
+            elif cmd == "nameroom":
+                rm_id   = xml_dom.getAttribute("rmid")
+                rm_name = xml_dom.getAttribute("name")
+                result = self.change_group_name(rm_id,rm_name,pid)
+                msg ="<msg to='" + pid + "' from='0' group_id='" + gid + "'/>" + result
+                self.players[pid].outbox.put(msg)
+
+            elif cmd == "passwd":
+                tgid = xml_dom.getAttribute("gid")
+                npwd = xml_dom.getAttribute("pass")
+                if tgid == "0":
+                    msg ="<msg to='" + pid + "' from='0' group_id='" + gid + "'>Server password may not be changed remotely!"
+                    self.players[pid].outbox.put(msg)
+                else:
+                    try:
+                        self.groups[tgid].boot_pwd = npwd
+                        msg ="<msg to='" + pid + "' from='0' group_id='" + gid + "'>Password changed for room " + tgid
+                        self.players[pid].outbox.put(msg)
+                    except: pass
+
+            elif cmd == "savemaps":
+                for g in self.groups.itervalues():
+                    g.save_map()
+
+                msg ="<msg to='" + pid + "' from='0' group_id='" + gid + "'>Persistent room maps saved"
+                self.players[pid].outbox.put(msg)
+
+
+            else:
+                msg ="<msg to='" + pid + "' from='0' group_id='" + gid + "'><i>[Unknown Remote Administration Command]</i>"
+                self.players[pid].outbox.put(msg)
+
+
+        except Exception, e:
+            self.log_msg("Exception: Remote Admin Handler Error: " + str(e))
+            traceback.print_exc()
+
+
+    def toggleRemoteKill(self):
+        if self.allowRemoteKill:
+            self.allowRemoteKill = False
+        else:
+            self.allowRemoteKill = True
+
+        return self.allowRemoteKill
+
+    def toggleRemoteAdmin(self):
+        if self.allowRemoteAdmin:
+            self.allowRemoteAdmin = False
+        else:
+            self.allowRemoteAdmin = True
+
+        return self.allowRemoteAdmin
+
+#-----------------------------------------------------------------
+# Remote Administrator Help (returns from server not client)
+#-----------------------------------------------------------------
+    def AdminHelpMessage(self):
+        "returns a string to be sent as a message to a remote admin"
+
+        #define the help command list information
+        info = []
+        info.append( ['list', '/admin list', 'Displays information about rooms and players on the server'] )
+        info.append( ['uptime', '/admin uptime', 'Information on how long server has been running'] )
+        info.append( ['help', '/admin help', 'This help message'])
+        info.append( ['passwd', '/admin passwd &lt;group id&gt; &lt;new password&gt;', 'Changes a rooms bootpassword. Server(lobby) password may not be changed'])
+        info.append( ['roompasswords', '/admin roompasswords', 'Allow/Disallow Room Passwords on the server (toggles)'])
+        info.append( ['message', '/admin message &lt;user id&gt; &lt;message&gt;', 'Send a message to a specific user on the server'])
+        info.append( ['broadcast', '/admin broadcast &lt;message&gt;', 'Broadcast message to all players on server'])
+        info.append( ['createroom', '/admin createroom &lt;room name&gt; &lt;boot password&gt; [password]', 'Creates a temporary persistant room if possible.<i>Rooms created this way are lost on server restarts'])
+        info.append( ['nameroom', '/admin nameroom &lt;group id&gt; &lt;new name&gt;', 'Rename a room'])
+        info.append( ['killgroup', '/admin killgroup &lt;room id&gt;', 'Remove a room from the server and kick everyone in it.'])
+        if self.allowRemoteKill:
+            info.append( ['killserver', '/admin killserver', 'Shuts down the server. <b>WARNING: Server cannot be restarted remotely via OpenRPG</b>'])
+        info.append( ['ban', '/admin ban {playerId}', 'Ban a player from the server.'])
+        info.append( ['unban', '/admin unban {bannedIP}', 'UnBan a player from the server.'])
+        info.append( ['banlist', '/admin banlist', 'List Banned IPs and the Names associated with them'])
+        info.append( ['savemaps', '/admin savemaps', 'Save all persistent room maps that are not using the default map file.'])
+
+
+        #define the HTML for the help display
+        FS = "<font size='-1'>"
+        FE = "<font>"
+
+        help = "<hr><B>REMOTE ADMINISTRATOR COMMANDS SUPPORTED</b><br /><br />"
+        help += "<table border='1' cellpadding='2'>"
+        help += "<tr><td width='15%'><b>Command</b></td><td width='25%'><b>Format</b></td><td width='60%'><b>Description</b></td></tr>"
+        for n in info:
+            help += "<tr><td>" + FS + n[0] + FE + "</td><td><nobr>" + FS + n[1] + FE + "</nobr></td><td>" + FS + n[2] + FE + "</td></tr>"
+        help += "</table>"
+        return help
+
+
+    #----------------------------------------------------------------
+    # Create Persistant Group -- Added by Snowdog 6/03
+    #
+    # Allows persistant groups to be created on the fly.
+    # These persistant groups are not added to the server.ini file
+    # however and are lost on server restarts
+    #
+    # Updated function code to use per-group based persistance and
+    # removed references to outdated persistRoomIdThreshold
+    #----------------------------------------------------------------
+
+    def create_temporary_persistant_room(self, roomname, bootpass, password=""):
+        # if the room id just above the persistant room limit is available (not in use)
+         # then it will be assigned as a persistant room on the server
+        "create a temporary persistant room"
+
+        group_id = str(self.next_group_id)
+        self.next_group_id += 1
+        self.groups[group_id] = game_group( group_id, roomname, password, "", bootpass, persist = 1 )
+        cgmsg = "Create Temporary Persistant Group: (" + str(group_id) + ") " + str(roomname)
+        self.log_msg( cgmsg )
+        self.send_to_all('0',self.groups[group_id].toxml('new'))
+        self.send_to_all('0',self.groups[group_id].toxml('update'))
+        return str("Persistant room created (group " + group_id + ").")
+
+    #----------------------------------------------------------------
+    # Prune Room  -- Added by Snowdog 6/03
+    #
+    # similar to remove_room() except rooms are removed regardless
+    # of them being persistant or not
+    #
+    # Added some error checking and updated room removal for per-room
+    # based persistance -- Snowdog 4/04
+    #----------------------------------------------------------------
+
+    def prune_room(self,group):
+        #don't allow lobby to be removed
+        if group == '0': return "Lobby is required to exist and cannot be removed."
+
+        #check that group id exists
+        if group not in self.groups:
+            return "Invalid Room Id. Ignoring remove request."
+
+        try:
+            keys = self.groups[group].get_player_ids()
+            for k in keys:
+                self.move_player(k,'0')
+
+            ins = "Room"
+            if self.isPersistentRoom(group) : ins="Persistant room"
+            self.send_to_all("0",self.groups[group].toxml('del'))
+            del self.groups[group]
+            self.log_msg(("delete_group", ('0',group)))
+            return ins + " removed."
+
+        except:
+            traceback.print_exc()
+            return "An Error occured on the server during room removal!"
+
+
+#----------------------------------------------------------------
+#  Remote Player List  -- Added by snowdog 6/03
+#
+#  Similar to console listing except formated for web display
+#  in chat window on remote client
+#----------------------------------------------------------------
+    def player_list_remote(self):
+        COLOR1 = "\"#004080\""  #header/footer background color
+        COLOR2 = "\"#DDDDDD\""  #group line background color
+        COLOR3 = "\"#FFFFFF\""  #player line background color
+        COLOR4 = "\"#FFFFFF\""  #header/footer text color
+        PCOLOR = "\"#000000\""  #Player text color
+        LCOLOR = "\"#404040\""  #Lurker text color
+        GCOLOR = "\"#FF0000\""  #GM text color
+        SIZE   = "size=\"-1\""  #player info text size
+        FG = PCOLOR
+
+
+        "display a condensed list of players on the server"
+        self.p_lock.acquire()
+        pl = "<br /><table border=\"0\" cellpadding=\"1\" cellspacing=\"2\">"
+        pl += "<tr><td colspan='4' bgcolor=" + COLOR1 + "><font color=" + COLOR4 + "><b>GROUP &amp; PLAYER LIST</b></font></td></tr>"
+        try:
+
+            keys = self.groups.keys()
+            keys.sort(id_compare)
+            for k in keys:
+                groupstring = "<tr><td bgcolor=" + COLOR2 + " colspan='2'><b>Group " + str(k)  + ": " +  self.groups[k].name  + "</b>"
+                groupstring += "</td><td bgcolor=" + COLOR2 + " > <i>Password: \"" + self.groups[k].pwd + "\"</td><td bgcolor=" + COLOR2 + " > Boot: \"" + self.groups[k].boot_pwd + "\"</i></td></tr>"
+                pl += groupstring
+                ids = self.groups[k].get_player_ids()
+                ids.sort(id_compare)
+                for id in ids:
+                    if self.players.has_key(id):
+                        if k != "0":
+                            if (self.players[id]).role == "GM": FG = GCOLOR
+                            elif (self.players[id]).role == "Player": FG = PCOLOR
+                            else: FG = LCOLOR
+                        else: FG = PCOLOR
+                        pl += "<tr><td bgcolor=" + COLOR3 + ">"
+                        pl += "<font color=" + FG + " " + SIZE + ">&nbsp;&nbsp;(" +  (self.players[id]).id  + ") "
+                        pl += (self.players[id]).name
+                        pl += "</font></td><td bgcolor=" + COLOR3 + " ><font color=" + FG + " " + SIZE + ">[IP: " + (self.players[id]).ip + "]</font></td><td  bgcolor=" + COLOR3 + " ><font color=" + FG + " " + SIZE + "> "
+                        pl += (self.players[id]).idle_status()
+                        pl += "</font></td><td><font color=" + FG + " " + SIZE + ">"
+                        pl += (self.players[id]).connected_time_string()
+                        pl += "</font>"
+
+                    else:
+                        self.groups[k].remove_player(id)
+                        pl +="<tr><td colspan='4' bgcolor=" + COLOR3 + " >Bad Player Ref (#" + id + ") in group"
+                pl+="</td></tr>"
+            pl += "<tr><td colspan='4' bgcolor=" + COLOR1 + "><font color=" + COLOR4 + "><b><i>Statistics: groups: " + str(len(self.groups)) + "  players: " +  str(len(self.players)) + "</i></b></font></td></tr></table>"
+        except Exception, e:
+            self.log_msg(str(e))
+        self.p_lock.release()
+        return pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/mplay_server_gui.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,602 @@
+#!/usr/bin/env python
+#
+#   OpenRPG Server Graphical Interface
+#        GNU General Public License
+#
+
+__appname__=' OpenRPG GUI Server v0.7 '
+__version__='$Revision: 1.26 $'[11:-2]
+__cvsinfo__='$Id: mplay_server_gui.py,v 1.26 2007/11/06 00:32:39 digitalxero Exp $'[5:-2]
+__doc__="""
+OpenRPG Server Graphical Interface
+"""
+
+import os
+import sys
+import time
+import types
+import orpg.dirpath
+import orpg.systempath
+from orpg.orpg_wx import *
+import webbrowser
+from threading import Thread
+from meta_server_lib import post_server_data, remove_server
+from mplay_server import mplay_server
+
+# Constants ######################################
+SERVER_RUNNING = 1
+SERVER_STOPPED = 0
+MENU_START_SERVER = wx.NewId()
+MENU_STOP_SERVER = wx.NewId()
+MENU_EXIT = wx.NewId()
+MENU_REGISTER_SERVER = wx.NewId()
+MENU_UNREGISTER_SERVER = wx.NewId()
+MENU_START_PING_PLAYERS = wx.NewId()
+MENU_STOP_PING_PLAYERS = wx.NewId()
+MENU_PING_INTERVAL = wx.NewId()
+
+# Add our menu id's for our right click popup
+MENU_PLAYER_BOOT = wx.NewId()
+MENU_PLAYER_CREATE_ROOM = wx.NewId()
+MENU_PLAYER_SEND_MESSAGE = wx.NewId()
+MENU_PLAYER_SEND_ROOM_MESSAGE = wx.NewId()
+MENU_PLAYER_SEND_SERVER_MESSAGE = wx.NewId()
+
+# Our new event type that gets posted from one
+# thread to another
+wxEVT_LOG_MESSAGE = wx.NewEventType()
+wxEVT_FUNCTION_MESSAGE = wx.NewEventType()
+
+# Our event connector -- wxEVT_LOG_MESSAGE
+EVT_LOG_MESSAGE = wx.PyEventBinder(wxEVT_LOG_MESSAGE, 1)
+
+# Our event connector -- wxEVT_FUNCTION_MESSAGE
+EVT_FUNCTION_MESSAGE = wx.PyEventBinder(wxEVT_FUNCTION_MESSAGE, 1)
+
+# Utils ##########################################
+def format_bytes(b):
+    f = ['b', 'Kb', 'Mb', 'Gb']
+    i = 0
+    while i < 3:
+        if b < 1024:
+            return str(b) + f[i]
+        else:
+            b = b/1024
+        i += 1
+    return str(b) + f[3]
+
+# wxEVT_LOG_MESSAGE
+# MessageLogEvent ###############################
+class MessageLogEvent(wx.PyEvent):
+    def __init__( self, message ):
+        wx.PyEvent.__init__( self )
+        self.SetEventType(wxEVT_LOG_MESSAGE)
+        self.message = message
+
+# wxEVT_TUPLE_MESSAGE
+# MessageLogEvent ###############################
+class MessageFunctionEvent(wx.PyEvent):
+    def __init__( self, func, message ):
+        wx.PyEvent.__init__( self )
+        self.SetEventType(wxEVT_FUNCTION_MESSAGE)
+        self.func = func
+        self.message = message
+
+# ServerConfig Object ############################
+class ServerConfig:
+    """ This class contains configuration
+        setting used to control the server.
+    """
+
+    def __init__(self, owner ):
+        """ Loads default configuration settings.
+        """
+        OPENRPG_PORT = 9775
+        self.owner = owner
+
+    def load_xml(self, xml):
+        """ Load configuration from XML data.
+            xml (xml) -- xml string to parse
+        """
+        pass
+
+    def save_xml(self):
+        """ Returns XML file representing
+            the active configuration.
+        """
+        pass
+
+# Server Monitor #################################
+
+class ServerMonitor(Thread):
+    """ Monitor thread for GameServer. """
+    def __init__(self, cb, conf, name, pwd):
+        """ Setup the server. """
+        Thread.__init__(self)
+        self.cb = cb
+        self.conf = conf
+        self.serverName = name
+        self.bootPwd = pwd
+
+    def log(self, mesg):
+        if type(mesg) == types.TupleType:
+            func, msg = mesg
+            event = MessageFunctionEvent( func, msg )
+        else:
+            event = MessageLogEvent( mesg )
+        wx.PostEvent( self.conf.owner, event )
+        del event
+
+    def run(self):
+        """ Start the server. """
+        self.server = mplay_server(self.log, self.serverName )
+        self.server.initServer(bootPassword=self.bootPwd, reg="No")
+        self.alive = 1
+        while self.alive:
+            time.sleep(3)
+
+    def stop(self):
+        """ Stop the server. """
+        self.server.kill_server()
+        self.alive = 0
+
+# GUI Server #####################################
+# Parent = notebook
+# Main = GUI
+class Connections(wx.ListCtrl):
+    def __init__( self, parent, main ):
+        wx.ListCtrl.__init__( self, parent, -1, wx.DefaultPosition, wx.DefaultSize, wx.LC_REPORT|wx.SUNKEN_BORDER|wx.EXPAND|wx.LC_HRULES )
+        self.main = main
+        self.roomList = { "0" : "Lobby" }
+        self._imageList = wx.ImageList( 16, 16, False )
+        img = wx.Image(orpg.dirpath.dir_struct["icon"]+"player.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        self._imageList.Add( img )
+        img = wx.Image(orpg.dirpath.dir_struct["icon"]+"player-whisper.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        self._imageList.Add( img )
+        self.SetImageList( self._imageList, wx.IMAGE_LIST_SMALL )
+
+        # Set The columns
+        self.InsertColumn(0, "ID")
+        self.InsertColumn(1, "Player")
+        self.InsertColumn(2, "Status")
+        self.InsertColumn(3, "Room")
+        self.InsertColumn(4, "Version")
+        self.InsertColumn(5, "Role")
+	self.InsertColumn(6, "IP")
+	self.InsertColumn(7, "Ping")
+
+        # Set the column widths
+        self.AutoAjust()
+
+        # Build our pop up menu to do cool things with the players in the list
+        self.menu = wx.Menu()
+        self.menu.SetTitle( "Player Menu" )
+        self.menu.Append( MENU_PLAYER_BOOT, "Boot Player" )
+        self.menu.AppendSeparator()
+        self.menu.Append( MENU_PLAYER_SEND_MESSAGE, "Send Message" )
+        self.menu.Append( MENU_PLAYER_SEND_SERVER_MESSAGE, "Broadcast Server Message" )
+
+        # Associate our events
+        self.Bind(wx.EVT_RIGHT_DOWN, self.OnPopupMenu)
+        self.Bind(wx.EVT_MENU, self.OnPopupMenuItem, id=MENU_PLAYER_BOOT)
+        self.Bind(wx.EVT_MENU, self.OnPopupMenuItem, id=MENU_PLAYER_SEND_MESSAGE)
+        self.Bind(wx.EVT_MENU, self.OnPopupMenuItem, id=MENU_PLAYER_SEND_ROOM_MESSAGE)
+        self.Bind(wx.EVT_MENU, self.OnPopupMenuItem, id=MENU_PLAYER_SEND_SERVER_MESSAGE)
+
+    def add(self, player):
+        i = self.InsertImageStringItem( 0, player["id"], 0 )
+        self.SetStringItem( i, 1, self.stripHtml( player["name"] ) )
+        self.SetStringItem( i, 2, self.stripHtml( player["status"] ) )
+        self.SetStringItem( i, 3, "ROOM" )
+        self.SetStringItem( i, 4, self.stripHtml( player["version"] ) )
+        self.SetStringItem( i, 5, self.stripHtml( player["role"] ) )
+	self.SetStringItem( i, 6, self.stripHtml( player["ip"] ) )
+	self.SetStringItem (i, 7, "PING" )
+        self.SetItemData( i, int(player["id"]) )
+        self.AutoAjust()
+
+    def remove(self, id):
+        i = self.FindItemData( -1, int(id) )
+        self.DeleteItem( i )
+        self.AutoAjust()
+
+    def AutoAjust(self):
+        self.SetColumnWidth(0, -1)
+        self.SetColumnWidth(1, -1)
+        self.SetColumnWidth(2, -1)
+        self.SetColumnWidth(3, -1)
+        self.SetColumnWidth(4, -1)
+        self.SetColumnWidth(5, -1)
+	self.SetColumnWidth(6, -1)
+	self.SetColumnWidth(7, -1)
+        self.Refresh()
+
+    def update(self, player):
+        i = self.FindItemData( -1, int(player["id"]) )
+        if i > -1:
+            self.SetStringItem(i, 1, player["name"])
+            self.SetStringItem(i, 2, self.stripHtml( player["status"] ) )
+            self.AutoAjust()
+        else:
+            self.add(player)
+
+    def updateRoom( self, data ):
+        (from_id, id) = data
+        i = self.FindItemData( -1, int(from_id) )
+        self.SetStringItem( i, 3, self.roomList[id] )
+        self.SetStringItem( i, 5, "Lurker")
+        self.Refresh()
+
+    def setPlayerRole( self, id, role ):
+        i = self.FindItemData( -1, int(id) )
+        self.SetStringItem( i, 5, role )
+
+    def stripHtml( self, name ):
+        ret_string = ""
+        x = 0
+        in_tag = 0
+        for x in range( len(name) ):
+            if name[x] == "<" or name[x] == ">" or in_tag == 1 :
+                if name[x] == "<" :
+                    in_tag = 1
+                elif name[x] == ">" :
+                    in_tag = 0
+                else :
+                    pass
+            else :
+                ret_string = ret_string + name[x]
+        return ret_string
+
+    # When we right click, cause our popup menu to appear
+    def OnPopupMenu( self, event ):
+        pos = wx.Point( event.GetX(), event.GetY() )
+        (item, flag) = self.HitTest( pos )
+        if item > -1:
+            self.selectedItem = item
+            self.PopupMenu( self.menu, pos )
+
+    # Process the events associated with our popup menu
+    def OnPopupMenuItem( self, event ):
+        # if we are not running, the player list is empty anyways
+        if self.main.STATUS == SERVER_RUNNING:
+            menuItem = event.GetId()
+            playerID = str( self.GetItemData( self.selectedItem ) )
+            idList = self.main.server.server.groups
+            groupID = 0
+            if menuItem == MENU_PLAYER_BOOT:
+                print "booting player: ", playerID
+                self.main.server.server.del_player( playerID, groupID )
+                self.main.server.server.check_group( playerID, groupID )
+                self.remove( playerID )
+            elif menuItem == MENU_PLAYER_SEND_MESSAGE:
+                print "send a message..."
+                msg = self.GetMessageInput( "Send a message to player" )
+                if len(msg):
+                    self.main.server.server.send( msg, playerID, str(groupID) )
+# Leave this in for now.
+#            elif menuItem == MENU_PLAYER_SEND_TO_ROOM:
+#                print "Send message to room..."
+#                msg = self.GetMessageInput( "Send message to room of this player")
+#                if len(msg):
+#                    self.main.server.server.send_to_group( 0, GroupID, msg )
+
+            elif menuItem == MENU_PLAYER_SEND_SERVER_MESSAGE:
+                print "broadcast a message..."
+                msg = self.GetMessageInput( "Broadcast Server Message" )
+                # If we got a message back, send it
+                if len(msg):
+                    self.main.server.server.broadcast( msg )
+
+    def GetMessageInput( self, title ):
+        prompt = "Please enter the message you wish to send:"
+        msg = wx.TextEntryDialog(self, prompt, title)
+        msg.ShowModal()
+        msg.Destroy()
+        return msg.GetValue()
+
+class ServerGUI(wx.Frame):
+    STATUS = SERVER_STOPPED
+
+    def __init__(self, parent, id, title):
+        wx.Frame.__init__(self, parent, id, title, size = (760, 560) )
+        if wx.Platform == '__WXMSW__':
+            icon = wx.Icon( orpg.dirpath.dir_struct["icon"]+'WAmisc9.ico', wx.BITMAP_TYPE_ICO )
+        else:
+            icon = wx.Icon( orpg.dirpath.dir_struct["icon"]+'connect.gif', wx.BITMAP_TYPE_GIF )
+        self.SetIcon(icon)
+        self.serverName = "Server Name"
+        self.bootPwd = ""
+
+        # Register our events to process -- notice the custom event handler
+        self.Bind(wx.EVT_CLOSE, self.OnExit)
+        self.Bind(EVT_LOG_MESSAGE, self.OnLogMessage)
+        self.Bind(EVT_FUNCTION_MESSAGE, self.OnFunctionMessage)
+
+        # Creat GUI
+        self.build_menu()
+        self.build_body()
+        self.build_status()
+
+        # Server Callbacks
+        cb = {}
+        cb["log"]        = self.Log
+        cb["connect"]    = self.OnConnect
+        cb["disconnect"] = self.OnDisconnect
+        cb["update"]     = self.OnUpdatePlayer
+        cb["data_recv"]  = self.OnDataRecv
+        cb["data_sent"]  = self.OnDataSent
+        cb["create_group"] = self.OnCreateGroup
+        cb["delete_group"] = self.OnDeleteGroup
+        cb["join_group"] = self.OnJoinGroup
+        cb["role"] = self.OnSetRole
+        self.callbacks = cb
+
+        # Misc.
+        self.conf = ServerConfig(self)
+        self.total_messages_received = 0
+        self.total_data_received = 0
+        self.total_messages_sent = 0
+        self.total_data_sent = 0
+
+    ### Build GUI ############################################
+
+    def build_menu(self):
+        """ Build the GUI menu. """
+        self.mainMenu = wx.MenuBar()
+        # File Menu
+        menu = wx.Menu()
+        # Start
+        menu.Append( MENU_START_SERVER, 'Start', 'Start server.')
+        self.Bind(wx.EVT_MENU, self.OnStart, id=MENU_START_SERVER)
+        # Stop
+        menu.Append( MENU_STOP_SERVER, 'Stop', 'Shutdown server.')
+        self.Bind(wx.EVT_MENU, self.OnStop, id=MENU_STOP_SERVER)
+        # Exit
+        menu.AppendSeparator()
+        menu.Append( MENU_EXIT, 'E&xit', 'Exit application.')
+        self.Bind(wx.EVT_MENU, self.OnExit, id=MENU_EXIT)
+        self.mainMenu.Append(menu, '&Server')
+        # Registration Menu
+        menu = wx.Menu()
+        # Register
+        menu.Append( MENU_REGISTER_SERVER, 'Register', 'Register with OpenRPG server directory.')
+        self.Bind(wx.EVT_MENU, self.OnRegister, id=MENU_REGISTER_SERVER)
+        # Unregister
+        menu.Append( MENU_UNREGISTER_SERVER, 'Unregister', 'Unregister from OpenRPG server directory.')
+        self.Bind(wx.EVT_MENU, self.OnUnregister, id=MENU_UNREGISTER_SERVER)
+        # Add the registration menu
+        self.mainMenu.Append( menu, '&Registration' )
+        # Server Configuration Menu
+        menu = wx.Menu()
+        # Ping Connected Players
+        menu.Append( MENU_START_PING_PLAYERS, 'Start Ping', 'Ping players to validate remote connection.' )
+        self.Bind(wx.EVT_MENU, self.PingPlayers, id=MENU_START_PING_PLAYERS)
+        # Stop Pinging Connected Players
+        menu.Append( MENU_STOP_PING_PLAYERS, 'Stop Ping', 'Stop validating player connections.' )
+        self.Bind(wx.EVT_MENU, self.StopPingPlayers, id=MENU_STOP_PING_PLAYERS)
+        # Set Ping Interval
+        menu.Append( MENU_PING_INTERVAL, 'Ping Interval', 'Change the ping interval.' )
+        self.Bind(wx.EVT_MENU, self.ConfigPingInterval, id=MENU_PING_INTERVAL)
+        self.mainMenu.Append( menu, '&Configuration' )
+        # Add the menus to the main menu bar
+        self.SetMenuBar( self.mainMenu )
+        # Disable register, unregister & stop server by default
+        self.mainMenu.Enable( MENU_STOP_SERVER, False )
+        self.mainMenu.Enable( MENU_REGISTER_SERVER, False )
+        self.mainMenu.Enable( MENU_UNREGISTER_SERVER, False )
+        # Disable the ping menu items
+        self.mainMenu.Enable( MENU_START_PING_PLAYERS, False )
+        self.mainMenu.Enable( MENU_STOP_PING_PLAYERS, False )
+        self.mainMenu.Enable( MENU_PING_INTERVAL, False )
+
+    def build_body(self):
+        """ Create the ViewNotebook and logger. """
+        splitter = wx.SplitterWindow(self, -1, style=wx.NO_3D | wx.SP_3D)
+        nb = wx.Notebook( splitter, -1 )
+        self.conns = Connections( nb, self )
+        nb.AddPage( self.conns, "Players" )
+#Not sure why this is Remarked TaS - Sirebral
+        #nb.AddPage( self.conns, "Rooms" )
+        #self.msgWindow = HTMLMessageWindow( nb )
+        #nb.AddPage( self.msgWindow, "Messages" )
+
+        log = wx.TextCtrl(splitter, -1, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL)
+        wx.Log.SetActiveTarget( wx.LogTextCtrl(log) )
+        splitter.SplitHorizontally(nb, log, 400)
+        splitter.SetMinimumPaneSize(40)
+        self.nb = nb
+        self.log = log
+
+    def build_status(self):
+        """ Create status bar and set defaults. """
+        sb = wx.StatusBar(self, -1)
+        sb.SetFieldsCount(5)
+        sb.SetStatusWidths([-1, 115, 115, 65, 200])
+        sb.SetStatusText("Sent: 0", 1)
+        sb.SetStatusText("Recv: 0", 2)
+        sb.SetStatusText("Stopped", 3)
+        sb.SetStatusText("Unregistered", 4)
+        self.SetStatusBar(sb)
+        self.sb = sb
+
+    def show_error(self, mesg, title = "Error"):
+        """ Display an error.
+            mesg (str) -- error message to display
+            title (str) -- title of window
+        """
+        dlg = wx.MessageDialog(self, mesg, title, wx.OK | wx.ICON_EXCLAMATION)
+        dlg.ShowModal()
+        dlg.Destroy()
+
+
+    # Event handler for out logging event
+    def OnLogMessage( self, event ):
+        self.Log( event.message )
+
+    # Event handler for out logging event
+    def OnFunctionMessage( self, event ):
+        self.callbacks[event.func]( event.message )
+
+    ### Server Callbacks #####################################
+    def Log(self, log):
+        wx.LogMessage(str(log))
+
+    def OnConnect(player, self, data):
+        self.conns.add(player)
+
+    def OnDisconnect(self, id):
+        self.conns.remove(id)
+
+    def OnUpdatePlayer(self, data):
+        self.conns.update(data)
+
+    def OnDataSent(self, bytes):
+        self.total_messages_sent += 1
+        self.total_data_sent += bytes
+        self.sb.SetStatusText("Sent: %s (%d)" % (format_bytes(self.total_data_sent), self.total_messages_sent), 1)
+
+    def OnDataRecv(self, bytes):
+        self.total_messages_received += 1
+        self.total_data_received += bytes
+        self.sb.SetStatusText("Recv: %s (%d)" % (format_bytes(self.total_data_received), self.total_messages_received), 2)
+
+    def OnCreateGroup( self, data ):
+        print "room list: ", self.conns.roomList
+        self.conns.roomList[id] = name
+	(id, name) = data
+        print "room list: ", self.conns.roomList
+
+    def OnDeleteGroup( self, data ):
+        (from_id, id) = data
+#        del self.conns.roomList[id]
+        print "OnDeleteGroup room list: ", self.conns.roomList, id
+
+    def OnJoinGroup( self, data ):
+        self.conns.updateRoom( data )
+
+    def OnSetRole( self, data ):
+        (id, role) = data
+        self.conns.setPlayerRole( id, role )
+
+    ### Misc. ################################################
+    def OnStart(self, event = None):
+        """ Start server. """
+        if self.STATUS == SERVER_STOPPED:
+            serverNameEntry = wx.TextEntryDialog( self, "Please Enter The Server Name You Wish To Use:",
+                                                 "Server's Name", self.serverName, wx.OK|wx.CANCEL|wx.CENTRE )
+            if serverNameEntry.ShowModal() == wx.ID_OK:
+                self.serverName = serverNameEntry.GetValue()
+            serverPasswordEntry = wx.TextEntryDialog(self, "Please Enter The Server Admin Password:",
+                                                 "Server's Password", self.bootPwd, wx.OK|wx.CANCEL|wx.CENTRE)
+            if serverPasswordEntry.ShowModal() == wx.ID_OK:
+                self.bootPwd = serverPasswordEntry.GetValue()
+            if len(self.serverName):
+                wx.BeginBusyCursor()
+                self.server = ServerMonitor(self.callbacks, self.conf, self.serverName, self.bootPwd)
+                self.server.start()
+                self.STATUS = SERVER_RUNNING
+                self.sb.SetStatusText("Running", 3)
+                self.SetTitle(__appname__ + "- (running) - (unregistered)")
+                self.mainMenu.Enable( MENU_START_SERVER, False )
+                self.mainMenu.Enable( MENU_STOP_SERVER, True )
+                self.mainMenu.Enable( MENU_REGISTER_SERVER, True )
+                wx.EndBusyCursor()
+            else:
+                self.show_error("Server is already running.", "Error Starting Server")
+
+    def OnStop(self, event = None):
+        """ Stop server. """
+        if self.STATUS == SERVER_RUNNING:
+            self.OnUnregister()
+            self.server.stop()
+            self.STATUS = SERVER_STOPPED
+            self.sb.SetStatusText("Stopped", 3)
+            self.SetTitle(__appname__ + "- (stopped) - (unregistered)")
+            self.mainMenu.Enable( MENU_STOP_SERVER, False )
+            self.mainMenu.Enable( MENU_START_SERVER, True )
+            self.mainMenu.Enable( MENU_REGISTER_SERVER, False )
+            self.mainMenu.Enable( MENU_UNREGISTER_SERVER, False )
+            # Delete any items that are still in the player list
+            self.conns.DeleteAllItems()
+
+    def OnRegister(self, event = None):
+        """ Call into mplay_server's register() function.
+            This will begin registerThread(s) to keep server
+            registered with configured metas
+        """
+        if len( self.serverName ):
+            wx.BeginBusyCursor()
+            self.server.server.register(self.serverName)
+            self.sb.SetStatusText( ("%s" % (self.serverName)), 4 )
+            self.mainMenu.Enable( MENU_REGISTER_SERVER, False )
+            self.mainMenu.Enable( MENU_UNREGISTER_SERVER, True )
+            self.mainMenu.Enable( MENU_STOP_SERVER, False )
+            self.SetTitle(__appname__ + "- (running) - (registered)")
+            wx.EndBusyCursor()
+
+    def OnUnregister(self, event = None):
+        """ Call into mplay_server's unregister() function.
+            This will kill any registerThreads currently running
+            and result in the server being de-listed
+            from all metas
+        """
+        wx.BeginBusyCursor()
+        self.server.server.unregister()
+        self.sb.SetStatusText( "Unregistered", 4 )
+        self.mainMenu.Enable( MENU_UNREGISTER_SERVER, False )
+        self.mainMenu.Enable( MENU_REGISTER_SERVER, True )
+        self.mainMenu.Enable( MENU_STOP_SERVER, True )
+        self.SetTitle(__appname__ + "- (running) - (unregistered)")
+        wx.EndBusyCursor()
+
+    def PingPlayers( self, event = None ):
+        "Ping all players that are connected at a periodic interval, detecting dropped connections."
+        wx.BeginBusyCursor()
+        wx.Yield()
+        wx.EndBusyCursor()
+
+    def StopPingPlayers( self, event = None ):
+        "Stop pinging connected players."
+
+    def ConfigPingInterval( self, event = None ):
+        "Configure the player ping interval.  Note that all players are pinged on a single timer."
+
+    def OnExit(self, event = None):
+        """ Quit the program. """
+        self.OnStop()
+        wx.CallAfter(self.Destroy)
+
+class ServerGUIApp(wx.App):
+    def OnInit(self):
+        # Make sure our image handlers are loaded before we try to display anything
+        wx.InitAllImageHandlers()
+        self.splash = wx.SplashScreen(wx.Bitmap(orpg.dirpath.dir_struct["icon"]+'splash.gif'),
+                              wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
+                              2000,
+                              None)
+        self.splash.Show(True)
+        wx.Yield()
+        wx.CallAfter(self.AfterSplash)
+        return True
+
+    def AfterSplash(self):
+        self.splash.Close(True)
+        frame = ServerGUI(None, -1, __appname__ + "- (stopped) - (unregistered)")
+        frame.Show(True)
+        frame.Raise()
+        self.SetTopWindow(frame)
+
+class HTMLMessageWindow(wx.html.HtmlWindow):
+    "Widget used to present user to admin messages, in HTML format, to the server administrator"
+
+    # Init using the derived from class
+    def __init__( self, parent ):
+        wx.html.HtmlWindow.__init__( self, parent )
+    def OnLinkClicked( self, ref ):
+        "Open an external browser to resolve our About box links!!!"
+        href = ref.GetHref()
+        webbrowser.open( href )
+
+if __name__ == '__main__':
+    app = ServerGUIApp(0)
+    app.MainLoop()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/networking/server_plugins.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,188 @@
+import sys
+import os
+from orpg.dirpath import dir_struct
+
+class _SingletonKey(object):
+    def __new__(cls, *args, **kwargs):
+        if not hasattr(cls, '_inst'):
+            cls._inst = super(_SingletonKey, cls).__new__(cls, *args, **kwargs)
+        return cls._inst
+
+class PluginData(object):
+    """A holder class for inactive plugins"""
+    def __init__(self, Name, File, Author="", Help=""):
+        self.File = File
+        self.Name = Name
+        self.Author = Author
+        self.Help = Help
+        self.Activated = False
+
+class BasePluginsClass(object):
+    def __init__(self, ptype):
+        self.__ptype = ptype
+        self.__plugins = {}
+
+    def initBase(self):
+        self._startPlugins()
+
+
+    #Methods
+    def _startPlugins(self):
+        autoload = []
+        #Fill autoload from some file with the Plugin Names
+
+        plugins = []
+        for root, dirs, files in os.walk(dir_struct['plugins'] + self.__ptype):
+            if '__init__.py' in files:
+                files.remove('__init__.py')
+            if 'base_plugin.py' in files:
+                files.remove('base_plugin.py')
+
+            for pfile in files:
+                if pfile[-3:] == '.py':
+                    plugins.append(PluginData('New Plugin', root + os.sep + pfile))
+
+        for plugin in plugins:
+            pdata = self._load(plugin)
+            if pdata != None:
+                if pdata.Name not in autoload:
+                    self._unload(plugin)
+
+    def activatePlugin(self, pluginName):
+        if not self.__plugins.has_key(pluginName):
+            #Print some error about invalid plugin
+            return
+        pluginData = self.__plugins[pluginName]
+        #Unload it first, just incase it is already loaded
+        self._unload(pluginData)
+
+        #Now Load it
+        self._load(pluginData)
+
+        #Write to the autoload file for this plugin
+
+        self.__plugins[pluginName].Activated = True
+        self.__plugins[pluginName].start()
+
+    def deactivatePugin(self, pluginData):
+        if not self.__plugins.has_key(pluginName):
+            #Print some error about invalid plugin
+            return
+        pluginData = self.__plugins[pluginName]
+
+        self.__plugins[pluginName].stop()
+
+        #Unload it
+        self._unload(pluginData)
+
+        #Remove this plugin from the autoload file
+
+    #Private Methods
+    def _findModule(self, pluginFile):
+        s1 = pluginFile.split(os.sep)
+        s2 = s1[-1].split('.')
+        return ('plugins.' + self.__ptype + '.' + s2[0], s2[0])
+
+    def _unload(self, pluginData):
+        self.__plugins[pluginData.Name] = PluginData(pluginData.Name, pluginData.File, pluginData.Author, pluginData.Help)
+        unload = []
+        mod = self._findModule(pluginData.File)[0]
+        for key, module in sys.modules.iteritems():
+            if str(module).find(mod) > -1:
+                unload.append(key)
+
+        for plugin in unload:
+            del sys.modules[plugin]
+
+    def _load(self, pluginData):
+        modinfo = self._findModule(pluginData.File)
+        try:
+            mod = __import__(modinfo[0])
+            mod = getattr(mod,self.__ptype)
+            tmp = getattr(mod, modinfo[1])
+            self.__plugins[pluginData.Name] = tmp.Plugin()
+            print "Loaded Module " + pluginData.File
+            return self.__plugins[pluginData.Name]
+        except Exception, e:
+            print e
+            print "Unable to load module" + pluginData.File
+
+        return None
+
+    #Property Methods
+    def _getPlugins(self):
+        return self.__plugins
+
+    def _getType(self):
+        return self.__ptype
+
+
+    #Properties
+    Plugins = property(_getPlugins, None)
+    Type = property(_getType, None)
+
+class ServerPluginsClass(BasePluginsClass):
+    def __init__(self, singletonKey):
+        if not isinstance(singletonKey, _SingletonKey):
+            raise invalid_argument(_("Use ServerPlugins to get access to singleton"))
+
+        BasePluginsClass.__init__(self, 'server')
+        self.initBase()
+
+    def preParseIncoming(self, xml_dom, data):
+        sent = True
+        errmsg = ""
+
+        for pluginName, pluginData in self.Plugins.iteritems():
+            if pluginData.Activated:
+                xml_dom, data = pluginData.preParseIncoming(xml_dom, data)
+
+        return xml_dom, data
+
+    def postParseIncoming(self, data):
+        for pluginName, pluginData in self.Plugins.iteritems():
+            if pluginData.Activated:
+                data = pluginData.postParseIncoming(data)
+
+        return data
+
+    def getPlayer(self):
+        players = []
+        for pluginName, pluginData in self.Plugins.iteritems():
+            if pluginData.Activated:
+                playerName = pluginData.addPlayer(data)
+                players.append(playerName)
+
+        return players
+
+    def setPlayer(self, playerData):
+        players = []
+        for pluginName, pluginData in self.Plugins.iteritems():
+            if pluginData.Activated:
+                playerName = pluginData.addPlayer(data)
+                players.append(playerName)
+
+        return
+
+    def preParseOutgoing(self):
+        data = []
+        for pluginName, pluginData in self.Plugins.iteritems():
+            if pluginData.Activated:
+                xml = pluginData.preParseOutgoing()
+                for msg in xml:
+                    data.append(msg)
+
+        return data
+
+    def postParseOutgoing(self):
+        data = []
+        for pluginName, pluginData in self.Plugins.iteritems():
+            if pluginData.Activated:
+                xml = pluginData.postParseOutgoing()
+                for msg in xml:
+                    data.append(msg)
+
+        return data
+
+__key = _SingletonKey()
+ServerPlugins = ServerPluginsClass(__key)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/orpgCore.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# Copyright (C) 2000-2006 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: main.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: orpgCore.py,v 1.8 2006/11/12 00:10:37 digitalxero Exp $
+#
+# Description: This is the core functionality that is used by both the client and server.
+#               As well as everything in here should be global to every file
+#
+
+__version__ = "$Id: orpgCore.py,v 1.8 2006/11/12 00:10:37 digitalxero Exp $"
+
+import time
+from string import *
+import os
+import os.path
+import thread
+import traceback
+import sys
+import systempath
+import re
+import string
+import urllib
+import webbrowser
+import random
+
+#########################
+## Error Types
+#########################
+ORPG_CRITICAL       = 1
+ORPG_GENERAL        = 2
+ORPG_INFO           = 4
+ORPG_NOTE           = 8
+ORPG_DEBUG          = 16
+
+########################
+## openrpg object
+########################
+
+class ORPGStorage(object):
+    __components = {}
+
+    def add_component(self, key, com):
+        self.__components[key] = com
+
+    def get_component(self, key):
+        if self.__components.has_key(key):
+            return self.__components[key]
+        else:
+            return None
+
+def singleton(cls):
+    instances = {}
+    def getinstance():
+        if cls not in instances:
+            instances[cls] = cls()
+        return instances[cls]
+    return getinstance
+
+ORPGStorage = singleton(ORPGStorage)
+open_rpg = ORPGStorage()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/orpg_version.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,20 @@
+VERSION = "1.7.9"
+SERVER_MIN_CLIENT_VERSION = "1.7.1"
+
+#BUILD NUMBER FORMAT: "YYMMDD-##" where ## is the incremental daily build index (if needed)
+DISTRO = "Traipse"
+DIS_VER = "Grumpy Goblin"
+BUILD = "090713-02"
+
+# This version is for network capability.
+PROTOCOL_VERSION = "1.2"
+
+CLIENT_STRING = DISTRO + ' {' + BUILD + '}'
+MENU_TITLE = DISTRO + " " + DIS_VER + ' {' + BUILD + '}'
+
+# These two are used in pyver.py to ensure a minimum python is available
+# If the minimum version you want doesn't have a micro (e.g. 2.0), use zero
+# for the micro
+NEEDS_PYTHON_MAJOR = 2
+NEEDS_PYTHON_MINOR = 3
+NEEDS_PYTHON_MICRO = 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/orpg_windows.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,655 @@
+# 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: orpg_windows.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: orpg_windows.py,v 1.42 2007/12/07 20:59:16 digitalxero Exp $
+#
+# Description: orpg custom windows
+#
+
+__version__ = "$Id: orpg_windows.py,v 1.42 2007/12/07 20:59:16 digitalxero Exp $"
+
+from orpg.orpg_wx import *
+from orpg.orpgCore import *
+import orpg.tools.rgbhex
+import orpg.orpg_xml
+import orpg.dirpath
+from orpg.tools.metamenus import MenuEx
+
+class img_helper:
+    def __init__(self):
+        pass
+
+    def load_url(self,path):
+        img_type = self.get_type(path)
+        try:
+            data = urllib.urlretrieve(path)
+            if data:
+                img = wx.Bitmap(data[0], img_type)
+            else:
+                raise IOError, "Image refused to load!"
+        except IOError, e:
+            img = None
+        return img
+
+    def load_file(self,path):
+        img_type = self.get_type(path)
+        return wx.Bitmap(path, img_type)
+
+    def get_type(self,file_name):
+        pos = string.rfind(file_name,'.')
+        ext = string.lower(file_name[pos+1:])
+        img_type = 0
+	# TaS - sirebral.  Replaces 10 lines with 6 lines.
+	recycle_bin = {"gif": wx.BITMAP_TYPE_GIF, "jpg": wx.BITMAP_TYPE_JPEG, "jpeg": wx.BITMAP_TYPE_JPEG, "bmp": wx.BITMAP_TYPE_BMP, "png": wx.BITMAP_TYPE_PNG}
+	if recycle_bin.has_key(ext):
+	    img_type = recycle_bin[ext]
+	else:
+	    img_type = None ## this was imf_type = None.  imf?
+	recycle_bin = {}; return img_type
+
+################################
+## Tabs
+################################
+class orpgTabberWnd(FNB.FlatNotebook):
+    def __init__(self, parent, closeable=False, size=wx.DefaultSize, style = False):
+        nbstyle = FNB.FNB_HIDE_ON_SINGLE_TAB|FNB.FNB_BACKGROUND_GRADIENT
+        FNB.FlatNotebook.__init__(self, parent, -1, size=size, style=nbstyle)
+        rgbcovert = orpg.tools.rgbhex.RGBHex()
+        self.log = open_rpg.get_component("log")
+        self.log.log("Enter orpgTabberWnd", ORPG_DEBUG)
+        self.settings = open_rpg.get_component("settings")
+        tabtheme = self.settings.get_setting('TabTheme')
+        tabtext = self.settings.get_setting('TabTextColor')
+        (tred, tgreen, tblue) = rgbcovert.rgb_tuple(tabtext)
+        tabbedwindows = open_rpg.get_component("tabbedWindows")
+        tabbedwindows.append(self)
+        open_rpg.add_component("tabbedWindows", tabbedwindows)
+
+        theme_dict = {'slanted&aqua': FNB.FNB_VC8, 'slanted&bw': FNB.FNB_VC8, 'flat&aqua': FNB.FNB_FANCY_TABS, 'flat&bw': FNB.FNB_FANCY_TABS, 'customflat': FNB.FNB_FANCY_TABS, 'customslant': FNB.FNB_VC8, 'slanted&colorful': FNB.FNB_VC8|FNB.FNB_COLORFUL_TABS, 'slant&colorful': FNB.FNB_VC8|FNB.FNB_COLORFUL_TABS}
+        nbstyle |= theme_dict[tabtheme]
+        if style:
+            nbstyle |= style
+        self.SetWindowStyleFlag(nbstyle)
+
+	#Tas - sirebral.  Planned changes to the huge statement below.  
+        if tabtheme == 'slanted&aqua':
+            self.SetGradientColourTo(wx.Color(0, 128, 255))
+            self.SetGradientColourFrom(wx.WHITE)
+
+        elif tabtheme == 'slanted&bw':
+            self.SetGradientColourTo(wx.WHITE)
+            self.SetGradientColourFrom(wx.WHITE)
+
+        elif tabtheme == 'flat&aqua':
+            self.SetGradientColourFrom(wx.Color(0, 128, 255))
+            self.SetGradientColourTo(wx.WHITE)
+            self.SetNonActiveTabTextColour(wx.BLACK)
+
+        elif tabtheme == 'flat&bw':
+            self.SetGradientColourTo(wx.WHITE)
+            self.SetGradientColourFrom(wx.WHITE)
+            self.SetNonActiveTabTextColour(wx.BLACK)
+
+        elif tabtheme == 'customflat':
+            gfrom = self.settings.get_setting('TabGradientFrom')
+            (red, green, blue) = rgbcovert.rgb_tuple(gfrom)
+            self.SetGradientColourFrom(wx.Color(red, green, blue))
+
+            gto = self.settings.get_setting('TabGradientTo')
+            (red, green, blue) = rgbcovert.rgb_tuple(gto)
+            self.SetGradientColourTo(wx.Color(red, green, blue))
+            self.SetNonActiveTabTextColour(wx.Color(tred, tgreen, tblue))
+
+        elif tabtheme == 'customslant':
+            gfrom = self.settings.get_setting('TabGradientFrom')
+            (red, green, blue) = rgbcovert.rgb_tuple(gfrom)
+            self.SetGradientColourFrom(wx.Color(red, green, blue))
+
+            gto = self.settings.get_setting('TabGradientTo')
+            (red, green, blue) = rgbcovert.rgb_tuple(gto)
+            self.SetGradientColourTo(wx.Color(red, green, blue))
+            self.SetNonActiveTabTextColour(wx.Color(tred, tgreen, tblue))
+
+        tabbg = self.settings.get_setting('TabBackgroundGradient')
+        (red, green, blue) = rgbcovert.rgb_tuple(tabbg)
+        self.SetTabAreaColour(wx.Color(red, green, blue))
+        self.Refresh()
+        self.log.log("Exit orpgTabberWnd", ORPG_DEBUG)
+
+
+########################
+## About HTML Dialog
+########################
+
+class AboutHTMLWindow(wx.html.HtmlWindow):
+    "Window used to display the About dialog box"
+
+    # Init using the derived from class
+    def __init__( self, parent, id, position, size, style ):
+        wx.html.HtmlWindow.__init__( self, parent, id, position, size, style )
+
+    def OnLinkClicked( self, ref ):
+        "Open an external browser to resolve our About box links!!!"
+        href = ref.GetHref()
+        webbrowser.open( href )
+
+#  This class extends wxSplitterWindow to add an auto expand behavior.  The idea is that the sash
+#       determines the ratio of the two windows, while the mouse position determines which
+#       side will get the larger share of the screen real estate.  It is used instead of regular
+#       wxSplitterWindows if the EnableSplittersAutoExpand setting doesn't evaluate as False.
+#
+#  Note:  To be truly functional, some way of passing EVT_MOTION events to this class, even when the
+#       event takes place over child windows needs to be accomplished.  Once this is accomplished,
+#       however, the class should work as written.
+class orpgFocusSplitterWindow(wx.SplitterWindow):
+
+    def __init__(self,parent,id = -1,AutoExpand = 1,point = wx.DefaultPosition,size = wx.DefaultSize,style=wx.SP_3D,name="splitterWindow"):
+        wx.SplitterWindow.__init__(self,parent,id,point,size,style,name)
+        self.auto = AutoExpand
+        self.Bind(wx.EVT_IDLE, self.OnIdle)  # used in workaround idea from Robin Dunn instead of EVT_MOTION
+
+    #  Get's called during idle times.  It checks to see if the mouse is over self and calls
+    #      OnMotion with the coordinates
+
+    def EnableAutoExpand(self,value):
+        self.auto = value
+
+    def OnIdle(self,event):
+        if self.auto:
+            (screen_x,screen_y) = wx.GetMousePosition()
+            (x,y) = self.ScreenToClientXY(screen_x,screen_y) # translate coordinates
+            (w,h) = self.GetSizeTuple()
+            if x >= 0 and x < w and y >= 0 and y < h:
+                self.OnMotion(x,y)
+        event.Skip()
+
+    def OnMotion(self,mouse_X,mouse_Y):
+        # Gather some info using standard wxWindows calls
+        (w,h) = self.GetClientSizeTuple()
+        (second_w,second_h) = self.GetWindow2().GetClientSizeTuple()
+        (second_x,second_y) = self.GetWindow2().GetPositionTuple()
+        splitmode = self.GetSplitMode()
+        sash = self.GetSashPosition()
+
+        if splitmode == wx.SPLIT_VERTICAL:
+            pos = mouse_X #  Position of the mouse pointer
+            second = second_x  #  Beginning of the second (Right) pane
+            second_size = second_w  # Size of the second pane
+        else:
+            pos = mouse_Y #  Position of the mouse pointer
+            second = second_y  #  Beginning of the second (Bottom) pane
+            second_size = second_h  # Size of the second pane
+        sash_size = second - sash    # Beginning of sash to beginning of second is the sash size
+
+        if (pos > sash + sash_size and second_size < sash) or (pos < sash and second_size > sash):
+            #  Equivalent to the following
+            #  if
+            #  (the mouse position is below/to the right of the sash, including it's thickness
+            #      i.e. in the second window
+            #  AND
+            #  the second window is smaller than the 1st (size = the sash position))
+            #
+            #  OR
+            #
+            #  (the mouse position is above/to the left of the sash
+            #      i.e. in the first window
+            #  AND
+            #  the second window is bigger than the 1st)
+
+
+            # flip the split
+            self.SetSashPosition(second_size)
+            #  Both cases above set the sash to a position that corresponds to the size of the
+            #  second window.  This has the effect of making the first window trade sizes with
+            #  the second window.
+            #      In the first part of the OR clause, the first window takes the second window's
+            #      size because the user wants the currently small lower window to be big, so
+            #      the first must take on the size of the small.
+            #
+            #      In the second case of the OR clause, the first window takes the second window's
+            #      size because the user wants the currently small upper window to be big (which
+            #      the second window currently is), so make the first take the size of the second.
+
+
+#####################
+## A text editor for openrpg related text
+#####################
+
+class html_text_edit(wx.Panel):
+    """ a text ctrl with html helpers """
+    def __init__(self, parent, id, text, callback,home_dir):
+        wx.Panel.__init__(self, parent,-1)
+        self.r_h = orpg.tools.rgbhex.RGBHex()
+        self.text = wx.TextCtrl(self, id, text, wx.DefaultPosition,
+                             wx.DefaultSize,
+                             wx.TE_MULTILINE )
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+        self.Bind(wx.EVT_TEXT, callback, id=id)
+        self.callback = callback
+        self.BOLD = wx.NewId()
+        self.ITALIC = wx.NewId()
+        self.UNDER = wx.NewId()
+        self.COLOR = wx.NewId()
+        self.DIE100 = wx.NewId()
+        self.DIE20 = wx.NewId()
+        self.DIE10 = wx.NewId()
+        self.DIE8 = wx.NewId()
+        self.DIE6 = wx.NewId()
+        self.DIE4 = wx.NewId()
+        self.DIE2 = wx.NewId()
+        self.DIE = wx.NewId()
+        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+        gif = wx.Image(orpg.dirpath.dir_struct["icon"]+"bold.gif", wx.BITMAP_TYPE_GIF)
+        self.sizer.Add(wx.BitmapButton(self, self.BOLD, gif.ConvertToBitmap()), 0, wx.EXPAND)
+        gif = wx.Image(orpg.dirpath.dir_struct["icon"]+"italic.gif", wx.BITMAP_TYPE_GIF)
+        self.sizer.Add(wx.BitmapButton(self, self.ITALIC, gif.ConvertToBitmap()), 0, wx.EXPAND)
+        gif = wx.Image(orpg.dirpath.dir_struct["icon"]+"underlined.gif", wx.BITMAP_TYPE_GIF)
+        self.sizer.Add(wx.BitmapButton(self, self.UNDER, gif.ConvertToBitmap()), 0, wx.EXPAND)
+        self.color_button = wx.Button(self, self.COLOR, "C",wx.Point(0,0),wx.Size(22,0))
+        self.color_button.SetBackgroundColour(wx.BLACK)
+        self.color_button.SetForegroundColour(wx.WHITE)
+        self.sizer.Add(self.color_button, 0, wx.EXPAND)
+        self.Bind(wx.EVT_BUTTON, self.on_text_format, id=self.BOLD)
+        self.Bind(wx.EVT_BUTTON, self.on_text_format, id=self.ITALIC)
+        self.Bind(wx.EVT_BUTTON, self.on_text_format, id=self.UNDER)
+        self.Bind(wx.EVT_BUTTON, self.on_text_format, id=self.COLOR)
+
+    def on_text_format(self,event):
+        id = event.GetId()
+        if wx.Platform == '__WXMSW__':
+            txt = self.text.GetLabel()
+        else:
+            txt = self.text.GetValue()
+        (beg,end) = self.text.GetSelection()
+        if beg != end:
+            sel_txt = txt[beg:end]
+        else:
+            return
+        print txt
+	# TaS - sirebral. Replaces 6 lines with 4 lines.
+	recycle_bin = {self.BOLD: "b", self.ITALIC: "i", self.UNDER: "u"}
+	if recycle_bin.has_key(id):
+	    sel_txt = "<" + recycle_bin[id] + ">" + sel_txt + "</" + recycle_bin[id] + ">"
+	    recycle_bin = {}
+
+        elif id == self.COLOR:
+            hexcolor = self.r_h.do_hex_color_dlg(self)
+            if hexcolor:
+                sel_txt = "<font color='"+hexcolor+"'>"+sel_txt+"</font>"
+                self.color_button.SetBackgroundColour(hexcolor)
+
+        txt = txt[:beg] + sel_txt + txt[end:]
+       # print txt
+        if wx.Platform == '__WXMSW__':
+            txt = self.text.SetLabel(txt)
+        else:
+            txt = self.text.SetValue(txt)
+        self.text.SetInsertionPoint(beg)
+        self.text.SetFocus()
+        self.callback(wx.Event(self.text.GetId()))
+
+    def set_text(self,txt):
+        self.text.SetValue(txt)
+
+    def get_text(self):
+        return self.text.GetValue()
+
+    def OnSize(self,event):
+        (w,h) = self.GetClientSizeTuple()
+        self.text.SetDimensions(0,0,w,h-25)
+        self.sizer.SetDimension(0,h-25,w,25)
+
+###########################
+## HTML related clasees
+###########################
+
+class http_html_window(wx.html.HtmlWindow):
+    """ a wx.html.HTMLwindow that will load links  """
+    def __init__(self, parent, id):
+        wx.html.HtmlWindow.__init__(self, parent, id, wx.DefaultPosition,wx.DefaultSize, wx.SUNKEN_BORDER | wx.html.HW_SCROLLBAR_AUTO)
+        self.path = ""
+        self.local = 0
+        #self.title = title
+
+    def OnLinkClicked(self, linkinfo):
+        address = linkinfo.GetHref()
+        if address[:4] == "http":
+            self.load_url(address)
+            self.local = 0
+        elif address[0] == "#" or self.local:
+            self.base_OnLinkClicked(linkinfo)
+        else:
+            self.load_url(self.path+address)
+
+    def load_url(self,path):
+        print path
+        dlg = wx.ProgressDialog("HTML Document","Loading...",3,self)
+        dlg.Update(1)
+        try:
+            data = urllib.urlretrieve(path)
+            file = open(data[0])
+            dlg.Update(2)
+            self.SetPage(file.read())
+            i = string.rfind(path,"/")
+            self.path = path[:i+1]
+        except:
+            wx.MessageBox("Invalid URL","Browser Error",wx.OK)
+            #self.SetPage("<h3>Invalid URL</h3>")
+        dlg.Update(3)
+        dlg.Destroy()
+
+    def load_file(self,path):
+        self.LoadPage(path)
+        self.local = 1
+
+###########################
+## Some misc dialogs
+###########################
+
+class orpgMultiCheckBoxDlg(wx.Dialog):
+    """ notes """
+    def __init__(self, parent, opts, text, caption, selected=[], pos=wx.DefaultPosition):
+        wx.Dialog.__init__(self, parent, wx.ID_ANY, caption, pos, wx.DefaultSize)
+        sizers = { 'ctrls' : wx.BoxSizer(wx.VERTICAL), 'buttons' : wx.BoxSizer(wx.HORIZONTAL) }
+        self.opts = opts
+        self.list = wx.CheckListBox(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize,opts)
+        for s in selected:
+            self.list.Check(s,1)
+        sizers['ctrls'].Add(wx.StaticText(self, -1, text), 0, 0)
+        sizers['ctrls'].Add(wx.Size(10,10))
+        sizers['ctrls'].Add(self.list, 1, wx.EXPAND)
+        sizers['buttons'].Add(wx.Button(self, wx.ID_OK, "OK"), 1, wx.EXPAND)
+        sizers['buttons'].Add(wx.Size(10,10))
+        sizers['buttons'].Add(wx.Button(self, wx.ID_CANCEL, "Cancel"), 1, wx.EXPAND)
+        sizers['ctrls'].Add(sizers['buttons'], 0, wx.EXPAND)
+        self.SetSizer(sizers['ctrls'])
+        self.SetAutoLayout(True)
+        self.Fit()
+        self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)
+
+    def on_ok(self,evt):
+        checked = []
+        for i in range(len(self.opts)):
+            if self.list.IsChecked(i):
+                checked.append(i)
+        self.checked = checked
+        self.EndModal(wx.ID_OK)
+
+    def get_selections(self):
+        return self.checked
+
+
+class orpgMultiTextEntry(wx.Dialog):
+    """ a dialog that takes two lists (labels and values) and creates a
+        'label: value' style text edit control for each node in the dic"""
+    def __init__(self,parent,tlist,vlist,caption,pos=wx.DefaultPosition):
+        wx.Dialog.__init__(self,parent,-1,caption,pos,wx.DefaultSize)
+        num = len(tlist)
+        sizers = { 'ctrls' : wx.FlexGridSizer(num,2,5,0),
+                    'buttons' : wx.BoxSizer(wx.HORIZONTAL) }
+        #keys = mlist.keys()
+        self.tlist = tlist
+        self.vlist = vlist
+        add_list = []
+        ctrls = []
+        for i in range(len(tlist)):
+            add_list.append((wx.StaticText(self, -1, tlist[i]+": "),0,wx.ALIGN_CENTER_VERTICAL ))
+            ctrls.append(wx.TextCtrl(self, 10, vlist[i]))
+            add_list.append((ctrls[i],1,wx.EXPAND))
+        self.ctrls = ctrls
+        sizers['ctrls'].AddMany(add_list)
+        sizers['ctrls'].AddGrowableCol(1)
+        sizers['buttons'].Add(wx.Button(self, wx.ID_OK, "OK"), 1, wx.EXPAND)
+        sizers['buttons'].Add(wx.Size(10,10))
+        sizers['buttons'].Add(wx.Button(self, wx.ID_CANCEL, "Cancel"), 1, wx.EXPAND)
+        width = 300
+        (w,h) = ctrls[0].GetSizeTuple()
+        h = h + 5
+        height = ((num)*h)+35
+        self.SetClientSizeWH(width,height)
+        sizers['ctrls'].SetDimension(10,5,width-20,num*30)
+        sizers['buttons'].SetDimension(10,(num*h)+5,width-20,25)
+        self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)
+
+    def on_ok(self,evt):
+        for i in range(len(self.ctrls)):
+            self.vlist[i] = self.ctrls[i].GetValue()
+        self.EndModal(wx.ID_OK)
+
+    def get_values(self):
+        return self.vlist
+
+class orpgScrolledMessageFrameEditor(wx.Frame):
+    "class to implement wxScrolledMessageFrame with Find feature for the text of chatbuffer in a popup"
+    def __init__(self, parent, msg, caption, pos = None, size = None):
+        ID_ORPGTEXTCTRL = wx.NewId()
+        ID_MATCHINSTRUCTION = wx.NewId()
+        ID_MATCHINPUT = wx.NewId()
+        ID_MATCHBUTTON = wx.NewId()
+        ID_MATCHCASEINSTRUCTION = wx.NewId()
+        ID_MATCHCASECHECKBOX = wx.NewId()
+        ID_DEHTML = wx.NewId()
+        wx.Frame.__init__(self, parent, -1, caption, pos=wx.DefaultPosition, size=(640,480))
+        self.SetBackgroundColour(wx.WHITE)
+        self.text = wx.TextCtrl(self, ID_ORPGTEXTCTRL, msg, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE )
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.sizer.Add(self.text, 1, wx.EXPAND)
+        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
+        self.matchInstruction = wx.StaticText(self, ID_MATCHINSTRUCTION, "Text to search for: ")
+        self.sizer.Add(self.matchInstruction, 0, wx.ALIGN_CENTER_VERTICAL)
+        self.matchCaseInstruction = wx.StaticText(self, ID_MATCHCASEINSTRUCTION, "Match case:")
+        sizer1.Add(self.matchCaseInstruction, 0, wx.ALIGN_CENTER_VERTICAL)
+        self.matchCaseCheckBox = wx.CheckBox(self, ID_MATCHCASECHECKBOX, "")
+        sizer1.Add(self.matchCaseCheckBox, 0, wx.ALIGN_CENTER_VERTICAL)
+        self.matchInput = wx.TextCtrl(self, ID_MATCHINPUT, "")
+        sizer1.Add(self.matchInput, 1, wx.EXPAND)
+        self.sizer.Add(sizer1, 0, wx.EXPAND)
+        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
+        self.matchButton = wx.Button(self, ID_MATCHBUTTON, "Find")
+        self.Bind(wx.EVT_BUTTON, self.OnMatchMe, id=ID_MATCHBUTTON)
+        sizer2.Add(self.matchButton, 1, wx.EXPAND)
+        self.dehtmlButton = wx.Button(self, ID_DEHTML, "Remove HTML")
+        self.Bind(wx.EVT_BUTTON, self.OnDeHTML, id=ID_DEHTML)
+        sizer2.Add(self.dehtmlButton, 1, wx.EXPAND)
+        self.cancel = wx.Button(self, wx.ID_CANCEL, "Cancel")
+        self.Bind(wx.EVT_BUTTON, self.OnCloseMe, id=wx.ID_CANCEL)
+        sizer2.Add(self.cancel, 1, wx.EXPAND)
+        self.sizer.Add(sizer2, 0, wx.EXPAND)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        #self.Bind(wx.EVT_SIZE, self.OnSize)
+
+        # current position
+        self.matchPosition = 0
+        # former position
+        self.matchPositionOld = 0
+
+    def OnDeHTML(self, event):
+        text = re.sub( "\<[^<]*?\>", "", self.text.GetValue() )
+        self.text.SetValue(text)
+
+    def OnMatchMe(self, event):
+        # match case sensitive
+        if self.matchCaseCheckBox.GetValue() == 1:
+            textValue = self.text.GetValue()
+            matchValue = self.matchInput.GetValue()
+        # match case insensitive
+        else:
+            textValue = string.upper(self.text.GetValue())
+            matchValue = string.upper(self.matchInput.GetValue())
+
+        # continue search from insertion point instead of top
+        self.matchPosition = self.matchPositionOld = self.text.GetInsertionPoint()
+
+        # find search string in chatbuffer
+        self.matchPosition = string.find(textValue[self.matchPositionOld:], matchValue)
+        # cumulate position for substring matching in continuing search
+        self.matchPositionOld = self.matchPositionOld + self.matchPosition
+
+        # if match was found
+        if self.matchPosition >= 0:
+            # highlight(select) match
+            self.text.SetSelection(self.matchPositionOld, self.matchPositionOld + len(matchValue))
+            # continue search from end of match
+            self.text.SetInsertionPoint(self.matchPositionOld + len(matchValue))
+        # if match was not found, but match exists somewhere in buffer, start from top
+        elif string.find(textValue, matchValue) >= 0:
+            self.text.SetInsertionPoint(0)
+            self.OnMatchMe(self)
+
+    def OnCloseMe(self, event):
+        self.Close(True)
+
+    def OnCloseWindow(self, event):
+        self.Destroy()
+
+class orpgProgressDlg(wx.Dialog):
+    def __init__(self, parent,  title="", text="", range=10 ):
+        wx.Dialog.__init__(self, parent, -1, title, size= wx.Size(200,75))
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.text = wx.StaticText( self, -1, text)
+        self.gauge = wx.Gauge(self,-1,range)
+        self.sizer.Add(self.text,1,wx.ALIGN_CENTER | wx.EXPAND)
+        self.sizer.Add(self.gauge,1,wx.ALIGN_CENTER | wx.EXPAND)
+        (w,h) = self.GetClientSizeTuple()
+        self.sizer.SetDimension(10,10,w-20,h-20)
+
+    def Update(self,pos,text=None):
+        self.gauge.SetValue(pos)
+        if text:
+            self.text.SetLabel(text)
+
+#########################
+#status frame window
+#########################
+class status_bar(wx.StatusBar):
+    def __init__(self, parent):
+        wx.StatusBar.__init__(self, parent, -1)
+        GENERAL_MENU = 1
+        URL_MENU = 2
+        self.parent = parent
+        self.connect_status = "Not Connected"
+        self.urlis = ""
+        self.window = 1
+        self.menu = wx.Menu("Switch layout to...")
+        item = wx.MenuItem(self.menu, wx.ID_ANY, "General", "General", wx.ITEM_CHECK)
+        self.Bind(wx.EVT_MENU, self.OnM_SwitchlayouttoGeneral, item)
+        self.menu.AppendItem(item)
+        item = wx.MenuItem(self.menu, wx.ID_ANY, "Url Display", "Url Display", wx.ITEM_CHECK)
+        self.Bind(wx.EVT_MENU, self.OnM_SwitchlayouttoUrlDisplay, item)
+        self.menu.AppendItem(item)
+
+        #menu = [["Switch layout to..."],
+        #     ["  General"],
+        #     ["  Url Display"]]
+        #self.menu = MenuEx(self, menu)
+
+        self.Bind(wx.EVT_RIGHT_DOWN, self.onPopup)
+        self.widths = [-1,200]
+        (msgwidth,msgheight) = self.GetTextExtent(`self.connect_status`)
+        #parent.SetClientSize(wx.Size(450,msgheight+8))
+        #self.SetClientSize(wx.Size(450,msgheight+7))
+        self.SetFieldsCount(2)
+        self.timer = wx.Timer(self, wx.NewId())
+        self.Bind(wx.EVT_TIMER, self.Notify)
+        self.timer.Start(3000)
+
+    def onPopup(self, evt):
+        self.PopupMenu(self.menu)
+
+    def OnM_SwitchlayouttoUrlDisplay(self, evt):
+        self.window = 2
+        self.bar1()
+
+    def OnM_SwitchlayouttoGeneral(self, evt):
+        self.window = 1
+        self.bar0()
+
+    def set_connect_status(self,connect):
+        self.connect_status = connect
+
+    def Notify(self, event):
+        if self.window == 1:
+            self.bar0()
+        elif self.window == 2:
+            self.bar1()
+        pass
+
+    def bar1(self):
+        self.SetFieldsCount(1)
+        self.widths = [-1]
+        self.SetStatusWidths(self.widths)
+        self.SetStatusText("URL: " + self.urlis, 0)
+
+    def bar0(self):
+        self.SetFieldsCount(2)
+        self.widths = [-1,200]
+        t = time.gmtime(time.time())
+        st = time.strftime("GMT: %d-%b-%Y  %I:%M:%S", t)
+        #(x,y) = self.GetTextExtent(self.connect_status)
+        #self.widths[0] = x+10
+        (x,y) = self.GetTextExtent(st)
+        self.widths[1] =  x+10
+        self.SetStatusWidths(self.widths)
+        self.SetStatusText(self.connect_status, 0)
+        self.SetStatusText(st, 1)
+
+    def set_url(self, url):
+        self.urlis = url
+
+    def __del__(self):
+        self.timer.Stop()
+        del self.timer
+
+#####################
+## Some misc utilties for the GUI
+#####################
+
+def do_progress_dlg(parent,title,text,range):
+    " Returns a new progress dialog"
+    if wx.Platform == '__WXMSW__':
+        dlg = orpgProgressDlg(parent,title,text,range)
+        dlg.Centre()
+        dlg.Show(1)
+        dlg.Raise()
+    else:
+        dlg = wx.ProgressDialog(title,text,range,parent)
+    return dlg
+
+def parseXml_with_dlg(parent,s,ownerDocument=None):
+    "Parse xml with progress dialog"
+    dlg = do_progress_dlg(parent,"XML Parser","Reading Configuration Files...",2)
+    #dlg.Update(1)
+    doc = orpg.orpg_xml.parseXml(s)
+    dlg.Update(1,"Done.")
+    dlg.Destroy()
+    return doc
+
+def createMaskedButton( parent, image, tooltip, id, mask_color=wx.WHITE, image_type=wx.BITMAP_TYPE_GIF ):
+    gif = wx.Image( image, image_type, ).ConvertToBitmap()
+    mask = wx.Mask( gif, mask_color )
+    gif.SetMask( mask )
+    btn = wx.BitmapButton( parent, id, gif )
+    btn.SetToolTip( wx.ToolTip( tooltip ) )
+    return btn
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/orpg_wx.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,38 @@
+try:
+    import wxversion
+    wxversion.select(["2.6", "2.7", "2.8"])
+    import wx
+    import wx.html
+    import wx.lib.wxpTag
+    import wx.grid
+    import wx.media
+    from wx.lib.filebrowsebutton import  *
+    try:
+        import wx.aui as AUI
+    except:
+        import orpg.tools.PyAUI as AUI
+    if wx.VERSION_STRING < "2.7.2" and wx.VERSION_STRING > "2.7.0":
+        AUI.AuiManager = AUI.FrameManager
+        AUI.AuiManagerEvent = AUI.FrameManagerEvent
+        AUI.AuiPaneInfo = AUI.PaneInfo
+        AUI.AuiFloatingPane = AUI.FloatingPane
+    try:
+        import wx.lib.flatnotebook as FNB
+        if FNB.FNB_FF2:
+            pass
+    except:
+        import orpg.tools.FlatNotebook as FNB
+    if wx.VERSION_STRING < "2.7":
+        wx.Rect.Contains = lambda self, point: wx.Rect.Inside(self, point)
+
+    WXLOADED = True
+except ImportError:
+    WXLOADED = False
+    print "*WARNING* failed to import wxPython."
+    print "Download the correct version here: http://openrpg.digitalxero.net/"
+    print "If you are running the server with no gui you can ignore this message"
+except Exception, e:
+    WXLOADED = False
+    print e
+    print "\nYou do not have the correct version of wxPython installed, please"
+    print "Download the correct version here: http://openrpg.digitalxero.net/"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/orpg_xml.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,74 @@
+# 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: orpg_xml.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: orpg_xml.py,v 1.12 2007/07/19 20:33:10 digitalxero Exp $
+#
+# Description: xml utilies
+#
+
+from orpg import minidom
+import string
+
+def toxml(root,pretty=0):
+    return root.toxml(pretty)
+
+def parseXml(s):
+    "parse and return doc"
+    try:
+        doc = minidom.parseString(s)
+        doc.normalize()
+        return doc
+    except Exception, e:
+        print e
+        return None
+
+def safe_get_text_node(xml_dom):
+    """ returns the child text node or creates one if doesnt exist """
+    t_node = xml_dom._get_firstChild()
+    if t_node == None:
+        t_node = minidom.Text("")
+        t_node = xml_dom.appendChild(t_node)
+    return t_node
+
+def strip_unicode(txt):
+    for i in xrange(len(txt)):
+        if txt[i] not in string.printable:
+            try:
+                txt = txt.replace(txt[i], '&#' + str(ord(txt[i])) + ';')
+            except:
+                txt = txt.replace(txt[i], '{?}')
+    return txt
+
+def strip_text(txt):
+    #  The following block strips out 8-bit characters
+    u_txt = ""
+    bad_txt_found = 0
+    txt = strip_unicode(txt)
+    for c in txt:
+        if ord(c) < 128:
+            u_txt += c
+        else:
+            bad_txt_found = 1
+    if bad_txt_found:
+        print "Some non 7-bit ASCII characters found and stripped"
+    return u_txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/player_list.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,572 @@
+#!/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: player_list.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: player_list.py,v 1.29 2007/03/30 19:12:06 digitalxero Exp $
+#
+# Description: This is the main entry point of the oprg application
+#
+
+__version__ = "$Id: player_list.py,v 1.29 2007/03/30 19:12:06 digitalxero Exp $"
+
+from orpg.orpg_windows import *
+import orpg.dirpath
+
+# global definitions
+global ROLE_GM; ROLE_GM = "GM"
+global ROLE_PLAYER; ROLE_PLAYER = "PLAYER"
+global ROLE_LURKER; ROLE_LURKER = "LURKER"
+
+#########################
+#player frame window
+#########################
+PLAYER_BOOT = wx.NewId()
+PLAYER_WHISPER = wx.NewId()
+PLAYER_IGNORE = wx.NewId()
+PLAYER_ROLE_MENU = wx.NewId()
+PLAYER_ROLE_LURKER = wx.NewId()
+PLAYER_ROLE_PLAYER = wx.NewId()
+PLAYER_ROLE_GM = wx.NewId()
+PLAYER_MODERATE_MENU = wx.NewId()
+PLAYER_MODERATE_ROOM_ON = wx.NewId()
+PLAYER_MODERATE_ROOM_OFF = wx.NewId()
+PLAYER_MODERATE_GIVE_VOICE = wx.NewId()
+PLAYER_MODERATE_TAKE_VOICE = wx.NewId()
+PLAYER_SHOW_VERSION = wx.NewId()
+WG_LIST = {}
+
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+PLAYER_WG_MENU = wx.NewId()
+PLAYER_WG_CREATE = wx.NewId()
+PLAYER_WG_CLEAR_ALL = wx.NewId()
+WG_MENU_LIST = {}
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+
+#---------------------------------------------------------
+# [START] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+PLAYER_COMMAND_MENU = wx.NewId()
+PLAYER_COMMAND_PASSWORD_ALTER = wx.NewId()
+PLAYER_COMMAND_ROOM_RENAME = wx.NewId()
+#---------------------------------------------------------
+# [END] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+
+class player_list(wx.ListCtrl):
+    def __init__( self, parent):
+##        wx.ListCtrl.__init__( self, parent, -1, wx.DefaultPosition, wx.DefaultSize, wx.LC_REPORT|wx.SUNKEN_BORDER|wx.EXPAND )
+        wx.ListCtrl.__init__( self, parent, -1, wx.DefaultPosition, wx.DefaultSize, wx.LC_REPORT|wx.SUNKEN_BORDER|wx.EXPAND|wx.LC_HRULES )
+        self.session = open_rpg.get_component("session")
+        self.settings = open_rpg.get_component('settings')
+        self.chat = open_rpg.get_component('chat')
+        self.password_manager = open_rpg.get_component("password_manager")
+        # Create in image list -- for whatever reason...guess it will be nice when we can tell which is a bot
+        self.whisperCount = 0
+        self._imageList = wx.ImageList( 16, 16, False )
+        img = wx.Image(orpg.dirpath.dir_struct["icon"]+"player.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        self._imageList.Add( img )
+        img = wx.Image(orpg.dirpath.dir_struct["icon"]+"player-whisper.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        self._imageList.Add( img )
+        self.SetImageList( self._imageList, wx.IMAGE_LIST_SMALL )
+        # Create our column headers
+        self.InsertColumn( 0, "ID" )
+        self.InsertColumn( 1, "Player" )
+        self.InsertColumn( 2, "Status" )
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+        ##Main Menu
+        self.wgMenu = wx.Menu()
+        #Add the Base Menu items, so they are always at the bottom
+        self.wgMenu.Append(PLAYER_WG_CREATE, "Create")
+        self.wgMenu.Append(PLAYER_WG_CLEAR_ALL, "Delete All Groups")
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+        # Create the role menu
+        self.roleMenu = wx.Menu()
+        self.roleMenu.SetTitle( "Assign Role" )
+        self.roleMenu.Append( PLAYER_ROLE_LURKER, "Lurker" )
+        self.roleMenu.Append( PLAYER_ROLE_PLAYER, "Player" )
+        self.roleMenu.Append( PLAYER_ROLE_GM, "GM" )
+        # Create the moderation menu
+        self.moderateMenu = wx.Menu()
+        self.moderateMenu.SetTitle( "Moderate" )
+        self.moderateMenu.Append( PLAYER_MODERATE_ROOM_ON, "Room Moderation ON" )
+        self.moderateMenu.Append( PLAYER_MODERATE_ROOM_OFF, "Room Moderation OFF" )
+        self.moderateMenu.AppendSeparator()
+        self.moderateMenu.Append( PLAYER_MODERATE_GIVE_VOICE, "Give Voice" )
+        self.moderateMenu.Append( PLAYER_MODERATE_TAKE_VOICE, "Take Voice" )
+#---------------------------------------------------------
+# [START] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+        # Create the room control menu
+        self.commandMenu = wx.Menu()
+        self.commandMenu.SetTitle( "Room Controls" )
+        self.commandMenu.Append( PLAYER_COMMAND_PASSWORD_ALTER, "Password" )
+        self.commandMenu.Append( PLAYER_COMMAND_ROOM_RENAME, "Room Name" )
+        self.commandMenu.AppendSeparator()
+#---------------------------------------------------------
+# [END] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+        # Create the pop up menu
+        self.menu = wx.Menu()
+        self.menu.SetTitle( "Player Menu" )
+        self.menu.Append( PLAYER_BOOT, "Boot" )
+        self.menu.AppendSeparator()
+        self.menu.Append( PLAYER_IGNORE, "Toggle &Ignore" )
+        self.menu.AppendSeparator()
+        self.menu.Append( PLAYER_WHISPER, "Whisper" )
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+        self.menu.AppendMenu(PLAYER_WG_MENU, "Whisper Groups", self.wgMenu)
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+        self.menu.AppendSeparator()
+        self.menu.AppendMenu( PLAYER_MODERATE_MENU, "Moderate", self.moderateMenu )
+        self.menu.AppendMenu( PLAYER_COMMAND_MENU, "Room Control", self.commandMenu )
+        self.menu.AppendSeparator()
+        self.menu.AppendMenu( PLAYER_ROLE_MENU, "Assign Role", self.roleMenu )
+        self.menu.AppendSeparator()
+        self.menu.Append( PLAYER_SHOW_VERSION, "Version" )
+        # Event processing for our menu
+        self.Bind(wx.EVT_MENU, self.on_menu_item, id=PLAYER_BOOT)
+        self.Bind(wx.EVT_MENU, self.on_menu_item, id=PLAYER_IGNORE)
+        self.Bind(wx.EVT_MENU, self.on_menu_item, id=PLAYER_WHISPER)
+        self.Bind(wx.EVT_MENU, self.on_menu_moderate, id=PLAYER_MODERATE_ROOM_ON)
+        self.Bind(wx.EVT_MENU, self.on_menu_moderate, id=PLAYER_MODERATE_ROOM_OFF)
+        self.Bind(wx.EVT_MENU, self.on_menu_moderate, id=PLAYER_MODERATE_GIVE_VOICE)
+        self.Bind(wx.EVT_MENU, self.on_menu_moderate, id=PLAYER_MODERATE_TAKE_VOICE)
+        self.Bind(wx.EVT_MENU, self.on_menu_role_change, id=PLAYER_ROLE_LURKER)
+        self.Bind(wx.EVT_MENU, self.on_menu_role_change, id=PLAYER_ROLE_PLAYER)
+        self.Bind(wx.EVT_MENU, self.on_menu_role_change, id=PLAYER_ROLE_GM)
+        self.Bind(wx.EVT_MENU, self.on_menu_item, id=PLAYER_SHOW_VERSION)
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+        self.Bind(wx.EVT_MENU, self.on_menu_whispergroup, id=PLAYER_WG_CREATE)
+        self.Bind(wx.EVT_MENU, self.on_menu_whispergroup, id=PLAYER_WG_CLEAR_ALL)
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+#---------------------------------------------------------
+# [START] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+        self.Bind(wx.EVT_MENU, self.on_menu_password, id=PLAYER_COMMAND_PASSWORD_ALTER)
+        self.Bind(wx.EVT_MENU, self.on_menu_room_rename, id=PLAYER_COMMAND_ROOM_RENAME)
+#---------------------------------------------------------
+# [END] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+        self.Bind(wx.EVT_LEFT_DCLICK, self.on_d_lclick)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.on_menu)
+        self.sized = 1
+#---------------------------------------------------------
+# [START] Snowdog Password/Room Name altering code 12/02
+#
+#       Revised 8/03 to add support for password manager
+#---------------------------------------------------------
+    def on_menu_password( self, evt ):
+        id = evt.GetId()
+        self.session = open_rpg.get_component("session")
+        self.password_manager = open_rpg.get_component("password_manager")
+        self.chat = open_rpg.get_component("chat")
+        boot_pwd = self.password_manager.GetPassword("admin",int(self.session.group_id))
+        if boot_pwd != None:
+            alter_pwd_dialog = wx.TextEntryDialog(self,"Enter new room password: (blank for no password)","Alter Room Password")
+            if alter_pwd_dialog.ShowModal() == wx.ID_OK:
+                new_pass = alter_pwd_dialog.GetValue()
+                self.chat.InfoPost( "Requesting password change on server..." )
+                self.session.set_room_pass(new_pass, boot_pwd)
+
+    def on_menu_room_rename( self, evt ):
+        id = evt.GetId()
+        self.session = open_rpg.get_component("session")
+        self.password_manager = open_rpg.get_component("password_manager")
+        self.chat = open_rpg.get_component("chat")
+        boot_pwd = self.password_manager.GetPassword("admin",int(self.session.group_id))
+        if boot_pwd != None:
+            alter_name_dialog = wx.TextEntryDialog(self,"Enter new room name: ","Change Room Name")
+            if alter_name_dialog.ShowModal() == wx.ID_OK:
+                new_name = alter_name_dialog.GetValue()
+                self.chat.InfoPost( "Requesting room name change on server..." )
+                loc = new_name.find("&")
+                oldloc=0
+                while loc > -1:
+                    loc = new_name.find("&",oldloc)
+                    if loc > -1:
+                        b = new_name[:loc]
+                        e = new_name[loc+1:]
+                        new_name = b + "&amp;" + e
+                        oldloc = loc +1
+                loc = new_name.find("'")
+                oldloc=0
+                while loc > -1:
+                    loc = new_name.find("'",oldloc)
+                    if loc > -1:
+                        b = new_name[:loc]
+                        e = new_name[loc+1:]
+                        new_name = b + "&#39;" + e
+                        oldloc = loc +1
+                loc = new_name.find('"')
+                oldloc=0
+                while loc > -1:
+                    loc = new_name.find('"',oldloc)
+                    if loc > -1:
+                        b = new_name[:loc]
+                        e = new_name[loc+1:]
+                        new_name = b + "&quote" + e
+                        oldloc = loc +1
+                self.session.set_room_name(new_name, boot_pwd)
+#---------------------------------------------------------
+# [END] Snowdog Password/Room Name altering code 12/02
+#---------------------------------------------------------
+
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+    def clean_sub_menus(self):
+        for mid in WG_MENU_LIST:
+            try:
+                self.wgMenu.Remove(WG_MENU_LIST[mid]["menuid"])
+                WG_MENU_LIST[mid]["menu"].Destroy()
+            except:
+                self.wgMenu.UpdateUI()
+        if self.wgMenu.GetMenuItemCount() == 2:
+            WG_MENU_LIST.clear()
+        return
+
+    def on_menu_whispergroup( self, evt ):
+        self.session = open_rpg.get_component("session")
+        self.settings = open_rpg.get_component('settings')
+        self.chat = open_rpg.get_component('chat')
+        "Add/Remove players from Whisper Groups"
+        id = evt.GetId()
+        item = self.GetItem( self.selected_item )
+        #See if it is the main menu
+        if id == PLAYER_WG_CREATE:
+            create_new_group_dialog = wx.TextEntryDialog(self,"Enter Group Name","Create New Whisper Group")
+            if create_new_group_dialog.ShowModal() == wx.ID_OK:
+                group_name = create_new_group_dialog.GetValue()
+                WG_LIST[group_name] = {}
+            return
+        elif id == PLAYER_WG_CLEAR_ALL:
+            WG_LIST.clear()
+            return
+        #Check Sub Menus
+        for mid in WG_MENU_LIST:
+            if id == WG_MENU_LIST[mid]["add"]:
+                WG_LIST[mid][int(item.GetText())] = int(item.GetText())
+                return
+            elif id == WG_MENU_LIST[mid]["remove"]:
+                del WG_LIST[mid][int(item.GetText())]
+                return
+            elif id == WG_MENU_LIST[mid]["clear"]:
+                WG_LIST[mid].clear()
+                return
+            elif id == WG_MENU_LIST[mid]["whisper"]:
+                self.chat.set_chat_text("/gw " + mid + "=")
+                return
+        return
+
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+
+    def on_menu_moderate( self, evt ):
+        "Change the moderated status of a room or player."
+        id = evt.GetId()
+        self.chat = open_rpg.get_component( "chat" )
+        self.session = open_rpg.get_component("session")
+        playerID = self.GetItemData( self.selected_item )
+        moderationString = None
+        moderateRoomBase = "/moderate %s"
+        moderatePlayerBase = "/moderate %d=%s"
+        infoRoomBase = "Attempting to %s moderation in the current room..."
+        infoPlayerBase = "Attempting to %s voice to %s (%d)..."
+
+        if id == PLAYER_MODERATE_ROOM_ON:
+            moderationString = (moderateRoomBase % "on")
+            infoString = (infoRoomBase % "ENABLE")
+        if id == PLAYER_MODERATE_ROOM_OFF:
+            moderationString = (moderateRoomBase % "off")
+            infoString = (infoRoomBase % "DISABLE")
+        elif id == PLAYER_MODERATE_GIVE_VOICE:
+            moderationString = (moderatePlayerBase % (playerID, "on"))
+            infoString = (infoPlayerBase % ("GIVE", self.session.get_player_by_player_id( str(playerID) )[0], playerID))
+        elif id == PLAYER_MODERATE_TAKE_VOICE:
+            moderationString = (moderatePlayerBase % (playerID, "off"))
+            infoString = (infoPlayerBase % ("TAKE", self.session.get_player_by_player_id( str(playerID) )[0], playerID))
+
+        # Now, send it to the server
+        if moderationString:
+            self.chat.chat_cmds.docmd( moderationString )
+            # Now, provide local feedback as to what we requested
+            self.chat.InfoPost( infoString )
+
+    def on_menu_role_change( self, evt ):
+        self.session = open_rpg.get_component("session")
+        "Change the role of the selected id."
+        id = evt.GetId()
+        self.chat = open_rpg.get_component( "chat" )
+        playerID = self.GetItemData( self.selected_item )
+        roleString = None
+        roleBase = "/role %d=%s"
+        infoBase = "Attempting to assign the role of %s to (%d) %s..."
+        # Do type specific processing
+	recycle_bin = {PLAYER_ROLE_LURKER: ROLE_LURKER, PLAYER_ROLE_PLAYER: ROLE_PLAYER, PLAYER_ROLE_GM: ROLE_GM}
+	if recycle_bin.has_key(id):
+	    roleName = recycle_bin[id]
+	    roleString = (roleBase % ( playerID, roleName ))
+	    recycle_bin = {}
+
+        # Now, send it to the server
+        if roleString:
+            self.chat.chat_cmds.docmd( roleString )
+            # Now, provide local feedback as to what we requested
+            displayName = self.session.get_player_by_player_id( str(playerID) )[0]
+            infoString = (infoBase % ( roleName, playerID, displayName ))
+            self.chat.InfoPost( infoString )
+
+    def on_d_lclick(self,evt):
+        pos = wx.Point(evt.GetX(),evt.GetY())
+        (item, flag) = self.HitTest(pos)
+        id = self.GetItemText(item)
+        self.chat = open_rpg.get_component("chat")
+        self.chat.set_chat_text("/w " + id + "=")
+
+    def on_menu_item(self,evt):
+        id = evt.GetId()
+        self.session = open_rpg.get_component("session")
+        self.password_manager = open_rpg.get_component("password_manager")
+
+        if id == PLAYER_BOOT:
+            id = str(self.GetItemData(self.selected_item))
+            boot_pwd = self.password_manager.GetPassword("admin",int(self.session.group_id))
+            if boot_pwd != None:
+                self.session.boot_player(id,boot_pwd)
+        elif id == PLAYER_WHISPER:
+            id = self.GetItemText(self.selected_item)
+            self.chat = open_rpg.get_component("chat")
+            self.chat.set_chat_text("/w " + id + "=")
+        elif id == PLAYER_IGNORE:
+            id = str(self.GetItemData(self.selected_item))
+            self.chat = open_rpg.get_component("chat")
+            (result,id,name) = self.session.toggle_ignore(id)
+            if result == 0:
+                self.chat.Post(self.chat.colorize(self.chat.syscolor, "Player " + name + " with ID:" + id +" no longer ignored"))
+            else:
+                self.chat.Post(self.chat.colorize(self.chat.syscolor, "Player " + name + " with ID:" + id +" now being ignored"))
+        elif id == PLAYER_SHOW_VERSION:
+            id = str(self.GetItemData(self.selected_item))
+            version_string = self.session.players[id][6]
+            if version_string:
+                wx.MessageBox("Running client version " + version_string,"Version")
+            else:
+                wx.MessageBox("No client version available for this player","Version")
+
+    def on_menu(self, evt):
+        pos = wx.Point(evt.GetX(),evt.GetY())
+        (item, flag) = self.HitTest(pos)
+        if item > -1:
+            self.selected_item = item
+            #  This if-else block makes the menu item to boot players active or inactive, as appropriate
+	    # This block is enabled for 1.7.8. Ver. 1.7.9 will boast Admin features.
+            #if open_rpg.get_component("session").group_id == "0":
+            #    self.menu.Enable(PLAYER_BOOT,0)
+            #    self.menu.SetLabel(PLAYER_BOOT,"Can't boot from Lobby")
+            #else:
+            self.menu.Enable(PLAYER_BOOT,1)
+            self.menu.SetLabel(PLAYER_BOOT,"Boot")
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+            self.menu.Enable(PLAYER_WG_MENU, True)
+            item = self.GetItem( self.selected_item )
+            if len(WG_MENU_LIST) > len(WG_LIST):
+                self.clean_sub_menus()
+            if len(WG_LIST) == 0:
+                self.wgMenu.Enable(PLAYER_WG_CLEAR_ALL, False)
+            else:
+                self.wgMenu.Enable(PLAYER_WG_CLEAR_ALL, True)
+            for gid in WG_LIST:
+                if not WG_MENU_LIST.has_key(gid):
+                    WG_MENU_LIST[gid] = {}
+                    WG_MENU_LIST[gid]["menuid"] = wx.NewId()
+                    WG_MENU_LIST[gid]["whisper"] = wx.NewId()
+                    WG_MENU_LIST[gid]["add"] = wx.NewId()
+                    WG_MENU_LIST[gid]["remove"] = wx.NewId()
+                    WG_MENU_LIST[gid]["clear"] = wx.NewId()
+                    WG_MENU_LIST[gid]["menu"] = wx.Menu()
+                    WG_MENU_LIST[gid]["menu"].SetTitle(gid)
+                    WG_MENU_LIST[gid]["menu"].Append(WG_MENU_LIST[gid]["whisper"], "Whisper")
+                    WG_MENU_LIST[gid]["menu"].Append(WG_MENU_LIST[gid]["add"], "Add")
+                    WG_MENU_LIST[gid]["menu"].Append(WG_MENU_LIST[gid]["remove"], "Remove")
+                    WG_MENU_LIST[gid]["menu"].Append(WG_MENU_LIST[gid]["clear"], "Clear")
+                    self.wgMenu.PrependMenu(WG_MENU_LIST[gid]["menuid"], gid, WG_MENU_LIST[gid]["menu"])
+                if WG_LIST[gid].has_key(int(item.GetText())):
+                    WG_MENU_LIST[gid]["menu"].Enable(WG_MENU_LIST[gid]["remove"], True)
+                    WG_MENU_LIST[gid]["menu"].Enable(WG_MENU_LIST[gid]["add"], False)
+                else:
+                    WG_MENU_LIST[gid]["menu"].Enable(WG_MENU_LIST[gid]["remove"], False)
+                    WG_MENU_LIST[gid]["menu"].Enable(WG_MENU_LIST[gid]["add"], True)
+                if len(WG_LIST[gid]) == 0:
+                    WG_MENU_LIST[gid]["menu"].Enable(WG_MENU_LIST[gid]["whisper"], False)
+                    WG_MENU_LIST[gid]["menu"].Enable(WG_MENU_LIST[gid]["clear"], False)
+                else:
+                    WG_MENU_LIST[gid]["menu"].Enable(WG_MENU_LIST[gid]["whisper"], True)
+                    WG_MENU_LIST[gid]["menu"].Enable(WG_MENU_LIST[gid]["clear"], True)
+
+                #Event Stuff
+                self.Bind(wx.EVT_MENU, self.on_menu_whispergroup, id=WG_MENU_LIST[gid]["whisper"] )
+                self.Bind(wx.EVT_MENU, self.on_menu_whispergroup, id=WG_MENU_LIST[gid]["add"] )
+                self.Bind(wx.EVT_MENU, self.on_menu_whispergroup, id=WG_MENU_LIST[gid]["remove"] )
+                self.Bind(wx.EVT_MENU, self.on_menu_whispergroup, id=WG_MENU_LIST[gid]["clear"] )
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+            self.PopupMenu(self.menu,pos)
+
+    def add_player(self,player):
+        i = self.InsertImageStringItem(0,player[2],0)
+        self.SetStringItem(i,1,self.strip_html(player))
+        self.SetItemData(i,int(player[2]))
+        self.SetStringItem(i, 2,player[3])
+        self.colorize_player_list()
+        self.Refresh()
+        # play sound
+        setobj = open_rpg.get_component('settings')
+        sound_file = setobj.get_setting("AddSound")
+        if sound_file != '':
+            sound_player = open_rpg.get_component('sound')
+            sound_player.play(sound_file)
+
+    def del_player(self,player):
+        i = self.FindItemData(-1,int(player[2]))
+        self.DeleteItem(i)
+
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+        for gid in WG_LIST:
+            if WG_LIST[gid].has_key(int(player[2])):
+                del WG_LIST[gid][int(player[2])]
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+
+        # play sound
+        setobj = open_rpg.get_component('settings')
+        sound_file = setobj.get_setting("DelSound")
+        if sound_file != '':
+            sound_player = open_rpg.get_component('sound')
+            sound_player.play(sound_file)
+        ic = self.GetItemCount()
+        self.whisperCount = 0
+        index = 0
+        while index < ic:
+            item = self.GetItem( index )
+            if item.GetImage():
+                self.whisperCount += 1
+            index += 1
+        self.colorize_player_list()
+        self.Refresh()
+
+    #  This method updates the player info
+    #
+    #  self:  reference to this PlayerList
+    #  player:  reference to a player structure(list)
+    #
+    #  Returns:  None
+    #
+    def update_player(self,player):
+        i = self.FindItemData(-1,int(player[2]))  #  finds the right list box index
+        self.SetStringItem(i,1,self.strip_html(player))
+        self.SetStringItem(i,2,player[3])
+        item = self.GetItem(i)
+        self.colorize_player_list()
+        self.Refresh()
+
+    def colorize_player_list(self):
+        session = open_rpg.get_component("session")
+        settings = open_rpg.get_component('settings')
+        mode = settings.get_setting("ColorizeRoles")
+        if mode.lower() == "off":
+            return
+        gmColor = settings.get_setting("GMRoleColor")
+        plColor = settings.get_setting("PlayerRoleColor")
+        lkColor = settings.get_setting("LurkerRoleColor")
+        players = session.players
+        for m in players.keys():
+            item_list_location = self.FindItemData(-1,int(m))
+            if item_list_location == -1: continue
+            player_info = session.get_player_by_player_id(m)
+            item = self.GetItem(item_list_location)
+            role = player_info[7].lower()
+            color = wx.GREEN
+            if self.session.group_id != "0":
+		recycle_bin = {'lurker': lkColor, 'player': plColor, 'gm': gmColor }
+		item.SetTextColour(recycle_bin[role])
+		recycle_bin = {}
+            self.SetItem(item)
+
+    def reset(self):
+        self.whisperCount = 0
+#---------------------------------------------------------
+# [START] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+        WG_LIST.clear()
+#---------------------------------------------------------
+# [END] Digitalxero Multi Whisper Group 1/1/05
+#---------------------------------------------------------
+        self.DeleteAllItems()
+
+    def strip_html(self,player):
+        ret_string = ""
+        x = 0
+        in_tag = 0
+        for x in range(len(player[0])) :
+            if player[0][x] == "<" or player[0][x] == ">" or in_tag == 1 :
+                if player[0][x] == "<" :
+                    in_tag = 1
+                elif player[0][x] == ">" :
+                    in_tag = 0
+                else :
+                    pass
+            else :
+                ret_string = ret_string + player[0][x]
+        return ret_string
+
+    def size_cols(self):
+##        # moved skip here to see if it breaks
+##        w,h = self.GetClientSizeTuple()
+##        w /= 8
+##        self.SetColumnWidth( 0, w*2 )
+##        self.SetColumnWidth( 1, w*2 )
+##        self.SetColumnWidth( 2, w*3 )
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/plugindb.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,203 @@
+import xmltramp
+import orpg.dirpath
+import orpg.tools.validate
+from types import *
+
+class PluginDB:
+    def __init__(self, filename="plugindb.xml"):
+        self.filename = orpg.dirpath.dir_struct["user"] + filename
+        orpg.tools.validate.Validate().config_file(filename,"default_plugindb.xml")
+        self.xml_dom = self.LoadDoc()
+
+    def GetString(self, plugname, strname, defaultval, verbose=0):
+        strname = self.safe(strname)
+        for plugin in self.xml_dom:
+            if plugname == plugin._name:
+                for child in plugin._dir:
+                    if child._name == strname:
+                        #str() on this to make sure it's ASCII, not unicode, since orpg can't handle unicode.
+                        if verbose:
+                            print "successfully found the value"
+                        if len(child):
+                            return str( self.normal(child[0]) )
+                        else:
+                            return ""
+        else:
+            if verbose:
+                print "plugindb: no value has been stored for " + strname + " in " + plugname + " so the default has been returned"
+            return defaultval
+
+    def SetString(self, plugname, strname, val):
+        val = self.safe(val)
+        strname = self.safe(strname)
+        for plugin in self.xml_dom:##this isn't absolutely necessary, but it saves the trouble of sending a parsed object instead of a simple string.
+            if plugname == plugin._name:
+                plugin[strname] = val
+                plugin[strname]._attrs["type"] = "string"
+                self.SaveDoc()
+                return "found plugin"
+        else:
+            self.xml_dom[plugname] = xmltramp.parse("<" + strname + " type=\"string\">" + val + "</" + strname + ">")
+            self.SaveDoc()
+            return "added plugin"
+
+    def FetchList(self, parent):
+        retlist = []
+        if not len(parent):
+            return []
+        for litem in parent[0]._dir:
+            if len(litem):
+                if litem._attrs["type"] == "int":
+                    retlist += [int(litem[0])]
+                elif litem._attrs["type"] == "long":
+                    retlist += [long(litem[0])]
+                elif litem._attrs["type"] == "float":
+                    retlist += [float(litem[0])]
+                elif litem._attrs["type"] == "list":
+                    retlist += [self.FetchList(litem)]
+                elif litem._attrs["type"] == "dict":
+                    retlist += [self.FetchDict(litem)]
+                else:
+                    retlist += [str( self.normal(litem[0]) )]
+            else:
+                retlist += [""]
+        return retlist
+
+    def GetList(self, plugname, listname, defaultval, verbose=0):
+        listname = self.safe(listname)
+        for plugin in self.xml_dom:
+            if plugname == plugin._name:
+                for child in plugin._dir:
+                    if child._name == listname and child._attrs["type"] == "list":
+                        retlist = self.FetchList(child)
+                        if verbose:
+                            print "successfully found the value"
+                        return retlist
+        else:
+            if verbose:
+                print "plugindb: no value has been stored for " + listname + " in " + plugname + " so the default has been returned"
+            return defaultval
+
+    def BuildList(self, val):
+        listerine = "<list>"
+        for item in val:
+            if isinstance(item, basestring):#it's a string
+                listerine += "<lobject type=\"str\">" + self.safe(item) + "</lobject>"
+            elif isinstance(item, IntType):#it's an int
+                listerine += "<lobject type=\"int\">" + str(item) + "</lobject>"
+            elif isinstance(item, FloatType):#it's a float
+                listerine += "<lobject type=\"float\">" + str(item) + "</lobject>"
+            elif isinstance(item, LongType):#it's a long
+                listerine += "<lobject type=\"long\">" + str(item) + "</lobject>"
+            elif isinstance(item, ListType):#it's a list
+                listerine += "<lobject type=\"list\">" + self.BuildList(item) + "</lobject>"
+            elif isinstance(item, DictType):#it's a dictionary
+                listerine += "<lobject type=\"dict\">" + self.BuildDict(item) + "</lobject>"
+            else:
+                return "type unknown"
+        listerine += "</list>"
+        return listerine
+
+    def SetList(self, plugname, listname, val):
+        listname = self.safe(listname)
+        list = xmltramp.parse(self.BuildList(val))
+        for plugin in self.xml_dom:
+            if plugname == plugin._name:
+                plugin[listname] = list
+                plugin[listname]._attrs["type"] = "list"
+                self.SaveDoc()
+                return "found plugin"
+        else:
+            self.xml_dom[plugname] = xmltramp.parse("<" + listname + "></" + listname + ">")
+            self.xml_dom[plugname][listname] = list
+            self.xml_dom[plugname][listname]._attrs["type"] = "list"
+            self.SaveDoc()
+            return "added plugin"
+
+    def BuildDict(self, val):
+        dictator = "<dict>"
+        for item in val.keys():
+            if isinstance(val[item], basestring):
+                dictator += "<dobject name=\"" + self.safe(item) + "\" type=\"str\">" + self.safe(val[item]) + "</dobject>"
+            elif isinstance(val[item], IntType):#it's an int
+                dictator += "<dobject name=\"" + self.safe(item) + "\" type=\"int\">" + str(val[item]) + "</dobject>"
+            elif isinstance(val[item], FloatType):#it's a float
+                dictator += "<dobject name=\"" + self.safe(item) + "\" type=\"float\">" + str(val[item]) + "</dobject>"
+            elif isinstance(val[item], LongType):#it's a long
+                dictator += "<dobject name=\"" + self.safe(item) + "\" type=\"long\">" + str(val[item]) + "</dobject>"
+            elif isinstance(val[item], DictType):#it's a dictionary
+                dictator += "<dobject name=\"" + self.safe(item) + "\" type=\"dict\">" + self.BuildDict(val[item]) + "</dobject>"
+            elif isinstance(val[item], ListType):#it's a list
+                dictator += "<dobject name=\"" + self.safe(item) + "\" type=\"list\">" + self.BuildList(val[item]) + "</dobject>"
+            else:
+                return str(val[item]) + ": type unknown"
+        dictator += "</dict>"
+        return dictator
+
+    def SetDict(self, plugname, dictname, val, file="plugindb.xml"):
+        dictname = self.safe(dictname)
+        dict = xmltramp.parse(self.BuildDict(val))
+        for plugin in self.xml_dom:
+            if plugname == plugin._name:
+                plugin[dictname] = dict
+                plugin[dictname]._attrs["type"] = "dict"
+                self.SaveDoc()
+                return "found plugin"
+        else:
+            self.xml_dom[plugname] = xmltramp.parse("<" + dictname + "></" + dictname + ">")
+            self.xml_dom[plugname][dictname] = dict
+            self.xml_dom[plugname][dictname]._attrs["type"] = "dict"
+            self.SaveDoc()
+            return "added plugin"
+
+    def FetchDict(self, parent):
+        retdict = {}
+        if not len(parent):
+            return {}
+        for ditem in parent[0]._dir:
+            if len(ditem):
+                ditem._attrs["name"] = self.normal(ditem._attrs["name"])
+                if ditem._attrs["type"] == "int":
+                    retdict[ditem._attrs["name"]] = int(ditem[0])
+                elif ditem._attrs["type"] == "long":
+                    retdict[ditem._attrs["name"]] = long(ditem[0])
+                elif ditem._attrs["type"] == "float":
+                    retdict[ditem._attrs["name"]] = float(ditem[0])
+                elif ditem._attrs["type"] == "list":
+                    retdict[ditem._attrs["name"]] = self.FetchList(ditem)
+                elif ditem._attrs["type"] == "dict":
+                    retdict[ditem._attrs["name"]] = self.FetchDict(ditem)
+                else:
+                    retdict[ditem._attrs["name"]] = str( self.normal(ditem[0]) )
+            else:
+                retdict[ditem._attrs["name"]] = ""
+        return retdict
+
+    def GetDict(self, plugname, dictname, defaultval, verbose=0):
+        dictname = self.safe(dictname)
+        for plugin in self.xml_dom:
+            if plugname == plugin._name:
+                for child in plugin._dir:
+                    if child._name == dictname and child._attrs["type"] == "dict":
+                        return self.FetchDict(child)
+        else:
+            if verbose:
+                print "plugindb: no value has been stored for " + dictname + " in " + plugname + " so the default has been returned"
+            return defaultval
+
+    def safe(self, string):
+        return string.replace("<", "$$lt$$").replace(">", "$$gt$$").replace("&","$$amp$$").replace('"',"$$quote$$")
+
+    def normal(self, string):
+        return string.replace("$$lt$$", "<").replace("$$gt$$", ">").replace("$$amp$$","&").replace("$$quote$$",'"')
+
+    def SaveDoc(self):
+        f = open(self.filename, "w")
+        f.write(self.xml_dom.__repr__(1, 1))
+        f.close()
+
+    def LoadDoc(self):
+        xml_file = open(self.filename)
+        plugindb = xml_file.read()
+        xml_file.close()
+        return xmltramp.parse(plugindb)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/pluginhandler.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,122 @@
+from orpg.orpg_wx import *
+from orpg.orpgCore import open_rpg
+
+class PluginHandler:
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    def __init__(self, plugindb, parent):
+        self.session = open_rpg.get_component("session")
+        self.chat = open_rpg.get_component("chat")
+        self.settings = open_rpg.get_component("settings")
+        self.gametree = open_rpg.get_component("tree")
+        self.startplugs = open_rpg.get_component("startplugs")
+        self.xml = open_rpg.get_component("xml")
+        self.validate = open_rpg.get_component("validate")
+        self.topframe = open_rpg.get_component("frame")
+        self.plugindb = plugindb
+        self.parent = parent
+        self.shortcmdlist = self.chat.chat_cmds.shortcmdlist
+        self.cmdlist = self.chat.chat_cmds.cmdlist
+
+    def plugin_enabled(self):
+        pass
+
+    def plugin_disabled(self):
+        pass
+
+    def menu_start(self):
+        rootMenu = open_rpg.get_component("pluginmenu")
+        try:
+            self.plugin_menu()
+            rootMenu.AppendMenu(wx.ID_ANY, self.name, self.menu)
+        except:
+            self.menu = wx.Menu()
+            empty = wx.MenuItem(self.menu, wx.ID_ANY, 'Empty')
+            self.menu.AppendItem(empty)
+            rootMenu.AppendMenu(wx.ID_ANY, self.name, self.menu)
+
+    def menu_cleanup(self):
+        self.settings.save()
+        rootMenu = open_rpg.get_component("pluginmenu")
+        menus = rootMenu.MenuItems
+        for mi in menus:
+            if mi.GetText() == self.name:
+                rootMenu.RemoveItem(mi)
+		del self.menu
+
+    def plugin_addcommand(self, cmd, function, helptext, show_msg=True):
+        self.chat.chat_cmds.addcommand(cmd, function, helptext)
+        if(show_msg):
+            msg = '<br /><b>New Command added</b><br />'
+            msg += '<b><font color="#000000">%s</font></b>  %s' % (cmd, helptext)
+            self.chat.InfoPost(msg)
+
+    def plugin_commandalias(self, shortcmd, longcmd, show_msg=True):
+        self.chat.chat_cmds.addshortcmd(shortcmd, longcmd)
+        if(show_msg):
+            msg = '<br /><b>New Command Alias added:</b><br />'
+            msg += '<b><font color="#0000CC">%s</font></b> is short for <font color="#000000">%s</font>' % (shortcmd, longcmd)
+            self.chat.InfoPost(msg)
+
+    def plugin_removecmd(self, cmd):
+        self.chat.chat_cmds.removecmd(cmd)
+        msg = '<br /><b>Command Removed:</b> %s' % (cmd)
+        self.chat.InfoPost(msg)
+
+    def plugin_add_nodehandler(self, nodehandler, nodeclass):
+        self.gametree.add_nodehandler(nodehandler, nodeclass)
+
+    def plugin_remove_nodehandler(self, nodehandler):
+        self.gametree.remove_nodehandler(nodehandler)
+
+    def plugin_add_msg_handler(self, tag, function):
+        self.session.add_msg_handler(tag, function)
+
+    def plugin_delete_msg_handler(self, tag):
+        self.session.remove_msg_handler(tag)
+
+    def plugin_add_setting(self, setting, value, options, help):
+        self.settings.add_tab('Plugins', self.name, 'grid')
+        self.settings.add_setting(self.name, setting, value, options, help)
+
+    def plugin_send_msg(self, to, plugin_msg):
+        xml_dom = self.xml.parseXml(plugin_msg)
+        xml_dom = xml_dom._get_documentElement()
+        xml_dom.setAttribute('from', str(self.session.id))
+        xml_dom.setAttribute('to', str(to))
+        xml_dom.setAttribute('group_id', str(self.session.group_id))
+        tag_name = xml_dom._get_tagName()
+        if not tag_name in self.session.core_msg_handlers:
+            xml_msg = '<plugin to="' + str(to)
+            xml_msg += '" from="' + str(self.session.id)
+            xml_msg += '" group_id="' + str(self.session.group_id)
+            xml_msg += '" />' + xml_dom.toxml()
+            self.session.outbox.put(xml_msg)
+        else:
+            #Spoofing attempt
+            pass
+        xml_dom.unlink()
+
+    def message(self, text):
+        return text
+
+    def pre_parse(self, text):
+        return text
+
+    def send_msg(self, text, send):
+        return text, send
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        return text, type, name
+
+    def post_msg(self, text, myself):
+        return text
+
+    def refresh_counter(self):
+        pass
+
+    def close_module(self):
+        #This is called when OpenRPG shuts down
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/pulldom.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,236 @@
+import minidom
+import xml.sax,xml.sax.handler
+import types
+
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+try:
+    _StringTypes = [types.StringType, types.UnicodeType]
+except AttributeError:
+    _StringTypes = [types.StringType]
+
+START_ELEMENT = "START_ELEMENT"
+END_ELEMENT = "END_ELEMENT"
+COMMENT = "COMMENT"
+START_DOCUMENT = "START_DOCUMENT"
+END_DOCUMENT = "END_DOCUMENT"
+PROCESSING_INSTRUCTION = "PROCESSING_INSTRUCTION"
+IGNORABLE_WHITESPACE = "IGNORABLE_WHITESPACE"
+CHARACTERS = "CHARACTERS"
+
+class PullDOM(xml.sax.ContentHandler):
+    def __init__(self):
+        self.firstEvent = [None, None]
+        self.lastEvent = self.firstEvent
+        self._ns_contexts = [{}] # contains uri -> prefix dicts
+        self._current_context = self._ns_contexts[-1]
+
+    def setDocumentLocator(self, locator): pass
+
+    def startPrefixMapping(self, prefix, uri):
+        self._ns_contexts.append(self._current_context.copy())
+        self._current_context[uri] = prefix
+
+    def endPrefixMapping(self, prefix):
+        del self._ns_contexts[-1]
+
+    def startElementNS(self, name, tagName , attrs):
+        uri,localname = name
+        if uri:
+            # When using namespaces, the reader may or may not
+            # provide us with the original name. If not, create
+            # *a* valid tagName from the current context.
+            if tagName is None:
+                tagName = self._current_context[uri] + ":" + localname
+            node = self.document.createElementNS(uri, tagName)
+        else:
+            # When the tagname is not prefixed, it just appears as
+            # localname
+            node = self.document.createElement(localname)
+        for aname,value in attrs.items():
+            a_uri, a_localname = aname
+            if a_uri:
+                qname = self._current_context[a_uri] + ":" + a_localname
+                attr = self.document.createAttributeNS(a_uri, qname)
+            else:
+                attr = self.document.createAttribute(a_localname)
+            attr.value = value
+            node.setAttributeNode(attr)
+        parent = self.curNode
+        node.parentNode = parent
+        self.curNode = node
+        self.lastEvent[1] = [(START_ELEMENT, node), None]
+        self.lastEvent = self.lastEvent[1]
+        #self.events.append((START_ELEMENT, node))
+
+    def endElementNS(self, name, tagName):
+        node = self.curNode
+        self.lastEvent[1] = [(END_ELEMENT, node), None]
+        self.lastEvent = self.lastEvent[1]
+        #self.events.append((END_ELEMENT, node))
+        self.curNode = node.parentNode
+
+    def startElement(self, name, attrs):
+        node = self.document.createElement(name)
+        for aname,value in attrs.items():
+            attr = self.document.createAttribute(aname)
+            attr.value = value
+            node.setAttributeNode(attr)
+        parent = self.curNode
+        node.parentNode = parent
+        self.curNode = node
+        self.lastEvent[1] = [(START_ELEMENT, node), None]
+        self.lastEvent = self.lastEvent[1]
+        #self.events.append((START_ELEMENT, node))
+
+    def endElement(self, name):
+        node = self.curNode
+        self.lastEvent[1] = [(END_ELEMENT, node), None]
+        self.lastEvent = self.lastEvent[1]
+        #self.events.append((END_ELEMENT, node))
+        self.curNode = node.parentNode
+
+    def comment(self, s):
+        node = self.document.createComment(s)
+        parent = self.curNode
+        node.parentNode = parent
+        self.lastEvent[1] = [(COMMENT, node), None]
+        self.lastEvent = self.lastEvent[1]
+        #self.events.append((COMMENT, node))
+
+    def processingInstruction(self, target, data):
+        node = self.document.createProcessingInstruction(target, data)
+        parent = self.curNode
+        node.parentNode = parent
+        self.lastEvent[1] = [(PROCESSING_INSTRUCTION, node), None]
+        self.lastEvent = self.lastEvent[1]
+        #self.events.append((PROCESSING_INSTRUCTION, node))
+
+    def ignorableWhitespace(self, chars):
+        node = self.document.createTextNode(chars[start:start + length])
+        parent = self.curNode
+        node.parentNode = parent
+        self.lastEvent[1] = [(IGNORABLE_WHITESPACE, node), None]
+        self.lastEvent = self.lastEvent[1]
+        #self.events.append((IGNORABLE_WHITESPACE, node))
+
+    def characters(self, chars):
+        node = self.document.createTextNode(chars)
+        parent = self.curNode
+        node.parentNode = parent
+        self.lastEvent[1] = [(CHARACTERS, node), None]
+        self.lastEvent = self.lastEvent[1]
+
+    def startDocument(self):
+        node = self.curNode = self.document = minidom.Document()
+        node.parentNode = None
+        self.lastEvent[1] = [(START_DOCUMENT, node), None]
+        self.lastEvent = self.lastEvent[1]
+        #self.events.append((START_DOCUMENT, node))
+
+    def endDocument(self):
+        assert not self.curNode.parentNode
+        for node in self.curNode.childNodes:
+            if node.nodeType == node.ELEMENT_NODE:
+                self.document.documentElement = node
+        #if not self.document.documentElement:
+        #    raise Error, "No document element"
+        self.lastEvent[1] = [(END_DOCUMENT, node), None]
+        #self.events.append((END_DOCUMENT, self.curNode))
+
+class ErrorHandler:
+    def warning(self, exception):
+        print exception
+    def error(self, exception):
+        raise exception
+    def fatalError(self, exception):
+        raise exception
+
+class DOMEventStream:
+    def __init__(self, stream, parser, bufsize):
+        self.stream = stream
+        self.parser = parser
+        self.bufsize = bufsize
+        self.reset()
+
+    def reset(self):
+        self.pulldom = PullDOM()
+        # This content handler relies on namespace support
+        self.parser.setFeature(xml.sax.handler.feature_namespaces,1)
+        self.parser.setContentHandler(self.pulldom)
+
+    def __getitem__(self, pos):
+        rc = self.getEvent()
+        if rc:
+            return rc
+        raise IndexError
+
+    def expandNode(self, node):
+        event = self.getEvent()
+        while event:
+            token, cur_node = event
+            if cur_node is node:
+                return
+            if token != END_ELEMENT:
+                cur_node.parentNode.appendChild(cur_node)
+            event = self.getEvent()
+
+    def getEvent(self):
+        if not self.pulldom.firstEvent[1]:
+            self.pulldom.lastEvent = self.pulldom.firstEvent
+        while not self.pulldom.firstEvent[1]:
+            buf=self.stream.read(self.bufsize)
+            if not buf:
+                #FIXME: why doesn't Expat close work?
+                #self.parser.close()
+                return None
+            self.parser.feed(buf)
+        rc = self.pulldom.firstEvent[1][0]
+        self.pulldom.firstEvent[1] = self.pulldom.firstEvent[1][1]
+        return rc
+
+class SAX2DOM(PullDOM):
+
+    def startElementNS(self, name, tagName , attrs):
+        PullDOM.startElementNS(self, name, tagName, attrs)
+        self.curNode.parentNode.appendChild(self.curNode)
+
+    def startElement(self, name, attrs):
+        PullDOM.startElement(self, name, attrs)
+        self.curNode.parentNode.appendChild(self.curNode)
+
+    def processingInstruction(self, target, data):
+        PullDOM.processingInstruction(self, target, data)
+        node = self.lastEvent[0][1]
+        node.parentNode.appendChild(node)
+
+    def ignorableWhitespace(self, chars):
+        PullDOM.ignorableWhitespace(self, chars)
+        node = self.lastEvent[0][1]
+        node.parentNode.appendChild(node)
+
+    def characters(self, chars):
+        PullDOM.characters(self, chars)
+        node = self.lastEvent[0][1]
+        node.parentNode.appendChild(node)
+
+default_bufsize = (2 ** 14) - 20
+
+def parse(stream_or_string, parser=None, bufsize=default_bufsize):
+    if type(stream_or_string) in _StringTypes:
+        stream = open(stream_or_string)
+    else:
+        stream = stream_or_string
+    if not parser:
+        parser = xml.sax.make_parser()
+    return DOMEventStream(stream, parser, bufsize)
+
+def parseString(string, parser=None):
+    bufsize = len(string)
+    buf = StringIO(string)
+    if not parser:
+        parser = xml.sax.make_parser()
+    return DOMEventStream(buf, parser, bufsize)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/systempath.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+import sys
+if sys.platform != 'win32':
+    sys.path.append( "/usr/local/lib/python2.2/site-packages/" )
+    sys.path.append( "/usr/lib/python2.2/site-packages/" )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/about.html	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+  <body bgcolor="#FFFFFF">
+    <table cellspacing=3 cellpadding=4 width="100%">
+      <tr>
+	<td bgcolor="#101010" align="bottom">
+	  <center><a href="http://www.openrpg.com"><img src="images/splash.gif"></a></center>
+	</td>
+      </tr>
+      <tr>
+	<td bgcolor="#101010" align="center">
+	  <font size="+4" color="#FFFFFF"><b><br>Version VeRsIoNrEpLaCeMeNtStRiNg<br></b></font>
+	</td>
+      </tr>
+      <tr>
+	<td bgcolor="#73A183" align="center">
+	  <b><font size="+1">Special thanks to Chris Davis for this endeavor</a></font></b>
+	  <p>
+	  <table cellpadding="0" cellspacing="0" width="100%">
+	    <tr>
+	      <td align="center" width="100%">
+		Of course, many thanks goes to all of those who contributed.
+		<BR>
+		The developers in alphabetical order are:
+		<BR>
+		Thomas Baleno, Andrew Bennett, Lex Berezhny, Ted Berg,
+		Bernhard Bergbauer, Chris Blocher, David Byron, Ben Collins-Sussman, Robin Cook, Greg Copeland,
+		Chris Davis, Michael Edwards, Andrew Ettinger, Todd Faris, Dj Gilcrease,
+        Christopher Hickman, Paul Hosking, Brian Manning, Scott Mackay, Jesse McConnell, 
+		Brian Osman, Rome Reginelli, Christopher Rouse, Dave Sanders, Tyler Starke, and Mark Tarrabain.
+	      </td>
+	    </tr>
+	    <tr>
+	      <td align="center" width="100%">
+		<a href="http://www.python.org"><img src="images/python55.gif"></a>
+		<a href="http://www.wxwindows.org"><img src="images/wxWinButton.png"></a>
+		<a href="http://www.wxpython.org"><img src="images/wxPyButton.png"></a>
+		<a href="http://www.sourceforge.net"><img src="images/sflogo.png"></a>
+	      </td>
+	    </tr>
+	    <tr>
+	      <td align="center" width="100%">
+		This product is licensed under the <a href="http://www.gnu.org">GNU</a> <a href="http://www.gnu.org/philosophy/license-list.html">GPL License.</a>
+	      </td>
+	    </tr>
+	  </table>
+	</td>
+      </tr>
+    </table>
+
+<!-- Created: Fri Jun  1 17:32:22 CDT 2001 -->
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_LobbyMessage.html	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+  <body>
+    <table cellspacing=3 cellpadding=4 width="100%">
+      <tr>
+       <td bgcolor="#101010" align="bottom">
+         <center><a href="http://www.openrpg.com"><img src="images/splash.gif" border="0"></a></center>
+       </td>
+      </tr>
+      <tr>
+        <td bgcolor="#73A183" align="center">
+         <table cellpadding="0" cellspacing="0" width="100%">
+           <tr>
+            <td align="center" width="100%">
+             Many thanks goes to all of those who contributed!
+             <BR>
+              The developers in alphabetical order are:
+             <BR>
+            Thomas Baleno, Andrew Bennett, Lex Berezhny, Ted Berg,
+            Bernhard Bergbauer, Chris Blocher ,Ben Collins-Sussman, Robin Cook, Greg Copeland,
+            Chris Davis, Michael Edwards, Andrew Ettinger, Dj Gilcrease, Todd Faris,
+            Christopher Hickman, Paul Hosking, Scott Mackay, Brian Manning,
+            Jesse McConnell, Brian Osman, Rome Reginelli, Christopher Rouse, Dave Sanders and Mark Tarrabain.
+            </td>
+              </tr>
+                <tr>
+                <td align="center" width="100%">
+                This product is licensed under the <a href="http://www.gnu.org">GNU</a> <a href="http://www.gnu.org/philosophy/license-list.html">GPL License.</a>
+             </td>
+             </tr>
+            </table>
+           </td>
+      </tr>
+    </table>
+
+<!-- Created: Thursday November 9 23:55:12 PDT 2003 -->
+  </body>
+</html>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_Lobby_map.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,7 @@
+<nodehandler class="min_map" icon="compass" module="core" name="miniature Map">
+<map version='1.0' sizex='300' sizey='300' action='new'>
+<grid size='50'  mode='0' line='0' snap='1' color='#000000'/>
+<bg path='http://www.openrpg.com/images/maps/Lobby_image.png' type='2'/>
+<miniatures serial='2'/>
+</map>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_alias.alias	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,33 @@
+<aliaslib>
+    <alias name="Test" color="Default" />
+    <alias name="Test1" color="#FF0000" />
+    <filter name="Rogue or Pirate">
+        <rule match="ia" sub="'a" />
+        <rule match="(\W+)(?i)it is(\W+)" sub="\1t'is\2" />
+        <rule match="(\W+)(?i)h" sub="\1'" />
+        <rule match="(?i)his" sub="'is" />
+        <rule match="n[tkg](\W+)" sub="n'\1" />
+        <rule match="(\W+)(?i)to(\W+)" sub="\1t'\2" />
+        <rule match="(\w+)th(\W+)" sub="\1t'\2" />
+        <rule match="(\W+)([Yy])ou(\W+)" sub="\1\2ea\3" />
+        <rule match="(\W+)([Yy])our(\W+)" sub="\1\2er\3" />
+        <rule match="'+" sub="'" />
+        <rule match="th" sub="d'" />
+        <rule match="ass" sub="arse" />
+    </filter>
+    <filter name="Static Communicator">
+        <rule match="ce" sub="ssss(**shhhshh**)" />
+        <rule match="[IiOo]" sub="(**sss**)" />
+        <rule match="wi" sub="(*squack*) " />
+        <rule match="run" sub="@#$%DDD " />
+        <rule match="[Ss][Tt]" sub=";lkj%$# " />
+        <rule match="opy" sub="op.....he" />
+        <rule match="[Bb]" />
+        <rule match="the" sub="te" />
+        <rule match="ow" sub="ashh" />
+        <rule match="[Aa][Bb]" sub="bbb" />
+        <rule match="ll" sub="ya" />
+        <rule match="support" sub="(*rssshhhh*)'pt" />
+        <rule match="ey" sub="eeee" />
+    </filter>
+</aliaslib>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_ban_list.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+<server></server>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_gui.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,92 @@
+<!--
+    Window Container Options
+    <tab>
+    <splitter  pos="?" type="v or h" >
+    <dialog width="?" height="?" posx="?" posy="?"  stayontop="1 or 0" >
+
+    OpenRPG Window Tags
+    <map enable="1" />
+    <chat enable="1" />
+    <tree enable="1" />
+    <player enable="1" />
+-->
+
+<!--
+
+STANDARD LAYOUT
+-->
+<orpg_gui width="1" height="1" posx="10" posy="10" >
+    <splitter  pos="300"  type="v" >
+        <splitter  pos="400" type="h" >
+            <tree enable="1" name="Tree" />
+            <player enable="1" name="Player"/>
+        </splitter>
+        <splitter  pos="300"  type="h" >
+            <map enable="1" name="Map" />
+            <chat enable="1" name="Chat" />
+        </splitter>
+    </splitter>
+</orpg_gui>
+
+
+
+<!--
+
+ Tree and Player list in Tab.  Chat and Map in a splitter
+
+
+
+<orpg_gui width="1" height="1" posx="1" posy="1" >
+    <splitter  pos="300"  type="v" >
+        <tab  pos="400" type="h" >
+            <tree enable="1" name="Tree" />
+            <player enable="1" name="Player"/>
+        </tab>
+        <splitter  pos="300"  type="h" >
+            <map enable="1" name="Map" />
+            <chat enable="1" name="Chat" />
+        </splitter>
+    </splitter>
+</orpg_gui>
+
+-->
+
+
+<!--
+
+ Tree Map, Chat in splitters. Player list in dialog.
+
+<orpg_gui width="1" height="1" posx="1" posy="1" >
+    <splitter  pos="300"  type="v" >
+        <tree enable="1" name="Tree" />
+        <splitter  pos="300"  type="h" >
+            <map enable="1" name="Map" />
+            <chat enable="1" name="Chat" />
+        </splitter>
+    </splitter>
+    <dialog width="200" height="200" posx="10" posy="10"  stayontop="1" >
+        <player enable="1" name="Player"/>
+    </dialog>
+</orpg_gui>
+
+-->
+
+<!--
+
+Map main window. Tree, players, and chat in dialogs.
+
+<orpg_gui width="1" height="1" posx="1" posy="1" >
+    <map enable="1" name="Map" />
+    <dialog width="250" height="400" posx="10" posy="10"  stayontop="1" >
+        <tree enable="1" name="Tree" />
+    </dialog>
+    <dialog width="250" height="200" posx="10" posy="350"  stayontop="1" >
+        <player enable="1" name="Player"/>
+    </dialog>
+    <dialog width="600" height="250" posx="200" posy="10"  stayontop="1" >
+        <chat enable="1" name="Chat" />
+    </dialog>
+</orpg_gui>
+
+-->
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_ini.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,73 @@
+<openrpg>
+  <autoupdate_config catagory="general" options= "0=off, 1=on" help="Turns on/off auto-configfile updating of the ini.xml file" value="1"/>
+  <player catagory="chat" help="This is your name as it appears in chat." options="Any text" value="No Name"/>
+  <gametree catagory="gametree" help="This is the path on your computer pointing to the xml file\n for your tree (normaly tree.xml in the myfiles directory)." options="URL" value="myfiles/tree.xml"/>
+  <SaveGameTreeOnExit catagory="gametree" help="Set this to 1 if you want your game tree to automaticaly be saved when you log out." options="0=no; 1=yes" value="1"/>
+  <bgcolor catagory="colors" help="This is the background color of the chat window." options="color in hex RRGGBB" value="#ffffff"/>
+  <textcolor catagory="colors" help="This is the default color used when text is printed into the chat window." options="color in hex RRGGBB" value="#000000"/>
+  <mytextcolor catagory="colors" help="This is the color of your text in the chat window." options="color in hex RRGGBB" value="#000080"/>
+  <syscolor catagory="colors" help="This is the color of system messages printed in the chat window." options="color in hex RRGGBB" value="#ff0000"/>
+  <infocolor catagory="colors" help="This is the color of informational messages printed in the chat window." options="color in hex RRGGBB" value="#ff8000"/>
+  <emotecolor catagory="colors" help="This is the color of your emotes in the chat window." options="color in hex RRGGBB" value="#008000"/>
+  <whispercolor catagory="colors" help="This is the color of whisper messages in the chat window." options="color in hex RRGGBB" value="#ff8000"/>
+  <striphtml catagory="chat" help="Set this to 1 to have HTML tags stripped from the chat window." options="0=no; 1=yes" value="0"/>
+  <tabbedwhispers catagory="tabs" help="Set this to 1 to receive whispered messages in separate chat tabs ." options="0=no; 1=yes" value="0"/>
+  <GMWhisperTab catagory="tabs" help="Creates a tab for all GM whispers, tabbedwhispers being on is required for this too work" options="0=no; 1=yes" value="1"/>
+  <GroupWhisperTab catagory="tabs" help="Creates a tab for all Group whispers, tabbedwhispers being on is required for this too work" options="0=no; 1=yes" value="1"/>
+  <defaultfont catagory="chat" help="Set this to a preferred font to use at startup." options="a font name" value="Arial"/>
+  <defaultfontsize catagory="chat" help="Set this to a preferred fontsize to use at startup." options="a font size" value="10"/>
+  <buffersize catagory="chat" help="This is the amount of backscroll allowed.  If this number is too large your computer will lag after a while.\nIt is not the same as the history buffer which is infinite unless you set the purge options." options="Any number" value="100"/>
+  <MultipleWindows help="Setting this to 1 will break apart the Map, Chat,\nGametree, and player list into their own windows." options="0=off; 1=on" value="0"/>
+  <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=""/>
+  <MetaServerBaseURL help="This is the URL that contains the server list." options="URL" value="http://www.openrpg.com/openrpg_servers.php"/>
+  <GameLogPrefix help="This text is the files name minus the extention of your log file.  You can use\n%d, %m, %y for the log to insert the day, month or year respectively." options="Any text" value=""/>
+  <TimeStampGameLog options="0=no; 1=yes" value="1" help="Set this to 1 to have time stamps added to the log." />
+  <ShowIDInChat catagory="chat" options="0=no; 1=yes" value="1" help="Set this to 1 to have the Player Id show up next to the player name in chat." />
+  <AlwaysShowMapScale catagory="map" options="0=no; 1=yes" value="0" help="Setting this to 1 will keep the map scale displayed in the upper left corner of the map." />
+  <SuppressChatAutoComplete catagory="chat" options="0=no; 1=yes" value="0" help="Setting this to 1 will turn off auto complete in chat." />
+  <TypingStatusAlias catagory="chat" options="Any text" value="Typing" help="This is the text displayed in the Player list under status while you are typing." />
+  <IdleStatusAlias catagory="chat" options="Any text" value="Idle" help="This is the text displayed in the Player list under status while you are not typing." />
+  <treedclick catagory="gametree" 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." />
+  <dieroller catagory="chat" options="std, wod, d20, hero" value="std" help="This sets the dieroller to use." />
+  <NameSameEmoteColor catagory="chat" options="yes, no" value="No" help="Setting this will display your name in the same color as your emote." />
+  <ImageCacheSize catagory="map" options="Any number" value="32" help="This sets the number of images to cache.  A higher number will load a map\nfaster if the map contains images pointing to the same URL.  It will also take\nup more memory." />
+  <F1 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F1 macro key"/>
+  <F2 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F2 macro key"/>
+  <F3 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F3 macro key"/>
+  <F4 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F4 macro key"/>
+  <F5 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F5 macro key"/>
+  <F6 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F6 macro key"/>
+  <F7 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F7 macro key"/>
+  <F8 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F8 macro key"/>
+  <F9 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F9 macro key"/>
+  <F10 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F10 macro key"/>
+  <F11 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F11 macro key"/>
+  <F12 catagory="macros" help="What you enter here will be sent to chat when this function key is pressed." options="Any text" value="/me found the F12 macro key"/>
+  <EnableSplittersAutoExpand options="0=no; 1=yes" value="0" help="Setting this will invert the size of the splitter with its neighbors." />
+  <PackagesBaseURL options="URL" value="http://openrpg.sourceforge.net/orpg_packages.xml" help="This is a URL pointing where to get updates from." />
+  <Disableupdate options="0=no; 1=yes" value="0" help="Setting this to 1 will render the /update command unavailable and prevent automatic update checking on startup." />
+  <LoadGameTreeFeatures catagory="gametree" options="0=no; 1=yes" value="1" help="Setting this to 1 will load the gametree features next time you run OpenRPG." />
+  <Heartbeat options="0=off, 1=on" value="0" help="This sends a message to the server to keep alive your connection when idle.\nThis is usefull if your ISP automaticaly disconnects you when you are idle or if\nan OpenRPG server's ISP drops the connection when you are idle." />
+  <ColorizeRoles catagory="chat" options="on,off" value="on" help="Setting this to 1 colorizes roles in the player list.  Setting this to 0 disables the colors in the player list." />
+  <AutoPurgeAfterSave catagory="chat" help="When saving your log, this option will either automaticaly purge the buffer\n(see PurgAtBuffersizeTimesLines), ask you if you want to purge the buffer,\nor not purge at all." options="ask,yes,no" value="ask"/>
+  <PurgeAtBuffersizeTimesLines catagory="chat" 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="Any number" value="2"/>
+  <dcmsg catagory="chat" help="This is the message that gets sent when you disconnect from a server. It can be regular text or an emote action (started with /me)." options="max 80 chars of text" value="Disconnecting from server..."/>
+  <RoomColor_Lobby  catagory="roomlist" help="Sets the color used to display the 'Lobby' in the Room List in the Gameserver window" options="color in hex RRGGBB" value="#000080"/>
+  <RoomColor_Empty  catagory="roomlist" help="Sets the color used to display empty rooms (persistant only) in the Room List in the Gameserver window" options="color in hex RRGGBB" value="#bebebe"/>
+  <RoomColor_Locked catagory="roomlist" help="Sets the color used to display password protected rooms in the Room List in the Gameserver window" options="color in hex RRGGBB" value="#b70000"/>
+  <RoomColor_Active catagory="roomlist" help="Sets the color used to display non-passworded non-empty rooms in the Room List in the Gameserver window" options="color in hex RRGGBB" value="#000000"/>
+  <Toolbar_On catagory="toolbar" help="Turns the toolbar on or off" options="1=yes, 0=no" value="1"/>
+  <AliasTool_On catagory="toolbar" help="Show the Alias Tool in the toolbar?" options="1=yes, 0=no" value="1"/>
+  <FormattingButtons_On catagory="toolbar" help="Show the Formatting Buttons (Bold, italic, underline, color) in the toolbar?" options="1=yes, 0=no" value="1"/>
+  <DiceButtons_On catagory="toolbar" help="Show the dice buttons in the toolbar?" options="1=yes, 0=no" value="1"/>
+  <ToGMsButton_On catagory="toolbar" help="Show the 'To GM(s)' button in the toolbar?" options="1=yes, 0=no" value="0"/>
+  <dievars catagory="general" help="Replace the ? in die rolls with the value when it is sent to chat" options="1=yes, 0=no" value="1"/>
+  <gwtext catagory="chat" help="This is attached prior to your group whispers(ie /gw Hello group would send '(GW): Hello group')" options="Any Text" value="(GW): "/>
+  <Chat_Time_Indexing catagory="chat" 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="0=none, 1=short, 2=long, [timestring]" value="0"/>
+  <Show_Images_In_Chat catagory="chat" help="Allows Images to be displaied in the chat window. Default is off because large images can crash you client" options="0=Off(Do not display img), 1=On(Display img)" value="0"/>
+</openrpg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_layout.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,6 @@
+<orpg_gui height="768" maximized="0" posx="20" posy="20" width="1024">
+    <chat    dockable="1"    height="356"    width="824"    caption="Chat Window"   direction="Bottom"   layer="1"    pos="1" />
+    <tree    dockable="1"    height="357"    width="200"    caption="Gametree"      direction="Left"     layer="3"    pos="0" />
+    <player  dockable="1"    height="356"    width="200"    caption="Player List"   direction="Left"     layer="3"    pos="1" />
+    <map     dockable="1"    height="357"    width="824"    caption="Map Window"    direction="Center"   layer="2"    pos="0" />
+</orpg_gui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_map.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,8 @@
+<nodehandler class="min_map" icon="compass" module="core" name="miniature Map">
+<map version='1.0' sizex='1000' sizey='1000' action='new'>
+<miniatures serial='0'/>
+<bg color='#008040' type='3'/>
+<grid snap='1' color='#000000' line='2'  mode='0' size='60'/>
+<whiteboard serial='0'/>
+</map>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_plugindb.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+<plugindb></plugindb>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_server_ini.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,22 @@
+<server lobbyname="Lobby" boot="" register="" name="">
+  <service port="6774" address="hostname/address" />
+  <map file="Lobby_map.xml" />
+  <message file="LobbyMessage.html" />
+  <validate_protocol value="true" />
+  <autokick silent="no" delay="480" />
+  <version min="1.6.3" />
+  <cheat text="**Fudged Roll**" help="The text will be included in any faked roll by anyone but a GM" />
+  <room_defaults>
+    <passwords allow="yes"/>
+    <map file=""/>
+    <message file="LobbyMessage.html"/>
+  </room_defaults>
+  <room name="Example Persistant #1" password="password" boot="password">
+    <map file="Lobby_map.xml" />
+    <message file="LobbyMessage.html" />
+  </room>
+  <room name="Example Persistant #2" password="" boot="password">
+    <map file="Lobby_map.xml" />
+    <message file="LobbyMessage.html" />
+  </room>
+</server>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_settings.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,121 @@
+<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"/>
+            <LocalImageBaseURL help="This is the URL that contains the server list." options="URL" value="http://127.0.0.1:6774/webfiles/"/>
+            <LocalorRemote help="Decide to load files locally or remotely. CherryPy must be running for local files." options="Local | Remote" value="Local"/>
+        </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">
+            <PackagesURL help="The URL to the package_list.xml" options="URL" value="http://openrpg.digitalxero.net/updates/package_list.xml"/>
+            <PackagesType help="The type of package you want to DL, this is usualy set progmaticly" options="text" value="developer"/>
+            <PackagesName help="The name of the package you want to DL, this is usualy set progmaticly" options="text" value="OpenRPG+ 1.7.x"/>
+            <AutoUpdate help="Wether or not the updater automaticly downloads the files" options="On|Off" value="Off"/>
+            <FastStart help="Weather the Patcher will automaticly exit after it updates or if there are no updates to get" options="On|Off" value="On"/>
+        </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 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>
+    <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"/>
+    </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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/default_tree.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,102 @@
+
+<gametree version="1.0">
+  <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Behir (Example Sheet)" version="1.0">
+    <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+      <form height="500" width="400"/>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+        <text multiline="0" send_button="0">Behir</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="HD" version="1.0">
+        <text multiline="0" send_button="0">9d10+45 (94 hp)</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+        <text multiline="0" send_button="0">40 ft., climb 15 ft</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC" version="1.0">
+        <text multiline="0" send_button="0">16 (-2 size, +1 Dex, +7 natural)</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face Reach" version="1.0">
+        <text multiline="0" send_button="0">10 ft. by 30 ft./10 ft.</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+        <text multiline="0" send_button="0">Cleave, Power Attack</text>
+      </nodehandler>
+      <nodehandler class="listbox_handler" icon="gear" module="forms" name="Skills" version="1.0">
+        <list send_button="1" type="1">
+          <option selected="0" value="0">Climb [1d20+18]</option>
+          <option selected="0" value="0">Hide [1d20+5]</option>
+          <option selected="1" value="0">Spot [1d20+7]</option>
+        </list>
+      </nodehandler>
+      <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+        <grid autosize="1" border="1">
+          <row version="1.0">
+            <cell>Str</cell>
+            <cell>28</cell>
+          </row>
+          <row version="1.0">
+            <cell>Dex</cell>
+            <cell>13</cell>
+          </row>
+          <row version="1.0">
+            <cell>Con</cell>
+            <cell>21</cell>
+          </row>
+          <row version="1.0">
+            <cell>Int</cell>
+            <cell>7</cell>
+          </row>
+          <row version="1.0">
+            <cell>Wis</cell>
+            <cell>14</cell>
+          </row>
+          <row version="1.0">
+            <cell>Cha</cell>
+            <cell>12</cell>
+          </row>
+        </grid>
+        <macros>
+          <macro name=""/>
+        </macros>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="form_handler" icon="form" module="forms" name="Combat Rolls" version="1.0">
+      <form height="300" width="400"/>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP" version="1.0">
+        <text multiline="0" send_button="0">text</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+        <text multiline="0" send_button="1">[1d20+1]</text>
+      </nodehandler>
+      <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+        <list send_button="1" type="3">
+          <option selected="0" value="0">Bite [1d20+15], Damage [2d4+8]</option>
+          <option selected="0" value="0">Claw [1d20+10], Damage [1d4+4]</option>
+          <option selected="0" value="0">Claw [1d20+10], Damage [1d4+4]</option>
+        </list>
+      </nodehandler>
+      <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+        <list send_button="1" type="1">
+          <option selected="1" value="0">Will Power [1d20+5]</option>
+          <option selected="0" value="0">Relex [1d20+7]</option>
+          <option selected="0" value="0">Fortitude [1d20+11]</option>
+        </list>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Combat Info" version="1.0">
+      <text multiline="1" send_button="1">A behir usually bites and grabs its prey first, then either swallows or constricts the opponent. If beset by a large number of foes, it uses its breath weapon.
+
+Breath Weapon (Su): Line of lightning 5 feet wide, 5 feet high, and 20 feet long, once a minute; damage 7d6, Reflex half DC 19.
+
+Improved Grab (Ex): To use this ability, the behir must hit with its bite attack. If it gets a hold, it can attempt to swallow or constrict the opponent.
+
+Swallow Whole (Ex): A behir can try to swallow a grabbed Medium-size or smaller opponent by making a successful grapple check. A behir that swallows an opponent can use its Cleave feat to bite and grab another opponent. The swallowed creature takes 2d8+8 points of crushing damage and 8 points of acid damage per round from the behirs gizzard. A swallowed creature can also cut its way out by using claws or a Small or Tiny slashing weapon to deal 25 points of damage to the gizzard (AC 20). Once the creature exits, muscular action closes the hole; another swallowed opponent must again cut its own way out.
+
+The behirs gizzard can hold two Medium-size, four Small, eight Tiny, sixteen Diminutive, or thirty-two Fine or smaller opponents.
+
+Constrict (Ex): A behir deals 2d8+8 damage with a successful grapple check against Gargantuan or smaller creatures. It can use its claws against the grappled foe as well.
+
+</text>
+    </nodehandler>
+  </nodehandler>
+</gametree>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/feature.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,108 @@
+
+<nodehandler class="tabber_handler" icon="help" module="containers" name="OpenRPG+ 1.7.1" version="1.0">
+  <nodehandler class="link_handler" icon="html" module="forms" name="Release Notes" version="1.0">
+    <link href="http://openrpg.digitalxero.net/wiki/index.php?page=Release+Notes"/>
+  </nodehandler>
+  <nodehandler class="link_handler" icon="html" module="forms" name="OpenRPG User Guide" version="1.0">
+    <link href="http://openrpg.digitalxero.net"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="help" module="core" name="Load Die Roller Notes" version="1.0">
+    <file name="die_roller_notes.xml"/>
+  </nodehandler>
+  <nodehandler class="group_handler" icon="gear" module="containers" name="Templates" status="useful" version="1.0">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="group_handler" icon="flask" module="containers" name="Nodes" status="useful" version="1.0">
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="file_loader" icon="note" module="core" name="Create New Text Box" version="1.0">
+        <file name="textctrl.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="gear" module="core" name="Create New List Box" version="1.0">
+        <file name="listbox.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="grid" module="core" name="Create New Grid" version="1.0">
+        <file name="grid.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="html" module="core" name="Create New Web Link" version="1.0">
+        <file name="link.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="image" module="core" name="Create New Web Image" version="1.0">
+        <file name="image.xml"/>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="group_handler" module="containers" name="Containers" status="useful" version="1.0">
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="file_loader" module="core" name="Create New Folder" version="1.0">
+        <file name="group.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="tabber" module="core" name="Create New Tabber" version="1.0">
+        <file name="tabber.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="divider" module="core" name="Create New Splitter" version="1.0">
+        <file name="split.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="form" module="core" name="Create New Form" version="1.0">
+        <file name="form.xml"/>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="group_handler" icon="gear" module="containers" name="Tools" status="useful" version="1.0">
+      <group_atts border="1" cols="1"/>
+      <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 Miniature Library Tool" version="1.0">
+        <file name="minlib.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="gear" module="core" name="Create remote node loader" version="1.0">
+        <file name="urloader.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="d20" module="core" name="Create New d20 Character Tool" version="1.0">
+        <file name="d20character.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="d20" module="core" name="Create New St*r W*rs Character Tool" version="1.0">
+        <file name="StarWars_d20character.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="d20" module="core" name="3rd Edition Character Tool" version="1.0">
+        <file name="dnd3e.xml"/>
+      </nodehandler>
+      <nodehandler class="file_loader" icon="d20" module="core" name="3.5 Tool" version="1.0">
+        <file name="dnd3.5.xml"/>
+      </nodehandler>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="group_handler" icon="browser" module="containers" name="OpenRPG+ Resources" version="1.0">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="link_handler" icon="html" module="forms" name="OpenRPG+ Home Page" version="1.0">
+      <link href="http://openrpg.digitalxero.net"/>
+    </nodehandler>
+    <nodehandler class="link_handler" icon="html" module="forms" name="OpenRPG Project Page" version="1.0">
+      <link href="http://openrpg.digitalxero.net"/>
+    </nodehandler>
+    <nodehandler class="link_handler" icon="html" module="forms" name="OpenRPG Forums" version="1.0">
+      <link href="http://forums.rpghost.com/forumdisplay.php?s=&amp;forumid=118"/>
+    </nodehandler>
+    <nodehandler class="link_handler" icon="html" module="forms" name="Submit A Bug Report" version="1.0">
+      <link href="http://openrpg.digitalxero.net/phpbb/viewtopic.php?t=10"/>
+    </nodehandler>
+    <nodehandler class="link_handler" icon="html" module="forms" name="OpenRPG Plugin HQ" version="1.0">
+      <link href="http://openrpg.mduo13.com/plugins.php"/>
+    </nodehandler>
+    <nodehandler class="link_handler" icon="html" module="forms" name="OpenRPG Web Ring" version="1.0">
+      <link href="http://www.ringsurf.com/netring?ring=OpenRPG;action=home"/>
+    </nodehandler>
+    <nodehandler class="link_handler" icon="html" module="forms" name="AutoRealm" version="1.0">
+      <link href="http://www.gryc.ws/autorealm.htm"/>
+    </nodehandler>
+    <nodehandler class="link_handler" icon="html" module="forms" name="PCGen" version="1.0">
+      <link href="http://pcgen.sourceforge.net/01_overview.php"/>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="group_handler" module="containers" name="Examples (Adventures)" version="1.0">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="file_loader" icon="d20" module="core" name="Bastion Press d20 Adventure" version="1.0">
+      <file name="Bastion_adventure.xml"/>
+    </nodehandler>
+    <nodehandler class="file_loader" icon="d20" module="core" name="Darwin's World d20 Adventure" version="1.0">
+      <file name="Darwin_adventure.xml"/>
+    </nodehandler>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/metaservers.cache	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+http://www.openrpg.com/openrpg_servers.php 1 2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/Bastion_adventure.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,136 @@
+
+<nodehandler class="group_handler" module="containers" name="Bastion Press" version="1.0">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="link_handler" icon="html" module="forms" name="Bastion Press Website" version="1.0">
+    <link href="http://www.bastionpress.com/"/>
+  </nodehandler>
+  <nodehandler class="group_handler" module="containers" name="Albine's Lair Adventure" version="1.0">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="link_handler" icon="html" module="forms" name="Albine's Lair Adventure (PDF)" version="1.0">
+      <link href="http://www.bastionpress.com/Downloads/albines%20lair.pdf"/>
+    </nodehandler>
+    <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Encounters" version="1.0">
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter Area 1- Pit Trap" version="1.0">
+        <text multiline="1" send_button="1">When theparty steps onto the pittrap, the ground aroundthem collapses, and theytumble 20 feet into a darktunnel, taking 2d6 points offalling damage. The pit itself collapses behind themand fills with rubble, making exiting the way they came in impossible. The area of the trap is 10 ft. by 10ft., which may not be large enough to encompass thewhole party. If it is necessary to keep the partytogether, this area can be enlarged. The trap will not spring until the weight of a certain number of crea-tures, chosen by the DM, is applied to it. This area canbe set in any of the pit trap areas shown on the en-counter map.
+
+Once the trap is sprung, the albine in area 3 willhear the commotion and come to devour its new meal.The encounter with the albine will either take placehere or in area 2, the tunnels, depending on whether the party stays put or moves into the tunnels.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter Area 2 - Tunnels" version="1.0">
+        <text multiline="1" send_button="1">This network oftunnels was dug by the albine, and winds throughoutits domain, connecting its traps to its lair. Thesetunnels are generally 5 feet wide and 7 feet high, haveearthen walls, and are not lit. </text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter Area 3 - Albine Lair" version="1.0">
+        <text multiline="1" send_button="1">This chamber isthe living space of the albine. It contains an array oflitter and debris, most of which is bones. A sizablepile of rags in one corner serves as the albines bed. </text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter area 4 - Refuse Chamber" version="1.0">
+        <text multiline="1" send_button="1">This areasmells quite foul, and contains two distinct piles, oneof discarded equipment and one of the albines waste.The equipment pile has several salvageable items:400gp, a suit of ordinary human-size chain mail, apotion of invisibility, and a blessed ring (see below). </text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter Area 5 - Exit" version="1.0">
+        <text multiline="1" send_button="1">This tunnel leads backto the surface. It can connect back to the originalencounter map at any point the DM chooses. A kindDM will connect this exit to a secret area or to some-where behind enemy lines. </text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Blessed Ring" version="1.0">
+      <text multiline="1" send_button="1">This ring subjects its wearer to effects similar to a bless spell at all times. While wearing this ring a character receives a +1 morale bonus to all his attack rolls and saving throws. </text>
+    </nodehandler>
+    <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Albine" version="1.0">
+      <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+        <form height="500" width="400"/>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+          <text multiline="0" send_button="0">Albine (Medium-size Monstrous Humanoid)</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="HD" version="1.0">
+          <text multiline="0" send_button="0">6d8+24 (51 hp) </text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+          <text multiline="0" send_button="0">30 ft, Burrow 10 ft. </text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC" version="1.0">
+          <text multiline="0" send_button="0">18 (+3 Dex, +5 natural) </text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face Reach" version="1.0">
+          <text multiline="0" send_button="0">5ft by 5ft / 5ft (10ft for tail) </text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+          <text multiline="0" send_button="0">Multiattack, Iron Will </text>
+        </nodehandler>
+        <nodehandler class="listbox_handler" icon="gear" module="forms" name="Skills" version="1.0">
+          <list send_button="1" type="1">
+            <option selected="0" value="0">Climb [1d20+6]</option>
+            <option selected="0" value="0">Hide [1d20+6]</option>
+            <option selected="1" value="0">Spot [1d20+5]</option>
+            <option selected="0" value="0">Listen [1d20+10]</option>
+            <option selected="0" value="0">Move Silently [1d20+7]</option>
+          </list>
+        </nodehandler>
+        <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+          <grid autosize="1" border="1">
+            <row version="1.0">
+              <cell>Str</cell>
+              <cell>21</cell>
+            </row>
+            <row version="1.0">
+              <cell>Dex</cell>
+              <cell>16</cell>
+            </row>
+            <row version="1.0">
+              <cell>Con</cell>
+              <cell>19</cell>
+            </row>
+            <row version="1.0">
+              <cell>Int</cell>
+              <cell>6</cell>
+            </row>
+            <row version="1.0">
+              <cell>Wis</cell>
+              <cell>13</cell>
+            </row>
+            <row version="1.0">
+              <cell>Cha</cell>
+              <cell>8</cell>
+            </row>
+          </grid>
+          <macros>
+            <macro name=""/>
+          </macros>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="form_handler" icon="form" module="forms" name="Combat Rolls" version="1.0">
+        <form height="300" width="400"/>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP" version="1.0">
+          <text multiline="0" send_button="0">51</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+          <text multiline="0" send_button="1">[1d20+3]</text>
+        </nodehandler>
+        <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+          <list send_button="1" type="3">
+            <option selected="1" value="0">Bite [1d20+11], Damage [2d6+5]</option>
+            <option selected="0" value="0">Claw [1d20+9], Damage [1d6+2]</option>
+            <option selected="0" value="0">Claw [1d20+9], Damage [1d6+2]</option>
+            <option selected="0" value="0">Tail [1d20+9], Damage None</option>
+          </list>
+        </nodehandler>
+        <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+          <list send_button="1" type="1">
+            <option selected="1" value="0">Will Power [1d20+5]</option>
+            <option selected="0" value="0">Relex [1d20+7]</option>
+            <option selected="0" value="0">Fortitude [1d20+11]</option>
+          </list>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Combat Info" version="1.0">
+        <text multiline="1" send_button="1">The albine charges readily into combat. While it fightswith its teeth and claws, it constantly whirls its tailback and forth around it, tripping its opponents. Ifreduced to ten or fewer hit points, it will retreat backinto its network of tunnels, or attempt to burrowaway.
+
+Whirl (Ex): While in combat, an albine constantlytwirls its tail around it along the ground. Once perround, roll a single melee touch attack for thecreatures tail. Everyone within 10 feet of the albinewhose AC is exceeded by this roll must immediatelymake an opposed strength check vs. the albines scoreof 5, or be tripped. Characters that succeed at thestrength check do not get an opportunity totrip the albine. This ability can be usedeven in cramped fighting quarters.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Description" version="1.0">
+        <text multiline="1" send_button="1">The ruthless albine is a rarebut dangerous subterra-nean creature. Thealbine cannot be found deepin the earth, but insteadstays quite close to thesurface at all times. It burrowsthrough the ground, building a complexnetwork of tunnel traps that connect toits lair. To make these traps, the albineburrows up underneath surface paths andcorridors, leaving just enough soil to keepthe path from collapsing unless it iswalked upon. Once ithas set a number ofthese traps, itretires to its lair atthe center of thisnetwork, andwaits and listensfor prey to comecrashing down into itsdomain.
+In appearance, the albine is onlyvaguely humanoid. It has an enlargedmouth, filled with sharp teeth poking outat wild angles, and extremely sharp claws. It has athick leathery hide with a pale, whitish hue. It is alsonoted for its long thick rat-like tail, that stretches outto ten feet in length, and which it uses to trip itsopponents in combat.
+
+The albine always carefully strips its prey offoreign objects before it devours it. These objects aregenerally disposed in a single tidy pile at the back ofits lair, making the albine a favorite amongst treasureseekers.
+</text>
+      </nodehandler>
+    </nodehandler>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/Darwin_adventure.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,493 @@
+
+<nodehandler class="group_handler" module="containers" name="Darwin's World" version="1.0">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="link_handler" icon="html" module="forms" name="Website" version="1.0">
+    <link href="http://www.darwinrpg.com/"/>
+  </nodehandler>
+  <nodehandler class="group_handler" module="containers" name="Cave of Life" version="1.0">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="link_handler" icon="html" module="forms" name="Cave of Life Adventure (PDF)" version="1.0">
+      <link href="http://www.rpgobjects.com/dlm/download.php?id=4"/>
+    </nodehandler>
+    <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Descriptions" version="1.0">
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Introduction" version="1.0">
+        <text multiline="1" send_button="1">
+The deserts of the post-holocaust world are a dangerous place, where fresh water is scarce and men brave enough to defend it even scarcer.  Storms of radiated wind, blights lasting years, and the savage bands of mutated raiders - often well equipped - are just some of the hazards that threaten to squelch good men and communities from rising from the ashes to find a new future.
+You and your fellows are a group of youths who have been raised in a small desert community in the wasteland.  You have seen the efforts and toil of your forefathers to build this small community into a hopeful future, and you have lived well among the others behind the adobe walls of your desert home.  Yet the time comes when you and your friends, nearing adulthood, will be required to pass the rite of manhood that all warriors and heroes of your tribe have taken since ages past.
+You must travel to the infamous Cave of Life, and retrieve water from the endless supply said to be hidden deep within the complex's many twisting caves.  You must brave the dangers of the cave, its rumored denizens, and bring back at least four full waterskins to your community so that you can take your place among the worthy.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="A. Cave Entrance." version="1.0">
+        <text multiline="1" send_button="1">
+Following the words of your elders, half concealed in deep spiritual poetry and deceptive metaphors, you have at last found what must be the entrance to the legendary Cave of Life.  Here, the flat level expanse of nothingness suddenly gives way to isolated bumps and rises, as if the sands themselves were acting to hide some secret just beneath their surface.  A few hundred paces up ahead, silhouetted by the rays of the dying sun, can be seen distant metal &quot;pillars&quot;, perhaps fifteen feet high or so, jutting from the sand in the distance.
+
+Nearby, amid the circle of rubble where the wind cannot groom the sand completely flat, the observant among you spot something of quieting wonder - a section of cracked stone, cleared of sand, at the center of which lies a huge &quot;hatch&quot; of thick metal, almost as large as a man.  Jammed open by thick flakes of age-old rust on its heavy iron hinges, it seems to beckon your approach with an almost &quot;malevolent&quot; impatience.
+
+This surely must be the Cave of Life.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="B. Entranceway." version="1.0">
+        <text multiline="1" send_button="1">
+A narrow shaft, made of some remarkably smooth stone - no doubt worked by some unknown hand - descends into the cold darkness below.  One by one you descend on old iron rungs set straight into the stone, aware of the growing blackness and increasing chill.
+
+Finally, at the bottom, you find what appears to be a series of small empty chambers, each filled with dust and accumulated debris - bits of stone, rotted trash, and bundles of wiring, piping, and twisted iron bars fused in some great catastrophe long, long ago.
+
+Your footsteps echo eerily into the darkness, but there is a slight comfort - a comfort in knowing that past heroes of your people once passed this way as well in search of the cave's eternal source of water.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="C. Elevator Shaft." version="1.0">
+        <text multiline="1" send_button="1">
+The cold and unfeeling silence of concrete has given way to an even colder and more menacing nothingness.  A metal platform sits here, ringed by a slender, weakened railing of rusted iron, over the edge of which you can only see plummeting darkness with seemingly no end - up or down.  A small set of rusted stairs appears to descend into pitch blackness as well, running along the insides of the inner wall - from this height, with your meager lights (torch or lantern), your eyes can barely see the level below where the stairs level off, but you cannot make out any distinct features.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="D. Blast Doors" version="1.0">
+        <text multiline="1" send_button="1">
+Your long descent into darkness, though precarious and terrifying, has finally come to an end at the great central shaft's end.  But the area at the bottom of the rickety iron stairs is no less cold and lifeless as the chambers above.  What few lights you have illuminate the open metal elevator shaft that continues on down past this area as well into more darkness below. A careful look over the edge shows a calm, black water has filled up the shaft nearly to this level, and there is no telling what lies further down the flooded shaft underwater.  The stairs, too, continue on under the murky water, slipping out of sight due to the weakness of your lights.
+
+Along the surface of the walls of this area, you find a small alcove; here stands a pair of truly gigantic metal doors, each several feet larger than the tallest man, with powerful hand wheels on the outer surface on each door.  The metal surface of the doors is marked by old graffiti from unknown visitors to the caves, no doubt many of which are decades old.
+
+Luckily, the massive metal doors stand permanently ajar; beyond, only darkness can be seen.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="E. Entrance Tunnel." version="1.0">
+        <text multiline="1" send_button="1">
+Beyond the massive portals of steel lies a strange tunnel, itself leading off into more unrelenting darkness.  Each person present gets a strange sense here, as if you have entered a place long...forgotten.  Indeed, the very nature of the tunnel seems strange to your unaccustomed eyes - not at all like the naturally occurring caves you have seen or heard of in the desert.
+
+This tunnel is oval in its bizarre shape, and seems to actually be made of a corrugated metal - heavily rusted, but easily differentiated from the stone you expected to see.
+
+Trash, bits of fallen stone, and lengths of discarded pipe lie everywhere.  Your first careless footsteps echo through the eerie emptiness, answered only by the gloomy sound of dripping water in the distance.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="F. Water Tanks." version="1.0">
+        <text multiline="1" send_button="1">
+Much to your collective surprise, a large alcove lies here off from the main tunnel.  Peering within, holding up lamps and torches, you see a series of old rusted pipes leading from the roof to a pair of gigantic, black, cylindrical tanks set straight into the metallic wall.
+
+Murky water and runny mud pools on the ground at the base of these giant metal tanks, while a trickle of ice-cold water can be seen dripping from various points on the old wall pipes.
+This must be the legendary wellspring of your people!
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="G. Communications Tunnel." version="1.0">
+        <text multiline="1" send_button="1">
+This tunnel stretches off into darkness.  The walls appear to be made of the same corrugated metal as elsewhere in the &quot;caves&quot;.  Long sections of rusted pipe line at least one whole section of the tunnel, also stretching off into darkness.  You can hear the sound of dripping water far in the distance.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="H. Junction." version="1.0">
+        <text multiline="1" send_button="1">
+Your long and careful progress down the narrow tunnel seems to have panned out somewhat - you stand at the junction of two tunnels, a small stone room reinforced by a jumble of old rusted pipes and cables, the purpose of which is long forgotten.  The room here seems to have flooded long ago, as a deep level of calm black water fills the entire room; metal steps lead from your elevated tunnel entrance and into the water below.  A narrow &quot;ledge&quot; of stacked pipes leads off along one wall to the right tunnel; the other tunnel has no such ledge.
+
+You hear water dripping from both directions.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="I. Antenna Silo A." version="1.0">
+        <text multiline="1" send_button="1">
+The flooded tunnel you have been following leads to what appears to be a dead end.  Lifting a light reveals, with murky dimness, a large iron portal at the corridor's far end, standing only slightly open - just wide enough for a slender person, perhaps, to slip through.
+
+Entering with great caution, abandoning your bulkier gear outside, you see the chamber here to also be flooded like the rest of this section of the caves; the water appears deeper here, however, and much darker.
+
+This cavern is smoothly cylindrical in nature, extending far up and into impenetrable darkness, the concrete walls ringed with rusted metal pipes, a maze of cables, etc.
+A short metal platform (with a submerged stair leading from beneath the water to the platform's level) sits in the center of the room almost like an island.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="J. Antenna Silo B." version="1.0">
+        <text multiline="1" send_button="1">
+At the end of the long waterlogged tunnel, a circular portal - now missing - leads into a darkened abyss.  Entering, you see that this chamber, too, is entirely flooded.  An old metal platform sits in the center of the cylindrical tunnel, but the roof above - if there is one - cannot be seen.
+
+The sound of dripping water and loud squeaks echoes throughout.  As you raise your torch for a better view, the darkness around the platform comes alive with nearly a dozen sets of glowing red eyes!
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="K. Environment Control Room." version="1.0">
+        <text multiline="1" send_button="1">
+This huge chamber appears to be a sphere in fact, and is completely open; your lights cast eerie shadows across old rusted equipment and heavy machines leading up to the domed ceiling high overhead.  Pipes and air ducts form a virtual &quot;web&quot; of metal and plastic everywhere, creating unnerving illusions of shadow and dim light wherever one looks.
+
+A set of narrow rusted stairs lead from the entrance tunnel down to the dusty machine floor below, while a metal catwalk runs from where you stand around the perimeter of the chamber, about ten feet or so above the floor (i.e. cutting the chamber in two).  From where you stand, you can see various tunnels on the catwalk level leading off into darkness.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="M(A). Crew Living Area." version="1.0">
+        <text multiline="1" send_button="1">
+As you enter this area you smell dampness and rot prevalent in the smothering, musty air.
+The chamber is strangely-shaped, as if it were the top part of a giant, cross-sectioned sphere - the floor, extensively damaged and littered with broken stone, discarded pipes, and rusted metal paneling, changes from bare concrete near the entrance tunnel to a black and white checkerboard towards the rear - as if a wall once separated this area into two distinct rooms.  The wall, however, is long gone.
+
+The south part of the room is sectioned off by a cracked stonewall, and an open doorway sits empty in its face.  A narrow set of stairs, and a fallen-away elevator can be seen near the entrance tunnel, and strange graffiti covers nearly every stretch of wall.
+The sound of dripping water can be heard echoing throughout.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Log Entry" version="1.0">
+        <text multiline="1" send_button="1">June 10, 2011.  I can't believe it; it's finally come to this.  I must have been in shock the day the order came because I can't remember us locking up.  All I could think of was dear Judy, my dear, sweet Judy.  New York City gone.  Completely gone.  But we have a job to do, and I must forget ... forget everything, forget it all.  It's all gone now anyway, isn't it?
+
+June 11, 2012.  We were hit last night.  Radar tracked the SS coming in.  We stand tall, despite the collapse of sections A and B.  It doesn't matter, we've already launched.  Kiev, Canterbury, and Munich stand in ruins.  And it was my order.  My goddamn order...
+
+June 16, 2012.  No contact.  Some of the men are showing signs of strain.  Nelson is keeping to himself, too quiet.  I wonder about him.  McCartney is keeping his hold well, and is taking stock of the supplies with his usual orderliness.  If this ends - if it ever ends - I want that man commended.
+
+June 17, 2012.  No contact.  Sent MacIntyre out today in a suit to check the surface radiation level.  He came back with a poor little dog that was dying from the radiation.  I could tell the man's sanity was slipping, but he needed to save something, anything.  We've kept the poor thing - I think he's nicknamed it &quot;Survivor&quot;.  Even made a nametag for it...
+
+June 18, 2012.  Still no contact.  Survivor is gone.  Nelson said he's somewhere in the tunnels, but our calls go unanswered.  Someone is stealing supplies - if this persists we're not going to last.  Who cares?  There's nothing left anyway.  Nothing but a desolate world of radiation and poison.  Dear God what have we done?
+
+June 19, 2012.  No contact.  What's the use?  There's no one out there, no one left.  We're all alone.
+
+July 20, 2012.  Strange noises tonight, echoing in the complex.  MacIntyre is losing his cool.  Thinks there's something in the complex with us.  McCartney thinks it's the structure in Silos A and B, just creaking.  That must be the cause.  Lasted for a few hours, then died out completely.
+
+July 21, 2012.  Nelson has failed to report to duty.  A search has turned up nothing.  He's gone mad, fled to the surface.  None of the suits are missing - he won't last three hours out there.  Poor devil.
+
+July 22, 2012.  MacIntyre has cracked.  Says he saw Survivor in the tunnels, but ... I fear MacIntyre is suffering from post-traumatic stress disorder and will be detained until he shows some sign of improvement.
+
+July 23, 2012.  Strange noises again last night.  I think the complex is weakening.  I fear we may not have much time left, though we cannot go to the surface yet - Rads registering at 2000.  MacIntyre was screaming nearly until dawn about Survivor being &quot;changed&quot;, a &quot;horror&quot;.  McCartney is keeping him under.  Note to self - are the supplies of morphine running extraordinarily low, or am I imagining it?  Did Nelson steal the stock before fleeing?  McCartney claims the stock is as it should be.
+
+July 25, 2012.  MacIntyre has had an overdose and is comatose.  I think McCartney has been dipping into the morphine - he is totally withdrawn.
+
+July 26, 2012.  Dear Lord watch over us.  Only you can save us now.  Confronted McCartney about the morphine.  He has confessed to stealing not only morphine, but also water and food.  Claims we are going to die no matter what.  He's right.  There really is no hope for us - the world outside is gone.  May we all burn in Hell for what we have done.
+
+July 28, 2012.  Strange noises again last night.  I can't help but wonder what keeps making that awful roar.  Sent McCartney into the tunnels to examine the structural damage.  He hasn't returned.  Will take gear and find him tomorrow.
+
+July 29, 2012.  Put raving MacIntyre out of his misery.  Taking pistol into the tunnels.  Will find McCartney.  Will return and take another surface reading.
+
+Here the journal ends.  No further entries are evident.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="M(B). Crew Living Area (Lower Level)." version="1.0">
+        <text multiline="1" send_button="1">Descending into this dismal and noxious place causes each of you to swoon in turn.  The stench of decay, heavy rust, and spoiled water is almost overpowering - as is the subtle smell of rotten flesh.  Columns of machinery and burnt-out computers rise from ceiling to floor here, creating an almost maze-like labyrinth; coils of heavy wiring dangle precariously from above like spider webs.  The sound of dripping water echoes mournfully through the darkness, interrupted suddenly by a momentary &quot;slithering&quot; noise.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="N. Launch Tunnel." version="1.0">
+        <text multiline="1" send_button="1">
+The sound of dripping water echoes here as elsewhere in the haunting subterranean complex; the passage is low, dark, and musty, and the sound of your footsteps echoes in the distance; it is obvious this passage goes on for quite awhile.
+
+A narrow channel of water, no doubt a result of subterranean flooding, leads off into the darkness.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="O. Fuel Room." version="1.0">
+        <text multiline="1" send_button="1">
+This small room is dominated by many pipes, several empty and overturned fuel cans, and a large black metal tank sunk into the concrete of the wall itself; the smell of kerosene is pervasive...
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="P. Junction." version="1.0">
+        <text multiline="1" send_button="1">
+This twisted &quot;cavern&quot;, filled with inches of black, smelly water, reeks strongly of rot and decay, with pipes hanging with rust &quot;curtains&quot; from the high ceiling overhead.  Tunnels lead off in every direction.
+
+A huge pile of trash and garbage rots underneath an open rusted hatch in the ceiling, and weak beams of sunlight can be seen piercing through the darkness above!
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Q. Collapsed Tunnels." version="1.0">
+        <text multiline="1" send_button="1">
+The tunnel here is abruptly cut off by twisted, corrugated sheeting and huge blocks of smooth, shaped stone; soil and twisted metal clog the passage.  Some great cataclysm apparently caused this area to cave in.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="R. Ventilation System Access Points." version="1.0">
+        <text multiline="1" send_button="1">
+At this point in the corridor a set of metal rungs, driven into the corrugated iron wall, leads up to an open shaft above.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="S. Silo Junction." version="1.0">
+        <text multiline="1" send_button="1">
+The tunnels lead to this wide barren chamber, the floor of which has fallen away in the center.  Looking down the hole (which runs the length of the place), you see all manner of trash, fallen debris, and rubble below.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="T. Fuel Silo." version="1.0">
+        <text multiline="1" send_button="1">
+From the broken doorway, looking in, you can see that this part of the complex has flooded considerably, almost up to the point where your party now stands; the dark black water is only a few feet or so below the level of the entrance tunnel.  Across the dark forbidding water can be seen an old concrete platform; a &quot;doorway&quot; beyond seems to be collapsed, and earth pours out onto the platform itself.
+
+The sound of dripping water fills the entire place, echoing loudly.
+As you approach, you see several eerie glows just beneath the surface of the water, which quickly sink deep into the darkness of the murky water as you enter...
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="U. Silo 3 Entry." version="1.0">
+        <text multiline="1" send_button="1">
+A large metal door, rusted and jammed in an open position, leads to a small barren room beyond; rusted pipes and creaking overhead ventilation shafts hang precariously from the dripping wet ceiling.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="V. Rocket Silo." version="1.0">
+        <text multiline="1" send_button="1">
+Your footsteps echo as you come to the edge of a small platform overlooking a massive cylindrical chamber that stretches up into darkness and down into dark murky water.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="W(A). Control Silo." version="1.0">
+        <text multiline="1" send_button="1">
+The corrugated tunnel you have been following leads into a dark circular chamber, the walls of which are hard, smooth stone.  A great deal of trash and debris litters the entire place; the smell is absolutely horrible.  A metal ladder, rusted in places, leads up to an open trapdoor above; another open trapdoor leads into darkness below.  A doorway, the door torn from its hinges long ago, sits across the chamber.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="W(B). Control Computers." version="1.0">
+        <text multiline="1" send_button="1">
+This dome-shaped chamber is filled with massive computer consoles and heavy electronic equipment along every stretch of wall, rising nearly from floor to ceiling.  All of these ancient machines are extensively damaged, rusted, or simply burned-out from time and abandonment, but a strange sense of dire purpose seems evident here - as if this place had once been the heart of the entire cavern complex.
+
+Now, however, it is just a grimy, moist, and rancid ruin.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="W(C). The Pit." version="1.0">
+        <text multiline="1" send_button="1">
+Creaking metal stairs lead down into a musty, awful chamber below.  The cluttered cavernous place is filled with old bashed and battered pieces of heavy electronic machinery, each covered in a thick layer of flaky rust or blackish-green verdigris.  Old dials and meters, once brimming with lights, have shattered or rusted completely over, turning the place dark and quiet.
+
+The pitch blackness here is only weakly illuminated by the glow of your torches and lamps, revealing motes of dust suspended in the air, kicked up by something waiting just on the edge of the light...
+</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="group_handler" module="containers" name="Creatures" version="1.0">
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Life Lamprey" version="1.0">
+        <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+          <form height="500" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+            <text multiline="0" send_button="0">Life Lamprey</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="HD" version="1.0">
+            <text multiline="0" send_button="0">2d8 (9 hp)</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+            <text multiline="0" send_button="0">swin 40 ft</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC" version="1.0">
+            <text multiline="0" send_button="0">15 (+3 Dex, +1 size, +1 natural)</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face Reach" version="1.0">
+            <text multiline="0" send_button="0">5 ft. by 5 ft./5 ft.</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+            <text multiline="0" send_button="0">Blindsight, Improved grab, Radioactive venom</text>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Skills" version="1.0">
+            <list send_button="1" type="1">
+              <option selected="1" value="0">Spot [1d20+2]</option>
+              <option selected="0" value="0">Listen [1d20+2]</option>
+            </list>
+          </nodehandler>
+          <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+            <grid autosize="1" border="1">
+              <row version="1.0">
+                <cell>Str</cell>
+                <cell>10</cell>
+              </row>
+              <row version="1.0">
+                <cell>Dex</cell>
+                <cell>17</cell>
+              </row>
+              <row version="1.0">
+                <cell>Con</cell>
+                <cell>11</cell>
+              </row>
+              <row version="1.0">
+                <cell>Int</cell>
+                <cell>2</cell>
+              </row>
+              <row version="1.0">
+                <cell>Wis</cell>
+                <cell>2</cell>
+              </row>
+              <row version="1.0">
+                <cell>Cha</cell>
+                <cell>2</cell>
+              </row>
+            </grid>
+            <macros>
+              <macro name=""/>
+            </macros>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="form_handler" icon="form" module="forms" name="Combat Rolls" version="1.0">
+          <form height="300" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP" version="1.0">
+            <text multiline="0" send_button="0">9</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+            <text multiline="0" send_button="1">[1d20+3]</text>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+            <list send_button="1" type="3">
+              <option selected="0" value="0">Bite [1d20+3], Damage [2d4]</option>
+            </list>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+            <list send_button="1" type="1">
+              <option selected="1" value="0">Will Power [1d20-4]</option>
+              <option selected="0" value="0">Relex [1d20+5]</option>
+              <option selected="0" value="0">Fortitude [1d20+2]</option>
+            </list>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Combat Info" version="1.0">
+          <text multiline="1" send_button="1">
+A life lamprey will attack its prey with its voracious bite and needle-sharp teeth.  Prey resistant to one or two bites will likely be injected with its radioactive venom.
+
+Attach (Ex):  A life lamprey that hits with its bite attack latches onto the opponent's body with its powerful jaws.  An attached life lamprey loses its Dex bonus to AC while attached.  A life lamprey must be attached to use its radioactive venom.
+
+Radioactive Venom (Sp):  Once it has a hold, a life lamprey can inject its prey with a lethal dosage of radiation.  Each round that it attempts this, the life lamprey afflicts such a victim with 1d20x10 Rads.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Pit Creature" version="1.0">
+        <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+          <form height="500" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+            <text multiline="0" send_button="0">Pit Creature</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="HD" version="1.0">
+            <text multiline="0" send_button="0">7d8+35 (67 hp)</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+            <text multiline="0" send_button="0">20 ft</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC" version="1.0">
+            <text multiline="0" send_button="0">13 (+1 Dex, -2 size, +4 natural)</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face Reach" version="1.0">
+            <text multiline="0" send_button="0">10 ft by 5 ft / 15 ft</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+            <text multiline="0" send_button="0">Blind-Fight, Cleave, Improved Initiative, Multiattack, Power Attack, Dark Vision</text>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Skills" version="1.0">
+            <list send_button="1" type="1">
+              <option selected="0" value="0">Hide [1d20+10]</option>
+              <option selected="1" value="0">Spot [1d20+7]</option>
+              <option selected="0" value="0">Listen [1d20+7]</option>
+            </list>
+          </nodehandler>
+          <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+            <grid autosize="1" border="1">
+              <row version="1.0">
+                <cell>Str</cell>
+                <cell>25</cell>
+              </row>
+              <row version="1.0">
+                <cell>Dex</cell>
+                <cell>12</cell>
+              </row>
+              <row version="1.0">
+                <cell>Con</cell>
+                <cell>21</cell>
+              </row>
+              <row version="1.0">
+                <cell>Int</cell>
+                <cell>9</cell>
+              </row>
+              <row version="1.0">
+                <cell>Wis</cell>
+                <cell>9</cell>
+              </row>
+              <row version="1.0">
+                <cell>Cha</cell>
+                <cell>5</cell>
+              </row>
+            </grid>
+            <macros>
+              <macro name=""/>
+            </macros>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="form_handler" icon="form" module="forms" name="Combat Rolls" version="1.0">
+          <form height="300" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP" version="1.0">
+            <text multiline="0" send_button="0">67</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+            <text multiline="0" send_button="1">[1d20+5]</text>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+            <list send_button="1" type="3">
+              <option selected="1" value="0">Bite [1d20+10], Damage [2d8+8]</option>
+              <option selected="0" value="0">Claw [1d20+7], Damage [2d4+7]</option>
+              <option selected="0" value="0">Claw [1d20+7], Damage [2d4+7]</option>
+            </list>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+            <list send_button="1" type="1">
+              <option selected="1" value="0">Will Power [1d20+1]</option>
+              <option selected="0" value="0">Relex [1d20+3]</option>
+              <option selected="0" value="0">Fortitude [1d20+10]</option>
+            </list>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Combat Info" version="1.0">
+          <text multiline="1" send_button="1">
+A life lamprey will attack its prey with its voracious bite and needle-sharp teeth.  Prey resistant to one or two bites will likely be injected with its radioactive venom.
+
+Attach (Ex):  A life lamprey that hits with its bite attack latches onto the opponent's body with its powerful jaws.  An attached life lamprey loses its Dex bonus to AC while attached.  A life lamprey must be attached to use its radioactive venom.
+
+Radioactive Venom (Sp):  Once it has a hold, a life lamprey can inject its prey with a lethal dosage of radiation.  Each round that it attempts this, the life lamprey afflicts such a victim with 1d20x10 Rads.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Gray Ooze" version="1.0">
+        <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+          <form height="500" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+            <text multiline="0" send_button="0">Gray Ooze</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="HD" version="1.0">
+            <text multiline="0" send_button="0">3d10+10 (26 hp)</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+            <text multiline="0" send_button="0">10 ft</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC" version="1.0">
+            <text multiline="0" send_button="0">5 (-5 Dex)</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face Reach" version="1.0">
+            <text multiline="0" send_button="0">5 ft by 5 ft / 15 ft</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+            <text multiline="0" send_button="0">Blindsight, cold and fire, immunity, ooze, camouflage</text>
+          </nodehandler>
+          <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+            <grid autosize="1" border="1">
+              <row version="1.0">
+                <cell>Str</cell>
+                <cell>25</cell>
+              </row>
+              <row version="1.0">
+                <cell>Dex</cell>
+                <cell>12</cell>
+              </row>
+              <row version="1.0">
+                <cell>Con</cell>
+                <cell>21</cell>
+              </row>
+              <row version="1.0">
+                <cell>Int</cell>
+                <cell>9</cell>
+              </row>
+              <row version="1.0">
+                <cell>Wis</cell>
+                <cell>9</cell>
+              </row>
+              <row version="1.0">
+                <cell>Cha</cell>
+                <cell>5</cell>
+              </row>
+            </grid>
+            <macros>
+              <macro name=""/>
+            </macros>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="form_handler" icon="form" module="forms" name="Combat Rolls" version="1.0">
+          <form height="300" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP" version="1.0">
+            <text multiline="0" send_button="0">26</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+            <text multiline="0" send_button="1">[1d20-5]</text>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+            <list send_button="1" type="3">
+              <option selected="1" value="0">Slam [1d20+3], Damage [1d6+1], Acid [1d6]</option>
+            </list>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+            <list send_button="1" type="1">
+              <option selected="1" value="0">Will Power [1d20-4]</option>
+              <option selected="0" value="0">Relex [1d20-4]</option>
+              <option selected="0" value="0">Fortitude [1d20+1]</option>
+            </list>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Combat Info" version="1.0">
+          <text multiline="1" send_button="1">Improved Grab (Ex): To use this ability, the gray ooze must hit with its slam attack. If it gets a hold, it can constrict.
+
+Acid (Ex): A gray ooze secretes a digestive acid that quickly dissolves organic material and metal. Any melee hit deals acid damage. The oozes acidic touch deals 40 points of damage per round to wood or metal objects. Armor or clothing dissolves and becomes useless immediately unless it succeeds at a Reflex save (DC 19). The acid cannot harm stone. A metal or wooden weapon that strikes a gray ooze also dissolves immediately unless it succeeds at a Reflex save (DC 19).
+
+Constrict (Ex): A gray ooze deals automatic slam and acid damage with a successful grapple check. The opponents clothing and armor suffer a -4 penalty to Reflex saves against the acid.
+
+Camouflage (Ex): It takes a successful Spot check (DC 15) to recognize a motionless gray ooze for what it really is.</text>
+        </nodehandler>
+      </nodehandler>
+    </nodehandler>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/FFE_adventure.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,3576 @@
+
+<nodehandler class="group_handler" module="containers" name="Fast Forward Entertainment Sponsorship of OpenRPG" version="1.0">
+  <nodehandler class="link_handler" icon="html" module="forms" name="Fast Forward Entertainment WebSite" version="1.0">
+    <link href="http://www.fastforwardgames.com/"/>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Introduction" version="1.0">
+    <text multiline="1" send_button="1">Welcome to Fast Forward Entertainment's free mini-campaign for OpenRPG. FFE is proud to sponsor such a wonderful project and community, one that has opened up a whole new world for those of us who love both computers and role playing games. It is our hope at Fast Forward that many more companies will follow suit in support of the OpenRPG project, which in the end can do nothing but benefit the hobby as a whole. Fast Forward hopes to include several free adventures in future builds of OpenRPG as well, supporting our products and d20 games in general.
+
+Included in this build of OpenRPG is a chapter from one of Fast Forward's best selling products of 2002-Treasure Quests. In the TQ campaign adventure the heroes travel overland on their way to Capital City, and along the way are finding clues and treasures left behind by three great heroes of the land: Ren the High Wizard, Thayer the Bard, and Hedras the Warlord. The included mini-campaign is a portion of Treasure Quests where the character's travel overland through a valley ruled by an unpleasant stone giant and his humanoid minions. How the Game Master incorporates these adventures into their OpenRPG campaign is up to them; they can break the sections into individual encounters, or play the entire mini-campaign as a whole.
+
+and if you want to see how it ends, pick up Treasure Quests at you local retailer!
+</text>
+  </nodehandler>
+  <nodehandler class="group_handler" module="containers" name="Valley of the Stone Giant" status="useful" version="1.0">
+    <nodehandler class="group_handler" module="containers" name="Scenario #2a: The Stone Giant Toll" version="1.0">
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #1: Stone Giant's Lair" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1 DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #1: Empty Stone Giant Lair
+   Overlooking the valley from a slight rise can be seen the gaping mouth of the lair said to belonging to the stone giant known as Sledge; however, at this time he is not here. He only returns to the lair to stash and accumulation of treasure he has obtained, or to sleep. The lair can be seen from most portions of this small valley on a Spot check (DC of 10-8 if the heroes know of its existence).
+
+   Reaching the lair is no easy feet for someone smaller than the giant, who simply jumps up the cave mouth. Before the entrance is a shear vertical slope, with a jagged edge jutting out from the cave's lip 20 feet up. Short of flying or magical spells (such as jump, levitate, or fly), heroes wanting to enter the lair must climb over the ledge. The base DC to climb this shear is 25; items such as a rope or climbing pitons can subtract from this number. For further information on climbing skill checks see page #65 of the Player's Handbook.
+
+   The interior of the lair is nondescript, being a rough hewn semi-circle 80 feet in diameter with a concave ceiling that reaches a height of 50 feet. In the center of the cave is an enormous boulder of irregular shape. Beneath the boulder is the giant's cache of treasure; however, the unbalanced shape of the boulder prevents it from being simply rolled to the side. Sledge's size and strength (13 feet tall, STR 27) allow him to lift the rock to one side, exposing the treasure vault. DC of 27 to move the rock.
+
+   The rock weighs 3,000 pounds, and will take the combined effort of several characters to move. Beneath the rock is the following: 2,433 gold, 1,289 silver, 766 copper, a rough-cut sapphire worth 500gp, and a ring of chameleon power.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="1">The rock weighs 3,000 pounds, and will take the combined effort of several characters to move. Beneath the rock is the following: 2,433 gold, 1,289 silver, 766 copper, a rough-cut sapphire worth 500gp, and a ring of chameleon power.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: Treasure (Player)" version="1.0">
+          <text multiline="1" send_button="1">Inside the hole appears to be a treasure cache. The sacks this treasure is in are extremely large -- giant size as it were. Beneath the rock is the following: 2,433 gold, 1,289 silver, 766 copper, a rough-cut sapphire, and a copper ring w/runes.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1 Cave Description" version="1.0">
+          <text multiline="1" send_button="1">The interior of the lair is nondescript, being a rough hewn semi-circle 80 feet in diameter with a concave ceiling that reaches a height of 50 feet. In the center of the cave is an enormous boulder of irregular shape.
+
+See lifting and dragging rules on page 142 of PHB</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: Conclusion/Read to Players" version="1.0">
+          <text multiline="1" send_button="1">It is mid afternoon and you have just pillaged the giant's lair. From this perch you can see the entrance to the valley, and it seems to be blocked by some structure. It is hard to tell from this distance.</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #2: Toll Block" version="1.0">
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Orcs (10)" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Orc NPC" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Orc NPC
+Class:      Fighter 1
+Level:      1
+Race:       Orc
+Alignment:      Chaotic Evil
+Gender:     M
+Size:       M
+Experience Points (XP):     0
+(XP) Needed for Next Level:     1000
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages: Orc
+*******************************************************************************
+Score:  Mod:    Abilities:
+15  +2  :Strength (STR)
+10  +0  :Dexterity (DEX)
+11  +0  :Constitution (CON)
+9   -1  :Intelligence(INT)
+8   -1  :Wisdom(WIS)
+8   -1  :Charisma(CHA)
+*******************************************************************************
+14  :(AC) Armor Class
+10  :(AC) vs. Touch
+14  :(AC) Flat footed
+10  :(HP) Maximum Hit Points
+*******************************************************************************
++0  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++1
+Ranged Bonuses:
++1
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+*Greataxe
++3  1d12+3  20/x3   0'
+Crossbow (Light)
++1  1d8 19-20/x2    80'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*Scale Mail
++4  +3  -4  25
+*******************************************************************************
+Saving Throws:
++2  :FORTITUDE (CON)
++0  :REFLEX (DEX)
+-1  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+2   Jump
+3   Listen
+3   Spot
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+66.0    133.0   200.0
+57.0 lbs    :Total Encumbrance
+Light   :Carry Load
+200.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Scale Mail
+1   Greataxe
+1   Crossbow (Light)
+1   Bolts (Crossbow/10)
+*******************************************************************************
+Funds:
+Gold Pieces: 190
+Misc Finds:
+Misc Magic:
+Total Valued: 106.0 gp
+*******************************************************************************
+Feats:
+Alertness,Armor Proficiency (Heavy) (1x),Armor Proficiency (Light) (1x),Armor Proficiency (Medium) (1x),Martial Weapon Proficiency,Point Blank Shot,Shield Proficiency,Simple Weapon Proficiency
+*******************************************************************************
+Class Abilities:
+Light Sensitivity(Ex)
+*******************************************************************************
+Racial Traits:
+
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="ORC INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">14</text>
+              </nodehandler>
+              <nodehandler class="form_handler" icon="form" module="forms" name="ORC HIT POINTS" version="1.0">
+                <form height="400" width="400"/>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 1 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 2 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 3 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 4 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 5 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 6 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 7 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 8 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 9 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 10 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">*Greataxe (Crit.20x3) - [1d20+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d12+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[3*1d12+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Crossbow (Light) (Crit.19-20x2) - [1d20+1]
+</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d8]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+2]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20-1]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Jump [1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Listen [1d20+3]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Spot [1d20+3]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2a: Random Orc Treasure" version="1.0">
+            <text multiline="0" send_button="1">[3d10] gold pieces each</text>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: DM Information" version="1.0">
+          <text multiline="1" send_button="1">Encounter #2: Stone Giant Toll Block
+   The only way to access this small valley is through either a north or south toll set up by the stone giant Sledge. The stone giant is currently at Scenario #2f: Mountain Pass.
+
+   The tolls here are constructed from several fallen oak trees from the center of the valley. The trees are layered together to form a barrier across the road, and jagged terrain in the area make passage around the toll by horse or wagon treacherous and impassable. For someone who is not the size and strength of a stone giant, a combined strength of 35 is needed to move the trees. Sledge's mass and leverage allow him to move the fallen oaks on his own. In addition, moving the trees cause them to fall with a tremendous &quot;THUD&quot; to the ground, and can possibly be heard from anywhere in the general locale (Listen check vs. DC 12).
+
+   The levy is enforced by a small group of orcs hired by the stone giant. They're allowed to keep  of all the tolls they collect, and with the threat of the giant's wrath they're pockets are kept lined with gold.
+
+Orcs (15): SZ M; HD 1d8+4; hp8; Init +0; Spd 20; AC 14 (+4 scale mail); Atk: greataxe (1d12+3) or light crossbow +0 (1d8); Face 5 ft. x 5 ft.; Reach 5 ft.; Save Fort +2, REF +0, Will -1; Str 15, Dex 10, Con 11, Int 9, Wis 8, Chr 8; AL CE; Skills: Listen +4, Spot +3; Feats: Alertness; Orcs suffer a -1 penalty to attack rolls in bright sunlight or within the radius of a daylight spell.
+
+   Each orc carries a longsword and 3d10 gold pieces. If the fight is going against them they flee towards Encounter #6: Orc Lair, where there are additional orc brethren.
+
+   The toll exacted here is two gold pieces per person to pass, five gold per horse, and 10 gold per wagon. The fee is always negotiable, and Sledge and the orcs often try and get more coins out of weary travelers.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: Read to Players" version="1.0">
+          <text multiline="1" send_button="1">The tolls themselves are constructed from several fallen oak trees from the center of the valley. The trees are layered together to form a barrier across the road, and jagged terrain in the area make passage around the toll by horse or wagon treacherous and impassable. </text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #3: Gold Mine" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3 DM Information" version="1.0">
+          <text multiline="1" send_button="1">Encounter #3: Dangerous Gold Mine
+   An old boarded up mine is located on a small incline to not far from the southern toll. The mine was &quot;played out&quot; many years ago by local dwarves, and the entrance has since become overgrown with shrubbery. Locating the mine is difficult even during daylight hours, and near impossible at night. A spot skill check versus a DC of 20 is needed to find the entrance from a distance greater than 30 feet (DC 30 at night), but drops to 12 if the heroes approach. Sledge knows of the mine's existence (and its lack of gold), but the orcs from Encounter #6 have never found it despite its proximity
+
+   The mine delves several hundred feet into the hillside, but the ceiling inside is extremely unstable. In its weakened state, 10 points of damage dealt to any portion of the mine (ceiling, supports, walls, etc) is enough to cause a collapse. Each section collapsed is 2d10 feet in length, and victims sustain 8d6 points of damage, half that amount if they make a successful Reflex saving throw (DC 15). Those that fail are pinned and unable to free themselves. For further information on cave-ins and collapses see page #114 of the Dungeon Master's Guide.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3 Read to Players" version="1.0">
+          <text multiline="1" send_button="1">An old boarded up mine is located on a small incline to not far from the southern toll. The mine was likely &quot;played out&quot; many years ago, as the entrance has since become overgrown with shrubbery.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #4: Rock Fall Trap" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4 DM Information" version="1.0">
+          <text multiline="1" send_button="1">Encounter #4: Rock Fall Trap
+   The unlawful tolls of the stone giant Sledge have been in jeopardy on more than one occasion. The giant set up this rock fall trap to cause a large avalanche of boulders to more effectively block the entrance to the pass should his position be threatened. While this landslide won't prevent those capable of climbing over piles of rocks, horses and wagons won't be able to pass through. Sledge (and the orcs too) are not afraid to trigger the rock fall, since it only takes the stone giant half a day to rest the trap. One orc (see Encounter #2 for stats) is always on duty here.
+
+   Though its primary function is to block the pass, on more than one occasion the fall has caught an unwary traveler. Victims caught in the slide zone of the fall (it is too small to have an effective bury zone) sustain 3d6 points of damage, or no damage on a successful Reflex saving throw (DC 15). Those who fail their save are pinned, suffering 1d6 points of subdual damage per minute. If a pinned victim falls unconscious, they must make a Constitution check (DC 15) or take 1d6 points of normal damage each minute until death.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #5: Dire Bear Den" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #5 Text" version="1.0">
+          <text multiline="1" send_button="1">Encounter #5: Dire Bear Den
+   This bear is an adopted pet, of sorts, for the stone giant Sledge. While the two do not have a loveable master-pet relationship, the giant often throws the bear meat scraps-which are sometimes humanoid or other unfortunate victims. Hence, the bear has developed a liking for humanoid flesh, and does not back down from the heroes or any other two-legged creatures. If the bear is killed, Sledge will be extremely upset.
+
+   The bear's den consists of one large circular cavern, 40 feet in diameter, with a small puddle fed by a slowly dripping spring. Bones of animal and humanoid meals line the walls, and the uneven ceiling of the cave varies from seven to 10 feet (1d6+4). Treasure accumulated from victims here (Spot check vs. DC 12) includes a pouch with 166gp and 20sp. A half-buried boney finger has a ring of protection +2 on it and is clutching a potion of remove blindess/deafness.
+
+Dire Bear: HD: 12d8+48 (102 hp); Init +1; Spd 40; AC 17 (-1 size, +1 Dex, +7 natural); Atk: 2 claws +18 melee, bite +13 melee; Dam claw 2d4+10, bite 2d8+5; Face 10ft. x 20ft./10ft; Special Atk Improved Grab; Special Qualites Scent; Saves Fort +12, Ref +9, Will +9; Str 31, Dex 13, Con 19, Int 2, Wis 12 Cha 10; AL N; Skills Listen +7, Spot +7, Swim +13
+
+Improved Grab (Ex): To use this ability, the dire lion must hit with its bite attack. If it gets a hold, it can rake.
+
+Scent (Ex): This ability allows the creature to detect approaching enemies, sniff out hidden goes, and track by sense of small.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dire Bear Treasure (Player)" version="1.0">
+          <text multiline="1" send_button="1">Treasure accumulated from victims here includes a pouch with 166gp and 20sp. A half-buried boney finger has a gold ring on it, and the other hand is clutching a potion.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dire Bear Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="1">Treasure accumulated from victims here (Spot check vs. DC 12) includes a pouch with 166gp and 20sp. A half-buried boney finger has a ring of protection +2 (AC) on it and is clutching a potion of remove blindess/deafness.</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Dire Bear" version="1.0">
+          <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Race" version="1.0">
+              <text multiline="0" send_button="0">Dire Bear</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+              <text multiline="0" send_button="0">Walk 40'</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC: Total / Touch / Flat Footed" version="1.0">
+              <text multiline="0" send_button="0">17 / 10 / 16</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face / Reach" version="1.0">
+              <text multiline="0" send_button="0">5 ft. by 5 ft./10</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="XP / For Next" version="1.0">
+              <text multiline="0" send_button="0">0 / </text>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+              <grid autosize="1" border="1">
+                <row version="1.0">
+                  <cell>STR</cell>
+                  <cell>31</cell>
+                  <cell>+10</cell>
+                </row>
+                <row version="1.0">
+                  <cell>DEX</cell>
+                  <cell>13</cell>
+                  <cell>+1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CON</cell>
+                  <cell>19</cell>
+                  <cell>+4</cell>
+                </row>
+                <row version="1.0">
+                  <cell>INT</cell>
+                  <cell>2</cell>
+                  <cell>-4</cell>
+                </row>
+                <row version="1.0">
+                  <cell>WIS</cell>
+                  <cell>12</cell>
+                  <cell>+1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CHA</cell>
+                  <cell>10</cell>
+                  <cell>+0</cell>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Special Abilities" version="1.0">
+              <text multiline="1" send_button="0">Improved Grab(Ex); Scent(Ex); </text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Combat" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Abilities" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">STR [1d20+10]</option>
+                <option selected="0" value="0">DEX [1d20+1]</option>
+                <option selected="0" value="0">CON [1d20+4]</option>
+                <option selected="0" value="0">INT [1d20-4]</option>
+                <option selected="0" value="0">WIS [1d20+1]</option>
+                <option selected="0" value="0">CHA [1d20+0]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Dire Bear Skills" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">Listen +7</option>
+                <option selected="1" value="0">Spot +7</option>
+                <option selected="0" value="0">Swim +13</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">Fortitude [+12+1d20]</option>
+                <option selected="0" value="0">Reflex [+9+1d20]</option>
+                <option selected="1" value="0">Will [+8+1d20]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP (out of 102)" version="1.0">
+              <text multiline="0" send_button="0"></text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+              <list send_button="1" type="3">
+                <option selected="1" value="0">Bite (Natural/Secondary): [1d20+13]  Damage [2d8+5]</option>
+                <option selected="1" value="0">Claw (Natural/Primary): [1d20+18/+18]  Damage [2d4+10]</option>
+              </list>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #6: Orc Lair" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6 Text" version="1.0">
+          <text multiline="1" send_button="1">Encounter #6: Orc Lair
+   When Sledge erected the tolls some time ago, he understood that he could not attend them all the time; hence, he enlisted the aid of a small tribe of orcs known as the Broken Claws. These orc warriors and their families populate this small cave complex of 4 interlocking caverns. The total number of orcs is 20 males, 11 females, and currently six children.
+
+   Any orcs not detailed in other areas can be found here. With the exception of the children, use the stats from Encounter #2 for all orcs, though arm females with clubs or short swords. If all the males are slain, the female orcs beg for their lives and those of their children. They will plead to be let go, telling the heroes they will leave the area forever. This is in fact a lie, as one or two of them attempt to inform Sledge of recent happenings.
+
+   This is a typical orc lair, filled with filth and refuse, and having an extremely unpleasant smell to it. A small fire burning in the entrance cavern fills the complex with black smoke and soot, causing a -1 to all rolls involving vision beyond a five foot distance. Searching the caves for treasure is difficult and time-consuming, requiring a Search check (DC 15) for each of the four caverns. Treasure distributed among the caves: 335gp, 501sp, uncut diamond worth 250gp, and a potion of charisma.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6 Orc Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="1">Searching the caves for treasure is difficult and time-consuming, requiring a Search check (DC 14) for each of the four caverns. Treasure distributed among the caves: 335gp, 501sp, uncut diamond worth 250gp, and a potion of charisma.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6 Orc Treasure (Players)" version="1.0">
+          <text multiline="1" send_button="1">treasure distributed among the caves: 335gp, 501sp, uncut diamond, and a potion.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Read to Players: Approaching Orc Lair" version="1.0">
+          <text multiline="1" send_button="1">The area up ahead seems to be the entrance to the orc's lair from this area. This is a typical orc lair, filled with filth and refuse, and having an extremely unpleasant smell to it. A small fire burning in the entrance cavern fills the complex with black smoke and soot. Several guards are out front.</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Orcs (31 total)" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Orc NPC" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Orc NPC
+Class:      Fighter 1
+Level:      1
+Race:       Orc
+Alignment:      Chaotic Evil
+Gender:     M
+Size:       M
+Experience Points (XP):     0
+(XP) Needed for Next Level:     1000
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages: Orc
+*******************************************************************************
+Score:  Mod:    Abilities:
+15  +2  :Strength (STR)
+10  +0  :Dexterity (DEX)
+11  +0  :Constitution (CON)
+9   -1  :Intelligence(INT)
+8   -1  :Wisdom(WIS)
+8   -1  :Charisma(CHA)
+*******************************************************************************
+14  :(AC) Armor Class
+10  :(AC) vs. Touch
+14  :(AC) Flat footed
+10  :(HP) Maximum Hit Points
+*******************************************************************************
++0  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++1
+Ranged Bonuses:
++1
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+*Greataxe
++3  1d12+3  20/x3   0'
+Crossbow (Light)
++1  1d8 19-20/x2    80'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*Scale Mail
++4  +3  -4  25
+*******************************************************************************
+Saving Throws:
++2  :FORTITUDE (CON)
++0  :REFLEX (DEX)
+-1  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+2   Jump
+3   Listen
+3   Spot
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+66.0    133.0   200.0
+57.0 lbs    :Total Encumbrance
+Light   :Carry Load
+200.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Scale Mail
+1   Greataxe
+1   Crossbow (Light)
+1   Bolts (Crossbow/10)
+*******************************************************************************
+Funds:
+Gold Pieces: 190
+Misc Finds:
+Misc Magic:
+Total Valued: 106.0 gp
+*******************************************************************************
+Feats:
+Alertness,Armor Proficiency (Heavy) (1x),Armor Proficiency (Light) (1x),Armor Proficiency (Medium) (1x),Martial Weapon Proficiency,Point Blank Shot,Shield Proficiency,Simple Weapon Proficiency
+*******************************************************************************
+Class Abilities:
+Light Sensitivity(Ex)
+*******************************************************************************
+Racial Traits:
+
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="ORC INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">14</text>
+              </nodehandler>
+              <nodehandler class="form_handler" icon="form" module="forms" name="ORC HIT POINTS 1-10" version="1.0">
+                <form height="400" width="400"/>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 1 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 2 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 3 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 4 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 5 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 6 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 7 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 8 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 9 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 10 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+              </nodehandler>
+              <nodehandler class="form_handler" icon="form" module="forms" name="ORC HIT POINTS 11-20" version="1.0">
+                <form height="400" width="400"/>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 11 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 12 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 13 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 14 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 15 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 16 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 17 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 18 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 19 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 20 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+              </nodehandler>
+              <nodehandler class="form_handler" icon="form" module="forms" name="ORC HIT POINTS 21-31" version="1.0">
+                <form height="400" width="400"/>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 21 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 22 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 23 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 24 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 25 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 26 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 27 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 28 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 29 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 30 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 31 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">*Greataxe (Crit.20x3) - [1d20+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d12+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[3*1d12+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Crossbow (Light) (Crit.19-20x2) - [1d20+1]
+</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d8]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+2]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20-1]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Jump [1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Listen [1d20+3]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Spot [1d20+3]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC TREASURE" version="1.0">
+            <text multiline="0" send_button="1">[3d10] gold pieces each</text>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="link_handler" icon="compass" module="forms" name="The Stone Gaint Troll Layer Map" version="1.0">
+        <link href="http://www.openrpg.com/orpgnuke/modules/My_eGallery/gallery/maps/Woodland/tq2a.jpg"/>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="group_handler" module="containers" name="Scenario #2b: Empty Dragon's Lair" version="1.0">
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #1: Cave Pool" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #1: Cave Pool
+   The entrance to this long abandoned dragon's lair is dominated by a significant pool of water. This pool is likely fed by the same subterranean source as the underground river in Encounter #6, and is quite drinkable and refreshing. A light source reveals no visible bottom to the pool, though brief movement can be seen it disappears quickly once exposed.
+
+   The pool is 60 feet in depth at its deepest point, which slopes downward to the north after 30 feet in depth. The most dangerous part about this pool, however, it a crosscurrent sweep running north to south-eventually leading to the underground river. Any hero attempted to explore the pool gets caught up in the cross flow after a depth of 25 feet, and is in danger of being swept away to the river. Escaping (or passing through) the current requires a Swim or Strength skill check (DC 15) to avoid being swept away. The DM should also take note of The Drowning Rule on page #85 of the Dungeon Master's Guide for any hero going underwater.
+
+   The movement underneath the surface is an illusion caused by the crosscurrent, as there is nothing &quot;alive&quot; in the pool. At the bottom, however, are the remains of a knight who long ago came to this cave to fight the resident dragon. The full armor of the knight is completely rusted and useless, but in the body's hand is a weapon that appears unaffected by the water. The greataxe of mighty cleaving +4 is heavy, imposing a -1 to all rolls in reaching the surface. Remember that heroes must pass though the current a second time to escape the pool.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: Player's Text" version="1.0">
+          <text multiline="1" send_button="1">The entrance to this dragon's lair is dominated by a significant pool of water. A light source reveals no visible bottom to the pool, though brief movement can be seen-but disappears quickly once exposed.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="1">At the bottom, however, are the remains of a knight who long ago came to this cave to fight the resident dragon. The full armor of the knight is completely rusted and useless, but in the body's hand is a weapon that appears unaffected by the water.
+
+Greataxe of mighty cleaving +4 (heavy, imposing a -1 to all rolls)
+
+
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: Treasure (Players)" version="1.0">
+          <text multiline="1" send_button="1">Greataxe -- very heavy
+Rusted full plate of armor</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #2: Bottomless Pit" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: DM Information" version="1.0">
+          <text multiline="1" send_button="1">Encounter #2: Bottomless Pit
+   While not truly bottomless, this roughly-hewn pit ranges from 20-25 feet in diameter and is a little more than one mile deep. When the dragon occupied the cave he often threw various refuse and other undesirable bits and pieces into the pit. Any heroes who happen to fall down this pit have a slim chance of survival. Unless a falling victim is lucky enough to grasp an outcropping of rock (Dexterity roll vs. DC 15), the one mile fall means instant death.
+
+   The pit does have several residents, a large flock of stirges who burst from the pit once any of the heroes approach the edge. The stirges erupt from the pit to attack with such surprise they gain a +1 to their initial attack.
+
+Stirges (50): Tiny Beast; Hit Dice: 1d10 (5 hp); Initiative: +4 (Dex); Speed: 10 ft., fly 40 ft.; AC: 16 (+2 size, +4 Dex); Attacks: Touch +6; Damage: Touch 1d3-4; Face/Reach: 2 1/2 ft. by 2 1/2 ft./0 ft.; Special Attack: Attach, blood drain; Saves: Fort +2, Ref +6, Will +1; Abilities: Str 3, Dex 19, Con 10, Int 1, Wis 12, Cha 6; AL N; Skills: Hide +14; Feats: Weapon Finesse (touch)
+
+   Stirges attacks by landing on a victim, finding a vulnerable spot, and plunging its proboscis into the flesh. This is a touch attack and can target only Small or larger creatures.
+
+   Attach (Ex): If a stirge hits with a touch attack, it uses its eight pincers to latch onto the opponent's body. An attached stirge has an AC of 12.
+
+   Blood Drain (Ex): A stirge drains blood, dealing 1d4 points of temporary Constitution damage each round it remains attached. Once it has drained 4 points of Constitution, it detaches and flies off to digest the meal.
+</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="skull" module="containers" name="Stirges" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Stirge" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Stirge
+Class:      Beast 1
+Level:      1
+Race:       Stirge
+Alignment:      Neutral
+Gender:     M
+Size:       T
+Experience Points (XP):     0
+(XP) Needed for Next Level:     0
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages:
+*******************************************************************************
+Score:  Mod:    Abilities:
+6   -2  :Strength (STR)
+25  +7  :Dexterity (DEX)
+15  +2  :Constitution (CON)
+4   -3  :Intelligence(INT)
+14  +2  :Wisdom(WIS)
+9   -1  :Charisma(CHA)
+*******************************************************************************
+19  :(AC) Armor Class
+19  :(AC) vs. Touch
+12  :(AC) Flat footed
+12  :(HP) Maximum Hit Points
+*******************************************************************************
++7  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++0
+Ranged Bonuses:
++0
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+Touch (Natural/Primary)
++11 1d3-2   20/x2   0'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*******************************************************************************
+Saving Throws:
++4  :FORTITUDE (CON)
++9  :REFLEX (DEX)
++2  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+18  Hide
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+10.0    20.0    30.0
+0.0 lbs :Total Encumbrance
+Light   :Carry Load
+30.0    :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Touch (Natural/Primary)
+*******************************************************************************
+Funds:
+Gold Pieces: 0.00
+Misc Finds:
+Misc Magic:
+Total Valued: 0.0 gp
+*******************************************************************************
+Feats:
+Weapon Finesse (Touch)
+*******************************************************************************
+Class Abilities:
+Attach(Ex),Blood Drain(Ex)
+*******************************************************************************
+Racial Traits:
+
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+7]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">19</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT HIT POINTS (HP)" version="1.0">
+                <text multiline="0" send_button="1">12 each</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Touch (Natural/Primary) (Crit.20x2) - [1d20+11]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d3-2]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d3-2]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+4]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+9]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+2]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Hide [1d20+18]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+7]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-3]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #3: Falling Ceiling" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #3: Skeleton &amp; Falling Ceiling
+   This area was once the primary sleeping area for the dragon, and was also where the wyrm was eventually slain. In the battle that finally killed the dragon the force of the conflict weakened the walls and ceiling of the area, and they're now on the verge of collapse. If more than 20 points of damage are done to either the walls or ceiling here, the entire cavern will give way. Anyone in the cavern sustains 8d6 points of damage, half that amount if they make a successful Reflex saving throw (DC 15). Those that fail are pinned and unable to free themselves.
+
+   As the heroes enter this area, they come face-to-face with an undead skeleton warrior dressed in a purple tunic and cloak. The undead warrior confronts the heroes by commanding them to leave &quot;his&quot; lair-or die. During any conflict the warrior taunts the heroes, even if he is losing the battle.
+
+Purple Skeleton Warrior: Medium-Size Undead; Hit Dice: 12d12 (48 hp); Initiative: +1 (Dex); Speed: 20 ft.; AC: 25 (+1 Dex, +4 natural, +10 armor); Attacks: bastard sword +2 +20/+15/+10 melee; Damage: bastard sword +2 1d10+9; Face/Reach: 5 ft. by 5 ft./5 ft.; Special Attacks: Fear aura, find target; Special Qualities: Undead, damage reduction 10/+1, SR 28, turning, immunity, darkvision 60 ft.; Saves: Fort +11, Ref +6, Will +7; Abilities: Str 21, Dex 13, Con -, Int 12, Wis 13, Cha 14; Skills: Climb +9, Intimidate +9, Jump +11, Listen +11, Ride +8, Search +5, Sense Motive +7, Spot +14, Swim +10; Feats: Alertness, Armor Proficiency (all), Cleave, Great Cleave, Martial Weapon; Proficiency (all), Power Attack, Shield Proficiency, Simple Weapon Proficiency; (all), Sunder, Track, Weapon Focus (bastard sword), Weapon Specialization (bastard sword);
+
+Magic: full plate armor +2, bastard sword +2, purple cloak of resistance +2 (Fort), boots of speed.
+
+   Undead: Immune to mind-influencing effects, poison, sleep, paralysis, stunning, and disease. Not subject to critical hits, subdual damage, ability damage, energy drain, or death from massive damage.
+</text>
+        </nodehandler>
+        <nodehandler class="form_handler" icon="skull" module="forms" name="Skeletal Warrior" version="1.0">
+          <form height="400" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC" version="1.0">
+            <text multiline="0" send_button="1">25</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current Hit Points" version="1.0">
+            <text multiline="0" send_button="1">48</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Initiative" version="1.0">
+            <text multiline="0" send_button="1">[1d20+1]</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack/Damage" version="1.0">
+            <text multiline="0" send_button="1">Bastard Sword [1d20+10] | Damage [1d10+9]</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Crit" version="1.0">
+            <text multiline="0" send_button="1">Bastard Sword Crit Damage: [1d10*2+9]</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Special A/Q" version="1.0">
+            <text multiline="1" send_button="1">Fear Aura
+Find target
+Undead
+Damage reduction 10/+1
+SR 28
+turning immunity +3
+darkvision 60'
+
+</text>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+            <list send_button="1" type="0">
+              <option selected="1" value="">Save - Fort [1d20+11]</option>
+              <option selected="0" value="">Save - Reflex [1d20+6]</option>
+              <option selected="0" value="">Save - Will [1d20+7]</option>
+            </list>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="Skills" version="1.0">
+            <list send_button="1" type="0">
+              <option selected="0" value="0">Climb [1d20+9]</option>
+              <option selected="0" value="0">Intimidate [1d20+9]</option>
+              <option selected="0" value="0">Jump [1d20+11]</option>
+              <option selected="0" value="0">Listen [1d20+11]</option>
+              <option selected="0" value="0">Ride [1d20+8]</option>
+              <option selected="0" value="0">Seach [1d20+5]</option>
+              <option selected="1" value="0">Sense Motive [1d20+7]</option>
+              <option selected="0" value="0">Spot [1d20+14]</option>
+              <option selected="0" value="0">Swim [1d20+10]</option>
+            </list>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+            <text multiline="1" send_button="0">Alertness, Cleave, Great Cleave, Power Attack, Track, Weapon Focus (bastard sword)</text>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: Magic (DM)" version="1.0">
+          <text multiline="1" send_button="1">Magic:
+full plate armor +2
+bastard sword +2
+purple cloak of resistance +4 (Fort)
+boots of speed.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: Magic (Player)" version="1.0">
+          <text multiline="1" send_button="1">Full plate armor
+Bastard sword
+Purple cloak
+Boots</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: Player's Text" version="1.0">
+          <text multiline="1" send_button="1">As you enter this area, they come face-to-face with an undead skeleton warrior dressed in a purple tunic and cloak. The undead warrior confronts you and commands &quot;Leave here or die!&quot;</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #4: Roper" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: DM Information" version="1.0">
+          <text multiline="1" send_button="1">Encounter #4: Ropers
+   The ceiling in his corridor disappears off into the darkness like a reverse crevasse, to a height of 70 feet. Years after the dragon here was slain a roper took up residence. With a steady flow of adventurers seeking the dragon's hidden horde, the roper has been well fed over the years. If the party is too weak to fight a CR 10 creature, limit the number of strands the roper has.
+
+Roper: Large Magical Beast; Hit Dice: 10d10+30 (85 hp); Initiative: +5 (+1 Dex, +4 Improved Initiative); Speed: 10 ft.; AC: 24 (-1 size, +1 Dex, +14 natural); Attacks: 6 strands +11 ranged, bite +8 melee; Damage: Strand (see text), bite 2d6+2; Face/Reach: 5 ft. by 5 ft./10 ft. (50 ft. with strand); Special Attacks: Strands, attach, weakness; Special Qualities: Electricity immunity, cold resistance 30, fire vulnerability, SR 28; Saves: Fort +10, Ref +8, Will +8; Abilities: Str 19, Dex 13, Con 17, Int 12, Wis 16, Cha_12; Skills: Climb +7, Hide +10*, Listen +13, Spot +13; Feats: Alertness, Improved Initiative, Iron Will, Weapon Focus (strand); AL: CE; CR 10
+
+*Ropers receive a +8 racial bonus to Hide checks in stony or icy areas.
+
+   A roper hunts by standing very still and imitating a bit of rock. This usually allows it to attack with surprise. When it notices prey, it lashes out with its strands and bites adjacent opponents with its powerful maw.
+
+   The roper has accumulated a sizable horde of treasure over the years from would-be treasure seekers. Found high up in the crevasse (Climb DC 15 to reach) on a ledge among bones and refuse is: finely cut emerald worth 500gp, 1,600gp gold brick, chain mail +1, large steel shield w/spell resistance (SR13), scroll: feeblemind, and potion of love.</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="skull" module="containers" name="Roper" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Roper" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Roper
+Class:      Magical Beast 10
+Level:      10
+Race:       Roper
+Alignment:      Chaotic Evil
+Gender:     M
+Size:       L
+Experience Points (XP):     0
+(XP) Needed for Next Level:     0
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages:
+*******************************************************************************
+Score:  Mod:    Abilities:
+19  +4  :Strength (STR)
+13  +1  :Dexterity (DEX)
+17  +3  :Constitution (CON)
+12  +1  :Intelligence(INT)
+17  +3  :Wisdom(WIS)
+12  +1  :Charisma(CHA)
+*******************************************************************************
+24  :(AC) Armor Class
+10  :(AC) vs. Touch
+23  :(AC) Flat footed
+85  :(HP) Maximum Hit Points
+*******************************************************************************
++5  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++10/+5
+Ranged Bonuses:
++10/+5
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+Bite (Natural/Secondary)
++8  2d6+2   20/x2   0'
+Strand (Natural/Primary)
++13/+13/+13/+13/+13/+13 0   20/x2   0'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*******************************************************************************
+Saving Throws:
++10 :FORTITUDE (CON)
++8  :REFLEX (DEX)
++6  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+10  Hide
+14  Listen
+14  Spot
+7   Climb
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+233.0   466.0   700.0
+0.0 lbs :Total Encumbrance
+Light   :Carry Load
+700.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Bite (Natural/Secondary)
+1   Strand (Natural/Primary)
+*******************************************************************************
+Funds:
+Gold Pieces: 0.00
+Misc Finds:
+Misc Magic:
+Total Valued: 0.0 gp
+*******************************************************************************
+Feats:
+Alertness,Improved Initiative
+*******************************************************************************
+Class Abilities:
+Strands(Ex),Attach(Ex),Weakness(Ex),Fire Vulnerability(Ex),+8 to Hide in Stony or Icy areas
+*******************************************************************************
+Racial Traits:
+
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+5]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">24</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT HIT POINTS (HP)" version="1.0">
+                <text multiline="0" send_button="1">85</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Bite (Natural/Secondary) (Crit.20x2) - [1d20+8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2d6+2]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*2d6+2]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Strand (Natural/Primary) (Crit.20x2) - [1d20+13]
+</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*0]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+10]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+6]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Hide [1d20+10]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Listen [1d20+14]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Spot [1d20+14]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Climb [1d20+7]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+4]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+3]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+3]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+            <text multiline="1" send_button="0">Alertness
+Improved Initiative
+Iron Will
+Weapon Focus (strand)</text>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="1">The roper has accumulated a sizable horde of treasure over the years from would-be treasure seekers. Found high up in the crevasse (Climb DC 15 to reach) on a ledge among bones and refuse is: finely cut emerald worth 500gp, 1,600gp gold brick, chain mail +1, large steel shield w/spell resistance (SR13), scroll: feeblemind, and potion of love.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: Treasure (Player)" version="1.0">
+          <text multiline="1" send_button="1">Finely cut emerald
+Gold brick
+chain mail
+large steel shield
+scroll: feeblemind
+potion</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #5: Hidden Hoard" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #5: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #5: Hidden Dragon Horde
+   This cavern is where the dragon kept his horde, which mysteriously has never been found. Part of the reason for this is the presence of the roper, who feeds on those trying to reach the cavern, but the other reason is the horde is hidden by magic. Like most dragons, the former owner of this lair was very greedy about his horde and used powerful magic to hide from others. It has remained here since the dragon's death.
+
+   The cavern is roughly 90 feet in diameter, with a 40 foot high ceiling-the later of which is the key. The ceiling height is actually 70 feet, with the last 30 feet being a powerful illusion laced with magical traps. The actual horde rests on a ledge to the west above the illusion, which is trapped with a symbol of death and a symbol of insanity.
+
+This area is riddled with symbols of stunning. Anyone stepping or moving on the ledge has an 8 in 10 chance of triggering one. If the players use detect magic they'll be able to see the symbols and easily avoid them.
+
+   The dragon's horde consists of the following: large steel shield +2; 3,966gp, potion of charisma, potion of vision, scrolls: ever scent, steal sleep; Summon Monster II (l2, cl3); light crossbow of distance +1, and an amulet of natural armor +2.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #5: Treasuire (DM)" version="1.0">
+          <text multiline="1" send_button="1">large steel shield +2
+3,966gp
+potion of charisma
+potion of vision
+scrolls: ever scent, steal sleep; Summon Monster II (l2, cl3);
+light crossbow of distance +1,
+amulet of natural armor +2.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #5: Treasuire (Player)" version="1.0">
+          <text multiline="1" send_button="1">large steel shield
+3,966gp
+potion
+potion
+scroll: ever scent, steal sleep; Summon Monster II (l2, cl3);
+light crossbow
+amulet</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #6: Underground River" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #6: Underground River
+   A long subterranean passage here leads to an underground river. The slope is steep (45 degree decline) and extremely slippery from moss and moisture, making for treacherous footing. Anyone making their way down the corridor must make a Dexterity check (DC 15) or begin sliding towards the river.
+
+   Heroes failing the check are allowed one final attempt to stop before reaching the bottom. With a successful Strength check (DC 15) a victim can catch themselves half way down the slope. However, if multiple heroes are sliding down the slope an additional Strength check is needed for each person slamming into someone else. Those failing their rolls plunge into the river and are swept miles away where the river exits the mountain range.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="link_handler" icon="compass" module="forms" name="Dragon's Lair Map" version="1.0">
+        <link href="http://www.openrpg.com/orpgnuke/modules/My_eGallery/gallery/maps/Dungeon/tq2b.jpg"/>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="group_handler" module="containers" name="Scenario #2c: Brown's Farm" version="1.0">
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #1: Cattle Barn" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #1: Cattle Barn
+   The cattle barn is a large structure, second only to the main cabin on the farm. Built from sturdy oaks that were cleared when the small farm was cultivated, the barn was built by Jebidiah Brown and his cousin Siegfried who lives at Encounter #5. There are 20 head of cattle found here or in the nearby pasture, along with five goats and several dozen chickens-all of which never stray far from the safety of the barn.
+
+   If the heroes befriend the Brown family, they are allowed to spend the night in the barn's rafters if they don't mind the accommodations. This is all Jebidiah has to offer weary travelers, and can be rather cozy if the weather outside is cold or wet. Ample amounts of hay here keep the heroes warm, but are warned by Jebidiah not to use fire of any sort here since the dry environment would easily ignite his cattle's hay.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #2: Main Cabin" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #2: Main Log Cabin
+
+   This cabin is the home of Jebidiah Brown, his wife Ginna, and their two little girls May and Brinna. The Brown family is kind and generous, and will offer the shelter of their barn to anyone traveling through the area. They have little of worth, living well off of their livestock and the small crops they produce.
+
+   An adventurer in his youth, Jebidiah (Half-Elf, Ftr6) was once a companion of the famous bard Thayer, and had amassed a fine fortune on which to start a family. Jebidiah was with Thayer when the rogue killed the dragon that was terrorizing the area, though he was knocked unconscious during the battle in which Thayer delivered the wyrm a killing blow. Though they found little treasure at the lair, the people of the area all contributed to give Thayer and his companions a reward. Jebidiah took his portion and built this farmstead.
+
+   Anyone accepting the hospitality of the Brown family learns about all the good deeds Thayer did around this area. Once the area was infested with orcs, goblins, and evil cultists and their undead, and it was Thayer and various other companions who help clear them out. The bard was most recognizable by his gleaming gold chain mail armor and emerald-encrusted magical gauntlets. All of his items also had the letter &quot;T&quot; on them somewhere. Jebidiah also tells the heroes Thayer had a magical harp and dagger, both of which supposedly spoke to him-though only Thayer could hear them.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #3: Horse Pasture" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #3: Horse Pasture
+
+   The Brown family has two horses, which they use to plow their fields and ride to nearby villages. They also have a wagon, but the left wheel is broken. If the heroes have horses with them, Jebidiah offers to shelter them in this penned pasture while they rest.
+
+   If the heroes take the time to search the area east of the pastures, they may discover something of note. A successful Spot check (DC 12) allows a hero in the area to find a set of strange animal footprints. These prints were made by the dire wolverine that has taken up residence south of the trout pond. While searching for food, the large wolverine was recently checking out the Brown's livestock.
+
+   If the heroes stake out the pasture that evening, they see the wolverine return that night to kill a horse (possibly a heroes' horse if they have any). If they do not notice the prints, the next day one of the horses would have been slain and dragged away. Stats for the wolverine can be found in Encounter #4, which is where the tracks lead the heroes.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #4: Trout Pond" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #4: Trout Pond
+
+   This small, natural pond is fed from an underground river that flows through the area. Some years ago Jebidiah transplanted some trout in the pond, which make their way there every year since to spawn. During a conversation with Jebidiah about the area, he mentions the disturbing lack of fish this season.
+
+   The reason for the lack of fish is the appearance of a dire wolverine in the area. The creature has been eating up much of the smaller wildlife over the past month, including the resident trout. If not encountered in the horse pasture, the heroes find the wolverine here looking for food. In addition, if the wolverine killed a horse the night before, the half-eaten carcass is here.
+
+Dire Wolverine: HD: 5d8+20 (42 hp); Init +3 (Dex); Spd 30, climb 10ft.; AC 16 (-1 size, +3 Dex, +4 natural); Atk: 2 claws +8 melee, bite +3 melee; Dam claw 1d6+6, bite 1d8+3; Face 5ft. x 10ft./5ft; Special Atk Rage; Special Qualites Scent; Saves Fort +8, Ref +7, Will +5; Str 22, Dex 17, Con 19, Int 2, Wis 12 Cha 10; AL N; Skills Climb +10, Listen +9, Spot +8
+
+   Rage (Ex): A dire wolverine that takes damage in combat flies into a berserk rage on its next turn, clawing and biting madly until either it or its opponent is dead. An enraged dire wolverine gains +4 Strength, +4 Constitution, and -2 AC. The creature cannot end its rage voluntarily.
+</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Dire Wolverine" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dire Wolverine" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Dire Wolverine
+Class:      Animal 5
+Level:      5
+Race:       Dire Wolverine
+Alignment:      Neutral
+Gender:     M
+Size:       L
+Experience Points (XP):     0
+(XP) Needed for Next Level:     0
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages:
+*******************************************************************************
+Score:  Mod:    Abilities:
+22  +6  :Strength (STR)
+17  +3  :Dexterity (DEX)
+19  +4  :Constitution (CON)
+4   -3  :Intelligence(INT)
+12  +1  :Wisdom(WIS)
+10  +0  :Charisma(CHA)
+*******************************************************************************
+16  :(AC) Armor Class
+12  :(AC) vs. Touch
+13  :(AC) Flat footed
+52  :(HP) Maximum Hit Points
+*******************************************************************************
++3  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++3
+Ranged Bonuses:
++3
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+Bite (Natural/Secondary)
+??? 1d8 20/x2   0'
+Claw (Natural/Primary)
++8/+8   1d6+6   20/x2   0'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*******************************************************************************
+Saving Throws:
++8  :FORTITUDE (CON)
++7  :REFLEX (DEX)
++2  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+346.0   693.0   1040.0
+0.0 lbs :Total Encumbrance
+Light   :Carry Load
+1040.0  :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Bite (Natural/Secondary)
+1   Claw (Natural/Primary)
+*******************************************************************************
+Funds:
+Gold Pieces: 0.00
+Misc Finds:
+Misc Magic:
+Total Valued: 0.0 gp
+*******************************************************************************
+Feats:
+
+*******************************************************************************
+Class Abilities:
+Rage(Ex),Scent(Ex)
+*******************************************************************************
+Racial Traits:
+Scent(Ex), Rage(Ex)
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">14</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT HIT POINTS (HP)" version="1.0">
+                <text multiline="0" send_button="1">75</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="0" send_button="1">Bite: [1d20+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d8+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="0" send_button="1">Claw (Natural/Primary) [1d20+8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d6+10]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d6+6]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+7]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+2]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill: Climb +10" version="1.0">
+              <text multiline="0" send_button="1"> [1d20+10]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill: Listen +9" version="1.0">
+              <text multiline="0" send_button="1"> [1d20+9]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill: Spot +8" version="1.0">
+              <text multiline="0" send_button="1"> [1d20+8]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+6]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+3]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+4]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-3]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #5: Cousin's Cabin" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #5: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #5: Cousin's Log Cabin
+   Jebidiah Brown's half-elf cousin Siegfried lives here in a one-room log cabin. Considerably older than his cousin, Siegfried was also an adventurer in his youth; however, he lost his right leg during a battle with a hill giant several years ago and retired here. Siegfried is good with animals, and Jebidiah allows him to work his keep by tending livestock.
+
+   If the heroes stop at the cabin, they immediately sense that something is wrong. Late the night before Siegfried was attacked by a wandering wight who had happened upon the farmstead. The creature entered Siegfried's cabin and drained his life as the sun rose that morning. Disliking the daylight hours, Siegfried and his wight master are waiting for nightfall to leave when the heroes stop to visit. If the heroes do not intervene, Siegfried pays his brother a visit the next night. There is nothing else of value here.
+
+Wights (2); Medium-Size Undead; Hit Dice: 4d12 (29, 18 hp); Initiative: +1 (Dex); Speed: 30ft. (15 for Siegfried); AC: 15 (+1 Dex, +4 natural); Attacks: Slam +3 melee; Damage: Slam 1d4+1 and energy drain; Face/Reach: 5 ft. by 5 ft./5 ft.; Special Attacks: Energy drain, create spawn; Special Qualities: Undead; Saves: Fort +1, Ref +2, Will_+5; Abilities: Str 12, Dex 12, Con_-, Int 11, Wis 13, Cha 15; Skills: Climb +5, Hide +8, Listen +8, Move Silently +16*, Search +7, Spot +8; Feats: Blind-Fight
+* Skills: Wights receive a +8 racial bonus to Move Silently checks.
+
+   Energy Drain (Su): Living creatures hit by a wight's slam attack receive one negative level. The Fortitude save to remove the negative level has a DC of 14.
+
+   Create Spawn (Su): Any humanoid slain by a wight becomes a wight in 1d4 rounds. Spawn are under the command of the wight that created them and remain enslaved until its death. They do not possess any of the abilities they had in life.
+
+   Undead: Immune to mind-influencing effects, poison, sleep, paralysis, stunning, and disease. Not subject to critical hits, subdual damage, ability damage, energy drain, or death from massive damage.
+</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="skull" module="containers" name="Wights (2)" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wights" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Wights
+Class:      Undead 4
+Level:      4
+Race:       Wight
+Alignment:      Lawful Evil
+Gender:     M
+Size:       M
+Experience Points (XP):     0
+(XP) Needed for Next Level:     0
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages:
+*******************************************************************************
+Score:  Mod:    Abilities:
+12  +1  :Strength (STR)
+12  +1  :Dexterity (DEX)
+--  0   :Constitution (CON)
+11  +0  :Intelligence(INT)
+13  +1  :Wisdom(WIS)
+15  +2  :Charisma(CHA)
+*******************************************************************************
+15  :(AC) Armor Class
+11  :(AC) vs. Touch
+14  :(AC) Flat footed
+32  :(HP) Maximum Hit Points
+*******************************************************************************
++1  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++2
+Ranged Bonuses:
++2
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+Slam (Natural/Primary)
++3  1d4+1   20/x2   0'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*******************************************************************************
+Saving Throws:
++1  :FORTITUDE (CON)
++2  :REFLEX (DEX)
++5  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+5   Climb
+5   Hide
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+43.0    86.0    130.0
+0.0 lbs :Total Encumbrance
+Light   :Carry Load
+130.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Slam (Natural/Primary)
+*******************************************************************************
+Funds:
+Gold Pieces: 110.00
+Misc Finds:
+Misc Magic:
+Total Valued: 0.0 gp
+*******************************************************************************
+Feats:
+Blind-Fight
+*******************************************************************************
+Class Abilities:
+ ability damage, and disease. Not subject to critical hits, energy drain, or death from massive damage., paralysis, poison, sleep, stunning, subdual damage,Create Spawn(Su),Energy Drain(Su),Immune to mind-influencing effects
+*******************************************************************************
+Racial Traits:
+Energy Drain(Su), Create Spawn(Su)
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">15</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wight #1 Hit Points" version="1.0">
+                <text multiline="0" send_button="1">32</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wight #2 Hit Points" version="1.0">
+                <text multiline="0" send_button="1">32</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Slam (Natural/Primary) (Crit.20x2) - [1d20+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d4+1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d4+1]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+2]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+5]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Feats" version="1.0">
+              <form height="400" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Blind Fighting " version="1.0">
+                <text multiline="1" send_button="0">Blind-Fight [General]
+
+Benefit: In melee, every time a character misses because of concealment, the character can reroll the miss chance percentile roll one time to see if the character actually hit.
+
+An invisible attacker gets no bonus to hit the character in melee. That is, the character doesnt lose a Dexterity bonus to Armor Class, and the attacker doesnt get the usual +2 bonus. The invisible attackers bonuses do still apply for ranged attacks, however.
+
+The character suffers only half the usual penalty to speed for being unable to see. Darkness and poor visibility in general reduces the character's speed to three-quarters of normal, instead of one-half.
+</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Special" version="1.0">
+              <form height="400" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Undead" version="1.0">
+                <text multiline="1" send_button="1">   Undead: Immune to mind-influencing effects, poison, sleep, paralysis, stunning, and disease. Not subject to critical hits, subdual damage, ability damage, energy drain, or death from massive damage.</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Create Spawn" version="1.0">
+                <text multiline="1" send_button="1">   Create Spawn (Su): Any humanoid slain by a wight becomes a wight in 1d4 rounds. Spawn are under the command of the wight that created them and remain enslaved until its death. They do not possess any of the abilities they had in life.</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Energy Drain" version="1.0">
+                <text multiline="1" send_button="1">   Energy Drain (Su): Living creatures hit by a wight's slam attack receive one negative level. The Fortitude save to remove the negative level has a DC of 14.</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Climb [1d20+5]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Hide [1d20+8]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Listen [1d20+8]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Move Silently [1d20+16]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Search [1d20+7]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Spot [1d20+8]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #6: Ruined Tower" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: DM Information" version="1.0">
+          <text multiline="1" send_button="1">Encounter #6: Ruined Ancient Tower
+   Jebidiah Brown knows little about this tower, other than it was here long before anyone in the area can remember. It was rumored to once by the home of a powerful wizard, but no one knows for sure. Jebidiah has searched the structure many times, always finding nothing. He had even shown it to Thayer, a powerful bard,  once and the bard said he found nothing as well.
+
+   In truth, Thayer returned to the tower after visiting Jebidiah years ago to hide his magical dagger and harp. The items can only be found using a detect magic, instantly revealing their location. This is the only way to find the items.
+
+   Thayer's dagger is called Blind Sight, which has an emerald encrusted hilt and wicked barb at its tip. The weapon has a +3 enchantment, and when held grants darkvision, bestows the feat Blind-Fight, and senses the approach of anyone in a 50 foot radius (Semiempathy, Int 9, Wis 10, Cha 11, AL: Chaotic Good).
+
+   The bard's magical harp is called Entrancer, and is a small harp of masterwork quality. The harp is gold with jade interwoven into it, and its strings are of the purest dwarven mithral. Three times per day the music of the harp (Perform check vs. DC 12) can cast charm monster, which aided Thayer in his defeat of many beasts in the area. The harp talks to the wielder constantly (Telepathy, Wis 12, Int 10, Cha 12, AL: Chaotic Neutral), demanding to entrance more and more monsters. Eventually Thayer tired of the harp's prattling and dumped it at this ruined tower.
+</text>
+        </nodehandler>
+        <nodehandler class="form_handler" icon="form" module="forms" name="Treasure" version="1.0">
+          <form height="400" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Treasure (Players)" version="1.0">
+            <text multiline="1" send_button="1">Magical Dagger
+Magical Harp</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #2: Sleeping Quarters" version="1.0">
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: DM Information" version="1.0">
+              <text multiline="1" send_button="0">Encounter #2: Sleeping Quarters
+   These six chambers once served as sleeping quarters for the dwarves stationed here. The door to each room is still intact, but hangs rotted on rusty hinges. Each room at one time contained two dwarf-sized cots, and a small table with an iron bowl for wash water. Currently many of the beds are either overturned or broken, the rotted wood littering the floor in hundreds of pieces.
+
+   These rooms now serve as daytime nests for the wights that dwell here. The number of wights in these chambers has grown in the past year, as more and more would-be heroes come to investigate the mountain ruins. The slightest sound from any of the other rooms here rousts them from their undead rest (Listen +8 vs. DC 12), causing them to swarm to the source of the noise. There are seven human, three dwarven, and five orc wights total for this entire catacomb.
+
+Wights (15); Medium-Size Undead; Hit Dice: 4d12 (20hp each); Initiative: +1 (Dex); Speed: 30ft.; AC: 15 (+1 Dex, +4 natural); Attacks: Slam +3 melee; Damage: Slam 1d4+1 and energy drain; Face/Reach: 5 ft. by 5 ft./5 ft.; Special Attacks: Energy drain, create spawn; Special Qualities: Undead; Saves: Fort +1, Ref +2, Will_+5; Abilities: Str 12, Dex 12, Con -, Int 11, Wis 13, Cha 15; Skills: Climb +5, Hide +8, Listen +8, Move Silently +16*, Search +7, Spot +8; Feats: Blind-Fight
+* Skills: Wights receive a +8 racial bonus to Move Silently checks; AL: CE; CR: 3
+
+   Many of the wights still carry items of treasure upon them, but have either forgotten about them or care not in their undead state. Distributed among the 15 wights is the following: 86sp, 94gp, 149pp, potion of blur, ring of feather falling, potion of flaming fists; scroll: protection from arrows (arcane, caster level 3).
+</text>
+            </nodehandler>
+            <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Wights (15)" version="1.0">
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wights" version="1.0">
+                <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Wights
+Class:      Undead 4
+Level:      4
+Race:       Wight
+Alignment:      Lawful Evil
+Gender:     M
+Size:       M
+Experience Points (XP):     0
+(XP) Needed for Next Level:     0
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages:
+*******************************************************************************
+Score:  Mod:    Abilities:
+12  +1  :Strength (STR)
+12  +1  :Dexterity (DEX)
+--  0   :Constitution (CON)
+11  +0  :Intelligence(INT)
+13  +1  :Wisdom(WIS)
+15  +2  :Charisma(CHA)
+*******************************************************************************
+15  :(AC) Armor Class
+11  :(AC) vs. Touch
+14  :(AC) Flat footed
+32  :(HP) Maximum Hit Points
+*******************************************************************************
++1  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++2
+Ranged Bonuses:
++2
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+Slam (Natural/Primary)
++3  1d4+1   20/x2   0'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*******************************************************************************
+Saving Throws:
++1  :FORTITUDE (CON)
++2  :REFLEX (DEX)
++5  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+5   Climb
+5   Hide
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+43.0    86.0    130.0
+0.0 lbs :Total Encumbrance
+Light   :Carry Load
+130.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Slam (Natural/Primary)
+*******************************************************************************
+Funds:
+Gold Pieces: 110.00
+Misc Finds:
+Misc Magic:
+Total Valued: 0.0 gp
+*******************************************************************************
+Feats:
+Blind-Fight
+*******************************************************************************
+Class Abilities:
+ ability damage, and disease. Not subject to critical hits, energy drain, or death from massive damage., paralysis, poison, sleep, stunning, subdual damage,Create Spawn(Su),Energy Drain(Su),Immune to mind-influencing effects
+*******************************************************************************
+Racial Traits:
+Energy Drain(Su), Create Spawn(Su)
+*******************************************************************************
+
+*******************************************************************************</text>
+              </nodehandler>
+              <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+                <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+                  <form height="140" width="400"/>
+                  <nodehandler class="textctrl_handler" icon="d20" module="forms" name="INITIATIVE ROLL" version="1.0">
+                    <text multiline="0" send_button="1">[1d20+1]</text>
+                  </nodehandler>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT ARMOR CLASS (AC)" version="1.0">
+                    <text multiline="0" send_button="1">15</text>
+                  </nodehandler>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wight Hit Points" version="1.0">
+                    <text multiline="0" send_button="1">20 each</text>
+                  </nodehandler>
+                </nodehandler>
+                <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+                  <form height="900" width="400"/>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                    <text multiline="1" send_button="1">Slam (Natural/Primary) (Crit.20x2) - [1d20+3]</text>
+                  </nodehandler>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                    <text multiline="0" send_button="1">[1d4+1]</text>
+                  </nodehandler>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                    <text multiline="0" send_button="1">[2*1d4+1]</text>
+                  </nodehandler>
+                </nodehandler>
+                <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+                  <form height="200" width="400"/>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                    <text multiline="0" send_button="1">[1d20+1]</text>
+                  </nodehandler>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                    <text multiline="0" send_button="1">[1d20+2]</text>
+                  </nodehandler>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                    <text multiline="0" send_button="1">[1d20+5]</text>
+                  </nodehandler>
+                </nodehandler>
+                <nodehandler class="form_handler" icon="form" module="forms" name="Feats" version="1.0">
+                  <form height="400" width="400"/>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Blind Fighting " version="1.0">
+                    <text multiline="1" send_button="0">Blind-Fight [General]
+
+Benefit: In melee, every time a character misses because of concealment, the character can reroll the miss chance percentile roll one time to see if the character actually hit.
+
+An invisible attacker gets no bonus to hit the character in melee. That is, the character doesnt lose a Dexterity bonus to Armor Class, and the attacker doesnt get the usual +2 bonus. The invisible attackers bonuses do still apply for ranged attacks, however.
+
+The character suffers only half the usual penalty to speed for being unable to see. Darkness and poor visibility in general reduces the character's speed to three-quarters of normal, instead of one-half.
+</text>
+                  </nodehandler>
+                </nodehandler>
+                <nodehandler class="form_handler" icon="form" module="forms" name="Special" version="1.0">
+                  <form height="400" width="400"/>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Undead" version="1.0">
+                    <text multiline="1" send_button="1">   Undead: Immune to mind-influencing effects, poison, sleep, paralysis, stunning, and disease. Not subject to critical hits, subdual damage, ability damage, energy drain, or death from massive damage.</text>
+                  </nodehandler>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Create Spawn" version="1.0">
+                    <text multiline="1" send_button="1">   Create Spawn (Su): Any humanoid slain by a wight becomes a wight in 1d4 rounds. Spawn are under the command of the wight that created them and remain enslaved until its death. They do not possess any of the abilities they had in life.</text>
+                  </nodehandler>
+                  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Energy Drain" version="1.0">
+                    <text multiline="1" send_button="1">   Energy Drain (Su): Living creatures hit by a wight's slam attack receive one negative level. The Fortitude save to remove the negative level has a DC of 14.</text>
+                  </nodehandler>
+                </nodehandler>
+              </nodehandler>
+              <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+                <form height="1410" width="400"/>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+                  <text multiline="0" send_button="1">Climb [1d20+5]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+                  <text multiline="0" send_button="1">Hide [1d20+8]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+                  <text multiline="0" send_button="1">Listen [1d20+8]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+                  <text multiline="0" send_button="1">Move Silently [1d20+16]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+                  <text multiline="0" send_button="1">Search [1d20+7]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+                  <text multiline="0" send_button="1">Spot [1d20+8]</text>
+                </nodehandler>
+              </nodehandler>
+              <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+                <form height="200" width="400"/>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+                  <text multiline="0" send_button="1">[1d20+1]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+                  <text multiline="0" send_button="1">[1d20+1]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+                  <text multiline="0" send_button="1">[1d20]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+                  <text multiline="0" send_button="1">[1d20]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+                  <text multiline="0" send_button="1">[1d20+1]</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+                  <text multiline="0" send_button="1">[1d20+2]</text>
+                </nodehandler>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: Treasure (DM)" version="1.0">
+              <text multiline="1" send_button="0">Distributed among the 15 wights is the following: 86sp, 94gp, 149pp, potion of blur, ring of feather falling, potion of flaming fists; scroll: protection from arrows (arcane, caster level 3).</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: Treasure (Player)" version="1.0">
+              <text multiline="1" send_button="0">86sp
+94gp
+149pp
+potion
+ring
+potion
+scroll: protection from arrows (arcane, caster level 3).</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Treasure (DM)" version="1.0">
+            <text multiline="1" send_button="1">   Thayer's dagger is called Blind Sight, which has an emerald encrusted hilt and wicked barb at its tip. The weapon has a +3 enchantment, and when held grants darkvision, bestows the feat Blind-Fight, and senses the approach of anyone in a 50 foot radius (Semiempathy, Int 9, Wis 10, Cha 11, AL: Chaotic Good).
+
+   The bard's magical harp is called Entrancer, and is a small harp of masterwork quality. The harp is gold with jade interwoven into it, and its strings are of the purest dwarven mithral. Three times per day the music of the harp (Perform check vs. DC 12) can cast charm monster, which aided Thayer in his defeat of many beasts in the area. The harp talks to the wielder constantly (Telepathy, Wis 12, Int 10, Cha 12, AL: Chaotic Neutral), demanding to entrance more and more monsters. Eventually Thayer tired of the harp's prattling and dumped it at this ruined tower.</text>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="link_handler" icon="compass" module="forms" name="Farmer Brown's Farm Map" version="1.0">
+        <link href="http://www.openrpg.com/orpgnuke/modules/My_eGallery/gallery/maps/Woodland/tq2c.jpg"/>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="group_handler" module="containers" name="Scenario #2d: Hill Ruins" version="1.0">
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #2: Hedras' Ruined Tower" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #2 Hedras' Ruined Tower
+   This fortress tower once belonged to the warlord Hedras, comrade of the wizard Ren and bard Thayer, and stood here has a deterrent to invaders entering this pass. The structure now lies in ruin, but no one in the area seems to know exactly how the tower was destroyed. One rumor speaks of a magical experiment gone wrong; while another says a clan of vengeful stone giants besieged it. The later of the two rumors was started by Sledge make him more imposing mountain residents.
+
+   There is little left of Hedras' original tower other than a gutted spire and scattered bricks, all covered in years of vine and moss growth. The area has been searched a dozen times by countless passers by, and if the warlord left any possessions behind they have long since been pillaged. A thorough search of the tower (Search DC 15) does reveal one clue an oblong carving in the wall that is roughly circular with a point at one end. This is a clue to nearby hidden treasure.
+
+   In truth, Hedras did leave a cache of treasure behind, though it is not in the tower. A mere 30 yards west of the tower, at the crest a nearby hill is a large boulder whose shape is identical to that of the tower drawing. If the heroes state they're looking specifically for something nearby of that &quot;shape&quot;, they spy the boulder on a successful Spot check (DC 10). Otherwise the chance to put two-and-two together with the nearby boulder requires both a successful Spot (DC 15) and Search (DC 15) check. The boulder weighs 1,400 pounds, and requires several heroes to move (see page #142 of the Player's Handbook).
+
+   Beneath the rock are Hedras' enchanted gloves and bracers, which he often kept here when not adventuring. The gloves are gloves of storing, and currently have a dagger +2 hidden away in stasis. The bracers are bracers of armor +4, but only seem to function for warrior-class characters. Should a wizard, rogue, or other non-fighter class put them on they sear the wearer's wrists for 1d3 points of damage each round.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: Read to Players" version="1.0">
+          <text multiline="1" send_button="1">The structure now lies in ruin, but no one in the area seems to know exactly how the tower was destroyed. There is little left of the original tower other than a gutted spire and scattered bricks, all covered in years of vine and moss growth.
+
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="0">Gloves of Storing
+
+Dagger +2
+
+Bracers of Armor +4
+(Only seem to function for warrior-class characters. Should a wizard, rogue, or other non-fighter class put them on they sear the wearer's wrists for 1d3 points of damage each round)</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: Treasure (Player)" version="1.0">
+          <text multiline="1" send_button="1">Leather gloves
+Bracers
+
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #3: Thayer's Ruined Mansion" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #3: Thayer's Ruined Mansion
+   Much like Hedras' tower, there is little that remains of the structure that was once Thayer the bard's mansion. Local legend says Thayer destroyed the mansion himself instead of allowing it to fall into the evil's hands, which is actually a true account of events. The what, where, and who of the evil was apparently only known to Thayer himself.
+
+   The remaining shattered walls and crumbling pillars often serves as daylight protection for various types of undead. Some of the more intelligent creatures have dug small burrows for further defense, which are easily found on a Spot check (DC 10). Currently, the ruins are home to a group of six ghasts who are typically inactive during the day; however, if disturbed they do venture out into the light to feed.
+
+Ghasts (6); Medium-Size Undead; Hit Dice: 4d12 (26 hp each); Initiative: +2 (Dex); Speed: 30 ft.; AC: 16 (+2 Dex, +4 natural); Attacks: Bite +4 melee, 2 claws +1 melee; Damage: 1d8+1 and paralysis, claws 1d4 and paralysis; Face/Reach: 5 ft. by 5 ft./5 ft.; Special Attacks: Stench, paralysis, create spawn; Special Qualities: Undead, +2 turn resistance; Saves: Fort +1, Ref +3, Will +6; Abilities: Str 13, Dex 15, Con  , Int 13, Wis 14, Cha 16; AL: CE; Skills: Climb +6, Escape Artist +8, Hide +8, Listen +8, Move Silently +7, Search +6, Spot +8, Jump +6, Intuit Direction +3; Feats: Multiattack, Weapon Finesse (bite)
+
+   The ghasts have little treasure, having just taken up residence in these parts, but one of them has a ring of regeneration on his right hand (though it doesn't work for undead).
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: Read to Players" version="1.0">
+          <text multiline="1" send_button="1">Much like other structures in this area, there is little that remains of the structure that was once a great mansion. Shattered walls and crumbling pillars are all that are left of the building.</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="skull" module="containers" name="Ghasts (6)" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Ghast" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Ghast
+Class:      Undead 4
+Level:      4
+Race:       Ghast
+Alignment:      Chaotic Evil
+Gender:     M
+Size:       M
+Experience Points (XP):     0
+(XP) Needed for Next Level:     0
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages: Common
+*******************************************************************************
+Score:  Mod:    Abilities:
+13  +1  :Strength (STR)
+15  +2  :Dexterity (DEX)
+--  0   :Constitution (CON)
+13  +1  :Intelligence(INT)
+14  +2  :Wisdom(WIS)
+16  +3  :Charisma(CHA)
+*******************************************************************************
+16  :(AC) Armor Class
+12  :(AC) vs. Touch
+14  :(AC) Flat footed
+37  :(HP) Maximum Hit Points
+*******************************************************************************
++2  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++2
+Ranged Bonuses:
++2
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+Bite (Natural/Primary)
++3  1d6+1   20/x2   0'
+Claw (Natural/Secondary)
+-2/-2   1d4 20/x2   0'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*******************************************************************************
+Saving Throws:
++1  :FORTITUDE (CON)
++3  :REFLEX (DEX)
++6  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+6   Climb
+5   Hide
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+50.0    100.0   150.0
+0.0 lbs :Total Encumbrance
+Light   :Carry Load
+150.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Bite (Natural/Primary)
+1   Claw (Natural/Secondary)
+*******************************************************************************
+Funds:
+Gold Pieces: 80.00
+Misc Finds:
+Misc Magic:
+Total Valued: 0.0 gp
+*******************************************************************************
+Feats:
+Multiattack
+*******************************************************************************
+Class Abilities:
+ ability damage, and disease. Not subject to critical hits, energy drain, or death from massive damage., paralysis, poison, sleep, stunning, subdual damage,+2 Turn Resistance,Create Spawn(Su),Immune to mind-influencing effects,Paralysis(Ex),Stench(Ex)
+*******************************************************************************
+Racial Traits:
++2 Turn Resistance, Stench(Ex), Paralysis(Ex), Create Spawn(Su)
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+2]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">16</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT HIT POINTS (HP)" version="1.0">
+                <text multiline="0" send_button="1">26</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Bite (Natural/Primary) (Crit.20x2) - [1d20+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d6+1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d6+1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Claw (Natural/Secondary) (Crit.20x2) - [1d20-2/-2]
+</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d4]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d4]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+6]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Climb [1d20+6]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Hide [1d20+8]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Listen [1d20+8]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Move Silently [1d20+7]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Search  [1d20+6]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Spot [1d20+8]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Jump [1d20+6]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Intuit Direction [1d20+3]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d200]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+3]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+            <text multiline="1" send_button="0">MultiAttack
+Weapon Finesse - Bite</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Undead Abilities" version="1.0">
+            <text multiline="1" send_button="0">Stench (Ex): The stink of death and corruption surrounding these creatures is sickening. Those within 10 feet must succeed at a Fortitude save (DC 15) or be wracked with nausea, suffering a -2 circumstance penalty to all attacks, saves, and skill checks for 1d6+4 minutes.
+
+Paralysis (Ex): Those hit by a ghasts bite or claw attack must succeed at a Fortitude save (DC 15) or be paralyzed for 1d6+4 minutes. Even elves are vulnerable to this paralysis.
+</text>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: Ghast Treasure - DM" version="1.0">
+          <text multiline="0" send_button="0">Ring of Regeneration</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: Ghast Treasure - Player" version="1.0">
+          <text multiline="0" send_button="0">Magical ring</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #4: Stone Cliff Ruins" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: DM Introduction" version="1.0">
+          <text multiline="1" send_button="0">Encounter #4: Granite Stone Cliff Ruins
+   Carved into the side of a steep nearby cliff is an old outlook post used by Thayer and Hedras. The post suffered considerable damage some years ago when giants pummeled it with boulders, and since has been abandoned. Due to the steep nature of the cliff, reaching the outpost is difficult (Climb DC 20). There is nothing of value left in the ruins, though the location offers excellent shelter from the elements or to spend the night.
+
+If the PCs are taking no efforts to conceal their approach into this section of the valley, there is a chance (opposed Spot) they will be spotted by the Ogres in this area, who will set up an ambush from this look out.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: Read to Players" version="1.0">
+          <text multiline="1" send_button="1">Carved into the side of a steep nearby cliff is an old outlook post or lookout platform. The post suffered considerable damage at some point when it was pummeled it with boulders, and since has been abandoned. Due to the steep nature of the cliff, reaching the outpost is difficult.
+
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #5 Destroyed Castle" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #5: DM Introduction" version="1.0">
+          <text multiline="1" send_button="0">Encounter #5: Destroyed Castle
+   Like many of the other structures in this area, this once proud castle now lies in utter ruin only remnants of the foundation remain. Also like other ruins in the area, adventurers and monsters alike picked it clean of booty years ago. Recently the ogres living at Encounter #6 have been staking out this area looking for easy prey, since travelers often explore the ruins.
+
+   Anyone entering the area of the ruined castle has a chance (Spot roll) of attracting the attention of the nearby ogres. Since the ogres know the terrain better than the heroes, it is more difficult to see them approach (Spot DC 18). See Encounter #6 for the ogre's stats.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #5: Read to Players" version="1.0">
+          <text multiline="1" send_button="1">Like many of the other structures in this area, this once proud castle now lies in utter ruin. Only remnants of the foundation remain. Also like other ruins in the area, adventurers and monsters alike picked it clean of booty years ago.</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #6: Ogre Catacomb" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: DM Introduction" version="1.0">
+          <text multiline="1" send_button="0">Encounter #6: Ogre Catacomb
+   In the hillside here are a small complex of catacombs that serve as the home for a group of ogre raiders. The ogres had often joined with Sledge the stone giant on raids of the area, and took up residence in these local caves. There are six ogre males and two females here, though the females do not leave the catacombs unless threatened. From the mouth of their cave they can see most of the valley, and use this advantage to ambush unwary travelers.
+
+Ogres (8): Large Giant; HD 4d8+8 (26 hp); Init  1 (Dex); Spd 30ft.; AC 16 (-1 size, -1 Dex, +5 natural, +3 hide); Atk: Huge greatclub +8 melee; or Huge longspear +1 ranged; Dam Huge greatclub 2d6+7; or Huge longspear 2d6+5; Face 5 ft. by 5 ft./10 ft. (15-20 ft. with longspear); Save Fort +6, Ref +0, Will +1; Str 21, Dex 8, Con 15, Int 6, Wis 10, Cha 7; AL CE; Skills: Climb +4, Listen +2, Spot +2; Feats: Weapon Focus (greatclub).
+
+   The ogres have a large iron chest where they keep their treasure, buried beneath bones and other refuse near the rear of the caves. The treasure consists of: 1,401cp, 141sp, 1,345gp, 5pp, scroll: heat metal (divine, caster level 3), wand of magic missle (5th, 20 charges), potion of vision, and a ring of protection +2.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: Read to Players" version="1.0">
+          <text multiline="1" send_button="1">In the hillside here are a small complex of catacombs. Smoke and a foul stench can be detected from across the valley. </text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Ogres (8)" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Ogre" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Ogre
+Class:      Giant 4
+Level:      4
+Race:       Ogre
+Alignment:      Chaotic Evil
+Gender:     M
+Size:       L
+Experience Points (XP):     0
+(XP) Needed for Next Level:     0
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages: Giant
+*******************************************************************************
+Score:  Mod:    Abilities:
+21  +5  :Strength (STR)
+8   -1  :Dexterity (DEX)
+15  +2  :Constitution (CON)
+6   -2  :Intelligence(INT)
+10  +0  :Wisdom(WIS)
+7   -2  :Charisma(CHA)
+*******************************************************************************
+13  :(AC) Armor Class
+8   :(AC) vs. Touch
+13  :(AC) Flat footed
+26  :(HP) Maximum Hit Points
+*******************************************************************************
+-1  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++3
+Ranged Bonuses:
++3
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+*Greatclub
++8  1d10+7  20/x2   0'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*******************************************************************************
+Saving Throws:
++6  :FORTITUDE (CON)
++0  :REFLEX (DEX)
++1  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+2   Listen
+2   Spot
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+306.0   613.0   920.0
+10.0 lbs    :Total Encumbrance
+Light   :Carry Load
+920.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Greatclub
+*******************************************************************************
+Funds:
+Gold Pieces: 0.00
+Misc Finds:
+Misc Magic:
+Total Valued: 5.0 gp
+*******************************************************************************
+Feats:
+Armor Proficiency (Light) (1x),Armor Proficiency (Medium) (1x),Martial Weapon Proficiency,Weapon Focus (Greatclub),
+*******************************************************************************
+Class Abilities:
+
+*******************************************************************************
+Racial Traits:
+
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20-1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">13</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT HIT POINTS (HP)" version="1.0">
+                <text multiline="0" send_button="1">26 each</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">*Greatclub (Crit.20x2) - [1d20+8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d10+7]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d10+7]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Longspear" version="1.0">
+                <text multiline="0" send_button="1">[1d20+1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Longspear Damage" version="1.0">
+                <text multiline="0" send_button="1">[2d6+5]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+6]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+1]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Listen [1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Spot [1d20+2]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+5]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-2]</text>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="0">   The ogres have a large iron chest where they keep their treasure, buried beneath bones and other refuse near the rear of the caves. The treasure consists of:
+
+1,401cp
+141sp
+1,345gp
+5pp
+Scroll: heat metal (divine, caster level 3)
+wand of magic missle (5th, 20 charges)
+potion of vision
+ring of protection +2 (Reflex)</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: Treasure (Player)" version="1.0">
+          <text multiline="1" send_button="0">The ogres have a large iron chest where they keep their treasure, buried beneath bones and other refuse near the rear of the caves. The treasure consists of:
+
+1,401cp
+141sp
+1,345gp
+5pp
+Scroll: heat metal (divine, caster level 3)
+Wand
+Potion
+Ring
+</text>
+        </nodehandler>
+      </nodehandler>
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="link_handler" icon="compass" module="forms" name="Hill Ruins Map" version="1.0">
+        <link href="http://www.openrpg.com/orpgnuke/modules/My_eGallery/gallery/maps/Woodland/tq2d.jpg"/>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="group_handler" module="containers" name="Scenario #2e: Dwarven Catacomb" version="1.0">
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #1: King's Hall" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #1: King's Hall
+   This elongated chamber was once called King's Hall, serving as both a grand meeting place and tribute to dwarven kings of old; however, now the room lies gutted and in ruin. Tattered remnants of tapestries hang from the all of the walls, and the floor here is jagged and covered in rubble. The later is from the removal of gold that had been inlayed into the stone floor, proving that others have been through this area before.
+
+   There is little else of value in this chamber; however, even the slightest bit of noise in this room draws the attention of wights from Encounter #2. These wights are a mix of humans, dwarves, and orcs a collection of several races who have investigated these catacombs over the years and did nothing but add to the wight's numbers. Stats for the undead can be found in Encounter #2 for all wights found in this area.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #3: Blacksmith Chambers" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #3: Dwarven Blacksmith Chambers
+   Any dwarf or hero with weaponsmithing instantly recognizes this area as being a blacksmithing room, despite its shambled appearance. A 75 pound anvil lies buried beneath several large chunks of ceiling rubble, and a cursory Search (DC 10) reveals several (6 total) blocks of iron and steel used to craft weapons. All items of value were removed by raiders long ago, but all the tools to craft or repair armor or weapons can still be found here.
+
+   The only other item of note here is mauled the body of a dwarf, a member of the last dwarven expedition to try and reclaim these catacombs from the current undead residents. The dwarf has only been dead a couple of weeks, allowing the heroes to use speak with dead if there are any clerics present. The dwarf was Lawful Good in alignment for the purposes of the spell.
+
+   The dwarven corpse says that he and three other dwarves came here looking for the Crown of Moradin, and ancient relic of the dwarves rumored to be hidden here. When the wights attacked before they found it, and he was struck in the head and killed before he could be converted to an undead. The wights then fed on his flesh and left him where he fell. That is all the corpse remembers. DM should be sure to thoroughly read the speak with dead spell before answering further questions posed by the heroes.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #4: Hidden Treasure Chamber" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: DM Information" version="1.0">
+          <text multiline="1" send_button="0">#4: Hidden Treasure Chamber
+   This dwarven treasure vault has stood undisturbed since these catacombs were first invaded. Between the resident undead and the difficulty in finding the entrance, the horde of these dwarves has yet to be discovered. The reason for this is the trigger to open the tomb is not near the secret door, but is in Encounter #6: Dwarven Temple.
+
+   Within the temple is a small altar dedicated to Moradin, lawful good God of the Dwarves. If a donation is placed on the altar (see Encounter #6), a button magically appears three feet off the ground at the treasure chamber entrance. The button is only visible for two seconds, before disappearing again. Pressing the button is the only way to open the treasure vault..
+
+   Any dwarf may make a Wisdom check (DC 22) to see if they can decipher the way into the chamber. Others with Knowledge-Religion (DC 30) may try as well. In places of dwarven worship, this is a common form of tribute to Moradin. Items placed on the altar are not transported to this treasure chamber.
+
+   Once opened the vault reveals a literal horde of treasure, which consists of: 3,001cp, 2,701sp, 4,551gp, 30pp, 12 assorted gems (14gp, 90gp, 8gp, 6gp, 900gp, 1,000gp, 40gp, 7gp, 60gp, 500gp, 40gp, 12gp), scroll: heat metal, doom, lesser restoration (divine, caster level 3), ring of wizardry I, and the Crown of Moradin.
+
+   The Crown of Moradin is a dwarven relic native to this region of the world. It was created by a dwarven blacksmith and cleric in this very catacomb long ago, and has actually never left these chambers. To a non-dwarf the crown is just a hunk of metal, though it is inlayed with assorted gems totaling 9,000gp in value. When placed on the head of a dwarf, however, his or her Charisma is considered to be 25 to all other dwarves, and gains an additional +3 points to all others. The crown was originally crafted for some forgotten royalty, but for unknown reasons never left these caverns.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="0">   Once opened the vault reveals a literal horde of treasure, which consists of: 3,001cp, 2,701sp, 4,551gp, 30pp, 12 assorted gems (14gp, 90gp, 8gp, 6gp, 900gp, 1,000gp, 40gp, 7gp, 60gp, 500gp, 40gp, 12gp), scroll: heat metal, doom, lesser restoration (divine, caster level 3), ring of wizardry I, and the Crown of Moradin.
+
+   The Crown of Moradin is a dwarven relic native to this region of the world. It was created by a dwarven blacksmith and cleric in this very catacomb long ago, and has actually never left these chambers. To a non-dwarf the crown is just a hunk of metal, though it is inlayed with assorted gems totaling 9,000gp in value. When placed on the head of a dwarf, however, his or her Charisma is considered to be 25 to all other dwarves, and gains an additional +3 points to all others. The crown was originally crafted for some forgotten royalty, but for unknown reasons never left these caverns.
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: Treasure (Player)" version="1.0">
+          <text multiline="1" send_button="1">   Once opened the vault reveals a literal horde of treasure, which consists of: 3,001cp, 2,701sp, 4,551gp, 30pp, 12 assorted gems (14gp, 90gp, 8gp, 6gp, 900gp, 1,000gp, 40gp, 7gp, 60gp, 500gp, 40gp, 12gp), scroll: heat metal, doom, lesser restoration (divine, caster level 3), ring, and a crown.</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #5: Armory" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #5: Dwarven Armory
+   Racks once holding suits of dwarven armor and weapons now lie empty among rubble in this chamber. The ceiling here has begun to fail, and much of the outer mortar has already fallen to the floor. While the room appears to be structurally in poor condition, with the exception of a few falling of bricks (1 point of damage if jarred) the outer supports of the cavern are sound. In fact, the two doors to this chamber are constructed of heavy iron, and can be barred from the inside if the heroes are fleeing the local undead.
+
+   This room has been undisturbed for a long time, as even the undead have had not reason to venture in here. With the exception of its secure doors, there is nothing else of value here.
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #6: Dwarven Temple" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #6: Dwarven Temple
+   This long chamber was once a temple to Moradin, Dwarven God of the Earth and Protection. Like many of the chambers here, a thick layer of dust and fallen mortar cover the floor. Although it's in disarray, the divine presence of Moradin can still be felt here by any dwarf who worships him. Lawful good heroes also feel a sense of virtue while in this chamber. The wights will not enter this room.
+
+   The altar here was sacred to the dwarves, who often placed finely crafted items on it in tribute to their god. The altar is a simple stone slab with dwarven runes encircling it at the top. The runes tell of how the world was created by Moradin, who then entrusted it to the dwarves. This is one of many version of creation told to worshipers of the God of the Dwarves, and is commonly duplicated in other faiths. Outside of the altar, there is nothing else of value here.
+
+   As outlined in Encounter #4 any item placed on the altar disappears (considered out of game play) in tribute, and briefly activates a button used to access the treasure vault. This is the only way to activate the secret button, though magic such as true seeing does reveal the location of the vault. </text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #7: Mines" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #7: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #7 Dwarven Mines
+   Though hardly extensive compared to other dwarven mining operations, these tunnels produced a large amount of gold, iron ore and some mithral hundreds of years ago. Unfortunately, the veins were small and played out quickly. Afterwards, a few of the dwarves stayed to worship Moradin and craft armor for their brethren far away. It is a mystery what caused their deaths, for any dwarf (or anyone that knows them) would tell you they would never have abandoned the caverns-especially with such a large cache of treasure still here.
+
+   The only current resident of the caverns is a gibbering mouther who has been slowly starving to death (undead are not to its liking). Like the wights, if the heroes are heard by the mouther it immediately comes forward looking for a meal.
+
+Gibbering Mouther: Medium-Size Aberration; Hit Dice: 4d8+4 (22 hp); Initiative: +1 (Dex); Speed: 10 ft., swim 20 ft.; AC: 19 (+1 Dex, +8 natural); Attacks: 6 bites +4 melee; Damage: Bite 1; Face/Reach: 5 ft. by 5 ft./5 ft.; Special Attacks: Gibbering, spittle, improved grab, blood drain, engulf, ground manipulation; Special Qualities: Amorphous; Saves: Fort +2, Ref +2, Will +5; Str 10, Dex 13, Con 12, Int 4, Wis 13, Cha 13; Skills: Listen +8, Spot +12; Feats: Weapon Finesse (bite); AL: N; CR: 5
+</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Gibbering Mouther" version="1.0">
+          <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+              <text multiline="0" send_button="0">Gibbering Mouther</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Race" version="1.0">
+              <text multiline="0" send_button="0">Gibbering Mouther</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+              <text multiline="0" send_button="0">Walk 10', Swim 20'</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC: Total / Touch / Flat Footed" version="1.0">
+              <text multiline="0" send_button="0">19 / 11 / 18</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face / Reach" version="1.0">
+              <text multiline="0" send_button="0">5 ft. by 5 ft./5</text>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+              <grid autosize="1" border="1">
+                <row version="1.0">
+                  <cell>STR</cell>
+                  <cell>10</cell>
+                  <cell>+0</cell>
+                </row>
+                <row version="1.0">
+                  <cell>DEX</cell>
+                  <cell>13</cell>
+                  <cell>+1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CON</cell>
+                  <cell>12</cell>
+                  <cell>+1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>INT</cell>
+                  <cell>3</cell>
+                  <cell>-4</cell>
+                </row>
+                <row version="1.0">
+                  <cell>WIS</cell>
+                  <cell>13</cell>
+                  <cell>+1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CHA</cell>
+                  <cell>14</cell>
+                  <cell>+2</cell>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+              <text multiline="1" send_button="0">Weapon Finesse - Bite</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Templates" version="1.0">
+              <text multiline="1" send_button="0"></text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Combat" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+              <text multiline="0" send_button="1">[1d20+1]</text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Abilities" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">STR [1d20+0]</option>
+                <option selected="0" value="0">DEX [1d20+1]</option>
+                <option selected="0" value="0">CON [1d20+1]</option>
+                <option selected="0" value="0">INT [1d20-4]</option>
+                <option selected="0" value="0">WIS [1d20+1]</option>
+                <option selected="0" value="0">CHA [1d20+2]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">Fortitude [+2+1d20]</option>
+                <option selected="0" value="0">Reflex [+2+1d20]</option>
+                <option selected="1" value="0">Will [+3+1d20]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP (out of 22)" version="1.0">
+              <text multiline="0" send_button="0"></text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+              <list send_button="1" type="3">
+                <option selected="0" value="0">Bite (Natural/Primary): [1d20+3/+3/+3/+3/+3/+3]  Damage [1]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Skills" version="1.0">
+              <list send_button="0" type="0">
+                <option selected="0" value="0">Listen [1d20+8]</option>
+                <option selected="0" value="0">Spot [1d20+12]</option>
+              </list>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="link_handler" icon="compass" module="forms" name="Dwarven Empty Catacombs Map" version="1.0">
+        <link href="http://www.openrpg.com/orpgnuke/modules/My_eGallery/gallery/maps/Dungeon/tq2e.jpg"/>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="group_handler" module="containers" name="Scenario #2f: Mountain Pass" version="1.0">
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #1: Mystical Pool" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #1: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #1: Mystical Bubbling Pool
+   At the center of this mountain pass is a bubbling pool, one the locals say has magical powers. Of all the locations in the mountain, this region is the most cherished by the stone giant Sledge and his associates. Sledge will be in this area when the PCs arrive, along with 1d6 ogres (stats from Encounter #2D) and 1d8 orcs (stats from Encounter #2A). The giant will actually be here or guarding the pass at Encounter #3 (stats for Sledge there).
+
+   The bubbling pool does have minor magical abilities; curing 2d4 hit points from any injured party up to twice per day. The pool is surrounded by a series of boulders, and each have an elven rune carved upon them. Anyone able to read elf can determine the effects of the pools magic without drinking first. The water in the pool is crystal clear, has a diameter of five feet, and is roughly three feet in depth at the center.
+</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Orcs (1d8)" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Orc NPC" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Orc NPC
+Class:      Fighter 1
+Level:      1
+Race:       Orc
+Alignment:      Chaotic Evil
+Gender:     M
+Size:       M
+Experience Points (XP):     0
+(XP) Needed for Next Level:     1000
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages: Orc
+*******************************************************************************
+Score:  Mod:    Abilities:
+15  +2  :Strength (STR)
+10  +0  :Dexterity (DEX)
+11  +0  :Constitution (CON)
+9   -1  :Intelligence(INT)
+8   -1  :Wisdom(WIS)
+8   -1  :Charisma(CHA)
+*******************************************************************************
+14  :(AC) Armor Class
+10  :(AC) vs. Touch
+14  :(AC) Flat footed
+10  :(HP) Maximum Hit Points
+*******************************************************************************
++0  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++1
+Ranged Bonuses:
++1
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+*Greataxe
++3  1d12+3  20/x3   0'
+Crossbow (Light)
++1  1d8 19-20/x2    80'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*Scale Mail
++4  +3  -4  25
+*******************************************************************************
+Saving Throws:
++2  :FORTITUDE (CON)
++0  :REFLEX (DEX)
+-1  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+2   Jump
+3   Listen
+3   Spot
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+66.0    133.0   200.0
+57.0 lbs    :Total Encumbrance
+Light   :Carry Load
+200.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Scale Mail
+1   Greataxe
+1   Crossbow (Light)
+1   Bolts (Crossbow/10)
+*******************************************************************************
+Funds:
+Gold Pieces: 190
+Misc Finds:
+Misc Magic:
+Total Valued: 106.0 gp
+*******************************************************************************
+Feats:
+Alertness,Armor Proficiency (Heavy) (1x),Armor Proficiency (Light) (1x),Armor Proficiency (Medium) (1x),Martial Weapon Proficiency,Point Blank Shot,Shield Proficiency,Simple Weapon Proficiency
+*******************************************************************************
+Class Abilities:
+Light Sensitivity(Ex)
+*******************************************************************************
+Racial Traits:
+
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="ORC INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">14</text>
+              </nodehandler>
+              <nodehandler class="form_handler" icon="form" module="forms" name="ORC HIT POINTS" version="1.0">
+                <form height="400" width="400"/>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 1 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 2 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 3 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 4 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 5 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 6 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 7 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 8 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 9 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="ORC 10 (10)" version="1.0">
+                  <text multiline="0" send_button="1">10</text>
+                </nodehandler>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">*Greataxe (Crit.20x3) - [1d20+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d12+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[3*1d12+3]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">Crossbow (Light) (Crit.19-20x2) - [1d20+1]
+</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d8]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+2]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20-1]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Jump [1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Listen [1d20+3]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Spot [1d20+3]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2a: Random Orc Treasure" version="1.0">
+            <text multiline="0" send_button="1">[3d10] gold pieces each</text>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Ogres (1d6)" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Ogre" version="1.0">
+            <text multiline="1" send_button="0">OpenRPG D&amp;D CHARACTER RECORD SHEET (3rd Edition)
+*******************************************************************************
+Character Name: Ogre
+Class:      Giant 4
+Level:      4
+Race:       Ogre
+Alignment:      Chaotic Evil
+Gender:     M
+Size:       L
+Experience Points (XP):     0
+(XP) Needed for Next Level:     0
+*******************************************************************************
+Description:
+*******************************************************************************
+Languages: Giant
+*******************************************************************************
+Score:  Mod:    Abilities:
+21  +5  :Strength (STR)
+8   -1  :Dexterity (DEX)
+15  +2  :Constitution (CON)
+6   -2  :Intelligence(INT)
+10  +0  :Wisdom(WIS)
+7   -2  :Charisma(CHA)
+*******************************************************************************
+13  :(AC) Armor Class
+8   :(AC) vs. Touch
+13  :(AC) Flat footed
+26  :(HP) Maximum Hit Points
+*******************************************************************************
+-1  :Initiative
+*******************************************************************************
+Attacks Weapons Melee &amp; Ranged:
+Melee Bonuses:
++3
+Ranged Bonuses:
++3
+Weapon Name:
+Bonus:  Damage: Crit.   Range:
+*Greatclub
++8  1d10+7  20/x2   0'
+*******************************************************************************
+Armor Name:
+Bonus:  Max Dex:    Penalty:    Spell Failure:
+*******************************************************************************
+Saving Throws:
++6  :FORTITUDE (CON)
++0  :REFLEX (DEX)
++1  :WILL (WIS)
+*******************************************************************************
+Skills:
+Total:  Name:
+2   Listen
+2   Spot
+*******************************************************************************
+Carrying Limits:
+Light:  Medium: Heavy:
+306.0   613.0   920.0
+10.0 lbs    :Total Encumbrance
+Light   :Carry Load
+920.0   :Carry Capacity
+*******************************************************************************
+Equipment List:
+Qty:    Name:
+1   Greatclub
+*******************************************************************************
+Funds:
+Gold Pieces: 0.00
+Misc Finds:
+Misc Magic:
+Total Valued: 5.0 gp
+*******************************************************************************
+Feats:
+Armor Proficiency (Light) (1x),Armor Proficiency (Medium) (1x),Martial Weapon Proficiency,Weapon Focus (Greatclub),
+*******************************************************************************
+Class Abilities:
+
+*******************************************************************************
+Racial Traits:
+
+*******************************************************************************
+
+*******************************************************************************</text>
+          </nodehandler>
+          <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Combat" version="1.0">
+            <nodehandler class="form_handler" icon="form" module="forms" name="AC / HP" version="1.0">
+              <form height="140" width="400"/>
+              <nodehandler class="textctrl_handler" icon="d20" module="forms" name="INITIATIVE ROLL" version="1.0">
+                <text multiline="0" send_button="1">[1d20-1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT ARMOR CLASS (AC)" version="1.0">
+                <text multiline="0" send_button="1">13</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="CURRENT HIT POINTS (HP)" version="1.0">
+                <text multiline="0" send_button="1">26 each</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Weapons" version="1.0">
+              <form height="900" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Attack with" version="1.0">
+                <text multiline="1" send_button="1">*Greatclub (Crit.20x2) - [1d20+8]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Normal Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[1d10+7]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Critical Damage Roll" version="1.0">
+                <text multiline="0" send_button="1">[2*1d10+7]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Longspear" version="1.0">
+                <text multiline="0" send_button="1">[1d20+1]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Longspear Damage" version="1.0">
+                <text multiline="0" send_button="1">[2d6+5]</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Saving Throws" version="1.0">
+              <form height="200" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - FORTITUDE" version="1.0">
+                <text multiline="0" send_button="1">[1d20+6]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - REFLEX" version="1.0">
+                <text multiline="0" send_button="1">[1d20+0]</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving Throw - WILL" version="1.0">
+                <text multiline="0" send_button="1">[1d20+1]</text>
+              </nodehandler>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Skill Checks" version="1.0">
+            <form height="1410" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Listen [1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Skill" version="1.0">
+              <text multiline="0" send_button="1">Spot [1d20+2]</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Abilities Checks" version="1.0">
+            <form height="200" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Strength (STR) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+5]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Dexterity (DEX) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Constitution (CON) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Intellengence (INT) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-2]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Wisdom (WIS) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20+0]</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Charisma (CHA) Check" version="1.0">
+              <text multiline="0" send_button="1">[1d20-2]</text>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #2: Trees" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #2: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #2: Trees
+   On the hill just south of the pool is a small copse of five towering oak trees, planted here by elves long ago. The trees are ancient, planted by the same mystical elves that enchanted the bubbling pool. Originally there were two dozen oak trees dotting the hillside, but over the millennia they have either died off or have been harvested for their wood. Any elf heroes instantly see the trees for the sacred symbol they represent, and have a chance (Wisdom vs. DC 20) to notice that one of the trees is actually a treant.
+
+   The treant has been keeping a low profile in recent years, given the presence of Sledge and other evil forces in the mountain area. He is the last of a group of treants left here by the elves to protect the mystical pool from evil, and considers himself a disappointment for failing to fulfill his promise. If a hero in the group is a good aligned elf or half-elf, the treant may consider speaking with them if it's sure others in the party are as moral in nature. The treant knows of the powers of the pool, and may assist the heroes to help them against Sledge if it seems like they have a good chance at winning. Though he considers himself to have failed in his duty he is not about to abandon the pool, and will not throw his life away in a futile attempt to try and defeat the storm giant.
+
+Treant: Huge Plant; Hit Dice: 7d8+35 (66 hp); Initiative: -1 (Dex); Speed: 30 ft.; AC: 20 (-2 size, -1 Dex, +13 natural); Attacks: 2 slams +12 melee; Damage: Slam 2d6+9; Face/Reach: 10 ft. by 10 ft./15 ft.; Special Attacks: Animate trees, trample, double damage against objects; Special Qualities: Plant, fire vulnerability, half damage from piercing; Saves: Fort +10, Ref +1, Will +6; Str 29, Dex 8, Con 21, Int 12, Wis 15, Cha 12; Skills: Hide -9*, Intimidate +8, Knowledge (any one) +8, Listen +9, Sense Motive +9, Spot_+9, Wilderness Lore +9; Feats: Iron Will, Power Attack; AL: NG; CR 8
+
+*Treants receive skills as though they were fey*. They have a +16 racial bonus to Hide checks made in forested areas
+
+   If the heroes admirably defend the pool from evil forces, the treant rewards the bravest party member (or good aligned elf) with an elven masterwork longsword +3 which is hidden among its branches.
+
+</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Treant" version="1.0">
+          <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+              <text multiline="0" send_button="0">Treant</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Race" version="1.0">
+              <text multiline="0" send_button="0">Treant</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+              <text multiline="0" send_button="0">Walk 30'</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC: Total / Touch / Flat Footed" version="1.0">
+              <text multiline="0" send_button="0">20 / 7 / 20</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face / Reach" version="1.0">
+              <text multiline="0" send_button="0">5 ft. by 5 ft./15</text>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+              <grid autosize="1" border="1">
+                <row version="1.0">
+                  <cell>STR</cell>
+                  <cell>29</cell>
+                  <cell>+9</cell>
+                </row>
+                <row version="1.0">
+                  <cell>DEX</cell>
+                  <cell>8</cell>
+                  <cell>-1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CON</cell>
+                  <cell>21</cell>
+                  <cell>+5</cell>
+                </row>
+                <row version="1.0">
+                  <cell>INT</cell>
+                  <cell>12</cell>
+                  <cell>+1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>WIS</cell>
+                  <cell>15</cell>
+                  <cell>+2</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CHA</cell>
+                  <cell>12</cell>
+                  <cell>+1</cell>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+              <text multiline="1" send_button="0">Iron Will, Power Attack</text>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Skills" version="1.0">
+              <grid autosize="1" border="0">
+                <row version="1.0">
+                  <cell>Skill</cell>
+                  <cell>Total</cell>
+                  <cell>Rnk</cell>
+                  <cell>Stat</cell>
+                  <cell>Msc</cell>
+                </row>
+                <row version="1.0">
+                  <cell>Hide</cell>
+                  <cell/>
+                  <cell>-9</cell>
+                  <cell/>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Intimidate</cell>
+                  <cell/>
+                  <cell>+8</cell>
+                  <cell/>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Knowledge (Nature)</cell>
+                  <cell/>
+                  <cell>+8</cell>
+                  <cell/>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Listen</cell>
+                  <cell/>
+                  <cell>+9</cell>
+                  <cell/>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Sense Motive</cell>
+                  <cell/>
+                  <cell>+9</cell>
+                  <cell/>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Spot</cell>
+                  <cell/>
+                  <cell>+9</cell>
+                  <cell/>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Wilderness Lore</cell>
+                  <cell/>
+                  <cell>+9</cell>
+                  <cell/>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell/>
+                  <cell/>
+                  <cell/>
+                  <cell/>
+                  <cell/>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Special Abilities" version="1.0">
+              <text multiline="1" send_button="0">Treants speak their own language, plus Common and Sylvan.
+
+Combat
+
+Treants prefer to watch potential foes carefully before attacking. They often charge suddenly from cover to trample the despoilers of forests. If sorely pressed, they animate trees as reinforcements.
+
+Animate Trees (Sp): A treant can animate trees within 180 feet at will, controlling up to two trees at a time. It takes a full round for a normal tree to uproot itself. Thereafter it moves at a speed of 10 and fights as a treant in all respects. Animated trees lose their ability to move if the treant who animated them is incapacitated or moves out of range. The ability is otherwise similar to liveoak as cast by a 12th-level druid.
+
+Trample (Ex): A treant or animated tree can trample Medium-size or smaller creatures for 2d12+5 points of damage. Opponents who do not make attacks of opportunity against the treant or animated tree can attempt a Reflex save (DC 20) to halve the damage.
+
+Double Damage against Objects (Ex): A treant or animated tree that makes a full attack against an object or structure deals double damage.
+
+Plant: Immune to mind-influencing effects, poison, sleep, paralysis, stunning, and polymorphing. Not subject to critical hits.
+
+Fire Vulnerability (Ex): A treant or animated tree takes double damage from fire attacks unless the attack allows a save, in which case it takes double damage on a failure and no damage on a success.
+
+Half Damage from Piercing (Ex): Piercing weapons deal only half damage to treants, with a minimum of 1 point of damage.
+
+Skills: Treants receive skills as though they were fey*. They have a +16 racial bonus to Hide checks made in forested areas.
+
+</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Combat" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+              <text multiline="0" send_button="1">[1d20-1]</text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Abilities" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">STR [1d20+9]</option>
+                <option selected="0" value="0">DEX [1d20-1]</option>
+                <option selected="0" value="0">CON [1d20+5]</option>
+                <option selected="0" value="0">INT [1d20+1]</option>
+                <option selected="0" value="0">WIS [1d20+2]</option>
+                <option selected="0" value="0">CHA [1d20+1]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">Fortitude [+10+1d20]</option>
+                <option selected="0" value="0">Reflex [+1+1d20]</option>
+                <option selected="1" value="0">Will [+3+1d20]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP (out of 66)" version="1.0">
+              <text multiline="0" send_button="0"></text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+              <list send_button="1" type="3">
+                <option selected="0" value="0">Slam (Natural/Primary): [1d20+12/+12]  Damage [2d6+13]</option>
+              </list>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #3: Stone Giant Raiders" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #3: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #3: Stone Giant Raiders
+   The entrance to this enchanted valley is where Sledge will make his stand against the heroes, not willing to give up any ground against them. Sledge has been tracking the hero's progress through the mountain ruins, and if there are any surviving orcs or ogres from previous encounters the giant gathers them together here.
+
+   Sledge is prepared for the hero's approach, day or night. Unless the group states they approaching with stealth, there is a good chance the stone giant will get the drop on them as they approach. If all the orcs and ogres from earlier encounters have been slain, Sledge angrily faces the heroes alone. He is not afraid of death, and takes great pains to make sure that if he is to die one of the heroes is going to follow him to the afterlife.
+
+Sledge, Stone Giant: Large Giant (Earth); HD: 14d8+56 (119 hp); Int +2 (Dex); Spd 40ft.; AC 25 ( 1 size, +2 Dex, +11 natural, +3 Hide armor); Atk huge greatclub +3 +20/+12 melee, or rock +12/+7 ranged; Dam huge greatclub +3 2d6+15, rock 2d8+8; Face 5 ft. by 5 ft./10 ft.; Special Attacks Rock Throwing; Special Qualities Rock Catching; Saves Fort +13, Ref +6, Will +4; Str 27, Dex 15, Con 19, Int 12, Wis 12, Cha 11; Skills Climb +10, Hide +0*, Jump +10, Spot +3; Feats Combat Reflexes, Point Blank Shot, Power Attack, Precise Shot; AL: N(E); CR: 8
+
+*A stone giant gains a +8 racial bonus to Hide checks in rocky terrain.
+
+   Rock Throwing (Ex): Adult giants are accomplished rock throwers and receive a +1 racial bonus to attack rolls when throwing rocks. A giant of at least Large size can hurl rocks weighing 40 to 50 pounds each (Small objects) up to 5 range increments. The size of the range increment varies with the giant's variety. A Huge giant can hurl rocks of 60 to 80 pounds (Medium-size objects).
+
+   Rock Catching (Ex): A giant of at least Large size can catch Small, Medium-size, or Large rocks (or projectiles of similar shape). Once per round, a giant that would normally be hit by a rock can make a Reflex save to catch it as a free action. The DC is 15 for a Small rock, 20 for a Medium-size one, and 25 for a Large one. (If the projectile has a magical bonus to attack, the DC increases by that amount.) The giant must be ready for and aware of the attack.
+
+   The stone giant Sledge carries a powerful magical weapon, one that allows him to escape defeat at the hands of the heroes a green glowing huge greatclub +3. In addition to the weapon's magical bonues', the club has the following powers at a 13th level of ability: dimension door 3x/day, cure serious wounds 3x/day, and protection from arrows 2x/day. The DM should keep track the use of these special powers should the heroes encounter the giant more than once per day. The club requires a Strength of 27 just to lift, and cannot be used by the heroes.
+</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Sledge, Stone Giant" version="1.0">
+          <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+              <text multiline="0" send_button="0">Sledge</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Race" version="1.0">
+              <text multiline="0" send_button="0">Stone Giant</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+              <text multiline="0" send_button="0">Walk 40'</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC: Total / Touch / Flat Footed" version="1.0">
+              <text multiline="0" send_button="0">22 / 11 / 20</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face / Reach" version="1.0">
+              <text multiline="0" send_button="0">5 ft. by 5 ft./10</text>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+              <grid autosize="1" border="1">
+                <row version="1.0">
+                  <cell>STR</cell>
+                  <cell>27</cell>
+                  <cell>+8</cell>
+                </row>
+                <row version="1.0">
+                  <cell>DEX</cell>
+                  <cell>15</cell>
+                  <cell>+2</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CON</cell>
+                  <cell>19</cell>
+                  <cell>+4</cell>
+                </row>
+                <row version="1.0">
+                  <cell>INT</cell>
+                  <cell>12</cell>
+                  <cell>+1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>WIS</cell>
+                  <cell>12</cell>
+                  <cell>+1</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CHA</cell>
+                  <cell>11</cell>
+                  <cell>+0</cell>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+              <text multiline="1" send_button="0">Combat Reflexes
+Point Blank Shot
+Power Attack
+Precise Shot
+</text>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Skills" version="1.0">
+              <grid autosize="1" border="0">
+                <row version="1.0">
+                  <cell>Skill</cell>
+                  <cell>Total</cell>
+                  <cell>Rnk</cell>
+                  <cell>Stat</cell>
+                  <cell>Msc</cell>
+                </row>
+                <row version="1.0">
+                  <cell>Climb</cell>
+                  <cell>10</cell>
+                  <cell>2.0</cell>
+                  <cell>8</cell>
+                  <cell>0</cell>
+                </row>
+                <row version="1.0">
+                  <cell>Hide</cell>
+                  <cell>6</cell>
+                  <cell>8.0</cell>
+                  <cell>2</cell>
+                  <cell>-4</cell>
+                </row>
+                <row version="1.0">
+                  <cell>Jump</cell>
+                  <cell>10</cell>
+                  <cell>2.0</cell>
+                  <cell>8</cell>
+                  <cell>0</cell>
+                </row>
+                <row version="1.0">
+                  <cell>Spot</cell>
+                  <cell>3</cell>
+                  <cell>2.0</cell>
+                  <cell>1</cell>
+                  <cell>0</cell>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Special Abilities" version="1.0">
+              <text multiline="1" send_button="0">(Elder Abilities); +8 to Hide in Rocky Terrain; Rock Catching(Ex); Rock Throwing(Ex); </text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Combat" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Abilities" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">STR [1d20+8]</option>
+                <option selected="0" value="0">DEX [1d20+2]</option>
+                <option selected="0" value="0">CON [1d20+4]</option>
+                <option selected="0" value="0">INT [1d20+1]</option>
+                <option selected="0" value="0">WIS [1d20+1]</option>
+                <option selected="0" value="0">CHA [1d20+0]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Skills" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">Climb Check: [10+1d20]</option>
+                <option selected="0" value="0">Hide Check: [6+1d20]</option>
+                <option selected="0" value="0">Jump Check: [10+1d20]</option>
+                <option selected="0" value="0">Spot Check: [3+1d20]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">Fortitude [+13+1d20]</option>
+                <option selected="0" value="0">Reflex [+6+1d20]</option>
+                <option selected="1" value="0">Will [+9+1d20]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP (out of 119)" version="1.0">
+              <text multiline="0" send_button="0"></text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+              <list send_button="1" type="3">
+                <option selected="1" value="0">Magical Green Club +3 (2atks): [1d20+23] / [1d20+15]</option>
+                <option selected="1" value="0">Rock (2 atks): [1d20+12] / [1d20+7]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Damage" version="1.0">
+              <list send_button="1" type="3">
+                <option selected="1" value="0">Magical Green Club +3: [2d6+18]</option>
+                <option selected="1" value="0">Rock: [2d8+8]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Magical Club +3" version="1.0">
+              <text multiline="1" send_button="0">The stone giant Sledge carries a powerful magical weapon, one that allows him to escape defeat at the hands of the heroes a green glowing huge greatclub +3. In addition to the weapon's magical bonues', the club has the following powers at a 13th level of ability: dimension door 3x/day, cure serious wounds 3x/day, and protection from arrows 2x/day. The DM should keep track the use of these special powers should the heroes encounter the giant more than once per day. The club requires a Strength of 27 just to lift, and cannot be used by the heroes.
+</text>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #4: Wizard's Cabin" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #4: DM Information" version="1.0">
+          <text multiline="1" send_button="0">#4 Wizard's Cabin
+   This cabin is the home of Riddick the Hermit, a 60 year old retired wizard who wants nothing else than to be left alone. Once a high-wizard of a nearby kingdom, Riddick has retreat to this remote mountain cabin for the rest and relaxation of retirement after years of hard work in the world of man. While he is rather cranky about being left alone, Riddick is not above helping anyone in dire need. He wants nothing to do with Sledge or his forces, for they leave him alone and vice versa.
+
+   Riddick does have one weakness: pretty girls. He does not know about dryad the from Encounter #5, and will be very pleased if the heroes impart that information (she has been hiding from him because she distrusts wizards). If any of the heroes is a human female with a Charisma of 13 or greater, the wizard can possibly be talked into helping them against Sledge. Riddick is a grumpy, but colorful character that can be used as a future NPC for the group. He knows much about Thayer and Hedras, and can impart to them any information about the two presented in this section. He also knows about the pool's curative powers, and of the treant that lives across the pass.
+
+Riddick, Male Human Wiz10/Ftr3: CR 13; Size M (5 ft., 11 in. tall); HD 10d4+10 + 3d10+3; hp 59; Init +1 (+1 Dex); Spd 30 ft.; AC 11 (+1 Dex); Attack +11/+6 (+8 Base, +3 Str) melee, or +9/+4 (+8 Base, +1 Dex) ranged; SV Fort +7, Ref +5, Will +12; AL NG; Str 17 (+3), Dex 13 (+1), Con 13 (+1), Int 19 (+4), Wis 18 (+4), Cha 12 (+1); Skills: Climb +7, Diplomacy +2, Forgery +5, Heal +6.5, Hide +2, Jump +9, Knowledge +17, Knowledge (arcana) +17, Listen +4, Move silently +1, Open lock +6, Profession +16, Ride +7, Scry +17, Search +5, Spellcraft +14, Spot +4, Swim +9, Tumble +1.5; Feats: Combat casting, Craft wondrous item, Dodge, Empower spell, Improved critical (lance, light), Improved unarmed strike, Maximize spell, Quicken spell, [Scribe scroll], Weapon focus (sword, short), Weapon focus (longbow, composite); AL: NG; CR 13
+
+   Spells (4/5/5/4/4/2): 0th: Arcane Mark, Dancing Lights, Daze, Detect Magic, Detect Poison, Disrupt Undead, Flare, Ghost Sound, Light, Mage Hand, Mending, Open/Close, Prestidigitation, Ray of Frost, Read Magic, Resistance.  1st: Detect Secret Doors, Feather Fall, Mage Armor, Magic Missile, Ray of Enfeeblement, Shield, Silent Image, Summon Monster I. 2nd: Alter Self, Blur, Levitate, Resist Elements, Web. 3rd: Dispel Magic, Fireball, Flame Arrow, Fly, Haste, Hold Person, Lightning Bolt, Slow, Summon Monster III. 4th: Arcane Eye, Charm Monster, Fear, Improved Invisibility, Polymorph Other. 5th: Cloudkill, Dominate Person, Leomund's Secret Chest, Summon Monster V, Teleport.
+
+   If asked, Riddick knows nothing about the ruined tower to the west. He looked it over once, and his magic told him nothing was hidden there-though if pressed he mentions that something isn't right about that area.
+
+***NOTE REGARDING RIDDICK***
+Riddick is an NPC that should benefit the party, not threaten it. Should the PCs foolishly attack the hermit wizard he disappears and does nothing to help the party. Befriended he is a great ally, and will help the PCs even beyond this encounter.</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #5: Dryad" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #5: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #5: Dryad Glen
+   Much like Encounter #2, this area consists of a small copse of oak trees, though these are not as old or large as the others (and there is no treant here). This lesser grove is home to a dryad, who will hide from the heroes unless there is an elf or druid in the party-or they attack her trees. Much like the treant the dryad was here during the time of the elves, and has seen many of the trees here put to the axe over the centuries.
+
+   The dryad is kind, but very sad over the condition of the area. She observes the heroes for some time before approaching, trying to determine their allegiance and reason for being here. Since Sledge uses this pass as a gathering area for his raiders, she will be suspicious at first. The dryad wants nothing from the heroes, though she will thank them for defending the pool or her friend the treant. She has been avoiding the hermit wizard, since wielders of magic destroyed many trees in this area long ago. If befriended (i.e. killing Sledge, befriending the treant), she gives the heroes a potion of hiding she has been saving for a special use. The dryad knows nothing about the nearby ruined tower, since it is just beyond her ability to travel from her tree home.
+</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="orc" module="containers" name="Dryad" version="1.0">
+          <nodehandler class="form_handler" icon="form" module="forms" name="Details" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Name" version="1.0">
+              <text multiline="0" send_button="0">Dryad</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Race" version="1.0">
+              <text multiline="0" send_button="0">Dryad</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Speed" version="1.0">
+              <text multiline="0" send_button="0">Walk 30'</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="AC: Total / Touch / Flat Footed" version="1.0">
+              <text multiline="0" send_button="0">12 / 12 / 10</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Face / Reach" version="1.0">
+              <text multiline="0" send_button="0">5 ft. by 5 ft./5</text>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Abilities" version="1.0">
+              <grid autosize="1" border="1">
+                <row version="1.0">
+                  <cell>STR</cell>
+                  <cell>10</cell>
+                  <cell>+0</cell>
+                </row>
+                <row version="1.0">
+                  <cell>DEX</cell>
+                  <cell>15</cell>
+                  <cell>+2</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CON</cell>
+                  <cell>11</cell>
+                  <cell>+0</cell>
+                </row>
+                <row version="1.0">
+                  <cell>INT</cell>
+                  <cell>14</cell>
+                  <cell>+2</cell>
+                </row>
+                <row version="1.0">
+                  <cell>WIS</cell>
+                  <cell>15</cell>
+                  <cell>+2</cell>
+                </row>
+                <row version="1.0">
+                  <cell>CHA</cell>
+                  <cell>18</cell>
+                  <cell>+4</cell>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Feats" version="1.0">
+              <text multiline="1" send_button="0">Alertness
+Dodge
+Improved Initiative</text>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Skills" version="1.0">
+              <grid autosize="1" border="0">
+                <row version="1.0">
+                  <cell>Skill</cell>
+                  <cell>Total</cell>
+                  <cell>Rnk</cell>
+                  <cell>Stat</cell>
+                  <cell>Msc</cell>
+                </row>
+                <row version="1.0">
+                  <cell>Animal Empathy</cell>
+                  <cell>13</cell>
+                  <cell>9.0</cell>
+                  <cell>4</cell>
+                  <cell>0</cell>
+                </row>
+                <row version="1.0">
+                  <cell>Knowledge (Nature)</cell>
+                  <cell>8</cell>
+                  <cell>6.0</cell>
+                  <cell>2</cell>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Escape Artist</cell>
+                  <cell>9</cell>
+                  <cell>7.0</cell>
+                  <cell>2</cell>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Hide</cell>
+                  <cell>9</cell>
+                  <cell>7.0</cell>
+                  <cell>2</cell>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Listen</cell>
+                  <cell>11</cell>
+                  <cell>9.0</cell>
+                  <cell>2</cell>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Move Silently</cell>
+                  <cell>9</cell>
+                  <cell>7.0</cell>
+                  <cell>2</cell>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Sense Motive</cell>
+                  <cell>9</cell>
+                  <cell>7.0</cell>
+                  <cell>2</cell>
+                  <cell/>
+                </row>
+                <row version="1.0">
+                  <cell>Spot</cell>
+                  <cell/>
+                  <cell>9.0</cell>
+                  <cell>2</cell>
+                  <cell/>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Special Abilities" version="1.0">
+              <text multiline="1" send_button="0">Dryads speak Common, Elven, and Sylvan.
+
+Combat
+
+Spell-Like Abilities: Dryads can communicate with plants at will (as speak with plants). They can also, at will, step inside any tree and use dimension door as cast by a 7th-level sorcerer to reach their own oak tree. A dryad can use charm person three times per day, as cast by a 4th-level sorcerer; targets must succeed at a Will save (DC 15) or be charmed for 4 hours.
+
+Symbiosis (Su): Each dryad is mystically bound to a single, enormous oak tree and must never stray more than 300 yards from it. Any who do become ill and die within 4d6 hours. A dryads oak does not radiate magic.
+
+</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Combat" version="1.0">
+            <form height="600" width="400"/>
+            <nodehandler class="textctrl_handler" icon="d20" module="forms" name="Initiative" version="1.0">
+              <text multiline="0" send_button="1">[1d20+2]</text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Abilities" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">STR [1d20+0]</option>
+                <option selected="0" value="0">DEX [1d20+2]</option>
+                <option selected="0" value="0">CON [1d20+0]</option>
+                <option selected="0" value="0">INT [1d20+2]</option>
+                <option selected="0" value="0">WIS [1d20+2]</option>
+                <option selected="0" value="0">CHA [1d20+4]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Saving Throws" version="1.0">
+              <list send_button="1" type="0">
+                <option selected="0" value="0">Fortitude [+0+1d20]</option>
+                <option selected="0" value="0">Reflex [+5+1d20]</option>
+                <option selected="1" value="0">Will [+1+1d20]</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Current HP (out of 9)" version="1.0">
+              <text multiline="0" send_button="0"></text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="Attacks" version="1.0">
+              <list send_button="1" type="3">
+                <option selected="1" value="0">Dagger: [1d20+1] / Damage: [1d4]</option>
+              </list>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="d20" module="containers" name="Encounter #6: Ruined Treasure Tower" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: DM Information" version="1.0">
+          <text multiline="1" send_button="0">Encounter #6: Ruined Treasure Tower
+   This tower was once an elven repository for treasure and magic, which despite appearances is still functional. A large boulder near the center of the shattered tower walls (Search DC 20 to find) covers a marble platform encircled with ancient elvish ruins. The ruins read, &quot;An elven traveler without knowledge is a bird without wings.&quot; This phrase is written in an old elven tongue (Knowledge (+ elven language) vs. DC 20 to read properly), and if read while standing on the platform teleports one person 100 yards beneath the surface to a secret treasure vault. Only one person can transport at any time, no one else can until the first returns.
+
+   Hidden in the vault is the following: 1351sp, 2,451gp, 783pp, scroll: levitate, displacement, blindness (arcane, caster level 5), potion of speak with animals, an elven masterwork longsword +2, oil of timelessness, full plate +2 (medium sized), and a staff of fire w/34 charges, and a bag of holding (type 4).
+
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: Treasure (DM)" version="1.0">
+          <text multiline="1" send_button="0">1351sp
+2,451gp
+783pp
+scroll: levitate, displacement, blindness (arcane, caster level 5)
+potion of speak with animals
+elven masterwork longsword +2
+oil of timelessness
+full plate +2 (medium sized)
+staff of fire w/34 charges
+bag of holding (type 4).
+
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Encounter #6: Treasure (Players)" version="1.0">
+          <text multiline="1" send_button="0">1351sp
+2,451gp
+783pp
+scroll: levitate, displacement, blindness (arcane, caster level 5)
+potion
+elven masterwork longsword
+vile of oil
+full plate (medium size)
+staff
+bag</text>
+        </nodehandler>
+      </nodehandler>
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="link_handler" icon="compass" module="forms" name="Enchanted Forest Mountain Pass Map" version="1.0">
+        <link href="http://www.openrpg.com/orpgnuke/modules/My_eGallery/gallery/maps/Woodland/tq2f.jpg"/>
+      </nodehandler>
+    </nodehandler>
+    <group_atts border="1" cols="1"/>
+  </nodehandler>
+  <group_atts border="1" cols="1"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/Idiots_guide_to_openrpg.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,267 @@
+
+<nodehandler class="static_handler" module="core" name="Idiot's Guide, 0.9.4" status="useful">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="text_handler" icon="note" module="core" name="Table of contents:">&lt;a href=&quot;#c1&quot;&gt;Chapter 1:&lt;/a&gt; Chatting up them OpenRPGers
+&lt;a href=&quot;#c2&quot;&gt;Chapter 2:&lt;/a&gt; Rolling those dice and whispering those sweet nothings
+&lt;a href=&quot;#c3&quot;&gt;Chapter 3:&lt;/a&gt; Creating your own Character sheet
+&lt;a href=&quot;#c4&quot;&gt;Chapter 4:&lt;/a&gt; Opening your own room and sharing your character with all
+&lt;a href=&quot;#c5&quot;&gt;Chapter 5:&lt;/a&gt; Advanced stuffs (like pictures)
+&lt;a href=&quot;#c6&quot;&gt;Chapter 6:&lt;/a&gt; Setting up your own server</nodehandler>
+  <nodehandler class="text_handler" icon="note" module="core" name="Introduction">Welcome, Mateys, to the online gaming universe of the OpenRPG!  By now you have successfully downloaded and installed the program or you would not be able to read this document.  But we are not here to discuss what you already know, we are here to get around to telling you how to do those things you don't know.
+
+&lt;b&gt;What we'll cover in this guide:&lt;/b&gt;
+1)How to use the basic OpenRPG
+2)How to do those really nifty advanced things in OpenRPG
+3)How to get your brother to do those nifty things in OpenRPG while you beat him with a whip
+4)Getting along with those others on the OpenRPG program
+
+And, before long, before you know it, and before that chicken in the microwave is done, you will know and have mastered the art of using OpenRPG.</nodehandler>
+  <nodehandler class="static_handler" module="core" name="Chapter 1:OpenRPG Basics">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="text_handler" icon="note" module="core" name="Chapter 1:  OpenRPG and you">&lt;a name=&quot;c1&quot;&gt;&lt;/a&gt;
+Alright.  Let us start at the beginning.  Many many years ago the dinosaurs roamed the planet.  But then an asteroid, now most commonly known as &quot;Bill Gates 1,&quot; crashed into the earth and wiped them all out, turning them into oil.  The basic upshot of all that is with this oil we have created electricity which is now running the program.
+
+Before we start it is nessessary to make a name for yourself.  Why?  Well, you wouldn't like to be going around with everybody knowing only as 'blankman' would you?  To type in your name, go up to the top left window, and dragging donw the &quot;OpenRPG&quot; menu bar press &quot;settings&quot;.  in there you can alter your name and colours.  Whenever you wish to change your name, you must first disconnect from any server (We'll get to servers and conenction next paragraph)) then reconnect before the change occurs.
+
+You can also rename yourself in a much more simple way by typing in the command &quot;/name &quot; and your name behind it in the chat window.  I just made you go into the settings so that you know where they are later.
+
+Now that you have yourself labled, you are probably wondering &quot;where's all the chat?&quot;  Well, my friend, we are here to answer that.  As your program booted up, it should have brought up a window marked &quot;OpenRPG&quot; in the top left, a &quot;Player list&quot; in the bottom left, a map on the top right, and a chat screen on the lower right.  We will first concern outselves with getting that lower right window to do the work and play with the others later.
+
+To get the chat window to work, first we actually need a place to chat.  To do this, we need to browse the list of rooms on what is known as the &lt;i&gt;Tracker&lt;/i&gt;.  To open the tracker window go to the &quot;OpenRPG&quot; window and under the menu &quot;Game Server&quot; click on &quot;Browse Tracker&quot;
+
+As you can see, a new window popped up.   This is the &lt;i&gt;Tracker Window&lt;/i&gt;.  On the left you will see a list of various servers running OpenRPG online.  Click on the one at the top of the list and press &quot;Connect&quot;.
+
+After pressing the &quot;Connect&quot; button you will join that server and be instantly dropped into it's lobby.  Welcome to your first chatroom.  Feel free to stay in this room as long as you like or move off to another room listed on the Tracker.  When you first turned on OpenRPG the chat window filled up with all the different chat commands available.  If you are like me and have forgotten them by now you can either click on the little text entry box and press &lt;i&gt;Page Up&lt;/i&gt;   or type in &lt;i&gt;/help&lt;/i&gt;.  Should you wish a way to scroll up instead of 'page up'ing you can increase the amount of lines the Chat Window stores by making the &lt;i&gt;Buffer Size&lt;/i&gt; larger.  Either do this in the settings window or in the chatwindow itself and it will be saved for the next time you use OpenRPG.
+
+By now I'm sure you are also wondering what that &lt;i&gt;status&lt;/i&gt; thingy in the bottom left window is.  If you notice, every time you type a message in the chat window your status changes.  And when you finish your msg (or sit for 5 seconds waiting) it changes back to Idle.  What a clever invention.  Of course, if you wish to set your own status to override &quot;Idle&quot; temporarily, just type in the chat window &quot;/status My_Status&quot;; replacing &quot;My_Status&quot; for whatever msg you wish (no more than 13 letters can be seen though).
+
+As for now, spend some time and enjoy yourself in the lobby... we can come back to the tutorial when you are ready to learn about Rolling dice, whispering, and creating and using character sheets.</nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="Chapter 2: Dice Rolling and Whispering">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="text_handler" icon="note" module="core" name="Ah, you're back!">&lt;/a name=&quot;c2&quot;&gt;&lt;/a&gt;
+I'm astonished.  82% of all people who use this program never bother to read the tutorial, let alone come back to it.  Then again 67% of all statistics are made up on the spot so we'll leave that for now and get on with what we are doing
+
+Now, to get back to business.  By now you've hopefully held a conversation with someone (or at least yourself) in our chatroom and now I'm sure you're wondering &quot;How can I roll some dice so I can smite those foolish mortals.&quot;  Well, don't be discouraged, because that is what I'm here for.
+
+To roll dice you have two options.  you can simply press the button on the dice toolbar  or you can type in how many times you wish to roll.  As pressing buttons is mostly self explanitory, this part will only cover typing in dicerolls.
+
+First we will choose how many dice we wish to roll at once.  To say how many dice you wish to roll, just type in the number ((say.. 1))... so it would look like this:
+
+1
+
+now tell the type of dice you wish to roll.  to do this you write a 'd' ((d for dice)) and the number of sides that dice has.  Say I want to roll a 20 sided dice that one time it would look like this:
+
+1d20
+
+Now put those square brackets around it.  They are the ones directly to the right of the &quot;P&quot; key.  ((for quick reference on what I mean, find chapter two in this guide, right click-edit it, and look at how I have the dice written in the text blocks))
+
+[1d20]
+
+hope I rolled well.  I know it looks like those round brackets above the '9' and '0' keys but to type it in you need to use square brackets between the &quot;P&quot; and &quot;\&quot; keys.  All you do is type that in the into the chat text box and it will automatically roll it for you.  You could even do combinations of dice or simple mathmatics inside the dice macro as well...
+
+[4d20+1d10]
+
+[1d8-4]
+
+[6d4+3]
+
+[1d10*10]
+
+[(1d10+5)+(6d4-3)-12]
+
+Now that you have gotten the basics of the dice roll down, let us move on to some more advanced stuff.  What I'm talking about is mainly for Whitewolf players, but good to know anyway.
+
+let's take a dice roll, shall we.  Let's roll 5d10:
+
+[5d10]
+
+as you can see, I used a lowercase &quot;d&quot; and it came up with each dice roll and the sum of all of them.  Let's change that by capitalising the &quot;D&quot; shall we:
+
+[5D10]
+
+As you can see, instead of giving us a sum of all the dice, it instead told us what each dice had rolled.  This is useful if you want individual dice rolls shown rather than a final result.
+
+Now that we have that basis down, we are going to set up the dice macro so that it compares how many dice have gotten over a number you tell it.  you'll see what I mean in this demonstration:
+
+[5D10 vs 7]
+
+All that was added was the capital &quot;D&quot; and add the words &quot;vs 7&quot;.  Of course, you can change the number to whatever you wish.  You will also notice, when you use it, that it will automatically detract the result for every '1' you roll on the dice.  So if you rolled 5D10 and got a 7, an 8, a 9, and two '1's with a &quot;vs 7&quot; you would only have one success.
+
+Now for those of you who play AEG's Legend of the 5 Rings RPG (and possibly their other games), your probably wondering how do I handle rolling for L5R. No worries there, we've got you covered. The syntax is the same as in the rule book. Say we want to roll 5 dice and keep only 2, we type that in as:
+
+[5k2]
+
+This will roll 5d10 and keep the highest 2 dice, and will handle the re-rolling of any 10's. But what about all those unskilled and similar skill rolls you need to make, where you can't re-roll the 10's. It only takes a small change to the previous roll. We just tack on a 'u' to the end, like so:
+
+[5k2u]
+
+
+</nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;Need to tell that special someone that you like them but don't want the room to hear?&lt;/u&gt;&lt;/b&gt;">Well, the designer of OpenRPG thought of such eventualities, and has provided us with the ability to whisper at will to whoever is in the room.  Doing so is quite easy in it's own way, and we'll show you here.
+
+All that is required is for you to type in a &lt;i&gt;/w&lt;/i&gt; then the person's name ((Caps are important!!!)), an equals sign (( = )) and then your message... so it would probably look something like this:
+
+&lt;i&gt;/w Monkeyman=&lt;/i&gt;I think that Woody person that is usually in the Lobby is soo cute
+
+&lt;i&gt;/w Susan=&lt;/i&gt;yeah, but I hear he likes bananas so I won't go near him
+
+And there we go.  But I can hear your cries &quot;What about those people that have extrodinarily annoying names?&quot;  Well, do not fret... just go over to your player list in the lower lefthand corner and right click on the little heads next to their name.  Magically, one of those menu bars will appear and give you the option to whisper to them.  Press it and watch as the '/w' command comes up in the chat text box.  How nice!
+
+And should you wish to whisper to more than one person, just add each name after the initial '/w' and put commas between them, like so:
+
+&lt;i&gt;/w Susan, Monkeyman, Thatotherguy=&lt;/i&gt;Vote Woody for Overlord 2001!!!
+
+simple as that. You could even whisper dice the exact same way.
+
+Stay tuned for the next chapter where we get into how to create, use, and abuse your own character sheets.</nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="Chapter 3: Nodes and Character Sheets">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;The Basics&lt;/b&gt;">&lt;a name=&quot;c3&quot;&gt;&lt;/a&gt;
+alright.. by now you have grasped the basics on how to chat, so we will move on to the next phase: character sheets.  This part of the tutorial will cover how to make a full usable document from the nodes.
+
+Let us first start by describing what the different nodes are and where we can find them:</nodehandler>
+    <nodehandler class="static_handler" module="core" name="The Nodes">
+      <group_atts border="1" cols="4"/>
+      <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;the Group Node&lt;/u&gt;&lt;/b&gt;">as we look at the various nodes they all appear to be stored in a blue bottled icon called &quot;Wizards&quot;.  Wizards itself is a group node and it shows the function of the 'group': to store and combine all the nodes which would otherwise just float randomly on the Gametree.
+
+When we make a group, we do so so that we can store our information into one well organized array rather than a collection of scattered files.</nodehandler>
+      <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;Text Block&lt;/u&gt;&lt;/b&gt;">The text block is where we will be storing most of our text information.  It allows you to type in words, paragraphs, or entire pages of information you want to keep on your character sheet without the needless bother of grids.  If you look carefully you will notice that the Dice Macro box is also just another Text Box but with some dice in it.  You are fully able to put any dice on here in any place in the Text Body and when the character sheet is opened you will see that the dice has been rolled.</nodehandler>
+      <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;The Grid&lt;/u&gt;&lt;/b&gt;">Now, text boxes are all well and good for storing words and paragraphs, but for easy storage of numbers that we use for reference there is nothing better than a Grid.  The grid allows us to set up any manner of chart or graph to for quick reference and study.</nodehandler>
+      <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;The Macro Node&lt;/u&gt;&lt;/b&gt;">The Macro Node works just like a normal text box node, but instead of seeing it in it's own window, everything that is typed into it is broadcast directly into the chat window as if you were saying it.  This includes actions, whispers, status changes, etc.  Good if you want to change names or perform series of speeches and actions in order just by doubleclicking on a node.  We won't be getting to know them in this, but they are pretty self explanitory.  Just doubleclick to use them and right click to edit.</nodehandler>
+    </nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;Pick your Nodes and Eat It Too&lt;/u&gt;&lt;/b&gt;">Now that you know what the various nodes are, I'm sure you're asking &quot;how do I use then then?&quot;  Well, let us start with the main node, the Group.
+
+To create a group doubleclick on the &quot;New Group&quot; Node in the Wizards tree.  you will see that a new &quot;Group&quot; has appeared at the top in the main Gametree.  Open the new group by doubleclicking on it.
+
+.........
+
+Oh my... all rather empty, isn't it?  maybe we should fill it up with something.  Let's make a text box to put in that void, shall we?  Alright.   Go into the Wizards directory again and doubleclick on the &quot;New Text Block&quot;.  Again you will see a Text Block has appeared at the top in the gametree.  Doubleclick on it to see what's inside.
+
+.........
+
+again it all seems very flat.  I don't like that text in there so let's change it.  this time rightclick on the Text Block and press &quot;Edit&quot;.  Voila!!  You will see an 'edit' box where you can change the title and the text body, with lots of little extras down below.  Go ahead and type in a new title and message for yourself.
+
+Now that you have added your personal message to the world it is time to add it to your group.  Close down the Text Box Editor (it automatically saves, so just hit the 'x' button) and look up to your gametree again.  Once you see it, grab your new edited Text Box and drag it into the group that you created earlier.  Now doubleclick on the group and marvel at your creation.  Congratulations on making your first character sheet.
+
+**Note.  To edit everything in a group at once just right click on the group and press Edit.**</nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;That's It?!?&lt;/u&gt;&lt;/b&gt;">&quot;That's it?!  I want my money back!  In fact, since it was free, I want you to give me money to compensate for having to read this long tutorial just for that!!!&quot;  Is what I'm sure you're thinking right now.  But don't worry, our character sheets will be getting much more complex and much better looking from this point out.
+
+Turning back to our creation, we have a Group with a single Text box inside it.  Now, I like that message you have typed in there, so let's double it's voice across the program.  Right click on the text box inside the group you created, and press &quot;clone&quot;.  Voila again!!!  You'll see an exact copy of your message has appeared at the top of the Gametree.  Grab that Text box and Drag it onto the Group as well.  Now open up your group by doubleclicking on it and take a look again at what you have created.
+
+.....
+
+Hhhmmm.... It's all very well and dandy, but it's going to get a little long, isn't it.  If each item we put in there is going to be lined up straight down this thing is going to be pages in length.  Well, luckily for you, we have a way around that.  closing down your group window let us look up to the Gametree again.  This time, instead of doubleclicking on the Group, right click on it.  You'll see it brings up the same menubar as what the textbox had.  Don't worry, we'll get to play with all the nifty features later.  Go down and press edit.
+
+A little edit box should have popped up with Three Options.  The first is to rename the Group, the second dictates how many columns we can have (1-4), and the third just asks if we want borders.  Lets first change the name of our group to &quot;Lagmonkey's Group&quot;.  Then, lets change the 'Columns&quot; from one to two then close down the edit box.  Open up the Group again and look now as it appears the two text messages are side by side.  How convenient!  In fact, as you saw you can have up to four things side by side at the same time.  Any further Nodes inside the group ((the 5th one and beyond)) will just get put in one of the columns under the first four, in order.</nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;Ah, but will she swallow it, Stevie Wonder?&lt;/u&gt;&lt;/b&gt;">Now that we have our simple group with it's two columns and it's text boxes, it's time to add a Grid, I think.  create a grid the same way as we did the Text Box and the group, then go in and edit it.  What we want is a Grid that looks exactly like this:</nodehandler>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Grid">
+      <grid border="1">
+        <row>
+          <cell>I</cell>
+          <cell>Really</cell>
+          <cell>Love</cell>
+          <cell>Bananas</cell>
+          <cell>Matey!</cell>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name=" ">Be sure to add three extra columns for the words by pressing the &quot;Add Columns&quot; button.  Oh, and get rid of that bottom row, we don't need it right now (though, if you wish, you are welcome to put numbers in the bottom row instead of removing it for practice if you like.)
+
+Now that we have our grid, let's drag it onto our group and open up our group, shall we?  Oh my.. the Bananas are overlapping, aren't they?  It appears the column isn't wide enough to support a grid so wide.  But I like bananas, so we are going to have to keep the grid.  What we need would be a nice large column to hold it though, so let's make one.
+
+Go into the Wizards again and create another new group.  Now drag the Grid over onto the new group, then picku p and drag &quot;Lagmonkey's Group' onto the new group.  yes, matey, a group within a group.  Open it up and see what the result is.  Interesting.  It appears that the grid and the two columns are all inside the new group's main column.  We could continue to stack columns into each other into infinity, though it would get increasingly hard to read.
+
+Getting back, you can now see the grid is on top and Lagmonkey's group is on the bottom.  I don't like that arrangement so let's change it.  Close down the New Group box then on the gametree grab the grid and drag it onto the new group's icon once again.  You'll see that that had the effect of re-adding the grid to the new group, this time placing it at the bottom.  Whenever you add something to a group, it always appears at the bottom.</nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;&lt;u&gt;That's it!&lt;/u&gt;&lt;/b&gt;">That's basically it for making character sheets.  Feel free to experiment with various combinations of items and settings.. add and remove borders to see what you like.. create grids of varous sizes and shapes and stick them into your columns...perhaps put some dice in those text boxes of yours ((with the appropriate &quot;[3d6]&quot;  around them))... but most of all: Have fun!
+
+**Note: Be sure to save your character sheet if you want to.  To do so, right click on it and press &quot;Save Node&quot;**</nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name="Moving Characters from almost ANY Character Generator to here.">The following are stpes on how to move your character sheets from your favorite Character Generation program into OpenRPG.  It's very quick and clean cut.
+
+1) first, using your character generator, you convert your character to either txt format, or, preferably (if you have the ability) HTML format.
+
+2) Then, you go to wizards.put a text block into the gametree
+
+3) if you are using txt, select all, and copy.  if you are using HTML format, edit it using notepad, but don't change any of the HTML Program. then select all and copy
+
+4) now, going back into OpenRPG, right click on the text block you created and push 'edit'
+
+5) clear the text block of the few words of text that appear there then paste the copied info into by either rightclicking-paste or pressing 'ctrl v'
+
+6) then name your character in the Title then close the text block.  Finished
+
+no more need to create character sheets!  That simple and any time you want to see it, just doubleclick on the text node.</nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="Chapter 4: Setting up rooms/games and Ignore">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="text_handler" icon="note" module="core" name="There's a party going on down my street tonight...">&lt;a name=&quot;c4&quot;&gt;&lt;/a&gt;
+now that you have your custom built char sheet and you have talked enough players into starting a game, it's time to set up your own room.  Let's bring up that Tracker window again ((under the Game Server menu)).  Looking to the bottom left of it, you'll see a little box that allows you to type in your own room name and add a password if you like.  Let's start a room called &quot;Working on that darned fun tutorial&quot; and not put a password up.
+
+as you see the room you create will be exactly the same as the lobby, so there is nothing to worry about.  Lets load up our character sheet by right clicking on the &quot;Game Tree&quot; and pressing Insert file&quot;
+
+Now that we have our little char sheet we might as well show the world.  This can be done in one of three ways.  The first way is to right click on it and send it to other players (provided there are any).  This is how other players can get your sheet as they can't see it until you send it to them.
+
+The second way is to send it directly to the chat and let everyone see it there.  go ahead and do that by right clicking on the char sheet and pressing &quot;Send to Chat&quot;.  Now everyone gets to see it as it appears on their text chat window.
+
+the third option, Whisper to Player, is much the same as &quot;Send to chat&quot; only it sends it to a specific person or persons to see in their chat instead.
+
+Of course, there sometimes comes a day when there is a little spammonkey running about.  You know the type.  Just keeps askin pointless questions, hitting you with the same text, and generally making himself a nuesance.  Well fear not!  We have added in a brand new feature that allows you to ignore those unscrupulous people.  In the chat entry box just enter the command &quot;/i player_ID#&quot; and that will put a person on ignore (or toggle them back to un-ignore).  To get a list of ID's who are on your ignore list, just type &quot;/i&quot; alone, with nothing else.  You can even ignore/un-ignore multiple people at once, just put commas (( , )) between the names.  And that's it.  Ignore at your leasure.</nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="Chapter 5: Maps and Minis">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="text_handler" icon="note" module="core" name="So you had to ask...">&lt;a name=&quot;c5&quot;&gt;&lt;/a&gt;
+Now comes the hardest part of the tutorial.. describing how to use minis and pictures.  Well, the first step is to bring up the map.  With any luck it is still open so let's find it.  It'll be the big green one that was in the top right of the screen.  If it's not there, click on the &quot;Windows&quot; menu under the OpenRPG main window and press &quot;Map Window&quot;.
+
+&lt;/center&gt;**note!** If you are in the lobby, the changes you make to the map will not be seen by other players.  You will need to create another room to have a shared map.&lt;/center&gt;
+
+In this map window you'll see all of the basic operations needed to load an image and edit the map.  Let us attempt to load our first picture.  At the bottom of the map window you'll see a large box where you can type in text.  This is where you put  the Webpage Address for the image you wish.  Let's type in this addy.
+
+http://www.openrpg.com/images/mins/amazon.gif
+
+after you have typed it in press the &quot;add minature&quot; button, sit back, and watch as it loads the beautiful amazonian woman (well, almost) into the map folder
+
+&lt;center&gt;&lt;b&gt;&lt;u&gt;OpenRPG tip:&lt;/u&gt;&lt;/b&gt; All images need to be on webpages... you cannot load directly from your hard drive.  The reason being this is actually one giant web browser.&lt;/center&gt;
+
+Feel free to move it about, get a feel for how it works.  You'll see at the moment that the amazon seems to hop from grid square to grid square.  Well... if you're like me, you don't like being confined to grids or rules.  So let's get rid of that grid, shall we?
+
+Looking at the map window again, you will see a little red diamond thing.  They say it is a compass but it just looks like a flying fish to me.  Anyway.. click on that and it will bring up all the map settings.  In there you can change the size of the map, what colour it is, or even load up a background on which all the mini's sit ((say, a dungeon map you drew or perhaps that picture of Britney Spears I know you have lying around)).  The thing we are interested the most, though, will be the grid settings at the bottom.  As you can see, you can change the size of the grid and switch the grid from square(4 sided) to hex (6 sided).  You'll also be able to let mini's either abide by the grid, or become free from the black lined prison.  Let us turn off the grid snap and press &quot;apply.&quot;
+
+Now, go back into the map window and try moving your mini again.  You'll see that it now moves freely, and ignores those lines.  But ignoring them is not enough, did I hear you say?  You want them gone?  Ok.  Let's go back into the settings (the red star button) and this time change the grid size to zero.  Pressing 'apply' you will see that the grid has disappeared entirely from map.  Course, if you want it back, just go put in the size again (50 is default).
+
+There are other options (like direction pointing for your mini's) so feel free to play around a bit.
+
+&lt;center&gt;&lt;b&gt;&lt;u&gt;Note:&lt;/u&gt;&lt;/b&gt; you can find many great mini's on the OpenRPG webpage.&lt;/center&gt;</nodehandler>
+    <nodehandler class="link_handler" icon="html" module="core" name="&lt;center&gt;www.OpenRPG.com&lt;/center&gt;">
+      <link href="http://www.OpenRPG.com"/>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="Chapter 6: Make your fortune in Server Control!">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="text_handler" icon="note" module="core" name="&lt;b&gt;Well, maybe not any money...&lt;/b&gt;">&lt;a name=&quot;c6&quot;&gt;&lt;/a&gt;
+Well, maybe not any money in creating a server on this program, but there are definately perks to running one.  But to run one we must first understand what they do.
+
+A server is the mother for all of us on OpenRPG.  It runs all of our major commmands, keeps us all talking together, and hosts all the games on OpenRPG.  When you first connected to OpenRPG you had to choose a server from a list in the tracker, on the left.  Now let us put your name (or at least your computer's) out there so others can flock to you.
+
+For this we must return to your OS.  Go into your computer, into the OpenRPG folder.  In there, you will see a file marked:
+
+mplay_server.py
+
+You can also find it in your start menu, right next to the OpenRPG program itself.  Once you find it, (double)click on it.  The first thing that will come up is a python MSDOS window... It will first prompt on if you wish for your server to bee seen by the OpenRPG tracker list.  Press &quot;Y&quot; and hit enter (won't get anyone if we don't know it exists).  Next, it will prompt you for a name for your Server.  Write in &quot;Temporary Server&quot; and press enter.
+
+Now the window will start running the various server-y looking bits of code.  as soon as it is up and going, you will told the various commands that are available to the server.  The first one will be &quot;kill&quot;.  this is what you type into the MSDOS window to shut the server down.  &lt;b&gt;**IMPORTANT**&lt;/b&gt; if you shut down the server all rooms on it will close.  You don't want to shut it down if others are using it.
+
+The second command is the &quot;dump&quot; command.  This will list all the people on your server, as well as their ID number.  It really isn't important at this time but it will come in handy later.
+
+Broadcast is self explanitory.  The 'Announce' and 'Remove' features allow you to choose later on if your server can be seen on the tracker, or not.  Dump Groups gets the same info as Dump but does Groups instead of people.  And finally you can bring up this little list of commands at any time by typing 'help' or '?'.
+
+You can type these commands in any time in the python server window.  Go ahead and try 'help' or 'dump' and see what comes up.
+
+Alright!  That's it.  to access your server, run you OpenRPG program as normal (with the server MSDOS window in the background) and log onto your server as we had shown you in chapter 1.  This will come in handy if there are no Servers running and you wish to use OpenRPG with, perhaps your gaming buddies or maybe some underworld kingpins you need to 'have a talk' with.
+
+And From All of us at OpenRPG HQ, we wish you good gaming!</nodehandler>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/MiniatureLibrary.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,183 @@
+
+<nodehandler class="minilib_handler" icon="gear" module="minilib" name="Fantasy &amp; Might Miniatures Library">
+  <miniature name="bearblack" unique="N" url="http://members1.chello.nl/~t.wong/add/animals/bearblack.gif"/>
+  <miniature name="bearbrown" url="http://members1.chello.nl/~t.wong/add/animals/bearbrown.gif"/>
+  <miniature name="bear" url="http://members1.chello.nl/~t.wong/add/monsters/bear.gif"/>
+  <miniature name="cow" url="http://members1.chello.nl/~t.wong/add/animals/cow.gif"/>
+  <miniature name="giantcrab" url="http://members1.chello.nl/~t.wong/add/monsters/giantcrab.gif"/>
+  <miniature name="FireBeetle" url="http://www.openrpg.com/images/mins/FireBeetle.gif"/>
+  <miniature name="LargeSpider" url="http://www.openrpg.com/images/mins/LargeSpider.gif"/>
+  <miniature name="lion" url="http://members1.chello.nl/~t.wong/add/animals/lion.gif"/>
+  <miniature name="lion2" url="http://members1.chello.nl/~t.wong/add/animals/lion2.gif"/>
+  <miniature name="scorpion" url="http://members1.chello.nl/~t.wong/add/monsters2/scorpion.gif"/>
+  <miniature name="sheep" url="http://members1.chello.nl/~t.wong/add/animals/sheep.gif"/>
+  <miniature name="giantspider" url="http://members1.chello.nl/~t.wong/add/monsters/giantspider.gif"/>
+  <miniature name="spiderwraith" url="http://members1.chello.nl/~t.wong/add/monsters2/spiderwraith.gif"/>
+  <miniature name="WolfDire" url="http://www.openrpg.com/images/mins/WolfDire.gif"/>
+  <miniature name="wilderniss" url="http://members1.chello.nl/~t.wong/add/maps/wilderniss.jpg"/>
+  <miniature name="bricktile" url="http://www.openrpg.com/images/maps/bricktile.gif"/>
+  <miniature name="bridge" url="http://members1.chello.nl/~t.wong/add/maps/bridge.jpg"/>
+  <miniature name="cavern" url="http://members1.chello.nl/~t.wong/add/maps/cavern.jpg"/>
+  <miniature name="firecamp" url="http://members1.chello.nl/~t.wong/add/maps/firecamp.jpg"/>
+  <miniature name="mainbg" url="http://www.planescape-torment.com/images/mainbg.gif"/>
+  <miniature name="clubthug" url="http://members1.chello.nl/~t.wong/add/add/clubthug.gif"/>
+  <miniature name="grognard" url="http://home.t-online.de/home/nimbul/grognard.gif"/>
+  <miniature name="bugbear" url="http://members1.chello.nl/~t.wong/add/monsters/bugbear.gif"/>
+  <miniature name="bullywug" url="http://members1.chello.nl/~t.wong/add/monsters/bullywug.gif"/>
+  <miniature name="dopplegangerlesser" url="http://members1.chello.nl/~t.wong/add/monsters/dopplegangerlesser.gif"/>
+  <miniature name="dopplegangergreater" url="http://members1.chello.nl/~t.wong/add/monsters/dopplegangergreater.gif"/>
+  <miniature name="ettin" url="http://members1.chello.nl/~t.wong/add/monsters/ettin.gif"/>
+  <miniature name="FrostGiant" url="http://www.openrpg.com/images/mins/FrostGiant.gif"/>
+  <miniature name="gibberling" url="http://members1.chello.nl/~t.wong/add/monsters/gibberling.gif"/>
+  <miniature name="gnollnormal" url="http://members1.chello.nl/~t.wong/add/monsters/gnollnormal.gif"/>
+  <miniature name="grimlock" url="http://members1.chello.nl/~t.wong/add/monsters/grimlock.gif"/>
+  <miniature name="hobgoblin" url="http://members1.chello.nl/~t.wong/add/monsters/hobgoblin.gif"/>
+  <miniature name="hobgoblin2" url="http://members1.chello.nl/~t.wong/add/monsters/hobgoblin2.gif"/>
+  <miniature name="kobold" url="http://www.openrpg.com/images/mins/kobold.gif"/>
+  <miniature name="kobold" url="http://members1.chello.nl/~t.wong/add/monsters/kobold.gif"/>
+  <miniature name="kuotoa" url="http://members1.chello.nl/~t.wong/add/monsters/kuotoa.gif"/>
+  <miniature name="lizardman" url="http://members1.chello.nl/~t.wong/add/monsters2/lizardman.gif"/>
+  <miniature name="mindflayer" url="http://members1.chello.nl/~t.wong/add/monsters2/mindflayer.gif"/>
+  <miniature name="MindFlayerfin" url="http://www.openrpg.com/images/mins/MindFlayerfin.gif"/>
+  <miniature name="Ogre" url="http://www.openrpg.com/images/mins/Ogre.gif"/>
+  <miniature name="ogreberserker" url="http://members1.chello.nl/~t.wong/add/monsters2/ogreberserker.gif"/>
+  <miniature name="ogrillon" url="http://members1.chello.nl/~t.wong/add/monsters2/ogrillon.gif"/>
+  <miniature name="troglodyte" url="http://members1.chello.nl/~t.wong/add/monsters2/troglodyte.gif"/>
+  <miniature name="troll" url="http://www.openrpg.com/images/mins/troll.gif"/>
+  <miniature name="troll" url="http://members1.chello.nl/~t.wong/add/monsters2/troll.gif"/>
+  <miniature name="bookstand" url="http://members1.chello.nl/~t.wong/add/item/bookstand.gif"/>
+  <miniature name="chest" url="http://www.openrpg.com/images/mins/chest.gif"/>
+  <miniature name="open chest" url="http://www.coolarchive.com/icons/miscA72.gif"/>
+  <miniature name="corpse" url="http://www.openrpg.com/images/mins/corpse.gif"/>
+  <miniature name="CrystalBall" url="http://www.openrpg.com/images/mins/CrystalBall.gif"/>
+  <miniature name="Gems" url="http://www.coolarchive.com/icons/miscA73.gif"/>
+  <miniature name="Globe" url="http://www.openrpg.com/images/mins/Globe.gif"/>
+  <miniature name="headstone" url="http://www.openrpg.com/images/mins/headstone.gif"/>
+  <miniature name="oillamp" url="http://members1.chello.nl/~t.wong/add/item/oillamp.gif"/>
+  <miniature name="large_coffer" url="http://www.openrpg.com/images/mins/large_coffer.gif"/>
+  <miniature name="Mirror" url="http://www.openrpg.com/images/mins/Mirror.gif"/>
+  <miniature name="mirror" url="http://members1.chello.nl/~t.wong/add/item/mirror.gif"/>
+  <miniature name="moneybag" url="http://www.coolarchive.com/icons/miscA96.gif"/>
+  <miniature name="old_crate" url="http://www.openrpg.com/images/mins/old_crate.gif"/>
+  <miniature name="potion2" url="http://members1.chello.nl/~t.wong/add/item/potion2.gif"/>
+  <miniature name="potion_table" url="http://www.openrpg.com/images/mins/potion_table.gif"/>
+  <miniature name="throne" url="http://members1.chello.nl/~t.wong/add/item/throne.gif"/>
+  <miniature name="diningtable" url="http://members1.chello.nl/~t.wong/add/item/diningtable.gif"/>
+  <miniature name="Table" url="http://www.openrpg.com/images/mins/Table.gif"/>
+  <miniature name="throne" url="http://members1.chello.nl/~t.wong/add/item/throne.gif"/>
+  <miniature name="treasure" url="http://www.openrpg.com/images/mins/treasure.gif"/>
+  <miniature name="druid" url="http://www.openrpg.com/images/mins/druid.gif"/>
+  <miniature name="female_illusionist" url="http://www.openrpg.com/images/mins/female_illusionist.gif"/>
+  <miniature name="Illusionist" url="http://www.openrpg.com/images/mins/Illusionist.gif"/>
+  <miniature name="magician" url="http://www.openrpg.com/images/mins/magician.gif"/>
+  <miniature name="Priest" url="http://www.openrpg.com/images/mins/Priest.gif"/>
+  <miniature name="ruby_warlock" url="http://www.openrpg.com/images/mins/ruby_warlock.gif"/>
+  <miniature name="sage" url="http://members1.chello.nl/~t.wong/add/add/sage.gif"/>
+  <miniature name="Wizard" url="http://www.openrpg.com/images/mins/Wizard.gif"/>
+  <miniature name="wizardelfmale" url="http://members1.chello.nl/~t.wong/add/add/wizardelfmale.gif"/>
+  <miniature name="wizardfemale" url="http://members1.chello.nl/~t.wong/add/add/wizardfemale.gif"/>
+  <miniature name="wizardmale" url="http://members1.chello.nl/~t.wong/add/add/wizardmale.gif"/>
+  <miniature name="wizardmale5" url="http://members1.chello.nl/~t.wong/add/add/wizardmale5.gif"/>
+  <miniature name="wizardmale2" url="http://members1.chello.nl/~t.wong/add/add/wizardmale2.gif"/>
+  <miniature name="wizardmale4" url="http://members1.chello.nl/~t.wong/add/add/wizardmale4.gif"/>
+  <miniature name="amazon" url="http://www.openrpg.com/images/mins/amazon.gif"/>
+  <miniature name="warriorfemale5" unique="N" url="http://members1.chello.nl/~t.wong/add/add/warriorfemale5.gif"/>
+  <miniature name="femalewarrior" url="http://members1.chello.nl/~t.wong/add/add/femalewarrior.gif"/>
+  <miniature name="warriormale6" url="http://members1.chello.nl/~t.wong/add/add/warriormale6.gif"/>
+  <miniature name="beetlegiant" url="http://members1.chello.nl/~t.wong/add/monsters/beetlegiant.gif"/>
+  <miniature name="beholder" url="http://members1.chello.nl/~t.wong/add/monsters/beholder.gif"/>
+  <miniature name="AirElemental" url="http://www.openrpg.com/images/mins/AirElemental.gif"/>
+  <miniature name="ankheg" url="http://members1.chello.nl/~t.wong/add/monsters/ankheg.gif"/>
+  <miniature name="ankheggreater" url="http://members1.chello.nl/~t.wong/add/monsters/ankheggreater.gif"/>
+  <miniature name="arch_fiend" url="http://www.openrpg.com/images/mins/arch_fiend.gif"/>
+  <miniature name="aurumvorax" url="http://members1.chello.nl/~t.wong/add/monsters/aurumvorax.gif"/>
+  <miniature name="basilisk" url="http://members1.chello.nl/~t.wong/add/monsters/basilisk.gif"/>
+  <miniature name="bulette" url="http://members1.chello.nl/~t.wong/add/monsters/bulette.gif"/>
+  <miniature name="bulette3e" url="http://members1.chello.nl/~t.wong/add/monsters/bulette3e.gif"/>
+  <miniature name="CarrionCrawler" url="http://www.openrpg.com/images/mins/CarrionCrawler.gif"/>
+  <miniature name="carrioncrawler2" url="http://members1.chello.nl/~t.wong/add/monsters/carrioncrawler2.gif"/>
+  <miniature name="chimera" url="http://members1.chello.nl/~t.wong/add/monsters/chimera.gif"/>
+  <miniature name="cryptcrawler" url="http://members1.chello.nl/~t.wong/add/monsters/cryptcrawler.gif"/>
+  <miniature name="djini" url="http://www.openrpg.com/images/mins/djini.gif"/>
+  <miniature name="dragon" url="http://members1.chello.nl/~t.wong/add/monsters/dragon.gif"/>
+  <miniature name="dragonred" url="http://members1.chello.nl/~t.wong/add/monsters/dragonred.gif"/>
+  <miniature name="EarthElemental" url="http://www.openrpg.com/images/mins/EarthElemental.gif"/>
+  <miniature name="ettercap" url="http://members1.chello.nl/~t.wong/add/monsters/ettercap.gif"/>
+  <miniature name="FireElemental" url="http://www.openrpg.com/images/mins/FireElemental.gif"/>
+  <miniature name="floatinghorror" url="http://members1.chello.nl/~t.wong/add/monsters/floatinghorror.gif"/>
+  <miniature name="GOLEM" url="http://www.openrpg.com/images/mins/GOLEM.gif"/>
+  <miniature name="irongolem" url="http://members1.chello.nl/~t.wong/add/monsters/irongolem.gif"/>
+  <miniature name="green_slime" url="http://www.openrpg.com/images/mins/green_slime.gif"/>
+  <miniature name="harpy" url="http://members1.chello.nl/~t.wong/add/monsters/harpy.gif"/>
+  <miniature name="ixitxachit" url="http://members1.chello.nl/~t.wong/add/monsters/ixitxachit.gif"/>
+  <miniature name="medusa" url="http://members1.chello.nl/~t.wong/add/monsters2/medusa.gif"/>
+  <miniature name="mudman" url="http://members1.chello.nl/~t.wong/add/monsters2/mudman.gif"/>
+  <miniature name="nightmare" url="http://www.openrpg.com/images/mins/nightmare.gif"/>
+  <miniature name="grayooze" url="http://members1.chello.nl/~t.wong/add/monsters/grayooze.gif"/>
+  <miniature name="otyugh" url="http://members1.chello.nl/~t.wong/add/monsters2/otyugh.gif"/>
+  <miniature name="owlbear" url="http://members1.chello.nl/~t.wong/add/monsters2/owlbear.gif"/>
+  <miniature name="pegasus" url="http://members1.chello.nl/~t.wong/add/monsters2/pegasus.gif"/>
+  <miniature name="pit_fiend" url="http://www.openrpg.com/images/mins/pit_fiend.gif"/>
+  <miniature name="Rust-Monsterfin" unique="N" url="http://www.openrpg.com/images/mins/Rust-Monsterfin.gif"/>
+  <miniature name="shamblingmound" url="http://members1.chello.nl/~t.wong/add/monsters2/shamblingmound.gif"/>
+  <miniature name="treant" url="http://members1.chello.nl/~t.wong/add/monsters2/treant.gif"/>
+  <miniature name="UmberHulk" url="http://www.openrpg.com/images/mins/UmberHulk.gif"/>
+  <miniature name="WaterElemental" url="http://www.openrpg.com/images/mins/WaterElemental.gif"/>
+  <miniature name="will_o_wisp" url="http://www.openrpg.com/images/mins/will_o_wisp.gif"/>
+  <miniature name="worm" url="http://members1.chello.nl/~t.wong/add/monsters2/worm.gif"/>
+  <miniature name="wormsyoung" url="http://members1.chello.nl/~t.wong/add/monsters2/wormsyoung.gif"/>
+  <miniature name="wyvern" url="http://www.openrpg.com/images/mins/wyvern.gif"/>
+  <miniature name="yeti" url="http://www.openrpg.com/images/mins/yeti.gif"/>
+  <miniature name="farmer" url="http://members1.chello.nl/~t.wong/add/add/farmer.gif"/>
+  <miniature name="royal_lady" url="http://www.openrpg.com/images/mins/royal_lady.gif"/>
+  <miniature name="Assassin" url="http://www.openrpg.com/images/mins/Assassin.gif"/>
+  <miniature name="sirine" url="http://members1.chello.nl/~t.wong/add/monsters2/sirine.gif"/>
+  <miniature name="ranger" url="http://members1.chello.nl/~t.wong/add/add/ranger.gif"/>
+  <miniature name="Soldier" url="http://www.openrpg.com/images/mins/Soldier.gif"/>
+  <miniature name="Thief" url="http://www.openrpg.com/images/mins/Thief.gif"/>
+  <miniature name="rockpile" url="http://members1.chello.nl/~t.wong/add/item/rockpile.gif"/>
+  <miniature name="stonerunes" url="http://members1.chello.nl/~t.wong/add/item/stonerunes.gif"/>
+  <miniature name="stairew" url="http://www.openrpg.com/images/mins/stairew.gif"/>
+  <miniature name="stairns" url="http://www.openrpg.com/images/mins/stairns.gif"/>
+  <miniature name="red_flame" url="http://www.openrpg.com/images/mins/red_flame.gif"/>
+  <miniature name="Door" url="http://www.openrpg.com/images/mins/Door.gif"/>
+  <miniature name="well" url="http://members1.chello.nl/~t.wong/add/item/well.gif"/>
+  <miniature name="tree5" url="http://members1.chello.nl/~t.wong/add/item/tree5.gif"/>
+  <miniature name="treereaper" url="http://members1.chello.nl/~t.wong/add/item/treereaper.gif"/>
+  <miniature name="tree" url="http://members1.chello.nl/~t.wong/add/item/tree.gif"/>
+  <miniature name="tree2" url="http://members1.chello.nl/~t.wong/add/item/tree2.gif"/>
+  <miniature name="tree3" url="http://members1.chello.nl/~t.wong/add/item/tree3.gif"/>
+  <miniature name="tree4" url="http://members1.chello.nl/~t.wong/add/item/tree4.gif"/>
+  <miniature name="treetrunk" url="http://members1.chello.nl/~t.wong/add/item/treetrunk.gif"/>
+  <miniature name="banshee" url="http://members1.chello.nl/~t.wong/add/monsters/banshee.gif"/>
+  <miniature name="ghast" url="http://members1.chello.nl/~t.wong/add/monsters/ghast.gif"/>
+  <miniature name="ghost" url="http://www.openrpg.com/images/mins/ghost.gif"/>
+  <miniature name="ghoul" url="http://members1.chello.nl/~t.wong/add/monsters/ghoul.gif"/>
+  <miniature name="ghoulgreater" url="http://members1.chello.nl/~t.wong/add/monsters/ghoulgreater.gif"/>
+  <miniature name="lich" url="http://www.openrpg.com/images/mins/lich.gif"/>
+  <miniature name="lich2" url="http://members1.chello.nl/~t.wong/add/monsters2/lich2.gif"/>
+  <miniature name="revenant" url="http://members1.chello.nl/~t.wong/add/monsters2/revenant.gif"/>
+  <miniature name="skeleton" url="http://members1.chello.nl/~t.wong/add/monsters2/skeleton.gif"/>
+  <miniature name="wingedsnake" url="http://members1.chello.nl/~t.wong/add/monsters2/wingedsnake.gif"/>
+  <miniature name="spectre" url="http://www.openrpg.com/images/mins/spectre.gif"/>
+  <miniature name="wraith" url="http://members1.chello.nl/~t.wong/add/monsters2/wraith.gif"/>
+  <miniature name="zombie" url="http://www.openrpg.com/images/mins/zombie.gif"/>
+  <miniature name="Bandit" url="http://www.openrpg.com/images/mins/Bandit.gif"/>
+  <miniature name="bowman" url="http://members1.chello.nl/~t.wong/add/add/bowman.gif"/>
+  <miniature name="fighterchain-sword" url="http://www.openrpg.com/images/mins/fighterchain-sword.gif"/>
+  <miniature name="dwarf2" url="http://www.openrpg.com/images/mins/dwarf2.gif"/>
+  <miniature name="dwarf_fighter" url="http://www.openrpg.com/images/mins/dwarf_fighter.gif"/>
+  <miniature name="Dwarf_w_axe" url="http://www.openrpg.com/images/mins/Dwarf_w_axe.gif"/>
+  <miniature name="sophan" url="http://home.t-online.de/home/nimbul/images/sophan.gif"/>
+  <miniature name="knight" url="http://www.openrpg.com/images/mins/knight.gif"/>
+  <miniature name="WAmisc7" url="http://www.coolarchive.com/icons/WAmisc7.gif"/>
+  <miniature name="angantir" url="http://home.t-online.de/home/nimbul/images/angantir.gif"/>
+  <miniature name="warriorfemale2" url="http://members1.chello.nl/~t.wong/add/add/warriorfemale2.gif"/>
+  <miniature name="warriorfemale3" url="http://members1.chello.nl/~t.wong/add/add/warriorfemale3.gif"/>
+  <miniature name="warriormale5" url="http://members1.chello.nl/~t.wong/add/add/warriormale5.gif"/>
+  <miniature name="warriormale3" url="http://members1.chello.nl/~t.wong/add/add/warriormale3.gif"/>
+  <miniature name="warriormale7" url="http://members1.chello.nl/~t.wong/add/add/warriormale7.gif"/>
+  <miniature name="warriorshield-flail" url="http://www.openrpg.com/images/mins/warriorshield-flail.gif"/>
+  <miniature name="warriorwsword" url="http://www.openrpg.com/images/mins/warriorwsword.gif"/>
+  <miniature/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/StarWars_d20character.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,120 @@
+<nodehandler class="SWd20char_handler" icon="knight" module="StarWarsd20" name="Star Wars Character" version="1.0">
+    <howtouse version="1.0">
+        <howto>
+To use this you do:
+
+To use this you just need to add all the stuff you wish, set your stats ect, then click on the
++(plus) next too the gears, and right click. This will cause it to roll what is needed to roll
+
+In the attaks section Base2 - Base6 are for additional attacks at a diff base then your first
+If you set these, it will automaticly roll the additional attacks for each base you set when
+you right click on the weapon you wish to attack with.
+
+If you are having problems selecting spells, please verify that your class level is high enough
+to use the spell you want to add, and that the spell is in the list.
+</howto>
+    </howtouse>
+    <general version="1.0">
+        <name>Player Name</name>
+        <player>Your Name</player>
+        <race>none</race>
+        <size acmodifier="0">none</size>
+        <height>none</height>
+        <weight>none</weight>
+        <age>none</age>
+        <gender>none</gender>
+        <eyes>none</eyes>
+        <hair>none</hair>
+        <speed>30</speed>
+        <currentxp>0</currentxp>
+        <xptolevel>1000</xptolevel>
+    </general>
+    <classes level="0" version="1.0"/>
+    <abilities version="1.0">
+        <stat abbr="Str" base="0" name="Strength"/>
+        <stat abbr="Dex" base="0" name="Dexterity"/>
+        <stat abbr="Con" base="0" name="Constitution"/>
+        <stat abbr="Int" base="0" name="Intelligence"/>
+        <stat abbr="Wis" base="0" name="Wisdom"/>
+        <stat abbr="Cha" base="0" name="Charisma"/>
+    </abilities>
+    <saves version="1.0">
+        <save base="0" magmod="0" miscmod="0" name="Fortitude" stat="Con"/>
+        <save base="0" magmod="0" miscmod="0" name="Reflex" stat="Dex"/>
+        <save base="0" magmod="0" miscmod="0" name="Will" stat="Wis"/>
+    </saves>
+    <inventory version="1.0">
+        <credits>0</credits>
+    </inventory>
+    <skills version="1.0">
+        <skill armorcheck="0" crossclass="0" misc="0" name="Affect Mind" rank="0" stat="Cha" untrained="1" feat="Alter"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Alchemy" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Appraise" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Astrogate" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Balance" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Battlemind" rank="0" stat="Con" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Bluff" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Climb" rank="0" stat="Str" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Computer Use" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Control Mind" rank="0" stat="Cha" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Craft" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Demolitions" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Diplomacy" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Disable Device" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Disguise" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Drain Energy" rank="0" stat="Con" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Drain Knowledge" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Empathy" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Enhance Ability" rank="0" stat="Con" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Enhance Senses" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Entertain" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Escape Artist" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Farseeing" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Fear" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Force Defense" rank="0" stat="Con" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Force Grip" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Force Lightning" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Force Stealth" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Force Strike" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Forgery" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Friendship" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Gamble" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Gather Information" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Handle Animal" rank="0" stat="Cha" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Heal Another" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Heal Self" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Hide" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Illusion" rank="0" stat="Cha" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Intimidate" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Jump" rank="0" stat="Str" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Listen" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Move Object" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Move Silently" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Pilot" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Profession" rank="0" stat="Wis" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Read/Write Language" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Repair" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Ride" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Search" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="See Force" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Sense Motive" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Sleight of Hand" rank="0" stat="Dex" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Speak Language" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Spot" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Survival" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Swim" rank="0" stat="Str" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Telepathy" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Transfer Essence" rank="0" stat="Cha" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Treat Injury" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Tumble" rank="0" stat="Dex" untrained="0"/>
+    </skills>
+    <feats version="1.0"/>
+    <wp current="0" max="0" version="1.0"/>
+    <vp current="0" max="0" version="1.0"/>
+    <attacks version="1.0">
+        <melee base="0" fifth="0" forth="0" misc="0" second="0" sixth="0" third="0"/>
+        <ranged base="0" fifth="0" forth="0" misc="0" second="0" sixth="0" third="0"/>
+    </attacks>
+    <ac misc="" natural="" version="1.0"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/Userguide098.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,287 @@
+
+<nodehandler class="tabber_handler" icon="book" module="containers" name="User's Guide to OpenRPG 0.9.8" status="useful" version="1.0">
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Introduction" version="1.0">
+    <text multiline="1" send_button="0">Welcome to the online gaming universe of the OpenRPG!  By now you have successfully downloaded and installed the program or you would not be able to read this document.  But we are not here to discuss what you already know, but to tell you how to do those things you don't know.
+
+What we'll cover in this guide:
+1)How to use the basic OpenRPG
+2)How to do those really nifty advanced things in OpenRPG
+3)How to get your brother to do those nifty things in OpenRPG while you beat him with a whip
+4)Getting along with those others on the OpenRPG program
+
+And, before long, before you know it, and before that chicken in the microwave is done, you will know and have mastered the art of using OpenRPG.</text>
+  </nodehandler>
+  <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Guide Chapters" version="1.0">
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Art Of The Chat" version="1.0">
+      <text multiline="1" send_button="0">The basics:
+
+Alright.  Let us start at the beginning.  Many many years ago the dinosaurs roamed the planet.  But then an asteroid, now most commonly known as &quot;Bill Gates 1,&quot; crashed into the earth and wiped them all out, turning them into oil.  The basic upshot of all that is with this oil we have created electricity which is now running the program.
+
+Before we start it is necessary to make a name for yourself.  Why?  Well, you wouldn't like to be going around with everybody knowing only as 'blankman' would you?  To type in your name, go up to the top left window, and dragging down the &quot;OpenRPG&quot; menu bar press &quot;Settings&quot;.  in there you can alter your name and colours.
+
+You can also rename yourself in a much more simple way by typing in the command &quot;/name &quot; and your name behind it in the chat window.  I just made you go into the settings so that you know where they are later.
+
+--------------------------------------------------------------------------------
+
+Now that you have yourself labled, you are probably wondering &quot;where's all the chat?&quot;  Well, my friend, we are here to answer that.  As your program booted up, it should have brought up a four-window layout.  The Gametree Window is in the top-left, the Player Window is the bottom-left, The Chat Window is the bottom-right, and the Map Window is the top-right.  We will first concern outselves with getting that lower right window to do the work and play with the others later.
+
+To get the chat window to work, first we actually need a place to chat.  To do this, we need to browse the list of rooms on what is known as the Tracker.  To open the tracker window go to the menubar on the top and under the menu &quot;Game Server&quot; click on &quot;Browse Tracker&quot;
+
+As you can see, a new window popped up.   This is the Tracker Window.  On the left you will see a list of various servers running OpenRPG online.  Click on the one at the top of the list and press &quot;Connect&quot;.
+
+After pressing the &quot;Connect&quot; button you will join that server and be instantly dropped into it's lobby.  Welcome to your first chatroom.  Feel free to stay in this room as long as you like or move off to another room listed on the Tracker.  When you first turned on OpenRPG the chat window filled up with all the different chat commands available.  If you are like me and have forgotten them by now you can either click on the text display box and press PageUp   or click on the little text entry box and type in &quot;/help&quot;.
+
+Should you wish a way to scroll up instead of 'PageUp'ing you can increase the amount of lines the Chat Window stores by making the Buffer Size larger.  You can do this by changing the &quot;buffersize&quot; setting in the Settings menu or by using /lines [number] replacing [number] with the number of lines..  A good setting is 100-200 lines though you can have it as large as you want (though it is warned against having it larger than 1000 as large numbers tend to increase lag.)
+
+------------------------------------------------------------------------------
+
+By now I'm sure you are also wondering what that status thingy in the bottom left window is.  If you notice, every time you type a message in the chat window your status changes.  And when you finish your msg (or sit for 5 seconds waiting) it changes back to Idle.  What a clever invention.  Of course, if you wish to set your own status to override &quot;Idle&quot; temporarily, just type in the chat window &quot;/status My_Status&quot;; replacing &quot;My_Status&quot; for whatever msg you wish (no more than 18 letters can be seen though).
+
+As for now, spend some time and enjoy yourself in the lobby... we can come back to the tutorial when you are ready to learn about Rolling dice, whispering, and creating and using character sheets.</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Rolling Dice" version="1.0">
+      <text multiline="1" send_button="0">Dice:
+
+there are two methods of rolling dice.  The first way is very simple: press the bottons on the dice toolbar.  The only explination there is on the left there is a box saying how many dice to roll, and on the right there is a box for modifiers.
+
+The second way is a little more advanced, and powerful.  You can also roll dice by typing them into your text intry box under the dice toolbar (where you type your normal chat messages).  to do so, first choose how many dice you want to roll.  example:
+
+1
+
+now, put the letter &quot;d&quot; after that.
+
+1d
+
+then put the dice type (number of sides):
+
+1d20
+
+And to finish it off, put the square [ ] brackets around the dice and hit enter
+
+[1d20]
+
+you'll see it rolls a dice into the chat window.  Magic. we can even do things like having multiple types of dice inside it in various mathmatical combinations:
+
+[1d20+4d6-12]
+[3d8+2]
+[1d1-1]
+
+
+------------------------------------------------------------------------
+
+ now we let the fun begin.  Inside the square brackets we can also add special modifiers.  it'll come in the following format:
+
+[ 1d20.mod(#) ]
+
+you can even have multiple modifiers if you wish.  the list of all of them are below:
+
+.ascending()
+lists your dicerolls from lowest to highest
+
+.descending()
+lists your dicerolls from highest to lowest
+
+.takeHighest(#)
+takes the highest # amount of rolls
+
+.takeLowest(#)
+takes the lowest # amount of rolls
+
+.extra(#)
+rolls 1 extra dice for every Original roll that gets above the #
+
+.open(#)
+rolls an extra dice for Every roll that gets above the #
+
+.minroll(#)
+no dice will roll below #
+
+.each(#)
+adds # to each diceroll
+</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Whispering and Ignoring" version="1.0">
+      <text multiline="1" send_button="0">Of course, not everything should be heard.  This is true twice over in this chapter, as we will be discussing how to whisper and how to ignore.
+
+Whispering is a very simple task in OpenRPG.  First type in &quot;/w &quot; in the text entry box.  next, type in the ID number of the person you wish to whisper to and add an equals sign, so it looks like this:  &quot;/w 4=&quot;.  Add your message to the end of that and hit enter.
+
+/w 4=Hello ID number 4.
+
+You can even have multiple ID numbers in there.   Just seperate them up with a comma between each one, like so:
+
+/w 3, 4, 12=Hey, I dare you to poke that Woody
+
+Of course, if you don't want to type out the weird ID numbers you can always just right click on those little Easter Island Heads next to the person's name in the bottom left player window.  You can then select &quot;whisper&quot; and it will put the whisper command into your text entry box automatically.
+
+-------------------------------------------------------------------------------
+
+Sometimes there is someone that will just really get on your nerves.  Be it that they keep sending the same line of text to the chat, or maybe that they are spouting out Britney Spears lyrics nonstop.  Either way, you'll probably want to hear less of them.
+
+Well fear not!  for by just typing in &quot;/i their_ID_#&quot; (changing &quot;their_ID_#&quot; for their ID number) and hitting Enter, then they will be added to your ignore list and no further messages will be heard by them.  Should you wish to take them off again (in case you go the wrong guy) just type the same thing and it'll toggle it off.  You can also get a list of who's on your ignore list just by typing in &quot;/i&quot;
+
+Simple as that.</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Create Your Own Character" version="1.0">
+      <text multiline="1" send_button="0">((before we begin, see the character sheet repository:
+   http://openrpg.wrathof.com
+for pre-made character sheets.))
+
+
+Character Sheet creation (By Melanthos):
+
+So you wanna make a character sheet, huh? Well, follow these few short steps and you'll be on your way to Sheet Creation Godhood...or not. But don't blame me if you don't read this and can't make yourself a sheet. I'm going to guess you have a basic understanding of how the nodes work. I'll be using Harvester Incorporated(TM)'s Faction Wars(TM) as an example.
+
+First, you'll need a tabber. You can find it under containers in the templates node.  Click the + to the left of templates then click the plus next to containers.  Next double click the Create New Tabber.  Got it? Good. Now, you'll need a form. Forms are what we use to create each page of the tabber. They hold the rest of the nodes neatly, and open up - unlike groups, which cannot be opened in a sheet. To create a form double click on Create New Form.  You will notice two new nodes at the top of the game tree.
+
+
+        Tabbers, Forms, and Text-boxes, OH MY!
+
+Alright. Here is where I store the basics of the character. Since I don't like to scroll too much, I've broken down the information even more, so that I can fit into smaller tabbers and forms. First I'll create a second tabber. This will hold all the basic info - Level, Class, Player &amp; Character name, and a few other things. Since I've got 8 things I want to list, I've broken them up into 2 seperate groups, which have been put into different forms: General Info, and Specific Info. For simple things like name and level, I suggest using single-line text boxes. I've gone ahead and made 8 of these, naming each of them one of my 8 items and stuck them into their forms. Now that I've finished that, I'll put the two forms under my second tabber, and put that into my big form, &quot;General.&quot; I've done the same thing with the Attributes. Most of the sheet uses this principal, so I won't go into each form. Kinda redundant.
+
+
+                   List Boxes &amp; Die Creation
+
+Alright, for my second form, Vitals, I need some die-rollers. Since I don't need to change the rolls often, I've decided to use List Boxes to hold all my rolls. It's not too hard, and it's a really easy way to store rolls(It can be used for other things, but this is the best use I've found.): Now you don't have to type out those dice every time you wanna attack. First, I've created myself a List Box. Next, I'll decide how I want my list box to be shown: There are four choices. Drop Down, List Box, Radio Box, and Check List. Since I only need to roll one at a time, I've chosen Drop Down. But to show you how the Drop Down and Check List options work, I'll change things a bit and make a List Box and a Check List as well. When editing, make sure to click the &quot;Send Button&quot; box.
+
+
+        Drop Down
+
+The Drop Down is the least space- consuming of the List Box options. It's just like a single-line Text Box, except it holds more than one line. Click the arrow on the right to make it drop down, and click the die you want to roll. Then hit the Send button, and POOF! Your die has been rolled to the chat window.
+
+
+        Check List
+
+The Check List is a neat option. It lets you roll more than one die at a time. All you have to do is check the box next to the die (or dice) you want to roll, and POOF! Your dice have been rolled. Neat, eh?
+
+           Splitter
+
+Now what could these be used for, you wonder? Not much, but there are a few things. I've used it to hold my different abilities. It's easier for me to just see my choices than to flip between tabbers or scroll down and back up every few seconds.
+
+
+The rest is pretty easy. Just keep on with the things you've done so far, and in no time you'll be on your way to creating great sheets! Most of sheet creation is your personal preference. If you ask Woody, he might say something completely different than I might. Of course, you should make the sheets the way you want them. This is just to get you started. After you've got the basics of sheet making down, you can play around with sheets and nodes and things until you've got the sheet perfect for you. GOOD LUCK!!
+
+
+--Melanthos, Founder &amp; Game Designer - Harvester Incorporated</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Running Your Own Server" version="1.0">
+      <text multiline="1" send_button="0">Creating a server:
+
+OpenRPG has come a long way since the dark ages of gaming.  With this new version we can now create servers on-the-fly!  No more black boxes or strange voodoo magic.  Just click on your &quot;Game Server&quot; menu, and select &quot;Start new server.&quot;  Fill in the options and away you go!  the server will appear instantly on the list.
+
+Of course, this won't provide you with all the same powers that the full server does.  While you can create a server on the short term, if you want to have a longterm dedicated server running which you can control, you'll want to run the OpenRPG Server in your start menu-programs-openrpg (or mplay_server.py in your openrpg folder).
+
+-------------------------------------------------------------------
+
+So now that you have your server, your custom character sheet, and have talked enough players into starting a game, you'll want to set up a private room so that you aren't disturbed by every wandering weirdo or Woody that happens upon your server (and be forwarned, you will find that various people will wander from server to server.  Do not be frightened, most of them are harmless).
+
+So, lets pull up your tracker window.  If you haven't done so already, be sure to connect to your server.  Once in there look onto the right side of the tracker window.  You will see some boxes marked &quot;Room name&quot;, &quot;Boot password&quot;, and if you check a little box, &quot;password&quot;.  the first two are rather self explanatory, and the last one is to lock your room in case you don't want to be bothered.  Be sure to tell your players what it is though.
+
+If you don't password protect a room you may get people wandering in looking for a game or just lurking watching your game.  If you don't want them in there you should first let them know and ask them to leave.  If they refuse your request then boot them.  It is very rude to boot someone from a non passworded room with out warning.
+</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Of Maps and Miniatures" version="1.0">
+      <text multiline="1" send_button="0">The Map:
+
+Now comes the hardest part of the tutorial.. describing how to use miniatures (minis)  and background images.  Well, the first step is to bring up the map.  Fortunately the new OpenRPG now loads maps differently than previous versions.  Likewise, it no longer seems to cause crashes for Windows users.  No longer will your client lag, lock up, or crash when loading images, large or small.  The entire map will now load in the background while you continue with your normal activity with the program.  Additionally, images, once loaded, will also be cached (stored) in RAM (up to 64; overflow is handled by randomly purging images within the cache to make room for new images).  That means switching back and forth between maps, in the same session, will result in instantaneous access to your map; no more waiting.  Furthermore, not only are your images loaded in the background, but they are loaded concurrently.  That means your map will be available faster than ever!  Hopefully these new innovations will lead to a renewed usage of the map.  Even modem users have indicated that using maps are now fun again!  Anyway, on with the tutorial.
+
+The Map Window is where most of your image viewing will reside.  In it you can manipulate miniatures and maps to give your players a well good idea of what your gaming world looks like.  In case you haven't guessed, it's the top-right window.  Looking at the Map Window, you'll see all of the basic operations needed to load an image and edit the map.  Let us attempt to load our first picture.  At the bottom of the map window you'll see a large box where you can type in text.  This is where you put  the Web-page Address for the image you wish.  Let's type in this URL/URI:  http://www.openrpg.com/images/mins/amazon.gif.  Notice that there are many great miniatures ready for you to use at OpenRPG's home-page.
+
+After you have typed it in press the &quot;add miniature&quot; button, sit back, and watch as it loads the beautiful amazonian woman (well, almost) into the map window.  Please note, however, all images need to be on web-pages and served up by a web server... you cannot load directly from your hard drive.  Well, you can, but no one else will be able to see it.  The reason being is because OpenRPG's client actually one giant web browser.
+
+Feel free to move it about, get a feel for how it works.  You'll see at the moment that the amazon seems to hop from grid square to grid square.  Well... if you're like me, you don't like being confined to grids or rules.  So let's get rid of that grid, shall we?  Again, looking at the map window, you will see a little red diamond thing.  They say it is a compass but it just looks like a flying fish to me.  Anyway.. click on that and it will bring up all the map settings.  In there you can change the size of the map, what color it is, or even load up a background on which all the mini's sit ((say, a dungeon map you drew or perhaps that picture of Britney Spears I know you have lying around)).  The thing we are interested in the most, though, will be the grid settings at the bottom.  As you can see, you can change the size of the grid and switch the grid from square(4 sided) to hex (6 sided).  You'll also be able to let mini's either abide by the grid, or become free from the black lined prison.  Let us turn off the &quot;snap to grid&quot; and press &quot;apply&quot;.  Also note that you can also make the grids disappear by making them match the background color, however, this is not the ideal way to do it as it still requires processing to draw and redraw the grid lines, even if they blend in without being visible.
+
+Now, go back into the map window and try moving your mini again.  You'll see that it now moves freely, and ignores those lines.  But ignoring them is not enough, did I hear you say?  You want them gone?  Ok.  Let's go back into the settings (the red star button) and this time change the grid size to zero.  Pressing 'apply' you will see that the grid has disappeared entirely from map.  Course, if you want it back, just go put in the size again (50 is default).  Feel free to make the grid size match your miniature and map scale.  Also, if you prefer to have the snap to grid, however, do not feel that it provides enough resolution in miniature movement, feel free to set the grid size to half or one forth of what your actual scale is.  This way, you miniatures will still fit nicely as well as be able to use the &quot;snap to grid&quot;, however, you'll better be able to stagger your miniatures or slightly offset tiled map sets.
+
+
+Tiled Map Sets:
+
+Often, DM's and GM's have the need to present a map to their players, however, they do not which to reveal the whole map and/or map section to their players.  After all, it does remove a lot of surprise if everything is right before the player's eyes.  Enter three new miniature options: &quot;Lock to back&quot;, &quot;Lock to front&quot;, &quot;Unlock Front/Back&quot;.  Previous to these options being available, either the whole map had to be loaded by changing the background image or players and DM's where forced to struggle when map sections were loaded as if they were miniatures.  Even though there was z-ordering options available, it was still tedious as miniatures would often disappear behind a map image.  Now, entire maps can be built before as game and loaded all at once, without giving away any surprises.
+
+By loading all of your map sections together as miniatures and marking each one visible or not visible, you can have great fun again with your maps!  Wait a minute, isn't this the same boat we were on before.  Well, enter the three new options.  Using the &quot;Lock to back&quot; option on each of your map sections, it will force them to always stay on the bottom of the z-order (stacking) of your miniatures.  That means, you'll now be able to change the z-order of your miniatures while all of them stay on top of your map image.  In other words, while the image is locked to the back of the z-order, it acts just like a background image should.  To restore a miniature to normal z-ordering, simply using the &quot;Unlock Front/Back&quot; option and it will once again be ordered just as any other miniature is.
+
+Wait a minute!  That's only two of the three options that you mentioned.  Well, the third option allows for even more flexibility in what DM's and GM's can present to their players, even on a visible map section.  Enter, the poor-man's fog-of-war.  By creating a &quot;mask&quot; in just about any shape you like (feel free to use transparent colors on gifs as needed), you can effectively lock the &quot;masking&quot; image to the top of the z-order which will hide a section of your visible map.  While this is still somewhat tedious, it will allow for much finer grain of control of what is and is not visible on your maps!
+
+
+Looking here, moving there:
+
+Directional markers have been a part of OpenRPG for some time now, however, there use is going to be better explained here.  If you notice, each miniature has a &quot;facing&quot; and &quot;heading&quot; available to it.  These serves not only to indicate a miniature's facing, but it's current direction of travel.  After all, you're quite able to turn your head and talk to your buddy while walking in a direction different than the one your head is pointed.  While this feature mostly serves board game players, RPGer's may find use in this too, especially when making canned maps.  Imagine, the 'ol evil cleric behind the podium with his back to you.  Now, you can make it clear on the map.  Oh wait, the cleric's minions do not have their back to you, rather, they are standing to his side and looking directly at him.  Closer look, and you see that the cleric too had his head turned to his minions in conversation.  Again, this can be expressed on your map.  Information like this makes it easier for the players to understand that the back-stab that they obviously want to do, won't be so easy to pull off.  All this, without having to verbally describe every little detail.  After all, isn't this what the maps is for?  Enjoy!
+
+
+Saving and Loading Your Map:
+
+Don't forget that you can save your hard work.  Once you have built your map, be sure to right click on the map and hit &quot;Save Map&quot;.  When you need it again, just do &quot;Load Map&quot;.  You don't even need to delete the contents of the previous map.  The client does all that for you!  Don't forget, if you load a previously viewed image, the caching will considerable speed the rate at which it's visible to you and your players.  Using a common set of miniatures helps to take advantage of this fact.
+</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Other Features; Logs, Initiative, Upgrades" version="1.0">
+      <text multiline="1" send_button="0">Other features:
+
+It may seem a little odd that this seventh window is tucked behind some arrows, but I always thought this chapter was a little on the sinful side so over here it now lay.
+
+
+By getting here, you must have had quite a bit of time to read through the rest of the chapters.  Well, let's finish off with what few interesting toys we have left.
+
+To start with, lets talk about logging  To create a logfile, you can just press the disk button in the bottom right corner of the OpenRPG program, and choosing a name and place to save the file.  Badaboom!  a chatlog of everything you have seen is saved.
+
+Of course, most of you will want a way to have it automatically save a log each time it see's text.  To do that, type in &quot;/log to my_log&quot; (replacing &quot;my_log&quot; for what you want to call the file). It should automatically start logging to that filename (and will include the date in the filename you gave it).  if you want to turn it on or off later on, just type &quot;/log on&quot; or &quot;/log off&quot;
+
+Note: The chat logging system no longer appends a datestamp to the logfile
+specified.  This means that for the system to generate the logfile
+
+/foo/bar/gamelog-02-10-01.html for linux or \foo\bar\gamelog-02-10-01.html for windows
+
+Previously the user only had to have &quot;/foo/bar/gamelog&quot; entered as GameLogChatPrefix.
+To get the same logging pattern now, they'll have to set GameLogChatPrefix to
+the following:
+
+/foo/bar/gamelog-%d-%m-%y.html
+
+
+--------------------------------------------------------------------------------
+
+The Initiative Tool.  A marvelous invention allowing DMs to organise and handle an otherwise chaotic scene that we know as combat.  Compatable with all versions of OpenRPG, it works incredibly simply.
+
+All that is needed is for the DM to press &quot;New Initiative&quot; in the tool, have all of the players and the DM to type in &quot;init [diceroll]&quot; (replacing 'diceroll' for your dice) and hit enter.  An example:
+
+init [1d10+6]
+
+once everyone has done that, the DM pulls up his tool again, hits refresh, and then sorts the list however he wants.  once it is sorted, he just hits &quot;send to chat&quot; and the person at the top of the list is then sent to the chat.
+
+Once all the people have been cycled through, it will say &quot;End of Init&quot; in the chat.  The DM just presses the &quot;Start new Initiative&quot; button one more time and away you go on another round of hacking!
+
+Now, if you are the player and not using the tool, you can even put your init command into a macro node (not a text node) and do it that way.  At the time of this publication there was a bug in the initiative code and the DM running the tool cannot use macro nodes for this.
+
+You can also set up your character sheets to be able to be rolled from.  To do it, you will just need a special node that you can find here:
+
+www.geocities.com/woody_j_dick/initiative.xml
+
+You can put it directly into OpenRPG by right clicking on your gametree, pressing &quot;Insert URL&quot; and putting that URL above into the box that appears.  Even the DM can use that node for rolling his initiatives.
+
+CAUTION!  Do not rename the node.  It will not work unless it has that name.
+
+
+The last button, &quot;Keep list&quot; is for Whitewolf players who need to use the list twice.  If you edit the list, and want to keep the edited copy, then just press that and it will save the list so all you need to do is hit refresh to get it back.
+
+--------------------------------------------------------------------------------
+
+OpenRPG is a great program, no doubt about it.  And it's best quality is that it is opensourced, meaning anyone can look at the code and alter it to make it work better.  One such person, the one that compiled this tutorial no less, is going to capitalise on this free advertising to say a few quiet words about his webpage.
+
+www.geocities.com/woody_j_dick/OpenRPG.html
+
+The 8.3 Convention webpage, it offers many 'additional extras' That are not yet included in the main program.  Not only are the upgrades good for the current versions of OpenRPG, but it also retains a list of upgrades for previous versions, in case you want to go back and use that 'good ole' 9.4 or 9.6 client with the modern day servers.
+
+</text>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="form_handler" icon="form" module="forms" name="About" version="1.0">
+    <form height="240" width="400"/>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="OpenRPG" version="1.0">
+      <text multiline="1" send_button="0">OpenRPG is an online chat program designed to let people all around the globe roleplay together.  It supports the use of maps, character sheets, dice, specialised tools, and much more.  We welcome you to the OpenRPG family.</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="This Tutorial" version="1.0">
+      <text multiline="1" send_button="0">This tutorial was written by Woody to replace the older and aging Idiot's Guide.  This tutorial was written to be used for versions 0.9.8a and beyond. If, at any time, you spot a discrepancy between what this tutorial says and how your OpenRPG program actually works, please go onto the OpenRPG forums from www.OpenRPG.com and post your discovery.
+
+A special thanks goes to Harvester Inc., for assisting Woody.  Of course, thanks to the users that provided feedback for the contents contained within.
+</text>
+    </nodehandler>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/Userguide13.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,564 @@
+
+<nodehandler class="tabber_handler" icon="tabber" module="containers" name="User's Manual for OpenRPG 1.3" version="1.0">
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Updates" version="1.0">
+    <text multiline="1" send_button="0">From 1.0.3 version
+
+
+I added a section for the user rolls and moderation, also i updated the section on the map for OpenRPG 1.3. And added an explination for the Text View Option in the other features section.
+
+
+Raburn</text>
+  </nodehandler>
+  <nodehandler class="tabber_handler" icon="questionhead" module="containers" name="Idiot's Guide to OpenRPG v. 1.3" status="useful" version="1.0">
+    <nodehandler class="form_handler" icon="form" module="forms" name="About" version="1.0">
+      <form height="240" width="400"/>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="OpenRPG" version="1.0">
+        <text multiline="1" send_button="0">OpenRPG is an online chat program designed to let people all around the globe roleplay together.  It supports the use of maps, character sheets, dice, specialized tools, and much more.  We welcome you to the OpenRPG family.</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="This Tutorial" version="1.0">
+        <text multiline="1" send_button="0">This Tutorial was created by Woody for use with OpenRPG. It was thrown together and updated for use with 1.3 by Raburn. A special thanks goes to HInc for the material that they made for use in this tutorial.</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Introduction" version="1.0">
+      <text multiline="1" send_button="0">Welcome, Mateys, to the online gaming universe of the OpenRPG!  By now you have successfully downloaded and installed the program or you would not be able to read this document.  But we are not here to discuss what you already know, we are here to get around to telling you how to do those things you don't know.</text>
+    </nodehandler>
+    <nodehandler class="form_handler" icon="form" module="forms" name="Chatting" version="1.0">
+      <form height="400" width="400"/>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Art Of The Chat" version="1.0">
+        <text multiline="1" send_button="0">The basics:
+
+Alright.  Let us start at the beginning.  Many many years ago the dinosaurs roamed the planet.  But then an asteroid, now most commonly known as &quot;Bill Gates 1,&quot; crashed into the earth and wiped them all out, turning them into oil.  The basic upshot of all that is with this oil we have created electricity which is now running the program.
+
+Before we start it is necessary to make a name for yourself.  Why?  Well, you wouldn't like to be going around with everybody knowing only as 'blankman' would you?  To type in your name, go up to the top left window, and dragging down the &quot;OpenRPG&quot; menu bar press &quot;Settings&quot;.  in there you can alter your name and colours.
+
+You can also rename yourself in a much more simple way by typing in the command &quot;/name &quot; and your name behind it in the chat window.  I just made you go into the settings so that you know where they are later.
+
+--------------------------------------------------------------------------------
+
+Now that you have yourself labled, you are probably wondering &quot;where's all the chat?&quot;  Well, my friend, we are here to answer that.  As your program booted up, it should have brought up a four-window layout.  The Gametree Window is in the top-left, the Player Window is the bottom-left, The Chat Window is the bottom-right, and the Map Window is the top-right.  We will first concern outselves with getting that lower right window to do the work and play with the others later.
+
+To get the chat window to work, first we actually need a place to chat.  To do this, we need to browse the list of rooms on what is known as the Tracker.  To open the tracker window go to the menubar on the top and under the menu &quot;Game Server&quot; click on &quot;Browse Tracker&quot;
+
+As you can see, a new window popped up.   This is the Tracker Window.  On the left you will see a list of various servers running OpenRPG online.  Click on the one at the top of the list and press &quot;Connect&quot;.
+
+After pressing the &quot;Connect&quot; button you will join that server and be instantly dropped into it's lobby.  Welcome to your first chatroom.  Feel free to stay in this room as long as you like or move off to another room listed on the Tracker.  When you first turned on OpenRPG the chat window filled up with all the different chat commands available.  If you are like me and have forgotten them by now you can either click on the text display box and press PageUp   or click on the little text entry box and type in &quot;/help&quot;.
+
+Should you wish a way to scroll up instead of 'PageUp'ing you can increase the amount of lines the Chat Window stores by making the Buffer Size larger.  You can do this by changing the &quot;buffersize&quot; setting in the Settings menu or by using /lines [number] replacing [number] with the number of lines..  A good setting is 100-200 lines though you can have it as large as you want (though it is warned against having it larger than 1000 as large numbers tend to increase lag.)
+
+------------------------------------------------------------------------------
+
+By now I'm sure you are also wondering what that status thingy in the bottom left window is.  If you notice, every time you type a message in the chat window your status changes.  And when you finish your msg (or sit for 5 seconds waiting) it changes back to Idle.  What a clever invention.  Of course, if you wish to set your own status to override &quot;Idle&quot; temporarily, just type in the chat window &quot;/status My_Status&quot;; replacing &quot;My_Status&quot; for whatever msg you wish (no more than 18 letters can be seen though).
+
+As for now, spend some time and enjoy yourself in the lobby... we can come back to the tutorial when you are ready to learn about Rolling dice, whispering, and creating and using character sheets.</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="form_handler" icon="form" module="forms" name="Dice Rolling" version="1.0">
+      <form height="400" width="400"/>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Rolling Dice" version="1.0">
+        <text multiline="1" send_button="0">Dice:
+
+I'm astonished.  82% of all people who use this program never bother to read the tutorial, let alone come back to it.  Then again 67% of all statistics are made up on the spot so we'll leave that for now and get on with what we are doing
+
+Now, to get back to business.  By now you've hopefully held a conversation with someone (or at least yourself) in our chatroom and now I'm sure you're wondering &quot;How can I roll some dice so I can smite those foolish mortals.&quot;  Well, don't be discouraged, because that is what I'm here for.
+
+To roll dice you have two options.  you can simply press the button on the dice toolbar  or you can type in how many times you wish to roll.  As pressing buttons is mostly self explanitory, this part will only cover typing in dicerolls.
+
+The second way is a little more advanced, and powerful.  You can also roll dice by typing them into your text intry box under the dice toolbar (where you type your normal chat messages).  to do so, first choose how many dice you want to roll.  example:
+
+1
+
+now, put the letter &quot;d&quot; after that.
+
+1d
+
+then put the dice type (number of sides):
+
+1d20
+
+And to finish it off, put the square [ ] brackets around the dice and hit enter
+
+[1d20]
+
+you'll see it rolls a dice into the chat window.  Magic. we can even do things like having multiple types of dice inside it in various mathmatical combinations:
+
+[1d20+4d6-12]
+[3d8+2]
+[1d1-1]
+
+
+------------------------------------------------------------------------
+
+ now we let the fun begin.  Inside the square brackets we can also add special modifiers.  it'll come in the following format:
+
+[ 1d20.mod(#) ]
+
+you can even have multiple modifiers if you wish.  the list of all of them are below:
+
+.ascending()
+lists your dicerolls from lowest to highest
+
+.descending()
+lists your dicerolls from highest to lowest
+
+.takeHighest(#)
+takes the highest # amount of rolls
+
+.takeLowest(#)
+takes the lowest # amount of rolls
+
+.extra(#)
+rolls 1 extra dice for every Original roll that gets above the #
+
+.open(#)
+rolls an extra dice for Every roll that gets above the #
+
+.minroll(#)
+no dice will roll below #
+
+.each(#)
+adds # to each diceroll
+</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="form_handler" icon="form" module="forms" name="Whispering and Ignoring" version="1.0">
+      <form height="400" width="400"/>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Whispering and Ignoring" version="1.0">
+        <text multiline="1" send_button="0">Of course, not everything should be heard.  This is true twice over in this chapter, as we will be discussing how to whisper and how to ignore.
+
+Whispering is a very simple task in OpenRPG.  First type in &quot;/w &quot; in the text entry box.  next, type in the ID number of the person you wish to whisper to and add an equals sign, so it looks like this:  &quot;/w 4=&quot;.  Add your message to the end of that and hit enter.
+
+/w 4=Hello ID number 4.
+
+You can even have multiple ID numbers in there.   Just seperate them up with a comma between each one, like so:
+
+/w 3, 4, 12=Hey, I dare you to poke that Woody
+
+Of course, if you don't want to type out the weird ID numbers you can always just right click on those little Easter Island Heads next to the person's name in the bottom left player window.  You can then select &quot;whisper&quot; and it will put the whisper command into your text entry box automatically.
+
+-------------------------------------------------------------------------------
+
+Sometimes there is someone that will just really get on your nerves.  Be it that they keep sending the same line of text to the chat, or maybe that they are spouting out Britney Spears lyrics nonstop.  Either way, you'll probably want to hear less of them.
+
+Well fear not!  for by just typing in &quot;/i their_ID_#&quot; (changing &quot;their_ID_#&quot; for their ID number) and hitting Enter, then they will be added to your ignore list and no further messages will be heard by them.  Should you wish to take them off again (in case you go the wrong guy) just type the same thing and it'll toggle it off.  You can also get a list of who's on your ignore list just by typing in &quot;/i&quot;
+
+Simple as that.</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="tabber_handler" icon="tabber" module="containers" name="The Map" version="1.0">
+      <nodehandler class="form_handler" icon="form" module="forms" name="General Map Info" version="1.0">
+        <form height="400" width="400"/>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Of Maps and Miniatures" version="1.0">
+          <text multiline="1" send_button="0">The Map:
+
+Now comes the hardest part of the tutorial.. describing how to use miniatures (minis)  and background images.  Well, the first step is to bring up the map.  Fortunately the new OpenRPG now loads maps differently than previous versions.  Likewise, it no longer seems to cause crashes for Windows users.  No longer will your client lag, lock up, or crash when loading images, large or small.  The entire map will now load in the background while you continue with your normal activity with the program.  Additionally, images, once loaded, will also be cached (stored) in RAM (up to 64; overflow is handled by randomly purging images within the cache to make room for new images).  That means switching back and forth between maps, in the same session, will result in instantaneous access to your map; no more waiting.  Furthermore, not only are your images loaded in the background, but they are loaded concurrently.  That means your map will be available faster than ever!  Hopefully these new innovations will lead to a renewed usage of the map.  Even modem users have indicated that using maps are now fun again!  Anyway, on with the tutorial.
+
+The Map Window is where most of your image viewing will reside.  In it you can manipulate miniatures and maps to give your players a well good idea of what your gaming world looks like.  In case you haven't guessed, it's the top-right window.  Looking at the Map Window, you'll see all of the basic operations needed to load an image and edit the map.  Let us attempt to load our first picture.  At the bottom of the map window you'll see a large box where you can type in text.  This is where you put  the Web-page Address for the image you wish.  Let's type in this URL/URI:  http://www.openrpg.com/images/mins/amazon.gif.  Notice that there are many great miniatures ready for you to use at OpenRPG's home-page.
+
+After you have typed it in press the &quot;add miniature&quot; button, sit back, and watch as it loads the beautiful amazonian woman (well, almost) into the map window.  Please note, however, all images need to be on web-pages and served up by a web server... you cannot load directly from your hard drive.  Well, you can, but no one else will be able to see it.  The reason being is because OpenRPG's client actually one giant web browser.
+
+Feel free to move it about, get a feel for how it works.  You'll see at the moment that the amazon seems to hop from grid square to grid square.  Well... if you're like me, you don't like being confined to grids or rules.  So let's get rid of that grid, shall we?  Again, looking at the map window, you will see a little red diamond thing.  They say it is a compass but it just looks like a flying fish to me.  Anyway.. click on that and it will bring up all the map settings.  In there you can change the size of the map, what color it is, or even load up a background on which all the mini's sit ((say, a dungeon map you drew or perhaps that picture of Britney Spears I know you have lying around)).  The thing we are interested in the most, though, will be the grid settings at the bottom.  As you can see, you can change the size of the grid and switch the grid from square(4 sided) to hex (6 sided).  You'll also be able to let mini's either abide by the grid, or become free from the black lined prison.  Let us turn off the &quot;snap to grid&quot; and press &quot;apply&quot;.  Also note that you can also make the grids disappear by making them match the background color, however, this is not the ideal way to do it as it still requires processing to draw and redraw the grid lines, even if they blend in without being visible.
+
+Now, go back into the map window and try moving your mini again.  You'll see that it now moves freely, and ignores those lines.  But ignoring them is not enough, did I hear you say?  You want them gone?  Ok.  Let's go back into the settings (the red star button) and this time change the grid size to zero.  Pressing 'apply' you will see that the grid has disappeared entirely from map.  Course, if you want it back, just go put in the size again (50 is default).  Feel free to make the grid size match your miniature and map scale.  Also, if you prefer to have the snap to grid, however, do not feel that it provides enough resolution in miniature movement, feel free to set the grid size to half or one forth of what your actual scale is.  This way, you miniatures will still fit nicely as well as be able to use the &quot;snap to grid&quot;, however, you'll better be able to stagger your miniatures or slightly offset tiled map sets.
+
+
+Tiled Map Sets:
+
+Often, DM's and GM's have the need to present a map to their players, however, they do not which to reveal the whole map and/or map section to their players.  After all, it does remove a lot of surprise if everything is right before the player's eyes.  Enter three new miniature options: &quot;Lock to back&quot;, &quot;Lock to front&quot;, &quot;Unlock Front/Back&quot;.  Previous to these options being available, either the whole map had to be loaded by changing the background image or players and DM's where forced to struggle when map sections were loaded as if they were miniatures.  Even though there was z-ordering options available, it was still tedious as miniatures would often disappear behind a map image.  Now, entire maps can be built before as game and loaded all at once, without giving away any surprises.
+
+By loading all of your map sections together as miniatures and marking each one visible or not visible, you can have great fun again with your maps!  Wait a minute, isn't this the same boat we were on before.  Well, enter the three new options.  Using the &quot;Lock to back&quot; option on each of your map sections, it will force them to always stay on the bottom of the z-order (stacking) of your miniatures.  That means, you'll now be able to change the z-order of your miniatures while all of them stay on top of your map image.  In other words, while the image is locked to the back of the z-order, it acts just like a background image should.  To restore a miniature to normal z-ordering, simply using the &quot;Unlock Front/Back&quot; option and it will once again be ordered just as any other miniature is.
+
+Wait a minute!  That's only two of the three options that you mentioned.  Well, the third option allows for even more flexibility in what DM's and GM's can present to their players, even on a visible map section.  Enter, the poor-man's fog-of-war.  By creating a &quot;mask&quot; in just about any shape you like (feel free to use transparent colors on gifs as needed), you can effectively lock the &quot;masking&quot; image to the top of the z-order which will hide a section of your visible map.  While this is still somewhat tedious, it will allow for much finer grain of control of what is and is not visible on your maps!
+
+
+Looking here, moving there:
+
+Directional markers have been a part of OpenRPG for some time now, however, there use is going to be better explained here.  If you notice, each miniature has a &quot;facing&quot; and &quot;heading&quot; available to it.  These serves not only to indicate a miniature's facing, but it's current direction of travel.  After all, you're quite able to turn your head and talk to your buddy while walking in a direction different than the one your head is pointed.  While this feature mostly serves board game players, RPGer's may find use in this too, especially when making canned maps.  Imagine, the 'ol evil cleric behind the podium with his back to you.  Now, you can make it clear on the map.  Oh wait, the cleric's minions do not have their back to you, rather, they are standing to his side and looking directly at him.  Closer look, and you see that the cleric too had his head turned to his minions in conversation.  Again, this can be expressed on your map.  Information like this makes it easier for the players to understand that the back-stab that they obviously want to do, won't be so easy to pull off.  All this, without having to verbally describe every little detail.  After all, isn't this what the maps is for?  Enjoy!
+
+
+Saving and Loading Your Map:
+
+Don't forget that you can save your hard work.  Once you have built your map, be sure to right click on the map and hit &quot;Save Map&quot;.  When you need it again, just do &quot;Load Map&quot;.  You don't even need to delete the contents of the previous map.  The client does all that for you!  Don't forget, if you load a previously viewed image, the caching will considerable speed the rate at which it's visible to you and your players.  Using a common set of miniatures helps to take advantage of this fact.
+
+The Other Modes:
+
+Now, you can also use something called map modes. This feature lets you draw, and use rulers and other fun stuff. To turn it on you right click in the map, go under switch modes and choose either whiteboard (drawing) or ruler (ruler with distance gauge). This can be useful if you ever needed to point out something to someone. To remove a line just right click on it and click remove, its as easy as that!
+</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Map Tabs and Toolbar" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="The Map Toolbar" version="1.0">
+          <text multiline="1" send_button="0">This is the toolbar in the lower right hand side of the map, there you can, from left to right:
+
+Zoom in the map
+
+Zoom out
+
+View Map Properities
+
+Load Map
+
+Save Map</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="The Backround Layer" version="1.0">
+          <text multiline="1" send_button="0">This layer is used to change the backround of the map. there are 4 options
+
+none - no backround at all
+
+Texture - repeats the picture linked to the link in the box to the right of it all over the map to fill up the whole map
+
+Image - Displays the image at its full size once in the upper left corner of the map
+
+Color - Chooses a color for the backround
+
+
+Footnote:
+
+Only weblinks can be used for the backround at the moment, that and maps you make before hand and save from within OpenRPG.
+
+And if you right click on the map, you can load map, save map, go into a properities window where you can change other things such as the Grid (same as when u use the grid tab) and all the backround stuff.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="The Grid Layer" version="1.0">
+          <text multiline="1" send_button="0">In the grid layer (and in the map properities window which can be opened by right-clicking on the map) you can change the Pixels per Square (size of each square or hex); if there are lines and if they are dotted or solid; whether there are hex or square units; if the minitures are placed on the grid and can only be moved to another square or hex; the color of the grid; and a button to apply all changes.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="The Miniature Layer" version="1.0">
+          <text multiline="1" send_button="0">The miniature layer is where you take miniatures from the web, and put them on the map, this is done by putting the link in the box right above the Miniatures tab to the image of the minitiature, and clicking &quot;--&gt; Add Miniature&quot; Also there are a few more things you can do:
+
+Auto Label - Automatically label the mini based on the image name (ex. image.gif is                         named image1)
+
+                                    Right Click on Mini Menu Options
+
+Miniature Z-Order - Send the image to the front (be ontop of the other minis) or, back, or up a layer, or down a layer (like in MSWord), or lock it in a poisition
+
+Set Facing - Make a red arrow point to where the mini is &quot;facing&quot;
+
+Set Heading - Make a blue arrow point to where the mini is &quot;heading&quot;
+
+To Gametree - Copy the mini to the gametree
+
+Remove - Removes the mini from the map
+
+Snap to alignment - changes how the mini snaps to the grid
+
+Properities - lets you change most of whats above and lets u hide the mini by making it invisible</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="The Whiteboard Layer" version="1.0">
+          <text multiline="1" send_button="0">The whiteboard layer is used to draw on the map using a pen, you can change the pen color, and if you right click on a line you can delete it, or if you right click anywhere on the map you can delete all lines.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="The Fog Layer" version="1.0">
+          <text multiline="1" send_button="0">The fog layer is a new thing in OpenRPG to help hide things from the players, but not the GM, this is used by selecting the &quot;Fog&quot; tab, turning it on, then right clicking on the map (as a GM) and clicking Fog Mask, choose a color, then right click again and select hide all. This will activate the fog layer all over the map, and while the fog layer tab is clicked, u can shade and unshade portions at will.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="The General Layer" version="1.0">
+          <text multiline="1" send_button="0">The general layer is used for changing the map size and loading the default map (the green map you see on a lot of servers)</text>
+        </nodehandler>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="form_handler" icon="form" module="forms" name="Nodes and Character Sheets" version="1.0">
+      <form height="400" width="400"/>
+      <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Nodes" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="&lt;b&gt;The Basics&lt;/b&gt;" version="1.0">
+          <text multiline="1" send_button="0">&lt;a name=&quot;c3&quot;&gt;&lt;/a&gt;
+alright.. by now you have grasped the basics on how to chat, so we will move on to the next phase: character sheets.  This part of the tutorial will cover how to make a full usable document from the nodes.
+
+Let us first start by describing what the different nodes are and where we can find them</text>
+        </nodehandler>
+        <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Nodes" version="1.0">
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Nodes and Sheets" version="1.0">
+            <text multiline="1" send_button="0">------------------------------------------------------------------------
+--Character Sheets And Nodes
+------------------------------------------------------------------------
+
+Character sheets are the wonderful inventions that allow us to hold all the information we want about our characters and gaming worlds.  In OpenRPG they are made up of what we call &quot;Nodes.&quot;  These nodes are the building blocks for creating all manner of character sheets and can be rearranged into whatever form you wish.
+
+but to understand the nodes, we must first know how they work.  This chapter is dedicated to explaining the function of each node.  To get a new node, just find the one you want in the &quot;Templates&quot; tree and doubleclick on it.  That will put a fresh blank one in the gametree at the top.</text>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Nodes" version="1.0">
+            <form height="700" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="The Text Node" version="1.0">
+              <text multiline="1" send_button="1">The Text Node is the very basic and most fundamental part of the character sheet.  It is where we store most of our important information.  To use a text node, first you must right click on it and press &quot;design.&quot;  This will bring up the text node's option's window.  In there you can choose whether the Text node only has one line or if it is a small text window, what it's title is, and whether it has a send button or not.  The Send button will send whatever is in the text node directly to the chat.</text>
+            </nodehandler>
+            <nodehandler class="listbox_handler" icon="gear" module="forms" name="List Box" version="1.0">
+              <list send_button="0" type="0">
+                <option selected="1" value="0">The List Box is a marvelous tool that allows you to have ...</option>
+                <option selected="0" value="0">dropdown menus and a variety of checklists and radio boxes.</option>
+                <option selected="0" value="0">To use a List box, right click on one that you created in your</option>
+                <option selected="0" value="0">gametree and press &quot;design&quot;.  this will bring up a window </option>
+                <option selected="0" value="0">where you can choose what type of list box it is and what </option>
+                <option selected="0" value="0">options are inside of it.  Simple as that!</option>
+              </list>
+            </nodehandler>
+            <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Grid" version="1.0">
+              <grid autosize="1" border="1">
+                <row version="1.0">
+                  <cell size="144">The Grid lets you keep tables</cell>
+                  <cell size="138"></cell>
+                </row>
+                <row version="1.0">
+                  <cell></cell>
+                  <cell>like stats, or saving throws.</cell>
+                </row>
+                <row version="1.0">
+                  <cell>You can arrange numbers</cell>
+                  <cell></cell>
+                </row>
+                <row version="1.0">
+                  <cell></cell>
+                  <cell>more easily in a grid than</cell>
+                </row>
+                <row version="1.0">
+                  <cell>you can with a text node.</cell>
+                  <cell></cell>
+                </row>
+              </grid>
+              <macros>
+                <macro name=""/>
+              </macros>
+            </nodehandler>
+            <nodehandler class="webimg_handler" icon="image" module="forms" name="Image" version="1.0">
+              <link href="http://my.openrpg.com/images/logo200x60.jpg"/>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Images allow for images. Just right click to provide the webpage addy they're at" version="1.0">
+              <text multiline="0" send_button="0"></text>
+            </nodehandler>
+            <nodehandler class="link_handler" icon="html" module="forms" name="OpenRPG.com link" version="1.0">
+              <link href="http://www.OpenRPG.com"/>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name=" " version="1.0">
+              <text multiline="0" send_button="0">you can change the link node's url by right clicking on it in the gametree</text>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name=" " version="1.0">
+              <text multiline="1" send_button="0">Now that we know what the individual nodes do, we need ways to group them together.  continue to the next page to learn about the different forms of groups.</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="form_handler" icon="form" module="forms" name="containers" version="1.0">
+            <form height="700" width="700"/>
+            <nodehandler class="form_handler" icon="form" module="forms" name="Form" version="1.0">
+              <form height="400" width="400"/>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Text" version="1.0">
+                <text multiline="1" send_button="0">The Form is the basic and most used container of the lot.  It is what is able to hold all the other types of containers AND nodes.  Forms can also be placed within other containers as well, allowing for you to 'group up' a bunch of nodes in a form, then put that form as one page on a tabber. You can adjust the size by right clicking on a form in the game tree, pressing design, and adjusting the height and width</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="splitter_handler" icon="divider" module="containers" name="Splitter" version="1.0">
+              <nodehandler class="splitter_handler" icon="divider" module="containers" name="Splitter" version="1.0">
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Text" version="1.0">
+                  <text multiline="0" send_button="0">text</text>
+                </nodehandler>
+                <nodehandler class="textctrl_handler" icon="note" module="forms" name="Text" version="1.0">
+                  <text multiline="1" send_button="0">text</text>
+                </nodehandler>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name=" " version="1.0">
+                <text multiline="1" send_button="0">The splitter allows you to break a character sheet into segments, and have different datas side by side.  you can't have more than two single things inside a splitter, however you can put forms (or even other splitters!) inside a splitter.</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Folders" version="1.0">
+              <text multiline="1" send_button="0">Another node that is useful for keeping everything together is the folder. You can only view the stuff in it by using pretty print, but if you want to save a lot of work in one area, it is the thing you want to use.</text>
+            </nodehandler>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Character Sheets" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Moving Characters from almost ANY Character Generator to here." version="1.0">
+          <text multiline="1" send_button="0">The following are stpes on how to move your character sheets from your favorite Character Generation program into OpenRPG.  It's very quick and clean cut.
+
+1) first, using your character generator, you convert your character to either txt format, or, preferably (if you have the ability) HTML format.
+
+2) Then, you go to wizards.put a text block into the gametree
+
+3) if you are using txt, select all, and copy.  if you are using HTML format, edit it using notepad, but don't change any of the HTML Program. then select all and copy
+
+4) now, going back into OpenRPG, right click on the text block you created and push 'edit'
+
+5) clear the text block of the few words of text that appear there then paste the copied info into by either rightclicking-paste or pressing 'ctrl v'
+
+6) then name your character in the Title then close the text block.  Finished
+
+no more need to create character sheets!  That simple and any time you want to see it, just doubleclick on the text node.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Create Your Own Character" version="1.0">
+          <text multiline="1" send_button="0">((before we begin, see the character sheet repository:
+   http://openrpg.wrathof.com
+for pre-made character sheets.))
+
+
+Character Sheet creation (By Melanthos):
+
+So you wanna make a character sheet, huh? Well, follow these few short steps and you'll be on your way to Sheet Creation Godhood...or not. But don't blame me if you don't read this and can't make yourself a sheet. I'm going to guess you have a basic understanding of how the nodes work. I'll be using Harvester Incorporated(TM)'s Faction Wars(TM) as an example.
+
+First, you'll need a tabber. You can find it under containers in the templates node.  Click the + to the left of templates then click the plus next to containers.  Next double click the Create New Tabber.  Got it? Good. Now, you'll need a form. Forms are what we use to create each page of the tabber. They hold the rest of the nodes neatly, and open up - unlike groups, which cannot be opened in a sheet. To create a form double click on Create New Form.  You will notice two new nodes at the top of the game tree.
+
+
+        Tabbers, Forms, and Text-boxes, OH MY!
+
+Alright. Here is where I store the basics of the character. Since I don't like to scroll too much, I've broken down the information even more, so that I can fit into smaller tabbers and forms. First I'll create a second tabber. This will hold all the basic info - Level, Class, Player &amp; Character name, and a few other things. Since I've got 8 things I want to list, I've broken them up into 2 seperate groups, which have been put into different forms: General Info, and Specific Info. For simple things like name and level, I suggest using single-line text boxes. I've gone ahead and made 8 of these, naming each of them one of my 8 items and stuck them into their forms. Now that I've finished that, I'll put the two forms under my second tabber, and put that into my big form, &quot;General.&quot; I've done the same thing with the Attributes. Most of the sheet uses this principal, so I won't go into each form. Kinda redundant.
+
+
+                   List Boxes &amp; Die Creation
+
+Alright, for my second form, Vitals, I need some die-rollers. Since I don't need to change the rolls often, I've decided to use List Boxes to hold all my rolls. It's not too hard, and it's a really easy way to store rolls(It can be used for other things, but this is the best use I've found.): Now you don't have to type out those dice every time you wanna attack. First, I've created myself a List Box. Next, I'll decide how I want my list box to be shown: There are four choices. Drop Down, List Box, Radio Box, and Check List. Since I only need to roll one at a time, I've chosen Drop Down. But to show you how the Drop Down and Check List options work, I'll change things a bit and make a List Box and a Check List as well. When editing, make sure to click the &quot;Send Button&quot; box.
+
+
+        Drop Down
+
+The Drop Down is the least space- consuming of the List Box options. It's just like a single-line Text Box, except it holds more than one line. Click the arrow on the right to make it drop down, and click the die you want to roll. Then hit the Send button, and POOF! Your die has been rolled to the chat window.
+
+
+        Check List
+
+The Check List is a neat option. It lets you roll more than one die at a time. All you have to do is check the box next to the die (or dice) you want to roll, and POOF! Your dice have been rolled. Neat, eh?
+
+           Splitter
+
+Now what could these be used for, you wonder? Not much, but there are a few things. I've used it to hold my different abilities. It's easier for me to just see my choices than to flip between tabbers or scroll down and back up every few seconds.
+
+
+The rest is pretty easy. Just keep on with the things you've done so far, and in no time you'll be on your way to creating great sheets! Most of sheet creation is your personal preference. If you ask Woody, he might say something completely different than I might. Of course, you should make the sheets the way you want them. This is just to get you started. After you've got the basics of sheet making down, you can play around with sheets and nodes and things until you've got the sheet perfect for you. GOOD LUCK!!
+
+
+--Melanthos, Founder &amp; Game Designer - Harvester Incorporated</text>
+        </nodehandler>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="form_handler" icon="form" module="forms" name="Server Control" version="1.0">
+      <form height="400" width="400"/>
+      <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Server Control" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Running Your Own Server" version="1.0">
+          <text multiline="1" send_button="0">Creating a server:
+
+OpenRPG has come a long way since the dark ages of gaming.  With this new version we can now create servers on-the-fly!  No more black boxes or strange voodoo magic.  Just click on your &quot;Game Server&quot; menu, and select &quot;Start new server.&quot;  Fill in the options and away you go!  the server will appear instantly on the list.
+
+Of course, this won't provide you with all the same powers that the full server does (but it will for most if them).  While you can create a server on the short term, if you want to have a longterm dedicated server running which you can control, you'll want to run the OpenRPG Server in your start menu-programs-openrpg (or mplay_server.py in your openrpg folder).
+</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Running a Dedicated Server" version="1.0">
+          <text multiline="1" send_button="0">Well, maybe not any money in creating a server on this program, but there are definately perks to running one.  But to run one we must first understand what they do.
+
+A server is the mother for all of us on OpenRPG.  It runs all of our major commmands, keeps us all talking together, and hosts all the games on OpenRPG.  When you first connected to OpenRPG you had to choose a server from a list in the tracker, on the left.  Now let us put your name (or at least your computer's) out there so others can flock to you.
+
+For this we must return to your OS.  Go into your computer, into the OpenRPG folder.  In there, you will see a file marked:
+
+mplay_server.py
+
+You can also find it in your start menu, right next to the OpenRPG program itself.  Once you find it, (double)click on it.  The first thing that will come up is a python MSDOS window... It will first prompt on if you wish for your server to bee seen by the OpenRPG tracker list.  Press &quot;Y&quot; and hit enter (won't get anyone if we don't know it exists).  Next, it will prompt you for a name for your Server.  Write in &quot;Temporary Server&quot; and press enter.
+
+Now the window will start running the various server-y looking bits of code.  as soon as it is up and going, you will told the various commands that are available to the server.  The first one will be &quot;kill&quot;.  this is what you type into the MSDOS window to shut the server down.  &lt;b&gt;**IMPORTANT**&lt;/b&gt; if you shut down the server all rooms on it will close.  You don't want to shut it down if others are using it.
+
+The second command is the &quot;dump&quot; command.  This will list all the people on your server, as well as their ID number.  It really isn't important at this time but it will come in handy later.
+
+Broadcast is self explanitory.  The 'Announce' and 'Remove' features allow you to choose later on if your server can be seen on the tracker, or not.  Dump Groups gets the same info as Dump but does Groups instead of people.  And finally you can bring up this little list of commands at any time by typing 'help' or '?'.
+
+You can type these commands in any time in the python server window.  Go ahead and try 'help' or 'dump' and see what comes up.
+
+Alright!  That's it.  to access your server, run you OpenRPG program as normal (with the server MSDOS window in the background) and log onto your server as we had shown you in chapter 1.  This will come in handy if there are no Servers running and you wish to use OpenRPG with, perhaps your gaming buddies or maybe some underworld kingpins you need to 'have a talk' with.
+
+And From All of us at OpenRPG HQ, we wish you good gaming!</text>
+        </nodehandler>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Rolls and Room Moderation" version="1.0">
+      <nodehandler class="form_handler" icon="form" module="forms" name="Rolls" version="1.0">
+        <form height="400" width="400"/>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="General Info" version="1.0">
+          <text multiline="1" send_button="0">OpenRPG now uses a roll system where when you join a room other than the lobby you are assigned a roll, you change rolls by either using the /roll command (this also lists all the rolls in the room if that is all you type in) or right clicking the name of the player in the player list window. do change a roll you need to know the room administration password.</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Lurker (Whitish Grey)" version="1.0">
+          <text multiline="1" send_button="0">A lurker cannot talk when room moderation is turned on, also he cant change any of the map options. He is basically the watcher in a game, and is the roll that the person gets when he first joins the room</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Player (Black)" version="1.0">
+          <text multiline="1" send_button="0">Player is the main roll for a player in a game, he can do some map functions, and can talk automatically if room moderation is turned on. He is...well...the player</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="GM (Red)" version="1.0">
+          <text multiline="1" send_button="0">GM (Game Master) is the person who makes the room by default, he also can do all the stuff in the room, including use the map, the only thing he cannot do is change roles and turn on and off room moderation, unless he knows the administration password for the room.</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Room Moderation" version="1.0">
+        <text multiline="1" send_button="0">Room Moderation is used to make it so only certain people can chat in a room, this is usually used to prevents spammers who enters the room, but can be used for anything you want. It is turned on and off by right clicking on a name and turning it on (you need the administration password). After that you can right click on the name of a person you want to let talk, or not let him talk by giving or taking voice under the Moderation sub menu.</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Setting Up Rooms, Games, and Ignoring" version="1.0">
+      <text multiline="1" send_button="0">So now that you have your server, your custom character sheet, and have talked enough players into starting a game, you'll want to set up a private room so that you aren't disturbed by every wandering weirdo or Woody that happens upon your server (and be forwarned, you will find that various people will wander from server to server.  Do not be frightened, most of them are harmless).
+
+So, lets pull up your tracker window.  If you haven't done so already, be sure to connect to your server.  Once in there look onto the right side of the tracker window.  You will see some boxes marked &quot;Room name&quot;, &quot;Boot password&quot;, and if you check a little box, &quot;password&quot;.  the first two are rather self explanatory, and the last one is to lock your room in case you don't want to be bothered.  Be sure to tell your players what it is though.
+
+If you don't password protect a room you may get people wandering in looking for a game or just lurking watching your game.  If you don't want them in there you should first let them know and ask them to leave.  If they refuse your request then boot them.  It is very rude to boot someone from a non passworded room with out warning.
+
+Now that you have your custom built char sheet and you have talked enough players into starting a game, it's time to set up your own room.  Let's bring up that Tracker window again ((under the Game Server menu)).  Looking to the bottom left of it, you'll see a little box that allows you to type in your own room name and add a password if you like.  Let's start a room called &quot;Working on that darned fun tutorial&quot; and not put a password up.
+
+as you see the room you create will be exactly the same as the lobby, so there is nothing to worry about.  Lets load up our character sheet by right clicking on the &quot;Game Tree&quot; and pressing Insert file&quot;
+
+Now that we have our little char sheet we might as well show the world.  This can be done in one of three ways.  The first way is to right click on it and send it to other players (provided there are any).  This is how other players can get your sheet as they can't see it until you send it to them.
+
+The second way is to send it directly to the chat and let everyone see it there.  go ahead and do that by right clicking on the char sheet and pressing &quot;Send to Chat&quot;.  Now everyone gets to see it as it appears on their text chat window.
+
+the third option, Whisper to Player, is much the same as &quot;Send to chat&quot; only it sends it to a specific person or persons to see in their chat instead.
+
+Of course, there sometimes comes a day when there is a little spammonkey running about.  You know the type.  Just keeps askin pointless questions, hitting you with the same text, and generally making himself a nuesance.  Well fear not!  We have added in a brand new feature that allows you to ignore those unscrupulous people.  In the chat entry box just enter the command &quot;/i player_ID#&quot; and that will put a person on ignore (or toggle them back to un-ignore [you can also right click on the name and press toggle ignore]).  To get a list of ID's who are on your ignore list, just type &quot;/i&quot; alone, with nothing else.  You can even ignore/un-ignore multiple people at once, just put commas (( , )) between the names.  And that's it.  Ignore at your leasure.</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Other Features; Logs, Initiative, Upgrades" version="1.0">
+      <text multiline="1" send_button="0">Other features:
+
+It may seem a little odd that this seventh window is tucked behind some arrows, but I always thought this chapter was a little on the sinful side so over here it now lay.
+
+
+By getting here, you must have had quite a bit of time to read through the rest of the chapters.  Well, let's finish off with what few interesting toys we have left.
+
+To start with, lets talk about logging  To create a logfile, you can just press the disk button in the bottom right corner of the OpenRPG program, and choosing a name and place to save the file.  Badaboom!  a chatlog of everything you have seen is saved.
+
+Of course, most of you will want a way to have it automatically save a log each time it see's text.  To do that, type in &quot;/log to my_log&quot; (replacing &quot;my_log&quot; for what you want to call the file). It should automatically start logging to that filename (and will include the date in the filename you gave it).  if you want to turn it on or off later on, just type &quot;/log on&quot; or &quot;/log off&quot;
+
+Note: The chat logging system no longer appends a datestamp to the logfile
+specified.  This means that for the system to generate the logfile
+
+/foo/bar/gamelog-02-10-01.html for linux or \foo\bar\gamelog-02-10-01.html for windows
+
+Previously the user only had to have &quot;/foo/bar/gamelog&quot; entered as GameLogChatPrefix.
+To get the same logging pattern now, they'll have to set GameLogChatPrefix to
+the following:
+
+/foo/bar/gamelog-%d-%m-%y.html
+
+
+--------------------------------------------------------------------------------
+The Text View Tool
+
+This is used if you want to find specific text in the chat buffer or view the raw HTML, its handy also if you want to cut and paste phrases out of the chat buffer.
+
+
+--------------------------------------------------------------------------------
+
+The Initiative Tool.  A marvelous invention allowing DMs to organise and handle an otherwise chaotic scene that we know as combat.  Compatable with all versions of OpenRPG, it works incredibly simply.
+
+All that is needed is for the DM to press &quot;New Initiative&quot; in the tool, have all of the players and the DM to type in &quot;init [diceroll]&quot; (replacing 'diceroll' for your dice) and hit enter.  An example:
+
+init [1d10+6]
+
+once everyone has done that, the DM pulls up his tool again, hits refresh, and then sorts the list however he wants.  once it is sorted, he just hits &quot;send to chat&quot; and the person at the top of the list is then sent to the chat.
+
+Once all the people have been cycled through, it will say &quot;End of Init&quot; in the chat.  The DM just presses the &quot;Start new Initiative&quot; button one more time and away you go on another round of hacking!
+
+Now, if you are the player and not using the tool, you can even put your init command into a macro node (not a text node) and do it that way.  At the time of this publication there was a bug in the initiative code and the DM running the tool cannot use macro nodes for this.
+
+You can also set up your character sheets to be able to be rolled from.  To do it, you will just need a special node that you can find here:
+
+www.geocities.com/woody_j_dick/initiative.xml
+
+You can put it directly into OpenRPG by right clicking on your gametree, pressing &quot;Insert URL&quot; and putting that URL above into the box that appears.  Even the DM can use that node for rolling his initiatives.
+
+CAUTION!  Do not rename the node.  It will not work unless it has that name.
+
+
+The last button, &quot;Keep list&quot; is for Whitewolf players who need to use the list twice.  If you edit the list, and want to keep the edited copy, then just press that and it will save the list so all you need to do is hit refresh to get it back.
+
+--------------------------------------------------------------------------------
+
+OpenRPG is a great program, no doubt about it.  And it's best quality is that it is opensourced, meaning anyone can look at the code and alter it to make it work better.  One such person, the one that compiled this tutorial no less, is going to capitalise on this free advertising to say a few quiet words about his webpage.
+
+www.geocities.com/woody_j_dick/OpenRPG.html
+
+The 8.3 Convention webpage, it offers many 'additional extras' That are not yet included in the main program.  Not only are the upgrades good for the current versions of OpenRPG, but it also retains a list of upgrades for previous versions, in case you want to go back and use that 'good ole' 9.4 or 9.6 client with the modern day servers.
+
+The webpage doesnt have any modifications for OpenRPG at the moment, but it might in a future.
+
+</text>
+    </nodehandler>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/adnd_2e_char_sheet.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,513 @@
+
+<nodehandler class="static_handler" icon="d10" module="core" name="ADnD Character Sheet ">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="static_handler" module="core" name="&lt;b&gt;&lt;u&gt;Main&lt;/b&gt;&lt;/u&gt;">
+    <group_atts border="1" cols="2"/>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Vitals">
+      <grid border="1">
+        <row>
+          <cell>Name</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Character</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Race</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Class</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Sex</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Level</cell>
+          <cell/>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="static_handler" module="core" name="Statistics">
+      <group_atts border="0" cols="1"/>
+      <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name=" ">
+        <grid border="1">
+          <row>
+            <cell>HP</cell>
+            <cell>0</cell>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell>XP</cell>
+            <cell>0</cell>
+            <cell>0</cell>
+          </row>
+        </grid>
+        <macros>
+          <macro name=""/>
+        </macros>
+      </nodehandler>
+      <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name=" ">
+        <grid border="1">
+          <row>
+            <cell>AC </cell>
+            <cell/>
+          </row>
+          <row>
+            <cell>Thaco (base)</cell>
+            <cell/>
+          </row>
+        </grid>
+        <macros>
+          <macro name=""/>
+        </macros>
+      </nodehandler>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="&lt;b&gt;&lt;u&gt;Stats&lt;/b&gt;&lt;/u&gt;">
+    <group_atts border="0" cols="4"/>
+    <nodehandler class="static_handler" module="core" name="&lt;b&gt;Basic stat&lt;/b&gt; ">
+      <group_atts border="1" cols="2"/>
+      <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Stats">
+        <grid border="0">
+          <row>
+            <cell>Str</cell>
+          </row>
+          <row>
+            <cell>Dex</cell>
+          </row>
+          <row>
+            <cell>Con</cell>
+          </row>
+          <row>
+            <cell>Int</cell>
+          </row>
+          <row>
+            <cell>Wis</cell>
+          </row>
+          <row>
+            <cell>Cha</cell>
+          </row>
+        </grid>
+        <macros>
+          <macro name=""/>
+        </macros>
+      </nodehandler>
+      <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Stat ##">
+        <grid border="1">
+          <row>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell>0</cell>
+          </row>
+        </grid>
+        <macros>
+          <macro name=""/>
+        </macros>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Strength">
+      <grid border="1">
+        <row>
+          <cell size="119">Hit Prob</cell>
+          <cell size="39"/>
+        </row>
+        <row>
+          <cell>Dmg Adj</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Wght Allow</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Max Press</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>opn Drs</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>bb/lg</cell>
+          <cell/>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Constitution">
+      <grid border="1">
+        <row>
+          <cell>HP Adj.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Sys Shock</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Res Surv.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Pois Save</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Regen.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>/  /  /  /  /  /  /  /  /  /</cell>
+          <cell>/  /  /  /  /  /  /  /  /  /</cell>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Intelligence">
+      <grid border="1">
+        <row>
+          <cell># of Lang</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Spell lvl.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Learn Spl</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Spells / lvl</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Immun.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>/  /  /  /  /  /  /  /  /  /</cell>
+          <cell>/  /  /  /  /  /  /  /  /  /</cell>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name=" ">
+    <group_atts border="0" cols="3"/>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Dexterity">
+      <grid border="1">
+        <row>
+          <cell>React Adj.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Missile Adj.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Defense Adj.</cell>
+          <cell/>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Charisma">
+      <grid border="1">
+        <row>
+          <cell># of Henchmen</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Loyalty Base</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>React Adj.</cell>
+          <cell/>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Wisdom">
+      <grid border="1">
+        <row>
+          <cell>Magic Def.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Bonus Spls.</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Spl Fail</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Immune.</cell>
+          <cell/>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="&lt;b&gt;&lt;u&gt;Weapon Proficiencies&lt;/b&gt;&lt;/u&gt;">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Weapons">
+      <grid border="1">
+        <row>
+          <cell>Weapon</cell>
+          <cell>Thaco</cell>
+          <cell>To Hit/Dmg</cell>
+          <cell>Dmg S/M</cell>
+          <cell>Dmg L</cell>
+          <cell>spd</cell>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+          <cell/>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="&lt;b&gt;&lt;u&gt;NonWeapon Proficiencies, Saves, etc&lt;/b&gt;&lt;/u&gt;">
+    <group_atts border="1" cols="2"/>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Non-Weapon Proficiencies">
+      <grid border="1">
+        <row>
+          <cell>Proficiency</cell>
+          <cell>modifier</cell>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+        <row>
+          <cell/>
+          <cell/>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="static_handler" module="core" name="&lt;b&gt;Saving Throws&lt;/b&gt;">
+      <group_atts border="0" cols="2"/>
+      <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name=" ">
+        <grid border="0">
+          <row>
+            <cell>&lt;b&gt;Save&lt;/b&gt;</cell>
+          </row>
+          <row>
+            <cell>Para/Pois/Death</cell>
+          </row>
+          <row>
+            <cell>Rod/Staff/Wand</cell>
+          </row>
+          <row>
+            <cell>Petri/Poly</cell>
+          </row>
+          <row>
+            <cell>Breath Wpn</cell>
+          </row>
+          <row>
+            <cell>Spell</cell>
+          </row>
+        </grid>
+        <macros>
+          <macro name=""/>
+        </macros>
+      </nodehandler>
+      <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name=" ##">
+        <grid border="1">
+          <row>
+            <cell>Roll</cell>
+            <cell>mod</cell>
+          </row>
+          <row>
+            <cell/>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell/>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell/>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell/>
+            <cell>0</cell>
+          </row>
+          <row>
+            <cell/>
+            <cell>0</cell>
+          </row>
+        </grid>
+        <macros>
+          <macro name=""/>
+        </macros>
+      </nodehandler>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="&lt;b&gt;&lt;u&gt;Equipment&lt;/b&gt;&lt;/u&gt;">
+    <group_atts border="1" cols="2"/>
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Money">
+      <grid border="1">
+        <row>
+          <cell>Platinum</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Gold</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Silver</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>Copper</cell>
+          <cell/>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name="Backpack / Pouches / etc.">empty</nodehandler>
+  </nodehandler>
+  <nodehandler class="static_handler" module="core" name="&lt;b&gt;&lt;u&gt;Magic&lt;/b&gt;&lt;/u&gt;">
+    <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Spells per level">
+      <grid border="1">
+        <row>
+          <cell>1st lev</cell>
+          <cell/>
+          <cell>2nd lev</cell>
+          <cell/>
+          <cell>3rd lev</cell>
+          <cell/>
+          <cell>4th lev</cell>
+          <cell/>
+          <cell>5th lev</cell>
+          <cell/>
+        </row>
+        <row>
+          <cell>6th lev</cell>
+          <cell/>
+          <cell>7th lev</cell>
+          <cell/>
+          <cell>8th lev</cell>
+          <cell/>
+          <cell>9th lev</cell>
+          <cell/>
+          <cell>///</cell>
+          <cell>///</cell>
+        </row>
+      </grid>
+      <macros>
+        <macro name=""/>
+      </macros>
+    </nodehandler>
+    <nodehandler class="text_handler" icon="note" module="core" name="Spells">Magic Missile
+Sleep
+Bigby's Poking Finger</nodehandler>
+    <group_atts border="1" cols="2"/>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/alias.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,32 @@
+
+<nodehandler class="voxchat_handler" icon="player" module="voxchat" name="Alias Library" use.filter="0" version="1.0">
+  <voxchat.filter name="Rogue or Pirate">
+    <rule match="ia" sub="'a"/>
+    <rule match="(\W+)(?i)it is(\W+)" sub="\1t'is\2"/>
+    <rule match="(\W+)(?i)h" sub="\1'"/>
+    <rule match="(?i)his" sub="'is"/>
+    <rule match="n[tkg](\W+)" sub="n'\1"/>
+    <rule match="(\W+)(?i)to(\W+)" sub="\1t'\2"/>
+    <rule match="(\w+)th(\W+)" sub="\1t'\2"/>
+    <rule match="(\W+)([Yy])ou(\W+)" sub="\1\2ea\3"/>
+    <rule match="(\W+)([Yy])our(\W+)" sub="\1\2er\3"/>
+    <rule match="'+" sub="'"/>
+    <rule match="th" sub="d'"/>
+    <rule match="ass" sub="arse"/>
+  </voxchat.filter>
+  <voxchat.filter name="Static Communicator">
+    <rule match="ce" sub="ssss(**shhhshh**)"/>
+    <rule match="[IiOo]" sub="(**sss**)"/>
+    <rule match="wi" sub="(*squack*) "/>
+    <rule match="run" sub="@#$%DDD "/>
+    <rule match="[Ss][Tt]" sub=";lkj%$# "/>
+    <rule match="opy" sub="op.....he"/>
+    <rule match="[Bb]"/>
+    <rule match="the" sub="te"/>
+    <rule match="ow" sub="ashh"/>
+    <rule match="[Aa][Bb]" sub="bbb"/>
+    <rule match="ll" sub="ya"/>
+    <rule match="support" sub="(*rssshhhh*)'pt"/>
+    <rule match="ey" sub="eeee"/>
+  </voxchat.filter>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/browser.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+
+<nodehandler class="webbrowser_handler" icon="browser" module="core" name="Browser Link">
+  <link href="http://"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/d20character.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,119 @@
+
+<nodehandler class="d20char_handler" icon="knight" module="d20" name="D20 Character" version="1.0">
+  <howtouse version="1.0">
+    <howto>
+To use this you do:
+
+To use this you just need to add all the stuff you wish, set your stats ect, then click on the
++(plus) next too the gears, and right click. This will cause it to roll what is needed to roll
+
+In the attaks section Base2 - Base6 are for additional attacks at a diff base then your first
+If you set these, it will automaticly roll the additional attacks for each base you set when
+you right click on the weapon you wish to attack with.
+
+If you are having problems selecting spells, please verify that your class level is high enough
+to use the spell you want to add, and that the spell is in the list.
+</howto>
+  </howtouse>
+  <general version="1.0">
+    <name>Player Name</name>
+    <player>Your Name</player>
+    <race>none</race>
+    <alignment abbr="LG">none</alignment>
+    <deity>none</deity>
+    <size acmodifier="0">none</size>
+    <height>none</height>
+    <weight>none</weight>
+    <age>none</age>
+    <gender>none</gender>
+    <eyes>none</eyes>
+    <hair>none</hair>
+    <speed>30</speed>
+    <currentxp>0</currentxp>
+    <xptolevel>1000</xptolevel>
+  </general>
+    <classes level="0" version="1.0"/>
+    <abilities version="1.0">
+      <stat abbr="Str" base="0" name="Strength"/>
+      <stat abbr="Dex" base="0" name="Dexterity"/>
+      <stat abbr="Con" base="0" name="Constitution"/>
+      <stat abbr="Int" base="0" name="Intelligence"/>
+      <stat abbr="Wis" base="0" name="Wisdom"/>
+      <stat abbr="Cha" base="0" name="Charisma"/>
+    </abilities>
+    <saves version="1.0">
+      <save base="0" magmod="0" miscmod="0" name="Fortitude" stat="Con"/>
+      <save base="0" magmod="0" miscmod="0" name="Reflex" stat="Dex"/>
+      <save base="0" magmod="0" miscmod="0" name="Will" stat="Wis"/>
+    </saves>
+  <inventory version="1.0">
+    <plat>0</plat>
+    <gold>0</gold>
+    <silver>0</silver>
+    <copper>0</copper>
+    <generalgear>None</generalgear>
+    <magicalgear>None</magicalgear>
+  </inventory>
+    <skills version="1.0">
+      <skill armorcheck="0" crossclass="0" misc="0" name="Alchemy" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Animal Empathy" rank="0" stat="Cha" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Appraise" rank="0" stat="Int" untrained="1"/>
+      <skill armorcheck="1" crossclass="0" misc="0" name="Balance" rank="0" stat="Dex" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Bluff" rank="0" stat="Cha" untrained="1"/>
+      <skill armorcheck="1" crossclass="1" misc="0" name="Climb" rank="0" stat="Str" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Concetration" rank="0" stat="Con" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Craft" rank="0" stat="Int" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Decipher Script" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Diplomacy" rank="0" stat="Cha" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Disable Device" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Disguise" rank="0" stat="Cha" untrained="1"/>
+      <skill armorcheck="1" crossclass="0" misc="0" name="Escape Artist" rank="0" stat="Dex" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Forgery" rank="0" stat="Int" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Gather Information" rank="0" stat="Cha" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Handle Animal" rank="0" stat="Cha" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Heal" rank="0" stat="Wis" untrained="1"/>
+      <skill armorcheck="1" crossclass="0" misc="0" name="Hide" rank="0" stat="Dex" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Innuendo" rank="0" stat="Wis" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Intimidate" rank="0" stat="Cha" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Intuit Direction" rank="0" stat="Wis" untrained="0"/>
+      <skill armorcheck="1" crossclass="0" misc="0" name="Jump" rank="0" stat="Str" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Arcana" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Architecture and Engineering" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Geography" rank="0" stat="Int" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: History" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Local" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nature" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nobility and Royalty" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: The Planes" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Religion" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Listen" rank="0" stat="Wis" untrained="1"/>
+      <skill armorcheck="1" crossclass="0" misc="0" name="Move Silently" rank="0" stat="Dex" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Open Lock" rank="0" stat="Dex" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Perform" rank="0" stat="Cha" untrained="1"/>
+      <skill armorcheck="1" crossclass="0" misc="0" name="Pick Pocket" rank="0" stat="Dex" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Profession" rank="0" stat="Wis" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Read Lips" rank="0" stat="Int" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Ride" rank="0" stat="Dex" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Scry" rank="0" stat="Int" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Search" rank="0" stat="Int" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Sense Motive" rank="0" stat="Wis" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Spellcraft" rank="0" stat="Int" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Spot" rank="0" stat="Wis" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Swim" rank="0" stat="Str" untrained="1"/>
+      <skill armorcheck="1" crossclass="0" misc="0" name="Tumble" rank="0" stat="Dex" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Use Magic Device" rank="0" stat="Cha" untrained="0"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Use Rope" rank="0" stat="Dex" untrained="1"/>
+      <skill armorcheck="0" crossclass="0" misc="0" name="Wilderness Lore" rank="0" stat="Wis" untrained="1"/>
+    </skills>
+    <feats version="1.0"/>
+    <spells version="1.0"/>
+    <powers version="1.0"/>
+    <divine version="1.0"/>
+    <pp current1="0" free="0" max1="0" maxfree="0" version="1.0"/>
+    <hp current="0" max="0" version="1.0"/>
+    <attacks version="1.0">
+      <melee base="0" fifth="0" forth="0" misc="0" second="0" sixth="0" third="0"/>
+      <ranged base="0" fifth="0" forth="0" misc="0" second="0" sixth="0" third="0"/>
+    </attacks>
+    <ac misc="" natural="" version="1.0"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/d20sites.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,15 @@
+
+<nodehandler class="static_handler" icon="browser" module="core" name="d20 Sites">
+  <nodehandler class="webbrowser_handler" icon="d20" module="core" name="d20 Adventures">
+    <link href="http://www.rpgarchive.com/index.php?sysid=37&amp;page=adv&amp;sort=Alpha"/>
+  </nodehandler>
+  <nodehandler class="webbrowser_handler" icon="browser" module="core" name="D&amp;D 3rd Edition (official site)   ">
+    <link href="http://www.wizards.com/dnd/Welcomex.asp"/>
+  </nodehandler>
+  <nodehandler class="webbrowser_handler" icon="browser" module="core" name="Open Gaming Foundation">
+    <link href="http://www.opengamingfoundation.org/"/>
+  </nodehandler>
+  <nodehandler class="webbrowser_handler" icon="browser" module="core" name="Eric Noah's DnD3e News ">
+    <link href="http://www.rpgplanet.com/dnd3e/"/>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/d20srd.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+
+<nodehandler class="link_handler" icon="html" module="core" name="d20 SRD">
+  <link href="http://www.opengamingfoundation.org/srd.html" icon="note"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/d20wizards.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,94 @@
+
+<nodehandler class="static_handler" icon="flask" module="core" name="d20 Wizards">
+  <nodehandler class="node_loader" icon="knight" module="core" name="New d20 character Tool">
+    <nodehandler class="d20char_handler" icon="knight" module="d20" name="d20 character">
+      <general>
+        <name>Player Name</name>
+        <player>Your Name</player>
+        <race>none</race>
+        <alignment abbr="LG">none</alignment>
+        <deity>none</deity>
+        <size acmodifier="0">none</size>
+        <height>none</height>
+        <weight>none</weight>
+        <age>none</age>
+        <gender>none</gender>
+        <eyes>none</eyes>
+        <hair>none</hair>
+        <speed>30</speed>
+      </general>
+      <classes level="0"/>
+      <abilities>
+        <stat abbr="Str" base="0" name="Strength"/>
+        <stat abbr="Dex" base="0" name="Dexterity"/>
+        <stat abbr="Con" base="0" name="Constitution"/>
+        <stat abbr="Int" base="0" name="Intelligence"/>
+        <stat abbr="Wis" base="0" name="Wisdom"/>
+        <stat abbr="Cha" base="0" name="Charisma"/>
+      </abilities>
+      <saves>
+        <save base="0" magmod="0" miscmod="0" name="Fortitude" stat="Con"/>
+        <save base="0" magmod="0" miscmod="0" name="Reflex" stat="Dex"/>
+        <save base="0" magmod="0" miscmod="0" name="Will" stat="Wis"/>
+      </saves>
+      <hp current="0" max="0"/>
+      <skills>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Alchemy" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Animal Empathy" rank="0" stat="Cha" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Appraise" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Balance" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Bluff" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="1" crossclass="1" misc="0" name="Climb" rank="0" stat="Str" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Concetration" rank="0" stat="Con" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Craft" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Decipher Script" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Diplomacy" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Disable Device" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Disguise" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Escape Artist" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Forgery" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Gather Information" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Handle Animal" rank="0" stat="Cha" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Heal" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Hide" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Innuendo" rank="0" stat="Wis" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Intimidate" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Intuit Direction" rank="0" stat="Wis" untrained="0"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Jump" rank="0" stat="Str" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Arcana" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Architecture and Engineering" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Geography" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: History" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Local" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nature" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nobility and Royalty" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: The Planes" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Religion" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Listen" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Move Silently" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Open Lock" rank="0" stat="Dex" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Perform" rank="0" stat="Cha" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Pick Pocket" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Profession" rank="0" stat="Wis" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Read Lips" rank="0" stat="Int" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Ride" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Scry" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Search" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Sense Motive" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Spellcraft" rank="0" stat="Int" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Spot" rank="0" stat="Wis" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Swim" rank="0" stat="Str" untrained="1"/>
+        <skill armorcheck="1" crossclass="0" misc="0" name="Tumble" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Use Magic Device" rank="0" stat="Cha" untrained="0"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Use Rope" rank="0" stat="Dex" untrained="1"/>
+        <skill armorcheck="0" crossclass="0" misc="0" name="Wilderness Lore" rank="0" stat="Wis" untrained="1"/>
+      </skills>
+      <feats/>
+      <attacks>
+        <melee base="0" misc="0"/>
+        <ranged base="0" misc="0"/>
+      </attacks>
+      <ac misc="" natural=""/>
+    </nodehandler>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/default_map.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,7 @@
+<nodehandler class="min_map" icon="compass" module="core" name="miniature Map">
+<map sizex='1000' sizey='1000' action='new'>
+<grid size='60'  mode='0' line='2' snap='1' color='#000000'/>
+<bg type='3' color='#008040'/>
+<miniatures serial='0'/>
+</map>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/die_macro.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2 @@
+
+<nodehandler class="dieroll_handler" icon="d20" module="core" name="Die Macro">example roll [1d20+4]</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/die_roller_notes.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,117 @@
+
+<nodehandler class="tabber_handler" icon="tabber" module="containers" name="Die Roller Notes" version="1.0">
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Basics" version="1.0">
+    <text multiline="1" send_button="1">
+The new dieroller is design with expansion in mind. While there are a number of new dieroller options in the base roller, the new design facilitates the building of new rollers that can be loaded at any time.  In this test build three are 3 rollers: std, d20, and wod.  The std roller is the generic roller.  It has generic dice options and is the base for all other dierollers. The d20 and wod rollers are game specific rollers and have game specific options.  They also serve as examples for how to create your own rollers in python.
+
+** Please not that this is our initial release of the roller.  The new syntax might see odd to you.  We are considering an alternative syntax, and this is being discussed on the openrpg.com forums. In if you have strong opinions on this, you might want to hop over there and give your 2 cents. **
+
+Dierollers:
+You can see what roller you are using by using the &quot;/dieroller&quot; command in chat.  By default it should be &quot;std&quot;.  To set the die roller, use &quot;/dieroller roller_name&quot;.  So to load the d20 roller, type &quot;/dieroller d20&quot;. Its easy!
+
+Basic Syntax.
+The basic syntax is the same, 3d6+3, rolls three six side dice plus 3.  However, the new roller has other options, they look like this:
+
+3d6.option(value)
+
+If you know anything about programming, that probably looks familiar.  For average users, this might look a little confusing, but lets look at a real example.
+
+[10d6.takeLowest(2)]
+
+Now this option rolls 10d6 and takes the lowest two rolls. Basically, to use an option, you have put a . + the option name + the values for the option between ( ).  You can also chain many options together.
+
+[10d10.minroll(4).takeLowest(5)]
+
+This example rolls 10d10 with a minimum roll of 4 and takes the lowest 5.  Pretty nifty if I do say so myself.
+</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="STD rollers" version="1.0">
+    <text multiline="1" send_button="1">
+Now that you know how to roll dice, lets look at the standard options.
+
+takeHighest - take highest X rolls
+
+[10d10.takeHighest(4)] - takes highest 4
+
+takeLowest - take lowest x rolls
+
+[10d10.takeLowest(4) - take lowest 4
+
+minroll - minimum low range
+
+[10d10.minroll(4)] - no die roll lower than 4
+
+extra - roll an extra die when roll greater or equal to X
+
+[10d10.extra(9)] - roll an extra die when a die roll is 9 or higher.
+
+open - same as extra but roll extra dice until a die is not greater or equal to X (even the extra roll).
+
+[10d10.open(9)] - roll extra dice until a die roll is not 9 or higher.
+
+each - apply X value to all dice
+
+[10d10.each(2)] - add 2 to every die roll
+</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="d20 roller" version="1.0">
+    <text multiline="1" send_button="1">
+Remember, to use the d20 roller type: &quot;/dieroller d20&quot;
+
+dc(DC,mod) - make a DC check.
+
+[1d20.dc(20,5)] - make a DC check against DC value of 20 and a modifier of +5.
+
+attack(AC,mod,critical) - make an attack roll.
+
+[1d20.attack(20,5,19) - make an attack roll against AC 20 with a modifier of +5 and a critical range of 19-20.
+</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="wod roller" version="1.0">
+    <text multiline="1" send_button="1">
+Remember, to use the wod roller type: &quot;/dieroller wod&quot;
+
+vs(target) - vs roll against target
+
+[3d10.vs(5)] - vs roll against 5.</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Hero Roller" version="1.0">
+    <text multiline="1" send_button="0">
+Skill Roller, example [3d6.sk(11,0)]--
+Make a SKill roll.  The first number of the two modifiers is the rating in the skill, 11 meaning 11 or less.  The second number is any penalty or bonus you have for the roll.  A positive number is a bonus, a negative number is a penalty.  As with many Hero system rolls, the only die choice that makes sense is 3d6
+
+To-hit roller, example  [3d6.cv(5,1)]
+Make a to-hit roll.  The first modifier is your Combat Value.  The second number is any penalty or bonus you have for the roll.  A bonus is positive, and a penalty is negative.  Again, the only roll that is sensible is 3d6.  The result of the roll is the the highest Defensive Combat Value that can be hit with that roll.
+
+Killing damage roller, example [(1d6+1d6/2).k(0)]
+Make a damage roll for Killing damage.  The only modifier is the bonus to the stun multiplier.  A 1 in that field would indicate an increased stun multiplier of +1.  The result shows body and stun totals.  Only sensible for d6 values.
+
+Normal damage roller, example [(5d6+1d6/2).n()]
+Make a damage roll for Normal damage. Results show body and stun totals.  No modifiers exist.  Only sensible for d6 values.
+
+Hit Location roller, example [3d6.hl()]
+Roll on the hit location chart. Results show the location hit (including left or right side) and multipliers to damage when hitting that location.  No modifiers exist. Contributed by Heroman
+
+Basic Killing damage roller, example [2d6.hk()]
+Make a damage roll for Killing damage.  Always uses a stun multiplier of 1 for ease of use with the Hit Location roller mentioned above.  No modifiers exist.  Contributed by Heroman
+</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="? Option" version="1.0">
+    <text multiline="1" send_button="1">
+Another new feature is the ? option.  If you place a ? in a dice string you will be prompt by a dialog for the value.  This is useful when using die rolls in character sheets. From example:
+
+[3d6+?] - will ask you for a value to replace ?.
+[3d6+?StrMod] will ask you for a value to replace ? and give you the Hint that it should be your StrMod. This Hint system can be used for as many ? as you have in your roll
+[?NumDice}d6+?StrMod+?Weapon Bonus+?Misc Bonus] - If you notice the } after NumDice, that tells the Hint system to stop looking for Alpha character, otherwise it would try to include the d in it's hint.
+
+A more game specific example might look like this:
+
+[1d20.dc(?DC,5)] - this will prompted you for the ? value, which is the DC.</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="The End" version="1.0">
+    <text multiline="1" send_button="1">Well, that's all I have to say about the new roller.  More options and game specific rollers on the way.  If you're interested in coding a roller for your favorite game, drop by the dev server and we'll try and help you out.
+
+-Chris Davis
+</text>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/dnd3.5.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,106 @@
+
+<nodehandler class="dnd35char_handler" icon="knight" module="dnd35" name="DnD 3.5 Character">
+<general name="General">
+    <name>Character Name</name>
+    <player>Your Name</player>
+    <race>none</race>
+    <alignment abbr="LG">none</alignment>
+    <deity>none</deity>
+    <size acmodifier="0">none</size>
+    <height>none</height>
+    <weight>none</weight>
+    <age>none</age>
+    <gender>none</gender>
+    <eyes>none</eyes>
+    <hair>none</hair>
+    <speed>30</speed>
+    <currentxp>0</currentxp>
+    <xptolevel>1000</xptolevel>
+</general>
+<character name="Classes and Stats">
+  <classes level="0"/>
+  <abilities>
+    <stat abbr="Str" base="0" name="Strength"/>
+    <stat abbr="Dex" base="0" name="Dexterity"/>
+    <stat abbr="Con" base="0" name="Constitution"/>
+    <stat abbr="Int" base="0" name="Intelligence"/>
+    <stat abbr="Wis" base="0" name="Wisdom"/>
+    <stat abbr="Cha" base="0" name="Charisma"/>
+  </abilities>
+  <saves>
+    <save base="0" magmod="0" miscmod="0" name="Fortitude" stat="Con"/>
+    <save base="0" magmod="0" miscmod="0" name="Reflex" stat="Dex"/>
+    <save base="0" magmod="0" miscmod="0" name="Will" stat="Wis"/>
+  </saves>
+</character>
+<inventory  name="Inventory">
+<Platinum>0</Platinum>
+<Gold>0</Gold>
+<Silver>0</Silver>
+<Copper>0</Copper>
+<Gear>None</Gear>
+<Magic>None</Magic>
+<Languages>Common</Languages>
+</inventory>
+<snf name="Skills and Feats">
+  <skills>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Appraise" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Autohypnosis" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Balance" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Bluff" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="1" misc="0" name="Climb" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Concentration" rank="0" stat="Con" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Craft" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Decipher Script" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Diplomacy" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Disable Device" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Disguise" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Escape Artist" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Forgery" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Gather Information" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Handle Animal" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Heal" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Hide" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Intimidate" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Jump" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Arcana" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Architecture and Engineering" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Dungeoneering" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Geography" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: History" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Local" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nature" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nobility and Royalty" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Religion" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: The Planes" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Psionics" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Listen" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Move Silently" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Open Lock" rank="0" stat="Dex" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Perform" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Profession" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Psicraft" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Ride" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Search" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Sense Motive" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Sleight of Hand" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Spellcraft" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Spot" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Survival" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Swim" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Tumble" rank="0" stat="Dex" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Magic Device" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Psionic Device" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Rope" rank="0" stat="Dex" untrained="1"/>
+  </skills>
+  <feats/>
+</snf>
+<combat name="Combat">
+  <hp current="0" max="0"/>
+  <attacks>
+    <melee base="0" misc="0" second="0" third="0" forth="0" fifth="0" sixth="0"/>
+    <ranged base="0" misc="0" second="0" third="0" forth="0" fifth="0" sixth="0"/>
+  </attacks>
+  <ac misc="" natural=""/>
+</combat>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/dnd3e.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,135 @@
+
+<nodehandler class="dnd3echar_handler" icon="knight" module="dnd3e" name="DnD 3E Character">
+<howtouse>
+<howto>
+To use this you do:
+
+To use this you just need to add all the stuff you wish, set your stats etc., then click on the
++(plus) next too the gears, and right click. This will cause it to roll what is needed to roll
+
+If you double left-click on the character's highest node, you will get a tabbed
+panel which will allow you to edit any attribute of the character.
+
+In the attacks section Base2 - Base6 are for additional attacks at a diff base then your first
+If you set these, it will automaticly roll the additional attacks for each base you set when
+you right click on the weapon you wish to attack with.
+
+</howto>
+</howtouse>
+  <general>
+    <name>Character Name</name>
+    <player>Your Name</player>
+    <race>none</race>
+    <alignment abbr="LG">none</alignment>
+    <deity>none</deity>
+    <size acmodifier="0">none</size>
+    <height>none</height>
+    <weight>none</weight>
+    <age>none</age>
+    <gender>none</gender>
+    <eyes>none</eyes>
+    <hair>none</hair>
+    <speed>30</speed>
+    <currentxp>0</currentxp>
+    <xptolevel>1000</xptolevel>
+  </general>
+<character name="Classes and Stats">
+  <classes level="0"/>
+  <abilities>
+    <stat abbr="Str" base="0" name="Strength"/>
+    <stat abbr="Dex" base="0" name="Dexterity"/>
+    <stat abbr="Con" base="0" name="Constitution"/>
+    <stat abbr="Int" base="0" name="Intelligence"/>
+    <stat abbr="Wis" base="0" name="Wisdom"/>
+    <stat abbr="Cha" base="0" name="Charisma"/>
+  </abilities>
+  <saves>
+    <save base="0" magmod="0" miscmod="0" name="Fortitude" stat="Con"/>
+    <save base="0" magmod="0" miscmod="0" name="Reflex" stat="Dex"/>
+    <save base="0" magmod="0" miscmod="0" name="Will" stat="Wis"/>
+  </saves>
+</character>
+<inventory>
+<Platinum>0</Platinum>
+<Gold>0</Gold>
+<Silver>0</Silver>
+<Copper>0</Copper>
+<Gear>None</Gear>
+<Magic>None</Magic>
+<Languages>Common</Languages>
+</inventory>
+<snf name="Skills and Feats">
+  <skills>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Alchemy" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Animal Empathy" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Appraise" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="AutoHypnosis" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Balance" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Bluff" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="1" misc="0" name="Climb" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Concentration" rank="0" stat="Con" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Craft" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Decipher Script" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Diplomacy" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Disable Device" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Disguise" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Escape Artist" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Forgery" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Gather Information" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Handle Animal" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Heal" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Hide" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Innuendo" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Intimidate" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Intuit Direction" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Jump" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Arcana" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Architecture and Engineering" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Geography" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: History" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Local" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nature" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Nobility and Royalty" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: The Planes" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Psionics" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Knowledge: Religion" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Listen" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Move Silently" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Open Lock" rank="0" stat="Dex" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Perform" rank="0" stat="Cha" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Pick Pocket" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Profession" rank="0" stat="Wis" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Psicraft" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Read Lips" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Remote View" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Ride" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Scry" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Search" rank="0" stat="Int" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Sense Motive" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Spellcraft" rank="0" stat="Int" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Spot" rank="0" stat="Wis" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Stabilize Self" rank="0" stat="Con" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Swim" rank="0" stat="Str" untrained="1"/>
+    <skill armorcheck="1" crossclass="0" misc="0" name="Tumble" rank="0" stat="Dex" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Magic Device" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Psionic Device" rank="0" stat="Cha" untrained="0"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Use Rope" rank="0" stat="Dex" untrained="1"/>
+    <skill armorcheck="0" crossclass="0" misc="0" name="Wilderness Lore" rank="0" stat="Wis" untrained="1"/>
+  </skills>
+  <feats/>
+</snf>
+<snp name="Spells and Powers">
+  <spells/>
+  <powers/>
+<divine/>
+  <pp current1="0" free="0" max1="0" maxfree="0"/>
+</snp>
+<combat name="Combat">
+  <hp current="0" max="0"/>
+  <attacks>
+    <melee base="0" misc="0" second="0" third="0" forth="0" fifth="0" sixth="0"/>
+    <ranged base="0" misc="0" second="0" third="0" forth="0" fifth="0" sixth="0"/>
+  </attacks>
+  <ac misc="" natural=""/>
+</combat>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/encounter.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,8 @@
+
+<nodehandler class="static_handler" icon="compass" module="core" name="Encounter">
+  <nodehandler class="text_handler" icon="note" module="core" name="PC Description">None</nodehandler>
+  <nodehandler class="text_handler" icon="note" module="core" name="GM Info">None</nodehandler>
+  <nodehandler class="text_handler" icon="goblin" module="core" name="Creatures">None</nodehandler>
+  <nodehandler class="text_handler" icon="money" module="core" name="Treasure">None</nodehandler>
+  <group_atts border="1" cols="1"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/form.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+
+<nodehandler class="form_handler" icon="form" module="forms" name="Form" version="1.0">
+  <form height="400" width="400"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/grid.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,16 @@
+
+<nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Grid" version="1.0">
+  <grid autosize="1" border="1">
+    <row version="1.0">
+      <cell/>
+      <cell/>
+    </row>
+    <row version="1.0">
+      <cell/>
+      <cell/>
+    </row>
+  </grid>
+  <macros>
+    <macro name=""/>
+  </macros>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/group.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+
+<nodehandler class="group_handler" module="containers" name="Group" version="1.0">
+  <group_atts border="1" cols="1"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/image.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+
+<nodehandler class="webimg_handler" icon="image" module="forms" name="Image" version="1.0">
+  <link href="http://openrpg.wrathof.com/splash1.jpg"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/link.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+
+<nodehandler class="link_handler" icon="html" module="forms" name="Link" version="1.0">
+  <link href="http://www.openrpg.com"/>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/listbox.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,8 @@
+
+<nodehandler class="listbox_handler" icon="gear" module="forms" name="List Box" version="1.0">
+  <list send_button="0" type="0">
+    <option selected="1" value="">Option Text I</option>
+    <option selected="0" value="">Option Text II</option>
+    <option selected="0" value="">Option Text III</option>
+  </list>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/macro.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+
+<nodehandler class="macro_handler" icon="gear" module="chatmacro" name="Macro" version="1.0">
+  <text>/me used a blank macro</text>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/minlib.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2 @@
+
+<nodehandler class="minilib_handler" icon="gear" module="minilib" name="Miniature Library" version="1.0"/>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/openrpg_links.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,13 @@
+
+<nodehandler class="static_handler" icon="browser" module="core" name="OpenRPG Resources">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="webbrowser_handler" icon="compass" module="core" name="Miniature Maps">
+    <link href="http://www.openrpg.com/map.php"/>
+  </nodehandler>
+  <nodehandler class="webbrowser_handler" icon="chess" module="core" name="Miniature Icons">
+    <link href="http://www.openrpg.com/mins.php"/>
+  </nodehandler>
+  <nodehandler class="webbrowser_handler" icon="html" module="core" name="Forums">
+    <link href="http://www.openrpg.com/forum.php"/>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/split.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2 @@
+
+<nodehandler class="splitter_handler" icon="divider" module="containers" name="Splitter" version="1.0"/>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/tabber.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2 @@
+
+<nodehandler class="tabber_handler" icon="tabber" module="containers" name="Tabber" version="1.0"/>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/text.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,3 @@
+
+<nodehandler class="text_handler" icon="note" module="core" name="Text Block">
+some text here</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/textctrl.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+
+<nodehandler class="textctrl_handler" icon="note" module="forms" name="Text" version="1.0">
+  <text multiline="0" send_button="0">text</text>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/u_idiots_guide_to_openrpg.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,457 @@
+
+<nodehandler class="tabber_handler" icon="tabber" module="containers" name="The Ultimate Idiot's Guide to OpenRPG" version="1.0">
+  <nodehandler class="form_handler" icon="form" module="forms" name="About" version="1.0">
+    <form height="240" width="400"/>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="OpenRPG" version="1.0">
+      <text multiline="1" send_button="0">OpenRPG is an online chat program designed to let people all around the globe roleplay together.  It supports the use of maps, character sheets, dice, specialized tools, and much more.  We welcome you to the OpenRPG family.</text>
+    </nodehandler>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="This Tutorial" version="1.0">
+      <text multiline="1" send_button="0">This Tutorial was created by Woody for use with OpenRPG. It was thrown together and updated for use with 1.0 and beyond by Raburn. A special thanks goes to HInc for the material that they made for use in this tutorial.</text>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Introduction" version="1.0">
+    <text multiline="1" send_button="0">Welcome, Mateys, to the online gaming universe of the OpenRPG!  By now you have successfully downloaded and installed the program or you would not be able to read this document.  But we are not here to discuss what you already know, we are here to get around to telling you how to do those things you don't know.</text>
+  </nodehandler>
+  <nodehandler class="form_handler" icon="form" module="forms" name="Chatting" version="1.0">
+    <form height="400" width="400"/>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Art Of The Chat" version="1.0">
+      <text multiline="1" send_button="0">The basics:
+
+Alright.  Let us start at the beginning.  Many many years ago the dinosaurs roamed the planet.  But then an asteroid, now most commonly known as &quot;Bill Gates 1,&quot; crashed into the earth and wiped them all out, turning them into oil.  The basic upshot of all that is with this oil we have created electricity which is now running the program.
+
+Before we start it is necessary to make a name for yourself.  Why?  Well, you wouldn't like to be going around with everybody knowing only as 'blankman' would you?  To type in your name, go up to the top left window, and dragging down the &quot;OpenRPG&quot; menu bar press &quot;Settings&quot;.  in there you can alter your name and colours.
+
+You can also rename yourself in a much more simple way by typing in the command &quot;/name &quot; and your name behind it in the chat window.  I just made you go into the settings so that you know where they are later.
+
+--------------------------------------------------------------------------------
+
+Now that you have yourself labled, you are probably wondering &quot;where's all the chat?&quot;  Well, my friend, we are here to answer that.  As your program booted up, it should have brought up a four-window layout.  The Gametree Window is in the top-left, the Player Window is the bottom-left, The Chat Window is the bottom-right, and the Map Window is the top-right.  We will first concern outselves with getting that lower right window to do the work and play with the others later.
+
+To get the chat window to work, first we actually need a place to chat.  To do this, we need to browse the list of rooms on what is known as the Tracker.  To open the tracker window go to the menubar on the top and under the menu &quot;Game Server&quot; click on &quot;Browse Tracker&quot;
+
+As you can see, a new window popped up.   This is the Tracker Window.  On the left you will see a list of various servers running OpenRPG online.  Click on the one at the top of the list and press &quot;Connect&quot;.
+
+After pressing the &quot;Connect&quot; button you will join that server and be instantly dropped into it's lobby.  Welcome to your first chatroom.  Feel free to stay in this room as long as you like or move off to another room listed on the Tracker.  When you first turned on OpenRPG the chat window filled up with all the different chat commands available.  If you are like me and have forgotten them by now you can either click on the text display box and press PageUp   or click on the little text entry box and type in &quot;/help&quot;.
+
+Should you wish a way to scroll up instead of 'PageUp'ing you can increase the amount of lines the Chat Window stores by making the Buffer Size larger.  You can do this by changing the &quot;buffersize&quot; setting in the Settings menu or by using /lines [number] replacing [number] with the number of lines..  A good setting is 100-200 lines though you can have it as large as you want (though it is warned against having it larger than 1000 as large numbers tend to increase lag.)
+
+------------------------------------------------------------------------------
+
+By now I'm sure you are also wondering what that status thingy in the bottom left window is.  If you notice, every time you type a message in the chat window your status changes.  And when you finish your msg (or sit for 5 seconds waiting) it changes back to Idle.  What a clever invention.  Of course, if you wish to set your own status to override &quot;Idle&quot; temporarily, just type in the chat window &quot;/status My_Status&quot;; replacing &quot;My_Status&quot; for whatever msg you wish (no more than 18 letters can be seen though).
+
+As for now, spend some time and enjoy yourself in the lobby... we can come back to the tutorial when you are ready to learn about Rolling dice, whispering, and creating and using character sheets.</text>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="form_handler" icon="form" module="forms" name="Dice Rolling" version="1.0">
+    <form height="400" width="400"/>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Rolling Dice" version="1.0">
+      <text multiline="1" send_button="0">Dice:
+
+I'm astonished.  82% of all people who use this program never bother to read the tutorial, let alone come back to it.  Then again 67% of all statistics are made up on the spot so we'll leave that for now and get on with what we are doing
+
+Now, to get back to business.  By now you've hopefully held a conversation with someone (or at least yourself) in our chatroom and now I'm sure you're wondering &quot;How can I roll some dice so I can smite those foolish mortals.&quot;  Well, don't be discouraged, because that is what I'm here for.
+
+To roll dice you have two options.  you can simply press the button on the dice toolbar  or you can type in how many times you wish to roll.  As pressing buttons is mostly self explanitory, this part will only cover typing in dicerolls.
+
+The second way is a little more advanced, and powerful.  You can also roll dice by typing them into your text intry box under the dice toolbar (where you type your normal chat messages).  to do so, first choose how many dice you want to roll.  example:
+
+1
+
+now, put the letter &quot;d&quot; after that.
+
+1d
+
+then put the dice type (number of sides):
+
+1d20
+
+And to finish it off, put the square [ ] brackets around the dice and hit enter
+
+[1d20]
+
+you'll see it rolls a dice into the chat window.  Magic. we can even do things like having multiple types of dice inside it in various mathmatical combinations:
+
+[1d20+4d6-12]
+[3d8+2]
+[1d1-1]
+
+
+------------------------------------------------------------------------
+
+ now we let the fun begin.  Inside the square brackets we can also add special modifiers.  it'll come in the following format:
+
+[ 1d20.mod(#) ]
+
+you can even have multiple modifiers if you wish.  the list of all of them are below:
+
+.ascending()
+lists your dicerolls from lowest to highest
+
+.descending()
+lists your dicerolls from highest to lowest
+
+.takeHighest(#)
+takes the highest # amount of rolls
+
+.takeLowest(#)
+takes the lowest # amount of rolls
+
+.extra(#)
+rolls 1 extra dice for every Original roll that gets above the #
+
+.open(#)
+rolls an extra dice for Every roll that gets above the #
+
+.minroll(#)
+no dice will roll below #
+
+.each(#)
+adds # to each diceroll
+</text>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="form_handler" icon="form" module="forms" name="Whispering and Ignoring" version="1.0">
+    <form height="400" width="400"/>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Whispering and Ignoring" version="1.0">
+      <text multiline="1" send_button="0">Of course, not everything should be heard.  This is true twice over in this chapter, as we will be discussing how to whisper and how to ignore.
+
+Whispering is a very simple task in OpenRPG.  First type in &quot;/w &quot; in the text entry box.  next, type in the ID number of the person you wish to whisper to and add an equals sign, so it looks like this:  &quot;/w 4=&quot;.  Add your message to the end of that and hit enter.
+
+/w 4=Hello ID number 4.
+
+You can even have multiple ID numbers in there.   Just seperate them up with a comma between each one, like so:
+
+/w 3, 4, 12=Hey, I dare you to poke that Woody
+
+Of course, if you don't want to type out the weird ID numbers you can always just right click on those little Easter Island Heads next to the person's name in the bottom left player window.  You can then select &quot;whisper&quot; and it will put the whisper command into your text entry box automatically.
+
+-------------------------------------------------------------------------------
+
+Sometimes there is someone that will just really get on your nerves.  Be it that they keep sending the same line of text to the chat, or maybe that they are spouting out Britney Spears lyrics nonstop.  Either way, you'll probably want to hear less of them.
+
+Well fear not!  for by just typing in &quot;/i their_ID_#&quot; (changing &quot;their_ID_#&quot; for their ID number) and hitting Enter, then they will be added to your ignore list and no further messages will be heard by them.  Should you wish to take them off again (in case you go the wrong guy) just type the same thing and it'll toggle it off.  You can also get a list of who's on your ignore list just by typing in &quot;/i&quot;
+
+Simple as that.</text>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="form_handler" icon="form" module="forms" name="Nodes and Character Sheets" version="1.0">
+    <form height="400" width="400"/>
+    <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Nodes" version="1.0">
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="&lt;b&gt;The Basics&lt;/b&gt;" version="1.0">
+        <text multiline="1" send_button="0">&lt;a name=&quot;c3&quot;&gt;&lt;/a&gt;
+alright.. by now you have grasped the basics on how to chat, so we will move on to the next phase: character sheets.  This part of the tutorial will cover how to make a full usable document from the nodes.
+
+Let us first start by describing what the different nodes are and where we can find them</text>
+      </nodehandler>
+      <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Nodes" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Nodes and Sheets" version="1.0">
+          <text multiline="1" send_button="0">------------------------------------------------------------------------
+--Character Sheets And Nodes
+------------------------------------------------------------------------
+
+Character sheets are the wonderful inventions that allow us to hold all the information we want about our characters and gaming worlds.  In OpenRPG they are made up of what we call &quot;Nodes.&quot;  These nodes are the building blocks for creating all manner of character sheets and can be rearranged into whatever form you wish.
+
+but to understand the nodes, we must first know how they work.  This chapter is dedicated to explaining the function of each node.  To get a new node, just find the one you want in the &quot;Templates&quot; tree and doubleclick on it.  That will put a fresh blank one in the gametree at the top.</text>
+        </nodehandler>
+        <nodehandler class="form_handler" icon="form" module="forms" name="Nodes" version="1.0">
+          <form height="700" width="400"/>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="The Text Node" version="1.0">
+            <text multiline="1" send_button="1">The Text Node is the very basic and most fundamental part of the character sheet.  It is where we store most of our important information.  To use a text node, first you must right click on it and press &quot;design.&quot;  This will bring up the text node's option's window.  In there you can choose whether the Text node only has one line or if it is a small text window, what it's title is, and whether it has a send button or not.  The Send button will send whatever is in the text node directly to the chat.</text>
+          </nodehandler>
+          <nodehandler class="listbox_handler" icon="gear" module="forms" name="List Box" version="1.0">
+            <list send_button="0" type="0">
+              <option selected="1" value="0">The List Box is a marvelous tool that allows you to have ...</option>
+              <option selected="0" value="0">dropdown menus and a variety of checklists and radio boxes.</option>
+              <option selected="0" value="0">To use a List box, right click on one that you created in your</option>
+              <option selected="0" value="0">gametree and press &quot;design&quot;.  this will bring up a window </option>
+              <option selected="0" value="0">where you can choose what type of list box it is and what </option>
+              <option selected="0" value="0">options are inside of it.  Simple as that!</option>
+            </list>
+          </nodehandler>
+          <nodehandler class="rpg_grid_handler" icon="grid" module="rpg_grid" name="Grid" version="1.0">
+            <grid autosize="1" border="1">
+              <row version="1.0">
+                <cell size="144">The Grid lets you keep tables</cell>
+                <cell size="138"></cell>
+              </row>
+              <row version="1.0">
+                <cell></cell>
+                <cell>like stats, or saving throws.</cell>
+              </row>
+              <row version="1.0">
+                <cell>You can arrange numbers</cell>
+                <cell></cell>
+              </row>
+              <row version="1.0">
+                <cell></cell>
+                <cell>more easily in a grid than</cell>
+              </row>
+              <row version="1.0">
+                <cell>you can with a text node.</cell>
+                <cell></cell>
+              </row>
+            </grid>
+            <macros>
+              <macro name=""/>
+            </macros>
+          </nodehandler>
+          <nodehandler class="webimg_handler" icon="image" module="forms" name="Image" version="1.0">
+            <link href="http://my.openrpg.com/images/logo200x60.jpg"/>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Images allow for images. Just right click to provide the webpage addy they're at" version="1.0">
+            <text multiline="0" send_button="0"></text>
+          </nodehandler>
+          <nodehandler class="link_handler" icon="html" module="forms" name="OpenRPG.com link" version="1.0">
+            <link href="http://www.OpenRPG.com"/>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name=" " version="1.0">
+            <text multiline="0" send_button="0">you can change the link node's url by right clicking on it in the gametree</text>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name=" " version="1.0">
+            <text multiline="1" send_button="0">Now that we know what the individual nodes do, we need ways to group them together.  continue to the next page to learn about the different forms of groups.</text>
+          </nodehandler>
+        </nodehandler>
+        <nodehandler class="form_handler" icon="form" module="forms" name="containers" version="1.0">
+          <form height="700" width="700"/>
+          <nodehandler class="form_handler" icon="form" module="forms" name="Form" version="1.0">
+            <form height="400" width="400"/>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name="Text" version="1.0">
+              <text multiline="1" send_button="0">The Form is the basic and most used container of the lot.  It is what is able to hold all the other types of containers AND nodes.  Forms can also be placed within other containers as well, allowing for you to 'group up' a bunch of nodes in a form, then put that form as one page on a tabber. You can adjust the size by right clicking on a form in the game tree, pressing design, and adjusting the height and width</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="splitter_handler" icon="divider" module="containers" name="Splitter" version="1.0">
+            <nodehandler class="splitter_handler" icon="divider" module="containers" name="Splitter" version="1.0">
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Text" version="1.0">
+                <text multiline="0" send_button="0">text</text>
+              </nodehandler>
+              <nodehandler class="textctrl_handler" icon="note" module="forms" name="Text" version="1.0">
+                <text multiline="1" send_button="0">text</text>
+              </nodehandler>
+            </nodehandler>
+            <nodehandler class="textctrl_handler" icon="note" module="forms" name=" " version="1.0">
+              <text multiline="1" send_button="0">The splitter allows you to break a character sheet into segments, and have different datas side by side.  you can't have more than two single things inside a splitter, however you can put forms (or even other splitters!) inside a splitter.</text>
+            </nodehandler>
+          </nodehandler>
+          <nodehandler class="textctrl_handler" icon="note" module="forms" name="Folders" version="1.0">
+            <text multiline="1" send_button="0">Another node that is useful for keeping everything together is the folder. You can only view the stuff in it by using pretty print, but if you want to save a lot of work in one area, it is the thing you want to use.</text>
+          </nodehandler>
+        </nodehandler>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Character Sheets" version="1.0">
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Moving Characters from almost ANY Character Generator to here." version="1.0">
+        <text multiline="1" send_button="0">The following are stpes on how to move your character sheets from your favorite Character Generation program into OpenRPG.  It's very quick and clean cut.
+
+1) first, using your character generator, you convert your character to either txt format, or, preferably (if you have the ability) HTML format.
+
+2) Then, you go to wizards.put a text block into the gametree
+
+3) if you are using txt, select all, and copy.  if you are using HTML format, edit it using notepad, but don't change any of the HTML Program. then select all and copy
+
+4) now, going back into OpenRPG, right click on the text block you created and push 'edit'
+
+5) clear the text block of the few words of text that appear there then paste the copied info into by either rightclicking-paste or pressing 'ctrl v'
+
+6) then name your character in the Title then close the text block.  Finished
+
+no more need to create character sheets!  That simple and any time you want to see it, just doubleclick on the text node.</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Create Your Own Character" version="1.0">
+        <text multiline="1" send_button="0">((before we begin, see the character sheet repository:
+   http://openrpg.wrathof.com
+for pre-made character sheets.))
+
+
+Character Sheet creation (By Melanthos):
+
+So you wanna make a character sheet, huh? Well, follow these few short steps and you'll be on your way to Sheet Creation Godhood...or not. But don't blame me if you don't read this and can't make yourself a sheet. I'm going to guess you have a basic understanding of how the nodes work. I'll be using Harvester Incorporated(TM)'s Faction Wars(TM) as an example.
+
+First, you'll need a tabber. You can find it under containers in the templates node.  Click the + to the left of templates then click the plus next to containers.  Next double click the Create New Tabber.  Got it? Good. Now, you'll need a form. Forms are what we use to create each page of the tabber. They hold the rest of the nodes neatly, and open up - unlike groups, which cannot be opened in a sheet. To create a form double click on Create New Form.  You will notice two new nodes at the top of the game tree.
+
+
+        Tabbers, Forms, and Text-boxes, OH MY!
+
+Alright. Here is where I store the basics of the character. Since I don't like to scroll too much, I've broken down the information even more, so that I can fit into smaller tabbers and forms. First I'll create a second tabber. This will hold all the basic info - Level, Class, Player &amp; Character name, and a few other things. Since I've got 8 things I want to list, I've broken them up into 2 seperate groups, which have been put into different forms: General Info, and Specific Info. For simple things like name and level, I suggest using single-line text boxes. I've gone ahead and made 8 of these, naming each of them one of my 8 items and stuck them into their forms. Now that I've finished that, I'll put the two forms under my second tabber, and put that into my big form, &quot;General.&quot; I've done the same thing with the Attributes. Most of the sheet uses this principal, so I won't go into each form. Kinda redundant.
+
+
+                   List Boxes &amp; Die Creation
+
+Alright, for my second form, Vitals, I need some die-rollers. Since I don't need to change the rolls often, I've decided to use List Boxes to hold all my rolls. It's not too hard, and it's a really easy way to store rolls(It can be used for other things, but this is the best use I've found.): Now you don't have to type out those dice every time you wanna attack. First, I've created myself a List Box. Next, I'll decide how I want my list box to be shown: There are four choices. Drop Down, List Box, Radio Box, and Check List. Since I only need to roll one at a time, I've chosen Drop Down. But to show you how the Drop Down and Check List options work, I'll change things a bit and make a List Box and a Check List as well. When editing, make sure to click the &quot;Send Button&quot; box.
+
+
+        Drop Down
+
+The Drop Down is the least space- consuming of the List Box options. It's just like a single-line Text Box, except it holds more than one line. Click the arrow on the right to make it drop down, and click the die you want to roll. Then hit the Send button, and POOF! Your die has been rolled to the chat window.
+
+
+        Check List
+
+The Check List is a neat option. It lets you roll more than one die at a time. All you have to do is check the box next to the die (or dice) you want to roll, and POOF! Your dice have been rolled. Neat, eh?
+
+           Splitter
+
+Now what could these be used for, you wonder? Not much, but there are a few things. I've used it to hold my different abilities. It's easier for me to just see my choices than to flip between tabbers or scroll down and back up every few seconds.
+
+
+The rest is pretty easy. Just keep on with the things you've done so far, and in no time you'll be on your way to creating great sheets! Most of sheet creation is your personal preference. If you ask Woody, he might say something completely different than I might. Of course, you should make the sheets the way you want them. This is just to get you started. After you've got the basics of sheet making down, you can play around with sheets and nodes and things until you've got the sheet perfect for you. GOOD LUCK!!
+
+
+--Melanthos, Founder &amp; Game Designer - Harvester Incorporated</text>
+      </nodehandler>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="form_handler" icon="form" module="forms" name="Server Control" version="1.0">
+    <form height="400" width="400"/>
+    <nodehandler class="tabber_handler" icon="tabber" module="containers" name="Server Control" version="1.0">
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Running Your Own Server" version="1.0">
+        <text multiline="1" send_button="0">Creating a server:
+
+OpenRPG has come a long way since the dark ages of gaming.  With this new version we can now create servers on-the-fly!  No more black boxes or strange voodoo magic.  Just click on your &quot;Game Server&quot; menu, and select &quot;Start new server.&quot;  Fill in the options and away you go!  the server will appear instantly on the list.
+
+Of course, this won't provide you with all the same powers that the full server does.  While you can create a server on the short term, if you want to have a longterm dedicated server running which you can control, you'll want to run the OpenRPG Server in your start menu-programs-openrpg (or mplay_server.py in your openrpg folder).
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Running a Dedicated Server" version="1.0">
+        <text multiline="1" send_button="0">Well, maybe not any money in creating a server on this program, but there are definately perks to running one.  But to run one we must first understand what they do.
+
+A server is the mother for all of us on OpenRPG.  It runs all of our major commmands, keeps us all talking together, and hosts all the games on OpenRPG.  When you first connected to OpenRPG you had to choose a server from a list in the tracker, on the left.  Now let us put your name (or at least your computer's) out there so others can flock to you.
+
+For this we must return to your OS.  Go into your computer, into the OpenRPG folder.  In there, you will see a file marked:
+
+mplay_server.py
+
+You can also find it in your start menu, right next to the OpenRPG program itself.  Once you find it, (double)click on it.  The first thing that will come up is a python MSDOS window... It will first prompt on if you wish for your server to bee seen by the OpenRPG tracker list.  Press &quot;Y&quot; and hit enter (won't get anyone if we don't know it exists).  Next, it will prompt you for a name for your Server.  Write in &quot;Temporary Server&quot; and press enter.
+
+Now the window will start running the various server-y looking bits of code.  as soon as it is up and going, you will told the various commands that are available to the server.  The first one will be &quot;kill&quot;.  this is what you type into the MSDOS window to shut the server down.  &lt;b&gt;**IMPORTANT**&lt;/b&gt; if you shut down the server all rooms on it will close.  You don't want to shut it down if others are using it.
+
+The second command is the &quot;dump&quot; command.  This will list all the people on your server, as well as their ID number.  It really isn't important at this time but it will come in handy later.
+
+Broadcast is self explanitory.  The 'Announce' and 'Remove' features allow you to choose later on if your server can be seen on the tracker, or not.  Dump Groups gets the same info as Dump but does Groups instead of people.  And finally you can bring up this little list of commands at any time by typing 'help' or '?'.
+
+You can type these commands in any time in the python server window.  Go ahead and try 'help' or 'dump' and see what comes up.
+
+Alright!  That's it.  to access your server, run you OpenRPG program as normal (with the server MSDOS window in the background) and log onto your server as we had shown you in chapter 1.  This will come in handy if there are no Servers running and you wish to use OpenRPG with, perhaps your gaming buddies or maybe some underworld kingpins you need to 'have a talk' with.
+
+And From All of us at OpenRPG HQ, we wish you good gaming!</text>
+      </nodehandler>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="form_handler" icon="form" module="forms" name="The Map" version="1.0">
+    <form height="400" width="400"/>
+    <nodehandler class="textctrl_handler" icon="note" module="forms" name="Of Maps and Miniatures" version="1.0">
+      <text multiline="1" send_button="0">The Map:
+
+Now comes the hardest part of the tutorial.. describing how to use miniatures (minis)  and background images.  Well, the first step is to bring up the map.  Fortunately the new OpenRPG now loads maps differently than previous versions.  Likewise, it no longer seems to cause crashes for Windows users.  No longer will your client lag, lock up, or crash when loading images, large or small.  The entire map will now load in the background while you continue with your normal activity with the program.  Additionally, images, once loaded, will also be cached (stored) in RAM (up to 64; overflow is handled by randomly purging images within the cache to make room for new images).  That means switching back and forth between maps, in the same session, will result in instantaneous access to your map; no more waiting.  Furthermore, not only are your images loaded in the background, but they are loaded concurrently.  That means your map will be available faster than ever!  Hopefully these new innovations will lead to a renewed usage of the map.  Even modem users have indicated that using maps are now fun again!  Anyway, on with the tutorial.
+
+The Map Window is where most of your image viewing will reside.  In it you can manipulate miniatures and maps to give your players a well good idea of what your gaming world looks like.  In case you haven't guessed, it's the top-right window.  Looking at the Map Window, you'll see all of the basic operations needed to load an image and edit the map.  Let us attempt to load our first picture.  At the bottom of the map window you'll see a large box where you can type in text.  This is where you put  the Web-page Address for the image you wish.  Let's type in this URL/URI:  http://www.openrpg.com/images/mins/amazon.gif.  Notice that there are many great miniatures ready for you to use at OpenRPG's home-page.
+
+After you have typed it in press the &quot;add miniature&quot; button, sit back, and watch as it loads the beautiful amazonian woman (well, almost) into the map window.  Please note, however, all images need to be on web-pages and served up by a web server... you cannot load directly from your hard drive.  Well, you can, but no one else will be able to see it.  The reason being is because OpenRPG's client actually one giant web browser.
+
+Feel free to move it about, get a feel for how it works.  You'll see at the moment that the amazon seems to hop from grid square to grid square.  Well... if you're like me, you don't like being confined to grids or rules.  So let's get rid of that grid, shall we?  Again, looking at the map window, you will see a little red diamond thing.  They say it is a compass but it just looks like a flying fish to me.  Anyway.. click on that and it will bring up all the map settings.  In there you can change the size of the map, what color it is, or even load up a background on which all the mini's sit ((say, a dungeon map you drew or perhaps that picture of Britney Spears I know you have lying around)).  The thing we are interested in the most, though, will be the grid settings at the bottom.  As you can see, you can change the size of the grid and switch the grid from square(4 sided) to hex (6 sided).  You'll also be able to let mini's either abide by the grid, or become free from the black lined prison.  Let us turn off the &quot;snap to grid&quot; and press &quot;apply&quot;.  Also note that you can also make the grids disappear by making them match the background color, however, this is not the ideal way to do it as it still requires processing to draw and redraw the grid lines, even if they blend in without being visible.
+
+Now, go back into the map window and try moving your mini again.  You'll see that it now moves freely, and ignores those lines.  But ignoring them is not enough, did I hear you say?  You want them gone?  Ok.  Let's go back into the settings (the red star button) and this time change the grid size to zero.  Pressing 'apply' you will see that the grid has disappeared entirely from map.  Course, if you want it back, just go put in the size again (50 is default).  Feel free to make the grid size match your miniature and map scale.  Also, if you prefer to have the snap to grid, however, do not feel that it provides enough resolution in miniature movement, feel free to set the grid size to half or one forth of what your actual scale is.  This way, you miniatures will still fit nicely as well as be able to use the &quot;snap to grid&quot;, however, you'll better be able to stagger your miniatures or slightly offset tiled map sets.
+
+
+Tiled Map Sets:
+
+Often, DM's and GM's have the need to present a map to their players, however, they do not which to reveal the whole map and/or map section to their players.  After all, it does remove a lot of surprise if everything is right before the player's eyes.  Enter three new miniature options: &quot;Lock to back&quot;, &quot;Lock to front&quot;, &quot;Unlock Front/Back&quot;.  Previous to these options being available, either the whole map had to be loaded by changing the background image or players and DM's where forced to struggle when map sections were loaded as if they were miniatures.  Even though there was z-ordering options available, it was still tedious as miniatures would often disappear behind a map image.  Now, entire maps can be built before as game and loaded all at once, without giving away any surprises.
+
+By loading all of your map sections together as miniatures and marking each one visible or not visible, you can have great fun again with your maps!  Wait a minute, isn't this the same boat we were on before.  Well, enter the three new options.  Using the &quot;Lock to back&quot; option on each of your map sections, it will force them to always stay on the bottom of the z-order (stacking) of your miniatures.  That means, you'll now be able to change the z-order of your miniatures while all of them stay on top of your map image.  In other words, while the image is locked to the back of the z-order, it acts just like a background image should.  To restore a miniature to normal z-ordering, simply using the &quot;Unlock Front/Back&quot; option and it will once again be ordered just as any other miniature is.
+
+Wait a minute!  That's only two of the three options that you mentioned.  Well, the third option allows for even more flexibility in what DM's and GM's can present to their players, even on a visible map section.  Enter, the poor-man's fog-of-war.  By creating a &quot;mask&quot; in just about any shape you like (feel free to use transparent colors on gifs as needed), you can effectively lock the &quot;masking&quot; image to the top of the z-order which will hide a section of your visible map.  While this is still somewhat tedious, it will allow for much finer grain of control of what is and is not visible on your maps!
+
+
+Looking here, moving there:
+
+Directional markers have been a part of OpenRPG for some time now, however, there use is going to be better explained here.  If you notice, each miniature has a &quot;facing&quot; and &quot;heading&quot; available to it.  These serves not only to indicate a miniature's facing, but it's current direction of travel.  After all, you're quite able to turn your head and talk to your buddy while walking in a direction different than the one your head is pointed.  While this feature mostly serves board game players, RPGer's may find use in this too, especially when making canned maps.  Imagine, the 'ol evil cleric behind the podium with his back to you.  Now, you can make it clear on the map.  Oh wait, the cleric's minions do not have their back to you, rather, they are standing to his side and looking directly at him.  Closer look, and you see that the cleric too had his head turned to his minions in conversation.  Again, this can be expressed on your map.  Information like this makes it easier for the players to understand that the back-stab that they obviously want to do, won't be so easy to pull off.  All this, without having to verbally describe every little detail.  After all, isn't this what the maps is for?  Enjoy!
+
+
+Saving and Loading Your Map:
+
+Don't forget that you can save your hard work.  Once you have built your map, be sure to right click on the map and hit &quot;Save Map&quot;.  When you need it again, just do &quot;Load Map&quot;.  You don't even need to delete the contents of the previous map.  The client does all that for you!  Don't forget, if you load a previously viewed image, the caching will considerable speed the rate at which it's visible to you and your players.  Using a common set of miniatures helps to take advantage of this fact.
+
+The Other Modes:
+
+Now, you can also use something called map modes. This feature lets you draw, and use rulers and other fun stuff. To turn it on you right click in the map, go under switch modes and choose either whiteboard (drawing) or ruler (ruler with distance gauge). This can be useful if you ever needed to point out something to someone. To remove a line just right click on it and click remove, its as easy as that!
+</text>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Setting Up Rooms, Games, and Ignoring" version="1.0">
+    <text multiline="1" send_button="0">So now that you have your server, your custom character sheet, and have talked enough players into starting a game, you'll want to set up a private room so that you aren't disturbed by every wandering weirdo or Woody that happens upon your server (and be forwarned, you will find that various people will wander from server to server.  Do not be frightened, most of them are harmless).
+
+So, lets pull up your tracker window.  If you haven't done so already, be sure to connect to your server.  Once in there look onto the right side of the tracker window.  You will see some boxes marked &quot;Room name&quot;, &quot;Boot password&quot;, and if you check a little box, &quot;password&quot;.  the first two are rather self explanatory, and the last one is to lock your room in case you don't want to be bothered.  Be sure to tell your players what it is though.
+
+If you don't password protect a room you may get people wandering in looking for a game or just lurking watching your game.  If you don't want them in there you should first let them know and ask them to leave.  If they refuse your request then boot them.  It is very rude to boot someone from a non passworded room with out warning.
+
+Now that you have your custom built char sheet and you have talked enough players into starting a game, it's time to set up your own room.  Let's bring up that Tracker window again ((under the Game Server menu)).  Looking to the bottom left of it, you'll see a little box that allows you to type in your own room name and add a password if you like.  Let's start a room called &quot;Working on that darned fun tutorial&quot; and not put a password up.
+
+as you see the room you create will be exactly the same as the lobby, so there is nothing to worry about.  Lets load up our character sheet by right clicking on the &quot;Game Tree&quot; and pressing Insert file&quot;
+
+Now that we have our little char sheet we might as well show the world.  This can be done in one of three ways.  The first way is to right click on it and send it to other players (provided there are any).  This is how other players can get your sheet as they can't see it until you send it to them.
+
+The second way is to send it directly to the chat and let everyone see it there.  go ahead and do that by right clicking on the char sheet and pressing &quot;Send to Chat&quot;.  Now everyone gets to see it as it appears on their text chat window.
+
+the third option, Whisper to Player, is much the same as &quot;Send to chat&quot; only it sends it to a specific person or persons to see in their chat instead.
+
+Of course, there sometimes comes a day when there is a little spammonkey running about.  You know the type.  Just keeps askin pointless questions, hitting you with the same text, and generally making himself a nuesance.  Well fear not!  We have added in a brand new feature that allows you to ignore those unscrupulous people.  In the chat entry box just enter the command &quot;/i player_ID#&quot; and that will put a person on ignore (or toggle them back to un-ignore [you can also right click on the name and press toggle ignore]).  To get a list of ID's who are on your ignore list, just type &quot;/i&quot; alone, with nothing else.  You can even ignore/un-ignore multiple people at once, just put commas (( , )) between the names.  And that's it.  Ignore at your leasure.</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Other Features; Logs, Initiative, Upgrades" version="1.0">
+    <text multiline="1" send_button="0">Other features:
+
+It may seem a little odd that this seventh window is tucked behind some arrows, but I always thought this chapter was a little on the sinful side so over here it now lay.
+
+
+By getting here, you must have had quite a bit of time to read through the rest of the chapters.  Well, let's finish off with what few interesting toys we have left.
+
+To start with, lets talk about logging  To create a logfile, you can just press the disk button in the bottom right corner of the OpenRPG program, and choosing a name and place to save the file.  Badaboom!  a chatlog of everything you have seen is saved.
+
+Of course, most of you will want a way to have it automatically save a log each time it see's text.  To do that, type in &quot;/log to my_log&quot; (replacing &quot;my_log&quot; for what you want to call the file). It should automatically start logging to that filename (and will include the date in the filename you gave it).  if you want to turn it on or off later on, just type &quot;/log on&quot; or &quot;/log off&quot;
+
+Note: The chat logging system no longer appends a datestamp to the logfile
+specified.  This means that for the system to generate the logfile
+
+/foo/bar/gamelog-02-10-01.html for linux or \foo\bar\gamelog-02-10-01.html for windows
+
+Previously the user only had to have &quot;/foo/bar/gamelog&quot; entered as GameLogChatPrefix.
+To get the same logging pattern now, they'll have to set GameLogChatPrefix to
+the following:
+
+/foo/bar/gamelog-%d-%m-%y.html
+
+
+--------------------------------------------------------------------------------
+
+The Initiative Tool.  A marvelous invention allowing DMs to organise and handle an otherwise chaotic scene that we know as combat.  Compatable with all versions of OpenRPG, it works incredibly simply.
+
+All that is needed is for the DM to press &quot;New Initiative&quot; in the tool, have all of the players and the DM to type in &quot;init [diceroll]&quot; (replacing 'diceroll' for your dice) and hit enter.  An example:
+
+init [1d10+6]
+
+once everyone has done that, the DM pulls up his tool again, hits refresh, and then sorts the list however he wants.  once it is sorted, he just hits &quot;send to chat&quot; and the person at the top of the list is then sent to the chat.
+
+Once all the people have been cycled through, it will say &quot;End of Init&quot; in the chat.  The DM just presses the &quot;Start new Initiative&quot; button one more time and away you go on another round of hacking!
+
+Now, if you are the player and not using the tool, you can even put your init command into a macro node (not a text node) and do it that way.  At the time of this publication there was a bug in the initiative code and the DM running the tool cannot use macro nodes for this.
+
+You can also set up your character sheets to be able to be rolled from.  To do it, you will just need a special node that you can find here:
+
+www.geocities.com/woody_j_dick/initiative.xml
+
+You can put it directly into OpenRPG by right clicking on your gametree, pressing &quot;Insert URL&quot; and putting that URL above into the box that appears.  Even the DM can use that node for rolling his initiatives.
+
+CAUTION!  Do not rename the node.  It will not work unless it has that name.
+
+
+The last button, &quot;Keep list&quot; is for Whitewolf players who need to use the list twice.  If you edit the list, and want to keep the edited copy, then just press that and it will save the list so all you need to do is hit refresh to get it back.
+
+--------------------------------------------------------------------------------
+
+OpenRPG is a great program, no doubt about it.  And it's best quality is that it is opensourced, meaning anyone can look at the code and alter it to make it work better.  One such person, the one that compiled this tutorial no less, is going to capitalise on this free advertising to say a few quiet words about his webpage.
+
+www.geocities.com/woody_j_dick/OpenRPG.html
+
+The 8.3 Convention webpage, it offers many 'additional extras' That are not yet included in the main program.  Not only are the upgrades good for the current versions of OpenRPG, but it also retains a list of upgrades for previous versions, in case you want to go back and use that 'good ole' 9.4 or 9.6 client with the modern day servers.
+
+</text>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/urloader.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,5 @@
+
+<nodehandler class="url_loader" icon="gear" module="core" name="Remote URL" version="1.0">
+   <url url="http://" />
+</nodehandler>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/nodes/wizards.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,37 @@
+
+<nodehandler class="static_handler" icon="flask" module="core" name="Wizards" status="useful">
+  <nodehandler class="file_loader" icon="d20" module="core" name="New Die Macro">
+    <file name="die_macro.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" module="core" name="New Group">
+    <file name="group.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="note" module="core" name="New Text Block">
+    <file name="text.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="grid" module="core" name="New Grid">
+    <file name="grid.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="html" module="core" name="New Link">
+    <file name="link.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="image" module="core" name="New Web Image">
+    <file name="image.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="browser" module="core" name="New Browser Link">
+    <file name="browser.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="gear" module="core" name="New Macro">
+    <file name="macro.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="compass" module="core" name="New Encounter">
+    <file name="encounter.xml"/>
+  </nodehandler>
+  <nodehandler class="file_loader" icon="gear" module="core" name="New Miniature Library">
+    <file name="minlib.xml"/>
+  </nodehandler>
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="file_loader" icon="player" module="core" name="New Alias Library">
+    <file name="alias.xml"/>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/templates/templates.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,63 @@
+<nodehandler class="group_handler" icon="gear" module="containers" name="Templates" status="useful" version="1.0">
+<group_atts border="1" cols="1"/>
+<nodehandler class="group_handler" icon="flask" module="containers" name="Nodes" status="useful" version="1.0">
+<group_atts border="1" cols="1"/>
+<nodehandler class="file_loader" icon="note" module="core" name="Create New Text Box" version="1.0">
+<file name="textctrl.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="gear" module="core" name="Create New List Box" version="1.0">
+<file name="listbox.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="grid" module="core" name="Create New Grid" version="1.0">
+<file name="grid.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="html" module="core" name="Create New Web Link" version="1.0">
+<file name="link.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="image" module="core" name="Create New Web Image" version="1.0">
+<file name="image.xml"/>
+</nodehandler>
+</nodehandler>
+<nodehandler class="group_handler" module="containers" name="Containers" status="useful" version="1.0">
+<group_atts border="1" cols="1"/>
+<nodehandler class="file_loader" module="core" name="Create New Folder" version="1.0">
+<file name="group.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="tabber" module="core" name="Create New Tabber" version="1.0">
+<file name="tabber.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="divider" module="core" name="Create New Splitter" version="1.0">
+<file name="split.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="form" module="core" name="Create New Form" version="1.0">
+<file name="form.xml"/>
+</nodehandler>
+</nodehandler>
+<nodehandler class="group_handler" icon="gear" module="containers" name="Tools" status="useful" version="1.0">
+<group_atts border="1" cols="1"/>
+<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="player" module="core" name="Create New Alias Library Tool" version="1.0">
+<file name="alias.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="gear" module="core" name="Create New Miniature Library Tool" version="1.0">
+<file name="minlib.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="gear" module="core" name="Create remote node loader" version="1.0">
+<file name="urloader.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="d20" module="core" name="Create New d20 Character Tool" version="1.0">
+<file name="d20character.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="d20" module="core" name="Create New St*r W*rs Character Tool" version="1.0">
+<file name="StarWars_d20character.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="d20" module="core" name="3rd Edition Character Tool" version="1.0">
+<file name="dnd3e.xml"/>
+</nodehandler>
+<nodehandler class="file_loader" icon="d20" module="core" name="3.5 Tool" version="1.0">
+<file name="dnd3.5.xml"/>
+</nodehandler>
+</nodehandler>
+</nodehandler>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/ButtonPanel.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1602 @@
+# --------------------------------------------------------------------------- #
+# FANCYBUTTONPANEL Widget wxPython IMPLEMENTATION
+#
+# Original C++ Code From Eran. You Can Find It At:
+#
+# http://wxforum.shadonet.com/viewtopic.php?t=6619
+#
+# License: wxWidgets license
+#
+#
+# Python Code By:
+#
+# Andrea Gavana, @ 02 Oct 2006
+# Latest Revision: 17 Oct 2006, 17.00 GMT
+#
+#
+# For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please
+# Write To Me At:
+#
+# andrea.gavana@gmail.com
+# gavana@kpo.kz
+#
+# Or, Obviously, To The wxPython Mailing List!!!
+#
+#
+# End Of Comments
+# --------------------------------------------------------------------------- #
+
+"""
+With `ButtonPanel` class you have a panel with gradient coloring
+on it and with the possibility to place some buttons on it. Using a
+standard panel with normal wx.Buttons leads to an ugly result: the
+buttons are placed correctly on the panel - but with grey area around
+them.  Gradient coloring is kept behind the images - this was achieved
+due to the PNG format and the transparency of the bitmaps.
+
+The image are functioning like a buttons and can be caught in your
+code using the usual self.Bind(wx.EVT_BUTTON, self.OnButton) method.
+
+The control is generic, and support theming (well, I tested it under
+Windows with the three defauls themes: grey, blue, silver and the
+classic look).
+
+
+Usage
+-----
+
+ButtonPanel supports 4 alignments: left, right, top, bottom, which have a
+different meaning and behavior wrt wx.Toolbar. The easiest thing is to try
+the demo to understand, but I'll try to explain how it works.
+
+CASE 1: ButtonPanel has a main caption text
+
+Left alignment means ButtonPanel is horizontal, with the text aligned to the
+left. When you shrink the demo frame, if there is not enough room for all
+the controls to be shown, the controls closest to the text are hidden;
+
+Right alignment means ButtonPanel is horizontal, with the text aligned to the
+right. Item layout as above;
+
+Top alignment means ButtonPanel is vertical, with the text aligned to the top.
+Item layout as above;
+
+Bottom alignment means ButtonPanel is vertical, with the text aligned to the
+bottom. Item layout as above.
+
+
+CASE 2: ButtonPanel has *no* main caption text
+In this case, left and right alignment are the same (as top and bottom are the same),
+but the layout strategy changes: now if there is not enough room for all the controls
+to be shown, the last added items are hidden ("last" means on the far right for
+horizontal ButtonPanels and far bottom for vertical ButtonPanels).
+
+
+The following example shows a simple implementation that uses ButtonPanel
+inside a very simple frame::
+
+  class MyFrame(wx.Frame):
+
+      def __init__(self, parent, id=-1, title="ButtonPanel", pos=wx.DefaultPosition,
+                   size=(800, 600), style=wx.DEFAULT_FRAME_STYLE):
+
+          wx.Frame.__init__(self, parent, id, title, pos, size, style)
+
+          mainPanel = wx.Panel(self, -1)
+          self.logtext = wx.TextCtrl(mainPanel, -1, "", style=wx.TE_MULTILINE)
+
+          vSizer = wx.BoxSizer(wx.VERTICAL)
+          mainPanel.SetSizer(vSizer)
+
+          alignment = BP_ALIGN_RIGHT
+
+          titleBar = ButtonPanel(mainPanel, -1, "A Simple Test & Demo")
+
+          btn1 = ButtonInfo(wx.NewId(), wx.Bitmap("png4.png", wx.BITMAP_TYPE_PNG))
+          titleBar.AddButton(btn1)
+          self.Bind(wx.EVT_BUTTON, self.OnButton, btn1)
+
+          btn2 = ButtonInfo(wx.NewId(), wx.Bitmap("png3.png", wx.BITMAP_TYPE_PNG))
+          titleBar.AddButton(btn2)
+          self.Bind(wx.EVT_BUTTON, self.OnButton, btn2)
+
+          btn3 = ButtonInfo(wx.NewId(), wx.Bitmap("png2.png", wx.BITMAP_TYPE_PNG))
+          titleBar.AddButton(btn3)
+          self.Bind(wx.EVT_BUTTON, self.OnButton, btn3)
+
+          btn4 = ButtonInfo(wx.NewId(), wx.Bitmap("png1.png", wx.BITMAP_TYPE_PNG))
+          titleBar.AddButton(btn4)
+          self.Bind(wx.EVT_BUTTON, self.OnButton, btn4)
+
+          vSizer.Add(titleBar, 0, wx.EXPAND)
+          vSizer.Add((20, 20))
+          vSizer.Add(self.logtext, 1, wx.EXPAND|wx.ALL, 5)
+
+          vSizer.Layout()
+
+  # our normal wxApp-derived class, as usual
+
+  app = wx.PySimpleApp()
+
+  frame = MyFrame(None)
+  app.SetTopWindow(frame)
+  frame.Show()
+
+  app.MainLoop()
+
+
+License And Version:
+
+ButtonPanel Is Freeware And Distributed Under The wxPython License.
+
+Latest Revision: Andrea Gavana @ 12 Oct 2006, 17.00 GMT
+Version 0.3.
+
+"""
+
+
+import wx
+
+# Some constants to tune the BPArt class
+BP_BACKGROUND_COLOR = 0
+""" Background brush colour when no gradient shading exists. """
+BP_GRADIENT_COLOR_FROM = 1
+""" Starting gradient colour, used only when BP_USE_GRADIENT style is applied. """
+BP_GRADIENT_COLOR_TO = 2
+""" Ending gradient colour, used only when BP_USE_GRADIENT style is applied. """
+BP_BORDER_COLOR = 3
+""" Pen colour to paint the border of ButtonPanel. """
+BP_TEXT_COLOR = 4
+""" Main ButtonPanel caption colour. """
+BP_BUTTONTEXT_COLOR = 5
+""" Text colour for buttons with text. """
+BP_BUTTONTEXT_INACTIVE_COLOR = 6
+""" Text colour for inactive buttons with text. """
+BP_SELECTION_BRUSH_COLOR = 7
+""" Brush colour to be used when hovering or selecting a button. """
+BP_SELECTION_PEN_COLOR = 8
+""" Pen colour to be used when hovering or selecting a button. """
+BP_SEPARATOR_COLOR = 9
+""" Pen colour used to paint the separators. """
+BP_TEXT_FONT = 10
+""" Font of the ButtonPanel main caption. """
+BP_BUTTONTEXT_FONT = 11
+""" Text font for the buttons with text. """
+
+BP_BUTTONTEXT_ALIGN_BOTTOM = 12
+""" Flag that indicates the image and text in buttons is stacked. """
+BP_BUTTONTEXT_ALIGN_RIGHT = 13
+""" Flag that indicates the text is shown alongside the image in buttons with text. """
+
+BP_SEPARATOR_SIZE = 14
+"""
+Separator size. NB: This is not the line width, but the sum of the space before
+and after the separator line plus the width of the line.
+"""
+BP_MARGINS_SIZE = 15
+"""
+Size of the left/right margins in ButtonPanel (top/bottom for vertically
+aligned ButtonPanels).
+"""
+BP_BORDER_SIZE = 16
+""" Size of the border. """
+BP_PADDING_SIZE = 17
+""" Inter-tool separator size. """
+
+# Caption Gradient Type
+BP_GRADIENT_NONE = 0
+""" No gradient shading should be used to paint the background. """
+BP_GRADIENT_VERTICAL = 1
+""" Vertical gradient shading should be used to paint the background. """
+BP_GRADIENT_HORIZONTAL = 2
+""" Horizontal gradient shading should be used to paint the background. """
+
+# Flags for HitTest() method
+BP_HT_BUTTON = 200
+BP_HT_NONE = 201
+
+# Alignment of buttons in the panel
+BP_ALIGN_RIGHT = 1
+BP_ALIGN_LEFT = 2
+BP_ALIGN_TOP = 4
+BP_ALIGN_BOTTOM = 8
+
+# ButtonPanel styles
+BP_DEFAULT_STYLE = 1
+BP_USE_GRADIENT = 2
+
+# Delay used to cancel the longHelp in the statusbar field
+_DELAY = 3000
+
+# Check for the new method in 2.7 (not present in 2.6.3.3)
+if wx.VERSION_STRING < "2.7":
+    wx.Rect.Contains = lambda self, point: wx.Rect.Inside(self, point)
+
+def BrightenColour(color, factor):
+    """ Bright the input colour by a factor."""
+
+    val = color.Red()*factor
+    if val > 255:
+        red = 255
+    else:
+        red = val
+    val = color.Green()*factor
+    if val > 255:
+        green = 255
+    else:
+        green = val
+    val = color.Blue()*factor
+    if val > 255:
+        blue = 255
+    else:
+        blue = val
+    return wx.Color(red, green, blue)
+
+def GrayOut(anImage):
+    """
+    Convert the given image (in place) to a grayed-out version,
+    appropriate for a 'Disabled' appearance.
+    """
+    factor = 0.7        # 0 < f < 1.  Higher Is Grayer
+    anImage = anImage.ConvertToImage()
+    if anImage.HasAlpha():
+        anImage.ConvertAlphaToMask(1)
+    if anImage.HasMask():
+        maskColor = (anImage.GetMaskRed(), anImage.GetMaskGreen(), anImage.GetMaskBlue())
+    else:
+        maskColor = None
+    data = map(ord, list(anImage.GetData()))
+    for i in range(0, len(data), 3):
+        pixel = (data[i], data[i+1], data[i+2])
+        pixel = MakeGray(pixel, factor, maskColor)
+        for x in range(3):
+            data[i+x] = pixel[x]
+    anImage.SetData(''.join(map(chr, data)))
+    anImage = anImage.ConvertToBitmap()
+    return anImage
+
+def MakeGray((r,g,b), factor, maskColor):
+    """
+    Make a pixel grayed-out. If the pixel matches the maskColor, it won't be
+    changed.
+    """
+    if (r,g,b) != maskColor:
+        return map(lambda x: int((230 - x) * factor) + x, (r,g,b))
+    else:
+        return (r,g,b)
+
+# ---------------------------------------------------------------------------- #
+# Class BPArt
+# Handles all the drawings for buttons, separators and text and allows the
+# programmer to set colours, sizes and gradient shadings for ButtonPanel
+# ---------------------------------------------------------------------------- #
+
+class BPArt:
+    """
+    BPArt is an art provider class which does all of the drawing for ButtonPanel.
+    This allows the library caller to customize the BPArt or to completely replace
+    all drawing with custom BPArts.
+    """
+
+    def __init__(self, parentStyle):
+        """ Default class constructor. """
+        base_color = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)
+        self._background_brush = wx.Brush(base_color, wx.SOLID)
+        self._gradient_color_to = wx.WHITE
+        self._gradient_color_from = wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION)
+        if parentStyle & BP_USE_GRADIENT:
+            self._border_pen = wx.Pen(wx.WHITE, 3)
+            self._caption_text_color = wx.WHITE
+            self._buttontext_color = wx.Colour(70, 143, 255)
+            self._separator_pen = wx.Pen(BrightenColour(self._gradient_color_from, 1.4))
+            self._gradient_type = BP_GRADIENT_VERTICAL
+        else:
+            self._border_pen = wx.Pen(BrightenColour(base_color, 0.9), 3)
+            self._caption_text_color = wx.BLACK
+            self._buttontext_color = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNTEXT)
+            self._separator_pen = wx.Pen(BrightenColour(base_color, 0.9))
+            self._gradient_type = BP_GRADIENT_NONE
+        self._buttontext_inactive_color = wx.SystemSettings_GetColour(wx.SYS_COLOUR_GRAYTEXT)
+        self._selection_brush = wx.Brush(wx.Color(225, 225, 255))
+        self._selection_pen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION))
+        sysfont = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        self._caption_font = wx.Font(sysfont.GetPointSize(), wx.DEFAULT, wx.NORMAL, wx.BOLD,
+                                     False, sysfont.GetFaceName())
+        self._buttontext_font = wx.Font(sysfont.GetPointSize(), wx.DEFAULT, wx.NORMAL, wx.NORMAL,
+                                        False, sysfont.GetFaceName())
+        self._separator_size = 7
+        self._margins_size = wx.Size(6, 6)
+        self._caption_border_size = 3
+        self._padding_size = wx.Size(6, 6)
+
+    def GetMetric(self, id):
+        """ Returns sizes of customizable options. """
+        if id == BP_SEPARATOR_SIZE:
+            return self._separator_size
+        elif id == BP_MARGINS_SIZE:
+            return self._margins_size
+        elif id == BP_BORDER_SIZE:
+            return self._caption_border_size
+        elif id == BP_PADDING_SIZE:
+            return self._padding_size
+        else:
+            raise "\nERROR: Invalid Metric Ordinal. "
+
+    def SetMetric(self, id, new_val):
+        """ Sets sizes for customizable options. """
+        if id == BP_SEPARATOR_SIZE:
+            self._separator_size = new_val
+        elif id == BP_MARGINS_SIZE:
+            self._margins_size = new_val
+        elif id == BP_BORDER_SIZE:
+            self._caption_border_size = new_val
+            self._border_pen.SetWidth(new_val)
+        elif id == BP_PADDING_SIZE:
+            self._padding_size = new_val
+        else:
+            raise "\nERROR: Invalid Metric Ordinal. "
+
+    def GetColor(self, id):
+        """ Returns colours of customizable options. """
+        if id == BP_BACKGROUND_COLOR:
+            return self._background_brush.GetColour()
+        elif id == BP_GRADIENT_COLOR_FROM:
+            return self._gradient_color_from
+        elif id == BP_GRADIENT_COLOR_TO:
+            return self._gradient_color_to
+        elif id == BP_BORDER_COLOR:
+            return self._border_pen.GetColour()
+        elif id == BP_TEXT_COLOR:
+            return self._caption_text_color
+        elif id == BP_BUTTONTEXT_COLOR:
+            return self._buttontext_color
+        elif id == BP_BUTTONTEXT_INACTIVE_COLOR:
+            return self._buttontext_inactive_color
+        elif id == BP_SELECTION_BRUSH_COLOR:
+            return self._selection_brush.GetColour()
+        elif id == BP_SELECTION_PEN_COLOR:
+            return self._selection_pen.GetColour()
+        elif id == BP_SEPARATOR_COLOR:
+            return self._separator_pen.GetColour()
+        else:
+            raise "\nERROR: Invalid Colour Ordinal. "
+
+    def SetColor(self, id, colour):
+        """ Sets colours for customizable options. """
+        if id == BP_BACKGROUND_COLOR:
+            self._background_brush.SetColour(colour)
+        elif id == BP_GRADIENT_COLOR_FROM:
+            self._gradient_color_from = colour
+        elif id == BP_GRADIENT_COLOR_TO:
+            self._gradient_color_to = colour
+        elif id == BP_BORDER_COLOR:
+            self._border_pen.SetColour(colour)
+        elif id == BP_TEXT_COLOR:
+            self._caption_text_color = colour
+        elif id == BP_BUTTONTEXT_COLOR:
+            self._buttontext_color = colour
+        elif id == BP_BUTTONTEXT_INACTIVE_COLOR:
+            self._buttontext_inactive_color = colour
+        elif id == BP_SELECTION_BRUSH_COLOR:
+            self._selection_brush.SetColour(colour)
+        elif id == BP_SELECTION_PEN_COLOR:
+            self._selection_pen.SetColour(colour)
+        elif id == BP_SEPARATOR_COLOR:
+            self._separator_pen.SetColour(colour)
+        else:
+            raise "\nERROR: Invalid Colour Ordinal. "
+    GetColour = GetColor
+    SetColour = SetColor
+
+    def SetFont(self, id, font):
+        """ Sets font for customizable options. """
+        if id == BP_TEXT_FONT:
+            self._caption_font = font
+        elif id == BP_BUTTONTEXT_FONT:
+            self._buttontext_font = font
+
+    def GetFont(self, id):
+        """ Returns font of customizable options. """
+        if id == BP_TEXT_FONT:
+            return self._caption_font
+        elif id == BP_BUTTONTEXT_FONT:
+            return self._buttontext_font
+        return wx.NoneFont
+
+    def SetGradientType(self, gradient):
+        """ Sets the gradient type for BPArt drawings. """
+        self._gradient_type = gradient
+
+    def GetGradientType(self):
+        """ Returns the gradient type for BPArt drawings. """
+        return self._gradient_type
+
+    def DrawSeparator(self, dc, rect, isVertical):
+        """ Draws a separator in ButtonPanel. """
+        dc.SetPen(self._separator_pen)
+        if isVertical:
+            ystart = yend = rect.y + rect.height/2
+            xstart = int(rect.x + 1.5*self._caption_border_size)
+            xend = int(rect.x + rect.width - 1.5*self._caption_border_size)
+            dc.DrawLine(xstart, ystart, xend, yend)
+        else:
+            xstart = xend = rect.x + rect.width/2
+            ystart = int(rect.y + 1.5*self._caption_border_size)
+            yend = int(rect.y + rect.height - 1.5*self._caption_border_size)
+            dc.DrawLine(xstart, ystart, xend, yend)
+
+    def DrawCaption(self, dc, rect, captionText):
+        """ Draws the main caption text in ButtonPanel. """
+        textColour = self._caption_text_color
+        textFont = self._caption_font
+        padding = self._padding_size
+        dc.SetTextForeground(textColour)
+        dc.SetFont(textFont)
+        dc.DrawText(captionText, rect.x + padding.x, rect.y+padding.y)
+
+    def DrawButton(self, dc, rect, parentSize, buttonBitmap, isVertical,
+                   buttonStatus, isToggled, textAlignment, text=""):
+        """ Draws a button in ButtonPanel, together with its text (if any). """
+        bmpxsize, bmpysize = buttonBitmap.GetWidth(), buttonBitmap.GetHeight()
+        dx = dy = focus = 0
+        borderw = self._caption_border_size
+        padding = self._padding_size
+        buttonFont = self._buttontext_font
+        dc.SetFont(buttonFont)
+        if isVertical:
+            rect = wx.Rect(borderw, rect.y, rect.width-2*borderw, rect.height)
+            if text != "":
+                textW, textH = dc.GetTextExtent(text)
+                if textAlignment == BP_BUTTONTEXT_ALIGN_RIGHT:
+                    fullExtent = bmpxsize + padding.x/2 + textW
+                    bmpypos = rect.y + (rect.height - bmpysize)/2
+                    bmpxpos = rect.x + (rect.width - fullExtent)/2
+                    textxpos = bmpxpos + padding.x/2 + bmpxsize
+                    textypos = bmpypos + (bmpysize - textH)/2
+                else:
+                    bmpxpos = rect.x + (rect.width - bmpxsize)/2
+                    bmpypos = rect.y + padding.y
+                    textxpos = rect.x + (rect.width - textW)/2
+                    textypos = bmpypos + bmpysize + padding.y/2
+            else:
+                bmpxpos = rect.x + (rect.width - bmpxsize)/2
+                bmpypos = rect.y + (rect.height - bmpysize)/2
+        else:
+            rect = wx.Rect(rect.x, borderw, rect.width, rect.height-2*borderw)
+            if text != "":
+                textW, textH = dc.GetTextExtent(text)
+                if textAlignment == BP_BUTTONTEXT_ALIGN_RIGHT:
+                    fullExtent = bmpxsize + padding.x/2 + textW
+                    bmpypos = rect.y + (rect.height - bmpysize)/2
+                    bmpxpos = rect.x + (rect.width - fullExtent)/2
+                    textxpos = bmpxpos + padding.x/2 + bmpxsize
+                    textypos = bmpypos + (bmpysize - textH)/2
+                else:
+                    fullExtent = bmpysize + padding.y/2 + textH
+                    bmpxpos = rect.x + (rect.width - bmpxsize)/2
+                    bmpypos = rect.y + (rect.height - fullExtent)/2
+                    textxpos = rect.x + (rect.width - textW)/2
+                    textypos = bmpypos + bmpysize + padding.y/2
+            else:
+                bmpxpos = rect.x + (rect.width - bmpxsize)/2
+                bmpypos = rect.y + (rect.height - bmpysize)/2
+
+        # Draw a button
+        # [ Padding | Text | .. Buttons .. | Padding ]
+
+        if buttonStatus in ["Pressed", "Toggled", "Hover"]:
+            dc.SetBrush(self._selection_brush)
+            dc.SetPen(self._selection_pen)
+            dc.DrawRoundedRectangleRect(rect, 4)
+        if buttonStatus == "Pressed" or isToggled:
+            dx = dy = 1
+        dc.DrawBitmap(buttonBitmap, bmpxpos+dx, bmpypos+dy, True)
+        if text != "":
+            isEnabled = buttonStatus != "Disabled"
+            self.DrawLabel(dc, text, isEnabled, textxpos+dx, textypos+dy)
+
+    def DrawLabel(self, dc, text, isEnabled, xpos, ypos):
+        """ Draws the label for a button. """
+        if not isEnabled:
+            dc.SetTextForeground(self._buttontext_inactive_color)
+        else:
+            dc.SetTextForeground(self._buttontext_color)
+        dc.DrawText(text, xpos, ypos)
+
+    def DrawButtonPanel(self, dc, rect, style):
+        """ Paint the ButtonPanel's background. """
+
+        if style & BP_USE_GRADIENT:
+            # Draw gradient color in the backgroud of the panel
+            self.FillGradientColor(dc, rect)
+        # Draw a rectangle around the panel
+        backBrush = (style & BP_USE_GRADIENT and [wx.TRANSPARENT_BRUSH] or \
+                     [self._background_brush])[0]
+        dc.SetBrush(backBrush)
+        dc.SetPen(self._border_pen)
+        dc.DrawRectangleRect(rect)
+
+    def FillGradientColor(self, dc, rect):
+        """ Gradient fill from colour 1 to colour 2 with top to bottom or left to right. """
+        if rect.height < 1 or rect.width < 1:
+            return
+        isVertical = self._gradient_type == BP_GRADIENT_VERTICAL
+        size = (isVertical and [rect.height] or [rect.width])[0]
+        start = (isVertical and [rect.y] or [rect.x])[0]
+
+        # calculate gradient coefficients
+        col2 = self._gradient_color_from
+        col1 = self._gradient_color_to
+        rf, gf, bf = 0, 0, 0
+        rstep = float((col2.Red() - col1.Red()))/float(size)
+        gstep = float((col2.Green() - col1.Green()))/float(size)
+        bstep = float((col2.Blue() - col1.Blue()))/float(size)
+        for coord in xrange(start, start + size):
+            currCol = wx.Colour(col1.Red() + rf, col1.Green() + gf, col1.Blue() + bf)
+            dc.SetBrush(wx.Brush(currCol, wx.SOLID))
+            dc.SetPen(wx.Pen(currCol))
+            if isVertical:
+                dc.DrawLine(rect.x, coord, rect.x + rect.width, coord)
+            else:
+                dc.DrawLine(coord, rect.y, coord, rect.y + rect.height)
+            rf += rstep
+            gf += gstep
+            bf += bstep
+
+class StatusBarTimer(wx.Timer):
+    """Timer used for deleting StatusBar long help after _DELAY seconds."""
+
+    def __init__(self, owner):
+        """
+        Default class constructor.
+        For internal use: do not call it in your code!
+        """
+        wx.Timer.__init__(self)
+        self._owner = owner
+
+    def Notify(self):
+        """The timer has expired."""
+        self._owner.OnStatusBarTimer()
+
+class Control(wx.EvtHandler):
+
+    def __init__(self, parent, size=wx.Size(-1, -1)):
+        """
+        Default class constructor.
+
+        Base class for all pseudo controls
+        parent = parent object
+        size = (width, height)
+        """
+        wx.EvtHandler.__init__(self)
+        self._parent = parent
+        self._id = wx.NewId()
+        self._size = size
+        self._isshown = True
+        self._focus = False
+
+    def Show(self, show=True):
+        """ Shows or hide the control. """
+        self._isshown = show
+
+    def Hide(self):
+        """ Hides the control. """
+        self.Show(False)
+
+    def IsShown(self):
+        """ Returns whether the control is shown or not. """
+        return self._isshown
+
+    def GetId(self):
+        """ Returns the control id. """
+        return self._id
+
+    def GetBestSize(self):
+        """ Returns the control best size. """
+        return self._size
+
+    def Disable(self):
+        """ Disables the control. """
+        self.Enable(False)
+
+    def Enable(self, value=True):
+        """ Enables or disables the control. """
+        self.disabled = not value
+
+    def SetFocus(self, focus=True):
+        """ Sets or kills the focus on the control. """
+        self._focus = focus
+
+    def HasFocus(self):
+        """ Returns whether the control has the focus or not. """
+        return self._focus
+
+    def OnMouseEvent(self, x, y, event):
+        pass
+
+    def Draw(self, rect):
+        pass
+
+class Sizer(object):
+    """
+    Sizer
+
+    This is a mix-in class to add pseudo support to a wx sizer.  Just create
+    a new class that derives from this class and the wx sizer and intercepts
+    any methods that add to the wx sizer.
+    """
+    def __init__(self):
+        self.children = [] # list of child Pseudo Controls
+    # Sizer doesn't use the x1,y1,x2,y2 so allow it to
+    # be called with or without the coordinates
+    def Draw(self, dc, x1=0, y1=0, x2=0, y2=0):
+        for item in self.children:
+            # use sizer coordinates rather than
+            # what is passed in
+            c = item.GetUserData()
+            c.Draw(dc, item.GetRect())
+
+    def GetBestSize(self):
+        # this should be handled by the wx.Sizer based class
+        return self.GetMinSize()
+
+# Pseudo BoxSizer
+class BoxSizer(Sizer, wx.BoxSizer):
+    def __init__(self, orient=wx.HORIZONTAL):
+        wx.BoxSizer.__init__(self, orient)
+        Sizer.__init__(self)
+
+    #-------------------------------------------
+    # sizer overrides (only called from Python)
+    #-------------------------------------------
+    # no support for user data if it's a pseudocontrol
+    # since that is already used
+    def Add(self, item, proportion=0, flag=0, border=0, userData=None):
+        # check to see if it's a pseudo object or sizer
+        if isinstance(item, Sizer):
+            szitem = wx.BoxSizer.Add(self, item, proportion, flag, border, item)
+            self.children.append(szitem)
+        elif isinstance(item, Control): # Control should be what ever class your controls come from
+            sz = item.GetBestSize()
+            # add a spacer to track this object
+            szitem = wx.BoxSizer.Add(self, sz, proportion, flag, border, item)
+            self.children.append(szitem)
+        else:
+            wx.BoxSizer.Add(self, item, proportion, flag, border, userData)
+
+    def Prepend(self, item, proportion=0, flag=0, border=0, userData=None):
+        # check to see if it's a pseudo object or sizer
+        if isinstance(item, Sizer):
+            szitem = wx.BoxSizer.Prepend(self, item, proportion, flag, border, item)
+            self.children.append(szitem)
+        elif isinstance(item, Control): # Control should be what ever class your controls come from
+            sz = item.GetBestSize()
+            # add a spacer to track this object
+            szitem = wx.BoxSizer.Prepend(self, sz, proportion, flag, border, item)
+            self.children.insert(0,szitem)
+        else:
+            wx.BoxSizer.Prepend(self, item, proportion, flag, border, userData)
+
+    def Insert(self, before, item, proportion=0, flag=0, border=0, userData=None, realIndex=None):
+        # check to see if it's a pseudo object or sizer
+        if isinstance(item, Sizer):
+            szitem = wx.BoxSizer.Insert(self, before, item, proportion, flag, border, item)
+            self.children.append(szitem)
+        elif isinstance(item, Control): # Control should be what ever class your controls come from
+            sz = item.GetBestSize()
+            # add a spacer to track this object
+            szitem = wx.BoxSizer.Insert(self, before, sz, proportion, flag, border, item)
+            if realIndex is not None:
+                self.children.insert(realIndex,szitem)
+            else:
+                self.children.insert(before,szitem)
+        else:
+            wx.BoxSizer.Insert(self, before, item, proportion, flag, border, userData)
+
+    def Remove(self, indx, pop=-1):
+        if pop >= 0:
+            self.children.pop(pop)
+        wx.BoxSizer.Remove(self, indx)
+
+    def Layout(self):
+        for ii, child in enumerate(self.GetChildren()):
+            item = child.GetUserData()
+            if item and child.IsShown():
+                self.SetItemMinSize(ii, *item.GetBestSize())
+        wx.BoxSizer.Layout(self)
+
+    def Show(self, item, show=True):
+        child = self.GetChildren()[item]
+        if child and child.GetUserData():
+            child.GetUserData().Show(show)
+        wx.BoxSizer.Show(self, item, show)
+
+# ---------------------------------------------------------------------------- #
+# Class Separator
+# This class holds all the information to size and draw a separator inside
+# ButtonPanel
+# ---------------------------------------------------------------------------- #
+
+class Separator(Control):
+
+    def __init__(self, parent):
+        """ Default class constructor. """
+        self._isshown = True
+        self._parent = parent
+        Control.__init__(self, parent)
+
+    def GetBestSize(self):
+        """ Returns the separator best size. """
+        # 10 is completely arbitrary, but it works anyhow
+        if self._parent.IsVertical():
+            return wx.Size(10, self._parent._art.GetMetric(BP_SEPARATOR_SIZE))
+        else:
+            return wx.Size(self._parent._art.GetMetric(BP_SEPARATOR_SIZE), 10)
+
+    def Draw(self, dc, rect):
+        """ Draws the separator. Actually the drawing is done in BPArt. """
+        if not self.IsShown():
+            return
+        isVertical = self._parent.IsVertical()
+        self._parent._art.DrawSeparator(dc, rect, isVertical)
+
+# ---------------------------------------------------------------------------- #
+# Class ButtonPanelText
+# This class is used to hold data about the main caption in ButtonPanel
+# ---------------------------------------------------------------------------- #
+
+class ButtonPanelText(Control):
+
+    def __init__(self, parent, text=""):
+        """ Default class constructor. """
+        self._text = text
+        self._isshown = True
+        self._parent = parent
+        Control.__init__(self, parent)
+
+    def GetText(self):
+        """ Returns the caption text. """
+        return self._text
+
+    def SetText(self, text=""):
+        """ Sets the caption text. """
+        self._text = text
+
+    def CreateDC(self):
+        """ Convenience function to create a DC. """
+        dc = wx.ClientDC(self._parent)
+        textFont = self._parent._art.GetFont(BP_TEXT_FONT)
+        dc.SetFont(textFont)
+        return dc
+
+    def GetBestSize(self):
+        """ Returns the best size for the main caption in ButtonPanel. """
+        if self._text == "":
+            return wx.Size(0, 0)
+        dc = self.CreateDC()
+        rect = self._parent.GetClientRect()
+        tw, th = dc.GetTextExtent(self._text)
+        padding = self._parent._art.GetMetric(BP_PADDING_SIZE)
+        self._size = wx.Size(tw+2*padding.x, th+2*padding.y)
+        return self._size
+
+    def Draw(self, dc, rect):
+        """ Draws the main caption. Actually the drawing is done in BPArt. """
+        if not self.IsShown():
+            return
+        captionText = self.GetText()
+        self._parent._art.DrawCaption(dc, rect, captionText)
+
+# -- ButtonInfo class implementation ----------------------------------------
+# This class holds information about every button that is added to
+# ButtonPanel.  It is an auxiliary class that you should use
+# every time you add a button.
+
+class ButtonInfo(Control):
+
+    def __init__(self, parent, id=wx.ID_ANY, bmp=wx.NullBitmap,
+                 status="Normal", text="", kind=wx.ITEM_NORMAL,
+                 shortHelp="", longHelp=""):
+        """
+        Default class constructor.
+
+        Parameters:
+        - parent: the parent window (ButtonPanel);
+        - id: the button id;
+        - bmp: the associated bitmap;
+        - status: button status (pressed, hovered, normal).
+        - text: text to be displayed either below of to the right of the button
+        - kind: button kind, may be wx.ITEM_NORMAL for standard buttons or
+          wx.ITEM_CHECK for toggle buttons;
+        - shortHelp: a short help to be shown in the button tooltip;
+        - longHelp: this string is shown in the statusbar (if any) of the parent
+          frame when the mouse pointer is inside the button.
+        """
+
+        if id == wx.ID_ANY:
+            id = wx.NewId()
+        self._status = status
+        self._rect = wx.Rect()
+        self._text = text
+        self._kind = kind
+        self._toggle = False
+        self._textAlignment = BP_BUTTONTEXT_ALIGN_BOTTOM
+        self._shortHelp = shortHelp
+        self._longHelp = longHelp
+        self._menu = None
+        disabledbmp = GrayOut(bmp)
+        self._bitmaps = {"Normal": bmp, "Toggled": None, "Disabled": disabledbmp,
+                         "Hover": None, "Pressed": None}
+        Control.__init__(self, parent)
+
+    def GetBestSize(self):
+        """ Returns the best size for the button. """
+        xsize = self.GetBitmap().GetWidth()
+        ysize = self.GetBitmap().GetHeight()
+        if self.HasText():
+            # We have text in the button
+            dc = wx.ClientDC(self._parent)
+            normalFont = self._parent._art.GetFont(BP_BUTTONTEXT_FONT)
+            dc.SetFont(normalFont)
+            tw, th = dc.GetTextExtent(self.GetText())
+            if self.GetTextAlignment() == BP_BUTTONTEXT_ALIGN_BOTTOM:
+                xsize = max(xsize, tw)
+                ysize = ysize + th
+            else:
+                xsize = xsize + tw
+                ysize = max(ysize, th)
+        border = self._parent._art.GetMetric(BP_BORDER_SIZE)
+        padding = self._parent._art.GetMetric(BP_PADDING_SIZE)
+        if self._parent.IsVertical():
+            xsize = xsize + 2*border
+        else:
+            ysize = ysize + 2*border
+        self._size = wx.Size(xsize+2*padding.x, ysize+2*padding.y)
+        return self._size
+
+    def Draw(self, dc, rect):
+        """ Draws the button on ButtonPanel. Actually the drawing is done in BPArt. """
+        if not self.IsShown():
+            return
+        buttonBitmap = self.GetBitmap()
+        isVertical = self._parent.IsVertical()
+        text = self.GetText()
+        parentSize = self._parent.GetSize()[not isVertical]
+        buttonStatus = self.GetStatus()
+        isToggled = self.GetToggled()
+        textAlignment = self.GetTextAlignment()
+        self._parent._art.DrawButton(dc, rect, parentSize, buttonBitmap, isVertical,
+                                     buttonStatus, isToggled, textAlignment, text)
+        self.SetRect(rect)
+
+    def CheckRefresh(self, status):
+        """ Checks whether a ButtonPanel repaint is needed or not. Convenience function. """
+        if status == self._status:
+            self._parent.RefreshRect(self.GetRect())
+
+    def SetBitmap(self, bmp, status="Normal"):
+        """ Sets the associated bitmap. """
+        self._bitmaps[status] = bmp
+        self.CheckRefresh(status)
+
+    def GetBitmap(self, status=None):
+        """ Returns the associated bitmap. """
+        if status is None:
+            status = self._status
+        if not self.IsEnabled():
+            status = "Disabled"
+        if self._bitmaps[status] is None:
+            return self._bitmaps["Normal"]
+        return self._bitmaps[status]
+
+    def GetRect(self):
+        """ Returns the button rect. """
+        return self._rect
+
+    def GetStatus(self):
+        """ Returns the button status. """
+        return self._status
+
+    def GetId(self):
+        """ Returns the button id. """
+        return self._id
+
+    def SetRect(self, rect):
+        """ Sets the button rect. """
+        self._rect = rect
+
+    def SetStatus(self, status):
+        """ Sets the button status. """
+        if status == self._status:
+            return
+        if self.GetToggled() and status == "Normal":
+            status = "Toggled"
+        self._status = status
+        self._parent.RefreshRect(self.GetRect())
+
+    def GetTextAlignment(self):
+        """ Returns the text alignment in the button (bottom or right). """
+        return self._textAlignment
+
+    def SetTextAlignment(self, alignment):
+        """ Sets the text alignment in the button (bottom or right). """
+        if alignment == self._textAlignment:
+            return
+        self._textAlignment = alignment
+
+    def GetToggled(self):
+        """ Returns whether a wx.ITEM_CHECK button is toggled or not. """
+        if self._kind == wx.ITEM_NORMAL:
+            return False
+        return self._toggle
+
+    def SetToggled(self, toggle=True):
+        """ Sets a wx.ITEM_CHECK button toggled/not toggled. """
+        if self._kind == wx.ITEM_NORMAL:
+            return
+        self._toggle = toggle
+        if toggle:
+            self.SetStatus("Toggled")
+        else:
+            self.SetStatus("Normal")
+
+    def SetId(self, id):
+        """ Sets the button id. """
+        self._id = id
+
+    def AddStatus(self, name="Custom", bmp=wx.NullBitmap):
+        """
+        Add a programmer-defined status in addition to the 5 default status:
+        - Normal;
+        - Disabled;
+        - Hover;
+        - Pressed;
+        - Toggled.
+        """
+        self._bitmaps.update({name: bmp})
+
+    def Enable(self, enable=True):
+        if enable:
+            self._status = "Normal"
+        else:
+            self._status = "Disabled"
+
+    def IsEnabled(self):
+        return self._status != "Disabled"
+
+    def SetText(self, text=""):
+        """ Sets the text of the button. """
+        self._text = text
+
+    def GetText(self):
+        """ Returns the text associated to the button. """
+        return self._text
+
+    def HasText(self):
+        """ Returns whether the button has text or not. """
+        return self._text != ""
+
+    def SetKind(self, kind=wx.ITEM_NORMAL):
+        """ Sets the button type (standard or toggle). """
+        self._kind = kind
+
+    def GetKind(self):
+        """ Returns the button type (standard or toggle). """
+        return self._kind
+
+    def SetShortHelp(self, help=""):
+        """ Sets the help string to be shown in a tootip. """
+        self._shortHelp = help
+
+    def GetShortHelp(self):
+        """ Returns the help string shown in a tootip. """
+        return self._shortHelp
+
+    def SetLongHelp(self, help=""):
+        """ Sets the help string to be shown in the statusbar. """
+        self._longHelp = help
+
+    def GetLongHelp(self):
+        """ Returns the help string shown in the statusbar. """
+        return self._longHelp
+
+    def GetMenu(self):
+        return self._menu
+
+    def SetMenu(self, Menu):
+        self._menu = Menu
+    Bitmap = property(GetBitmap, SetBitmap)
+    Id     = property(GetId, SetId)
+    Rect   = property(GetRect, SetRect)
+    Status = property(GetStatus, SetStatus)
+
+# -- ButtonPanel class implementation ----------------------------------
+# This is the main class.
+
+class ButtonPanel(wx.PyPanel):
+
+    def __init__(self, parent, id=wx.ID_ANY, text="", style=BP_DEFAULT_STYLE,
+                 alignment=BP_ALIGN_LEFT, name="buttonPanel"):
+        """
+        Default class constructor.
+
+        - parent: parent window
+        - id: window ID
+        - text: text to draw
+        - style: window style
+        - alignment: alignment of buttons (left or right)
+        - name: window class name
+        """
+        wx.PyPanel.__init__(self, parent, id, wx.DefaultPosition, wx.DefaultSize,
+                            wx.NO_BORDER, name=name)
+        self._vButtons = []
+        self._vControls = []
+        self._vSeparators = []
+        self._nStyle = style
+        self._alignment = alignment
+        self._statusTimer = None
+        self._useHelp = True
+        self._freezeCount = 0
+        self._currentButton = -1
+        self._haveTip = False
+        self._art = BPArt(style)
+        self._controlCreated = False
+        direction = (self.IsVertical() and [wx.VERTICAL] or [wx.HORIZONTAL])[0]
+        self._mainsizer = BoxSizer(direction)
+        self.SetSizer(self._mainsizer)
+        margins = self._art.GetMetric(BP_MARGINS_SIZE)
+        # First spacer to create some room before the first text/button/control
+        self._mainsizer.Add((margins.x, margins.y), 0)
+        # Last spacer to create some room before the last text/button/control
+        self._mainsizer.Add((margins.x, margins.y), 0)
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+        self.Bind(wx.EVT_PAINT, self.OnPaint)
+        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
+        self.Bind(wx.EVT_MOTION, self.OnMouseMove)
+        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
+        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
+        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)
+        self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnterWindow)
+        self.SetBarText(text)
+        self.LayoutItems()
+
+    def SetBarText(self, text):
+        """ Sets the main caption text (leave text="" for no text). """
+        self.Freeze()
+        text = text.strip()
+        if self._controlCreated:
+            self.RemoveText()
+        self._text = ButtonPanelText(self, text)
+        lenChildren = len(self._mainsizer.GetChildren())
+        if text == "":
+            # Even if we have no text, we insert it an empty spacer anyway
+            # it is easier to handle if you have to recreate the sizer after.
+            if self.IsStandard():
+                self._mainsizer.Insert(1, self._text, 0, wx.ALIGN_CENTER,
+                                       userData=self._text, realIndex=0)
+            else:
+                self._mainsizer.Insert(lenChildren-1, self._text, 0, wx.ALIGN_CENTER,
+                                       userData=self._text, realIndex=lenChildren)
+            return
+        # We have text, so insert the text and an expandable spacer
+        # alongside it. "Standard" ButtonPanel are left or top aligned.
+        if self.IsStandard():
+            self._mainsizer.Insert(1, self._text, 0, wx.ALIGN_CENTER,
+                                    userData=self._text, realIndex=0)
+            self._mainsizer.Insert(2, (0, 0), 1, wx.EXPAND)
+        else:
+            self._mainsizer.Insert(lenChildren-1, self._text, 0, wx.ALIGN_CENTER,
+                                   userData=self._text, realIndex=lenChildren)
+            self._mainsizer.Insert(lenChildren-1, (0, 0), 1, wx.EXPAND)
+
+    def RemoveText(self):
+        """ Removes the main caption text. """
+        lenChildren = len(self._mainsizer.GetChildren())
+        lenCustom = len(self._vButtons) + len(self._vSeparators) + 1
+        if self.IsStandard():
+            # Detach the text
+            self._mainsizer.Remove(1, 0)
+            if self.HasBarText():
+                # Detach the expandable spacer
+                self._mainsizer.Remove(1, -1)
+        else:
+            # Detach the text
+            self._mainsizer.Remove(lenChildren-2, lenCustom-1)
+            if self.HasBarText():
+                # Detach the expandable spacer
+                self._mainsizer.Remove(lenChildren-3, -1)
+
+    def GetBarText(self):
+        """ Returns the main caption text. """
+        return self._text.GetText()
+
+    def HasBarText(self):
+        """ Returns whether ButtonPanel has a main caption text or not. """
+        return hasattr(self, "_text") and self._text.GetText() != ""
+
+    def AddButton(self, btnInfo):
+        """
+        Adds a button to ButtonPanel. Remember to pass a ButtonInfo instance to
+        this method. See the demo for details.
+        """
+        lenChildren = len(self._mainsizer.GetChildren())
+        self._mainsizer.Insert(lenChildren-1, btnInfo, 0, wx.ALIGN_CENTER|wx.EXPAND, userData=btnInfo)
+        self._vButtons.append(btnInfo)
+
+    def AddSpacer(self, size=(0, 0), proportion=1, flag=wx.EXPAND):
+        """ Adds a spacer (stretchable or fixed-size) to ButtonPanel. """
+        lenChildren = len(self._mainsizer.GetChildren())
+        self._mainsizer.Insert(lenChildren-1, size, proportion, flag)
+
+    def AddControl(self, control, proportion=0, flag=wx.ALIGN_CENTER|wx.ALL, border=None):
+        """ Adds a wxPython control to ButtonPanel. """
+        lenChildren = len(self._mainsizer.GetChildren())
+        if border is None:
+            border = self._art.GetMetric(BP_PADDING_SIZE)
+            border = max(border.x, border.y)
+        self._mainsizer.Insert(lenChildren-1, control, proportion, flag, border)
+        self._vControls.append(control)
+
+    def AddSeparator(self):
+        """ Adds a separator line to ButtonPanel. """
+        lenChildren = len(self._mainsizer.GetChildren())
+        separator = Separator(self)
+        self._mainsizer.Insert(lenChildren-1, separator, 0, wx.EXPAND)
+        self._vSeparators.append(separator)
+
+    def DisableAll(self):
+        """ Disable all buttons and controls """
+        for btn in self._vButtons:
+            btn.Enable(False)
+        for ctrl in self._vControls:
+            ctrl.Disable()
+
+    def EnableAll(self):
+        """ Enable all buttons and controls """
+        for btn in self._vButtons:
+            btn.Enable(True)
+        for ctrl in self._vControls:
+            ctrl.Enable()
+
+    def RemoveAllButtons(self):
+        """ Remove all the buttons from ButtonPanel. """
+        self._vButtons = []
+        self._vControls = []
+
+    def RemoveAllSeparators(self):
+        """ Remove all the separators from ButtonPanel. """
+        self._vSeparators = []
+
+    def GetAlignment(self):
+        """ Returns the button alignment (left, right, top, bottom). """
+        return self._alignment
+
+    def SetAlignment(self, alignment):
+        """ Sets the button alignment (left, right, top, bottom). """
+        if alignment == self._alignment:
+            return
+        self.Freeze()
+        text = self.GetBarText()
+        # Remove the text in any case
+        self.RemoveText()
+        # Remove the first and last spacers
+        self._mainsizer.Remove(0, -1)
+        self._mainsizer.Remove(len(self._mainsizer.GetChildren())-1, -1)
+        self._alignment = alignment
+        # Recreate the sizer accordingly to the new alignment
+        self.ReCreateSizer(text)
+
+    def IsVertical(self):
+        """ Returns whether ButtonPanel is vertically aligned or not. """
+        return self._alignment not in [BP_ALIGN_RIGHT, BP_ALIGN_LEFT]
+
+    def IsStandard(self):
+        """ Returns whether ButtonPanel is aligned "Standard" (left/top) or not. """
+        return self._alignment in [BP_ALIGN_LEFT, BP_ALIGN_TOP]
+
+    def DoLayout(self):
+        """
+        Do the Layout for ButtonPanel.
+        NB: Call this method every time you make a modification to the layout
+        or to the customizable sizes of the pseudo controls.
+        """
+        margins = self._art.GetMetric(BP_MARGINS_SIZE)
+        lenChildren = len(self._mainsizer.GetChildren())
+        self._mainsizer.SetItemMinSize(0, (margins.x, margins.y))
+        self._mainsizer.SetItemMinSize(lenChildren-1, (margins.x, margins.y))
+        self._controlCreated = True
+        self.LayoutItems()
+
+        # *VERY* WEIRD: the sizer seems not to respond to any layout until I
+        # change the ButtonPanel size and restore it back
+        size = self.GetSize()
+        self.SetSize((size.x+1, size.y+1))
+        self.SetSize((size.x, size.y))
+        if self.IsFrozen():
+            self.Thaw()
+
+    def ReCreateSizer(self, text):
+        """ Recreates the ButtonPanel sizer accordingly to the alignment specified. """
+        children = self._mainsizer.GetChildren()
+        self.RemoveAllButtons()
+        self.RemoveAllSeparators()
+        # Create a new sizer depending on the alignment chosen
+        direction = (self.IsVertical() and [wx.VERTICAL] or [wx.HORIZONTAL])[0]
+        self._mainsizer = BoxSizer(direction)
+        margins = self._art.GetMetric(BP_MARGINS_SIZE)
+        # First spacer to create some room before the first text/button/control
+        self._mainsizer.Add((margins.x, margins.y), 0)
+        # Last spacer to create some room before the last text/button/control
+        self._mainsizer.Add((margins.x, margins.y), 0)
+        # This is needed otherwise SetBarText goes mad
+        self._controlCreated = False
+        for child in children:
+            userData = child.GetUserData()
+            if userData:
+                if isinstance(userData, ButtonInfo):
+                    # It is a ButtonInfo, can't be anything else
+                    self.AddButton(child.GetUserData())
+                elif isinstance(userData, Separator):
+                    self.AddSeparator()
+            else:
+                if child.IsSpacer():
+                    # This is a spacer, expandable or not
+                    self.AddSpacer(child.GetSize(), child.GetProportion(),
+                                   child.GetFlag())
+                else:
+                    # This is a wxPython control
+                    self.AddControl(child.GetWindow(), child.GetProportion(),
+                                    child.GetFlag(), child.GetBorder())
+        self.SetSizer(self._mainsizer)
+        # Now add the text. It doesn't matter if there is no text
+        self.SetBarText(text)
+        self.DoLayout()
+        self.Thaw()
+
+    def DoGetBestSize(self):
+        """ Returns the best size of ButtonPanel. """
+        w = h = btnWidth = btnHeight = 0
+        isVertical = self.IsVertical()
+        padding = self._art.GetMetric(BP_PADDING_SIZE)
+        border = self._art.GetMetric(BP_BORDER_SIZE)
+        margins = self._art.GetMetric(BP_MARGINS_SIZE)
+        separator_size = self._art.GetMetric(BP_SEPARATOR_SIZE)
+        # Add the space required for the main caption
+        if self.HasBarText():
+            w, h = self._text.GetBestSize()
+            if isVertical:
+                h += padding.y
+            else:
+                w += padding.x
+        else:
+            w = h = border
+        # Add the button's sizes
+        for btn in self._vButtons:
+            bw, bh = btn.GetBestSize()
+            btnWidth = max(btnWidth, bw)
+            btnHeight = max(btnHeight, bh)
+            if isVertical:
+                w = max(w, btnWidth)
+                h += bh
+            else:
+                h = max(h, btnHeight)
+                w += bw
+
+        # Add the control's sizes
+        for control in self.GetControls():
+            cw, ch = control.GetSize()
+            if isVertical:
+                h += ch
+                w = max(w, cw)
+            else:
+                w += cw
+                h = max(h, ch)
+        # Add the separator's sizes and the 2 SizerItems at the beginning
+        # and at the end
+        if self.IsVertical():
+            h += 2*margins.y + len(self._vSeparators)*separator_size
+        else:
+            w += 2*margins.x + len(self._vSeparators)*separator_size
+        return wx.Size(w, h)
+
+    def OnPaint(self, event):
+        """ Handles the wx.EVT_PAINT event for ButtonPanel. """
+        dc = wx.BufferedPaintDC(self)
+        rect = self.GetClientRect()
+        self._art.DrawButtonPanel(dc, rect, self._nStyle)
+        self._mainsizer.Draw(dc)
+
+    def OnEraseBackground(self, event):
+        """ Handles the wx.EVT_ERASE_BACKGROUND event for ButtonPanel (does nothing). """
+        pass
+
+    def OnSize(self, event):
+        """ Handles the wx.EVT_SIZE event for ButtonPanel. """
+        # NOTE: It seems like LayoutItems number of calls can be optimized in some way.
+        # Currently every DoLayout (or every parent Layout()) calls about 3 times
+        # the LayoutItems method. Any idea on how to improve it?
+        self.LayoutItems()
+        self.Refresh()
+        event.Skip()
+
+    def LayoutItems(self):
+        """
+        Layout the items using a different algorithm depending on the existance
+        of the main caption.
+        """
+        nonspacers, allchildren = self.GetNonFlexibleChildren()
+        if self.HasBarText():
+            self.FlexibleLayout(nonspacers, allchildren)
+        else:
+            self.SizeLayout(nonspacers, allchildren)
+        self._mainsizer.Layout()
+
+    def SizeLayout(self, nonspacers, children):
+        """ Layout the items when no main caption exists. """
+        size = self.GetSize()
+        isVertical = self.IsVertical()
+        corner = 0
+        indx1 = len(nonspacers)
+        for item in nonspacers:
+            corner += self.GetItemSize(item, isVertical)
+            if corner > size[isVertical]:
+                indx1 = nonspacers.index(item)
+                break
+        # Leave out the last spacer, it has to be there always
+        for ii in xrange(len(nonspacers)-1):
+            indx = children.index(nonspacers[ii])
+            self._mainsizer.Show(indx, ii < indx1)
+
+    def GetItemSize(self, item, isVertical):
+        """ Returns the size of an item in the main ButtonPanel sizer. """
+        if item.GetUserData():
+            return item.GetUserData().GetBestSize()[isVertical]
+        else:
+            return item.GetSize()[isVertical]
+
+    def FlexibleLayout(self, nonspacers, allchildren):
+        """ Layout the items when the main caption exists. """
+        if len(nonspacers) < 2:
+            return
+        isVertical = self.IsVertical()
+        isStandard = self.IsStandard()
+        size = self.GetSize()[isVertical]
+        padding = self._art.GetMetric(BP_PADDING_SIZE)
+        fixed = (isStandard and [nonspacers[1]] or [nonspacers[-2]])[0]
+        if isStandard:
+            nonspacers.reverse()
+            leftendx = fixed.GetSize()[isVertical] + padding.x
+        else:
+            rightstartx = size - fixed.GetSize()[isVertical]
+            size = 0
+        count = lennonspacers = len(nonspacers)
+        for item in nonspacers:
+            if isStandard:
+                size -= self.GetItemSize(item, isVertical)
+                if size < leftendx:
+                    break
+            else:
+                size += self.GetItemSize(item, isVertical)
+                if size > rightstartx:
+                    break
+            count = count - 1
+        nonspacers.reverse()
+        for jj in xrange(2, lennonspacers):
+            indx = allchildren.index(nonspacers[jj])
+            self._mainsizer.Show(indx, jj >= count)
+
+    def GetNonFlexibleChildren(self):
+        """
+        Returns all the ButtonPanel main sizer's children that are not
+        flexible spacers.
+        """
+        children1 = []
+        children2 = self._mainsizer.GetChildren()
+        for child in children2:
+            if child.IsSpacer():
+                if child.GetUserData() or child.GetProportion() == 0:
+                    children1.append(child)
+            else:
+                children1.append(child)
+        return children1, children2
+
+    def GetControls(self):
+        """ Returns the wxPython controls that belongs to ButtonPanel. """
+        children2 = self._mainsizer.GetChildren()
+        children1 = [child for child in children2 if not child.IsSpacer()]
+        return children1
+
+    def SetStyle(self, style):
+        """ Sets ButtonPanel style. """
+        if style == self._nStyle:
+            return
+        self._nStyle = style
+        self.Refresh()
+
+    def GetStyle(self):
+        """ Returns the ButtonPanel style. """
+        return self._nStyle
+
+    def OnMouseMove(self, event):
+        """ Handles the wx.EVT_MOTION event for ButtonPanel. """
+        # Check to see if we are hovering a button
+        tabId, flags = self.HitTest(event.GetPosition())
+        if flags != BP_HT_BUTTON:
+            self.RemoveHelp()
+            self.RepaintOldSelection()
+            self._currentButton = -1
+            return
+        btn = self._vButtons[tabId]
+        if not btn.IsEnabled():
+            self.RemoveHelp()
+            self.RepaintOldSelection()
+            return
+        if tabId != self._currentButton:
+            self.RepaintOldSelection()
+        if btn.GetRect().Contains(event.GetPosition()):
+            btn.SetStatus("Hover")
+        else:
+            btn.SetStatus("Normal")
+        if tabId != self._currentButton:
+            self.RemoveHelp()
+            self.DoGiveHelp(btn)
+        self._currentButton = tabId
+        event.Skip()
+
+    def OnLeftDown(self, event):
+        """ Handles the wx.EVT_LEFT_DOWN event for ButtonPanel. """
+        tabId, hit = self.HitTest(event.GetPosition())
+        if hit == BP_HT_BUTTON:
+            btn = self._vButtons[tabId]
+            if btn.IsEnabled():
+                btn.SetStatus("Pressed")
+                self._currentButton = tabId
+
+    def OnRightDown(self, event):
+        """ Handles the wx.EVT_RIGHT_DOWN event for ButtonPanel. """
+        tabId, hit = self.HitTest(event.GetPosition())
+        if hit == BP_HT_BUTTON:
+            btn = self._vButtons[tabId]
+            if btn.IsEnabled() and btn.GetMenu() != None:
+                self.PopupMenu(btn.GetMenu())
+
+    def OnLeftUp(self, event):
+        """ Handles the wx.EVT_LEFT_UP event for ButtonPanel. """
+        tabId, flags = self.HitTest(event.GetPosition())
+        if flags != BP_HT_BUTTON:
+            return
+        hit = self._vButtons[tabId]
+        if hit.GetStatus() == "Disabled":
+            return
+        for btn in self._vButtons:
+            if btn != hit:
+                btn.SetFocus(False)
+        if hit.GetStatus() == "Pressed":
+            # Fire a button click event
+            btnEvent = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, hit.GetId())
+            self.GetEventHandler().ProcessEvent(btnEvent)
+            hit.SetToggled(not hit.GetToggled())
+            # Update the button status to be hovered
+            hit.SetStatus("Hover")
+            hit.SetFocus()
+            self._currentButton = tabId
+
+    def OnMouseLeave(self, event):
+        """ Handles the wx.EVT_LEAVE_WINDOW event for ButtonPanel. """
+        # Reset all buttons statuses
+        for btn in self._vButtons:
+            if not btn.IsEnabled():
+                continue
+            btn.SetStatus("Normal")
+        self.RemoveHelp()
+        event.Skip()
+
+    def OnMouseEnterWindow(self, event):
+        """ Handles the wx.EVT_ENTER_WINDOW event for ButtonPanel. """
+        tabId, flags = self.HitTest(event.GetPosition())
+        if flags == BP_HT_BUTTON:
+            hit = self._vButtons[tabId]
+            if hit.GetStatus() == "Disabled":
+                event.Skip()
+                return
+            self.DoGiveHelp(hit)
+            self._currentButton = tabId
+        event.Skip()
+
+    def DoGiveHelp(self, hit):
+        """ Gives tooltips and help in StatusBar. """
+        if not self.GetUseHelp():
+            return
+        shortHelp = hit.GetShortHelp()
+        if shortHelp:
+            self.SetToolTipString(shortHelp)
+            self._haveTip = True
+        longHelp = hit.GetLongHelp()
+        if not longHelp:
+            return
+        topLevel = wx.GetTopLevelParent(self)
+        if isinstance(topLevel, wx.Frame) and topLevel.GetStatusBar():
+            statusBar = topLevel.GetStatusBar()
+            if self._statusTimer and self._statusTimer.IsRunning():
+                self._statusTimer.Stop()
+                statusBar.PopStatusText(0)
+            statusBar.PushStatusText(longHelp, 0)
+            self._statusTimer = StatusBarTimer(self)
+            self._statusTimer.Start(_DELAY, wx.TIMER_ONE_SHOT)
+
+    def RemoveHelp(self):
+        """ Removes the tooltips and statusbar help (if any) for a button. """
+        if not self.GetUseHelp():
+            return
+        if self._haveTip:
+            self.SetToolTipString("")
+            self._haveTip = False
+        if self._statusTimer and self._statusTimer.IsRunning():
+            topLevel = wx.GetTopLevelParent(self)
+            statusBar = topLevel.GetStatusBar()
+            self._statusTimer.Stop()
+            statusBar.PopStatusText(0)
+            self._statusTimer = None
+
+    def RepaintOldSelection(self):
+        """ Repaints the old selected/hovered button. """
+        current = self._currentButton
+        if current == -1:
+            return
+        btn = self._vButtons[current]
+        if not btn.IsEnabled():
+            return
+        btn.SetStatus("Normal")
+
+    def OnStatusBarTimer(self):
+        """ Handles the timer expiring to delete the longHelp in the StatusBar. """
+        topLevel = wx.GetTopLevelParent(self)
+        statusBar = topLevel.GetStatusBar()
+        statusBar.PopStatusText(0)
+
+    def SetUseHelp(self, useHelp=True):
+        """ Sets whether or not shortHelp and longHelp should be displayed. """
+        self._useHelp = useHelp
+
+    def GetUseHelp(self):
+        """ Returns whether or not shortHelp and longHelp should be displayed. """
+        return self._useHelp
+
+    def HitTest(self, pt):
+        """
+        HitTest method for ButtonPanel. Returns the button (if any) and
+        a flag (if any).
+        """
+        for ii in xrange(len(self._vButtons)):
+            if not self._vButtons[ii].IsEnabled():
+                continue
+            if self._vButtons[ii].GetRect().Contains(pt):
+                return ii, BP_HT_BUTTON
+        return -1, BP_HT_NONE
+
+    def GetBPArt(self):
+        """ Returns the associated BPArt art provider. """
+        return self._art
+
+    def SetBPArt(self, art):
+        """ Sets a new BPArt to ButtonPanel. Useful only if another BPArt class is used. """
+        self._art = art
+        self.Refresh()
+
+    if wx.VERSION < (2,7,1,1):
+        def Freeze(self):
+            """Freeze ButtonPanel."""
+            self._freezeCount = self._freezeCount + 1
+            wx.PyPanel.Freeze(self)
+
+        def Thaw(self):
+            """Thaw ButtonPanel."""
+            if self._freezeCount == 0:
+                raise "\nERROR: Thawing Unfrozen ButtonPanel?"
+            self._freezeCount = self._freezeCount - 1
+            wx.PyPanel.Thaw(self)
+
+        def IsFrozen(self):
+            """ Returns whether a call to Freeze() has been done. """
+            return self._freezeCount != 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/FlatNotebook.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4898 @@
+# --------------------------------------------------------------------------- #
+# FLATNOTEBOOK Widget wxPython IMPLEMENTATION
+#
+# Original C++ Code From Eran. You Can Find It At:
+#
+# http://wxforum.shadonet.com/viewtopic.php?t=5761&start=0
+#
+# License: wxWidgets license
+#
+#
+# Python Code By:
+#
+# Andrea Gavana, @ 02 Oct 2006
+# Latest Revision: 22 Nov 2007, 14.00 GMT
+#
+#
+# For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please
+# Write To Me At:
+#
+# andrea.gavana@gmail.com
+# gavana@kpo.kz
+#
+# Or, Obviously, To The wxPython Mailing List!!!
+#
+#
+# End Of Comments
+# --------------------------------------------------------------------------- #
+
+"""
+The FlatNotebook is a full implementation of the wx.Notebook, and designed to be
+a drop-in replacement for wx.Notebook. The API functions are similar so one can
+expect the function to behave in the same way.
+
+Some features:
+
+  - The buttons are highlighted a la Firefox style
+  - The scrolling is done for bulks of tabs (so, the scrolling is faster and better)
+  - The buttons area is never overdrawn by tabs (unlike many other implementations I saw)
+  - It is a generic control
+  - Currently there are 5 differnt styles - VC8, VC 71, Standard, Fancy and Firefox 2;
+  - Mouse middle click can be used to close tabs
+  - A function to add right click menu for tabs (simple as SetRightClickMenu)
+  - All styles has bottom style as well (they can be drawn in the bottom of screen)
+  - An option to hide 'X' button or navigation buttons (separately)
+  - Gradient coloring of the selected tabs and border
+  - Support for drag 'n' drop of tabs, both in the same notebook or to another notebook
+  - Possibility to have closing button on the active tab directly
+  - Support for disabled tabs
+  - Colours for active/inactive tabs, and captions
+  - Background of tab area can be painted in gradient (VC8 style only)
+  - Colourful tabs - a random gentle colour is generated for each new tab (very cool, VC8 style only)
+
+
+And much more.
+
+
+License And Version:
+
+FlatNotebook Is Freeware And Distributed Under The wxPython License.
+
+Latest Revision: Andrea Gavana @ 22 Nov 2007, 14.00 GMT
+
+Version 2.4.
+
+@undocumented: FNB_HEIGHT_SPACER, VERTICAL_BORDER_PADDING, VC8_SHAPE_LEN,
+    wxEVT*, left_arrow_*, right_arrow*, x_button*, down_arrow*,
+    FNBDragInfo, FNBDropTarget, GetMondrian*
+"""
+
+__docformat__ = "epytext"
+
+
+#----------------------------------------------------------------------
+# Beginning Of FLATNOTEBOOK wxPython Code
+#----------------------------------------------------------------------
+
+import wx
+import random
+import math
+import weakref
+import cPickle
+
+if wx.Platform == '__WXMAC__':
+    import Carbon.Appearance
+
+# Check for the new method in 2.7 (not present in 2.6.3.3)
+if wx.VERSION_STRING < "2.7":
+    wx.Rect.Contains = lambda self, point: wx.Rect.Inside(self, point)
+
+FNB_HEIGHT_SPACER = 10
+
+# Use Visual Studio 2003 (VC7.1) style for tabs
+FNB_VC71 = 1
+"""Use Visual Studio 2003 (VC7.1) style for tabs"""
+
+# Use fancy style - square tabs filled with gradient coloring
+FNB_FANCY_TABS = 2
+"""Use fancy style - square tabs filled with gradient coloring"""
+
+# Draw thin border around the page
+FNB_TABS_BORDER_SIMPLE = 4
+"""Draw thin border around the page"""
+
+# Do not display the 'X' button
+FNB_NO_X_BUTTON = 8
+"""Do not display the 'X' button"""
+
+# Do not display the Right / Left arrows
+FNB_NO_NAV_BUTTONS = 16
+"""Do not display the right/left arrows"""
+
+# Use the mouse middle button for cloing tabs
+FNB_MOUSE_MIDDLE_CLOSES_TABS = 32
+"""Use the mouse middle button for cloing tabs"""
+
+# Place tabs at bottom - the default is to place them
+# at top
+FNB_BOTTOM = 64
+"""Place tabs at bottom - the default is to place them at top"""
+
+# Disable dragging of tabs
+FNB_NODRAG = 128
+"""Disable dragging of tabs"""
+
+# Use Visual Studio 2005 (VC8) style for tabs
+FNB_VC8 = 256
+"""Use Visual Studio 2005 (VC8) style for tabs"""
+
+# Firefox 2 tabs style
+FNB_FF2 = 131072
+"""Use Firefox 2 style for tabs"""
+
+# Place 'X' on a tab
+FNB_X_ON_TAB = 512
+"""Place 'X' close button on the active tab"""
+
+FNB_BACKGROUND_GRADIENT = 1024
+"""Use gradients to paint the tabs background"""
+
+FNB_COLORFUL_TABS = 2048
+"""Use colourful tabs (VC8 style only)"""
+
+# Style to close tab using double click - styles 1024, 2048 are reserved
+FNB_DCLICK_CLOSES_TABS = 4096
+"""Style to close tab using double click"""
+
+FNB_SMART_TABS = 8192
+"""Use Smart Tabbing, like Alt+Tab on Windows"""
+
+FNB_DROPDOWN_TABS_LIST = 16384
+"""Use a dropdown menu on the left in place of the arrows"""
+
+FNB_ALLOW_FOREIGN_DND = 32768
+"""Allows drag 'n' drop operations between different L{FlatNotebook}s"""
+
+FNB_HIDE_ON_SINGLE_TAB = 65536
+"""Hides the Page Container when there is one or fewer tabs"""
+
+VERTICAL_BORDER_PADDING = 4
+
+# Button size is a 16x16 xpm bitmap
+BUTTON_SPACE = 16
+"""Button size is a 16x16 xpm bitmap"""
+
+VC8_SHAPE_LEN = 16
+
+MASK_COLOR  = wx.Colour(0, 128, 128)
+"""Mask colour for the arrow bitmaps"""
+
+# Button status
+FNB_BTN_PRESSED = 2
+"""Navigation button is pressed"""
+FNB_BTN_HOVER = 1
+"""Navigation button is hovered"""
+FNB_BTN_NONE = 0
+"""No navigation"""
+
+# Hit Test results
+FNB_TAB = 1             # On a tab
+"""Indicates mouse coordinates inside a tab"""
+FNB_X = 2               # On the X button
+"""Indicates mouse coordinates inside the I{X} region"""
+FNB_TAB_X = 3           # On the 'X' button (tab's X button)
+"""Indicates mouse coordinates inside the I{X} region in a tab"""
+FNB_LEFT_ARROW = 4      # On the rotate left arrow button
+"""Indicates mouse coordinates inside the left arrow region"""
+FNB_RIGHT_ARROW = 5     # On the rotate right arrow button
+"""Indicates mouse coordinates inside the right arrow region"""
+FNB_DROP_DOWN_ARROW = 6 # On the drop down arrow button
+"""Indicates mouse coordinates inside the drop down arrow region"""
+FNB_NOWHERE = 0         # Anywhere else
+"""Indicates mouse coordinates not on any tab of the notebook"""
+
+FNB_DEFAULT_STYLE = FNB_MOUSE_MIDDLE_CLOSES_TABS | FNB_HIDE_ON_SINGLE_TAB
+"""L{FlatNotebook} default style"""
+
+# FlatNotebook Events:
+# wxEVT_FLATNOTEBOOK_PAGE_CHANGED: Event Fired When You Switch Page;
+# wxEVT_FLATNOTEBOOK_PAGE_CHANGING: Event Fired When You Are About To Switch
+# Pages, But You Can Still "Veto" The Page Changing By Avoiding To Call
+# event.Skip() In Your Event Handler;
+# wxEVT_FLATNOTEBOOK_PAGE_CLOSING: Event Fired When A Page Is Closing, But
+# You Can Still "Veto" The Page Changing By Avoiding To Call event.Skip()
+# In Your Event Handler;
+# wxEVT_FLATNOTEBOOK_PAGE_CLOSED: Event Fired When A Page Is Closed.
+# wxEVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU: Event Fired When A Menu Pops-up In A Tab.
+
+wxEVT_FLATNOTEBOOK_PAGE_CHANGED = wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED
+wxEVT_FLATNOTEBOOK_PAGE_CHANGING = wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING
+wxEVT_FLATNOTEBOOK_PAGE_CLOSING = wx.NewEventType()
+wxEVT_FLATNOTEBOOK_PAGE_CLOSED = wx.NewEventType()
+wxEVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU = wx.NewEventType()
+
+#-----------------------------------#
+#        FlatNotebookEvent
+#-----------------------------------#
+
+EVT_FLATNOTEBOOK_PAGE_CHANGED = wx.EVT_NOTEBOOK_PAGE_CHANGED
+"""Notify client objects when the active page in L{FlatNotebook}
+has changed."""
+EVT_FLATNOTEBOOK_PAGE_CHANGING = wx.EVT_NOTEBOOK_PAGE_CHANGING
+"""Notify client objects when the active page in L{FlatNotebook}
+is about to change."""
+EVT_FLATNOTEBOOK_PAGE_CLOSING = wx.PyEventBinder(wxEVT_FLATNOTEBOOK_PAGE_CLOSING, 1)
+"""Notify client objects when a page in L{FlatNotebook} is closing."""
+EVT_FLATNOTEBOOK_PAGE_CLOSED = wx.PyEventBinder(wxEVT_FLATNOTEBOOK_PAGE_CLOSED, 1)
+"""Notify client objects when a page in L{FlatNotebook} has been closed."""
+EVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU = wx.PyEventBinder(wxEVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU, 1)
+"""Notify client objects when a pop-up menu should appear next to a tab."""
+
+
+# Some icons in XPM format
+
+left_arrow_disabled_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #555555",
+    "# c #000000",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````.```````",
+    "```````..```````",
+    "``````.`.```````",
+    "`````.``.```````",
+    "````.```.```````",
+    "`````.``.```````",
+    "``````.`.```````",
+    "```````..```````",
+    "````````.```````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````"
+    ]
+
+x_button_pressed_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #4766e0",
+    "# c #9e9ede",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "`..............`",
+    "`.############.`",
+    "`.############.`",
+    "`.############.`",
+    "`.###aa####aa#.`",
+    "`.####aa##aa##.`",
+    "`.#####aaaa###.`",
+    "`.######aa####.`",
+    "`.#####aaaa###.`",
+    "`.####aa##aa##.`",
+    "`.###aa####aa#.`",
+    "`.############.`",
+    "`..............`",
+    "````````````````",
+    "````````````````"
+    ]
+
+
+left_arrow_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #555555",
+    "# c #000000",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````.```````",
+    "```````..```````",
+    "``````...```````",
+    "`````....```````",
+    "````.....```````",
+    "`````....```````",
+    "``````...```````",
+    "```````..```````",
+    "````````.```````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````"
+    ]
+
+x_button_hilite_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #4766e0",
+    "# c #c9dafb",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "`..............`",
+    "`.############.`",
+    "`.############.`",
+    "`.##aa####aa##.`",
+    "`.###aa##aa###.`",
+    "`.####aaaa####.`",
+    "`.#####aa#####.`",
+    "`.####aaaa####.`",
+    "`.###aa##aa###.`",
+    "`.##aa####aa##.`",
+    "`.############.`",
+    "`.############.`",
+    "`..............`",
+    "````````````````",
+    "````````````````"
+    ]
+
+x_button_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #555555",
+    "# c #000000",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````..````..````",
+    "`````..``..`````",
+    "``````....``````",
+    "```````..```````",
+    "``````....``````",
+    "`````..``..`````",
+    "````..````..````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````"
+    ]
+
+left_arrow_pressed_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #4766e0",
+    "# c #9e9ede",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "`..............`",
+    "`.############.`",
+    "`.############.`",
+    "`.#######a####.`",
+    "`.######aa####.`",
+    "`.#####aaa####.`",
+    "`.####aaaa####.`",
+    "`.###aaaaa####.`",
+    "`.####aaaa####.`",
+    "`.#####aaa####.`",
+    "`.######aa####.`",
+    "`.#######a####.`",
+    "`..............`",
+    "````````````````",
+    "````````````````"
+    ]
+
+left_arrow_hilite_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #4766e0",
+    "# c #c9dafb",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "`..............`",
+    "`.############.`",
+    "`.######a#####.`",
+    "`.#####aa#####.`",
+    "`.####aaa#####.`",
+    "`.###aaaa#####.`",
+    "`.##aaaaa#####.`",
+    "`.###aaaa#####.`",
+    "`.####aaa#####.`",
+    "`.#####aa#####.`",
+    "`.######a#####.`",
+    "`.############.`",
+    "`..............`",
+    "````````````````",
+    "````````````````"
+    ]
+
+right_arrow_disabled_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #555555",
+    "# c #000000",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "```````.````````",
+    "```````..```````",
+    "```````.`.``````",
+    "```````.``.`````",
+    "```````.```.````",
+    "```````.``.`````",
+    "```````.`.``````",
+    "```````..```````",
+    "```````.````````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````"
+    ]
+
+right_arrow_hilite_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #4766e0",
+    "# c #c9dafb",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "`..............`",
+    "`.############.`",
+    "`.####a#######.`",
+    "`.####aa######.`",
+    "`.####aaa#####.`",
+    "`.####aaaa####.`",
+    "`.####aaaaa###.`",
+    "`.####aaaa####.`",
+    "`.####aaa#####.`",
+    "`.####aa######.`",
+    "`.####a#######.`",
+    "`.############.`",
+    "`..............`",
+    "````````````````",
+    "````````````````"
+    ]
+
+right_arrow_pressed_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #4766e0",
+    "# c #9e9ede",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "`..............`",
+    "`.############.`",
+    "`.############.`",
+    "`.#####a######.`",
+    "`.#####aa#####.`",
+    "`.#####aaa####.`",
+    "`.#####aaaa###.`",
+    "`.#####aaaaa##.`",
+    "`.#####aaaa###.`",
+    "`.#####aaa####.`",
+    "`.#####aa#####.`",
+    "`.#####a######.`",
+    "`..............`",
+    "````````````````",
+    "````````````````"
+    ]
+
+
+right_arrow_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #555555",
+    "# c #000000",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "```````.````````",
+    "```````..```````",
+    "```````...``````",
+    "```````....`````",
+    "```````.....````",
+    "```````....`````",
+    "```````...``````",
+    "```````..```````",
+    "```````.````````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````"
+    ]
+
+down_arrow_hilite_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #4766e0",
+    "# c #c9dafb",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "``.............`",
+    "``.###########.`",
+    "``.###########.`",
+    "``.###########.`",
+    "``.#aaaaaaaaa#.`",
+    "``.##aaaaaaa##.`",
+    "``.###aaaaa###.`",
+    "``.####aaa####.`",
+    "``.#####a#####.`",
+    "``.###########.`",
+    "``.###########.`",
+    "``.###########.`",
+    "``.............`",
+    "````````````````",
+    "````````````````"
+    ]
+
+down_arrow_pressed_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #4766e0",
+    "# c #9e9ede",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "``.............`",
+    "``.###########.`",
+    "``.###########.`",
+    "``.###########.`",
+    "``.###########.`",
+    "``.###########.`",
+    "``.#aaaaaaaaa#.`",
+    "``.##aaaaaaa##.`",
+    "``.###aaaaa###.`",
+    "``.####aaa####.`",
+    "``.#####a#####.`",
+    "``.###########.`",
+    "``.............`",
+    "````````````````",
+    "````````````````"
+    ]
+
+
+down_arrow_xpm = [
+    "    16    16        8            1",
+    "` c #008080",
+    ". c #000000",
+    "# c #000000",
+    "a c #000000",
+    "b c #000000",
+    "c c #000000",
+    "d c #000000",
+    "e c #000000",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````.........```",
+    "`````.......````",
+    "``````.....`````",
+    "```````...``````",
+    "````````.```````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````",
+    "````````````````"
+    ]
+
+
+#----------------------------------------------------------------------
+def GetMondrianData():
+    return \
+'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\
+\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00qID\
+ATX\x85\xed\xd6;\n\x800\x10E\xd1{\xc5\x8d\xb9r\x97\x16\x0b\xad$\x8a\x82:\x16\
+o\xda\x84pB2\x1f\x81Fa\x8c\x9c\x08\x04Z{\xcf\xa72\xbcv\xfa\xc5\x08 \x80r\x80\
+\xfc\xa2\x0e\x1c\xe4\xba\xfaX\x1d\xd0\xde]S\x07\x02\xd8>\xe1wa-`\x9fQ\xe9\
+\x86\x01\x04\x10\x00\\(Dk\x1b-\x04\xdc\x1d\x07\x14\x98;\x0bS\x7f\x7f\xf9\x13\
+\x04\x10@\xf9X\xbe\x00\xc9 \x14K\xc1<={\x00\x00\x00\x00IEND\xaeB`\x82'
+
+
+def GetMondrianBitmap():
+    return wx.BitmapFromImage(GetMondrianImage().Scale(16, 16))
+
+
+def GetMondrianImage():
+    import cStringIO
+    stream = cStringIO.StringIO(GetMondrianData())
+    return wx.ImageFromStream(stream)
+
+
+def GetMondrianIcon():
+    icon = wx.EmptyIcon()
+    icon.CopyFromBitmap(GetMondrianBitmap())
+    return icon
+#----------------------------------------------------------------------
+
+
+def LightColour(color, percent):
+    """ Brighten input colour by percent. """
+
+    end_color = wx.WHITE
+
+    rd = end_color.Red() - color.Red()
+    gd = end_color.Green() - color.Green()
+    bd = end_color.Blue() - color.Blue()
+
+    high = 100
+
+    # We take the percent way of the color from color -. white
+    i = percent
+    r = color.Red() + ((i*rd*100)/high)/100
+    g = color.Green() + ((i*gd*100)/high)/100
+    b = color.Blue() + ((i*bd*100)/high)/100
+    return wx.Colour(r, g, b)
+
+
+def RandomColour():
+    """ Creates a random colour. """
+
+    r = random.randint(0, 255) # Random value betweem 0-255
+    g = random.randint(0, 255) # Random value betweem 0-255
+    b = random.randint(0, 255) # Random value betweem 0-255
+
+    return wx.Colour(r, g, b)
+
+
+def PaintStraightGradientBox(dc, rect, startColor, endColor, vertical=True):
+    """ Draws a gradient colored box from startColor to endColor. """
+
+    rd = endColor.Red() - startColor.Red()
+    gd = endColor.Green() - startColor.Green()
+    bd = endColor.Blue() - startColor.Blue()
+
+    # Save the current pen and brush
+    savedPen = dc.GetPen()
+    savedBrush = dc.GetBrush()
+
+    if vertical:
+        high = rect.GetHeight()-1
+    else:
+        high = rect.GetWidth()-1
+
+    if high < 1:
+        return
+
+    for i in xrange(high+1):
+
+        r = startColor.Red() + ((i*rd*100)/high)/100
+        g = startColor.Green() + ((i*gd*100)/high)/100
+        b = startColor.Blue() + ((i*bd*100)/high)/100
+
+        p = wx.Pen(wx.Colour(r, g, b))
+        dc.SetPen(p)
+
+        if vertical:
+            dc.DrawLine(rect.x, rect.y+i, rect.x+rect.width, rect.y+i)
+        else:
+            dc.DrawLine(rect.x+i, rect.y, rect.x+i, rect.y+rect.height)
+
+    # Restore the pen and brush
+    dc.SetPen(savedPen)
+    dc.SetBrush(savedBrush)
+
+
+
+# -----------------------------------------------------------------------------
+# Util functions
+# -----------------------------------------------------------------------------
+
+def DrawButton(dc, rect, focus, upperTabs):
+
+    # Define the rounded rectangle base on the given rect
+    # we need an array of 9 points for it
+    regPts = [wx.Point() for indx in xrange(9)]
+
+    if focus:
+        if upperTabs:
+            leftPt = wx.Point(rect.x, rect.y + (rect.height / 10)*8)
+            rightPt = wx.Point(rect.x + rect.width - 2, rect.y + (rect.height / 10)*8)
+        else:
+            leftPt = wx.Point(rect.x, rect.y + (rect.height / 10)*5)
+            rightPt = wx.Point(rect.x + rect.width - 2, rect.y + (rect.height / 10)*5)
+    else:
+        leftPt = wx.Point(rect.x, rect.y + (rect.height / 2))
+        rightPt = wx.Point(rect.x + rect.width - 2, rect.y + (rect.height / 2))
+
+    # Define the top region
+    top = wx.RectPP(rect.GetTopLeft(), rightPt)
+    bottom = wx.RectPP(leftPt, rect.GetBottomRight())
+
+    topStartColor = wx.WHITE
+
+    if not focus:
+        topStartColor = LightColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE), 50)
+
+    topEndColor = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)
+    bottomStartColor = topEndColor
+    bottomEndColor = topEndColor
+
+    # Incase we use bottom tabs, switch the colors
+    if upperTabs:
+        if focus:
+            PaintStraightGradientBox(dc, top, topStartColor, topEndColor)
+            PaintStraightGradientBox(dc, bottom, bottomStartColor, bottomEndColor)
+        else:
+            PaintStraightGradientBox(dc, top, topEndColor , topStartColor)
+            PaintStraightGradientBox(dc, bottom, bottomStartColor, bottomEndColor)
+
+    else:
+        if focus:
+            PaintStraightGradientBox(dc, bottom, topEndColor, bottomEndColor)
+            PaintStraightGradientBox(dc, top,topStartColor,  topStartColor)
+        else:
+            PaintStraightGradientBox(dc, bottom, bottomStartColor, bottomEndColor)
+            PaintStraightGradientBox(dc, top, topEndColor, topStartColor)
+
+    dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+
+# ---------------------------------------------------------------------------- #
+# Class FNBDropSource
+# Gives Some Custom UI Feedback during the DnD Operations
+# ---------------------------------------------------------------------------- #
+
+class FNBDropSource(wx.DropSource):
+    """
+    Give some custom UI feedback during the drag and drop operation in this
+    function. It is called on each mouse move, so your implementation must
+    not be too slow.
+    """
+
+    def __init__(self, win):
+        """ Default class constructor. Used internally. """
+
+        wx.DropSource.__init__(self, win)
+        self._win = win
+
+
+    def GiveFeedback(self, effect):
+        """ Provides user with a nice feedback when tab is being dragged. """
+
+        self._win.DrawDragHint()
+        return False
+
+
+# ---------------------------------------------------------------------------- #
+# Class FNBDragInfo
+# Stores All The Information To Allow Drag And Drop Between Different
+# FlatNotebooks.
+# ---------------------------------------------------------------------------- #
+
+class FNBDragInfo:
+
+    _map = weakref.WeakValueDictionary()
+
+    def __init__(self, container, pageindex):
+        """ Default class constructor. """
+
+        self._id = id(container)
+        FNBDragInfo._map[self._id] = container
+        self._pageindex = pageindex
+
+
+    def GetContainer(self):
+        """ Returns the L{FlatNotebook} page (usually a panel). """
+
+        return FNBDragInfo._map.get(self._id, None)
+
+
+    def GetPageIndex(self):
+        """ Returns the page index associated with a page. """
+
+        return self._pageindex
+
+
+# ---------------------------------------------------------------------------- #
+# Class FNBDropTarget
+# Simply Used To Handle The OnDrop() Method When Dragging And Dropping Between
+# Different FlatNotebooks.
+# ---------------------------------------------------------------------------- #
+
+class FNBDropTarget(wx.DropTarget):
+
+    def __init__(self, parent):
+        """ Default class constructor. """
+
+        wx.DropTarget.__init__(self)
+
+        self._parent = parent
+        self._dataobject = wx.CustomDataObject(wx.CustomDataFormat("FlatNotebook"))
+        self.SetDataObject(self._dataobject)
+
+
+    def OnData(self, x, y, dragres):
+        """ Handles the OnData() method to call the real DnD routine. """
+
+        if not self.GetData():
+            return wx.DragNone
+
+        draginfo = self._dataobject.GetData()
+        drginfo = cPickle.loads(draginfo)
+
+        return self._parent.OnDropTarget(x, y, drginfo.GetPageIndex(), drginfo.GetContainer())
+
+
+# ---------------------------------------------------------------------------- #
+# Class PageInfo
+# Contains parameters for every FlatNotebook page
+# ---------------------------------------------------------------------------- #
+
+class PageInfo:
+    """
+    This class holds all the information (caption, image, etc...) belonging to a
+    single tab in L{FlatNotebook}.
+    """
+
+    def __init__(self, caption="", imageindex=-1, tabangle=0, enabled=True):
+        """
+        Default Class Constructor.
+
+        Parameters:
+        @param caption: the tab caption;
+        @param imageindex: the tab image index based on the assigned (set) wx.ImageList (if any);
+        @param tabangle: the tab angle (only on standard tabs, from 0 to 15 degrees);
+        @param enabled: sets enabled or disabled the tab.
+        """
+
+        self._strCaption = caption
+        self._TabAngle = tabangle
+        self._ImageIndex = imageindex
+        self._bEnabled = enabled
+        self._pos = wx.Point(-1, -1)
+        self._size = wx.Size(-1, -1)
+        self._region = wx.Region()
+        self._xRect = wx.Rect()
+        self._color = None
+        self._hasFocus = False
+
+
+    def SetCaption(self, value):
+        """ Sets the tab caption. """
+
+        self._strCaption = value
+
+
+    def GetCaption(self):
+        """ Returns the tab caption. """
+
+        return self._strCaption
+
+
+    def SetPosition(self, value):
+        """ Sets the tab position. """
+
+        self._pos = value
+
+
+    def GetPosition(self):
+        """ Returns the tab position. """
+
+        return self._pos
+
+
+    def SetSize(self, value):
+        """ Sets the tab size. """
+
+        self._size = value
+
+
+    def GetSize(self):
+        """ Returns the tab size. """
+
+        return self._size
+
+
+    def SetTabAngle(self, value):
+        """ Sets the tab header angle (0 <= tab <= 15 degrees). """
+
+        self._TabAngle = min(45, value)
+
+
+    def GetTabAngle(self):
+        """ Returns the tab angle. """
+
+        return self._TabAngle
+
+
+    def SetImageIndex(self, value):
+        """ Sets the tab image index. """
+
+        self._ImageIndex = value
+
+
+    def GetImageIndex(self):
+        """ Returns the tab umage index. """
+
+        return self._ImageIndex
+
+
+    def GetEnabled(self):
+        """ Returns whether the tab is enabled or not. """
+
+        return self._bEnabled
+
+
+    def EnableTab(self, enabled):
+        """ Sets the tab enabled or disabled. """
+
+        self._bEnabled = enabled
+
+
+    def SetRegion(self, points=[]):
+        """ Sets the tab region. """
+
+        self._region = wx.RegionFromPoints(points)
+
+
+    def GetRegion(self):
+        """ Returns the tab region. """
+
+        return self._region
+
+
+    def SetXRect(self, xrect):
+        """ Sets the button 'X' area rect. """
+
+        self._xRect = xrect
+
+
+    def GetXRect(self):
+        """ Returns the button 'X' area rect. """
+
+        return self._xRect
+
+
+    def GetColour(self):
+        """ Returns the tab colour. """
+
+        return self._color
+
+
+    def SetColour(self, color):
+        """ Sets the tab colour. """
+
+        self._color = color
+
+
+# ---------------------------------------------------------------------------- #
+# Class FlatNotebookEvent
+# ---------------------------------------------------------------------------- #
+
+class FlatNotebookEvent(wx.PyCommandEvent):
+    """
+    This events will be sent when a EVT_FLATNOTEBOOK_PAGE_CHANGED,
+    EVT_FLATNOTEBOOK_PAGE_CHANGING, EVT_FLATNOTEBOOK_PAGE_CLOSING,
+    EVT_FLATNOTEBOOK_PAGE_CLOSED and EVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU is
+    mapped in the parent.
+    """
+
+    def __init__(self, eventType, id=1, nSel=-1, nOldSel=-1):
+        """ Default class constructor. """
+
+        wx.PyCommandEvent.__init__(self, eventType, id)
+        self._eventType = eventType
+
+        self.notify = wx.NotifyEvent(eventType, id)
+
+
+    def GetNotifyEvent(self):
+        """Returns the actual wx.NotifyEvent."""
+
+        return self.notify
+
+
+    def IsAllowed(self):
+        """Returns whether the event is allowed or not."""
+
+        return self.notify.IsAllowed()
+
+
+    def Veto(self):
+        """Vetos the event."""
+
+        self.notify.Veto()
+
+
+    def Allow(self):
+        """The event is allowed."""
+
+        self.notify.Allow()
+
+
+    def SetSelection(self, nSel):
+        """ Sets event selection. """
+
+        self._selection = nSel
+
+
+    def SetOldSelection(self, nOldSel):
+        """ Sets old event selection. """
+
+        self._oldselection = nOldSel
+
+
+    def GetSelection(self):
+        """ Returns event selection. """
+
+        return self._selection
+
+
+    def GetOldSelection(self):
+        """ Returns old event selection """
+
+        return self._oldselection
+
+
+# ---------------------------------------------------------------------------- #
+# Class TabNavigatorWindow
+# ---------------------------------------------------------------------------- #
+
+class TabNavigatorWindow(wx.Dialog):
+    """
+    This class is used to create a modal dialog that enables "Smart Tabbing",
+    similar to what you would get by hitting Alt+Tab on Windows.
+    """
+
+    def __init__(self, parent=None, icon=None):
+        """ Default class constructor. Used internally."""
+
+        wx.Dialog.__init__(self, parent, wx.ID_ANY, "", style=0)
+
+        self._selectedItem = -1
+        self._indexMap = []
+
+        if icon is None:
+            self._bmp = GetMondrianBitmap()
+        else:
+            self._bmp = icon
+
+        sz = wx.BoxSizer(wx.VERTICAL)
+
+        self._listBox = wx.ListBox(self, wx.ID_ANY, wx.DefaultPosition, wx.Size(200, 150), [], wx.LB_SINGLE | wx.NO_BORDER)
+
+        mem_dc = wx.MemoryDC()
+        mem_dc.SelectObject(wx.EmptyBitmap(1,1))
+        font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        font.SetWeight(wx.BOLD)
+        mem_dc.SetFont(font)
+
+        panelHeight = mem_dc.GetCharHeight()
+        panelHeight += 4 # Place a spacer of 2 pixels
+
+        # Out signpost bitmap is 24 pixels
+        if panelHeight < 24:
+            panelHeight = 24
+
+        self._panel = wx.Panel(self, wx.ID_ANY, wx.DefaultPosition, wx.Size(200, panelHeight))
+
+        sz.Add(self._panel)
+        sz.Add(self._listBox, 1, wx.EXPAND)
+
+        self.SetSizer(sz)
+
+        # Connect events to the list box
+        self._listBox.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
+        self._listBox.Bind(wx.EVT_NAVIGATION_KEY, self.OnNavigationKey)
+        self._listBox.Bind(wx.EVT_LISTBOX_DCLICK, self.OnItemSelected)
+
+        # Connect paint event to the panel
+        self._panel.Bind(wx.EVT_PAINT, self.OnPanelPaint)
+        self._panel.Bind(wx.EVT_ERASE_BACKGROUND, self.OnPanelEraseBg)
+
+        self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))
+        self._listBox.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))
+        self.PopulateListControl(parent)
+
+        self.GetSizer().Fit(self)
+        self.GetSizer().SetSizeHints(self)
+        self.GetSizer().Layout()
+        self.Centre()
+
+
+    def OnKeyUp(self, event):
+        """Handles the wx.EVT_KEY_UP for the L{TabNavigatorWindow}."""
+
+        if event.GetKeyCode() == wx.WXK_CONTROL:
+            self.CloseDialog()
+
+
+    def OnNavigationKey(self, event):
+        """Handles the wx.EVT_NAVIGATION_KEY for the L{TabNavigatorWindow}. """
+
+        selected = self._listBox.GetSelection()
+        bk = self.GetParent()
+        maxItems = bk.GetPageCount()
+
+        if event.GetDirection():
+
+            # Select next page
+            if selected == maxItems - 1:
+                itemToSelect = 0
+            else:
+                itemToSelect = selected + 1
+
+        else:
+
+            # Previous page
+            if selected == 0:
+                itemToSelect = maxItems - 1
+            else:
+                itemToSelect = selected - 1
+
+        self._listBox.SetSelection(itemToSelect)
+
+
+    def PopulateListControl(self, book):
+        """Populates the L{TabNavigatorWindow} listbox with a list of tabs."""
+
+        selection = book.GetSelection()
+        count = book.GetPageCount()
+
+        self._listBox.Append(book.GetPageText(selection))
+        self._indexMap.append(selection)
+
+        prevSel = book.GetPreviousSelection()
+
+        if prevSel != wx.NOT_FOUND:
+
+            # Insert the previous selection as second entry
+            self._listBox.Append(book.GetPageText(prevSel))
+            self._indexMap.append(prevSel)
+
+        for c in xrange(count):
+
+            # Skip selected page
+            if c == selection:
+                continue
+
+            # Skip previous selected page as well
+            if c == prevSel:
+                continue
+
+            self._listBox.Append(book.GetPageText(c))
+            self._indexMap.append(c)
+
+        # Select the next entry after the current selection
+        self._listBox.SetSelection(0)
+        dummy = wx.NavigationKeyEvent()
+        dummy.SetDirection(True)
+        self.OnNavigationKey(dummy)
+
+
+    def OnItemSelected(self, event):
+        """Handles the wx.EVT_LISTBOX_DCLICK event for the wx.ListBox inside L{TabNavigatorWindow}. """
+
+        self.CloseDialog()
+
+
+    def CloseDialog(self):
+        """Closes the L{TabNavigatorWindow} dialog, setting selection in L{FlatNotebook}."""
+
+        bk = self.GetParent()
+        self._selectedItem = self._listBox.GetSelection()
+        iter = self._indexMap[self._selectedItem]
+        bk._pages.FireEvent(iter)
+        self.EndModal(wx.ID_OK)
+
+
+    def OnPanelPaint(self, event):
+        """Handles the wx.EVT_PAINT event for L{TabNavigatorWindow} top panel. """
+
+        dc = wx.PaintDC(self._panel)
+        rect = self._panel.GetClientRect()
+
+        bmp = wx.EmptyBitmap(rect.width, rect.height)
+
+        mem_dc = wx.MemoryDC()
+        mem_dc.SelectObject(bmp)
+
+        endColour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)
+        startColour = LightColour(endColour, 50)
+        PaintStraightGradientBox(mem_dc, rect, startColour, endColour)
+
+        # Draw the caption title and place the bitmap
+        # get the bitmap optimal position, and draw it
+        bmpPt, txtPt = wx.Point(), wx.Point()
+        bmpPt.y = (rect.height - self._bmp.GetHeight())/2
+        bmpPt.x = 3
+        mem_dc.DrawBitmap(self._bmp, bmpPt.x, bmpPt.y, True)
+
+        # get the text position, and draw it
+        font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        font.SetWeight(wx.BOLD)
+        mem_dc.SetFont(font)
+        fontHeight = mem_dc.GetCharHeight()
+
+        txtPt.x = bmpPt.x + self._bmp.GetWidth() + 4
+        txtPt.y = (rect.height - fontHeight)/2
+        mem_dc.SetTextForeground(wx.WHITE)
+        mem_dc.DrawText("Opened tabs:", txtPt.x, txtPt.y)
+        mem_dc.SelectObject(wx.NullBitmap)
+
+        dc.DrawBitmap(bmp, 0, 0)
+
+
+    def OnPanelEraseBg(self, event):
+        """Handles the wx.EVT_ERASE_BACKGROUND event for L{TabNavigatorWindow} top panel. """
+
+        pass
+
+
+# ---------------------------------------------------------------------------- #
+# Class FNBRenderer
+# ---------------------------------------------------------------------------- #
+
+class FNBRenderer:
+    """
+    Parent class for the 4 renderers defined: I{Standard}, I{VC71}, I{Fancy}
+    and I{VC8}. This class implements the common methods of all 4 renderers.
+    """
+
+    def __init__(self):
+        """Default class constructor. """
+
+        self._tabHeight = None
+
+        if wx.Platform == "__WXMAC__":
+            # Hack to get proper highlight color for focus rectangle from
+            # current theme by creating a theme brush and getting its color.
+            # kThemeBrushFocusHighlight is available on Mac OS 8.5 and higher
+            brush = wx.BLACK_BRUSH
+            brush.MacSetTheme(Carbon.Appearance.kThemeBrushFocusHighlight)
+            self._focusPen = wx.Pen(brush.GetColour(), 2, wx.SOLID)
+        else:
+            self._focusPen = wx.Pen(wx.BLACK, 1, wx.USER_DASH)
+            self._focusPen.SetDashes([1, 1])
+            self._focusPen.SetCap(wx.CAP_BUTT)
+
+
+    def GetLeftButtonPos(self, pageContainer):
+        """ Returns the left button position in the navigation area. """
+
+        pc = pageContainer
+        style = pc.GetParent().GetWindowStyleFlag()
+        rect = pc.GetClientRect()
+        clientWidth = rect.width
+
+        if style & FNB_NO_X_BUTTON:
+            return clientWidth - 38
+        else:
+            return clientWidth - 54
+
+
+    def GetRightButtonPos(self, pageContainer):
+        """ Returns the right button position in the navigation area. """
+
+        pc = pageContainer
+        style = pc.GetParent().GetWindowStyleFlag()
+        rect = pc.GetClientRect()
+        clientWidth = rect.width
+
+        if style & FNB_NO_X_BUTTON:
+            return clientWidth - 22
+        else:
+            return clientWidth - 38
+
+
+    def GetDropArrowButtonPos(self, pageContainer):
+        """ Returns the drop down button position in the navigation area. """
+
+        return self.GetRightButtonPos(pageContainer)
+
+
+    def GetXPos(self, pageContainer):
+        """ Returns the 'X' button position in the navigation area. """
+
+        pc = pageContainer
+        style = pc.GetParent().GetWindowStyleFlag()
+        rect = pc.GetClientRect()
+        clientWidth = rect.width
+
+        if style & FNB_NO_X_BUTTON:
+            return clientWidth
+        else:
+            return clientWidth - 22
+
+
+    def GetButtonsAreaLength(self, pageContainer):
+        """ Returns the navigation area width. """
+
+        pc = pageContainer
+        style = pc.GetParent().GetWindowStyleFlag()
+
+        # ''
+        if style & FNB_NO_NAV_BUTTONS and style & FNB_NO_X_BUTTON and not style & FNB_DROPDOWN_TABS_LIST:
+            return 0
+
+        # 'x'
+        elif style & FNB_NO_NAV_BUTTONS and not style & FNB_NO_X_BUTTON and not style & FNB_DROPDOWN_TABS_LIST:
+            return 22
+
+        # '<>'
+        if not style & FNB_NO_NAV_BUTTONS and style & FNB_NO_X_BUTTON and not style & FNB_DROPDOWN_TABS_LIST:
+            return 53 - 16
+
+        # 'vx'
+        if style & FNB_DROPDOWN_TABS_LIST and not style & FNB_NO_X_BUTTON:
+            return 22 + 16
+
+        # 'v'
+        if style & FNB_DROPDOWN_TABS_LIST and style & FNB_NO_X_BUTTON:
+            return 22
+
+        # '<>x'
+        return 53
+
+
+    def DrawArrowAccordingToState(self, dc, pc, rect):
+
+        lightFactor = (pc.HasFlag(FNB_BACKGROUND_GRADIENT) and [70] or [0])[0]
+        PaintStraightGradientBox(dc, rect, pc._tabAreaColor, LightColour(pc._tabAreaColor, lightFactor))
+
+
+    def DrawLeftArrow(self, pageContainer, dc):
+        """ Draw the left navigation arrow. """
+
+        pc = pageContainer
+
+        style = pc.GetParent().GetWindowStyleFlag()
+        if style & FNB_NO_NAV_BUTTONS:
+            return
+
+        # Make sure that there are pages in the container
+        if not pc._pagesInfoVec:
+            return
+
+        # Set the bitmap according to the button status
+        if pc._nLeftButtonStatus == FNB_BTN_HOVER:
+            arrowBmp = wx.BitmapFromXPMData(left_arrow_hilite_xpm)
+        elif pc._nLeftButtonStatus == FNB_BTN_PRESSED:
+            arrowBmp = wx.BitmapFromXPMData(left_arrow_pressed_xpm)
+        else:
+            arrowBmp = wx.BitmapFromXPMData(left_arrow_xpm)
+
+        if pc._nFrom == 0:
+            # Handle disabled arrow
+            arrowBmp = wx.BitmapFromXPMData(left_arrow_disabled_xpm)
+
+        arrowBmp.SetMask(wx.Mask(arrowBmp, MASK_COLOR))
+
+        # Erase old bitmap
+        posx = self.GetLeftButtonPos(pc)
+        self.DrawArrowAccordingToState(dc, pc, wx.Rect(posx, 6, 16, 14))
+
+        # Draw the new bitmap
+        dc.DrawBitmap(arrowBmp, posx, 6, True)
+
+
+    def DrawRightArrow(self, pageContainer, dc):
+        """ Draw the right navigation arrow. """
+
+        pc = pageContainer
+
+        style = pc.GetParent().GetWindowStyleFlag()
+        if style & FNB_NO_NAV_BUTTONS:
+            return
+
+        # Make sure that there are pages in the container
+        if not pc._pagesInfoVec:
+            return
+
+        # Set the bitmap according to the button status
+        if pc._nRightButtonStatus == FNB_BTN_HOVER:
+            arrowBmp = wx.BitmapFromXPMData(right_arrow_hilite_xpm)
+        elif pc._nRightButtonStatus == FNB_BTN_PRESSED:
+            arrowBmp = wx.BitmapFromXPMData(right_arrow_pressed_xpm)
+        else:
+            arrowBmp = wx.BitmapFromXPMData(right_arrow_xpm)
+
+        # Check if the right most tab is visible, if it is
+        # don't rotate right anymore
+        if pc._pagesInfoVec[-1].GetPosition() != wx.Point(-1, -1):
+            arrowBmp = wx.BitmapFromXPMData(right_arrow_disabled_xpm)
+
+        arrowBmp.SetMask(wx.Mask(arrowBmp, MASK_COLOR))
+
+        # erase old bitmap
+        posx = self.GetRightButtonPos(pc)
+        self.DrawArrowAccordingToState(dc, pc, wx.Rect(posx, 6, 16, 14))
+
+        # Draw the new bitmap
+        dc.DrawBitmap(arrowBmp, posx, 6, True)
+
+
+    def DrawDropDownArrow(self, pageContainer, dc):
+        """ Draws the drop-down arrow in the navigation area. """
+
+        pc = pageContainer
+
+        # Check if this style is enabled
+        style = pc.GetParent().GetWindowStyleFlag()
+        if not style & FNB_DROPDOWN_TABS_LIST:
+            return
+
+        # Make sure that there are pages in the container
+        if not pc._pagesInfoVec:
+            return
+
+        if pc._nArrowDownButtonStatus == FNB_BTN_HOVER:
+            downBmp = wx.BitmapFromXPMData(down_arrow_hilite_xpm)
+        elif pc._nArrowDownButtonStatus == FNB_BTN_PRESSED:
+            downBmp = wx.BitmapFromXPMData(down_arrow_pressed_xpm)
+        else:
+            downBmp = wx.BitmapFromXPMData(down_arrow_xpm)
+
+        downBmp.SetMask(wx.Mask(downBmp, MASK_COLOR))
+
+        # erase old bitmap
+        posx = self.GetDropArrowButtonPos(pc)
+        self.DrawArrowAccordingToState(dc, pc, wx.Rect(posx, 6, 16, 14))
+
+        # Draw the new bitmap
+        dc.DrawBitmap(downBmp, posx, 6, True)
+
+
+    def DrawX(self, pageContainer, dc):
+        """ Draw the 'X' navigation button in the navigation area. """
+
+        pc = pageContainer
+
+        # Check if this style is enabled
+        style = pc.GetParent().GetWindowStyleFlag()
+        if style & FNB_NO_X_BUTTON:
+            return
+
+        # Make sure that there are pages in the container
+        if not pc._pagesInfoVec:
+            return
+
+        # Set the bitmap according to the button status
+        if pc._nXButtonStatus == FNB_BTN_HOVER:
+            xbmp = wx.BitmapFromXPMData(x_button_hilite_xpm)
+        elif pc._nXButtonStatus == FNB_BTN_PRESSED:
+            xbmp = wx.BitmapFromXPMData(x_button_pressed_xpm)
+        else:
+            xbmp = wx.BitmapFromXPMData(x_button_xpm)
+
+        xbmp.SetMask(wx.Mask(xbmp, MASK_COLOR))
+
+        # erase old bitmap
+        posx = self.GetXPos(pc)
+        self.DrawArrowAccordingToState(dc, pc, wx.Rect(posx, 6, 16, 14))
+
+        # Draw the new bitmap
+        dc.DrawBitmap(xbmp, posx, 6, True)
+
+
+    def DrawTabX(self, pageContainer, dc, rect, tabIdx, btnStatus):
+        """ Draws the 'X' in the selected tab. """
+
+        pc = pageContainer
+        if not pc.HasFlag(FNB_X_ON_TAB):
+            return
+
+        # We draw the 'x' on the active tab only
+        if tabIdx != pc.GetSelection() or tabIdx < 0:
+            return
+
+        # Set the bitmap according to the button status
+
+        if btnStatus == FNB_BTN_HOVER:
+            xBmp = wx.BitmapFromXPMData(x_button_hilite_xpm)
+        elif btnStatus == FNB_BTN_PRESSED:
+            xBmp = wx.BitmapFromXPMData(x_button_pressed_xpm)
+        else:
+            xBmp = wx.BitmapFromXPMData(x_button_xpm)
+
+        # Set the masking
+        xBmp.SetMask(wx.Mask(xBmp, MASK_COLOR))
+
+        # Draw the new bitmap
+        dc.DrawBitmap(xBmp, rect.x, rect.y, True)
+
+        # Update the vector
+        rr = wx.Rect(rect.x, rect.y, 14, 13)
+        pc._pagesInfoVec[tabIdx].SetXRect(rr)
+
+
+    def DrawTabsLine(self, pageContainer, dc, selTabX1=-1, selTabX2=-1):
+        """ Draws a line over the tabs. """
+
+        pc = pageContainer
+
+        clntRect = pc.GetClientRect()
+        clientRect3 = wx.Rect(0, 0, clntRect.width, clntRect.height)
+
+        if pc.HasFlag(FNB_FF2):
+            if not pc.HasFlag(FNB_BOTTOM):
+                fillColor = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)
+            else:
+                fillColor = wx.WHITE
+
+            dc.SetPen(wx.Pen(fillColor))
+
+            if pc.HasFlag(FNB_BOTTOM):
+
+                dc.DrawLine(1, 0, clntRect.width-1, 0)
+                dc.DrawLine(1, 1, clntRect.width-1, 1)
+
+                dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)))
+                dc.DrawLine(1, 2, clntRect.width-1, 2)
+
+                dc.SetPen(wx.Pen(fillColor))
+                dc.DrawLine(selTabX1 + 2, 2, selTabX2 - 1, 2)
+
+            else:
+
+                dc.DrawLine(1, clntRect.height, clntRect.width-1, clntRect.height)
+                dc.DrawLine(1, clntRect.height-1, clntRect.width-1, clntRect.height-1)
+
+                dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)))
+                dc.DrawLine(1, clntRect.height-2, clntRect.width-1, clntRect.height-2)
+
+                dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)))
+                dc.DrawLine(selTabX1 + 2, clntRect.height-2, selTabX2-1, clntRect.height-2)
+
+        else:
+
+            if pc.HasFlag(FNB_BOTTOM):
+
+                clientRect = wx.Rect(0, 2, clntRect.width, clntRect.height - 2)
+                clientRect2 = wx.Rect(0, 1, clntRect.width, clntRect.height - 1)
+
+            else:
+
+                clientRect = wx.Rect(0, 0, clntRect.width, clntRect.height - 2)
+                clientRect2 = wx.Rect(0, 0, clntRect.width, clntRect.height - 1)
+
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+            dc.SetPen(wx.Pen(pc.GetSingleLineBorderColour()))
+            dc.DrawRectangleRect(clientRect2)
+            dc.DrawRectangleRect(clientRect3)
+
+            dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)))
+            dc.DrawRectangleRect(clientRect)
+
+            if not pc.HasFlag(FNB_TABS_BORDER_SIMPLE):
+
+                dc.SetPen(wx.Pen((pc.HasFlag(FNB_VC71) and [wx.Colour(247, 243, 233)] or [pc._tabAreaColor])[0]))
+                dc.DrawLine(0, 0, 0, clientRect.height+1)
+
+                if pc.HasFlag(FNB_BOTTOM):
+
+                    dc.DrawLine(0, clientRect.height+1, clientRect.width, clientRect.height+1)
+
+                else:
+
+                    dc.DrawLine(0, 0, clientRect.width, 0)
+
+                dc.DrawLine(clientRect.width - 1, 0, clientRect.width - 1, clientRect.height+1)
+
+
+    def CalcTabWidth(self, pageContainer, tabIdx, tabHeight):
+        """ Calculates the width of the input tab. """
+
+        pc = pageContainer
+        dc = wx.MemoryDC()
+        dc.SelectObject(wx.EmptyBitmap(1,1))
+
+        boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        boldFont.SetWeight(wx.FONTWEIGHT_BOLD)
+
+        if pc.IsDefaultTabs():
+            shapePoints = int(tabHeight*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi))
+
+        # Calculate the text length using the bold font, so when selecting a tab
+        # its width will not change
+        dc.SetFont(boldFont)
+        width, pom = dc.GetTextExtent(pc.GetPageText(tabIdx))
+
+        # Set a minimum size to a tab
+        if width < 20:
+            width = 20
+
+        tabWidth = 2*pc._pParent.GetPadding() + width
+
+        # Style to add a small 'x' button on the top right
+        # of the tab
+        if pc.HasFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection():
+            # The xpm image that contains the 'x' button is 9 pixels
+            spacer = 9
+            if pc.HasFlag(FNB_VC8):
+                spacer = 4
+
+            tabWidth += pc._pParent.GetPadding() + spacer
+
+        if pc.IsDefaultTabs():
+            # Default style
+            tabWidth += 2*shapePoints
+
+        hasImage = pc._ImageList != None and pc._pagesInfoVec[tabIdx].GetImageIndex() != -1
+
+        # For VC71 style, we only add the icon size (16 pixels)
+        if hasImage:
+
+            if not pc.IsDefaultTabs():
+                tabWidth += 16 + pc._pParent.GetPadding()
+            else:
+                # Default style
+                tabWidth += 16 + pc._pParent.GetPadding() + shapePoints/2
+
+        return tabWidth
+
+
+    def CalcTabHeight(self, pageContainer):
+        """ Calculates the height of the input tab. """
+
+        if self._tabHeight:
+            return self._tabHeight
+
+        pc = pageContainer
+        dc = wx.MemoryDC()
+        dc.SelectObject(wx.EmptyBitmap(1,1))
+
+        # For GTK it seems that we must do this steps in order
+        # for the tabs will get the proper height on initialization
+        # on MSW, preforming these steps yields wierd results
+        normalFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        boldFont = normalFont
+
+        if "__WXGTK__" in wx.PlatformInfo:
+            boldFont.SetWeight(wx.FONTWEIGHT_BOLD)
+            dc.SetFont(boldFont)
+
+        height = dc.GetCharHeight()
+
+        tabHeight = height + FNB_HEIGHT_SPACER # We use 8 pixels as padding
+        if "__WXGTK__" in wx.PlatformInfo:
+            # On GTK the tabs are should be larger
+            tabHeight += 6
+
+        self._tabHeight = tabHeight
+
+        return tabHeight
+
+
+    def DrawTabs(self, pageContainer, dc):
+        """ Actually draws the tabs in L{FlatNotebook}."""
+
+        pc = pageContainer
+        if "__WXMAC__" in wx.PlatformInfo:
+            # Works well on MSW & GTK, however this lines should be skipped on MAC
+            if not pc._pagesInfoVec or pc._nFrom >= len(pc._pagesInfoVec):
+                pc.Hide()
+                return
+
+        # Get the text hight
+        tabHeight = self.CalcTabHeight(pageContainer)
+        style = pc.GetParent().GetWindowStyleFlag()
+
+        # Calculate the number of rows required for drawing the tabs
+        rect = pc.GetClientRect()
+        clientWidth = rect.width
+
+        # Set the maximum client size
+        pc.SetSizeHints(self.GetButtonsAreaLength(pc), tabHeight)
+        borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))
+
+        if style & FNB_VC71:
+            backBrush = wx.Brush(wx.Colour(247, 243, 233))
+        else:
+            backBrush = wx.Brush(pc._tabAreaColor)
+
+        noselBrush = wx.Brush(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE))
+        selBrush = wx.Brush(pc._activeTabColor)
+
+        size = pc.GetSize()
+
+        # Background
+        dc.SetTextBackground((style & FNB_VC71 and [wx.Colour(247, 243, 233)] or [pc.GetBackgroundColour()])[0])
+        dc.SetTextForeground(pc._activeTextColor)
+        dc.SetBrush(backBrush)
+
+        # If border style is set, set the pen to be border pen
+        if pc.HasFlag(FNB_TABS_BORDER_SIMPLE):
+            dc.SetPen(borderPen)
+        else:
+            colr = (pc.HasFlag(FNB_VC71) and [wx.Colour(247, 243, 233)] or [pc.GetBackgroundColour()])[0]
+            dc.SetPen(wx.Pen(colr))
+
+        if pc.HasFlag(FNB_FF2):
+            lightFactor = (pc.HasFlag(FNB_BACKGROUND_GRADIENT) and [70] or [0])[0]
+            PaintStraightGradientBox(dc, pc.GetClientRect(), pc._tabAreaColor, LightColour(pc._tabAreaColor, lightFactor))
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        dc.DrawRectangle(0, 0, size.x, size.y)
+
+        # We always draw the bottom/upper line of the tabs
+        # regradless the style
+        dc.SetPen(borderPen)
+
+        if not pc.HasFlag(FNB_FF2):
+            self.DrawTabsLine(pc, dc)
+
+        # Restore the pen
+        dc.SetPen(borderPen)
+
+        if pc.HasFlag(FNB_VC71):
+
+            greyLineYVal  = (pc.HasFlag(FNB_BOTTOM) and [0] or [size.y - 2])[0]
+            whiteLineYVal = (pc.HasFlag(FNB_BOTTOM) and [3] or [size.y - 3])[0]
+
+            pen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))
+            dc.SetPen(pen)
+
+            # Draw thik grey line between the windows area and
+            # the tab area
+            for num in xrange(3):
+                dc.DrawLine(0, greyLineYVal + num, size.x, greyLineYVal + num)
+
+            wbPen = (pc.HasFlag(FNB_BOTTOM) and [wx.BLACK_PEN] or [wx.WHITE_PEN])[0]
+            dc.SetPen(wbPen)
+            dc.DrawLine(1, whiteLineYVal, size.x - 1, whiteLineYVal)
+
+            # Restore the pen
+            dc.SetPen(borderPen)
+
+        # Draw labels
+        normalFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        boldFont.SetWeight(wx.FONTWEIGHT_BOLD)
+        dc.SetFont(boldFont)
+
+        posx = pc._pParent.GetPadding()
+
+        # Update all the tabs from 0 to 'pc._nFrom' to be non visible
+        for i in xrange(pc._nFrom):
+
+            pc._pagesInfoVec[i].SetPosition(wx.Point(-1, -1))
+            pc._pagesInfoVec[i].GetRegion().Clear()
+
+        count = pc._nFrom
+
+        #----------------------------------------------------------
+        # Go over and draw the visible tabs
+        #----------------------------------------------------------
+        x1 = x2 = -1
+        for i in xrange(pc._nFrom, len(pc._pagesInfoVec)):
+
+            dc.SetPen(borderPen)
+
+            if not pc.HasFlag(FNB_FF2):
+                dc.SetBrush((i==pc.GetSelection() and [selBrush] or [noselBrush])[0])
+
+            # Now set the font to the correct font
+            dc.SetFont((i==pc.GetSelection() and [boldFont] or [normalFont])[0])
+
+            # Add the padding to the tab width
+            # Tab width:
+            # +-----------------------------------------------------------+
+            # | PADDING | IMG | IMG_PADDING | TEXT | PADDING | x |PADDING |
+            # +-----------------------------------------------------------+
+            tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight)
+
+            # Check if we can draw more
+            if posx + tabWidth + self.GetButtonsAreaLength(pc) >= clientWidth:
+                break
+
+            count = count + 1
+
+            # By default we clean the tab region
+            pc._pagesInfoVec[i].GetRegion().Clear()
+
+            # Clean the 'x' buttn on the tab.
+            # A 'Clean' rectangle, is a rectangle with width or height
+            # with values lower than or equal to 0
+            pc._pagesInfoVec[i].GetXRect().SetSize(wx.Size(-1, -1))
+
+            # Draw the tab (border, text, image & 'x' on tab)
+            self.DrawTab(pc, dc, posx, i, tabWidth, tabHeight, pc._nTabXButtonStatus)
+
+            if pc.GetSelection() == i:
+                x1 = posx
+                x2 = posx + tabWidth + 2
+
+            # Restore the text forground
+            dc.SetTextForeground(pc._activeTextColor)
+
+            # Update the tab position & size
+            posy = (pc.HasFlag(FNB_BOTTOM) and [0] or [VERTICAL_BORDER_PADDING])[0]
+
+            pc._pagesInfoVec[i].SetPosition(wx.Point(posx, posy))
+            pc._pagesInfoVec[i].SetSize(wx.Size(tabWidth, tabHeight))
+            self.DrawFocusRectangle(dc, pc, pc._pagesInfoVec[i])
+
+            posx += tabWidth
+
+        # Update all tabs that can not fit into the screen as non-visible
+        for i in xrange(count, len(pc._pagesInfoVec)):
+            pc._pagesInfoVec[i].SetPosition(wx.Point(-1, -1))
+            pc._pagesInfoVec[i].GetRegion().Clear()
+
+        # Draw the left/right/close buttons
+        # Left arrow
+        self.DrawLeftArrow(pc, dc)
+        self.DrawRightArrow(pc, dc)
+        self.DrawX(pc, dc)
+        self.DrawDropDownArrow(pc, dc)
+
+        if pc.HasFlag(FNB_FF2):
+            self.DrawTabsLine(pc, dc, x1, x2)
+
+
+    def DrawFocusRectangle(self, dc, pageContainer, page):
+        """ Draws a focus rectangle like the native Notebooks. """
+
+        if not page._hasFocus:
+            return
+
+        tabPos = wx.Point(*page.GetPosition())
+        if pageContainer.GetParent().GetWindowStyleFlag() & FNB_VC8:
+            vc8ShapeLen = self.CalcTabHeight(pageContainer) - VERTICAL_BORDER_PADDING - 2
+            tabPos.x += vc8ShapeLen
+
+        rect = wx.RectPS(tabPos, page.GetSize())
+        rect = wx.Rect(rect.x+2, rect.y+2, rect.width-4, rect.height-8)
+
+        if wx.Platform == '__WXMAC__':
+            rect.SetWidth(rect.GetWidth() + 1)
+
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        dc.SetPen(self._focusPen)
+        dc.DrawRoundedRectangleRect(rect, 2)
+
+
+    def DrawDragHint(self, pc, tabIdx):
+        """
+        Draws tab drag hint, the default implementation is to do nothing.
+        You can override this function to provide a nice feedback to user.
+        """
+
+        pass
+
+
+    def NumberTabsCanFit(self, pageContainer, fr=-1):
+
+        pc = pageContainer
+
+        rect = pc.GetClientRect()
+        clientWidth = rect.width
+
+        vTabInfo = []
+
+        tabHeight = self.CalcTabHeight(pageContainer)
+
+        # The drawing starts from posx
+        posx = pc._pParent.GetPadding()
+
+        if fr < 0:
+            fr = pc._nFrom
+
+        for i in xrange(fr, len(pc._pagesInfoVec)):
+
+            tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight)
+            if posx + tabWidth + self.GetButtonsAreaLength(pc) >= clientWidth:
+                break;
+
+            # Add a result to the returned vector
+            tabRect = wx.Rect(posx, VERTICAL_BORDER_PADDING, tabWidth , tabHeight)
+            vTabInfo.append(tabRect)
+
+            # Advance posx
+            posx += tabWidth + FNB_HEIGHT_SPACER
+
+        return vTabInfo
+
+
+# ---------------------------------------------------------------------------- #
+# Class FNBRendererMgr
+# A manager that handles all the renderers defined below and calls the
+# appropriate one when drawing is needed
+# ---------------------------------------------------------------------------- #
+
+class FNBRendererMgr:
+    """
+    This class represents a manager that handles all the 4 renderers defined
+    and calls the appropriate one when drawing is needed.
+    """
+
+    def __init__(self):
+        """ Default class constructor. """
+
+        # register renderers
+
+        self._renderers = {}
+        self._renderers.update({-1: FNBRendererDefault()})
+        self._renderers.update({FNB_VC71: FNBRendererVC71()})
+        self._renderers.update({FNB_FANCY_TABS: FNBRendererFancy()})
+        self._renderers.update({FNB_VC8: FNBRendererVC8()})
+        self._renderers.update({FNB_FF2: FNBRendererFirefox2()})
+
+
+    def GetRenderer(self, style):
+        """ Returns the current renderer based on the style selected. """
+
+        if style & FNB_VC71:
+            return self._renderers[FNB_VC71]
+
+        if style & FNB_FANCY_TABS:
+            return self._renderers[FNB_FANCY_TABS]
+
+        if style & FNB_VC8:
+            return self._renderers[FNB_VC8]
+
+        if style & FNB_FF2:
+            return self._renderers[FNB_FF2]
+
+        # the default is to return the default renderer
+        return self._renderers[-1]
+
+
+#------------------------------------------
+# Default renderer
+#------------------------------------------
+
+class FNBRendererDefault(FNBRenderer):
+    """
+    This class handles the drawing of tabs using the I{Standard} renderer.
+    """
+
+    def __init__(self):
+        """ Default class constructor. """
+
+        FNBRenderer.__init__(self)
+
+
+    def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus):
+        """ Draws a tab using the I{Standard} style. """
+
+        # Default style
+        borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))
+        pc = pageContainer
+
+        tabPoints = [wx.Point() for ii in xrange(7)]
+        tabPoints[0].x = posx
+        tabPoints[0].y = (pc.HasFlag(FNB_BOTTOM) and [2] or [tabHeight - 2])[0]
+
+        tabPoints[1].x = int(posx+(tabHeight-2)*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi))
+        tabPoints[1].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0]
+
+        tabPoints[2].x = tabPoints[1].x+2
+        tabPoints[2].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0]
+
+        tabPoints[3].x = int(posx+tabWidth-(tabHeight-2)*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi))-2
+        tabPoints[3].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0]
+
+        tabPoints[4].x = tabPoints[3].x+2
+        tabPoints[4].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0]
+
+        tabPoints[5].x = int(tabPoints[4].x+(tabHeight-2)*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi))
+        tabPoints[5].y = (pc.HasFlag(FNB_BOTTOM) and [2] or [tabHeight - 2])[0]
+
+        tabPoints[6].x = tabPoints[0].x
+        tabPoints[6].y = tabPoints[0].y
+
+        if tabIdx == pc.GetSelection():
+
+            # Draw the tab as rounded rectangle
+            dc.DrawPolygon(tabPoints)
+
+        else:
+
+            if tabIdx != pc.GetSelection() - 1:
+
+                # Draw a vertical line to the right of the text
+                pt1x = tabPoints[5].x
+                pt1y = (pc.HasFlag(FNB_BOTTOM) and [4] or [tabHeight - 6])[0]
+                pt2x = tabPoints[5].x
+                pt2y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - 4] or [4])[0]
+                dc.DrawLine(pt1x, pt1y, pt2x, pt2y)
+
+        if tabIdx == pc.GetSelection():
+
+            savePen = dc.GetPen()
+            whitePen = wx.Pen(wx.WHITE)
+            whitePen.SetWidth(1)
+            dc.SetPen(whitePen)
+
+            secPt = wx.Point(tabPoints[5].x + 1, tabPoints[5].y)
+            dc.DrawLine(tabPoints[0].x, tabPoints[0].y, secPt.x, secPt.y)
+
+            # Restore the pen
+            dc.SetPen(savePen)
+
+        # -----------------------------------
+        # Text and image drawing
+        # -----------------------------------
+
+        # Text drawing offset from the left border of the
+        # rectangle
+
+        # The width of the images are 16 pixels
+        padding = pc.GetParent().GetPadding()
+        shapePoints = int(tabHeight*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi))
+        hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1
+        imageYCoord = (pc.HasFlag(FNB_BOTTOM) and [6] or [8])[0]
+
+        if hasImage:
+            textOffset = 2*pc._pParent._nPadding + 16 + shapePoints/2
+        else:
+            textOffset = pc._pParent._nPadding + shapePoints/2
+
+        textOffset += 2
+
+        if tabIdx != pc.GetSelection():
+
+            # Set the text background to be like the vertical lines
+            dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour())
+
+        if hasImage:
+
+            imageXOffset = textOffset - 16 - padding
+            pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc,
+                                     posx + imageXOffset, imageYCoord,
+                                     wx.IMAGELIST_DRAW_TRANSPARENT, True)
+
+        dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord)
+
+        # draw 'x' on tab (if enabled)
+        if pc.HasFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection():
+
+            textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx))
+            tabCloseButtonXCoord = posx + textOffset + textWidth + 1
+
+            # take a bitmap from the position of the 'x' button (the x on tab button)
+            # this bitmap will be used later to delete old buttons
+            tabCloseButtonYCoord = imageYCoord
+            x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16)
+
+            # Draw the tab
+            self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus)
+
+
+#------------------------------------------
+# Firefox2 renderer
+#------------------------------------------
+class FNBRendererFirefox2(FNBRenderer):
+    """
+    This class handles the drawing of tabs using the I{Firefox 2} renderer.
+    """
+
+    def __init__(self):
+        """ Default class constructor. """
+
+        FNBRenderer.__init__(self)
+
+
+    def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus):
+        """ Draws a tab using the I{Firefox 2} style. """
+
+        borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))
+        pc = pageContainer
+
+        tabPoints = [wx.Point() for indx in xrange(7)]
+        tabPoints[0].x = posx + 2
+        tabPoints[0].y = (pc.HasFlag(FNB_BOTTOM) and [2] or [tabHeight - 2])[0]
+
+        tabPoints[1].x = tabPoints[0].x
+        tabPoints[1].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0]
+
+        tabPoints[2].x = tabPoints[1].x+2
+        tabPoints[2].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0]
+
+        tabPoints[3].x = posx + tabWidth - 2
+        tabPoints[3].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0]
+
+        tabPoints[4].x = tabPoints[3].x + 2
+        tabPoints[4].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0]
+
+        tabPoints[5].x = tabPoints[4].x
+        tabPoints[5].y = (pc.HasFlag(FNB_BOTTOM) and [2] or [tabHeight - 2])[0]
+
+        tabPoints[6].x = tabPoints[0].x
+        tabPoints[6].y = tabPoints[0].y
+
+        #------------------------------------
+        # Paint the tab with gradient
+        #------------------------------------
+        rr = wx.RectPP(tabPoints[2], tabPoints[5])
+        DrawButton(dc, rr, pc.GetSelection() == tabIdx , not pc.HasFlag(FNB_BOTTOM))
+
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        dc.SetPen(borderPen)
+
+        # Draw the tab as rounded rectangle
+        dc.DrawPolygon(tabPoints)
+
+        # -----------------------------------
+        # Text and image drawing
+        # -----------------------------------
+
+        # The width of the images are 16 pixels
+        padding = pc.GetParent().GetPadding()
+        shapePoints = int(tabHeight*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi))
+        hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1
+        imageYCoord = (pc.HasFlag(FNB_BOTTOM) and [6] or [8])[0]
+
+        if hasImage:
+            textOffset = 2*padding + 16 + shapePoints/2
+        else:
+            textOffset = padding + shapePoints/2
+
+        textOffset += 2
+
+        if tabIdx != pc.GetSelection():
+
+            # Set the text background to be like the vertical lines
+            dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour())
+
+        if hasImage:
+            imageXOffset = textOffset - 16 - padding
+            pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc,
+                               posx + imageXOffset, imageYCoord,
+                               wx.IMAGELIST_DRAW_TRANSPARENT, True)
+
+        dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord)
+
+        # draw 'x' on tab (if enabled)
+        if pc.HasFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection():
+
+            textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx))
+            tabCloseButtonXCoord = posx + textOffset + textWidth + 1
+
+            # take a bitmap from the position of the 'x' button (the x on tab button)
+            # this bitmap will be used later to delete old buttons
+            tabCloseButtonYCoord = imageYCoord
+            x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16)
+
+            # Draw the tab
+            self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus)
+
+
+#------------------------------------------------------------------
+# Visual studio 7.1
+#------------------------------------------------------------------
+
+class FNBRendererVC71(FNBRenderer):
+    """
+    This class handles the drawing of tabs using the I{VC71} renderer.
+    """
+
+    def __init__(self):
+        """ Default class constructor. """
+
+        FNBRenderer.__init__(self)
+
+
+    def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus):
+        """ Draws a tab using the I{VC71} style. """
+
+        # Visual studio 7.1 style
+        borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))
+        pc = pageContainer
+
+        dc.SetPen((tabIdx == pc.GetSelection() and [wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))] or [borderPen])[0])
+        dc.SetBrush((tabIdx == pc.GetSelection() and [wx.Brush(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))] or [wx.Brush(wx.Colour(247, 243, 233))])[0])
+
+        if tabIdx == pc.GetSelection():
+
+            posy = (pc.HasFlag(FNB_BOTTOM) and [0] or [VERTICAL_BORDER_PADDING])[0]
+            tabH = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - 5] or [tabHeight - 3])[0]
+            dc.DrawRectangle(posx, posy, tabWidth, tabH)
+
+            # Draw a black line on the left side of the
+            # rectangle
+            dc.SetPen(wx.BLACK_PEN)
+
+            blackLineY1 = VERTICAL_BORDER_PADDING
+            blackLineY2 = tabH
+            dc.DrawLine(posx + tabWidth, blackLineY1, posx + tabWidth, blackLineY2)
+
+            # To give the tab more 3D look we do the following
+            # Incase the tab is on top,
+            # Draw a thik white line on topof the rectangle
+            # Otherwise, draw a thin (1 pixel) black line at the bottom
+
+            pen = wx.Pen((pc.HasFlag(FNB_BOTTOM) and [wx.BLACK] or [wx.WHITE])[0])
+            dc.SetPen(pen)
+            whiteLinePosY = (pc.HasFlag(FNB_BOTTOM) and [blackLineY2] or [VERTICAL_BORDER_PADDING ])[0]
+            dc.DrawLine(posx , whiteLinePosY, posx + tabWidth + 1, whiteLinePosY)
+
+            # Draw a white vertical line to the left of the tab
+            dc.SetPen(wx.WHITE_PEN)
+            if not pc.HasFlag(FNB_BOTTOM):
+                blackLineY2 += 1
+
+            dc.DrawLine(posx, blackLineY1, posx, blackLineY2)
+
+        else:
+
+            # We dont draw a rectangle for non selected tabs, but only
+            # vertical line on the left
+
+            blackLineY1 = (pc.HasFlag(FNB_BOTTOM) and [VERTICAL_BORDER_PADDING + 2] or [VERTICAL_BORDER_PADDING + 1])[0]
+            blackLineY2 = pc.GetSize().y - 5
+            dc.DrawLine(posx + tabWidth, blackLineY1, posx + tabWidth, blackLineY2)
+
+        # -----------------------------------
+        # Text and image drawing
+        # -----------------------------------
+
+        # Text drawing offset from the left border of the
+        # rectangle
+
+        # The width of the images are 16 pixels
+        padding = pc.GetParent().GetPadding()
+        hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1
+        imageYCoord = (pc.HasFlag(FNB_BOTTOM) and [5] or [8])[0]
+
+        if hasImage:
+            textOffset = 2*pc._pParent._nPadding + 16
+        else:
+            textOffset = pc._pParent._nPadding
+
+        if tabIdx != pc.GetSelection():
+
+            # Set the text background to be like the vertical lines
+            dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour())
+
+        if hasImage:
+
+            imageXOffset = textOffset - 16 - padding
+            pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc,
+                                     posx + imageXOffset, imageYCoord,
+                                     wx.IMAGELIST_DRAW_TRANSPARENT, True)
+
+        dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord)
+
+        # draw 'x' on tab (if enabled)
+        if pc.HasFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection():
+
+            textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx))
+            tabCloseButtonXCoord = posx + textOffset + textWidth + 1
+
+            # take a bitmap from the position of the 'x' button (the x on tab button)
+            # this bitmap will be used later to delete old buttons
+            tabCloseButtonYCoord = imageYCoord
+            x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16)
+
+            # Draw the tab
+            self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus)
+
+
+#------------------------------------------------------------------
+# Fancy style
+#------------------------------------------------------------------
+
+class FNBRendererFancy(FNBRenderer):
+    """
+    This class handles the drawing of tabs using the I{Fancy} renderer.
+    """
+
+    def __init__(self):
+        """ Default class constructor. """
+
+        FNBRenderer.__init__(self)
+
+
+    def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus):
+        """ Draws a tab using the I{Fancy} style, similar to VC71 but with gradients. """
+
+        # Fancy tabs - like with VC71 but with the following differences:
+        # - The Selected tab is colored with gradient color
+        borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))
+        pc = pageContainer
+
+        pen = (tabIdx == pc.GetSelection() and [wx.Pen(pc._pParent.GetBorderColour())] or [wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))])[0]
+
+        if tabIdx == pc.GetSelection():
+
+            posy = (pc.HasFlag(FNB_BOTTOM) and [2] or [VERTICAL_BORDER_PADDING])[0]
+            th = tabHeight - 5
+
+            rect = wx.Rect(posx, posy, tabWidth, th)
+
+            col2 = (pc.HasFlag(FNB_BOTTOM) and [pc._pParent.GetGradientColourTo()] or [pc._pParent.GetGradientColourFrom()])[0]
+            col1 = (pc.HasFlag(FNB_BOTTOM) and [pc._pParent.GetGradientColourFrom()] or [pc._pParent.GetGradientColourTo()])[0]
+
+            PaintStraightGradientBox(dc, rect, col1, col2)
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+            dc.SetPen(pen)
+            dc.DrawRectangleRect(rect)
+
+            # erase the bottom/top line of the rectangle
+            dc.SetPen(wx.Pen(pc._pParent.GetGradientColourFrom()))
+            if pc.HasFlag(FNB_BOTTOM):
+                dc.DrawLine(rect.x, 2, rect.x + rect.width, 2)
+            else:
+                dc.DrawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width, rect.y + rect.height - 1)
+
+        else:
+
+            # We dont draw a rectangle for non selected tabs, but only
+            # vertical line on the left
+            dc.SetPen(borderPen)
+            dc.DrawLine(posx + tabWidth, VERTICAL_BORDER_PADDING + 3, posx + tabWidth, tabHeight - 4)
+
+
+        # -----------------------------------
+        # Text and image drawing
+        # -----------------------------------
+
+        # Text drawing offset from the left border of the
+        # rectangle
+
+        # The width of the images are 16 pixels
+        padding = pc.GetParent().GetPadding()
+        hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1
+        imageYCoord = (pc.HasFlag(FNB_BOTTOM) and [6] or [8])[0]
+
+        if hasImage:
+            textOffset = 2*pc._pParent._nPadding + 16
+        else:
+            textOffset = pc._pParent._nPadding
+
+        textOffset += 2
+
+        if tabIdx != pc.GetSelection():
+
+            # Set the text background to be like the vertical lines
+            dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour())
+
+        if hasImage:
+
+            imageXOffset = textOffset - 16 - padding
+            pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc,
+                                     posx + imageXOffset, imageYCoord,
+                                     wx.IMAGELIST_DRAW_TRANSPARENT, True)
+
+        dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord)
+
+        # draw 'x' on tab (if enabled)
+        if pc.HasFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection():
+
+            textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx))
+            tabCloseButtonXCoord = posx + textOffset + textWidth + 1
+
+            # take a bitmap from the position of the 'x' button (the x on tab button)
+            # this bitmap will be used later to delete old buttons
+            tabCloseButtonYCoord = imageYCoord
+            x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16)
+
+            # Draw the tab
+            self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus)
+
+
+#------------------------------------------------------------------
+# Visual studio 2005 (VS8)
+#------------------------------------------------------------------
+class FNBRendererVC8(FNBRenderer):
+    """
+    This class handles the drawing of tabs using the I{VC8} renderer.
+    """
+
+    def __init__(self):
+        """ Default class constructor. """
+
+        FNBRenderer.__init__(self)
+        self._first = True
+        self._factor = 1
+
+
+    def DrawTabs(self, pageContainer, dc):
+        """ Draws all the tabs using VC8 style. Overloads The DrawTabs method in parent class. """
+
+        pc = pageContainer
+
+        if "__WXMAC__" in wx.PlatformInfo:
+            # Works well on MSW & GTK, however this lines should be skipped on MAC
+            if not pc._pagesInfoVec or pc._nFrom >= len(pc._pagesInfoVec):
+                pc.Hide()
+                return
+
+        # Get the text hight
+        tabHeight = self.CalcTabHeight(pageContainer)
+
+        # Set the font for measuring the tab height
+        normalFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        boldFont.SetWeight(wx.FONTWEIGHT_BOLD)
+
+        # Calculate the number of rows required for drawing the tabs
+        rect = pc.GetClientRect()
+
+        # Set the maximum client size
+        pc.SetSizeHints(self.GetButtonsAreaLength(pc), tabHeight)
+        borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))
+
+        # Create brushes
+        backBrush = wx.Brush(pc._tabAreaColor)
+        noselBrush = wx.Brush(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE))
+        selBrush = wx.Brush(pc._activeTabColor)
+        size = pc.GetSize()
+
+        # Background
+        dc.SetTextBackground(pc.GetBackgroundColour())
+        dc.SetTextForeground(pc._activeTextColor)
+
+        # If border style is set, set the pen to be border pen
+        if pc.HasFlag(FNB_TABS_BORDER_SIMPLE):
+            dc.SetPen(borderPen)
+        else:
+            dc.SetPen(wx.TRANSPARENT_PEN)
+
+        lightFactor = (pc.HasFlag(FNB_BACKGROUND_GRADIENT) and [70] or [0])[0]
+
+        # For VC8 style, we color the tab area in gradient coloring
+        lightcolour = LightColour(pc._tabAreaColor, lightFactor)
+        PaintStraightGradientBox(dc, pc.GetClientRect(), pc._tabAreaColor, lightcolour)
+
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        dc.DrawRectangle(0, 0, size.x, size.y)
+
+        # We always draw the bottom/upper line of the tabs
+        # regradless the style
+        dc.SetPen(borderPen)
+        self.DrawTabsLine(pc, dc)
+
+        # Restore the pen
+        dc.SetPen(borderPen)
+
+        # Draw labels
+        dc.SetFont(boldFont)
+
+        # Update all the tabs from 0 to 'pc.self._nFrom' to be non visible
+        for i in xrange(pc._nFrom):
+
+            pc._pagesInfoVec[i].SetPosition(wx.Point(-1, -1))
+            pc._pagesInfoVec[i].GetRegion().Clear()
+
+        # Draw the visible tabs, in VC8 style, we draw them from right to left
+        vTabsInfo = self.NumberTabsCanFit(pc)
+
+        activeTabPosx = 0
+        activeTabWidth = 0
+        activeTabHeight = 0
+
+        for cur in xrange(len(vTabsInfo)-1, -1, -1):
+
+            # 'i' points to the index of the currently drawn tab
+            # in pc.GetPageInfoVector() vector
+            i = pc._nFrom + cur
+            dc.SetPen(borderPen)
+            dc.SetBrush((i==pc.GetSelection() and [selBrush] or [noselBrush])[0])
+
+            # Now set the font to the correct font
+            dc.SetFont((i==pc.GetSelection() and [boldFont] or [normalFont])[0])
+
+            # Add the padding to the tab width
+            # Tab width:
+            # +-----------------------------------------------------------+
+            # | PADDING | IMG | IMG_PADDING | TEXT | PADDING | x |PADDING |
+            # +-----------------------------------------------------------+
+
+            tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight)
+            posx = vTabsInfo[cur].x
+
+            # By default we clean the tab region
+            # incase we use the VC8 style which requires
+            # the region, it will be filled by the function
+            # drawVc8Tab
+            pc._pagesInfoVec[i].GetRegion().Clear()
+
+            # Clean the 'x' buttn on the tab
+            # 'Clean' rectanlge is a rectangle with width or height
+            # with values lower than or equal to 0
+            pc._pagesInfoVec[i].GetXRect().SetSize(wx.Size(-1, -1))
+
+            # Draw the tab
+            # Incase we are drawing the active tab
+            # we need to redraw so it will appear on top
+            # of all other tabs
+
+            # when using the vc8 style, we keep the position of the active tab so we will draw it again later
+            if i == pc.GetSelection() and pc.HasFlag(FNB_VC8):
+
+                activeTabPosx = posx
+                activeTabWidth = tabWidth
+                activeTabHeight = tabHeight
+
+            else:
+
+                self.DrawTab(pc, dc, posx, i, tabWidth, tabHeight, pc._nTabXButtonStatus)
+
+            # Restore the text forground
+            dc.SetTextForeground(pc._activeTextColor)
+
+            # Update the tab position & size
+            pc._pagesInfoVec[i].SetPosition(wx.Point(posx, VERTICAL_BORDER_PADDING))
+            pc._pagesInfoVec[i].SetSize(wx.Size(tabWidth, tabHeight))
+
+        # Incase we are in VC8 style, redraw the active tab (incase it is visible)
+        if pc.GetSelection() >= pc._nFrom and pc.GetSelection() < pc._nFrom + len(vTabsInfo):
+
+            self.DrawTab(pc, dc, activeTabPosx, pc.GetSelection(), activeTabWidth, activeTabHeight, pc._nTabXButtonStatus)
+
+        # Update all tabs that can not fit into the screen as non-visible
+        for xx in xrange(pc._nFrom + len(vTabsInfo), len(pc._pagesInfoVec)):
+
+            pc._pagesInfoVec[xx].SetPosition(wx.Point(-1, -1))
+            pc._pagesInfoVec[xx].GetRegion().Clear()
+
+        # Draw the left/right/close buttons
+        # Left arrow
+        self.DrawLeftArrow(pc, dc)
+        self.DrawRightArrow(pc, dc)
+        self.DrawX(pc, dc)
+        self.DrawDropDownArrow(pc, dc)
+
+
+    def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus):
+        """ Draws a tab using VC8 style. """
+
+        pc = pageContainer
+        borderPen = wx.Pen(pc._pParent.GetBorderColour())
+        tabPoints = [wx.Point() for ii in xrange(8)]
+
+        # If we draw the first tab or the active tab,
+        # we draw a full tab, else we draw a truncated tab
+        #
+        #             X(2)                  X(3)
+        #        X(1)                            X(4)
+        #
+        #                                           X(5)
+        #
+        # X(0),(7)                                  X(6)
+        #
+        #
+
+        tabPoints[0].x = (pc.HasFlag(FNB_BOTTOM) and [posx] or [posx+self._factor])[0]
+        tabPoints[0].y = (pc.HasFlag(FNB_BOTTOM) and [2] or [tabHeight - 3])[0]
+
+        tabPoints[1].x = tabPoints[0].x + tabHeight - VERTICAL_BORDER_PADDING - 3 - self._factor
+        tabPoints[1].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0]
+
+        tabPoints[2].x = tabPoints[1].x + 4
+        tabPoints[2].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0]
+
+        tabPoints[3].x = tabPoints[2].x + tabWidth - 2
+        tabPoints[3].y = (pc.HasFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0]
+
+        tabPoints[4].x = tabPoints[3].x + 1
+        tabPoints[4].y = (pc.HasFlag(FNB_BOTTOM) and [tabPoints[3].y - 1] or [tabPoints[3].y + 1])[0]
+
+        tabPoints[5].x = tabPoints[4].x + 1
+        tabPoints[5].y = (pc.HasFlag(FNB_BOTTOM) and [(tabPoints[4].y - 1)] or [tabPoints[4].y + 1])[0]
+
+        tabPoints[6].x = tabPoints[2].x + tabWidth
+        tabPoints[6].y = tabPoints[0].y
+
+        tabPoints[7].x = tabPoints[0].x
+        tabPoints[7].y = tabPoints[0].y
+
+        pc._pagesInfoVec[tabIdx].SetRegion(tabPoints)
+
+        # Draw the polygon
+        br = dc.GetBrush()
+        dc.SetBrush(wx.Brush((tabIdx == pc.GetSelection() and [pc._activeTabColor] or [pc._colorTo])[0]))
+        dc.SetPen(wx.Pen((tabIdx == pc.GetSelection() and [wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)] or [pc._colorBorder])[0]))
+        dc.DrawPolygon(tabPoints)
+
+        # Restore the brush
+        dc.SetBrush(br)
+        rect = pc.GetClientRect()
+
+        if tabIdx != pc.GetSelection() and not pc.HasFlag(FNB_BOTTOM):
+
+            # Top default tabs
+            dc.SetPen(wx.Pen(pc._pParent.GetBorderColour()))
+            lineY = rect.height
+            curPen = dc.GetPen()
+            curPen.SetWidth(1)
+            dc.SetPen(curPen)
+            dc.DrawLine(posx, lineY, posx+rect.width, lineY)
+
+        # Incase we are drawing the selected tab, we draw the border of it as well
+        # but without the bottom (upper line incase of wxBOTTOM)
+        if tabIdx == pc.GetSelection():
+
+            borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))
+            dc.SetPen(borderPen)
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+            dc.DrawPolygon(tabPoints)
+
+            # Delete the bottom line (or the upper one, incase we use wxBOTTOM)
+            dc.SetPen(wx.WHITE_PEN)
+            dc.DrawLine(tabPoints[0].x, tabPoints[0].y, tabPoints[6].x, tabPoints[6].y)
+
+        self.FillVC8GradientColour(pc, dc, tabPoints, tabIdx == pc.GetSelection(), tabIdx)
+
+        # Draw a thin line to the right of the non-selected tab
+        if tabIdx != pc.GetSelection():
+
+            dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)))
+            dc.DrawLine(tabPoints[4].x-1, tabPoints[4].y, tabPoints[5].x-1, tabPoints[5].y)
+            dc.DrawLine(tabPoints[5].x-1, tabPoints[5].y, tabPoints[6].x-1, tabPoints[6].y)
+
+        # Text drawing offset from the left border of the
+        # rectangle
+
+        # The width of the images are 16 pixels
+        vc8ShapeLen = tabHeight - VERTICAL_BORDER_PADDING - 2
+        if pc.TabHasImage(tabIdx):
+            textOffset = 2*pc._pParent.GetPadding() + 16 + vc8ShapeLen
+        else:
+            textOffset = pc._pParent.GetPadding() + vc8ShapeLen
+
+        # Draw the image for the tab if any
+        imageYCoord = (pc.HasFlag(FNB_BOTTOM) and [6] or [8])[0]
+
+        if pc.TabHasImage(tabIdx):
+
+            imageXOffset = textOffset - 16 - pc._pParent.GetPadding()
+            pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc,
+                                     posx + imageXOffset, imageYCoord,
+                                     wx.IMAGELIST_DRAW_TRANSPARENT, True)
+
+        boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+
+        # if selected tab, draw text in bold
+        if tabIdx == pc.GetSelection():
+            boldFont.SetWeight(wx.FONTWEIGHT_BOLD)
+
+        dc.SetFont(boldFont)
+        dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord)
+
+        # draw 'x' on tab (if enabled)
+        if pc.HasFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection():
+
+            textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx))
+            tabCloseButtonXCoord = posx + textOffset + textWidth + 1
+
+            # take a bitmap from the position of the 'x' button (the x on tab button)
+            # this bitmap will be used later to delete old buttons
+            tabCloseButtonYCoord = imageYCoord
+            x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16)
+            # Draw the tab
+            self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus)
+
+        self.DrawFocusRectangle(dc, pc, pc._pagesInfoVec[tabIdx])
+
+
+    def FillVC8GradientColour(self, pageContainer, dc, tabPoints, bSelectedTab, tabIdx):
+        """ Fills a tab with a gradient shading. """
+
+        # calculate gradient coefficients
+        pc = pageContainer
+
+        if self._first:
+            self._first = False
+            pc._colorTo   = LightColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE), 0)
+            pc._colorFrom = LightColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE), 60)
+
+        col2 = pc._pParent.GetGradientColourTo()
+        col1 = pc._pParent.GetGradientColourFrom()
+
+        # If colorful tabs style is set, override the tab color
+        if pc.HasFlag(FNB_COLORFUL_TABS):
+
+            if not pc._pagesInfoVec[tabIdx].GetColour():
+
+                # First time, generate color, and keep it in the vector
+                tabColor = RandomColour()
+                pc._pagesInfoVec[tabIdx].SetColour(tabColor)
+
+            if pc.HasFlag(FNB_BOTTOM):
+
+                col2 = LightColour(pc._pagesInfoVec[tabIdx].GetColour(), 50)
+                col1 = LightColour(pc._pagesInfoVec[tabIdx].GetColour(), 80)
+
+            else:
+
+                col1 = LightColour(pc._pagesInfoVec[tabIdx].GetColour(), 50)
+                col2 = LightColour(pc._pagesInfoVec[tabIdx].GetColour(), 80)
+
+        size = abs(tabPoints[2].y - tabPoints[0].y) - 1
+
+        rf, gf, bf = 0, 0, 0
+        rstep = float(col2.Red() - col1.Red())/float(size)
+        gstep = float(col2.Green() - col1.Green())/float(size)
+        bstep = float(col2.Blue() - col1.Blue())/float(size)
+
+        y = tabPoints[0].y
+
+        # If we are drawing the selected tab, we need also to draw a line
+        # from 0.tabPoints[0].x and tabPoints[6].x . end, we achieve this
+        # by drawing the rectangle with transparent brush
+        # the line under the selected tab will be deleted by the drwaing loop
+        if bSelectedTab:
+            self.DrawTabsLine(pc, dc)
+
+        while 1:
+
+            if pc.HasFlag(FNB_BOTTOM):
+
+                if y > tabPoints[0].y + size:
+                    break
+
+            else:
+
+                if y < tabPoints[0].y - size:
+                    break
+
+            currCol = wx.Colour(col1.Red() + rf, col1.Green() + gf, col1.Blue() + bf)
+
+            dc.SetPen((bSelectedTab and [wx.Pen(pc._activeTabColor)] or [wx.Pen(currCol)])[0])
+            startX = self.GetStartX(tabPoints, y, pc.GetParent().GetWindowStyleFlag())
+            endX = self.GetEndX(tabPoints, y, pc.GetParent().GetWindowStyleFlag())
+            dc.DrawLine(startX, y, endX, y)
+
+            # Draw the border using the 'edge' point
+            dc.SetPen(wx.Pen((bSelectedTab and [wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)] or [pc._colorBorder])[0]))
+
+            dc.DrawPoint(startX, y)
+            dc.DrawPoint(endX, y)
+
+            # Progress the color
+            rf += rstep
+            gf += gstep
+            bf += bstep
+
+            if pc.HasFlag(FNB_BOTTOM):
+                y = y + 1
+            else:
+                y = y - 1
+
+
+    def GetStartX(self, tabPoints, y, style):
+        """ Returns the x start position of a tab. """
+
+        x1, x2, y1, y2 = 0.0, 0.0, 0.0, 0.0
+
+        # We check the 3 points to the left
+
+        bBottomStyle = (style & FNB_BOTTOM and [True] or [False])[0]
+        match = False
+
+        if bBottomStyle:
+
+            for i in xrange(3):
+
+                if y >= tabPoints[i].y and y < tabPoints[i+1].y:
+
+                    x1 = tabPoints[i].x
+                    x2 = tabPoints[i+1].x
+                    y1 = tabPoints[i].y
+                    y2 = tabPoints[i+1].y
+                    match = True
+                    break
+
+        else:
+
+            for i in xrange(3):
+
+                if y <= tabPoints[i].y and y > tabPoints[i+1].y:
+
+                    x1 = tabPoints[i].x
+                    x2 = tabPoints[i+1].x
+                    y1 = tabPoints[i].y
+                    y2 = tabPoints[i+1].y
+                    match = True
+                    break
+
+        if not match:
+            return tabPoints[2].x
+
+        # According to the equation y = ax + b => x = (y-b)/a
+        # We know the first 2 points
+
+        if x2 == x1:
+            return x2
+        else:
+            a = (y2 - y1)/(x2 - x1)
+
+        b = y1 - ((y2 - y1)/(x2 - x1))*x1
+
+        if a == 0:
+            return int(x1)
+
+        x = (y - b)/a
+
+        return int(x)
+
+
+    def GetEndX(self, tabPoints, y, style):
+        """ Returns the x end position of a tab. """
+
+        x1, x2, y1, y2 = 0.0, 0.0, 0.0, 0.0
+
+        # We check the 3 points to the left
+        bBottomStyle = (style & FNB_BOTTOM and [True] or [False])[0]
+        match = False
+
+        if bBottomStyle:
+
+            for i in xrange(7, 3, -1):
+
+                if y >= tabPoints[i].y and y < tabPoints[i-1].y:
+
+                    x1 = tabPoints[i].x
+                    x2 = tabPoints[i-1].x
+                    y1 = tabPoints[i].y
+                    y2 = tabPoints[i-1].y
+                    match = True
+                    break
+
+        else:
+
+            for i in xrange(7, 3, -1):
+
+                if y <= tabPoints[i].y and y > tabPoints[i-1].y:
+
+                    x1 = tabPoints[i].x
+                    x2 = tabPoints[i-1].x
+                    y1 = tabPoints[i].y
+                    y2 = tabPoints[i-1].y
+                    match = True
+                    break
+
+        if not match:
+            return tabPoints[3].x
+
+        # According to the equation y = ax + b => x = (y-b)/a
+        # We know the first 2 points
+
+        # Vertical line
+        if x1 == x2:
+            return int(x1)
+
+        a = (y2 - y1)/(x2 - x1)
+        b = y1 - ((y2 - y1)/(x2 - x1))*x1
+
+        if a == 0:
+            return int(x1)
+
+        x = (y - b)/a
+
+        return int(x)
+
+
+    def NumberTabsCanFit(self, pageContainer, fr=-1):
+        """ Returns the number of tabs that can fit in the visible area. """
+
+        pc = pageContainer
+
+        rect = pc.GetClientRect()
+        clientWidth = rect.width
+
+        # Empty results
+        vTabInfo = []
+        tabHeight = self.CalcTabHeight(pageContainer)
+
+        # The drawing starts from posx
+        posx = pc._pParent.GetPadding()
+
+        if fr < 0:
+            fr = pc._nFrom
+
+        for i in xrange(fr, len(pc._pagesInfoVec)):
+
+            vc8glitch = tabHeight + FNB_HEIGHT_SPACER
+            tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight)
+
+            if posx + tabWidth + vc8glitch + self.GetButtonsAreaLength(pc) >= clientWidth:
+                break
+
+            # Add a result to the returned vector
+            tabRect = wx.Rect(posx, VERTICAL_BORDER_PADDING, tabWidth, tabHeight)
+            vTabInfo.append(tabRect)
+
+            # Advance posx
+            posx += tabWidth + FNB_HEIGHT_SPACER
+
+        return vTabInfo
+
+
+# ---------------------------------------------------------------------------- #
+# Class FlatNotebook
+# ---------------------------------------------------------------------------- #
+
+class FlatNotebook(wx.PyPanel):
+    """
+    Display one or more windows in a notebook.
+
+    B{Events}:
+        - B{EVT_FLATNOTEBOOK_PAGE_CHANGING}: sent when the active
+            page in the notebook is changing
+        - B{EVT_FLATNOTEBOOK_PAGE_CHANGED}: sent when the active
+            page in the notebook has changed
+        - B{EVT_FLATNOTEBOOK_PAGE_CLOSING}: sent when a page in the
+            notebook is closing
+        - B{EVT_FLATNOTEBOOK_PAGE_CLOSED}: sent when a page in the
+            notebook has been closed
+        - B{EVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU}: sent when the user
+            clicks a tab in the notebook with the right mouse
+            button
+    """
+
+    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
+                 style=0, name="FlatNotebook"):
+        """
+        Default class constructor.
+
+        All the parameters are as in wxPython class construction, except the
+        'style': this can be assigned to whatever combination of FNB_* styles.
+
+        """
+
+        self._bForceSelection = False
+        self._nPadding = 6
+        self._nFrom = 0
+        style |= wx.TAB_TRAVERSAL
+        self._pages = None
+        self._windows = []
+        self._popupWin = None
+        self._naviIcon = None
+
+        wx.PyPanel.__init__(self, parent, id, pos, size, style)
+
+        self._pages = PageContainer(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, style)
+
+        self.Bind(wx.EVT_NAVIGATION_KEY, self.OnNavigationKey)
+
+        self.Init()
+
+
+    def Init(self):
+        """ Initializes all the class attributes. """
+
+        self._pages._colorBorder = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)
+
+        self._mainSizer = wx.BoxSizer(wx.VERTICAL)
+        self.SetSizer(self._mainSizer)
+
+        # The child panels will inherit this bg color, so leave it at the default value
+        #self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_APPWORKSPACE))
+
+        # Set default page height
+        dc = wx.ClientDC(self)
+
+        if "__WXGTK__" in wx.PlatformInfo:
+            # For GTK it seems that we must do this steps in order
+            # for the tabs will get the proper height on initialization
+            # on MSW, preforming these steps yields wierd results
+            boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+            boldFont.SetWeight(wx.FONTWEIGHT_BOLD)
+            dc.SetFont(boldFont)
+
+        height = dc.GetCharHeight()
+
+        tabHeight = height + FNB_HEIGHT_SPACER         # We use 8 pixels as padding
+
+        if "__WXGTK__" in wx.PlatformInfo:
+            tabHeight += 6
+
+        self._pages.SetSizeHints(-1, tabHeight)
+        # Add the tab container to the sizer
+        self._mainSizer.Insert(0, self._pages, 0, wx.EXPAND)
+        self._mainSizer.Layout()
+
+        self._pages._nFrom = self._nFrom
+        self._pDropTarget = FNBDropTarget(self)
+        self.SetDropTarget(self._pDropTarget)
+
+
+    def DoGetBestSize(self):
+        """ Overrides DoGetBestSize to handle sizers nicely. """
+
+        if not self._windows:
+            # Something is better than nothing... no pages!
+            return wx.Size(20, 20)
+
+        maxWidth = maxHeight = 0
+        tabHeight = self.GetPageBestSize().height
+
+        for win in self._windows:
+            # Loop over all the windows to get their best size
+            width, height = win.GetBestSize()
+            maxWidth, maxHeight = max(maxWidth, width), max(maxHeight, height)
+
+        return wx.Size(maxWidth, maxHeight+tabHeight)
+
+
+    def SetActiveTabTextColour(self, textColour):
+        """ Sets the text colour for the active tab. """
+
+        self._pages._activeTextColor = textColour
+
+
+    def OnDropTarget(self, x, y, nTabPage, wnd_oldContainer):
+        """ Handles the drop action from a DND operation. """
+
+        return self._pages.OnDropTarget(x, y, nTabPage, wnd_oldContainer)
+
+
+    def GetPreviousSelection(self):
+        """ Returns the previous selection. """
+
+        return self._pages._iPreviousActivePage
+
+
+    def AddPage(self, page, text, select=True, imageId=-1):
+        """
+        Add a page to the L{FlatNotebook}.
+
+        @param page: Specifies the new page.
+        @param text: Specifies the text for the new page.
+        @param select: Specifies whether the page should be selected.
+        @param imageId: Specifies the optional image index for the new page.
+
+        Return value:
+        True if successful, False otherwise.
+        """
+
+        # sanity check
+        if not page:
+            return False
+
+        # reparent the window to us
+        page.Reparent(self)
+
+        # Add tab
+        bSelected = select or len(self._windows) == 0
+
+        if bSelected:
+
+            bSelected = False
+
+            # Check for selection and send events
+            oldSelection = self._pages._iActivePage
+            tabIdx = len(self._windows)
+
+            event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CHANGING, self.GetId())
+            event.SetSelection(tabIdx)
+            event.SetOldSelection(oldSelection)
+            event.SetEventObject(self)
+
+            if not self.GetEventHandler().ProcessEvent(event) or event.IsAllowed() or len(self._windows) == 0:
+                bSelected = True
+
+        curSel = self._pages.GetSelection()
+
+        if not self._pages.IsShown():
+            self._pages.Show()
+
+        self._pages.AddPage(text, bSelected, imageId)
+        self._windows.append(page)
+
+        self.Freeze()
+
+        # Check if a new selection was made
+        if bSelected:
+
+            if curSel >= 0:
+
+                # Remove the window from the main sizer
+                self._mainSizer.Detach(self._windows[curSel])
+                self._windows[curSel].Hide()
+
+            if self.GetWindowStyleFlag() & FNB_BOTTOM:
+
+                self._mainSizer.Insert(0, page, 1, wx.EXPAND)
+
+            else:
+
+                # We leave a space of 1 pixel around the window
+                self._mainSizer.Add(page, 1, wx.EXPAND)
+
+            # Fire a wxEVT_FLATNOTEBOOK_PAGE_CHANGED event
+            event.SetEventType(wxEVT_FLATNOTEBOOK_PAGE_CHANGED)
+            event.SetOldSelection(oldSelection)
+            self.GetEventHandler().ProcessEvent(event)
+
+        else:
+
+            # Hide the page
+            page.Hide()
+
+        self.Thaw()
+        self._mainSizer.Layout()
+        self.Refresh()
+
+        return True
+
+
+    def SetImageList(self, imageList):
+        """ Sets the image list for the page control. """
+
+        self._pages.SetImageList(imageList)
+
+
+    def AssignImageList(self, imageList):
+        """ Assigns the image list for the page control. """
+
+        self._pages.AssignImageList(imageList)
+
+
+    def GetImageList(self):
+        """ Returns the associated image list. """
+
+        return self._pages.GetImageList()
+
+
+    def InsertPage(self, indx, page, text, select=True, imageId=-1):
+        """
+        Inserts a new page at the specified position.
+
+        @param indx: Specifies the position of the new page.
+        @param page: Specifies the new page.
+        @param text: Specifies the text for the new page.
+        @param select: Specifies whether the page should be selected.
+        @param imageId: Specifies the optional image index for the new page.
+
+        Return value:
+        True if successful, False otherwise.
+        """
+
+        # sanity check
+        if not page:
+            return False
+
+        # reparent the window to us
+        page.Reparent(self)
+
+        if not self._windows:
+
+            self.AddPage(page, text, select, imageId)
+            return True
+
+        # Insert tab
+        bSelected = select or not self._windows
+        curSel = self._pages.GetSelection()
+
+        indx = max(0, min(indx, len(self._windows)))
+
+        if indx <= len(self._windows):
+
+            self._windows.insert(indx, page)
+
+        else:
+
+            self._windows.append(page)
+
+        if bSelected:
+
+            bSelected = False
+
+            # Check for selection and send events
+            oldSelection = self._pages._iActivePage
+
+            event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CHANGING, self.GetId())
+            event.SetSelection(indx)
+            event.SetOldSelection(oldSelection)
+            event.SetEventObject(self)
+
+            if not self.GetEventHandler().ProcessEvent(event) or event.IsAllowed() or len(self._windows) == 0:
+                bSelected = True
+
+        self._pages.InsertPage(indx, text, bSelected, imageId)
+
+        if indx <= curSel:
+            curSel = curSel + 1
+
+        self.Freeze()
+
+        # Check if a new selection was made
+        if bSelected:
+
+            if curSel >= 0:
+
+                # Remove the window from the main sizer
+                self._mainSizer.Detach(self._windows[curSel])
+                self._windows[curSel].Hide()
+
+            self._pages.SetSelection(indx)
+
+            # Fire a wxEVT_FLATNOTEBOOK_PAGE_CHANGED event
+            event.SetEventType(wxEVT_FLATNOTEBOOK_PAGE_CHANGED)
+            event.SetOldSelection(oldSelection)
+            self.GetEventHandler().ProcessEvent(event)
+
+        else:
+
+            # Hide the page
+            page.Hide()
+
+        self.Thaw()
+        self._mainSizer.Layout()
+        self.Refresh()
+
+        return True
+
+
+    def SetSelection(self, page):
+        """
+        Sets the selection for the given page.
+        The call to this function generates the page changing events
+        """
+
+        if page >= len(self._windows) or not self._windows:
+            return
+
+        # Support for disabed tabs
+        if not self._pages.GetEnabled(page) and len(self._windows) > 1 and not self._bForceSelection:
+            return
+
+        curSel = self._pages.GetSelection()
+
+        # program allows the page change
+        self.Freeze()
+        if curSel >= 0:
+
+            # Remove the window from the main sizer
+            self._mainSizer.Detach(self._windows[curSel])
+            self._windows[curSel].Hide()
+
+        if self.GetWindowStyleFlag() & FNB_BOTTOM:
+
+            self._mainSizer.Insert(0, self._windows[page], 1, wx.EXPAND)
+
+        else:
+
+            # We leave a space of 1 pixel around the window
+            self._mainSizer.Add(self._windows[page], 1, wx.EXPAND)
+
+        self._windows[page].Show()
+        self.Thaw()
+
+        self._mainSizer.Layout()
+
+        if page != self._pages._iActivePage:
+            # there is a real page changing
+            self._pages._iPreviousActivePage = self._pages._iActivePage
+
+        self._pages._iActivePage = page
+        self._pages.DoSetSelection(page)
+
+
+    def DeletePage(self, page):
+        """
+        Deletes the specified page, and the associated window.
+        The call to this function generates the page changing events.
+        """
+
+        if page >= len(self._windows) or page < 0:
+            return
+
+        # Fire a closing event
+        event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CLOSING, self.GetId())
+        event.SetSelection(page)
+        event.SetEventObject(self)
+        self.GetEventHandler().ProcessEvent(event)
+
+        # The event handler allows it?
+        if not event.IsAllowed():
+            return
+
+        self.Freeze()
+
+        # Delete the requested page
+        pageRemoved = self._windows[page]
+
+        # If the page is the current window, remove it from the sizer
+        # as well
+        if page == self._pages.GetSelection():
+            self._mainSizer.Detach(pageRemoved)
+
+        # Remove it from the array as well
+        self._windows.pop(page)
+
+        # Now we can destroy it in wxWidgets use Destroy instead of delete
+        pageRemoved.Destroy()
+
+        self.Thaw()
+
+        self._pages.DoDeletePage(page)
+        self.Refresh()
+        self.Update()
+
+        # Fire a closed event
+        closedEvent = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CLOSED, self.GetId())
+        closedEvent.SetSelection(page)
+        closedEvent.SetEventObject(self)
+        self.GetEventHandler().ProcessEvent(closedEvent)
+
+
+    def DeleteAllPages(self):
+        """ Deletes all the pages. """
+
+        if not self._windows:
+            return False
+
+        self.Freeze()
+
+        for page in self._windows:
+            page.Destroy()
+
+        self._windows = []
+        self.Thaw()
+
+        # Clear the container of the tabs as well
+        self._pages.DeleteAllPages()
+        return True
+
+
+    def GetCurrentPage(self):
+        """ Returns the currently selected notebook page or None. """
+
+        sel = self._pages.GetSelection()
+        if sel < 0:
+            return None
+
+        return self._windows[sel]
+
+
+    def GetPage(self, page):
+        """ Returns the window at the given page position, or None. """
+
+        if page >= len(self._windows):
+            return None
+
+        return self._windows[page]
+
+
+    def GetPageIndex(self, win):
+        """ Returns the index at which the window is found. """
+
+        try:
+            return self._windows.index(win)
+        except:
+            return -1
+
+
+    def GetSelection(self):
+        """ Returns the currently selected page, or -1 if none was selected. """
+
+        return self._pages.GetSelection()
+
+
+    def AdvanceSelection(self, forward=True):
+        """
+        Cycles through the tabs.
+        The call to this function generates the page changing events.
+        """
+
+        self._pages.AdvanceSelection(forward)
+
+
+    def GetPageCount(self):
+        """ Returns the number of pages in the L{FlatNotebook} control. """
+
+        return self._pages.GetPageCount()
+
+    def SetNavigatorIcon(self, bmp):
+        """ Set the icon used by the L{TabNavigatorWindow} """
+        if isinstance(bmp, wx.Bitmap) and bmp.IsOk():
+            # Make sure image is proper size
+            if bmp.GetSize() != (16, 16):
+                img = bmp.ConvertToImage()
+                img.Rescale(16, 16, wx.IMAGE_QUALITY_HIGH)
+                bmp = wx.BitmapFromImage(img)
+            self._naviIcon = bmp
+        else:
+            raise TypeError, "SetNavigatorIcon requires a valid bitmap"
+
+    def OnNavigationKey(self, event):
+        """ Handles the wx.EVT_NAVIGATION_KEY event for L{FlatNotebook}. """
+
+        if event.IsWindowChange():
+            if len(self._windows) == 0:
+                return
+            # change pages
+            if self.HasFlag(FNB_SMART_TABS):
+                if not self._popupWin:
+                    self._popupWin = TabNavigatorWindow(self, self._naviIcon)
+                    self._popupWin.SetReturnCode(wx.ID_OK)
+                    self._popupWin.ShowModal()
+                    self._popupWin.Destroy()
+                    self._popupWin = None
+                else:
+                    # a dialog is already opened
+                    self._popupWin.OnNavigationKey(event)
+                    return
+            else:
+                # change pages
+                self.AdvanceSelection(event.GetDirection())
+
+        else:
+            event.Skip()
+
+
+    def GetPageShapeAngle(self, page_index):
+        """ Returns the angle associated to a tab. """
+
+        if page_index < 0 or page_index >= len(self._pages._pagesInfoVec):
+            return None, False
+
+        result = self._pages._pagesInfoVec[page_index].GetTabAngle()
+        return result, True
+
+
+    def SetPageShapeAngle(self, page_index, angle):
+        """ Sets the angle associated to a tab. """
+
+        if page_index < 0 or page_index >= len(self._pages._pagesInfoVec):
+            return
+
+        if angle > 15:
+            return
+
+        self._pages._pagesInfoVec[page_index].SetTabAngle(angle)
+
+
+    def SetAllPagesShapeAngle(self, angle):
+        """ Sets the angle associated to all the tab. """
+
+        if angle > 15:
+            return
+
+        for ii in xrange(len(self._pages._pagesInfoVec)):
+            self._pages._pagesInfoVec[ii].SetTabAngle(angle)
+
+        self.Refresh()
+
+
+    def GetPageBestSize(self):
+        """ Return the page best size. """
+
+        return self._pages.GetClientSize()
+
+
+    def SetPageText(self, page, text):
+        """ Sets the text for the given page. """
+
+        bVal = self._pages.SetPageText(page, text)
+        self._pages.Refresh()
+
+        return bVal
+
+
+    def SetPadding(self, padding):
+        """
+        Sets the amount of space around each page's icon and label, in pixels.
+        NB: only the horizontal padding is considered.
+        """
+
+        self._nPadding = padding.GetWidth()
+
+
+    def GetTabArea(self):
+        """ Returns the associated page. """
+
+        return self._pages
+
+
+    def GetPadding(self):
+        """ Returns the amount of space around each page's icon and label, in pixels. """
+
+        return self._nPadding
+
+
+    def SetWindowStyleFlag(self, style):
+        """ Sets the L{FlatNotebook} window style flags. """
+
+        wx.PyPanel.SetWindowStyleFlag(self, style)
+        renderer = self._pages._mgr.GetRenderer(self.GetWindowStyleFlag())
+        renderer._tabHeight = None
+
+        if self._pages:
+
+            # For changing the tab position (i.e. placing them top/bottom)
+            # refreshing the tab container is not enough
+            self.SetSelection(self._pages._iActivePage)
+
+        if not self._pages.HasFlag(FNB_HIDE_ON_SINGLE_TAB):
+            #For Redrawing the Tabs once you remove the Hide tyle
+            self._pages._ReShow()
+
+
+    def RemovePage(self, page):
+        """ Deletes the specified page, without deleting the associated window. """
+
+        if page >= len(self._windows):
+            return False
+
+        # Fire a closing event
+        event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CLOSING, self.GetId())
+        event.SetSelection(page)
+        event.SetEventObject(self)
+        self.GetEventHandler().ProcessEvent(event)
+
+        # The event handler allows it?
+        if not event.IsAllowed():
+            return False
+
+        self.Freeze()
+
+        # Remove the requested page
+        pageRemoved = self._windows[page]
+
+        # If the page is the current window, remove it from the sizer
+        # as well
+        if page == self._pages.GetSelection():
+            self._mainSizer.Detach(pageRemoved)
+
+        # Remove it from the array as well
+        self._windows.pop(page)
+        self.Thaw()
+
+        self._pages.DoDeletePage(page)
+
+        return True
+
+
+    def SetRightClickMenu(self, menu):
+        """ Sets the popup menu associated to a right click on a tab. """
+
+        self._pages._pRightClickMenu = menu
+
+
+    def GetPageText(self, nPage):
+        """ Returns the tab caption. """
+
+        return self._pages.GetPageText(nPage)
+
+
+    def SetGradientColours(self, fr, to, border):
+        """ Sets the gradient colours for the tab. """
+
+        self._pages._colorFrom = fr
+        self._pages._colorTo   = to
+        self._pages._colorBorder = border
+
+
+    def SetGradientColourFrom(self, fr):
+        """ Sets the starting colour for the gradient. """
+
+        self._pages._colorFrom = fr
+
+
+    def SetGradientColourTo(self, to):
+        """ Sets the ending colour for the gradient. """
+
+        self._pages._colorTo = to
+
+
+    def SetGradientColourBorder(self, border):
+        """ Sets the tab border colour. """
+
+        self._pages._colorBorder = border
+
+
+    def GetGradientColourFrom(self):
+        """ Gets first gradient colour. """
+
+        return self._pages._colorFrom
+
+
+    def GetGradientColourTo(self):
+        """ Gets second gradient colour. """
+
+        return self._pages._colorTo
+
+
+    def GetGradientColourBorder(self):
+        """ Gets the tab border colour. """
+
+        return self._pages._colorBorder
+
+
+    def GetBorderColour(self):
+        """ Returns the border colour. """
+
+        return self._pages._colorBorder
+
+
+    def GetActiveTabTextColour(self):
+        """ Get the active tab text colour. """
+
+        return self._pages._activeTextColor
+
+
+    def SetPageImage(self, page, image):
+        """
+        Sets the image index for the given page. Image is an index into the
+        image list which was set with SetImageList.
+        """
+
+        self._pages.SetPageImage(page, image)
+
+
+    def GetPageImage(self, nPage):
+        """
+        Returns the image index for the given page. Image is an index into the
+        image list which was set with SetImageList.
+        """
+
+        return self._pages.GetPageImage(nPage)
+
+
+    def GetEnabled(self, page):
+        """ Returns whether a tab is enabled or not. """
+
+        return self._pages.GetEnabled(page)
+
+
+    def EnableTab(self, page, enabled=True):
+        """ Enables or disables a tab. """
+
+        if page >= len(self._windows):
+            return
+
+        self._windows[page].Enable(enabled)
+        self._pages.EnableTab(page, enabled)
+
+
+    def GetNonActiveTabTextColour(self):
+        """ Returns the non active tabs text colour. """
+
+        return self._pages._nonActiveTextColor
+
+
+    def SetNonActiveTabTextColour(self, color):
+        """ Sets the non active tabs text colour. """
+
+        self._pages._nonActiveTextColor = color
+
+
+    def SetTabAreaColour(self, color):
+        """ Sets the area behind the tabs colour. """
+
+        self._pages._tabAreaColor = color
+
+
+    def GetTabAreaColour(self):
+        """ Returns the area behind the tabs colour. """
+
+        return self._pages._tabAreaColor
+
+
+    def SetActiveTabColour(self, color):
+        """ Sets the active tab colour. """
+
+        self._pages._activeTabColor = color
+
+
+    def GetActiveTabColour(self):
+        """ Returns the active tab colour. """
+
+        return self._pages._activeTabColor
+
+
+# ---------------------------------------------------------------------------- #
+# Class PageContainer
+# Acts as a container for the pages you add to FlatNotebook
+# ---------------------------------------------------------------------------- #
+
+class PageContainer(wx.Panel):
+    """
+    This class acts as a container for the pages you add to L{FlatNotebook}.
+    """
+
+    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
+                 size=wx.DefaultSize, style=0):
+        """ Default class constructor. """
+
+        self._ImageList = None
+        self._iActivePage = -1
+        self._pDropTarget = None
+        self._nLeftClickZone = FNB_NOWHERE
+        self._iPreviousActivePage = -1
+
+        self._pRightClickMenu = None
+        self._nXButtonStatus = FNB_BTN_NONE
+        self._nArrowDownButtonStatus = FNB_BTN_NONE
+        self._pParent = parent
+        self._nRightButtonStatus = FNB_BTN_NONE
+        self._nLeftButtonStatus = FNB_BTN_NONE
+        self._nTabXButtonStatus = FNB_BTN_NONE
+
+        self._pagesInfoVec = []
+
+        self._colorTo = wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION)
+        self._colorFrom = wx.WHITE
+        self._activeTabColor = wx.WHITE
+        self._activeTextColor = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNTEXT)
+        self._nonActiveTextColor = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNTEXT)
+        self._tabAreaColor = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE)
+
+        self._nFrom = 0
+        self._isdragging = False
+
+        # Set default page height, this is done according to the system font
+        memDc = wx.MemoryDC()
+        memDc.SelectObject(wx.EmptyBitmap(1,1))
+
+        if "__WXGTK__" in wx.PlatformInfo:
+            boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+            boldFont.SetWeight(wx.BOLD)
+            memDc.SetFont(boldFont)
+
+        height = memDc.GetCharHeight()
+        tabHeight = height + FNB_HEIGHT_SPACER # We use 10 pixels as padding
+
+        wx.Panel.__init__(self, parent, id, pos, wx.Size(size.x, tabHeight),
+                          style|wx.NO_BORDER|wx.NO_FULL_REPAINT_ON_RESIZE|wx.WANTS_CHARS)
+
+        self._pDropTarget = FNBDropTarget(self)
+        self.SetDropTarget(self._pDropTarget)
+        self._mgr = FNBRendererMgr()
+
+        self.Bind(wx.EVT_PAINT, self.OnPaint)
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
+        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
+        self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMiddleDown)
+        self.Bind(wx.EVT_MOTION, self.OnMouseMove)
+        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
+        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)
+        self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnterWindow)
+        self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
+        self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus)
+        self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
+        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
+
+
+    def OnEraseBackground(self, event):
+        """ Handles the wx.EVT_ERASE_BACKGROUND event for L{PageContainer} (does nothing)."""
+
+        pass
+
+
+    def _ReShow(self):
+        """ Handles the Redraw of the tabs when the FNB_HIDE_ON_SINGLE_TAB has been removed """
+        self.Show()
+        self.GetParent()._mainSizer.Layout()
+        self.Refresh()
+
+
+    def OnPaint(self, event):
+        """ Handles the wx.EVT_PAINT event for L{PageContainer}."""
+
+        dc = wx.BufferedPaintDC(self)
+        renderer = self._mgr.GetRenderer(self.GetParent().GetWindowStyleFlag())
+        renderer.DrawTabs(self, dc)
+
+        if self.HasFlag(FNB_HIDE_ON_SINGLE_TAB) and len(self._pagesInfoVec) <= 1:
+            self.Hide()
+            self.GetParent()._mainSizer.Layout()
+            self.Refresh()
+
+
+    def AddPage(self, caption, selected=True, imgindex=-1):
+        """
+        Add a page to the L{FlatNotebook}.
+
+        @param window: Specifies the new page.
+        @param caption: Specifies the text for the new page.
+        @param selected: Specifies whether the page should be selected.
+        @param imgindex: Specifies the optional image index for the new page.
+
+        Return value:
+        True if successful, False otherwise.
+        """
+
+        if selected:
+
+            self._iPreviousActivePage = self._iActivePage
+            self._iActivePage = len(self._pagesInfoVec)
+
+        # Create page info and add it to the vector
+        pageInfo = PageInfo(caption, imgindex)
+        self._pagesInfoVec.append(pageInfo)
+        self.Refresh()
+
+
+    def InsertPage(self, indx, text, selected=True, imgindex=-1):
+        """
+        Inserts a new page at the specified position.
+
+        @param indx: Specifies the position of the new page.
+        @param page: Specifies the new page.
+        @param text: Specifies the text for the new page.
+        @param select: Specifies whether the page should be selected.
+        @param imgindex: Specifies the optional image index for the new page.
+
+        Return value:
+        True if successful, False otherwise.
+        """
+
+        if selected:
+
+            self._iPreviousActivePage = self._iActivePage
+            self._iActivePage = len(self._pagesInfoVec)
+
+        self._pagesInfoVec.insert(indx, PageInfo(text, imgindex))
+
+        self.Refresh()
+        return True
+
+
+    def OnSize(self, event):
+        """ Handles the wx.EVT_SIZE events for L{PageContainer}. """
+
+        # When resizing the control, try to fit to screen as many tabs as we can
+        style = self.GetParent().GetWindowStyleFlag()
+        renderer = self._mgr.GetRenderer(style)
+
+        fr = 0
+        page = self.GetSelection()
+
+        for fr in xrange(self._nFrom):
+            vTabInfo = renderer.NumberTabsCanFit(self, fr)
+            if page - fr >= len(vTabInfo):
+                continue
+            break
+
+        self._nFrom = fr
+
+        self.Refresh() # Call on paint
+        event.Skip()
+
+
+    def OnMiddleDown(self, event):
+        """ Handles the wx.EVT_MIDDLE_DOWN events for L{PageContainer}. """
+
+        # Test if this style is enabled
+        style = self.GetParent().GetWindowStyleFlag()
+
+        if not style & FNB_MOUSE_MIDDLE_CLOSES_TABS:
+            return
+
+        where, tabIdx = self.HitTest(event.GetPosition())
+
+        if where == FNB_TAB:
+            self.DeletePage(tabIdx)
+
+        event.Skip()
+
+
+    def OnRightDown(self, event):
+        """ Handles the wx.EVT_RIGHT_DOWN events for L{PageContainer}. """
+
+        where, tabIdx = self.HitTest(event.GetPosition())
+
+        if where in [FNB_TAB, FNB_TAB_X]:
+
+            if self._pagesInfoVec[tabIdx].GetEnabled():
+                # Fire events and eventually (if allowed) change selection
+                self.FireEvent(tabIdx)
+
+                # send a message to popup a custom menu
+                event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU, self.GetParent().GetId())
+                event.SetSelection(tabIdx)
+                event.SetOldSelection(self._iActivePage)
+                event.SetEventObject(self.GetParent())
+                self.GetParent().GetEventHandler().ProcessEvent(event)
+
+                if self._pRightClickMenu:
+                    self.PopupMenu(self._pRightClickMenu)
+
+        event.Skip()
+
+
+    def OnLeftDown(self, event):
+        """ Handles the wx.EVT_LEFT_DOWN events for L{PageContainer}. """
+
+        # Reset buttons status
+        self._nXButtonStatus     = FNB_BTN_NONE
+        self._nLeftButtonStatus  = FNB_BTN_NONE
+        self._nRightButtonStatus = FNB_BTN_NONE
+        self._nTabXButtonStatus  = FNB_BTN_NONE
+        self._nArrowDownButtonStatus = FNB_BTN_NONE
+
+        self._nLeftClickZone, tabIdx = self.HitTest(event.GetPosition())
+
+        if self._nLeftClickZone == FNB_DROP_DOWN_ARROW:
+            self._nArrowDownButtonStatus = FNB_BTN_PRESSED
+            self.Refresh()
+        elif self._nLeftClickZone == FNB_LEFT_ARROW:
+            self._nLeftButtonStatus = FNB_BTN_PRESSED
+            self.Refresh()
+        elif self._nLeftClickZone == FNB_RIGHT_ARROW:
+            self._nRightButtonStatus = FNB_BTN_PRESSED
+            self.Refresh()
+        elif self._nLeftClickZone == FNB_X:
+            self._nXButtonStatus = FNB_BTN_PRESSED
+            self.Refresh()
+        elif self._nLeftClickZone == FNB_TAB_X:
+            self._nTabXButtonStatus = FNB_BTN_PRESSED
+            self.Refresh()
+
+        elif self._nLeftClickZone == FNB_TAB:
+
+            if self._iActivePage != tabIdx:
+
+                # In case the tab is disabled, we dont allow to choose it
+                if self._pagesInfoVec[tabIdx].GetEnabled():
+                    self.FireEvent(tabIdx)
+
+
+    def RotateLeft(self):
+
+        if self._nFrom == 0:
+            return
+
+        # Make sure that the button was pressed before
+        if self._nLeftButtonStatus != FNB_BTN_PRESSED:
+            return
+
+        self._nLeftButtonStatus = FNB_BTN_HOVER
+
+        # We scroll left with bulks of 5
+        scrollLeft = self.GetNumTabsCanScrollLeft()
+
+        self._nFrom -= scrollLeft
+        if self._nFrom < 0:
+            self._nFrom = 0
+
+        self.Refresh()
+
+
+    def RotateRight(self):
+
+        if self._nFrom >= len(self._pagesInfoVec) - 1:
+            return
+
+        # Make sure that the button was pressed before
+        if self._nRightButtonStatus != FNB_BTN_PRESSED:
+            return
+
+        self._nRightButtonStatus = FNB_BTN_HOVER
+
+        # Check if the right most tab is visible, if it is
+        # don't rotate right anymore
+        if self._pagesInfoVec[len(self._pagesInfoVec)-1].GetPosition() != wx.Point(-1, -1):
+            return
+
+        self._nFrom += 1
+        self.Refresh()
+
+
+    def OnLeftUp(self, event):
+        """ Handles the wx.EVT_LEFT_UP events for L{PageContainer}. """
+
+        # forget the zone that was initially clicked
+        self._nLeftClickZone = FNB_NOWHERE
+
+        where, tabIdx = self.HitTest(event.GetPosition())
+
+        # Make sure selected tab has focus
+        self.SetFocus()
+
+        if where == FNB_LEFT_ARROW:
+            self.RotateLeft()
+
+        elif where == FNB_RIGHT_ARROW:
+            self.RotateRight()
+
+        elif where == FNB_X:
+
+            # Make sure that the button was pressed before
+            if self._nXButtonStatus != FNB_BTN_PRESSED:
+                return
+
+            self._nXButtonStatus = FNB_BTN_HOVER
+
+            self.DeletePage(self._iActivePage)
+
+        elif where == FNB_TAB_X:
+
+            # Make sure that the button was pressed before
+            if self._nTabXButtonStatus != FNB_BTN_PRESSED:
+                return
+
+            self._nTabXButtonStatus = FNB_BTN_HOVER
+
+            self.DeletePage(self._iActivePage)
+
+        elif where == FNB_DROP_DOWN_ARROW:
+
+            # Make sure that the button was pressed before
+            if self._nArrowDownButtonStatus != FNB_BTN_PRESSED:
+                return
+
+            self._nArrowDownButtonStatus = FNB_BTN_NONE
+
+            # Refresh the button status
+            renderer = self._mgr.GetRenderer(self.GetParent().GetWindowStyleFlag())
+            dc = wx.ClientDC(self)
+            renderer.DrawDropDownArrow(self, dc)
+
+            self.PopupTabsMenu()
+
+        event.Skip()
+
+
+    def HitTest(self, pt):
+        """
+        HitTest method for L{PageContainer}.
+        Returns the flag (if any) and the hit page (if any).
+        """
+
+        style = self.GetParent().GetWindowStyleFlag()
+        render = self._mgr.GetRenderer(style)
+
+        fullrect = self.GetClientRect()
+        btnLeftPos = render.GetLeftButtonPos(self)
+        btnRightPos = render.GetRightButtonPos(self)
+        btnXPos = render.GetXPos(self)
+
+        tabIdx = -1
+
+        if len(self._pagesInfoVec) == 0:
+            return FNB_NOWHERE, tabIdx
+
+        rect = wx.Rect(btnXPos, 8, 16, 16)
+        if rect.Contains(pt):
+            return (style & FNB_NO_X_BUTTON and [FNB_NOWHERE] or [FNB_X])[0], tabIdx
+
+        rect = wx.Rect(btnRightPos, 8, 16, 16)
+        if style & FNB_DROPDOWN_TABS_LIST:
+            rect = wx.Rect(render.GetDropArrowButtonPos(self), 8, 16, 16)
+            if rect.Contains(pt):
+                return FNB_DROP_DOWN_ARROW, tabIdx
+
+        if rect.Contains(pt):
+            return (style & FNB_NO_NAV_BUTTONS and [FNB_NOWHERE] or [FNB_RIGHT_ARROW])[0], tabIdx
+
+        rect = wx.Rect(btnLeftPos, 8, 16, 16)
+        if rect.Contains(pt):
+            return (style & FNB_NO_NAV_BUTTONS and [FNB_NOWHERE] or [FNB_LEFT_ARROW])[0], tabIdx
+
+        # Test whether a left click was made on a tab
+        bFoundMatch = False
+
+        for cur in xrange(self._nFrom, len(self._pagesInfoVec)):
+
+            pgInfo = self._pagesInfoVec[cur]
+
+            if pgInfo.GetPosition() == wx.Point(-1, -1):
+                continue
+
+            if style & FNB_X_ON_TAB and cur == self.GetSelection():
+                # 'x' button exists on a tab
+                if self._pagesInfoVec[cur].GetXRect().Contains(pt):
+                    return FNB_TAB_X, cur
+
+            if style & FNB_VC8:
+
+                if self._pagesInfoVec[cur].GetRegion().Contains(pt.x, pt.y):
+                    if bFoundMatch or cur == self.GetSelection():
+                        return FNB_TAB, cur
+
+                    tabIdx = cur
+                    bFoundMatch = True
+
+            else:
+
+                tabRect = wx.Rect(pgInfo.GetPosition().x, pgInfo.GetPosition().y,
+                                  pgInfo.GetSize().x, pgInfo.GetSize().y)
+
+                if tabRect.Contains(pt):
+                    # We have a match
+                    return FNB_TAB, cur
+
+        if bFoundMatch:
+            return FNB_TAB, tabIdx
+
+        if self._isdragging:
+            # We are doing DND, so check also the region outside the tabs
+            # try before the first tab
+            pgInfo = self._pagesInfoVec[0]
+            tabRect = wx.Rect(0, pgInfo.GetPosition().y, pgInfo.GetPosition().x, self.GetParent().GetSize().y)
+            if tabRect.Contains(pt):
+                return FNB_TAB, 0
+
+            # try after the last tab
+            pgInfo = self._pagesInfoVec[-1]
+            startpos = pgInfo.GetPosition().x+pgInfo.GetSize().x
+            tabRect = wx.Rect(startpos, pgInfo.GetPosition().y, fullrect.width-startpos, self.GetParent().GetSize().y)
+
+            if tabRect.Contains(pt):
+                return FNB_TAB, len(self._pagesInfoVec)
+
+        # Default
+        return FNB_NOWHERE, -1
+
+
+    def SetSelection(self, page):
+        """ Sets the selected page. """
+
+        book = self.GetParent()
+        book.SetSelection(page)
+        self.DoSetSelection(page)
+
+
+    def DoSetSelection(self, page):
+        """ Does the actual selection of a page. """
+
+        if page < len(self._pagesInfoVec):
+            #! fix for tabfocus
+            da_page = self._pParent.GetPage(page)
+
+            if da_page != None:
+                da_page.SetFocus()
+
+        if not self.IsTabVisible(page):
+            # Try to remove one tab from start and try again
+
+            if not self.CanFitToScreen(page):
+
+                if self._nFrom > page:
+                    self._nFrom = page
+                else:
+                    while self._nFrom < page:
+                        self._nFrom += 1
+                        if self.CanFitToScreen(page):
+                            break
+
+        self.Refresh()
+
+
+    def DeletePage(self, page):
+        """ Delete the specified page from L{FlatNotebook}. """
+
+        book = self.GetParent()
+        book.DeletePage(page)
+        book.Refresh()
+
+
+    def IsTabVisible(self, page):
+        """ Returns whether a tab is visible or not. """
+
+        iLastVisiblePage = self.GetLastVisibleTab()
+        return page <= iLastVisiblePage and page >= self._nFrom
+
+
+    def DoDeletePage(self, page):
+        """ Does the actual page deletion. """
+
+        # Remove the page from the vector
+        book = self.GetParent()
+        self._pagesInfoVec.pop(page)
+
+        # Thanks to Yiaanis AKA Mandrav
+        if self._iActivePage >= page:
+            self._iActivePage = self._iActivePage - 1
+            self._iPreviousActivePage = -1
+
+        # The delete page was the last first on the array,
+        # but the book still has more pages, so we set the
+        # active page to be the first one (0)
+        if self._iActivePage < 0 and len(self._pagesInfoVec) > 0:
+            self._iActivePage = 0
+            self._iPreviousActivePage = -1
+
+        # Refresh the tabs
+        if self._iActivePage >= 0:
+
+            book._bForceSelection = True
+
+            # Check for selection and send event
+            event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CHANGING, self.GetParent().GetId())
+            event.SetSelection(self._iActivePage)
+            event.SetOldSelection(self._iPreviousActivePage)
+            event.SetEventObject(self.GetParent())
+
+            book.SetSelection(self._iActivePage)
+            book._bForceSelection = False
+
+            # Fire a wxEVT_FLATNOTEBOOK_PAGE_CHANGED event
+            event.SetEventType(wxEVT_FLATNOTEBOOK_PAGE_CHANGED)
+            event.SetOldSelection(self._iPreviousActivePage)
+            self.GetParent().GetEventHandler().ProcessEvent(event)
+
+        if not self._pagesInfoVec:
+            # Erase the page container drawings
+            dc = wx.ClientDC(self)
+            dc.Clear()
+
+
+    def DeleteAllPages(self):
+        """ Deletes all the pages. """
+
+        self._iActivePage = -1
+        self._iPreviousActivePage = -1
+        self._nFrom = 0
+        self._pagesInfoVec = []
+
+        # Erase the page container drawings
+        dc = wx.ClientDC(self)
+        dc.Clear()
+
+
+    def OnMouseMove(self, event):
+        """ Handles the wx.EVT_MOTION for L{PageContainer}. """
+
+        if self._pagesInfoVec and self.IsShown():
+
+            xButtonStatus = self._nXButtonStatus
+            xTabButtonStatus = self._nTabXButtonStatus
+            rightButtonStatus = self._nRightButtonStatus
+            leftButtonStatus = self._nLeftButtonStatus
+            dropDownButtonStatus = self._nArrowDownButtonStatus
+
+            style = self.GetParent().GetWindowStyleFlag()
+
+            self._nXButtonStatus = FNB_BTN_NONE
+            self._nRightButtonStatus = FNB_BTN_NONE
+            self._nLeftButtonStatus = FNB_BTN_NONE
+            self._nTabXButtonStatus = FNB_BTN_NONE
+            self._nArrowDownButtonStatus = FNB_BTN_NONE
+
+            where, tabIdx = self.HitTest(event.GetPosition())
+
+            if where == FNB_X:
+                if event.LeftIsDown():
+
+                    self._nXButtonStatus = (self._nLeftClickZone==FNB_X and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0]
+
+                else:
+
+                    self._nXButtonStatus = FNB_BTN_HOVER
+
+            elif where == FNB_DROP_DOWN_ARROW:
+                if event.LeftIsDown():
+
+                    self._nArrowDownButtonStatus = (self._nLeftClickZone==FNB_DROP_DOWN_ARROW and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0]
+
+                else:
+
+                    self._nArrowDownButtonStatus = FNB_BTN_HOVER
+
+            elif where == FNB_TAB_X:
+                if event.LeftIsDown():
+
+                    self._nTabXButtonStatus = (self._nLeftClickZone==FNB_TAB_X and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0]
+
+                else:
+
+                    self._nTabXButtonStatus = FNB_BTN_HOVER
+
+            elif where == FNB_RIGHT_ARROW:
+                if event.LeftIsDown():
+
+                    self._nRightButtonStatus = (self._nLeftClickZone==FNB_RIGHT_ARROW and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0]
+
+                else:
+
+                    self._nRightButtonStatus = FNB_BTN_HOVER
+
+            elif where == FNB_LEFT_ARROW:
+                if event.LeftIsDown():
+
+                    self._nLeftButtonStatus = (self._nLeftClickZone==FNB_LEFT_ARROW and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0]
+
+                else:
+
+                    self._nLeftButtonStatus = FNB_BTN_HOVER
+
+            elif where == FNB_TAB:
+                # Call virtual method for showing tooltip
+                self.ShowTabTooltip(tabIdx)
+
+                if not self.GetEnabled(tabIdx):
+                    # Set the cursor to be 'No-entry'
+                    wx.SetCursor(wx.StockCursor(wx.CURSOR_NO_ENTRY))
+
+                # Support for drag and drop
+                if event.Dragging() and not (style & FNB_NODRAG):
+
+                    self._isdragging = True
+                    draginfo = FNBDragInfo(self, tabIdx)
+                    drginfo = cPickle.dumps(draginfo)
+                    dataobject = wx.CustomDataObject(wx.CustomDataFormat("FlatNotebook"))
+                    dataobject.SetData(drginfo)
+                    dragSource = FNBDropSource(self)
+                    dragSource.SetData(dataobject)
+                    dragSource.DoDragDrop(wx.Drag_DefaultMove)
+
+            bRedrawX = self._nXButtonStatus != xButtonStatus
+            bRedrawRight = self._nRightButtonStatus != rightButtonStatus
+            bRedrawLeft = self._nLeftButtonStatus != leftButtonStatus
+            bRedrawTabX = self._nTabXButtonStatus != xTabButtonStatus
+            bRedrawDropArrow = self._nArrowDownButtonStatus != dropDownButtonStatus
+
+            render = self._mgr.GetRenderer(style)
+
+            if (bRedrawX or bRedrawRight or bRedrawLeft or bRedrawTabX or bRedrawDropArrow):
+
+                dc = wx.ClientDC(self)
+
+                if bRedrawX:
+
+                    render.DrawX(self, dc)
+
+                if bRedrawLeft:
+
+                    render.DrawLeftArrow(self, dc)
+
+                if bRedrawRight:
+
+                    render.DrawRightArrow(self, dc)
+
+                if bRedrawTabX:
+
+                    self.Refresh()
+
+                if bRedrawDropArrow:
+
+                    render.DrawDropDownArrow(self, dc)
+
+        event.Skip()
+
+
+    def GetLastVisibleTab(self):
+        """ Returns the last visible tab. """
+
+        if self._nFrom < 0:
+            return -1
+
+        ii = 0
+
+        for ii in xrange(self._nFrom, len(self._pagesInfoVec)):
+
+            if self._pagesInfoVec[ii].GetPosition() == wx.Point(-1, -1):
+                break
+
+        return ii-1
+
+
+    def GetNumTabsCanScrollLeft(self):
+        """ Returns the number of tabs than can be scrolled left. """
+
+        if self._nFrom - 1 >= 0:
+            return 1
+
+        return 0
+
+
+    def IsDefaultTabs(self):
+        """ Returns whether a tab has a default style. """
+
+        style = self.GetParent().GetWindowStyleFlag()
+        res = (style & FNB_VC71) or (style & FNB_FANCY_TABS) or (style & FNB_VC8)
+        return not res
+
+
+    def AdvanceSelection(self, bForward=True):
+        """
+        Cycles through the tabs.
+        The call to this function generates the page changing events.
+        """
+
+        nSel = self.GetSelection()
+
+        if nSel < 0:
+            return
+
+        nMax = self.GetPageCount() - 1
+
+        if bForward:
+            newSelection = (nSel == nMax and [0] or [nSel + 1])[0]
+        else:
+            newSelection = (nSel == 0 and [nMax] or [nSel - 1])[0]
+
+        if not self._pagesInfoVec[newSelection].GetEnabled():
+            return
+
+        self.FireEvent(newSelection)
+
+
+    def OnMouseLeave(self, event):
+        """ Handles the wx.EVT_LEAVE_WINDOW event for L{PageContainer}. """
+
+        self._nLeftButtonStatus = FNB_BTN_NONE
+        self._nXButtonStatus = FNB_BTN_NONE
+        self._nRightButtonStatus = FNB_BTN_NONE
+        self._nTabXButtonStatus = FNB_BTN_NONE
+        self._nArrowDownButtonStatus = FNB_BTN_NONE
+
+        style = self.GetParent().GetWindowStyleFlag()
+        render = self._mgr.GetRenderer(style)
+
+        dc = wx.ClientDC(self)
+
+        render.DrawX(self, dc)
+        render.DrawLeftArrow(self, dc)
+        render.DrawRightArrow(self, dc)
+
+        selection = self.GetSelection()
+
+        if selection == -1:
+            event.Skip()
+            return
+
+        if not self.IsTabVisible(selection):
+            if selection == len(self._pagesInfoVec) - 1:
+                if not self.CanFitToScreen(selection):
+                    event.Skip()
+                    return
+            else:
+                event.Skip()
+                return
+
+        render.DrawTabX(self, dc, self._pagesInfoVec[selection].GetXRect(), selection, self._nTabXButtonStatus)
+        render.DrawFocusRectangle(dc, self, self._pagesInfoVec[selection])
+
+        event.Skip()
+
+
+    def OnMouseEnterWindow(self, event):
+        """ Handles the wx.EVT_ENTER_WINDOW event for L{PageContainer}. """
+
+        self._nLeftButtonStatus = FNB_BTN_NONE
+        self._nXButtonStatus = FNB_BTN_NONE
+        self._nRightButtonStatus = FNB_BTN_NONE
+        self._nLeftClickZone = FNB_BTN_NONE
+        self._nArrowDownButtonStatus = FNB_BTN_NONE
+
+        event.Skip()
+
+
+    def ShowTabTooltip(self, tabIdx):
+        """ Shows a tab tooltip. """
+
+        pWindow = self._pParent.GetPage(tabIdx)
+
+        if pWindow:
+            pToolTip = pWindow.GetToolTip()
+            if pToolTip and pToolTip.GetWindow() == pWindow:
+                self.SetToolTipString(pToolTip.GetTip())
+
+
+    def SetPageImage(self, page, imgindex):
+        """ Sets the image index associated to a page. """
+
+        if page < len(self._pagesInfoVec):
+
+            self._pagesInfoVec[page].SetImageIndex(imgindex)
+            self.Refresh()
+
+
+    def GetPageImage(self, page):
+        """ Returns the image index associated to a page. """
+
+        if page < len(self._pagesInfoVec):
+
+            return self._pagesInfoVec[page].GetImageIndex()
+
+        return -1
+
+
+    def OnDropTarget(self, x, y, nTabPage, wnd_oldContainer):
+        """ Handles the drop action from a DND operation. """
+
+        # Disable drag'n'drop for disabled tab
+        if not wnd_oldContainer._pagesInfoVec[nTabPage].GetEnabled():
+            return wx.DragCancel
+
+        self._isdragging = True
+        oldContainer = wnd_oldContainer
+        nIndex = -1
+
+        where, nIndex = self.HitTest(wx.Point(x, y))
+
+        oldNotebook = oldContainer.GetParent()
+        newNotebook = self.GetParent()
+
+        if oldNotebook == newNotebook:
+
+            if nTabPage >= 0:
+
+                if where == FNB_TAB:
+                    self.MoveTabPage(nTabPage, nIndex)
+
+        elif self.GetParent().GetWindowStyleFlag() & FNB_ALLOW_FOREIGN_DND:
+
+            if wx.Platform in ["__WXMSW__", "__WXGTK__", "__WXMAC__"]:
+                if nTabPage >= 0:
+
+                    window = oldNotebook.GetPage(nTabPage)
+
+                    if window:
+                        where, nIndex = newNotebook._pages.HitTest(wx.Point(x, y))
+                        caption = oldContainer.GetPageText(nTabPage)
+                        imageindex = oldContainer.GetPageImage(nTabPage)
+                        oldNotebook.RemovePage(nTabPage)
+                        window.Reparent(newNotebook)
+
+                        if imageindex >= 0:
+
+                            bmp = oldNotebook.GetImageList().GetBitmap(imageindex)
+                            newImageList = newNotebook.GetImageList()
+
+                            if not newImageList:
+                                xbmp, ybmp = bmp.GetWidth(), bmp.GetHeight()
+                                newImageList = wx.ImageList(xbmp, ybmp)
+                                imageindex = 0
+                            else:
+                                imageindex = newImageList.GetImageCount()
+
+                            newImageList.Add(bmp)
+                            newNotebook.SetImageList(newImageList)
+
+                        newNotebook.InsertPage(nIndex, window, caption, True, imageindex)
+
+        self._isdragging = False
+
+        return wx.DragMove
+
+
+    def MoveTabPage(self, nMove, nMoveTo):
+        """ Moves a tab inside the same L{FlatNotebook}. """
+
+        if nMove == nMoveTo:
+            return
+
+        elif nMoveTo < len(self._pParent._windows):
+            nMoveTo = nMoveTo + 1
+
+        self._pParent.Freeze()
+
+        # Remove the window from the main sizer
+        nCurSel = self._pParent._pages.GetSelection()
+        self._pParent._mainSizer.Detach(self._pParent._windows[nCurSel])
+        self._pParent._windows[nCurSel].Hide()
+
+        pWindow = self._pParent._windows[nMove]
+        self._pParent._windows.pop(nMove)
+        self._pParent._windows.insert(nMoveTo-1, pWindow)
+
+        pgInfo = self._pagesInfoVec[nMove]
+
+        self._pagesInfoVec.pop(nMove)
+        self._pagesInfoVec.insert(nMoveTo - 1, pgInfo)
+
+        # Add the page according to the style
+        pSizer = self._pParent._mainSizer
+        style = self.GetParent().GetWindowStyleFlag()
+
+        if style & FNB_BOTTOM:
+
+            pSizer.Insert(0, pWindow, 1, wx.EXPAND)
+
+        else:
+
+            # We leave a space of 1 pixel around the window
+            pSizer.Add(pWindow, 1, wx.EXPAND)
+
+        pWindow.Show()
+
+        pSizer.Layout()
+        self._iActivePage = nMoveTo - 1
+        self._iPreviousActivePage = -1
+        self.DoSetSelection(self._iActivePage)
+        self.Refresh()
+        self._pParent.Thaw()
+
+
+    def CanFitToScreen(self, page):
+        """ Returns wheter a tab can fit in the left space in the screen or not. """
+
+        # Incase the from is greater than page,
+        # we need to reset the self._nFrom, so in order
+        # to force the caller to do so, we return false
+        if self._nFrom > page:
+            return False
+
+        style = self.GetParent().GetWindowStyleFlag()
+        render = self._mgr.GetRenderer(style)
+
+        vTabInfo = render.NumberTabsCanFit(self)
+
+        if page - self._nFrom >= len(vTabInfo):
+            return False
+
+        return True
+
+
+    def GetNumOfVisibleTabs(self):
+        """ Returns the number of visible tabs. """
+
+        count = 0
+        for ii in xrange(self._nFrom, len(self._pagesInfoVec)):
+            if self._pagesInfoVec[ii].GetPosition() == wx.Point(-1, -1):
+                break
+            count = count + 1
+
+        return count
+
+
+    def GetEnabled(self, page):
+        """ Returns whether a tab is enabled or not. """
+
+        if page >= len(self._pagesInfoVec):
+            return True # Seems strange, but this is the default
+
+        return self._pagesInfoVec[page].GetEnabled()
+
+
+    def EnableTab(self, page, enabled=True):
+        """ Enables or disables a tab. """
+
+        if page >= len(self._pagesInfoVec):
+            return
+
+        self._pagesInfoVec[page].EnableTab(enabled)
+
+
+    def GetSingleLineBorderColour(self):
+        """ Returns the colour for the single line border. """
+
+        if self.HasFlag(FNB_FANCY_TABS):
+            return self._colorFrom
+
+        return wx.WHITE
+
+
+    def HasFlag(self, flag):
+        """ Returns whether a flag is present in the L{FlatNotebook} style. """
+
+        style = self.GetParent().GetWindowStyleFlag()
+        res = (style & flag and [True] or [False])[0]
+        return res
+
+
+    def ClearFlag(self, flag):
+        """ Deletes a flag from the L{FlatNotebook} style. """
+
+        style = self.GetParent().GetWindowStyleFlag()
+        style &= ~flag
+        self.SetWindowStyleFlag(style)
+
+
+    def TabHasImage(self, tabIdx):
+        """ Returns whether a tab has an associated image index or not. """
+
+        if self._ImageList:
+            return self._pagesInfoVec[tabIdx].GetImageIndex() != -1
+
+        return False
+
+
+    def OnLeftDClick(self, event):
+        """ Handles the wx.EVT_LEFT_DCLICK event for L{PageContainer}. """
+
+        where, tabIdx = self.HitTest(event.GetPosition())
+
+        if where == FNB_RIGHT_ARROW:
+            self.RotateRight()
+
+        elif where == FNB_LEFT_ARROW:
+            self.RotateLeft()
+
+        elif self.HasFlag(FNB_DCLICK_CLOSES_TABS):
+
+            if where == FNB_TAB:
+                self.DeletePage(tabIdx)
+
+        else:
+
+            event.Skip()
+
+
+    def OnSetFocus(self, event):
+        """ Handles the wx.EVT_SET_FOCUS event for L{PageContainer}. """
+
+        if self._iActivePage < 0:
+            event.Skip()
+            return
+
+        self.SetFocusedPage(self._iActivePage)
+
+
+    def OnKillFocus(self, event):
+        """ Handles the wx.EVT_KILL_FOCUS event for L{PageContainer}. """
+
+        self.SetFocusedPage()
+
+
+    def OnKeyDown(self, event):
+        """
+        When the PageContainer has the focus tabs can be changed with
+        the left/right arrow keys.
+        """
+        key = event.GetKeyCode()
+        if key == wx.WXK_LEFT:
+            self.GetParent().AdvanceSelection(False)
+        elif key == wx.WXK_RIGHT:
+            self.GetParent().AdvanceSelection(True)
+        elif key == wx.WXK_TAB and not event.ControlDown():
+            flags = 0
+            if not event.ShiftDown(): flags |= wx.NavigationKeyEvent.IsForward
+            if event.CmdDown():       flags |= wx.NavigationKeyEvent.WinChange
+            self.Navigate(flags)
+        else:
+            event.Skip()
+
+
+    def SetFocusedPage(self, pageIndex=-1):
+        """
+        Sets/Unsets the focus on the appropriate page.
+        If pageIndex is defaulted, we have lost focus and no focus indicator is drawn.
+        """
+
+        for indx, page in enumerate(self._pagesInfoVec):
+            if indx == pageIndex:
+                page._hasFocus = True
+            else:
+                page._hasFocus = False
+
+        self.Refresh()
+
+
+    def PopupTabsMenu(self):
+        """ Pops up the menu activated with the drop down arrow in the navigation area. """
+
+        popupMenu = wx.Menu()
+
+        for i in xrange(len(self._pagesInfoVec)):
+            pi = self._pagesInfoVec[i]
+            item = wx.MenuItem(popupMenu, i+1, pi.GetCaption(), pi.GetCaption(), wx.ITEM_NORMAL)
+            self.Bind(wx.EVT_MENU, self.OnTabMenuSelection, item)
+
+            # There is an alignment problem with wx2.6.3 & Menus so only use
+            # images for versions above 2.6.3
+            if wx.VERSION > (2, 6, 3, 0) and self.TabHasImage(i):
+                item.SetBitmap(self.GetImageList().GetBitmap(pi.GetImageIndex()))
+
+            popupMenu.AppendItem(item)
+            item.Enable(pi.GetEnabled())
+
+        self.PopupMenu(popupMenu)
+
+
+    def OnTabMenuSelection(self, event):
+        """ Handles the wx.EVT_MENU event for L{PageContainer}. """
+
+        selection = event.GetId() - 1
+        self.FireEvent(selection)
+
+
+    def FireEvent(self, selection):
+        """
+        Fires the wxEVT_FLATNOTEBOOK_PAGE_CHANGING and wxEVT_FLATNOTEBOOK_PAGE_CHANGED events
+        called from other methods (from menu selection or Smart Tabbing).
+        Utility function.
+        """
+
+        if selection == self._iActivePage:
+            # No events for the same selection
+            return
+
+        oldSelection = self._iActivePage
+
+        event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CHANGING, self.GetParent().GetId())
+        event.SetSelection(selection)
+        event.SetOldSelection(oldSelection)
+        event.SetEventObject(self.GetParent())
+
+        if not self.GetParent().GetEventHandler().ProcessEvent(event) or event.IsAllowed():
+
+            self.SetSelection(selection)
+
+            # Fire a wxEVT_FLATNOTEBOOK_PAGE_CHANGED event
+            event.SetEventType(wxEVT_FLATNOTEBOOK_PAGE_CHANGED)
+            event.SetOldSelection(oldSelection)
+            self.GetParent().GetEventHandler().ProcessEvent(event)
+            self.SetFocus()
+
+
+    def SetImageList(self, imglist):
+        """ Sets the image list for the page control. """
+
+        self._ImageList = imglist
+
+
+    def AssignImageList(self, imglist):
+        """ Assigns the image list for the page control. """
+
+        self._ImageList = imglist
+
+
+    def GetImageList(self):
+        """ Returns the image list for the page control. """
+
+        return self._ImageList
+
+
+    def GetSelection(self):
+        """ Returns the current selected page. """
+
+        return self._iActivePage
+
+
+    def GetPageCount(self):
+        """ Returns the number of tabs in the L{FlatNotebook} control. """
+
+        return len(self._pagesInfoVec)
+
+
+    def GetPageText(self, page):
+        """ Returns the tab caption of the page. """
+
+        return self._pagesInfoVec[page].GetCaption()
+
+
+    def SetPageText(self, page, text):
+        """ Sets the tab caption of the page. """
+
+        self._pagesInfoVec[page].SetCaption(text)
+        return True
+
+
+    def DrawDragHint(self):
+        """ Draws small arrow at the place that the tab will be placed. """
+
+        # get the index of tab that will be replaced with the dragged tab
+        pt = wx.GetMousePosition()
+        client_pt = self.ScreenToClient(pt)
+        where, tabIdx = self.HitTest(client_pt)
+        self._mgr.GetRenderer(self.GetParent().GetWindowStyleFlag()).DrawDragHint(self, tabIdx)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/NotebookCtrl.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,6112 @@
+# --------------------------------------------------------------------------- #
+# NOTEBOOKCTRL Control wxPython IMPLEMENTATION
+# Python Code By:
+#
+# Andrea Gavana, @ 11 Nov 2005
+# Latest Revision: 06 Oct 2006, 18.10 GMT
+#
+#
+# AKNOWLEDGEMENTS
+#
+# A big load of thanks goes to Julianne Sharer that has implemented the new
+# features of left/right tabs, rotated or horizontal, with the ability to
+# switch between the two views by a single mouse click. Moreover, all the
+# work done to refactor NotebookCtrl in a more readable way has been done
+# by Julianne Sharer. Thanks Julianne.
+#
+#
+# TODO List/Caveats
+#
+# 1. Ay Idea?
+#
+# For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please
+# Write To Me At:
+#
+# andrea.gavana@gmail.com
+# gavana@kpo.kz
+#
+# Or, Obviously, To The wxPython Mailing List!!!
+#
+#
+# End Of Comments
+# --------------------------------------------------------------------------- #
+
+
+"""
+A full-featured notebook control, worked out by Andrea Gavana And Julianne Sharer.
+
+Description:
+
+NotebookCtrl Mimics The Behavior Of wx.Notebook, And Most Of Its Functionalities
+Are Implemented In NotebookCtrl. However, NotebookCtrl Has A Lot Of Options That
+wx.Notebook Does Not Have, And It Is Therefore Quite Customizable.
+wx.Notebook Styles Not Implemented in NotebookCtrl Are:
+
+    - wx.NB_MULTILINE (But NotebookCtrl Has A SpinButton To Navigate
+      Through Tabs).
+
+Supported Customizations For NotebookCtrl Include:
+
+    - Setting Individual Tab Font And Text Colour;
+    - Images On Tabs (Line wx.Notebook);
+    - Setting Individual Tab Colours;
+    - Disabling/Enabling Individual Tabs (Also Visually Effective);
+      Now Supports Grayed Out Icons When A Page Is Disabled;
+    - Drawing Of A Small Closing "X" At The Right Of Every Tab, That Enable The User
+      To Close A Tab With A Mouse Click (Like eMule Tab Style);
+    - Enabling Highlighted Tabs On Selection;
+    - Drawing Focus Indicator In Each Tab (Like wx.Notebook);
+    - Ctrl-Tab Keyboard Navigation Between Pages;
+    - Tab With Animated Icons (Animation On Tabs);
+    - Drag And Drop Tabs In NotebookCtrl (Plus A Visual Arrow Effect
+      To Indicate Dropping Position);
+    - Drag And Drop Event;
+    - ToolTips On Individual Tabs, With Customizable ToolTip Time
+      Popup And ToolTip Window Size For Individual Tabs;
+    - Possibility To Hide The TabCtrl There Is Only One Tab (Thus
+      Maximizing The Corresponding Window);
+    - Possibility To Convert The Tab Image Into A Close Button While
+      Mouse Is Hovering On The Tab Image;
+    - Popup Menus On Tabs (Popup Menus Specific To Each Tab);
+    - Showing Pages In "Column/Row Mode", Which Means That All Pages
+      Will Be Shown In NotebookCtrl While The Tabs Are Hidden. They
+      Can Be Shown In Columns (Default) Or In Rows;
+    - Possibility To Hide Tabs On User Request, Thus Showing Only The
+      Current Panel;
+    - Multiple Tabs Selection (Hold Ctrl Key Down And Left Mouse
+      Click), Useful When You Use The Show All The Panels In
+      Columns/Rows. In This Case, Only The Selected Tabs Are Shown In
+      Columns/Rows;
+    - Events For Mouse Events (Left Double Click, Middle Click, Right Click);
+    - Possibility To Reparent A NotebookCtrl Page To A Freshly Created
+      Frame As A Simple Panel Or To A New NotebookCtrl Created Inside
+      That New Frame.
+    - Possibility To Add A Custom Panel To Show A Logo Or HTML
+      Information Or Whatever You Like When There Are No Tabs In
+      NotebookCtrl;
+    - Possibility To Change The ToolTip Window Background Colour;
+    - Possibility To Draw Vertical Or Horizontal Gradient Coloured Tabs
+      (2 Colours);
+    - Themes On Tabs: Built-In Themes Are KDE (Unix/Linux), Metal,
+      Aqua Light And Aqua Dark (MacOS), Windows Silver (Windows) Or
+      Generic Gradient Coloured Tabs. It's Also Possible To Define A
+      Separate Theme For Selected Tabs And Control Background (The
+      Last Two Are Work In Progress);
+    - Contour Line Colour Around Tabs Is Customizable;
+    - Highlight Colour Of Selected Tab Is Customizable;
+    - Each Tab Can Have Its Own Gradient Colouring (2 Colours For Every Tab);
+    - Custom Images May Be Drawn As A "X" Close Buttons On Tabs;
+    - Possibility To Hide A Particular Tab Using A wx.PopupMenu That
+      Is Shown If You Call EnableHiding(True). Look At The Top Right
+      Of NotebookCtrl;
+    - Allows Drag And Drop Of Tabs/Pages Between Different
+      NotebookCtrls In The Same Application.
+    - Draw tabs on the left or right side, rotated or horizontal
+    - Allow user to switch between rotated and horizontal displays of
+      tabs on the left or right side.
+
+
+Usage:
+
+NotebookCtrl Construction Is Quite Similar To wx.Notebook::
+
+    NotebookCtrl.__init__(self, parent, id, pos=wx.DefaultPosition,
+                          size=wx.DefaultSize, style=style, sizer=nbsizer)
+
+See L{NotebookCtrl.__init__} Method For The Definition Of Non Standard (Non
+wxPython) Parameters.
+
+NotebookCtrl Control Is Freeware And Distributed Under The wxPython License.
+
+Latest Revision: Andrea Gavana @ 06 Oct 2006, 18.10 GMT
+
+@undocumented: NC_MAC*, topaqua*, botaqua*, distaqua*, disbaqua*,
+    kdetheme, silvertheme*, wxEVT*, attrs, GetMenuButton*, NCDragInfo,
+    NCDropTarget, TabbedPage, TabCtrl, TransientTipWindow,
+    macPopupWindow, macTransientTipWindow, NCFrame, DEFAULT_SIZE,
+    NotebookSpinButton, NotebookMenuButton
+"""
+
+__docformat__ = "epytext"
+
+
+#----------------------------------------------------------------------
+# Beginning Of NOTEBOOKCTRL wxPython Code
+#----------------------------------------------------------------------
+
+from orpg.orpg_wx import *
+from wx.lib.buttons import GenBitmapButton as BitmapButton
+import wx.xrc  as  xrc
+
+import cStringIO, zlib
+import cPickle
+import weakref
+
+# HitTest Results
+NC_HITTEST_NOWHERE = 0   # Not On Tab
+"""Indicates mouse coordinates not on any tab of the notebook"""
+NC_HITTEST_ONICON  = 1   # On Icon
+"""Indicates mouse coordinates on an icon in a tab of the notebook"""
+NC_HITTEST_ONLABEL = 2   # On Label
+"""Indicates mouse coordinates on a label in a tab of the notebook"""
+NC_HITTEST_ONITEM  = 4   # Generic, On Item
+"""Indicates mouse coordinates on a tab of the notebook"""
+NC_HITTEST_ONX = 8       # On Small Square On Every Page
+"""Indicates mouse coordinates on the closing I{X} in a tab of the notebook"""
+
+# NotebookCtrl Styles
+# NotebookCtrl Placed On Top (Default)
+NC_TOP = 1
+"""Specify tabs at the top of the notebook control."""
+# NotebookCtrl Placed At The Bottom
+NC_BOTTOM = 2
+"""Specify tabs at the bottom of the notebook control."""
+# NotebookCtrl With Fixed Width Tabs
+NC_FIXED_WIDTH = 4
+"""Specify tabs of a fixed width in the notebook control."""
+# NotebookCtrl Placed At The Left
+NC_LEFT = 8
+"""Specify tabs on the left side of the notebook control."""
+# NotebookCtrl Placed At The Right
+NC_RIGHT = 16
+"""Specify tabs on the right side of the notebook control."""
+# NotebookCtrl tab rotated
+NC_ROTATE = 32
+"""Specify rotated tabs (with vertical text) in the notebook control."""
+# NotebookCtrl switchable between compact and expanded sizes
+NC_EXPANDABLE = 64
+"""Specify that the notebook control includes a toggle button to
+switch between compact tabs (rotated on the left or right side)
+expanded tabs (horizontal on the left or right side)."""
+
+NC_DEFAULT_STYLE = NC_TOP | wx.NO_BORDER
+"""The default style for the notebook control (tabs on top with no border)"""
+# Also wx.STATIC_BORDER Is Supported
+
+# NotebookCtrl theme styles
+NC_GRADIENT_VERTICAL = 1
+"""Specify tabs rendered with a vertical gradient background."""
+NC_GRADIENT_HORIZONTAL = 2
+"""Specify tabs rendered with a horizontal gradient background."""
+NC_GRADIENT_SELECTION = 4
+NC_AQUA_LIGHT = 8
+"""Specify tabs rendered with a Mac I{Light Aqua}-like background."""
+NC_AQUA_DARK = 16
+"""Specify tabs rendered with a Mac I{Dark Aqua}-like background."""
+NC_AQUA = NC_AQUA_LIGHT
+"""Specify tabs rendered with a Mac I{Light Aqua}-like background."""
+NC_METAL = 32
+"""Specify tabs rendered with a Mac I{Metal}-like background."""
+NC_SILVER = 64
+"""Specify tabs rendered with a Windows I{Silver}-like background."""
+NC_KDE = 128
+"""Specify tabs rendered with a KDE-style background."""
+
+# Patch To Make NotebookCtrl Working Also On MacOS: Thanks To Stani ;-)
+if wx.Platform == '__WXMAC__':
+    DEFAULT_SIZE = wx.Size(26, 26)
+else:
+    DEFAULT_SIZE = wx.DefaultSize
+
+# Themes On Mac... This May Slow Down The Paint Event If You Turn It On!
+NC_MAC_LIGHT = (240, 236)
+NC_MAC_DARK = (232, 228)
+
+topaqua1 = [wx.Colour(106, 152, 231), wx.Colour(124, 173, 236)]
+botaqua1 = [wx.Colour(54, 128, 213), wx.Colour(130, 225, 249)]
+
+topaqua2 = [wx.Colour(176, 222, 251), wx.Colour(166, 211, 245)]
+botaqua2 = [wx.Colour(120, 182, 244), wx.Colour(162, 230, 245)]
+
+distaqua = [wx.Colour(248, 248, 248), wx.Colour(243, 243, 243)]
+disbaqua = [wx.Colour(219, 219, 219), wx.Colour(248, 248, 248)]
+
+# Themes On KDE... This May Slow Down The Paint Event If You Turn It On!
+kdetheme = [wx.Colour(0xf3,0xf7,0xf9), wx.Colour(0xf3,0xf7,0xf9),
+            wx.Colour(0xee,0xf3,0xf7), wx.Colour(0xee,0xf3,0xf7),
+            wx.Colour(0xea,0xf0,0xf4), wx.Colour(0xea,0xf0,0xf4),
+            wx.Colour(0xe6,0xec,0xf1), wx.Colour(0xe6,0xec,0xf1),
+            wx.Colour(0xe2,0xe9,0xef), wx.Colour(0xe2,0xe9,0xef),
+            wx.Colour(0xdd,0xe5,0xec), wx.Colour(0xdd,0xe5,0xec),
+            wx.Colour(0xd9,0xe2,0xea), wx.Colour(0xd9,0xe2,0xea)]
+
+# Themes On Windows... This May Slow Down The Paint Event If You Turn It On!
+silvertheme2 = [wx.Colour(255, 255, 255), wx.Colour(190, 190, 216),
+                wx.Colour(180, 180, 200)]
+silvertheme1 = [wx.Colour(252, 252, 254), wx.Colour(252, 252, 254)]
+
+# NotebookCtrl Events:
+# wxEVT_NOTEBOOKCTRL_PAGE_CHANGED: Event Fired When You Switch Page;
+# wxEVT_NOTEBOOKCTRL_PAGE_CHANGING: Event Fired When You Are About To Switch
+# Pages, But You Can Still "Veto" The Page Changing By Avoiding To Call
+# event.Skip() In Your Event Handler;
+# wxEVT_NOTEBOOKCTRL_PAGE_CLOSING: Event Fired When A Page Is Closing, But
+# You Can Still "Veto" The Page Changing By Avoiding To Call event.Skip()
+# In Your Event Handler;
+# wxEVT_NOTEBOOKCTRL_PAGE_DND: Event Fired When A Drag And Drop Action On
+# Tabs Ends.
+wxEVT_NOTEBOOKCTRL_PAGE_CHANGED = wx.NewEventType()
+wxEVT_NOTEBOOKCTRL_PAGE_CHANGING = wx.NewEventType()
+wxEVT_NOTEBOOKCTRL_PAGE_CLOSING = wx.NewEventType()
+wxEVT_NOTEBOOKCTRL_PAGE_DND = wx.NewEventType()
+wxEVT_NOTEBOOKCTRL_PAGE_DCLICK = wx.NewEventType()
+wxEVT_NOTEBOOKCTRL_PAGE_RIGHT = wx.NewEventType()
+wxEVT_NOTEBOOKCTRL_PAGE_MIDDLE = wx.NewEventType()
+
+#-----------------------------------#
+#        NotebookCtrlEvent
+#-----------------------------------#
+
+EVT_NOTEBOOKCTRL_PAGE_CHANGED = wx.PyEventBinder(wxEVT_NOTEBOOKCTRL_PAGE_CHANGED, 1)
+"""Notify client objects when the active page in the notebook control
+has changed."""
+
+EVT_NOTEBOOKCTRL_PAGE_CHANGING = wx.PyEventBinder(wxEVT_NOTEBOOKCTRL_PAGE_CHANGING, 1)
+"""Notify client objects when the active page in the notebook control
+is changing."""
+
+EVT_NOTEBOOKCTRL_PAGE_CLOSING = wx.PyEventBinder(wxEVT_NOTEBOOKCTRL_PAGE_CLOSING, 1)
+"""Notify client objects when a page in the notebook control is closing."""
+
+EVT_NOTEBOOKCTRL_PAGE_DND = wx.PyEventBinder(wxEVT_NOTEBOOKCTRL_PAGE_DND, 1)
+"""Enable client objects to override the behavior of the notebook control
+when a dragged tab is dropped onto it."""
+
+EVT_NOTEBOOKCTRL_PAGE_DCLICK = wx.PyEventBinder(wxEVT_NOTEBOOKCTRL_PAGE_DCLICK, 1)
+"""Notify client objects when the user double-clicks a tab
+in the notebook control."""
+
+EVT_NOTEBOOKCTRL_PAGE_RIGHT = wx.PyEventBinder(wxEVT_NOTEBOOKCTRL_PAGE_RIGHT, 1)
+"""Notify client objects when the user right-clicks a tab
+in the notebook control."""
+
+EVT_NOTEBOOKCTRL_PAGE_MIDDLE = wx.PyEventBinder(wxEVT_NOTEBOOKCTRL_PAGE_MIDDLE, 1)
+"""Notify client objects when the user clicks with the
+middle mouse button on a tab in the notebook control."""
+
+attrs = ["_backstyle", "_backtooltip", "_borderpen", "_convertimage", "_drawx",
+         "_drawxstyle", "_enabledragging", "_focusindpen", "_hideonsingletab",
+         "_highlight", "_padding", "_selectioncolour", "_selstyle", "_tabstyle",
+         "_upperhigh", "_usefocus", "_usegradients"]
+
+
+# Check for the new method in 2.7 (not present in 2.6.3.3)
+if wx.VERSION_STRING < "2.7":
+    wx.Rect.Contains = lambda self, point: wx.Rect.Inside(self, point)
+
+
+# ---------------------------------------------------------------------------- #
+def GetMenuButtonData():
+
+    return zlib.decompress(
+"x\xda\xeb\x0c\xf0s\xe7\xe5\x92\xe2b``\xe0\xf5\xf4p\t\x02\xd2\x9c \xcc\xc1\
+\x06$\x1fLd\x13\x00R,\xc5N\x9e!\x1c@P\xc3\x91\xd2\x01\xe4[x\xba8\x86HL\xed\
+\xbd`\xc8\xc7\xa0\xc0\xe1|q\xdb\x9d\xff'\xba\xb4\x1d\x05v\xff}\xe2\xab\x9a:c\
+\x99\xc4\xbe\xe9\xfd+\x9a\xc5W%t\x1a\xe5\x08\xa6\xd6,\xe2\xf0\x9a\xc2\xc8\
+\xf0\xe1\xf9r\xe6\xa3\xc9\x02b\xd9\x0c35\x80f0x\xba\xfa\xb9\xacsJh\x02\x00\
+\xcd-%1")
+
+
+def GetMenuButtonBitmap():
+
+    return wx.BitmapFromImage(GetMenuButtonImage())
+
+
+def GetMenuButtonImage():
+
+    stream = cStringIO.StringIO(GetMenuButtonData())
+    return wx.ImageFromStream(stream)
+
+# ---------------------------------------------------------------------------- #
+
+def GrayOut(anImage):
+    """
+    Convert The Given Image (In Place) To A Grayed-Out Version,
+    Appropriate For A 'Disabled' Appearance.
+    """
+
+    factor = 0.7        # 0 < f < 1.  Higher Is Grayer
+
+    if anImage.HasMask():
+        maskColor = (anImage.GetMaskRed(), anImage.GetMaskGreen(), anImage.GetMaskBlue())
+    else:
+        maskColor = None
+
+    data = map(ord, list(anImage.GetData()))
+
+    for i in range(0, len(data), 3):
+
+        pixel = (data[i], data[i+1], data[i+2])
+        pixel = MakeGray(pixel, factor, maskColor)
+
+        for x in range(3):
+            data[i+x] = pixel[x]
+
+    anImage.SetData(''.join(map(chr, data)))
+
+    return anImage
+
+
+def MakeGray((r,g,b), factor, maskColor):
+    """
+    Make A Pixel Grayed-Out. If The Pixel Matches The MaskColor, It Won't Be
+    Changed.
+    """
+
+    if (r,g,b) != maskColor:
+        return map(lambda x: int((230 - x) * factor) + x, (r,g,b))
+    else:
+        return (r,g,b)
+
+def GetDefaultTabStyle():
+    tabstyle = ThemeStyle()
+
+    # Draw Mac Themes On Tabs?
+    if wx.Platform == "__WXMAC__" or wx.Platform == "__WXCOCOA__":
+        tabstyle.EnableAquaTheme(True, 2)
+    # Draw Windows Silver Theme On Tabs?
+    elif wx.Platform == "__WXMSW__":
+        tabstyle.EnableSilverTheme(True)
+    else:
+        tabstyle.EnableKDETheme(True)
+
+    return tabstyle
+
+# ---------------------------------------------------------------------------- #
+# Class NotebookCtrlEvent
+# ---------------------------------------------------------------------------- #
+
+class NotebookCtrlEvent(wx.PyCommandEvent):
+    """
+    Represent details of the events that the L{NotebookCtrl} object sends.
+    """
+
+    def __init__(self, eventType, id=1, nSel=-1, nOldSel=-1):
+        """ Default Class Constructor. """
+
+        wx.PyCommandEvent.__init__(self, eventType, id)
+        self._eventType = eventType
+
+
+    def SetSelection(self, nSel):
+        """ Sets Event Selection. """
+
+        self._selection = nSel
+
+
+    def SetOldSelection(self, nOldSel):
+        """ Sets Old Event Selection. """
+
+        self._oldselection = nOldSel
+
+
+    def GetSelection(self):
+        """ Returns Event Selection. """
+
+        return self._selection
+
+
+    def GetOldSelection(self):
+        """ Returns Old Event Selection """
+
+        return self._oldselection
+
+
+    def SetOldPosition(self, pos):
+        """ Sets Old Event Position. """
+
+        self._oldposition = pos
+
+
+    def SetNewPosition(self, pos):
+        """ Sets New Event Position. """
+
+        self._newposition = pos
+
+
+    def GetOldPosition(self):
+        """ Returns Old Event Position. """
+
+        return self._oldposition
+
+
+    def GetNewPosition(self):
+        """ Returns New Event Position. """
+
+        return self._newposition
+
+
+# ---------------------------------------------------------------------------- #
+# Class NCDragInfo
+# Stores All The Information To Allow Drag And Drop Between Different
+# NotebookCtrls In The Same Application.
+# ---------------------------------------------------------------------------- #
+
+class NCDragInfo:
+
+    _map = weakref.WeakValueDictionary()
+
+    def __init__(self, container, pageindex):
+        """ Default Class Constructor. """
+
+        self._id = id(container)
+        NCDragInfo._map[self._id] = container
+        self._pageindex = pageindex
+
+
+    def GetContainer(self):
+        """ Returns The NotebookCtrl Page (Usually A Panel). """
+
+        return NCDragInfo._map.get(self._id, None)
+
+
+    def GetPageIndex(self):
+        """ Returns The Page Index Associated With A Page. """
+
+        return self._pageindex
+
+
+# ---------------------------------------------------------------------------- #
+# Class NCDropTarget
+# Simply Used To Handle The OnDrop() Method When Dragging And Dropping Between
+# Different NotebookCtrls.
+# ---------------------------------------------------------------------------- #
+
+class NCDropTarget(wx.DropTarget):
+
+    def __init__(self, parent):
+        """ Default Class Constructor. """
+
+        wx.DropTarget.__init__(self)
+
+        self._parent = parent
+        self._dataobject = wx.CustomDataObject(wx.CustomDataFormat("NotebookCtrl"))
+        self.SetDataObject(self._dataobject)
+
+
+    def OnData(self, x, y, dragres):
+        """ Handles The OnData() Method TO Call The Real DnD Routine. """
+
+        if not self.GetData():
+            return wx.DragNone
+
+        draginfo = self._dataobject.GetData()
+        drginfo = cPickle.loads(draginfo)
+
+        return self._parent.OnDropTarget(x, y, drginfo.GetPageIndex(), drginfo.GetContainer())
+
+
+# ---------------------------------------------------------------------------- #
+# Class ThemeStyle. Used To Define A Custom Style For Tabs And Control
+# Background Colour.
+# ---------------------------------------------------------------------------- #
+
+class ThemeStyle:
+    """
+    Represent the style for rendering a notebook tab.
+    """
+
+    GRADIENT_VERTICAL = 1
+    GRADIENT_HORIZONTAL = 2
+    DIFFERENT_GRADIENT_FOR_SELECTED = 4
+
+    def __init__(self):
+        """ Default Constructor For This Class."""
+
+        self.ResetDefaults()
+
+
+    def ResetDefaults(self):
+        """ Resets Default Theme. """
+
+        self._normal = True
+        self._aqua = False
+        self._metal = False
+        self._macstyle = False
+        self._kdetheme = False
+        self._silver = False
+        self._gradient = False
+        self._firstcolour = wx.WHITE
+        self._secondcolour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE)
+        self._firstcolourselected = wx.WHITE
+        self._secondcolourselected = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE)
+
+
+    def EnableMacTheme(self, enable=True, style=1):
+        """
+        Enables/Disables Mac Themes. style=1 Is The Light Style, While style=2
+        Is The Dark Style. Mainly Used For Control Background Colour, Not For Tabs.
+        """
+
+        if enable:
+            self._normal = False
+            self._macstyle = style
+            self._kdetheme = False
+            self._metal = False
+            self._aqua = False
+            self._silver = False
+            self._gradient = False
+        else:
+            self._macstyle = 0
+
+
+    def EnableKDETheme(self, enable=True):
+        """ Globally Enables/Disables Unix-Like KDE Theme For Tabs. """
+
+        self._kdetheme = enable
+
+        if enable:
+            self._normal = False
+            self._macstyle = False
+            self._metal = False
+            self._aqua = False
+            self._silver = False
+            self._gradient = False
+
+
+    def EnableMetalTheme(self, enable=True):
+        """ Globally Enables/Disables Mac-Like Metal Theme For Tabs. """
+
+        self._metal = enable
+
+        if enable:
+            self._normal = False
+            self._macstyle = False
+            self._kdetheme = False
+            self._aqua = False
+            self._silver = False
+            self._gradient = False
+
+
+    def EnableAquaTheme(self, enable=True, style=1):
+        """ Globally Enables/Disables Mac-Like Aqua Theme For Tabs. """
+
+        if enable:
+            self._aqua = style
+            self._normal = False
+            self._macstyle = False
+            self._kdetheme = False
+            self._metal = False
+            self._silver = False
+            self._gradient = False
+        else:
+            self._aqua = 0
+
+
+    def EnableSilverTheme(self, enable=True):
+        """ Globally Enables/Disables Windows Silver Theme For Tabs. """
+
+        self._silver = enable
+
+        if enable:
+            self._normal = False
+            self._macstyle = False
+            self._kdetheme = False
+            self._metal = False
+            self._aqua = False
+            self._gradient = False
+
+
+    def EnableGradientStyle(self, enable=True, style=1):
+        """
+        Enables/Disables Gradient Drawing On Tabs. style=1 Is The Vertical Gradient,
+        While style=2 Is The Horizontal Gradient.
+        If style flag 4 is set, the style has a separate set of colors for the
+        selected tab.
+        """
+
+        if enable:
+            self._normal = False
+            if style & self.GRADIENT_VERTICAL == 0 and style & self.GRADIENT_HORIZONTAL == 0:
+                style |= self.GRADIENT_VERTICAL
+            self._gradient = style
+            self._macstyle = False
+            self._kdetheme = False
+            self._metal = False
+            self._aqua = False
+            self._silver = False
+        else:
+            self._gradient = 0
+
+
+    def SetFirstGradientColour(self, colour=None):
+        """ Sets The First Gradient Colour. """
+
+        if colour is None:
+            colour = wx.WHITE
+
+        self._firstcolour = colour
+
+    def SetFirstGradientColourSelected(self, colour=None):
+        """Sets The First Gradient Colour For The Selected Tab."""
+        if colour is None:
+            colour = wx.WHITE
+
+        self._firstcolourselected = colour
+
+    def SetSecondGradientColour(self, colour=None):
+        """ Sets The Second Gradient Colour. """
+
+        if colour is None:
+            color = self.GetBackgroundColour()
+            r, g, b = int(color.Red()), int(color.Green()), int(color.Blue())
+            color = ((r >> 1) + 20, (g >> 1) + 20, (b >> 1) + 20)
+            colour = wx.Colour(color[0], color[1], color[2])
+
+        self._secondcolour = colour
+
+    def SetSecondGradientColourSelected(self, colour=None):
+        """ Sets The Second Gradient Colour For The Selected Tab. """
+
+        if colour is None:
+            color = self.GetBackgroundColour()
+            r, g, b = int(color.Red()), int(color.Green()), int(color.Blue())
+            color = ((r >> 1) + 20, (g >> 1) + 20, (b >> 1) + 20)
+            colour = wx.Colour(color[0], color[1], color[2])
+
+        self._secondcolourselected = colour
+
+    def GetFirstGradientColour(self, selected=False):
+        """ Returns The First Gradient Colour. """
+
+        if selected and self._gradient & self.DIFFERENT_GRADIENT_FOR_SELECTED:
+            return self._firstcolourselected
+        else:
+            return self._firstcolour
+
+    def GetSecondGradientColour(self, selected=False):
+        """ Returns The Second Gradient Colour. """
+
+        if selected and self._gradient & self.DIFFERENT_GRADIENT_FOR_SELECTED:
+            return self._secondcolourselected
+        else:
+            return self._secondcolour
+
+
+# ---------------------------------------------------------------------------- #
+# Class TabbedPage
+# This Is Just A Container Class That Initialize All The Default Settings For
+# Every Tab.
+# ---------------------------------------------------------------------------- #
+
+class TabbedPage:
+
+    def __init__(self, text="", image=-1, hidden=False):
+        """ Default Class Constructor. """
+
+        self._text = text
+        self._image = image
+        self._font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        self._secondaryfont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        self._pagetextcolour = wx.BLACK
+        self._pagecolour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE)
+        self._enable = True
+        self._animationimages = []
+        self._tooltip = ""
+        self._tooltiptime = 500
+        self._winsize = 400
+        self._menu = None
+        self._ishidden = hidden
+        self._firstcolour = color = wx.WHITE
+        r, g, b = int(color.Red()), int(color.Green()), int(color.Blue())
+        color = ((r >> 1) + 20, (g >> 1) + 20, (b >> 1) + 20)
+        colour = wx.Colour(color[0], color[1], color[2])
+        self._secondcolour = colour
+
+
+# ---------------------------------------------------------------------------- #
+# Class NotebookSpinButton
+# This SpinButton Is Created/Shown Only When The Total Tabs Size Exceed The
+# Client Size, Allowing The User To Navigate Between Tabs By Clicking On The
+# SpinButton. It Is Very Similar To The wx.Notebook SpinButton
+# ---------------------------------------------------------------------------- #
+
+class NotebookSpinButton(wx.SpinButton):
+
+    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
+                 size=wx.DefaultSize, style=wx.SP_HORIZONTAL):
+        """ Default Class Constructor. """
+
+        wx.SpinButton.__init__(self, parent, id, pos, size, style)
+        self._nb = parent
+        self._oldvalue = 0
+        self._style = style
+        self.Bind(wx.EVT_SPIN, self.OnSpin)
+
+    def GetValue(self):
+        result = super(NotebookSpinButton, self).GetValue()
+        if self._style & wx.SP_VERTICAL:
+            result = -result
+        return result
+
+    def OnSpin(self, event):
+        """ Handles The User's Clicks On The SpinButton. """
+
+        if type(event) != type(1):
+            pos = event.GetPosition()
+        else:
+            pos = event
+
+        if pos < self.GetMin():
+            self.SetValue(self.GetMin())
+            return
+
+        if type(event) != type(1):
+            if self._nb._enablehiding:
+                if pos < self._oldvalue:
+                    incr = -1
+                else:
+                    incr = 1
+                while self._nb._pages[pos]._ishidden:
+                    pos = pos + incr
+
+            self.SetValue(pos)
+
+        if self._nb.IsLastVisible():
+            if (self._style & wx.SP_HORIZONTAL and self._oldvalue < pos) or \
+                (self._style & wx.SP_VERTICAL and self._oldvalue > pos):
+                self.SetValue(self._oldvalue)
+                return
+
+        self._oldvalue = pos
+
+        self._nb.Refresh()
+
+
+# ---------------------------------------------------------------------------- #
+# Class NotebookMenuButton
+# This MenuButton Is Created/Shown Only When You Activate The Option EnableHiding
+# Of NotebookCtrl. This Small Button Will Be Shown Right Above The Spin Button
+# (If Present), Or In The Position Of The Spin Button.
+# ---------------------------------------------------------------------------- #
+
+class NotebookMenuButton(BitmapButton):
+
+    def __init__(self, parent, id=-1, pos=wx.DefaultPosition, size=(15, 11),
+                 style=0):
+        """ Default Class Constructor. """
+
+        bmp = GetMenuButtonBitmap()
+
+        BitmapButton.__init__(self, parent, id, bmp, pos, size, style)
+
+        self.SetUseFocusIndicator(False)
+        self.SetBezelWidth(1)
+
+        self._originalcolour = self.GetBackgroundColour()
+        self._nb = parent
+
+        self.Bind(wx.EVT_BUTTON, self.OnButton)
+        self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnterWindow)
+        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
+        self.Bind(wx.EVT_MENU, self.OnMenu)
+
+
+    def OnButton(self, event):
+        """ Handles The wx.EVT_BUTTON For NotebookMenuButton (Opens The wx.PopupMenu) """
+
+        count = self._nb.GetPageCount()
+
+        if count <= 0:
+            return
+
+        menu = wx.Menu()
+        id = wx.NewId()
+        myids = []
+
+        for ii in xrange(count):
+
+            id = id + 1
+            myids.append(id)
+            name = self._nb.GetPageText(ii)
+
+            if self._nb._pages[ii]._ishidden:
+                msg = "Page Hidden"
+                check = False
+            else:
+                msg = "Page Shown"
+                check = True
+
+            item = wx.MenuItem(menu, id, name, msg, wx.ITEM_CHECK)
+            menu.AppendItem(item)
+
+            if not self._nb._pages[ii]._ishidden:
+                item.Check()
+
+            menu.SetHelpString(id, msg)
+
+        self._myids = myids
+
+        self.PopupMenu(menu)
+
+        event.Skip()
+
+
+    def OnMenu(self, event):
+        """ Handles The wx.EVT_MENU For NotebookMenuButton. Calls HideTab(). """
+
+        indx = self._myids.index(event.GetId())
+        checked = not event.GetEventObject().IsChecked(event.GetId())
+
+        self._nb.HideTab(indx, not checked)
+
+        event.Skip()
+
+
+    def OnEnterWindow(self, event):
+        """
+        Changes The NotebookMenuButton Background Colour When The Mouse
+        Enters The Button Region.
+        """
+
+        entercolour = self.GetBackgroundColour()
+        firstcolour  = entercolour.Red()
+        secondcolour = entercolour.Green()
+        thirdcolour = entercolour.Blue()
+
+        if entercolour.Red() > 235:
+            firstcolour = entercolour.Red() - 40
+        if entercolour.Green() > 235:
+            secondcolour = entercolour.Green() - 40
+        if entercolour.Blue() > 235:
+            thirdcolour = entercolour.Blue() - 40
+
+        entercolour = wx.Colour(firstcolour+20, secondcolour+20, thirdcolour+20)
+
+        self.SetBackgroundColour(entercolour)
+        self.Refresh()
+
+        event.Skip()
+
+
+    def OnLeaveWindow(self, event):
+        """
+        Restore The NotebookMenuButton Background Colour When The Mouse
+        Leaves The Button Region.
+        """
+
+        self.SetBackgroundColour(self._originalcolour)
+        self.Refresh()
+
+        event.Skip()
+
+class _TabCtrlPaintTools(object):
+    # Structure-like object for passing data among
+    # private rendering methods
+    def __init__(self, backBrush, backPen, borderPen, highlightPen,
+        shadowPen, upperHighlightPen, selectionPen, selectionEdgePen,
+        xPen, focusPen):
+        self.BackBrush = backBrush
+        self.BackPen = backPen
+        self.BorderPen = borderPen
+        self.HighlightPen = highlightPen
+        self.ShadowPen = shadowPen
+        self.UpperHighlightPen = upperHighlightPen
+        self.SelectionPen = selectionPen
+        self.SelectionEdgePen = selectionEdgePen
+        self.XPen = xPen
+        self.FocusPen = focusPen
+
+
+# ---------------------------------------------------------------------------- #
+# Class TabCtrl
+# This Class Handles The Drawing Of Every Tab In The NotebookCtrl, And Also
+# All Settings/Methods For Every Tab.
+# ---------------------------------------------------------------------------- #
+
+class TabCtrl(wx.PyControl):
+
+    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
+                 size=DEFAULT_SIZE, style=NC_DEFAULT_STYLE,
+                 validator=wx.DefaultValidator, name="TabCtrl"):
+        """
+        Default Class Constructor.
+        Used Internally. Do Not Call It Explicitely!
+        """
+
+        wx.PyControl.__init__(self, parent, id, pos, size, wx.NO_BORDER | wx.WANTS_CHARS,
+                              validator, name)
+
+        # Set All The Default Parameters For TabCtrl
+        self._selection = -1
+        self._imglist = 0
+        self._style = style
+        self._expanded = False
+        self._pages = []
+        self._enabledpages = []
+
+        self._padding = wx.Point(8, 4)
+        self._spacetabs = 2
+        self._xrect = []
+        self._xrefreshed = False
+        self._imageconverted = False
+        self._convertimage = False
+        self._disabledcolour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_GRAYTEXT)
+
+        self._hover = False
+        self._parent = parent
+        self._firsttime = True
+        self._somethingchanged = True
+        self._isdragging = False
+        self._tabID = -1
+        self._enabledragging = False
+        self._olddragpos = -1
+        self._fromdnd = False
+        self._isleaving = False
+        self._highlight = False
+        self._usefocus = True
+        self._hideonsingletab = False
+        self._selectioncolour = wx.Colour(255, 200, 60)
+        self._selectionedgecolour = wx.Colour(230, 139, 44)
+
+        self._tabstyle = ThemeStyle()
+        self._backstyle = ThemeStyle()
+        self._selstyle = ThemeStyle()
+        self._usegradients = False
+
+        self._insidetab = -1
+        self._showtooltip = False
+        self._istooltipshown = False
+        self._tipwindow = None
+        self._tiptimer = wx.PyTimer(self.OnShowToolTip)
+        self._backtooltip = wx.Colour(255, 255, 230)
+        self._xvideo = wx.SystemSettings_GetMetric(wx.SYS_SCREEN_X)
+        self._yvideo = wx.SystemSettings_GetMetric(wx.SYS_SCREEN_Y)
+
+        self._selectedtabs = []
+
+        self._timers = []
+
+        self._dragcursor = wx.StockCursor(wx.CURSOR_HAND)
+        self._dragstartpos = wx.Point()
+
+        self._drawx = False
+        self._drawxstyle = 1
+
+        self._pmenu = None
+
+        self._enablehiding = False
+        if (style & NC_LEFT or style & NC_RIGHT) and style & NC_EXPANDABLE:
+            self._InitExpandableStyles(style)
+            self._InitExpandableTabStyles(self._style, self._expanded, self._tabstyle)
+            self._CreateSizeToggleButton()
+        else:
+            self._sizeToggleButton = None
+
+        self.SetDefaultPage()
+
+        if style & NC_TOP or style & NC_BOTTOM:
+            self.SetBestSize((-1, 28))
+            self._firsttabpos = wx.Point(3, 0)
+        else:
+            self.SetBestSize((28, -1))
+            self._firsttabpos = wx.Point(0, 3 + self._CalcSizeToggleBestSize()[1])
+
+        self._borderpen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))
+        self._highlightpen2 = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
+        self._highlightpen = wx.Pen((145, 167, 180))
+        self._upperhigh = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
+        self._shadowpen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DDKSHADOW), 2)
+        self._shadowpen.SetCap(wx.CAP_BUTT)
+        self._highlightpen.SetCap(wx.CAP_BUTT)
+        self._highlightpen2.SetCap(wx.CAP_BUTT)
+
+        if wx.Platform == "__WXMAC__":
+            self._focusindpen = wx.Pen(wx.BLACK, 1, wx.SOLID)
+        else:
+            self._focusindpen = wx.Pen(wx.BLACK, 1, wx.USER_DASH)
+            self._focusindpen.SetDashes([1,1])
+            self._focusindpen.SetCap(wx.CAP_BUTT)
+
+        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
+        self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseLeftDClick)
+        self.Bind(wx.EVT_MOTION, self.OnMouseMotion)
+        self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
+        self.Bind(wx.EVT_RIGHT_UP, self.OnMouseRightUp)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
+        self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMouseMiddleDown)
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+
+        self.Bind(wx.EVT_PAINT, self.OnPaint)
+        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
+
+        self.Bind(wx.EVT_TIMER, self.AnimateTab)
+        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
+        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
+
+        self._droptarget = NCDropTarget(self)
+        self.SetDropTarget(self._droptarget)
+
+    def Contract(self):
+        if self._style & NC_EXPANDABLE and self._expanded:
+            self._ToggleSize()
+
+    def Expand(self):
+        if self._style & NC_EXPANDABLE and not self._expanded:
+            self._ToggleSize()
+
+    def _ToggleSize(self, event=None):
+        if self._style & NC_EXPANDABLE and not self._expanded:
+            # contract
+            self._style = self._expandedstyle
+            self._tabstyle = self._expandedtabstyle
+            self._expanded = True
+            self._sizeToggleButton.SetLabel("<<")
+        elif self._style & NC_EXPANDABLE and self._expanded:
+            # expand
+            self._style = self._contractedstyle
+            self._tabstyle = self._contractedtabstyle
+            self._expanded = False
+            self._sizeToggleButton.SetLabel(">>")
+        self._OnStyleChange()
+
+
+    def OnDropTarget(self, x, y, nPage, oldcont):
+        """ Handles The OnDrop Action For Drag And Drop Between Different NotebookCtrl. """
+
+        where = self.HitTest(wx.Point(x, y))
+
+        oldNotebook = oldcont.GetParent()
+        newNotebook = self.GetParent()
+
+        if oldNotebook == newNotebook:
+            if where >= 0 and where != self._tabID:
+
+                self._isdragging = False
+                self._olddragpos = -1
+                eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_DND, self.GetId())
+                eventOut.SetOldPosition(self._tabID)
+                eventOut.SetNewPosition(where)
+                eventOut.SetEventObject(self)
+
+                if self.GetEventHandler().ProcessEvent(eventOut):
+                    self._tabID = -1
+                    self._olddragpos = -1
+                    self.SetCursor(wx.STANDARD_CURSOR)
+                    self.Refresh()
+                    return
+
+                self._parent.Freeze()
+
+                try:
+                    text = self.GetPageText(self._tabID)
+                    image = self.GetPageImage(self._tabID)
+                    font1 = self.GetPageTextFont(self._tabID)
+                    font2 = self.GetPageTextSecondaryFont(self._tabID)
+                    fontcolour = self.GetPageTextColour(self._tabID)
+                    pagecolour = self.GetPageColour(self._tabID)
+                    enabled = self.IsPageEnabled(self._tabID)
+                    tooltip, ontime, winsize = self.GetPageToolTip(self._tabID)
+                    menu = self.GetPagePopupMenu(self._tabID)
+                    firstcol = self.GetPageFirstGradientColour(self._tabID)
+                    secondcol = self.GetPageSecondGradientColour(self._tabID)
+                    ishidden = self._pages[self._tabID]._ishidden
+                except:
+                    self._parent.Thaw()
+                    self._tabID = -1
+                    self.SetCursor(wx.STANDARD_CURSOR)
+                    return
+
+                isanimated = 0
+                if self._timers[self._tabID].IsRunning():
+                    isanimated = 1
+                    timer = self._timers[self._tabID].GetInterval()
+
+                self.StopAnimation(self._tabID)
+                animatedimages = self.GetAnimationImages(self._tabID)
+
+                pagerange = range(self.GetPageCount())
+
+                newrange = pagerange[:]
+                newrange.remove(self._tabID)
+                newrange.insert(where, self._tabID)
+
+                newpages = []
+                counter = self.GetPageCount() - 1
+
+                for ii in xrange(self.GetPageCount()):
+                    newpages.append(self._parent.GetPage(ii))
+                    self._parent.bsizer.Detach(counter-ii)
+
+                cc = 0
+
+                self._parent._notebookpages = []
+
+                for jj in newrange:
+                    self._parent.bsizer.Add(newpages[jj], 1, wx.EXPAND | wx.ALL, 2)
+                    self._parent.bsizer.Show(cc, False)
+                    self._parent._notebookpages.append(newpages[jj])
+                    cc = cc + 1
+
+                self.DeletePage(self._tabID)
+
+                if enabled:
+                    if id == self.GetPageCount():
+                        self.AddPage(text, True, image)
+                    else:
+                        self.InsertPage(where, text, True, image)
+                else:
+                    if id == self.GetPageCount():
+                        self.AddPage(text, False, image)
+                    else:
+                        self.InsertPage(where, text, False, image)
+
+                self.SetPageImage(where, image)
+                self.SetPageText(where, text)
+                self.SetPageTextFont(where, font1)
+                self.SetPageTextSecondaryFont(where, font2)
+                self.SetPageTextColour(where, fontcolour)
+                self.SetPageColour(where, pagecolour)
+                self.EnablePage(where, enabled)
+                self.SetPageToolTip(where, tooltip, ontime, winsize)
+                self.SetPagePopupMenu(where, menu)
+                self.SetPageFirstGradientColour(where, firstcol)
+                self.SetPageSecondGradientColour(where, secondcol)
+                self._pages[where]._ishidden = ishidden
+
+                if isanimated and len(animatedimages) > 1:
+                    self.SetAnimationImages(where, animatedimages)
+                    self.StartAnimation(where, timer)
+
+                if enabled:
+                    self._parent.bsizer.Show(where, True)
+                else:
+                    sel = self.GetSelection()
+
+                    if sel == -1:
+                        sel = 0
+                    self._parent.bsizer.Show(where, False)
+                    self._parent.SetSelection(sel)
+                    self._parent.bsizer.Show(sel, True)
+
+                self._parent.bsizer.Layout()
+
+                self._parent.Thaw()
+
+            self._isdragging = False
+            self._olddragpos = -1
+            self._fromdnd = True
+            self.Refresh()
+            self._tabID = -1
+            self.SetCursor(wx.STANDARD_CURSOR)
+
+            return
+
+        if nPage >= 0 and where >= 0:
+            panel = oldNotebook.GetPage(nPage)
+
+            if panel:
+                eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_DND, oldNotebook.GetId())
+                eventOut.SetOldPosition(nPage)
+                eventOut.SetNewPosition(where)
+                eventOut.SetEventObject(oldNotebook)
+
+                if oldNotebook.GetEventHandler().ProcessEvent(eventOut):
+                    oldNotebook.nb._tabID = -1
+                    oldNotebook.nb._olddragpos = -1
+                    oldNotebook.SetCursor(wx.STANDARD_CURSOR)
+                    oldNotebook.Refresh()
+                    return
+
+                oldNotebook.Freeze()
+                infos = oldNotebook.GetPageInfo(nPage)
+
+                text = infos["text"]
+                image = infos["image"]
+                hidden = infos["ishidden"]
+
+                panel.Reparent(newNotebook)
+                newNotebook.InsertPage(where, panel, text, True, image, hidden)
+                newNotebook.SetPageInfo(where, infos)
+
+                oldNotebook.nb.DeletePage(nPage)
+
+                oldNotebook.bsizer.Detach(nPage)
+                oldNotebook.bsizer.Layout()
+                oldNotebook.sizer.Layout()
+
+                oldNotebook._notebookpages.pop(nPage)
+
+                oldNotebook.AdvanceSelection()
+
+                if oldNotebook.GetPageCount() == 0:
+                    if oldNotebook._style & NC_TOP:
+                        oldNotebook.sizer.Show(0, False)
+                        oldNotebook.sizer.Show(1, False)
+                    else:
+                        oldNotebook.sizer.Show(1, False)
+                        oldNotebook.sizer.Show(2, False)
+
+                    oldNotebook.sizer.Layout()
+
+                oldNotebook.Thaw()
+                newNotebook.Refresh()
+
+        return wx.DragMove
+
+    def OnLeaveWindow(self, event):
+        """ Handles The wx.EVT_LEAVE_WINDOW Events For TabCtrl. """
+
+        if self._enabledragging:
+            if self._isdragging:
+
+                page = self._parent.GetPage(self._tabID)
+                draginfo = NCDragInfo(page, self._tabID)
+                drginfo = cPickle.dumps(draginfo)
+                dataobject = wx.CustomDataObject(wx.CustomDataFormat("NotebookCtrl"))
+                dataobject.SetData(drginfo)
+                dragSource = wx.DropSource(self)
+                dragSource.SetData(dataobject)
+                dragSource.DoDragDrop(wx.Drag_DefaultMove)
+
+                self._isleaving = True
+                self.Refresh()
+
+        if self._istooltipshown:
+            self._tipwindow.Destroy()
+            self._istooltipshown = False
+            self.Refresh()
+
+        event.Skip()
+
+
+    def OnKeyDown(self, event):
+        """
+        Handles The wx.EVT_KEY_DOWN Event For TabCtrl. This Is Only Processed If
+        The User Navigate Through Tabs With Ctrl-Tab Keyboard Navigation.
+        """
+
+        if event.GetKeyCode == wx.WXK_TAB:
+            if event.ControlDown():
+                sel = self.GetSelection()
+                if sel == self.GetPageCount() - 1:
+                    sel = 0
+                else:
+                    sel = sel + 1
+
+                while not self.IsPageEnabled(sel):
+                    sel = sel + 1
+                    if sel == self.GetPageCount() - 1:
+                        sel = 0
+
+                self._parent.SetSelection(sel)
+
+        event.Skip()
+
+
+    def AddPage(self, text, select=False, img=-1, hidden=False):
+        """
+        Add A Page To The Notebook.
+
+        @param text: The Tab Text;
+        @param select: Whether The Page Should Be Selected Or Not;
+        @param img: Specifies The Optional Image Index For The New Page.
+        """
+
+        self._pages.append(TabbedPage(text, img, hidden))
+        self._somethingchanged = True
+
+        self._firsttime = True
+        self._timers.append(wx.Timer(self))
+
+        # JS: The following two lines caused this control not to fire
+        # the EVT_NOTEBOOKCTRL_PAGE_CHANGING/CHANGED events for the
+        # first page. The NotebookCtrl sets the selection later anyway,
+        # and without these two lines, the events fire.
+##        if select or self.GetSelection() == -1:
+##            self._selection = self.GetPageCount() - 1
+
+        if self._style & NC_LEFT or self._style & NC_RIGHT:
+            self.SetBestSize((self._CalcBestWidth(wx.ClientDC(self)), -1))
+
+        self.Refresh()
+
+
+    def InsertPage(self, nPage, text, select=False, img=-1, hidden=False):
+        """
+        Insert A Page Into The Notebook.
+
+        @param nPage: Specifies The Position For The New Page;
+        @param text: The Tab Text;
+        @param select: Whether The Page Should Be Selected Or Not;
+        @param img: Specifies The Optional Image Index For The New Page.
+        """
+
+        if nPage < 0 or (self.GetSelection() >= 0 and nPage >= self.GetPageCount()):
+            raise "\nERROR: Invalid Notebook Page In InsertPage: (" + str(nPage) + ")"
+
+        oldselection = self.GetSelection()
+
+        self._pages.insert(nPage, TabbedPage(text, img, hidden))
+        self._timers.insert(nPage, wx.Timer(self))
+
+        self._somethingchanged = True
+        self._firsttime = True
+
+        if select or self.GetSelection() == -1:
+            self._selection = nPage
+            self.SetSelection(nPage)
+        else:
+            if nPage <= oldselection:
+                self._selection = self._selection + 1
+
+        if self._style & NC_LEFT or self._style & NC_RIGHT:
+            self.SetBestSize((self._CalcBestWidth(wx.ClientDC(self)), -1))
+
+        self.Refresh()
+
+
+    def DeleteAllPages(self):
+        """ Deletes All NotebookCtrl Pages. """
+
+        for tims in self._timers:
+            if tims.IsRunning():
+                tims.Stop()
+
+            tims.Destroy()
+
+        self._timers = []
+        self._pages = []
+        self._selection = -1
+        self._somethingchanged = True
+        self._firsttime = True
+        self.Refresh()
+
+
+    def DeletePage(self, nPage, oncontinue=True):
+        """ Deletes The Page nPage, And The Associated Window. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In DeletePage: (" + str(nPage) + ")"
+
+        oldselection = self.GetSelection()
+
+        self._pages.pop(nPage)
+
+        if self._timers[nPage].IsRunning():
+            self._timers[nPage].Stop()
+
+        self._timers[nPage].Destroy()
+
+        if self._istooltipshown:
+            self._tipwindow.Destroy()
+            self._istooltipshown = False
+
+        if not oncontinue:
+            self._somethingchanged = True
+            self._firsttime = True
+            self.Refresh()
+            return
+
+        if nPage < self._selection:
+            self._selection = self._selection - 1
+        elif self._selection == nPage and self._selection == self.GetPageCount():
+            self._selection = self._selection - 1
+        else:
+            self._selection = oldselection
+
+        self._somethingchanged = True
+        self._firsttime = True
+        self.Refresh()
+
+
+    def SetSelection(self, nPage):
+        """
+        Sets The Current Tab Selection To The Given nPage. This Call Generates The
+        EVT_NOTEBOOKCTRL_PAGE_CHANGING Event.
+        """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetSelection: (" + str(nPage) + ")"
+
+        oldselection = self._selection
+
+        if nPage != self._selection:
+
+            if not self.IsPageEnabled(nPage):
+                return
+
+            eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_CHANGING, self.GetId())
+            eventOut.SetSelection(nPage)
+            eventOut.SetOldSelection(self._selection)
+            eventOut.SetEventObject(self)
+
+            if not self.GetEventHandler().ProcessEvent(eventOut):
+
+                # Prevent full paint unless never fully painted
+                if hasattr(self, "_initrect"):
+                    self._firsttime = False
+                # Program Allows The Page Change
+                self._selection = nPage
+                eventOut.SetEventType(wxEVT_NOTEBOOKCTRL_PAGE_CHANGED)
+                eventOut.SetOldSelection(self._selection)
+                self.GetEventHandler().ProcessEvent(eventOut)
+
+                if oldselection != -1:
+                    self._parent.bsizer.Show(oldselection, False)
+
+                self.EnsureVisible(self._selection)
+                self._parent.bsizer.Show(self._selection, True)
+                self._parent.bsizer.Layout()
+
+                self.Refresh()
+
+                self._shown = nPage
+
+
+    def EnsureVisible(self, selection):
+
+        if self.GetPageCount() < 2:
+            return
+
+        if not self.HasSpinButton():
+            return
+
+        fullrect = self.GetClientSize()
+        count = self._tabvisible[0:selection].count(0)
+        currect = self._tabrect[selection-self._firstvisible-count]
+
+        spinval = self._spinbutton.GetValue()
+        firstrect = self._initrect[spinval]
+        if self._style & NC_LEFT or self._style & NC_RIGHT:
+            pos = currect.y
+            size = currect.height
+            posIndex = 1
+        else:
+            pos = currect.x
+            size = currect.width
+            posIndex = 0
+        torefresh = 0
+
+        while pos + size > fullrect[posIndex] - self._spinbutton.GetSize()[posIndex]:
+
+            if self._style & NC_LEFT or self._style & NC_RIGHT:
+                pos -= firstrect.height
+            else:
+                pos -= firstrect.width
+
+            if not self._enablehiding:
+                spinval = spinval + 1
+            else:
+                oldspinval = spinval
+                spinval = spinval + self._tabvisible[0:selection].count(0)
+                if spinval == oldspinval:
+                    spinval = spinval + 1
+
+                if spinval >= len(self._initrect):
+                    spinval = spinval - 1
+
+            firstrect = self._initrect[spinval]
+
+            if self._style & NC_LEFT or self._style & NC_RIGHT:
+                self._spinbutton.OnSpin(-spinval)
+                self._spinbutton.SetValue(-spinval)
+            else:
+                self._spinbutton.OnSpin(spinval)
+                self._spinbutton.SetValue(spinval)
+
+            torefresh = 1
+
+        if torefresh:
+            self.Refresh()
+
+
+    def GetPageCount(self):
+        """ Returns The Number Of Pages In NotebookCtrl. """
+
+        return len(self._pages)
+
+
+    def GetSelection(self):
+        """ Returns The Current Selection. """
+
+        return self._selection
+
+
+    def GetImageList(self):
+        """ Returns The Image List Associated With The NotebookCtrl. """
+
+        return self._imglist
+
+
+    def SetImageList(self, imagelist):
+        """ Associate An Image List To NotebookCtrl. """
+
+        self._imglist = imagelist
+        self._grayedlist = wx.ImageList(16, 16, True, 0)
+
+        for ii in xrange(imagelist.GetImageCount()):
+
+            bmp = imagelist.GetBitmap(ii)
+            image = wx.ImageFromBitmap(bmp)
+            image = GrayOut(image)
+            newbmp = wx.BitmapFromImage(image)
+            self._grayedlist.Add(newbmp)
+
+
+    def AssignImageList(self, imagelist):
+        """ Associate An Image List To NotebookCtrl. """
+
+        self._imglist = imagelist
+        self._grayedlist = wx.ImageList(16, 16, True, 0)
+
+        for ii in xrange(imagelist.GetImageCount()):
+
+            bmp = imagelist.GetBitmap(ii)
+            image = wx.ImageFromBitmap(bmp)
+            image = GrayOut(image)
+            newbmp = wx.BitmapFromImage(image)
+            self._grayedlist.Add(newbmp)
+
+
+    def GetPadding(self):
+        """ Returns The (Horizontal, Vertical) Padding Of The Text Inside Tabs. """
+
+        return self._padding
+
+
+    def SetPadding(self, padding):
+        """ Sets The (Horizontal, Vertical) Padding Of The Text Inside Tabs. """
+
+        self._padding = padding
+        self._somethingchanged = True
+        self._firsttime = True
+        self.Refresh()
+
+
+    def GetPageText(self, nPage):
+        """ Returns The String For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageText: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._text
+
+
+    def SetPageText(self, nPage, text):
+        """ Sets The String For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageText: (" + str(nPage) + ")"
+
+        if self._pages[nPage]._text != text:
+            self._pages[nPage]._text = text
+            self._somethingchanged = True
+            self._firsttime = True
+            self.Refresh()
+
+
+    def GetPageImage(self, nPage):
+        """ Returns The Image Index For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageImage: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._image
+
+
+    def SetPageImage(self, nPage, img):
+        """ Sets The Image Index For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageImage: (" + str(nPage) + ")"
+
+        if self._pages[nPage]._image != img:
+            self._pages[nPage]._image = img
+            self._somethingchanged = True
+            self._firsttime = True
+
+            if self._style & NC_LEFT or self._style & NC_RIGHT:
+                self.SetBestSize((self._CalcBestWidth(wx.ClientDC(self)), -1))
+                self._parent.GetSizer().Layout()
+
+            self.Refresh()
+
+
+    def SetPageTextFont(self, nPage, font=None):
+        """ Sets The Primary Font For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageTextFont: (" + str(nPage) + ")"
+
+        if font is None:
+            font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+
+        normalfont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+
+        self._pages[nPage]._font = font
+
+        if font == normalfont:
+            self._parent.GetSizer().Layout()
+            self._somethingchanged = True
+            self._firsttime = True
+            self.Refresh()
+            return
+
+        dc = wx.ClientDC(self)
+        dc.SetFont(font)
+        w1, h1 = dc.GetTextExtent("Aq")
+        dc.SetFont(normalfont)
+        wn, hn = dc.GetTextExtent("Aq")
+        w2, h2 = (0, 0)
+
+        if hasattr(self._pages[nPage], "_secondaryfont"):
+            dc.SetFont(self._pages[nPage]._secondaryfont)
+            w2, h2 = dc.GetTextExtent("Aq")
+
+        h = max(h1, h2)
+
+        if h < hn:
+            self._somethingchanged = True
+            self._firsttime = True
+            self.Refresh()
+            return
+
+        if h + 2*self._padding.y < 24:
+            newheight = 24
+        else:
+            newheight = h + 2*self._padding.y
+
+        oldsize = self.GetSize()
+
+        if newheight < oldsize[1]:
+            newheight = oldsize[1]
+
+        if self._style & NC_TOP or self._style & NC_BOTTOM:
+            self.SetBestSize((-1, newheight))
+        else:
+            self.SetBestSize((self._CalcBestWidth(dc), -1))
+        self._parent.GetSizer().Layout()
+        self._somethingchanged = True
+        self._firsttime = True
+        self.Refresh()
+
+    def SetTabHeight(self, height=28):
+        """ Sets The Tabs Height. """
+
+        if self._style & NC_TOP or self._style & NC_BOTTOM:
+            self.SetBestSize((-1, height))
+            self._bestsize = height
+
+
+    def SetControlBackgroundColour(self, colour=None):
+        """ Sets The TabCtrl Background Colour (Behind The Tabs). """
+
+        if colour is None:
+            colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)
+
+        self.SetBackgroundColour(colour)
+        self.Refresh()
+
+
+    def GetPageTextFont(self, nPage):
+        """ Returns The Primary Font For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageTextFont: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._font
+
+
+    def SetPageTextSecondaryFont(self, nPage, font=None):
+        """ Sets The Secondary Font For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageTextSecondaryFont: (" + str(nPage) + ")"
+
+        if font is None:
+            font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+
+        normalfont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+
+        self._pages[nPage]._secondaryfont = font
+
+        if font == normalfont:
+            self._somethingchanged = True
+            self._firsttime = True
+            self.Refresh()
+            return
+
+        dc = wx.ClientDC(self)
+        dc.SetFont(font)
+        w1, h1 = dc.GetTextExtent("Aq")
+        dc.SetFont(normalfont)
+        wn, hn = dc.GetTextExtent("Aq")
+        w2, h2 = (0, 0)
+
+        if hasattr(self._pages[nPage], "_font"):
+            dc.SetFont(self._pages[nPage]._font)
+            w2, h2 = dc.GetTextExtent("Aq")
+
+        h = max(h1, h2)
+
+        if h < hn:
+            self._somethingchanged = True
+            self._firsttime = True
+            self.Refresh()
+            return
+
+        if h + 2*self._padding.y < 24:
+            newheight = 24
+        else:
+            newheight = h + 2*self._padding.y
+
+        oldsize = self.GetSize()
+
+        if newheight < oldsize[-1]:
+            newheight = oldsize[-1]
+
+        if self._style & NC_TOP or self._style & NC_BOTTOM:
+            self.SetBestSize((-1, newheight))
+        else:
+            self.SetBestSize((self._CalcBestWidth(dc), -1))
+        self._parent.GetSizer().Layout()
+
+        self._somethingchanged = True
+        self._firsttime = True
+        self.Refresh()
+
+
+    def GetPageTextSecondaryFont(self, nPage):
+        """ Returns The Secondary Font For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageTextSecondaryFont: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._secondaryfont
+
+
+    def SetPageTextColour(self, nPage, colour=None):
+        """ Sets The Text Colour For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageTextColour: (" + str(nPage) + ")"
+
+        if colour is None:
+            colour = wx.BLACK
+
+        self._pages[nPage]._pagetextcolour = colour
+        self._somethingchanged = True
+        self.Refresh()
+
+
+    def GetPageTextColour(self, nPage):
+        """ Returns The Text Colour For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageTextColour: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._pagetextcolour
+
+
+    def SetPageColour(self, nPage, colour=None):
+        """ Sets The Tab Background Colour For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageColour: (" + str(nPage) + ")"
+
+        if colour is None:
+            colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE)
+
+        self._pages[nPage]._pagecolour = colour
+        self._somethingchanged = True
+        self.Refresh()
+
+
+    def GetPageColour(self, nPage):
+        """ Returns The Tab Background Colour For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageColour: (" + str(nPage) + ")"
+
+        if not self._tabstyle._normal or self._usegradients:
+            if self._usegradients:
+                return self._tabstyle._firstcolour
+            else:
+                return self._GetThemePageColour(nPage)
+        else:
+            return self._pages[nPage]._pagecolour
+
+
+    def EnablePage(self, nPage, enable=True):
+        """ Enable/Disable The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In EnablePage: (" + str(nPage) + ")"
+
+        self._pages[nPage]._enable = enable
+
+        if not enable and self.GetSelection() == nPage:
+            defpage = self.GetDefaultPage()
+            if defpage < 0:
+                self.AdvanceSelection()
+            else:
+                if defpage >= self.GetPageCount():
+                    self.AdvanceSelection()
+                else:
+                    if defpage == nPage:
+                        self.AdvanceSelection()
+                    else:
+                        self.SetSelection(defpage)
+
+        self.Refresh()
+
+
+    def IsPageEnabled(self, nPage):
+        """ Returns Whether A Page Is Enabled Or Not. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In IsPageEnabled: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._enable
+
+
+    def SetHighlightSelection(self, highlight=True):
+        """ Globally Enables/Disables Tab Highlighting On Tab Selection. """
+
+        self._highlight = highlight
+        self.Refresh()
+
+
+    def GetHighlightSelection(self):
+        """ Returns Globally Enable/Disable State For Tab Highlighting On Tab Selection. """
+
+        return self._highlight
+
+
+    def SetUseFocusIndicator(self, focus=True):
+        """ Globally Enables/Disables Tab Focus Indicator. """
+
+        self._usefocus = focus
+        self.Refresh()
+
+
+    def GetUseFocusIndicator(self):
+        """ Returns Globally Enable/Disable State For Tab Focus Indicator. """
+
+        return self._usefocus
+
+
+    def SetPageToolTip(self, nPage, tooltip="", timer=500, winsize=400):
+        """
+        Sets A ToolTip For The Given Page nPage.
+
+        @param nPage: The Given Page;
+        @param tooltip: The ToolTip String;
+        @param timer: The Timer After Which The Tip Window Is Popped Up;
+        @param winsize: The Maximum Width Of The Tip Window.
+        """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageToolTip: (" + str(nPage) + ")"
+
+        self._pages[nPage]._tooltip = tooltip
+        self._pages[nPage]._tooltiptime = timer
+        self._pages[nPage]._winsize = winsize
+
+
+    def GetPageToolTip(self, nPage):
+        """ Returns A Tuple With All Page ToolTip Parameters. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageToolTip: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._tooltip, self._pages[nPage]._tooltiptime, \
+               self._pages[nPage]._winsize
+
+
+    def EnableToolTip(self, show=True):
+        """ Globally Enables/Disables Tab ToolTips. """
+
+        self._showtooltip = show
+
+        if show:
+            try:
+                wx.PopupWindow(self)
+                self.TransientTipWindow = TransientTipWindow
+
+            except NotImplementedError:
+
+                self.TransientTipWindow = macTransientTipWindow
+
+        else:
+            if self._istooltipshown:
+                self._tipwindow.Destroy()
+                self._istooltipshown = False
+                self.Refresh()
+
+            if self._tiptimer.IsRunning():
+                self._tiptimer.Stop()
+
+
+    def GetToolTipBackgroundColour(self):
+        """ Returns The ToolTip Window Background Colour. """
+
+        return self._backtooltip
+
+
+    def SetToolTipBackgroundColour(self, colour=None):
+        """ Sets The ToolTip Window Background Colour. """
+
+        if colour is None:
+            colour = wx.Colour(255, 255, 230)
+
+        self._backtooltip = colour
+
+
+    def EnableTabGradients(self, enable=True):
+        """ Globally Enables/Disables Drawing Of Gradient Coloured Tabs For Each Tab. """
+
+        self._usegradients = enable
+
+        if enable:
+            self._tabstyle.ResetDefaults()
+            self._selstyle.ResetDefaults()
+
+        self.Refresh()
+
+
+    def SetPageFirstGradientColour(self, nPage, colour=None):
+        """ Sets The Single Tab First Gradient Colour. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageFirstGradientColour: (" + str(nPage) + ")"
+
+        if colour is None:
+            colour = wx.WHITE
+
+        self._pages[nPage]._firstcolour = colour
+        self.Refresh()
+
+
+    def SetPageSecondGradientColour(self, nPage, colour=None):
+        """ Sets The Single Tab Second Gradient Colour. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageSecondGradientColour: (" + str(nPage) + ")"
+
+        if colour is None:
+            color = self._pages[nPage]._firstcolour
+            r, g, b = int(color.Red()), int(color.Green()), int(color.Blue())
+            color = ((r >> 1) + 20, (g >> 1) + 20, (b >> 1) + 20)
+            colour = wx.Colour(color[0], color[1], color[2])
+
+        self._pages[nPage]._secondcolour = colour
+        self.Refresh()
+
+
+    def GetPageFirstGradientColour(self, nPage):
+        """ Returns The Single Tab First Gradient Colour. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageFirstGradientColour: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._firstcolour
+
+
+    def GetPageSecondGradientColour(self, nPage):
+        """ Returns The Single Tab Second Gradient Colour. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageSecondGradientColour: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._secondcolour
+
+
+    def CancelTip(self):
+        """ Destroys The Tip Window (Probably You Won't Need This One). """
+
+        if self._istooltipshown:
+            self._istooltipshown = False
+            self._tipwindow.Destroy()
+            self.Refresh()
+
+
+    def AdvanceSelection(self, forward=True):
+        """
+        Cycles Through The Tabs. The Call To This Function Generates The
+        EVT_NOTEBOOKCTRL_PAGE_CHANGING Event.
+        """
+
+        if self.GetPageCount() <= 1:
+            return
+
+        sel = self.GetSelection()
+        count = 0
+
+        if forward:
+            if sel == self.GetPageCount() - 1:
+                sel = 0
+            else:
+                sel = sel + 1
+
+            while not self.IsPageEnabled(sel) or \
+                  (self._enablehiding and self._pages[sel]._ishidden):
+
+                count = count + 1
+                sel = sel + 1
+
+                if self._enablehiding and self._pages[sel]._ishidden:
+                    count = count + 1
+                    sel = sel + 1
+
+                if sel == self.GetPageCount() - 1:
+                    sel = 0
+
+                if count > self.GetPageCount() + 1:
+                    return None
+
+        else:
+            if sel == 0:
+                sel = self.GetPageCount() - 1
+            else:
+                sel = sel - 1
+
+            while not self.IsPageEnabled(sel):
+                count = count + 1
+                sel = sel - 1
+                if sel == 0:
+                    sel = self.GetPageCount() - 1
+
+                if count > self.GetPageCount() + 1:
+                    return None
+
+        self._parent.SetSelection(sel)
+
+
+    def SetDefaultPage(self, defaultpage=-1):
+        """
+        Sets The Default Page That Will Be Selected When An Active And Selected
+        Tab Is Made Inactive.
+        """
+
+        if defaultpage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetDefaultPage: (" + str(defaultpage) + ")"
+
+        self._defaultpage = defaultpage
+
+
+    def GetDefaultPage(self):
+        """ Returns The Default Page. """
+
+        return self._defaultpage
+
+
+    def UpdateSpinButton(self):
+        """ Update The NotebookSpinButton. Used Internally. """
+
+        count = self.GetPageCount()
+
+        if count == 0:
+            return
+
+        nbsize = []
+        nbsize.append(self._initrect[-1][0] + self._initrect[-1][2])
+        nbsize.append(self._initrect[-1][1] + self._initrect[-1][3])
+        clsize = self.GetClientSize()
+
+        if self._style & NC_TOP or self._style & NC_BOTTOM:
+            spinstyle = wx.SP_HORIZONTAL
+            showspin = nbsize[0] > clsize[0]
+        else:
+            spinstyle = wx.SP_VERTICAL
+            showspin = nbsize[1] > clsize[1]
+
+        if showspin:
+            if not hasattr(self, "_spinbutton"):
+                self._spinbutton = NotebookSpinButton(self, pos=(10000,10000),
+                    style=spinstyle)
+                self._spinbutton.SetValue(0)
+                self._originalspinsize = self._spinbutton.GetSize()
+
+            sbsize = self._spinbutton.GetSize()
+            if self._style & NC_LEFT or self._style & NC_RIGHT:
+                ypos = clsize[1] - sbsize[1]
+                if self._style & NC_LEFT:
+                    xpos = nbsize[0] - (2 + sbsize[0])
+                else:
+                    xpos = 2
+            else:
+                xpos = clsize[0] - sbsize[0]
+                if self._style & NC_BOTTOM:
+                    ypos = 2
+                else:
+                    ypos = clsize[1] - sbsize[1]
+
+            if self.HasMenuButton():
+                self._spinbutton.SetSize((-1, 16))
+            else:
+                self._spinbutton.SetSize(self._originalspinsize)
+
+            self._spinbutton.Move((xpos, ypos))
+            self._spinbutton.Show()
+            if self._style & NC_LEFT or self._style & NC_RIGHT:
+                self._spinbutton.SetRange(-(count-1), 0)
+            else:
+                self._spinbutton.SetRange(0, count-1)
+
+        else:
+
+            if hasattr(self, "_spinbutton") and self._spinbutton.IsShown():
+                self._spinbutton.Hide()
+                self._spinbutton.SetValue(0)
+
+
+    def HasSpinButton(self):
+        """ Returns Wheter The NotebookSpinButton Exists And Is Shown. """
+
+        return hasattr(self, "_spinbutton") and self._spinbutton.IsShown()
+
+
+    def IsLastVisible(self):
+        """ Returns Whether The Last Tab Is Visible Or Not. """
+
+        if self.HasSpinButton():
+            if self._style & NC_LEFT or self._style & NC_RIGHT:
+                pos, size = (1, 3)
+            else:
+                pos, size = (0, 2)
+            lastpos = self._tabrect[-1][pos] + self._tabrect[-1][size]
+            if lastpos < self._spinbutton.GetPosition()[pos]:
+                return True
+
+        return False
+
+
+    def UpdateMenuButton(self, show):
+        """ Updates The Notebook Menu Button To Show/Hide Tabs. Used Internally. """
+
+        count = self.GetPageCount()
+
+        if count == 0:
+            return
+
+        if not hasattr(self, "_initrect"):
+            return
+
+        if not show and not hasattr(self, "_menubutton"):
+            return
+
+        if not hasattr(self, "_menubutton"):
+            self._menubutton = NotebookMenuButton(self, pos=(10000,10000))
+
+        sbsize = self._menubutton.GetSize()
+        nbsize = []
+        nbsize.append(self._initrect[-1][0] + self._initrect[-1][2])
+        nbsize.append(self._initrect[-1][1] + self._initrect[-1][3])
+        clsize = self.GetClientSize()
+
+        xpos = clsize[0] - sbsize[0]
+        ypos = clsize[1] - sbsize[1]
+
+        if self.HasSpinButton():
+            self._menubutton.Move((xpos-1, ypos-16))
+        else:
+            self._menubutton.Move((xpos-1, ypos-1))
+
+        self._menubutton.Show(show)
+
+
+    def HasMenuButton(self):
+        """ Returns Wheter The NotebookMenuButton Exists And Is Shown. """
+
+        return hasattr(self, "_menubutton") and self._menubutton.IsShown()
+
+
+    def HideTab(self, nPage, hide=True):
+        """ Hides A Tab In The NotebookCtrl. """
+
+        if hide:
+            self._pages[nPage]._ishidden = True
+        else:
+            self._pages[nPage]._ishidden = False
+
+        if nPage == self.GetSelection():
+            self.AdvanceSelection()
+
+        self._firsttime = True
+        self.Refresh()
+
+
+    def HitTest(self, point, flags=0):
+        """
+        Standard NotebookCtrl HitTest() Method. If Called With 2 Outputs, It
+        Returns The Page Clicked (If Any) And One Of These Flags:
+
+        NC_HITTEST_NOWHERE = 0   ==> Hit Not On Tab
+        NC_HITTEST_ONICON  = 1   ==> Hit On Icon
+        NC_HITTEST_ONLABEL = 2   ==> Hit On Label
+        NC_HITTEST_ONITEM  = 4   ==> Hit Generic, On Item
+        NC_HITTEST_ONX = 8       ==> Hit On Closing "X" On Every Page
+        """
+
+        mirror = self._style & NC_BOTTOM
+        size = self.GetSize()
+        dc = wx.ClientDC(self)
+
+        height = self._tabrect[0].height
+
+        if flags:
+            flags = wx.NB_HITTEST_NOWHERE
+
+        if point.x <= 0 or point.x >= size.x:
+            if flags:
+                return wx.NOT_FOUND, flags
+            else:
+                return wx.NOT_FOUND
+
+        if not point.y >= self._tabrect[0].y and point.y < self._tabrect[0].y + height:
+            if flags:
+                return wx.NOT_FOUND, flags
+            else:
+                return wx.NOT_FOUND
+
+        posx = self._firsttabpos.x
+        posy = self._firsttabpos.y
+        maxwidth = max(self._maxtabwidths)
+
+        for ii in xrange(self._firstvisible, self.GetPageCount()):
+
+            if not self._enablehiding or not self._pages[ii]._ishidden:
+
+                width = self._CalcTabTextWidth(dc, ii)
+
+                bmpWidth, bmpHeight = self._CalcTabBitmapSize(ii)
+
+                tabrect = self._CalcTabRect(ii, posx, posy, width, bmpWidth, bmpHeight)
+
+                if tabrect.Contains(point):
+
+                    if flags:
+                        flags = NC_HITTEST_ONITEM
+
+                    #onx attempt
+                    if self.GetDrawX()[0]:
+                        count = self._tabvisible[0:ii].count(0)
+                        if flags and self._xrect[ii-self._firstvisible-count].Contains(point):
+                            flags = NC_HITTEST_ONX
+
+                   #onicon attempt
+                    if flags and bmpWidth > 0 and \
+                        wx.RectPS(wx.Point(*self._CalcTabBitmapPosition(ii,
+                            bmpWidth, bmpHeight, tabrect)),
+                            wx.Size(bmpWidth, bmpHeight)).Contains(point):
+                        flags = NC_HITTEST_ONICON
+
+                   #onlabel attempt
+                    elif flags and wx.RectPS(wx.Point(*self._CalcTabTextPosition(ii,
+                        tabrect, self._CalcTabBitmapSpace(bmpWidth, bmpHeight))),
+                            wx.Size(width, height)).Contains(point):
+                        flags = NC_HITTEST_ONLABEL
+
+                    if flags:
+                        return ii, flags
+                    else:
+                        return ii
+
+                if self._style & NC_TOP or self._style & NC_BOTTOM:
+                    posx += tabrect.width
+                else:
+                    posy += tabrect.height
+
+        if flags:
+            return wx.NOT_FOUND, flags
+        else:
+            return wx.NOT_FOUND
+
+
+    def EnableDragAndDrop(self, enable=True):
+        """ Globall Enables/Disables Tabs Drag And Drop. """
+
+        self._enabledragging = enable
+
+
+    def EnableHiding(self, enable=True):
+        """ Globally Enables/Disables Hiding On Tabs In Runtime. """
+
+        self._enablehiding = enable
+        self.UpdateMenuButton(enable)
+
+        wx.FutureCall(1000, self.UpdateMenuButton, enable)
+
+
+    def SetAnimationImages(self, nPage, imgarray):
+        """
+        Sets An Animation List Associated To The Given Page nPage.
+
+        @param nPage: The Given Page
+        @param imgarray: A List Of Image Indexes Of Images Inside The
+          ImageList Associated To NotebookCtrl.
+        """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetAnimationImages: (" + str(nPage) + ")"
+
+        if not imgarray:
+            raise "\nERROR: Invalid Image Array In SetAnimationImages: (" + repr(imgarray) + ")"
+
+        if min(imgarray) < 0:
+            raise "\nERROR: Invalid Image Array In SetAnimationImages: (Min(ImgArray) = " + \
+                  str(min(imgarray)) + " < 0)"
+
+        if max(imgarray) > self.GetImageList().GetImageCount() - 1:
+            raise "\nERROR: Invalid Image Array In SetAnimationImages: (Max(ImgArray) = " + \
+                  str(max(imgarray)) + " > " + str(self.GetImageList().GetImageCount()-1) + ")"
+
+        self._pages[nPage]._animationimages = imgarray
+
+
+    def GetAnimationImages(self, nPage):
+        """ Returns The Animation Images List Associated To The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetAnimationImages: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._animationimages
+
+
+    def AnimateTab(self, event):
+        """ Called When The Refreshing Animation Timer Expires. Used Internally"""
+
+        obj = event.GetEventObject()
+        nPage = self._timers.index(obj)
+
+        if not self.IsPageEnabled(nPage):
+            return
+
+        indx = self.GetPageImage(nPage)
+        images = self.GetAnimationImages(nPage)
+        myindx = images.index(indx)
+
+        if indx == images[-1]:
+            myindx = -1
+
+        myindx = myindx + 1
+
+        self.SetPageImage(nPage, images[myindx])
+
+
+    def StartAnimation(self, nPage, timer=500):
+        """ Starts The Animation On The Given Page, With Refreshing Time Rate "timer". """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In StartAnimation: (" + str(nPage) + ")"
+
+        images = self.GetAnimationImages(nPage)
+
+        if not images:
+            raise "\nERROR: No Images Array Defined For Page: (" + str(nPage) + ")"
+
+        if len(images) == 1:
+            raise "\nERROR: Impossible To Animate Tab: " + str(nPage) + " With Only One Image"
+
+        self._timers[nPage].Start(timer)
+
+
+    def StopAnimation(self, nPage):
+        """ Stops The Animation On The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In StopAnimation: (" + str(nPage) + ")"
+
+        if self._timers[nPage].IsRunning():
+            self._timers[nPage].Stop()
+
+
+    def SetDrawX(self, drawx=True, style=1, image1=None, image2=None):
+        """
+        Globally Enables/Disables The Drawing Of A Closing "X" In The Tab.
+
+        @param drawx: C{True} to enable drawing a closing "X"; C{False} to
+          disable it
+        @param style: the style of the X to draw when C{drawx} is C{True};
+          possible values are:
+            - C{1}: Small "X" At The Top-Right Of The Tab;
+            - C{2}: Bigger "X" In The Middle Vertical Of The Tab (Like Opera Notebook);
+            - C{3}: Custom "X" Image Is Drawn On Tabs.
+        @param image1: if C{style} is C{3}, the image to use when drawing
+          the X on an unhighlighted tab
+        @param image2: if C{style} is C{3}, the image to use when drawing
+          the X on a highlighted tab
+        """
+
+        self._drawx = drawx
+        self._drawxstyle = style
+
+        if style == 3:
+            self._imglist2 = wx.ImageList(16, 16, True, 0)
+            self._imglist2.Add(image1)
+            self._imglist2.Add(image2)
+
+        if self._style & NC_LEFT or self._style & NC_RIGHT:
+            self.SetBestSize((self._CalcBestWidth(wx.ClientDC(self)), -1))
+            self._parent.GetSizer().Layout()
+
+        self.Refresh()
+
+
+    def GetDrawX(self):
+        """
+        Returns The Enable/Disable State Of Drawing Of A Small "X" At The Top-Right Of
+        Every Page.
+        """
+
+        return self._drawx, self._drawxstyle
+
+
+    def GetInsideTab(self, pt):
+        """ Returns The Tab On Which The Mouse Is Hovering On. """
+
+        count = 0
+
+        for tabs in self._tabrect:
+            if tabs.Contains(pt):
+                return count
+
+            count = count + 1
+
+        return -1
+
+
+    def GetInsideX(self, pt):
+        """ Returns The Tab On Which The Mouse Is Hovering On The "X" Button. """
+
+        count = 0
+
+        for rects in self._xrect:
+            if rects.Contains(pt):
+                return count
+
+            count = count + 1
+
+        return -1
+
+
+    def SetImageToCloseButton(self, convert=True):
+        """ Set Whether The Tab Icon Should Be Converted To The Close Button Or Not. """
+
+        self._convertimage = convert
+
+
+    def GetImageToCloseButton(self):
+        """ Get Whether The Tab Icon Should Be Converted To The Close Button Or Not. """
+
+        return self._convertimage
+
+
+    def ConvertImageToCloseButton(self, page):
+        """ Globally Converts The Page Image To The "Opera" Style Close Button. """
+
+        bmpindex = self.GetPageImage(page)
+        if  bmpindex < 0:
+            return
+
+        tabrect = self._tabrect[page]
+        size = self.GetSize()
+
+        maxfont = self._maxfont
+
+        dc = wx.ClientDC(self)
+
+        dc.SetFont(maxfont)
+        pom, height = dc.GetTextExtent("Aq")
+
+        bmp = self._imglist.GetBitmap(bmpindex)
+
+        bmpposx = tabrect.x + self._padding.x
+        bmpposy = size.y - (height + 2*self._padding.y + bmp.GetHeight())/2 - 1
+
+        ypos = size.y - height - self._padding.y*2
+        ysize = height + self._padding.y*2 + 3
+
+        if page == self.GetSelection():
+            bmpposx = bmpposx + 1
+            bmpposy = bmpposy - 1
+            ypos = ypos - 3
+            ysize = ysize + 2
+
+        colour = self.GetPageColour(page)
+        bmprect = wx.Rect(bmpposx, bmpposy, bmp.GetWidth()+self._padding.x, bmp.GetHeight())
+
+        dc.SetBrush(wx.Brush(colour))
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.DrawRectangleRect(bmprect)
+
+        colour = self.GetPageTextColour(page)
+
+        r = colour.Red()
+        g = colour.Green()
+        b = colour.Blue()
+
+        hr, hg, hb = min(255,r+64), min(255,g+64), min(255,b+64)
+
+        colour = wx.Colour(hr, hg, hb)
+        back_colour = wx.WHITE
+
+        yypos = ypos+(ysize-height-self._padding.y/2)/2
+
+        xrect = wx.Rect(bmprect.x+(bmprect.width - self._padding.x - height)/2,
+                        yypos, height, height)
+
+        # Opera Style
+        dc.SetPen(wx.Pen(colour, 1))
+        dc.SetBrush(wx.Brush(colour))
+        dc.DrawRoundedRectangleRect(xrect, 2)
+        dc.SetPen(wx.Pen(back_colour, 2))
+        dc.DrawLine(xrect[0]+2, xrect[1]+2, xrect[0]+xrect[2]-3, xrect[1]+xrect[3]-3)
+        dc.DrawLine(xrect[0]+2, xrect[1]+xrect[3]-3, xrect[0]+xrect[2]-3, xrect[1]+2)
+
+
+    def RedrawClosingX(self, pt, insidex, drawx, highlight=False):
+        """ Redraw The Closing "X" Accordingly To The Mouse "Hovering" Position. """
+
+        colour = self.GetPageTextColour(insidex)
+        back_colour = self.GetBackgroundColour()
+        imagelist = 0
+
+        if highlight:
+            r = colour.Red()
+            g = colour.Green()
+            b = colour.Blue()
+
+            hr, hg, hb = min(255,r+64), min(255,g+64), min(255,b+64)
+
+            colour = wx.Colour(hr, hg, hb)
+            back_colour = wx.WHITE
+            imagelist = 1
+
+        dc = wx.ClientDC(self)
+        xrect = self._xrect[insidex]
+
+        if drawx == 1:
+            # Emule Style
+            dc.SetPen(wx.Pen(colour, 1))
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+            dc.DrawRectangleRect(xrect)
+        elif drawx == 2:
+            # Opera Style
+            dc.SetPen(wx.Pen(colour, 1))
+            dc.SetBrush(wx.Brush(colour))
+            dc.DrawRoundedRectangleRect(xrect, 2)
+            dc.SetPen(wx.Pen(back_colour, 2))
+            dc.DrawLine(xrect[0]+2, xrect[1]+2, xrect[0]+xrect[2]-3, xrect[1]+xrect[3]-3)
+            dc.DrawLine(xrect[0]+2, xrect[1]+xrect[3]-3, xrect[0]+xrect[2]-3, xrect[1]+2)
+        else:
+            self._imglist2.Draw(imagelist, dc, xrect[0], xrect[1],
+                                wx.IMAGELIST_DRAW_TRANSPARENT, True)
+
+
+    def HideOnSingleTab(self, hide=True):
+        """ Hides The TabCtrl When There Is Only One Tab In NotebookCtrl. """
+
+        self._hideonsingletab = hide
+
+
+    def SetPagePopupMenu(self, nPage, menu):
+        """ Sets A Popup Menu Specific To A Single Tab. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPagePopupMenu: (" + str(nPage) + ")"
+
+        self._pages[nPage]._menu = menu
+
+
+    def GetPagePopupMenu(self, nPage):
+        """ Returns The Popup Menu Associated To A Single Tab. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPagePopupMenu: (" + str(nPage) + ")"
+
+        return self._pages[nPage]._menu
+
+
+    def DrawInsertionMark(self, dc, nPage):
+        """
+        Draw An Insertion Arrow To Let The User Understand Where A Dragged Tab Will
+        Be Dropped (Between Which Tabs).
+        """
+
+        if not self._enablehiding:
+            if nPage < 0 or nPage >= len(self._tabrect):
+                return
+        else:
+            if nPage < 0 or nPage >= len(self._tabrect) + self._tabvisible.count(0):
+                return
+
+        colour = wx.BLACK
+        somehidden = False
+
+        if self._enablehiding:
+            for ii in xrange(nPage):
+                if self._pages[ii]._ishidden:
+                    nPage = nPage - 1
+                    somehidden = True
+
+        rect = self._tabrect[nPage]
+
+        x1 = rect.x - 4
+        y1 = rect.y - 1
+        x2 = rect.x
+        y2 = y1 + 5
+        x3 = rect.x + 3
+        y3 = y1
+
+        mybrush = wx.Brush(self.GetPageTextColour(nPage))
+
+        if not self._enablehiding:
+            if nPage > self._tabID:
+                x1 = x1 + rect.width
+                x2 = x2 + rect.width
+                x3 = x3 + rect.width
+        else:
+            mybrush = wx.Brush(self.GetPageTextColour(nPage))
+            if nPage >= self._tabID:
+                x1 = x1 + rect.width
+                x2 = x2 + rect.width
+                x3 = x3 + rect.width
+
+        dc.SetPen(wx.Pen(wx.BLACK, 1))
+        dc.SetBrush(mybrush)
+        dc.DrawPolygon([(x1, y1), (x2, y2), (x3, y3)])
+
+
+    def OnMouseMotion(self, event):
+        """ Handles The wx.EVT_MOTION Event For TabCtrl. """
+
+        pt = event.GetPosition()
+
+        if self._enabledragging:
+
+            if event.Dragging() and not event.RightIsDown() and not event.MiddleIsDown():
+
+                tolerance = 2
+
+                dx = abs(pt.x - self._dragstartpos.x)
+                dy = abs(pt.y - self._dragstartpos.y)
+
+                if dx <= tolerance and dy <= tolerance:
+                    self.SetCursor(wx.STANDARD_CURSOR)
+                    return
+
+                self.SetCursor(self._dragcursor)
+                self._isdragging = True
+                self._isleaving = False
+                newpos = self.HitTest(pt)
+
+                if newpos >= 0 and newpos != self._olddragpos:
+                    self._olddragpos = newpos
+                    self.Refresh()
+
+            else:
+
+                self._isdragging = False
+                self.SetCursor(wx.STANDARD_CURSOR)
+
+        if not event.Dragging():
+            drawx = self.GetDrawX()
+
+            if drawx[0]:
+                insidex = self.GetInsideX(pt)
+                if insidex >= 0:
+                    if self.IsPageEnabled(insidex):
+                        self.RedrawClosingX(pt, insidex, drawx[1], True)
+                        self._xrefreshed = False
+                else:
+                    if not self._xrefreshed:
+                        insidetab = self.GetInsideTab(pt)
+                        if insidetab >= 0:
+                            if self.IsPageEnabled(insidetab):
+                                self.RedrawClosingX(pt, insidetab, drawx[1])
+                                self._xrefreshed = True
+            else:
+                if self.GetImageToCloseButton():
+                    page, flags = self.HitTest(pt, 1)
+                    if page >= 0:
+                        if self.IsPageEnabled(page):
+                            if flags == NC_HITTEST_ONICON:
+                                if not self._imageconverted:
+                                    self.ConvertImageToCloseButton(page)
+                                    self._imageconverted = True
+                            else:
+                                if self._imageconverted:
+                                    self.Refresh()
+                                    self._imageconverted = False
+
+        if self._showtooltip:
+            if not event.Dragging():
+                if not event.LeftDown():
+
+                    oldinside = self._insidetab
+                    self._insidetab = self.GetInsideTab(pt)
+
+                    if self._insidetab >= 0:
+                        if oldinside != self._insidetab:
+
+                            if self._istooltipshown:
+                                self._tipwindow.Destroy()
+                                self._istooltipshown = False
+                                self.Refresh()
+
+                            if self._tiptimer.IsRunning():
+                                self._tiptimer.Stop()
+
+                            tip, ontime, winsize= self.GetPageToolTip(self._insidetab)
+
+                            if tip.strip() != "":
+                                self._currenttip = tip
+                                self._currentwinsize = winsize
+                                self._tiptimer.Start(ontime, wx.TIMER_ONE_SHOT)
+
+                    else:
+                        if self._istooltipshown:
+                            self._tipwindow.Destroy()
+                            self._istooltipshown = False
+                            self.Refresh()
+
+        self._mousepos = pt
+
+        event.Skip()
+
+
+    def OnShowToolTip(self):
+        """ Called When The Timer For The ToolTip Expires. Used Internally. """
+
+        pt = self.ScreenToClient(wx.GetMousePosition())
+
+        oldinside = self._insidetab
+        self._insidetab = self.GetInsideTab(pt)
+
+        if self._insidetab != oldinside or self._insidetab < 0:
+            return
+
+        self._istooltipshown = True
+        self._tipwindow = self.TransientTipWindow(self, self._currenttip,
+                                                  self._currentwinsize)
+
+        xsize, ysize = self._tipwindow.GetSize()
+        xpos, ypos = self.ClientToScreen(self._mousepos)
+
+        if xpos + xsize > self._xvideo - 10:
+            if ypos + ysize > self._yvideo - 10:  # SW Tip Positioning
+                posx = xpos - xsize
+                posy = ypos - ysize
+            else: # NE Tip Positioning
+                posx = xpos - xsize
+                posy = ypos
+        else:
+            if ypos + ysize > self._yvideo - 10:  # SE Tip Positioning
+                posx = xpos + 10
+                posy = ypos - ysize
+            else: # NW Tip Positioning
+                posx = xpos + 10
+                posy = ypos
+
+        if posy < 0:
+            posy = ypos
+
+        if posx < 0:
+            posx = xpos
+
+        self._tipwindow.SetPosition((posx, posy))
+        self._tipwindow.Show()
+
+
+    def OnMouseLeftDown(self, event):
+        """ Handles The wx.EVT_LEFT_DOWN Event For TabCtrl. """
+
+        pos = event.GetPosition()
+        page, flags = self.HitTest(pos, 1)
+        self._dragstartpos = pos
+
+        if page != wx.NOT_FOUND:
+
+            if self.IsPageEnabled(page):
+
+                if event.m_controlDown:
+                    if page in self._selectedtabs:
+                        self._selectedtabs.remove(page)
+                    else:
+                        self._selectedtabs.append(page)
+                    self.Refresh()
+                else:
+                    self._selectedtabs = []
+                    if flags == NC_HITTEST_ONX or (flags == NC_HITTEST_ONICON and self.GetImageToCloseButton()):
+                        eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_CLOSING, self.GetId())
+                        eventOut.SetOldSelection(self._selection)
+                        eventOut.SetSelection(page)
+                        eventOut.SetEventObject(self)
+
+                        if not self.GetEventHandler().ProcessEvent(eventOut):
+                            self._parent.DeletePage(page)
+                            self._parent.bsizer.Layout()
+
+                    else:
+                        self.SetSelection(page)
+                        self._tabID = page
+
+        event.Skip()
+
+
+    def OnMouseLeftDClick(self, event):
+        """ Handles The wx.EVT_LEFT_DCLICK Event For TabCtrl. """
+
+        pos = event.GetPosition()
+        page = self.HitTest(pos)
+        self._selectedtabs = []
+
+        if page == wx.NOT_FOUND:
+            return
+
+        if not self.IsPageEnabled(page):
+            return
+
+        eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_DCLICK, self.GetId())
+        eventOut.SetOldSelection(self._selection)
+        eventOut.SetSelection(page)
+        eventOut.SetEventObject(self)
+
+        if not self.GetEventHandler().ProcessEvent(eventOut):
+            return
+
+        event.Skip()
+
+
+    def OnMouseLeftUp(self, event):
+        """ Handles The wx.EVT_LEFT_UP Event For TabCtrl. """
+
+        if not self._enabledragging:
+            event.Skip()
+            return
+
+        if not self._isdragging:
+            self.SetCursor(wx.STANDARD_CURSOR)
+            event.Skip()
+            return
+
+        id = self.HitTest(wx.Point(event.GetX(), event.GetY()))
+
+        if id >= 0 and id != self._tabID:
+
+            self._isdragging = False
+            self._olddragpos = -1
+            eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_DND, self.GetId())
+            eventOut.SetOldPosition(self._tabID)
+            eventOut.SetNewPosition(id)
+            eventOut.SetEventObject(self)
+
+            if self.GetEventHandler().ProcessEvent(eventOut):
+                self._tabID = -1
+                self._olddragpos = -1
+                self.SetCursor(wx.STANDARD_CURSOR)
+                self.Refresh()
+                return
+
+            self._parent.Freeze()
+
+            try:
+                text = self.GetPageText(self._tabID)
+                image = self.GetPageImage(self._tabID)
+                font1 = self.GetPageTextFont(self._tabID)
+                font2 = self.GetPageTextSecondaryFont(self._tabID)
+                fontcolour = self.GetPageTextColour(self._tabID)
+                pagecolour = self.GetPageColour(self._tabID)
+                enabled = self.IsPageEnabled(self._tabID)
+                tooltip, ontime, winsize = self.GetPageToolTip(self._tabID)
+                menu = self.GetPagePopupMenu(self._tabID)
+                firstcol = self.GetPageFirstGradientColour(self._tabID)
+                secondcol = self.GetPageSecondGradientColour(self._tabID)
+                ishidden = self._pages[self._tabID]._ishidden
+            except:
+                self._parent.Thaw()
+                self._tabID = -1
+                self.SetCursor(wx.STANDARD_CURSOR)
+                return
+
+            isanimated = 0
+            if self._timers[self._tabID].IsRunning():
+                isanimated = 1
+                timer = self._timers[self._tabID].GetInterval()
+
+            self.StopAnimation(self._tabID)
+            animatedimages = self.GetAnimationImages(self._tabID)
+
+            pagerange = range(self.GetPageCount())
+
+            newrange = pagerange[:]
+            newrange.remove(self._tabID)
+            newrange.insert(id, self._tabID)
+
+            newpages = []
+            counter = self.GetPageCount() - 1
+
+            for ii in xrange(self.GetPageCount()):
+                newpages.append(self._parent.GetPage(ii))
+                self._parent.bsizer.Detach(counter-ii)
+
+            cc = 0
+
+            self._parent._notebookpages = []
+
+            for jj in newrange:
+                self._parent.bsizer.Add(newpages[jj], 1, wx.EXPAND | wx.ALL, 2)
+                self._parent.bsizer.Show(cc, False)
+                self._parent._notebookpages.append(newpages[jj])
+                cc = cc + 1
+
+            self.DeletePage(self._tabID)
+
+            if enabled:
+                if id == self.GetPageCount():
+                    self.AddPage(text, True, image)
+                else:
+                    self.InsertPage(id, text, True, image)
+            else:
+                if id == self.GetPageCount():
+                    self.AddPage(text, False, image)
+                else:
+                    self.InsertPage(id, text, False, image)
+
+            self.SetPageImage(id, image)
+            self.SetPageText(id, text)
+            self.SetPageTextFont(id, font1)
+            self.SetPageTextSecondaryFont(id, font2)
+            self.SetPageTextColour(id, fontcolour)
+            self.SetPageColour(id, pagecolour)
+            self.EnablePage(id, enabled)
+            self.SetPageToolTip(id, tooltip, ontime, winsize)
+            self.SetPagePopupMenu(id, menu)
+            self.SetPageFirstGradientColour(id, firstcol)
+            self.SetPageSecondGradientColour(id, secondcol)
+            self._pages[id]._ishidden = ishidden
+
+            if isanimated and len(animatedimages) > 1:
+                self.SetAnimationImages(id, animatedimages)
+                self.StartAnimation(id, timer)
+
+            if enabled:
+                self._parent.bsizer.Show(id, True)
+            else:
+                sel = self.GetSelection()
+
+                if sel == -1:
+                    sel = 0
+                self._parent.bsizer.Show(id, False)
+                self._parent.SetSelection(sel)
+                self._parent.bsizer.Show(sel, True)
+
+            self._parent.bsizer.Layout()
+
+            self._parent.Thaw()
+
+        self._isdragging = False
+        self._olddragpos = -1
+        self._fromdnd = True
+        self.Refresh()
+        self._tabID = -1
+        self.SetCursor(wx.STANDARD_CURSOR)
+
+        event.Skip()
+
+
+    def OnSize(self, event=None):
+        """ Handles The wx.EVT_SIZE Event For TabCtrl. """
+
+        if self._sizeToggleButton:
+            width = self.GetSize()[0]
+            height = self._CalcSizeToggleBestSize()[1]
+            self._sizeToggleButton.SetSize(wx.Size(width, height))
+        self.Refresh()
+
+        if event is not None:
+            event.Skip()
+
+
+    def OnMouseRightUp(self, event):
+        """ Handles The wx.EVT_RIGHT_UP Event For TabCtrl. """
+
+        pt = event.GetPosition()
+        id = self.HitTest(pt)
+
+        self._selectedtabs = []
+
+        if id >= 0:
+            if self.IsPageEnabled(id):
+                menu = self.GetPagePopupMenu(id)
+                if menu:
+                    self.PopupMenu(menu)
+
+        event.Skip()
+
+
+    def OnMouseRightDown(self, event):
+        """ Handles The wx.EVT_RIGHT_DOWN Event For TabCtrl. """
+
+        pos = event.GetPosition()
+        page = self.HitTest(pos)
+
+        self._selectedtabs = []
+
+        if page == wx.NOT_FOUND:
+            return
+
+        if not self.IsPageEnabled(page):
+            return
+
+        eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_RIGHT, self.GetId())
+        eventOut.SetOldSelection(self._selection)
+        eventOut.SetSelection(page)
+        eventOut.SetEventObject(self)
+
+        if not self.GetEventHandler().ProcessEvent(eventOut):
+            return
+
+        event.Skip()
+
+
+    def OnMouseMiddleDown(self, event):
+        """ Handles The wx.EVT_MIDDLE_DOWN Event For TabCtrl. """
+
+        pos = event.GetPosition()
+        page = self.HitTest(pos)
+
+        self._selectedtabs = []
+
+        if page == wx.NOT_FOUND:
+            return
+
+        if not self.IsPageEnabled(page):
+            return
+
+        eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_MIDDLE, self.GetId())
+        eventOut.SetOldSelection(self._selection)
+        eventOut.SetSelection(page)
+        eventOut.SetEventObject(self)
+
+        if not self.GetEventHandler().ProcessEvent(eventOut):
+            return
+
+        event.Skip()
+
+
+    def SetSelectionColour(self, colour=None):
+        """ Sets The Tab Selection Colour (Thin Line Above The Selected Tab). """
+
+        if colour is None:
+            colour = wx.Colour(255, 180, 0)
+
+        self._selectioncolour = colour
+
+
+    def SetContourLineColour(self, colour=None):
+        """ Sets The Contour Line Colour (Controur Line Around Tabs). """
+
+        if colour is None:
+            if not self._tabstyle._normal or self._usegradients:
+                colour = wx.Colour(145, 167, 180)
+            else:
+                colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)
+
+        if not self._tabstyle._normal or self._usegradients:
+            self._highlightpen = wx.Pen(colour)
+            self._highlightpen.SetCap(wx.CAP_BUTT)
+        else:
+            self._highlightpen2 = wx.Pen(colour)
+            self._highlightpen2.SetCap(wx.CAP_BUTT)
+
+        self.Refresh()
+
+
+    def ApplyTabTheme(self, theme=None):
+        """ Applies A Particular Theme To Be Drawn On Tabs. """
+
+        if theme is None:
+            theme = ThemeStyle()
+
+        self._tabstyle = theme
+        if self._style & NC_EXPANDABLE:
+            self._InitExpandableTabStyles(self._style, self._expanded, theme)
+        self.Refresh()
+
+    def DrawMacTheme(self, dc, tabrect, theme):
+        """ Draws The Mac Theme On Tabs, If It Is Enabled. """
+
+        if theme == 1:
+            col1, col2 = NC_MAC_LIGHT
+        else:
+            col1, col2 = NC_MAC_DARK
+
+        colour1 = wx.Colour(col1, col1, col1)
+        colour2 = wx.Colour(col2, col2, col2)
+
+        x, y, w, h = tabrect
+        endrange = self._style & NC_ROTATE and w or h
+
+        index = 0
+
+        for ii in xrange(0, endrange, 2):
+            if index%2 == 0:
+                colour = colour1
+            else:
+                colour = colour2
+
+            dc.SetBrush(wx.Brush(colour))
+            dc.SetPen(wx.Pen(colour))
+            if self._style & NC_ROTATE:
+                if ii > 3:
+                    dc.DrawRectangle(x+ii, y, 2, w)
+                else:
+                    dc.DrawRoundedRectangle(x+ii, y, 3, 3)
+            else:
+                if ii > 3:
+                    dc.DrawRectangle(x, y+ii, w, 2)
+                else:
+                    dc.DrawRoundedRectangle(x, y+ii, w, 3, 3)
+
+            index = index + 1
+
+        self._lastcolour = colour
+
+
+    def DrawKDETheme(self, dc, rect):
+        """ Draws Unix-Style KDE Theme On Tabs. """
+
+        x, y, w, h = rect
+
+        if self._style & NC_ROTATE and self._style & NC_RIGHT:
+            bandrange = xrange(13, -1, -1)
+            self._lastcolour = kdetheme[13]
+            brush = wx.Brush(kdetheme[0], wx.SOLID)
+        else:
+            bandrange = xrange(14)
+            self._lastcolour = kdetheme[0]
+            brush = wx.Brush(kdetheme[13], wx.SOLID)
+
+        dc.SetBackground(brush)
+        for band in bandrange:
+            pen = wx.Pen(kdetheme[band])
+            dc.SetPen(pen)
+            if self._style & NC_ROTATE:
+                if self._style & NC_RIGHT:
+                    dc.DrawLine(x+1+band, y+1, x+1+band, y+h-1)
+                    dc.DrawLine(x+w-(1+band), y+1, x+w-(1+band), y+h-2)
+                else:
+                    dc.DrawLine(x+1+band, y+1, x+1+band, y+h-1)
+                    dc.DrawLine(x+w-(2+band), y+1, x+w-(2+band), y+h-2)
+            else:
+                dc.DrawLine(x+1, y+band, x+w-1, y+band)
+                dc.DrawLine(x+1, y+h-1-band, x+w-2, y+h-1-band)
+
+
+    def DrawSilverTheme(self, dc, rect, selected):
+        """ Draws Windows XP Silver-Like Theme. """
+
+        x, y, w, h = rect
+
+        if selected:
+            r1 = silvertheme1[0].Red()
+            g1 = silvertheme1[0].Green()
+            b1 = silvertheme1[0].Blue()
+            r2 = silvertheme1[1].Red()
+            g2 = silvertheme1[1].Green()
+            b2 = silvertheme1[1].Blue()
+        else:
+            r1 = silvertheme2[0].Red()
+            g1 = silvertheme2[0].Green()
+            b1 = silvertheme2[0].Blue()
+            r2 = silvertheme2[1].Red()
+            g2 = silvertheme2[1].Green()
+            b2 = silvertheme2[1].Blue()
+            rend = silvertheme2[2].Red()
+            gend = silvertheme2[2].Green()
+            bend = silvertheme2[2].Blue()
+
+        if self._style & NC_ROTATE:
+            flrect = float(w-2)
+        else:
+            flrect = float(h-2)
+
+        rstep = float((r2 - r1)) / flrect
+        gstep = float((g2 - g1)) / flrect
+        bstep = float((b2 - b1)) / flrect
+
+        rf, gf, bf = 0, 0, 0
+
+        counter = 0
+
+        if self._style & NC_ROTATE:
+            if self._style & NC_RIGHT:
+                bandrange = xrange(x+w-2, x, -1)
+            else:
+                bandrange = xrange(x+1, x+w-1)
+        else:
+            bandrange = xrange(y+1, y+h)
+
+        for band in bandrange:
+            currCol = (int(round(r1 + rf)), int(round(g1 + gf)), int(round(b1 + bf)))
+            dc.SetBrush(wx.Brush(currCol, wx.SOLID))
+            dc.SetPen(wx.Pen(currCol))
+            if self._style & NC_ROTATE:
+                if counter == 0:
+                    ypos = y + 2
+                    yend = h - 4
+                elif counter == 1:
+                    ypos = y + 1
+                    yend = h - 2
+                else:
+                    ypos = y + 1
+                    yend = h - 2
+
+                dc.DrawRectangle(band, ypos, 1, yend)
+
+            else:
+                if counter == 0:
+                    xpos = x + 2
+                    xend = w - 4
+                elif counter == 1:
+                    xpos = x + 1
+                    xend = w - 2
+                else:
+                    xpos = x + 1
+                    xend = w - 2
+
+                dc.DrawRectangle(xpos, band, xend, 1)
+
+            counter = counter + 1
+            rf = rf + rstep
+            gf = gf + gstep
+            bf = bf + bstep
+            self._lastcolour = currCol
+
+        if not selected and self._style & NC_TOP:
+            dc.SetBrush(wx.Brush((rend, gend, bend)))
+            dc.SetPen(wx.Pen((rend, gend, bend)))
+            if self._style & NC_ROTATE:
+                if self._style & NC_LEFT:
+                    xpos = x + w - 4
+                else:
+                    xpos = x
+
+                dc.DrawRectangle(xpos, ypos, 3, yend)
+            else:
+                dc.DrawRectangle(xpos, y+h-3, xend, 3)
+            self._lastcolour = wx.Colour(rend, gend, bend)
+
+
+    def DrawAquaTheme(self, dc, rect, style, selected):
+        """ Draws Mac-Style Aqua Theme On Tabs. """
+
+        x, y, w, h = rect
+
+        if selected:
+            if style == 1:  # Dark Aqua
+                r1 = topaqua1[0].Red()
+                g1 = topaqua1[0].Green()
+                b1 = topaqua1[0].Blue()
+
+                r2 = topaqua1[1].Red()
+                g2 = topaqua1[1].Green()
+                b2 = topaqua1[1].Blue()
+            else:
+                r1 = topaqua2[0].Red()
+                g1 = topaqua2[0].Green()
+                b1 = topaqua2[0].Blue()
+
+                r2 = topaqua2[1].Red()
+                g2 = topaqua2[1].Green()
+                b2 = topaqua2[1].Blue()
+
+        else:
+            r1 = distaqua[0].Red()
+            g1 = distaqua[0].Green()
+            b1 = distaqua[0].Blue()
+
+            r2 = distaqua[1].Red()
+            g2 = distaqua[1].Green()
+            b2 = distaqua[1].Blue()
+
+        flrect = float((h-2)/2)
+
+        rstep = float((r2 - r1)) / flrect
+        gstep = float((g2 - g1)) / flrect
+        bstep = float((b2 - b1)) / flrect
+
+        rf, gf, bf = 0, 0, 0
+
+        counter = 0
+        dc.SetPen(wx.TRANSPARENT_PEN)
+
+        if self._style & NC_ROTATE:
+            startrange, endrange = (x, w)
+        else:
+            startrange, endrange = (y, h)
+        if self._style & NC_ROTATE and self._style & NC_RIGHT:
+            bandrange = xrange(startrange+endrange, startrange+endrange/2, -1)
+        else:
+            bandrange = xrange(startrange+1, startrange+endrange/2)
+
+        for band in bandrange:
+            currCol = (int(round(r1 + rf)), int(round(g1 + gf)), int(round(b1 + bf)))
+            dc.SetBrush(wx.Brush(currCol, wx.SOLID))
+            if self._style & NC_ROTATE:
+                if counter == 0:
+                    ypos = y + 2
+                    yend = h - 4
+                elif counter == 1:
+                    ypos = y + 1
+                    yend = h - 2
+                else:
+                    ypos = y + 1
+                    yend = h - 2
+
+                dc.DrawRectangle(band, ypos, 1, yend)
+            else:
+                if counter == 0:
+                    xpos = x + 2
+                    xend = w - 4
+                elif counter == 1:
+                    xpos = x + 1
+                    xend = w - 2
+                else:
+                    xpos = x + 1
+                    xend = w - 2
+
+                dc.DrawRectangle(xpos, band, xend, 1)
+
+            counter = counter + 1
+
+            rf = rf + rstep
+            gf = gf + gstep
+            bf = bf + bstep
+
+        if selected:
+            if style == 1:  # Dark Aqua
+                r1 = botaqua1[0].Red()
+                g1 = botaqua1[0].Green()
+                b1 = botaqua1[0].Blue()
+
+                r2 = botaqua1[1].Red()
+                g2 = botaqua1[1].Green()
+                b2 = botaqua1[1].Blue()
+            else:
+                r1 = botaqua2[0].Red()
+                g1 = botaqua2[0].Green()
+                b1 = botaqua2[0].Blue()
+
+                r2 = botaqua2[1].Red()
+                g2 = botaqua2[1].Green()
+                b2 = botaqua2[1].Blue()
+        else:
+            r1 = disbaqua[0].Red()
+            g1 = disbaqua[0].Green()
+            b1 = disbaqua[0].Blue()
+
+            r2 = disbaqua[1].Red()
+            g2 = disbaqua[1].Green()
+            b2 = disbaqua[1].Blue()
+
+        flrect = float((h-2)/2)
+
+        rstep = float((r2 - r1)) / flrect
+        gstep = float((g2 - g1)) / flrect
+        bstep = float((b2 - b1)) / flrect
+
+        rf, gf, bf = 0, 0, 0
+
+        counter = 0
+
+        if self._style & NC_ROTATE and self._style & NC_RIGHT:
+            bandrange = xrange(startrange+endrange/2, startrange+1, -1)
+        else:
+            bandrange = xrange(startrange+endrange/2, startrange+endrange)
+        for band in bandrange:
+            currCol = (int(round(r1 + rf)), int(round(g1 + gf)), int(round(b1 + bf)))
+            dc.SetBrush(wx.Brush(currCol, wx.SOLID))
+            if self._style & NC_ROTATE:
+                dc.DrawRectangle(band, y + 1, 1, h-2)
+            else:
+                dc.DrawRectangle(x+1, band, w-2, 1)
+            rf = rf + rstep
+            gf = gf + gstep
+            bf = bf + bstep
+
+        self._lastcolour = currCol
+
+
+    def DrawMetalTheme(self, dc, rect):
+        """ Draws Mac-Style Metal Gradient On Tabs. """
+
+        x, y, w, h = rect
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        counter = 0
+
+        if self._style & NC_ROTATE:
+            bandrange = xrange(x+1, x+w)
+        else:
+            bandrange = xrange(y+1, h+y)
+        for band in bandrange:
+            if self._style & NC_ROTATE:
+                intens = (230 + 80 * (x-band)/w)
+            else:
+                intens = (230 + 80 * (y-band)/h)
+
+            colour = wx.Colour(intens, intens, intens)
+            dc.SetBrush(wx.Brush(colour))
+
+            if self._style & NC_ROTATE:
+                if counter == 0:
+                    ypos = y + 2
+                    yend = h - 4
+                elif counter == 1:
+                    ypos = y + 1
+                    yend = h - 2
+                else:
+                    ypos = y + 1
+                    yend = h - 2
+                if self._style & NC_RIGHT:
+                    dc.DrawRectangle(x+w-band, ypos, 1, yend)
+                else:
+                    dc.DrawRectangle(x+band, ypos, 1, yend)
+
+            else:
+                if counter == 0:
+                    xpos = x + 2
+                    xend = w - 4
+                elif counter == 1:
+                    xpos = x + 1
+                    xend = w - 2
+                else:
+                    xpos = x + 1
+                    xend = w - 2
+                dc.DrawRectangle(xpos, band, xend, 1)
+
+            counter = counter + 1
+
+        self._lastcolour = colour
+
+
+    def DrawVerticalGradient(self, dc, rect, index):
+        """ Gradient Fill From Colour 1 To Colour 2 From Top To Bottom. """
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+
+        # calculate gradient coefficients
+        col2 = self._tabstyle.GetSecondGradientColour(index == self.GetSelection())
+        col1 = self._tabstyle.GetFirstGradientColour(index == self.GetSelection())
+
+        r1, g1, b1 = int(col1.Red()), int(col1.Green()), int(col1.Blue())
+        r2, g2, b2 = int(col2.Red()), int(col2.Green()), int(col2.Blue())
+
+        flrect = float(rect.height)
+
+        rstep = float((r2 - r1)) / flrect
+        gstep = float((g2 - g1)) / flrect
+        bstep = float((b2 - b1)) / flrect
+
+        rf, gf, bf = 0, 0, 0
+
+        counter = 0
+
+        bandrange = xrange(rect.y+1, rect.y + rect.height-1)
+        lenc = len(bandrange)
+
+        for y in bandrange:
+            currCol = (r1 + rf, g1 + gf, b1 + bf)
+
+            dc.SetBrush(wx.Brush(currCol, wx.SOLID))
+
+            # adjust along x-axis to preserve the curved tab edge
+            def GetXAdjust(counter):
+                if counter >=2 or counter <=lenc-2:
+                    return 1
+                if self._style & NC_LEFT or self._style & NC_RIGHT and not self._style & NC_ROTATE:
+                    if counter == 0 and self._style & NC_RIGHT or \
+                        counter == lenc - 1 and self._style & NC_LEFT:
+                        return 3
+                    elif counter == 1 and self._style & NC_RIGHT or \
+                        counter == lend - 2 and self._style & NC_LEFT:
+                        return 2
+                    else:
+                        return 1
+                else:
+                    if counter == lenc - 2:
+                        return 2
+                    elif counter == lenc - 1:
+                        return 3
+                    else:
+                        return 1
+
+            xadjust = GetXAdjust(counter)
+            xpos = rect.x + xadjust
+            xend = rect.width - xadjust
+
+
+            counter = counter + 1
+
+            dc.DrawRectangle(xpos, y, xend, 1)
+            rf = rf + rstep
+            gf = gf + gstep
+            bf = bf + bstep
+
+        self._lastcolour = currCol
+
+
+    def DrawHorizontalGradient(self, dc, rect, index):
+        """ Gradient Fill From Colour 1 To Colour 2 From Left To Right. """
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+
+        # calculate gradient coefficients
+        col2 = self._tabstyle.GetSecondGradientColour(index == self.GetSelection())
+        col1 = self._tabstyle.GetFirstGradientColour(index == self.GetSelection())
+
+        r1, g1, b1 = int(col1.Red()), int(col1.Green()), int(col1.Blue())
+        r2, g2, b2 = int(col2.Red()), int(col2.Green()), int(col2.Blue())
+
+        flrect = float(rect.width)
+
+        rstep = float((r2 - r1)) / flrect
+        gstep = float((g2 - g1)) / flrect
+        bstep = float((b2 - b1)) / flrect
+
+        rf, gf, bf = 0, 0, 0
+        counter = 0
+
+        bandrange = xrange(rect.x + 1, rect.x + rect.width - 1)
+        lenc = len(bandrange)
+
+        for x in bandrange:
+            currCol = (r1 + rf, g1 + gf, b1 + bf)
+
+            dc.SetBrush(wx.Brush(currCol, wx.SOLID))
+            # adjust along y-axis to preserve the curved tab edge
+            def GetYAdjust(counter):
+                if counter >=2 or counter <=lenc-2:
+                    return 1
+                if self._style & NC_TOP or self._style & NC_BOTTOM or \
+                    (self._style & NC_LEFT and self._style & NC_ROTATE):
+                    if counter == 0 or counter == lenc - 1 and not self._style & NC_LEFT:
+                        return 3
+                    elif counter == 1 or counter == lenc - 2 and not self._style & NC_LEFT:
+                        return 2
+                    else:
+                        return 1
+                else:
+                    if counter == lenc - 2:
+                        return 2
+                    elif counter == lenc - 1:
+                        return 3
+                    else:
+                        return 1
+
+            yadjust = GetYAdjust(counter)
+            ypos = rect.y + yadjust
+            yend = rect.height - yadjust
+
+            counter = counter + 1
+
+            dc.DrawRectangle(x, ypos, 1, yend)
+            rf = rf + rstep
+            gf = gf + gstep
+            bf = bf + bstep
+
+        self._lastcolour = currCol
+
+
+    def GetAllTextExtents(self, dc):
+        """ Returns All Tabs Text Extents. Used Internally. """
+
+        self._mintabwidths = []
+        self._maxtabwidths = []
+        self._mintabheights = []
+        self._maxtabheights = []
+        self._incrtext = []
+        minheight = 0
+
+        for ii in xrange(self.GetPageCount()):
+
+            txts = self.GetPageText(ii)
+            font1 = self.GetPageTextFont(ii)
+            dc.SetFont(font1)
+            w1, h1 = dc.GetTextExtent(txts)
+            minheight = max(minheight, h1)
+            self._mintabwidths.append(w1)
+            self._mintabheights.append(h1)
+            font2 = self.GetPageTextSecondaryFont(ii)
+            dc.SetFont(font2)
+            w2, h2 = dc.GetTextExtent(txts)
+            minheight = max(minheight, h2)
+
+            self._maxtabwidths.append(w2)
+            self._maxtabheights.append(h2)
+            self._incrtext.append(abs(self._mintabwidths[ii] - self._maxtabwidths[ii]))
+
+        mh1 = max(self._mintabheights)
+        font1 = self.GetPageTextFont(self._mintabheights.index(mh1))
+        mh2 = max(self._maxtabheights)
+        font2 = self.GetPageTextSecondaryFont(self._maxtabheights.index(mh2))
+
+        mhend = max(mh1, mh2)
+
+        if mhend == mh1:
+            maxfont = font1
+        else:
+            maxfont = font2
+
+        minheight = self.GetSize()[1]
+
+        return  minheight, maxfont
+
+
+    def DrawBuiltinStyle(self, dc, style, rect, index, selection):
+        """ Methods That Holds All The Theme Styles. """
+
+        if style._aqua:
+            if self._selstyle._normal:
+                self.DrawAquaTheme(dc, rect, style._aqua, index==selection)
+            else:
+                oldselstyle = self._selstyle[:]
+                self._selstyle._normal = True
+                self.DrawBuiltinStyle(dc, self._selstyle, rect, index, selection)
+                self._selstyle = oldselstyle
+
+        elif style._metal:
+            if self._selstyle._normal:
+                self.DrawMetalTheme(dc, rect)
+            else:
+                oldselstyle = self._selstyle[:]
+                self._selstyle._normal = True
+                self.DrawBuiltinStyle(dc, self._selstyle, rect, index, selection)
+                self._selstyle = oldselstyle
+
+        elif style._kdetheme:
+            if self._selstyle._normal:
+                self.DrawKDETheme(dc, rect)
+            else:
+                oldselstyle = self._selstyle[:]
+                self._selstyle._normal = True
+                self.DrawBuiltinStyle(dc, self._selstyle, rect, index, selection)
+                self._selstyle = oldselstyle
+
+        elif style._macstyle:
+            if self._selstyle._normal:
+                self.DrawMacTheme(dc, rect, style._macstyle)
+            else:
+                oldselstyle = self._selstyle[:]
+                self._selstyle._normal = True
+                self.DrawBuiltinStyle(dc, self._selstyle, rect, index, selection)
+                self._selstyle = oldselstyle
+
+        elif style._gradient:
+            if self._selstyle._normal:
+                if style._gradient & ThemeStyle.GRADIENT_VERTICAL:
+                    self.DrawVerticalGradient(dc, rect, index)
+                else:
+                    self.DrawHorizontalGradient(dc, rect, index)
+            else:
+                oldselstyle = self._selstyle[:]
+                self._selstyle._normal = True
+                self.DrawBuiltinStyle(dc, self._selstyle, rect, index, selection)
+                self._selstyle = oldselstyle
+
+        elif style._silver:
+            if self._selstyle._normal:
+                self.DrawSilverTheme(dc, rect, index==selection)
+            else:
+                oldselstyle = self._selstyle[:]
+                self._selstyle._normal = True
+                self.DrawBuiltinStyle(dc, self._selstyle, rect, index, selection)
+                self._selstyle = oldselstyle
+
+
+    def DrawGradientOnTab(self, dc, rect, col1, col2):
+        """ Draw A Gradient Coloured Tab. """
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+
+        r1, g1, b1 = int(col1.Red()), int(col1.Green()), int(col1.Blue())
+        r2, g2, b2 = int(col2.Red()), int(col2.Green()), int(col2.Blue())
+
+        flrect = float(rect.height)
+
+        rstep = float((r2 - r1)) / flrect
+        gstep = float((g2 - g1)) / flrect
+        bstep = float((b2 - b1)) / flrect
+
+        rf, gf, bf = 0, 0, 0
+
+        counter = 0
+
+        for y in xrange(rect.y+1, rect.y + rect.height):
+            currCol = (r1 + rf, g1 + gf, b1 + bf)
+
+            dc.SetBrush(wx.Brush(currCol, wx.SOLID))
+            if counter == 0:
+                xpos = rect.x + 2
+                xend = rect.width - 4
+            elif counter == 1:
+                xpos = rect.x + 1
+                xend = rect.width - 2
+            else:
+                xpos = rect.x
+                xend = rect.width
+
+            counter = counter + 1
+
+            dc.DrawRectangle(xpos, y, xend, 1)
+            rf = rf + rstep
+            gf = gf + gstep
+            bf = bf + bstep
+
+        self._lastcolour = currCol
+
+    def _CalcBestWidth(self, dc):
+        return max(self._CalcMaxTabWidth(dc), self._CalcSizeToggleBestSize()[0])
+
+    def _CalcMaxTabWidth(self, dc):
+        self._CalcMaxTextHeight(dc)
+        textWidth = max(self._maxtabwidths)
+        tabIndex = self._maxtabwidths.index(textWidth)
+        bmpWidth, bmpHeight = self._CalcTabBitmapSize(tabIndex)
+        tabrect = self._CalcTabRect(tabIndex, 0, 0, textWidth, bmpWidth, bmpHeight)
+        # return the width based on the longest label, plus 3 for
+        # the additional width of the selected tab
+        return tabrect.width + 3
+
+    def _CalcMaxTextHeight(self, dc):
+        if self._somethingchanged:
+            minheight, maxfont = self.GetAllTextExtents(dc)
+            self._minheight = minheight
+            self._maxfont = maxfont
+        else:
+            minheight = self._minheight
+            maxfont = self._maxfont
+
+        dc.SetFont(maxfont)
+        _, height = dc.GetTextExtent("Aq")
+        self._maxtextheight = height
+
+    def _CalcSizeToggleBestSize(self):
+        if self._sizeToggleButton:
+            return self._sizeToggleButton.GetBestSize()
+        else:
+            return wx.Size(0,0)
+
+    def _CalcTabBitmapPosition(self, tabIndex, bmpWidth, bmpHeight, tabrect):
+
+        if self._style & NC_ROTATE:
+            bmpposx = tabrect.x + (tabrect.width - bmpWidth) / 2
+            yoffset = self._padding.x
+            if self._style & NC_LEFT:
+                bmpposx += 1
+                bmpposy = tabrect.y + tabrect.height - (yoffset + bmpHeight)
+            else:
+                bmpposy = tabrect.y + yoffset
+            if tabIndex == self.GetSelection():
+                bmpposx += self._style & NC_LEFT and -1 or 1
+        else:
+            bmpposx = tabrect.x + self._padding.x
+            bmpposy = tabrect.y + (tabrect.height - bmpHeight) / 2
+            if tabIndex == self.GetSelection() and self._style & NC_TOP:
+                bmpposy -= 1
+
+        return (bmpposx, bmpposy)
+
+    def _CalcTabBitmapSize(self, tabIndex):
+        result = (0, 0)
+        bmp = self._GetTabBitmap(tabIndex)
+        bmpOk = bmp.Ok()
+        if bmpOk:
+            result = (bmp.GetWidth(), bmp.GetHeight())
+        return result
+
+    def _CalcTabBitmapSpace(self, bmpWidth, bmpHeight):
+        space = self._padding.x
+        bmpSpace = self._style & NC_ROTATE and bmpHeight or bmpWidth
+        if bmpSpace:
+            space = space + self._padding.x + bmpSpace
+        return space
+
+    def _CalcTabRect(self, tabIndex, posx, posy, textWidth, bmpWidth, bmpHeight):
+
+        xpos = posx
+        if self._style & NC_BOTTOM:
+            ypos = 1
+        elif self._style & NC_TOP:
+            ypos = self.GetSize().y - self._maxtextheight - self._padding.y*2
+        else:
+            ypos = posy
+
+        xsize = textWidth + self._CalcTabBitmapSpace(bmpWidth, bmpHeight) + \
+            self._padding.x + self._incrtext[tabIndex] + \
+            self._CalcXWidth()
+
+        ysize = self._maxtextheight + self._padding.y*2
+        if self._style & NC_TOP:
+            ysize += 3
+
+        if self._style & NC_ROTATE:
+            xsize, ysize = (ysize, xsize)
+
+        if tabIndex == self.GetSelection():
+            if self._style & NC_TOP or self._style & NC_BOTTOM:
+                xsize = xsize + self._spacetabs
+                if tabIndex > 0:
+                    xpos = xpos - self._spacetabs
+                    xsize = xsize + self._spacetabs
+                if self._style & NC_TOP:
+                    ypos -= 3
+                ysize = ysize + 2
+            else:
+                xsize += 3
+
+        if self._style & NC_LEFT:
+            xpos = self.GetSize().width - xsize
+        return wx.Rect(xpos, ypos, xsize, ysize)
+
+    def _CalcTabTextPosition(self, tabIndex, tabrect, space):
+        xtextpos = tabrect.x + space + self._incrtext[tabIndex]/2
+
+        if self._style & NC_BOTTOM:
+            ytextpos = self._padding.y
+        else:
+            ytextpos = tabrect.y + self._padding.y + 1
+        if tabIndex == self.GetSelection():
+            if tabIndex == 0 and self._style & NC_TOP or self._style & NC_BOTTOM:
+                xtextpos = xtextpos + self._spacetabs/2.0 + 1
+            if self._style & NC_BOTTOM:
+                ytextpos += 2
+            elif self._style & NC_TOP:
+                ytextpos -= 2
+
+        if self._style & NC_ROTATE:
+            xoffset = ytextpos - tabrect.y
+            yoffset = xtextpos - tabrect.x
+            if self._style & NC_LEFT:
+                xtextpos, ytextpos = (tabrect.x + xoffset - 1,
+                    tabrect.y + tabrect.height - yoffset)
+            else:
+                yoffset += self._CalcXWidth()
+                xtextpos, ytextpos = (tabrect.x + tabrect.width - xoffset,
+                    tabrect.y + yoffset)
+
+        return (xtextpos, ytextpos)
+
+    def _CalcTabTextWidth(self, dc, tabIndex):
+        if self._style & NC_FIXED_WIDTH:
+            result = max(self._maxtabwidths)
+        else:
+            dc.SetFont(self.GetPageTextFont(tabIndex))
+            result, _ = dc.GetTextExtent(self.GetPageText(tabIndex))
+        return result
+
+    def _CalcXRect(self, tabrect):
+        result = None
+        drawx, dxstyle = self.GetDrawX()
+        if drawx:
+            if dxstyle == 1:
+                mins = min(self._padding.x, self._padding.y) + 1
+                mins = min(mins, 6)
+                xoffset = tabrect.width-mins-3
+                yoffset = 2
+                xsize = ysize = mins+1
+            else:
+                if self._style & NC_ROTATE:
+                    xoffset = (tabrect.width-self._maxtextheight-self._padding.y/2)/2
+                    yoffset = self._padding.x/2
+                else:
+                    xoffset = tabrect.width-self._maxtextheight-self._padding.x
+                    yoffset = (tabrect.height-self._maxtextheight-self._padding.y/2)/2
+                xsize = ysize = self._maxtextheight
+            result = wx.Rect(tabrect.x+xoffset, tabrect.y+yoffset, xsize, ysize)
+        return result
+
+    def _CalcXWidth(self):
+        drawx, dxstyle = self.GetDrawX()
+        if drawx:
+            if dxstyle == 1:
+                xxspace = self._padding.x/2
+            else:
+                xxspace = self._padding.x + self._maxtextheight
+        else:
+            xxspace = 0
+        return xxspace
+
+    def _ClipAtPaperEdge(self, dc, tabrect, tabIndex):
+        selected = tabIndex == self.GetSelection()
+        if self._style & NC_TOP:
+            cliprect = (tabrect.x, tabrect.y, tabrect.width,
+                selected and tabrect.height - 2 or tabrect.height-3)
+        elif self._style & NC_LEFT:
+            cliprect = (tabrect.x, tabrect.y, tabrect.width - 2, tabrect.height)
+        elif self._style & NC_BOTTOM:
+            cliprect = (tabrect.x, tabrect.y + 2, tabrect.width, tabrect.height - 2)
+        else:
+            cliprect = (tabrect.x + 2, tabrect.y, tabrect.width - 2, tabrect.height)
+        dc.SetClippingRegion(*cliprect)
+
+    def _CreateSizeToggleButton(self):
+        buttonlabel = self._expanded and "<<" or ">>"
+        self._sizeToggleButton = wx.Button(self, wx.NewId(),
+            pos = wx.Point(0,0,), label = buttonlabel, style=wx.BU_EXACTFIT)
+        font = self._sizeToggleButton.GetFont()
+        if font.GetPointSize() > 6:
+            font.SetPointSize(6)
+            self._sizeToggleButton.SetFont(font)
+        self.Bind(wx.EVT_BUTTON, self._ToggleSize, self._sizeToggleButton)
+
+
+    def _DrawBackground(self, dc, paintTools):
+        #background
+        size = self.GetSize()
+        dc.SetBrush(paintTools.BackBrush)
+
+        if not (self._style & wx.NO_BORDER):
+            # full border
+            dc.SetPen(paintTools.BorderPen)
+            dc.SetPen(paintTools.HighlightPen)
+            dc.DrawRectangle(0, 0, size.x, size.y)
+
+        else:
+            dc.SetPen(paintTools.BackPen)
+            dc.DrawRectangle(0, 0, size.x, size.y)
+            self._DrawPageEdge(dc, paintTools)
+
+    def _DrawFocusIndicator(self, dc, paintTools, tabrect):
+        if self.GetUseFocusIndicator():
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+            dc.SetPen(paintTools.FocusPen)
+            dc.DrawRoundedRectangle(tabrect.x+self._padding.x/2, tabrect.y+self._padding.y/2,
+                                    tabrect.width-self._padding.x,
+                                    tabrect.height-self._padding.y-2, 2)
+
+    def _DrawPageEdge(self, dc, paintTools):
+        if self._style & NC_TOP:
+            dc.SetPen(paintTools.HighlightPen)
+            dc.DrawLine(0, self.GetSize().y-1, self.GetSize().x, self.GetSize().y-1)
+        else:
+            if not self._tabstyle._normal or self._usegradients:
+                dc.SetPen(paintTools.HighlightPen)
+            else:
+                dc.SetPen(paintTools.BorderPen)
+            if self._style & NC_BOTTOM:
+                dc.DrawLine(0, 1, self.GetSize().x, 1)
+            elif self._style & NC_LEFT:
+                dc.DrawLine(self.GetSize().width - 1, 0, self.GetSize().width - 1, self.GetSize().height)
+            elif self._style & NC_RIGHT:
+                dc.DrawLine(0, 0, 0, self.GetSize().height)
+
+    def _DrawTab(self, dc, paintTools, tabrect, tabIndex):
+        size = self.GetSize()
+        self._DrawTabGradientOutline(dc, paintTools, tabrect, tabIndex)
+        self._FillTab(dc, paintTools, tabrect, tabIndex)
+        self._DrawTabOutline(dc, paintTools, tabrect, tabIndex)
+        self._DrawTabPageEdge(dc, paintTools, tabrect, tabIndex)
+        self._HighlightTabEdge(dc, paintTools, tabrect)
+        self._ShadowTabEdge(dc, paintTools, tabrect)
+
+    def _DrawTabBitmap(self, dc, tabIndex, bmpposx, bmpposy):
+        bmpindex = self.GetPageImage(tabIndex)
+        if self.IsPageEnabled(tabIndex):
+            self._imglist.Draw(bmpindex, dc, bmpposx, bmpposy,
+                               wx.IMAGELIST_DRAW_TRANSPARENT, True)
+        else:
+            self._grayedlist.Draw(bmpindex, dc, bmpposx, bmpposy,
+                                  wx.IMAGELIST_DRAW_TRANSPARENT, True)
+
+    def _DrawTabGradientOutline(self, dc, paintTools, tabrect, tabIndex):
+        if not self._tabstyle._normal or self._usegradients:
+            if tabIndex != self.GetSelection() and self._style & NC_TOP:
+                dc.SetBrush(wx.TRANSPARENT_BRUSH)
+                dc.SetPen(paintTools.ShadowPen)
+                dc.DrawRoundedRectangle(tabrect.x+1, tabrect.y+1, tabrect.width, tabrect.height-1, 3)
+
+    def _DrawTabOutline(self, dc, paintTools, tabrect, tabIndex):
+        if not self._tabstyle._normal or self._usegradients:
+            dc.SetBrush(wx.TRANSPARENT_BRUSH)
+            dc.SetPen(paintTools.HighlightPen)
+        else:
+            dc.SetBrush(wx.Brush(self.GetPageColour(tabIndex)))
+            if self._style & NC_TOP:
+                dc.SetPen(paintTools.HighlightPen)
+            else:
+                dc.SetPen(paintTools.BorderPen)
+        self._ClipAtPaperEdge(dc, tabrect, tabIndex)
+        dc.DrawRoundedRectangle(tabrect.x, tabrect.y, tabrect.width, tabrect.height, 3)
+        dc.DestroyClippingRegion()
+
+    def _DrawTabPageEdge(self, dc, paintTools, tabrect, tabIndex):
+        if not self._tabstyle._normal or self._usegradients:
+            edgePen = paintTools.HighlightPen
+        else:
+            if self._style & NC_TOP:
+                edgePen = paintTools.HighlightPen
+            else:
+                edgePen = paintTools.BorderPen
+
+        if tabIndex == self.GetSelection():
+            # un-paint the line at the paper edge
+            cancelPen = wx.Pen(self.GetPageColour(tabIndex))
+            dc.SetPen(cancelPen)
+            if self._style & NC_BOTTOM:
+                dc.DrawLine(tabrect.x+1, tabrect.y, tabrect.x + tabrect.width, tabrect.y)
+            elif self._style & NC_LEFT:
+                dc.DrawLine(tabrect.x + tabrect.width-1, tabrect.y, tabrect.x + tabrect.width-1, tabrect.y + tabrect.height)
+            elif self._style & NC_RIGHT:
+                dc.DrawLine(tabrect.x, tabrect.y, tabrect.x, tabrect.y + tabrect.height)
+
+        if tabIndex != self.GetSelection():
+            if self._style & NC_TOP:
+                dc.DrawLine(tabrect.x, self.GetSize().y-1, tabrect.x + tabrect.width, self.GetSize().y-1)
+
+        # draw sharp corners at the paper edge
+        dc.SetPen(edgePen)
+        if self._style & NC_BOTTOM:
+            dc.DrawLine(tabrect.x, tabrect.y, tabrect.x, tabrect.y + 2)
+            dc.DrawLine((tabrect.x + tabrect.width)-1, tabrect.y,
+                (tabrect.x + tabrect.width)-1, tabrect.y + 2)
+        elif self._style & NC_LEFT:
+            dc.DrawLine(self.GetSize().width - 2, tabrect.y, self.GetSize().width, tabrect.y)
+            dc.DrawLine(self.GetSize().width - 2, tabrect.y + tabrect.height - 1, self.GetSize().width, tabrect.y + tabrect.height - 1)
+        elif self._style & NC_RIGHT:
+            dc.DrawLine(tabrect.x, tabrect.y, tabrect.x + 2, tabrect.y)
+            dc.DrawLine(tabrect.x, tabrect.y + tabrect.height - 1, tabrect.x + 2, tabrect.y + tabrect.height - 1)
+
+    def _DrawTabText(self, dc, tabIndex, xtextpos, ytextpos):
+        dc.SetFont(self.GetPageTextFont(tabIndex))
+        dc.SetTextForeground(self._GetTabTextColour(tabIndex))
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        if self._style & NC_ROTATE:
+            angle = (self._style & NC_LEFT) and 90.0 or 270.0
+            dc.DrawRotatedText(self.GetPageText(tabIndex), xtextpos, ytextpos, angle)
+        else:
+            dc.DrawText(self.GetPageText(tabIndex), xtextpos, ytextpos)
+
+    def _DrawX(self, dc, tabrect, xrect, textColour):
+        drawx, dxstyle = self.GetDrawX()
+        if drawx:
+            if dxstyle == 1:
+                mins = min(self._padding.x, self._padding.y) + 1
+                mins = min(mins, 6)
+                dc.SetPen(wx.Pen(textColour, 1))
+                dc.SetBrush(wx.TRANSPARENT_BRUSH)
+                dc.DrawLine(xrect.x, xrect.y, tabrect.x+tabrect.width-2, tabrect.y+3+mins)
+                dc.DrawLine(xrect.x, xrect.y+mins, tabrect.x+tabrect.width-2, tabrect.y+1)
+                dc.DrawRectangle(xrect.x, xrect.y, xrect.width, xrect.height)
+            elif dxstyle == 2:
+                dc.SetPen(wx.Pen(textColour))
+                dc.SetBrush(wx.Brush(textColour))
+                dc.DrawRoundedRectangle(xrect.x, xrect.y, xrect.width, xrect.height, 2)
+                dc.SetPen(wx.Pen(self.GetBackgroundColour(), 2))
+                dc.DrawLine(xrect.x+2, xrect.y+2, xrect.x+xrect.width-3, xrect.y+xrect.height-3)
+                dc.DrawLine(xrect.x+2, xrect.y+xrect.height-3, xrect.x+xrect.width-3, xrect.y+2)
+            else:
+                self._imglist2.Draw(0, dc, xrect.x, xrect.y, wx.IMAGELIST_DRAW_TRANSPARENT, True)
+
+    def _EnhanceMultiSelectedTab(self, dc, tabIndex, tabrect):
+        dc.SetPen(wx.Pen(self._GetTabTextColour(tabIndex), 1, wx.DOT_DASH))
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        dc.DrawRoundedRectangle(tabrect.x+self._padding.x/2+1,
+            tabrect.y+self._padding.y/2+1,
+            tabrect.width-self._padding.x-2,
+            tabrect.height-self._padding.y-2-2, 2)
+
+    def _EnhanceSelectedTab(self, dc, paintTools, tabrect):
+        xselpos = tabrect.x
+        xselsize = tabrect.width
+        yselsize = tabrect.height
+
+        if self._style & NC_BOTTOM:
+            yselpos = (tabrect.y + tabrect.height) - 2
+        elif self._style & NC_TOP:
+            yselpos = tabrect.y
+
+        self._HighlightSelectedTabEdge(dc, paintTools, tabrect)
+        self._ShadowTabEdge(dc, paintTools, tabrect)
+        self._DrawFocusIndicator(dc, paintTools, tabrect)
+
+    def _FillTab(self, dc, paintTools, tabrect, tabIndex):
+        if self._usegradients:
+            self.DrawGradientOnTab(dc, tabrect, self._pages[tabIndex]._firstcolour,
+                                    self._pages[tabIndex]._secondcolour)
+        elif not self._tabstyle._normal:
+            self.DrawBuiltinStyle(dc, self._tabstyle, tabrect, tabIndex,
+                self.GetSelection())
+
+    def _GetPaintTools(self):
+        back_colour = self.GetBackgroundColour()
+        back_brush = wx.Brush(back_colour)
+        back_pen = wx.Pen(back_colour)
+
+        border_pen = self._borderpen
+        highlightpen = self._highlightpen
+        if self._tabstyle._normal and not self._usegradients:
+            highlightpen = self._highlightpen2
+
+        shadowpen = self._shadowpen
+        upperhighpen = self._upperhigh
+
+        if self.GetHighlightSelection():
+            selectionpen = wx.Pen(self._selectioncolour)
+            selectionEdgePen = wx.Pen(self._selectionedgecolour)
+        else:
+            selectionpen = selectionEdgePen = None
+
+        x_pen = self.GetDrawX() == 1 and wx.BLACK_PEN or None
+        focusindpen = self.GetUseFocusIndicator() and self._focusindpen or None
+
+        return _TabCtrlPaintTools(back_brush, back_pen, border_pen, highlightpen,
+            shadowpen, upperhighpen, selectionpen, selectionEdgePen, x_pen,
+            focusindpen)
+
+    def _GetTabBitmap(self, tabIndex):
+        bmp = wx.NullBitmap
+        if self.GetPageImage(tabIndex) >= 0:
+            bmpindex = self.GetPageImage(tabIndex)
+            if self.IsPageEnabled(tabIndex):
+                bmp = self._imglist.GetBitmap(bmpindex)
+            else:
+                bmp = self._grayedlist.GetBitmap(bmpindex)
+        return bmp
+
+    def _GetTabTextColour(self, tabIndex):
+        if self.IsPageEnabled(tabIndex):
+            result = self.GetPageTextColour(tabIndex)
+        else:
+            result = self._disabledcolour
+        return result
+
+    def _GetThemePageColour(self, index):
+        if self._tabstyle._macstyle:
+            return NC_MAC_LIGHT
+        elif self._tabstyle._kdetheme:
+            return kdetheme[0]
+        elif self._tabstyle._aqua:
+            if index == self.GetSelection():
+                return topaqua1[0]
+            else:
+                return distaqua[0]
+        elif self._tabstyle._metal:
+            intens = (230 + 80 * (self._tabrect[0].y-self._tabrect[0].y+1)/self._tabrect[0].height)
+            return wx.Colour(intens, intens, intens)
+        elif self._tabstyle._silver:
+            if index == self.GetSelection():
+                return silvertheme1[0]
+            else:
+                return silvertheme2[0]
+        elif self._tabstyle._gradient:
+            color = wx.WHITE
+            if self._tabstyle._gradient & ThemeStyle.GRADIENT_VERTICAL:
+                if  self._style & NC_TOP:
+                    color = self._tabstyle.GetSecondGradientColour(index)
+                elif self._style & NC_BOTTOM:
+                    color = self._tabstyle.GetFirstGradientColour(index)
+            elif self._tabstyle._gradient & ThemeStyle.GRADIENT_HORIZONTAL and \
+                self._style & NC_ROTATE:
+                    if self._style & NC_LEFT:
+                        color = self._tabstyle.GetSecondGradientColour(index)
+                    else:
+                        color = self._tabstyle.GetFirstGradientColour(index)
+            return color
+
+    def _HighlightSelectedTabEdge(self, dc, paintTools, tabrect):
+        if self._style & NC_ROTATE:
+            yselpos = tabrect.y + 3
+            yselsize = tabrect.height - 6
+            xselpos = self._style & NC_RIGHT and tabrect.x + tabrect.width - 1 or tabrect.x
+            if self.GetHighlightSelection():
+                dc.SetBrush(paintTools.BackBrush)
+                dc.SetPen(paintTools.SelectionEdgePen)
+                dc.DrawLine(xselpos, yselpos, xselpos, yselpos + yselsize)
+                dc.SetPen(paintTools.SelectionPen)
+                for band in range(2):
+                    if self._style & NC_RIGHT:
+                        xselpos -= 1
+                    else:
+                        xselpos += 1
+                    yselpos -= 1
+                    yselsize += 2
+                    dc.DrawLine(xselpos, yselpos, xselpos, yselpos + yselsize)
+            else:
+                dc.SetPen(paintTools.HighlightPen)
+                dc.DrawLine(xselpos, yselpos, xselpos, yselpos + yselsize)
+        else:
+            xselpos = tabrect.x + 3
+            xselsize = tabrect.width - 6
+
+            if self._style & NC_BOTTOM:
+                yselpos = tabrect.y + tabrect.height - 1
+            else:
+                yselpos = tabrect.y
+                dc.SetPen(paintTools.HighlightPen)
+                dc.DrawLine(xselpos, yselpos, xselpos + xselsize, yselpos)
+
+            if self.GetHighlightSelection():
+                dc.SetBrush(paintTools.BackBrush)
+                dc.SetPen(paintTools.SelectionEdgePen)
+                dc.DrawLine(xselpos, yselpos, xselpos + xselsize, yselpos)
+                dc.SetPen(paintTools.SelectionPen)
+                for band in range(2):
+                    if self._style & NC_BOTTOM:
+                        yselpos -= 1
+                    else:
+                        yselpos += 1
+                    xselpos -= 1
+                    xselsize += 2
+                    dc.DrawLine(xselpos, yselpos, xselpos + xselsize, yselpos)
+
+    def _HighlightTabEdge(self, dc, paintTools, tabrect):
+        if not self._tabstyle._normal or self._usegradients:
+            if self._style & NC_TOP:
+                dc.SetPen(paintTools.UpperHighlightPen)
+                dc.DrawLine(tabrect.x+2, tabrect.y-1, tabrect.x + tabrect.width - 2, tabrect.y-1)
+        else:
+            if self._style & NC_TOP:
+                dc.SetPen(paintTools.HighlightPen)
+                dc.DrawLine(tabrect.x + 3, tabrect.y, tabrect.x + tabrect.width - 3, tabrect.y)
+
+    def _InitExpandableStyles(self, style):
+        self._expanded = not style & NC_ROTATE
+        if self._expanded:
+            self._expandedstyle = style
+            self._contractedstyle = style | NC_ROTATE
+        else:
+            self._contractedstyle = style
+            self._expandedstyle = (style ^ NC_ROTATE) | NC_FIXED_WIDTH
+
+    def _InitExpandableTabStyles(self, style, expanded, tabstyle):
+        if tabstyle._gradient:
+            alternatestyle = ThemeStyle()
+            firstcolor = tabstyle.GetFirstGradientColour()
+            secondcolor = tabstyle.GetSecondGradientColour()
+            swapcolors = (tabstyle._gradient & ThemeStyle.GRADIENT_VERTICAL and style & NC_RIGHT and expanded) or \
+                    (tabstyle._gradient & ThemeStyle.GRADIENT_HORIZONTAL and style & NC_RIGHT and not expanded)
+            if swapcolors:
+                firstcolor, secondcolor = (secondcolor, firstcolor)
+            if tabstyle._gradient & ThemeStyle.GRADIENT_VERTICAL:
+                othergradient = (tabstyle._gradient ^ ThemeStyle.GRADIENT_VERTICAL) | ThemeStyle.GRADIENT_HORIZONTAL
+            else:
+                othergradient = (tabstyle._gradient ^ ThemeStyle.GRADIENT_HORIZONTAL) | ThemeStyle.GRADIENT_VERTICAL
+            alternatestyle.EnableGradientStyle(True, othergradient)
+            alternatestyle.SetFirstGradientColour(firstcolor)
+            alternatestyle.SetSecondGradientColour(secondcolor)
+            if tabstyle._gradient & ThemeStyle.DIFFERENT_GRADIENT_FOR_SELECTED:
+                firstcolor = tabstyle.GetFirstGradientColour(True)
+                secondcolor = tabstyle.GetSecondGradientColour(True)
+                if swapcolors:
+                    firstcolor, secondcolor = (secondcolor, firstcolor)
+                alternatestyle.SetFirstGradientColourSelected(firstcolor)
+                alternatestyle.SetSecondGradientColourSelected(secondcolor)
+            if expanded:
+                self._expandedtabstyle = tabstyle
+                self._contractedtabstyle = alternatestyle
+            else:
+                self._contractedtabstyle = tabstyle
+                self._expandedtabstyle = alternatestyle
+        else:
+            self._expandedtabstyle = tabstyle
+            self._contractedtabstyle = tabstyle
+
+    def _OnStyleChange(self):
+        if self._style & NC_TOP or self._style & NC_BOTTOM:
+            self.SetBestSize((-1, newheight))
+        else:
+            self.SetBestSize((self._CalcBestWidth(wx.ClientDC(self)), -1))
+        self._parent.GetSizer().Layout()
+        self._somethingchanged = True
+        self._firsttime = True
+        self.Refresh()
+
+    def _ShadowTabEdge(self, dc, paintTools, tabrect):
+        dc.SetPen(paintTools.ShadowPen)
+        if self._style & NC_BOTTOM:
+            dc.DrawLine((tabrect.x + tabrect.width), tabrect.y+1,
+                (tabrect.x+tabrect.width), tabrect.y + tabrect.height-4)
+        elif self._style & NC_TOP:
+            dc.DrawLine(tabrect.x + tabrect.width, tabrect.y+3,
+                tabrect.x+tabrect.width, tabrect.y+tabrect.height-4)
+
+    def OnPaint(self, event):
+        """ Handles The wx.EVT_PAINT Event For TabCtrl. """
+
+        dc = wx.BufferedPaintDC(self)
+
+        if self.GetPageCount() == 0:
+            event.Skip()
+            return
+
+        pt = self._GetPaintTools()
+
+        dc.BeginDrawing()
+
+        self._DrawBackground(dc, pt)
+
+        self._CalcMaxTextHeight(dc)
+
+        posx = self._firsttabpos.x
+        posy = self._firsttabpos.y
+
+        if self._style & NC_LEFT:
+            _ = 1
+
+        if self._firsttime:
+            if not hasattr(self, "_initrect"):
+                self._initrect = []
+            if self.HasSpinButton() and self._fromdnd:
+                self._firstvisible = self._spinbutton.GetValue()
+                self._firsttime = False
+                self._fromdnd = False
+            else:
+                self._initrect = []
+                self._firstvisible = 0
+        else:
+            if self.HasSpinButton():
+                self._firstvisible = self._spinbutton.GetValue()
+            else:
+                self._firstvisible = 0
+
+        lastvisible = self.GetPageCount()
+
+        #and tabs
+        oncount = -1
+
+        self._tabvisible = [1]*self.GetPageCount()
+
+        tabrect = []
+        # some theme style rendering routines expect this to exist, so
+        # set it now:
+        self._tabrect = tabrect
+        Xrect = []
+
+        for ii in xrange(self._firstvisible, lastvisible):
+            if not self._enablehiding or not self._pages[ii]._ishidden:
+
+                oncount = oncount + 1
+
+                self._tabvisible[ii] = 1
+
+                newwidth = self._CalcTabTextWidth(dc, ii)
+
+                bmpWidth, bmpHeight = self._CalcTabBitmapSize(ii)
+
+                tabrect.append(self._CalcTabRect(ii, posx, posy,
+                    newwidth, bmpWidth, bmpHeight))
+
+                self._DrawTab(dc, pt, tabrect[-1], ii)
+
+                self._DrawTabText(dc, ii, *self._CalcTabTextPosition(ii,
+                    tabrect[-1], self._CalcTabBitmapSpace(bmpWidth, bmpHeight)))
+
+                if bmpWidth:
+                    self._DrawTabBitmap(dc, ii, *self._CalcTabBitmapPosition(ii,
+                        bmpWidth, bmpHeight, tabrect[-1]))
+
+                if self.GetSelection() in [ii, ii - 1]:
+                    # Handle this special case on the selected tab and
+                    # on the tab that follows it (if there is one), to ensure
+                    # proper rendering of the selected tab's right edge
+                    self._EnhanceSelectedTab(dc, pt, tabrect[self.GetSelection() - self._firstvisible])
+
+                if self.GetDrawX()[0]:
+                    Xrect.append(self._CalcXRect(tabrect[-1]))
+                    self._DrawX(dc, tabrect[-1], Xrect[-1],
+                        self._GetTabTextColour(ii))
+
+                if ii in self._selectedtabs:
+                    self._EnhanceMultiSelectedTab(dc, ii, tabrect[-1])
+
+                if self._style & NC_TOP or self._style & NC_BOTTOM:
+                    # horizontally positioned tabs along top or bottom
+                    posx = posx + tabrect[-1].width
+                else:
+                    # vertically stacked tabs along side
+                    posy = posy + tabrect[-1].height
+
+                if self._firsttime:
+                    self._initrect.append(tabrect[oncount])
+
+            else:
+
+                self._tabvisible[ii] = 0
+
+        self._xrect = Xrect
+
+        if self._firsttime:
+            self._firsttime = False
+
+        self.UpdateMenuButton(self.HasMenuButton())
+        self.UpdateSpinButton()
+
+        if self._enabledragging:
+            if self._isdragging and not self._isleaving:
+                self.DrawInsertionMark(dc, self._olddragpos)
+
+        dc.EndDrawing()
+
+
+# ---------------------------------------------------------------------------- #
+# Class NotebookCtrl
+# This Is The Main Class Implementation
+# ---------------------------------------------------------------------------- #
+
+class NotebookCtrl(wx.Panel):
+    """
+    Display one or more windows in a notebook.
+
+    B{Events}:
+        - B{EVT_NOTEBOOKCTRL_PAGE_CHANGING}: sent when the active
+            page in the notebook is changing
+        - B{EVT_NOTEBOOKCTRL_PAGE_CHANGED}: sent when the active
+            page in the notebook has changed
+        - B{EVT_NOTEBOOKCTRL_PAGE_CLOSING}: sent when a page in the
+            notebook is closing
+        - B{EVT_NOTEBOOKCTRL_PAGE_DND}: sent when a page has been
+            dropped onto the notebook in a drag-drop operation
+        - B{EVT_NOTEBOOKCTRL_PAGE_DCLICK}: sent when the user
+            double-clicks a tab in the notebook
+        - B{EVT_NOTEBOOKCTRL_PAGE_RIGHT}: sent when the user
+            clicks a tab in the notebook with the right mouse
+            button
+        - B{EVT_NOTEBOOKCTRL_PAGE_MIDDLE}: sent when the user
+            clicks a tab in the notebook with the middle mouse
+            button
+    """
+
+    def __init__(self, parent, id, pos=wx.DefaultPosition, size=wx.DefaultSize,
+                 style=NC_DEFAULT_STYLE, sizer=wx.HORIZONTAL, margin=2, name="NotebookCtrl"):
+        """
+        Default Class Constructor.
+
+        @param style: Style For The NotebookCtrl, Which May Be:
+          a) NC_TOP: NotebookCtrl Placed On Top (Default);
+          b) NC_BOTTOM: NotebookCtrl Placed At The Bottom;
+          c) NC_LEFT: NotebookCtrl Placed At The Left;
+          d) NC_RIGHT: NotebookCtrl Placed At The Right;
+          e) NC_FIXED_WIDTH: All Tabs Have The Same Width;
+          f) wx.NO_BORDER: Shows No Border For The Control (Default, Looks Better);
+          g) wx.STATIC_BORDER: Shows A Static Border On The Control.
+
+        @param sizer: The Sizer Orientation For The Sizer That Holds
+          All The Panels: Changing This Style Is Only Useful When You
+          Use The Tile Method. In This Case, If sizer=wx.HORIZONTAL,
+          All The Panels Will Be Shown In Columns, While If
+          sizer=wx.VERTICAL All The Panels Will Be Shown In Rows.
+
+        @param margin: An Integer Number Of Pixels That Add Space
+          Above TabCtrl If style=NC_TOP, Or Below It If
+          style=NC_BOTTOM
+        """
+
+        wx.Panel.__init__(self, parent, -1, style=wx.NO_FULL_REPAINT_ON_RESIZE |
+                          wx.CLIP_CHILDREN, name=name)
+
+        self.nb = TabCtrl(self, -1, pos, size, style)
+
+        self._notebookpages = []
+
+        if style & NC_TOP == 0 and style & NC_BOTTOM == 0 \
+            and style & NC_LEFT == 0 and style & NC_RIGHT == 0:
+            style = style | NC_TOP
+
+        if style & wx.NO_BORDER == 0 and \
+           style & wx.STATIC_BORDER == 0:
+            style = style | wx.NO_BORDER
+
+        self._style = style
+        self._showcolumns = False
+        self._showtabs = True
+        self._sizerstyle = sizer
+        self._custompanel = None
+        self._focusswitch = False
+        self._oldfocus = None
+
+        if style & NC_TOP or style & NC_BOTTOM:
+            self.sizer = wx.BoxSizer(wx.VERTICAL)
+            self.tabsizer = wx.BoxSizer(wx.VERTICAL)
+        else:
+            self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+            self.tabsizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        self.bsizer = wx.BoxSizer(sizer)
+
+        if style & NC_TOP or style & NC_BOTTOM:
+            tabBorderFlag = wx.LEFT | wx.RIGHT
+        else:
+            tabBorderFlag = wx.TOP | wx.BOTTOM
+
+        if style & NC_TOP or style & NC_LEFT:
+            self.sizer.Add(self.tabsizer, 0, wx.EXPAND | tabBorderFlag, 2)
+            self._AddMargin(style, margin)
+            self.tabsizer.Add(self.nb, 0, wx.EXPAND)
+            self.sizer.Add(self.bsizer, 1, wx.EXPAND)
+        elif style & NC_BOTTOM or style & NC_RIGHT:
+            self.sizer.Add(self.bsizer, 1, wx.EXPAND)
+            self.sizer.Add(self.tabsizer, 0, wx.EXPAND | tabBorderFlag, 2)
+            self.tabsizer.Add(self.nb, 0, wx.EXPAND)
+            self._AddMargin(style, margin)
+
+        self.SetSizer(self.sizer)
+
+        self.tabsizer.Show(self.nb, False)
+
+        self.sizer.Layout()
+        self.Bind(wx.EVT_MOTION, self.OnMouseMotion)
+        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
+
+
+    def OnKeyDown(self, event):
+        """
+        Handles The wx.EVT_KEY_DOWN Event For NotebookCtrl. This Is Only Processed
+        If The User Navigate Through Tabs With Ctrl-Tab Keyboard Navigation.
+        """
+
+        if event.GetKeyCode == wx.WXK_TAB:
+            if event.ControlDown():
+                sel = self.GetSelection()
+                if sel == self.GetPageCount() - 1:
+                    sel = 0
+                else:
+                    sel = sel + 1
+
+                while not self.IsPageEnabled(sel):
+                    sel = sel + 1
+                    if sel == self.GetPageCount() - 1:
+                        sel = 0
+
+                self.SetSelection(sel)
+
+        event.Skip()
+
+
+    def OnMouseMotion(self, event):
+        """ Handles The wx.EVT_MOTION Event For NotebookCtrl. """
+
+        if self.nb._enabledragging:
+
+            if event.Dragging() and not event.RightIsDown() and not event.MiddleIsDown():
+
+                tolerance = 2
+                pt = event.GetPosition()
+                dx = abs(pt.x - self.nb._dragstartpos.x)
+                dy = abs(pt.y - self.nb._dragstartpos.y)
+                if dx <= tolerance and dy <= tolerance:
+                    self.SetCursor(wx.STANDARD_CURSOR)
+                    return
+
+                self.SetCursor(self.nb._dragcursor)
+                self.nb._isdragging = True
+
+            else:
+
+                self.nb._isdragging = False
+                self.SetCursor(wx.STANDARD_CURSOR)
+
+        if self.nb._showtooltip:
+            if self.nb._istooltipshown:
+                pt = event.GetPosition()
+                self.nb._insidetab = self.nb.GetInsideTab(pt)
+                if  self.nb._insidetab < 0:
+                    try:
+                        self.nb._tipwindow.Destroy()
+                        self.nb._istooltipshown = False
+                    except:
+                        self.nb._istooltipshown = False
+
+                    self.nb.Refresh()
+
+        event.Skip()
+
+
+    def EnableChildFocus(self, enable=True):
+        """ Enables/Disables Sending EVT_NOTEBOOKCTRL_PAGE_CHANGING When In Tile Mode. """
+
+        self._focusswitch = enable
+
+
+    def FindFocusedPage(self, obj):
+        """ Find Which NotebookCtrl Page Has The Focus Based On Its Child Focus. """
+
+        while 1:
+            if obj in self._notebookpages:
+                return obj
+
+            try:
+                obj = obj.GetParent()
+            except:
+                return None
+
+        return None
+
+
+    def OnFocus(self, event):
+        """ Handles The wx.EVT_CHILD_FOCUS Event For NotebookCtrl. """
+
+        if not self._focusswitch:
+            event.Skip()
+            return
+
+        newfocus = self.FindFocusedPage(event.GetEventObject())
+
+        if newfocus == self._oldfocus or newfocus is None:
+            event.Skip()
+            return
+
+        self._oldfocus = newfocus
+
+        eventOut = NotebookCtrlEvent(wxEVT_NOTEBOOKCTRL_PAGE_CHANGING, self.GetId())
+
+        nPage = self._notebookpages.index(newfocus)
+        eventOut.SetSelection(nPage)
+        eventOut.SetOldSelection(self.GetSelection())
+        eventOut.SetEventObject(self)
+
+        if not self.GetEventHandler().ProcessEvent(eventOut):
+
+            # Program Allows The Page Change
+            self.nb._selection = nPage
+            eventOut.SetEventType(wxEVT_NOTEBOOKCTRL_PAGE_CHANGED)
+            eventOut.SetOldSelection(self.nb._selection)
+            self.GetEventHandler().ProcessEvent(eventOut)
+
+        event.Skip()
+
+
+    def AddPage(self, page, text, select=False, img=-1, hidden=False):
+        """
+        Add A Page To The Notebook.
+
+        @param page: Specifies The New Page;
+        @param text: The Tab Text;
+        @param select: Whether The Page Should Be Selected Or Not;
+        @param img: Specifies The Optional Image Index For The New Page.
+        """
+
+        self.Freeze()
+
+        oldselection = self.nb.GetSelection()
+
+        if self.GetPageCount() == 0:
+            if self.GetCustomPage() is not None:
+                self.bsizer.Detach(self._custompanel)
+                self._custompanel.Show(False)
+                self.bsizer.Layout()
+
+        self.bsizer.Add(page, 1, wx.EXPAND | wx.ALL, 2)
+
+        self.nb.AddPage(text, select, img, hidden)
+        self._notebookpages.append(page)
+
+        page.Bind(wx.EVT_CHILD_FOCUS, self.OnFocus)
+
+        if select:
+            if oldselection >= 0:
+               self.bsizer.Show(self.GetPage(oldselection), False)
+
+            self.nb.SetSelection(self.GetPageCount()-1)
+            self.bsizer.Layout()
+        else:
+            if oldselection >= 0:
+                self.bsizer.Show(page, False)
+            else:
+                self.bsizer.Show(page, True)
+                self.nb.SetSelection(self.GetPageCount()-1)
+                self.bsizer.Layout()
+
+        if self.GetPageCount() == 1:
+
+            self.bsizer.Show(page, True)
+
+            if self.nb._hideonsingletab:
+
+                self._ShowTabCtrl(False)
+
+            else:
+                self.nb.Show(True)
+                self._ShowTabCtrl(True)
+
+        else:
+
+            self.nb.Show(True)
+            self._ShowTabCtrl(True)
+
+        self.bsizer.Layout()
+        self.sizer.Layout()
+
+        self.Thaw()
+
+        self.Tile(self._showcolumns)
+        self.ShowTabs(self._showtabs)
+
+
+    def InsertPage(self, nPage, page, text, select=False, img=-1, hidden=False):
+        """
+        Insert A Page Into The Notebook.
+
+        @param page: Specifies The New Page;
+        @param nPage: Specifies The Position For The New Page;
+        @param text: The Tab Text;
+        @param select: Whether The Page Should Be Selected Or Not;
+        @param img: Specifies The Optional Image Index For The New Page.
+        @param hidden: C{True} to hide the page; C{False} to display it
+        """
+
+        if nPage < 0 or (self.GetSelection() >= 0 and nPage >= self.GetPageCount()):
+            raise "\nERROR: Invalid Notebook Page In InsertPage: (" + str(nPage) + ")"
+
+        self.Freeze()
+
+        oldselection = self.nb.GetSelection()
+
+        if self.GetPageCount() == 0:
+            if self.GetCustomPage() is not None:
+                self.bsizer.Detach(self._custompanel)
+                self._custompanel.Show(False)
+
+        if oldselection >= 0:
+            self.bsizer.Show(oldselection, False)
+            self.bsizer.Layout()
+
+        if oldselection >= nPage:
+            oldselection = oldselection + 1
+
+        self.nb.InsertPage(nPage, text, select, img, hidden)
+        self.bsizer.Insert(nPage, page, 1, wx.EXPAND | wx.ALL, 2)
+        self._notebookpages.insert(nPage, page)
+        self.bsizer.Layout()
+
+        page.Bind(wx.EVT_CHILD_FOCUS, self.OnFocus)
+
+        for ii in xrange(self.GetPageCount()):
+            self.bsizer.Show(ii, False)
+
+        self.bsizer.Layout()
+
+        if select:
+            self.bsizer.Show(nPage, True)
+            self.bsizer.Layout()
+        else:
+            if oldselection >= 0:
+                self.bsizer.Show(oldselection, True)
+                self.bsizer.Layout()
+            else:
+                self.bsizer.Show(nPage, True)
+
+        self.bsizer.Layout()
+
+        if self.GetPageCount() == 1:
+
+            if self.nb._hideonsingletab:
+
+                self._ShowTabCtrl(False)
+
+            else:
+
+                self.nb.Show(True)
+                self._ShowTabCtrl(True)
+
+        else:
+
+            self.nb.Show(True)
+            self._ShowTabCtrl(True)
+
+        self.sizer.Layout()
+
+        self.Thaw()
+
+        self.Tile(self._showcolumns)
+        self.ShowTabs(self._showtabs)
+
+
+    def GetPage(self, nPage):
+        """ Returns The Window At The Given Position nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPage: (" + str(nPage) + ")"
+
+        return self._notebookpages[nPage]
+
+
+    def DeleteAllPages(self):
+        """ Deletes All NotebookCtrl Pages. """
+
+        self.Freeze()
+
+        counter = self.GetPageCount() - 1
+
+        for ii in xrange(self.GetPageCount()):
+            self.bsizer.Detach(counter-ii)
+            panels = self.GetPage(counter-ii)
+            panels.Destroy()
+
+        self.nb.DeleteAllPages()
+        self._notebookpages = []
+        self.nb._selection = -1
+
+        self.nb.Show(False)
+
+        custom = self.GetCustomPage()
+
+        if custom is not None:
+            self.SetCustomPage(custom)
+            custom.Show(True)
+
+        self.bsizer.Layout()
+
+        self._ShowTabCtrl(False)
+
+        self.sizer.Layout()
+
+        self.Thaw()
+
+
+    def DeletePage(self, nPage):
+        """ Deletes The Page nPage, And The Associated Window. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In DeletePage: (" + str(nPage) + ")"
+
+        oldselection = self.GetSelection()
+
+        self.Freeze()
+
+        panel = self.GetPage(nPage)
+        self.bsizer.Detach(nPage)
+
+        self.bsizer.Layout()
+
+        self._notebookpages.pop(nPage)
+        self.nb.DeletePage(nPage)
+
+        panel.Destroy()
+
+        if self.GetPageCount() > 0:
+            if oldselection == nPage:
+                if self.GetSelection() > 0:
+                    self.SetSelection(self.GetSelection()-1)
+                else:
+                    self.SetSelection(self.GetSelection())
+                    self.bsizer.Show(self.GetSelection())
+                    self.bsizer.Layout()
+
+        if self.GetPageCount() == 0:
+            self.nb.Show(False)
+            self._ShowTabCtrl(False)
+
+            custom = self.GetCustomPage()
+
+            if custom is not None:
+                self.bsizer.Add(custom, 1, wx.EXPAND | wx.ALL, 2)
+                custom.Show(True)
+
+            self.bsizer.Layout()
+            self.sizer.Layout()
+            self.Thaw()
+            return
+
+        if self.GetPageCount() == 1:
+
+            if self.nb._hideonsingletab:
+
+                self._ShowTabCtrl(False)
+
+            else:
+
+                self.nb.Show(True)
+                self._ShowTabCtrl(True)
+
+        else:
+
+            self.nb.Show(True)
+            self._ShowTabCtrl(True)
+
+        self.sizer.Layout()
+
+        self.Thaw()
+
+        self.Tile(self._showcolumns)
+        self.ShowTabs(self._showtabs)
+
+
+    def SetSelection(self, nPage):
+        """
+        Sets The Current Tab Selection To The Given nPage. This Call Generates The
+        EVT_NOTEBOOKCTRL_PAGE_CHANGING Event.
+        """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetSelection: (" + str(nPage) + ")"
+
+        oldselection = self.GetSelection()
+
+        if oldselection == nPage:
+            return
+
+        self.nb.SetSelection(nPage)
+
+        self.Tile(self._showcolumns)
+        self.ShowTabs(self._showtabs)
+
+
+    def GetPageCount(self):
+        """ Returns The Number Of Pages In NotebookCtrl. """
+
+        return self.nb.GetPageCount()
+
+
+    def GetSelection(self):
+        """ Returns The Current Selection. """
+
+        return self.nb.GetSelection()
+
+
+    def GetImageList(self):
+        """ Returns The Image List Associated With The NotebookCtrl. """
+
+        return self.nb.GetImageList()
+
+
+    def SetImageList(self, imagelist):
+        """ Associate An Image List To NotebookCtrl. """
+
+        self.nb.SetImageList(imagelist)
+
+
+    def AssignImageList(self, imagelist):
+        """ Associate An Image List To NotebookCtrl. """
+
+        self.nb.AssignImageList(imagelist)
+
+
+    def GetPadding(self):
+        """ Returns The (Horizontal, Vertical) Padding Of The Text Inside Tabs. """
+
+        return self.nb.GetPadding()
+
+
+    def SetPadding(self, padding):
+        """ Sets The (Horizontal, Vertical) Padding Of The Text Inside Tabs. """
+
+        self.nb.SetPadding(padding)
+
+
+    def SetUseFocusIndicator(self, focus=True):
+        """ Globally Enables/Disables Tab Focus Indicator. """
+
+        self.nb.SetUseFocusIndicator(focus)
+
+
+    def GetUseFocusIndicator(self):
+        """ Returns Globally Enable/Disable State For Tab Focus Indicator. """
+
+        return self.nb.GetUseFocusIndicator()
+
+
+    def EnablePage(self, nPage, enable=True):
+        """ Enable/Disable The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In EnablePage: (" + str(nPage) + ")"
+
+        self.nb.EnablePage(nPage, enable)
+
+
+    def IsPageEnabled(self, nPage):
+        """ Returns Whether A Page Is Enabled Or Not. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In IsPageEnabled: (" + str(nPage) + ")"
+
+        return self.nb.IsPageEnabled(nPage)
+
+
+    def SetHighlightSelection(self, highlight=True):
+        """ Globally Enables/Disables Tab Highlighting On Tab Selection. """
+
+        self.nb.SetHighlightSelection(highlight)
+
+
+    def GetHighlightSelection(self):
+        """ Returns Globally Enable/Disable State For Tab Highlighting On Tab Selection. """
+
+        return self.nb.GetHighlightSelection()
+
+
+    def SetAnimationImages(self, nPage, imgarray):
+        """
+        Sets An Animation List Associated To The Given Page nPage.
+
+        @param nPage: The Given Page;
+        @param imgarray: A List Of Image Indexes Of Images Inside The
+          ImageList Associated To NotebookCtrl.
+        """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetAnimationImages: (" + str(nPage) + ")"
+
+        if not imgarray:
+            raise "\nERROR: Invalid Image Array In SetAnimationImages: (" + repr(imgarray) + ")"
+
+        if min(imgarray) < 0:
+            raise "\nERROR: Invalid Image Array In SetAnimationImages: (Min(ImgArray) = " + \
+                  str(min(imgarray)) + " < 0)"
+
+        if max(imgarray) > self.GetImageList().GetImageCount() - 1:
+            raise "\nERROR: Invalid Image Array In SetAnimationImages: (Max(ImgArray) = " + \
+                  str(max(imgarray)) + " > " + str(self.GetImageList().GetImageCount()-1) + ")"
+
+        self.nb.SetAnimationImages(nPage, imgarray)
+
+
+    def GetAnimationImages(self, nPage):
+        """ Returns The Animation Images List Associated To The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetAnimationImages: (" + str(nPage) + ")"
+
+        return self.nb.GetAnimationImages(nPage)
+
+
+    def StartAnimation(self, nPage, timer=500):
+        """ Starts The Animation On The Given Page nPage, With Refreshing Time Rate "timer". """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In StartAnimation: (" + str(nPage) + ")"
+
+        self.nb.StartAnimation(nPage, timer)
+
+
+    def StopAnimation(self, nPage):
+        """ Stops The Animation On The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In StopAnimation: (" + str(nPage) + ")"
+
+        self.nb.StopAnimation(nPage)
+
+
+    def EnableDragAndDrop(self, enable=True):
+        """ Globall Enables/Disables Tabs Drag And Drop. """
+
+        self.nb.EnableDragAndDrop(enable)
+
+
+    def EnableHiding(self, enable=True):
+        """ Globally Enables/Disables Hiding On Tabs In Runtime. """
+
+        self.nb.EnableHiding(enable)
+
+
+    def SetDrawX(self, drawx=True, style=1, image1=None, image2=None):
+        """
+        Globally Enables/Disables The Drawing Of A Closing "X" In The Tab.
+
+        @param drawx: C{True} to enable drawing a closing "X"; C{False} to
+          disable it
+        @param style: the style of the X to draw when C{drawx} is C{True};
+          possible values are:
+            - C{1}: Small "X" At The Top-Right Of The Tab;
+            - C{2}: Bigger "X" In The Middle Vertical Of The Tab (Like Opera Notebook);
+            - C{3}: Custom "X" Image Is Drawn On Tabs.
+        @param image1: if C{style} is C{3}, the image to use when drawing
+          the X on an unhighlighted tab
+        @param image2: if C{style} is C{3}, the image to use when drawing
+          the X on a highlighted tab
+        """
+
+        self.nb.SetDrawX(drawx, style, image1, image2)
+
+
+    def GetDrawX(self):
+        """
+        Returns The Enable/Disable State Of Drawing Of A Small "X" At The Top-Right Of
+        Every Page.
+        """
+
+        return self.nb.GetDrawX()
+
+
+    def SetImageToCloseButton(self, convert=True):
+        """ Set Whether The Tab Icon Should Be Converted To The Close Button Or Not. """
+
+        self.nb.SetImageToCloseButton(convert)
+
+
+    def GetImageToCloseButton(self):
+        """ Get Whether The Tab Icon Should Be Converted To The Close Button Or Not. """
+
+        return self.nb._convertimage
+
+
+    def HideOnSingleTab(self, hide=True):
+        """ Hides The TabCtrl When There Is Only One Tab In NotebookCtrl. """
+
+        self.nb.HideOnSingleTab(hide)
+
+        if self.GetPageCount() == 1:
+            if hide:
+                self._ShowTabCtrl(False)
+            else:
+                self._ShowTabCtrl(True)
+
+            self.sizer.Layout()
+
+
+    def SetPagePopupMenu(self, nPage, menu):
+        """ Sets A Popup Menu Specific To A Single Tab. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPagePopupMenu: (" + str(nPage) + ")"
+
+        self.nb.SetPagePopupMenu(nPage, menu)
+
+
+    def GetPagePopupMenu(self, nPage):
+        """ Returns The Popup Menu Associated To A Single Tab. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPagePopupMenu: (" + str(nPage) + ")"
+
+        return self.nb.GetPagePopupMenu(nPage)
+
+
+    def SetPageToolTip(self, nPage, tooltip="", timer=500, winsize=400):
+        """
+        Sets A ToolTip For The Given Page nPage.
+
+        @param nPage: The Given Page;
+        @param tooltip: The ToolTip String;
+        @param timer: The Timer After Which The Tip Window Is Popped Up;
+        @param winsize: The Maximum Width Of The Tip Window.
+        """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageToolTip: (" + str(nPage) + ")"
+
+        self.nb.SetPageToolTip(nPage, tooltip, timer, winsize)
+
+
+    def GetPageToolTip(self, nPage):
+        """ Returns A Tuple With All Page ToolTip Parameters. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageToolTip: (" + str(nPage) + ")"
+
+        return self.nb.GetPageToolTip(nPage)
+
+
+    def EnableToolTip(self, show=True):
+        """ Globally Enables/Disables Tab ToolTips. """
+
+        self.nb.EnableToolTip(show)
+
+
+    def GetToolTipBackgroundColour(self):
+        """ Returns The ToolTip Window Background Colour. """
+
+        return self.nb.GetToolTipBackgroundColour()
+
+
+    def SetToolTipBackgroundColour(self, colour=None):
+        """ Sets The ToolTip Window Background Colour. """
+
+        if colour is None:
+            colour = wx.Colour(255, 255, 230)
+
+        self.nb.SetToolTipBackgroundColour(colour)
+
+
+    def EnableTabGradients(self, enable=True):
+        """ Globally Enables/Disables Drawing Of Gradient Coloured Tabs For Each Tab. """
+
+        self.nb.EnableTabGradients(enable)
+
+
+    def SetPageFirstGradientColour(self, nPage, colour=None):
+        """ Sets The Single Tab First Gradient Colour. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageFirstGradientColour: (" + str(nPage) + ")"
+
+        if colour is None:
+            colour = wx.WHITE
+
+        self.nb.SetPageFirstGradientColour(nPage, colour)
+
+
+    def SetPageSecondGradientColour(self, nPage, colour=None):
+        """ Sets The Single Tab Second Gradient Colour. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageSecondGradientColour: (" + str(nPage) + ")"
+
+        self.nb.SetPageSecondGradientColour(nPage, colour)
+
+
+    def GetPageFirstGradientColour(self, nPage):
+        """ Returns The Single Tab First Gradient Colour. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageFirstGradientColour: (" + str(nPage) + ")"
+
+        return self.nb.GetPageFirstGradientColour(nPage)
+
+
+    def GetPageSecondGradientColour(self, nPage):
+        """ Returns The Single Tab Second Gradient Colour. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageSecondGradientColour: (" + str(nPage) + ")"
+
+        return self.nb.GetPageSecondGradientColour(nPage)
+
+
+    def CancelTip(self):
+        """ Destroys The Tip Window (Probably You Won't Need This One. """
+
+        self.nb.CancelTip()
+
+
+    def AdvanceSelection(self, forward=True):
+        """
+        Cycles Through The Tabs. The Call To This Function Generates The
+        EVT_NOTEBOOKCTRL_PAGE_CHANGING Event.
+        """
+
+        self.nb.AdvanceSelection(forward)
+
+
+    def SetDefaultPage(self, defaultpage=-1):
+        """
+        Sets The Default Page That Will Be Selected When An Active And Selected
+        Tab Is Made Inactive.
+        """
+
+        if defaultpage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetDefaultPage: (" + str(defaultpage) + ")"
+
+        self.nb.SetDefaultPage(defaultpage)
+
+
+    def GetDefaultPage(self):
+        """ Returns The Default Page. """
+
+        return self.nb.GetDefaultPage()
+
+
+    def GetPageText(self, nPage):
+        """ Returns The String For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageText: (" + str(nPage) + ")"
+
+        return self.nb.GetPageText(nPage)
+
+
+    def SetPageText(self, nPage, text):
+        """ Sets The String For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageText: (" + str(nPage) + ")"
+
+        self.nb.SetPageText(nPage, text)
+
+
+    def GetPageImage(self, nPage):
+        """ Returns The Image Index For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageImage: (" + str(nPage) + ")"
+
+        return self.nb.GetPageImage(nPage)
+
+
+    def SetPageImage(self, nPage, img):
+        """ Sets The Image Index For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageImage: (" + str(nPage) + ")"
+
+        self.nb.SetPageImage(nPage, img)
+
+
+    def SetPageTextFont(self, nPage, font=None):
+        """ Sets The Primary Font For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageTextFont: (" + str(nPage) + ")"
+
+        if font is None:
+            font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+
+        self.nb.SetPageTextFont(nPage, font)
+
+
+    def GetPageTextFont(self, nPage):
+        """ Returns The Primary Font For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageTextFont: (" + str(nPage) + ")"
+
+        return self.nb.GetPageTextFont(nPage)
+
+
+    def SetPageTextSecondaryFont(self, nPage, font=None):
+        """ Sets The Secondary Font For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageTextSecondaryFont: (" + str(nPage) + ")"
+
+        if font is None:
+            font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+
+        self.nb.SetPageTextSecondaryFont(nPage, font)
+
+
+    def GetPageTextSecondaryFont(self, nPage):
+        """ Returns The Secondary Font For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageTextSecondaryFont: (" + str(nPage) + ")"
+
+        return self.nb.GetPageTextSecondaryFont(nPage)
+
+
+    def SetPageTextColour(self, nPage, colour=None):
+        """ Sets The Text Colour For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageTextColour: (" + str(nPage) + ")"
+
+        if colour is None:
+            colour = wx.BLACK
+
+        self.nb.SetPageTextColour(nPage, colour)
+
+
+    def GetPageTextColour(self, nPage):
+        """ Returns The Text Colour For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageTextColour: (" + str(nPage) + ")"
+
+        return self.nb.GetPageTextColour(nPage)
+
+
+    def SetPageColour(self, nPage, colour=None):
+        """ Sets The Tab Background Colour For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In SetPageColour: (" + str(nPage) + ")"
+
+        if colour is None:
+            colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE)
+
+        self.nb.SetPageColour(nPage, colour)
+
+
+    def GetPageColour(self, nPage):
+        """ Returns The Tab Background Colour For The Given Page nPage. """
+
+        if nPage < 0 or nPage >= self.GetPageCount():
+            raise "\nERROR: Invalid Notebook Page In GetPageColour: (" + str(nPage) + ")"
+
+        return self.nb.GetPageColour(nPage)
+
+
+    def SetTabHeight(self, height=28):
+        """ Sets The Tabs Height. """
+
+        if height <= 0:
+            raise "\nERROR: Impossible To Set An Height <= 0. "
+
+        self.nb.SetTabHeight(height)
+
+
+    def SetControlBackgroundColour(self, colour=None):
+        """ Sets The TabCtrl Background Colour (Behind The Tabs). """
+
+        if colour is None:
+            colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)
+
+        self.nb.SetBackgroundColour(colour)
+
+
+    def ApplyTabTheme(self, theme=None):
+        """ Apply A Particular Theme To Be Drawn On Tabs. """
+
+        if theme is None:
+            theme = ThemeStyle()
+
+        self.nb.ApplyTabTheme(theme)
+
+
+    def SetSelectionColour(self, colour=None):
+        """ Sets The Tab Selection Colour (Thin Line Above The Selected Tab). """
+
+        if colour is None:
+            colour = wx.Colour(255, 180, 0)
+
+        self.nb.SetSelectionColour(colour)
+
+
+    def SetContourLineColour(self, colour=None):
+        """ Sets The Contour Line Colour (Controur Line Around Tabs). """
+
+        self.nb.SetContourLineColour(colour)
+
+    def Tile(self, show=True, orient=None):
+        """ Shows Pages In Column/Row Mode (One Panel After The Other In Columns/Rows). """
+
+        if self._GetTabCtrlWindow().IsShown() == show and orient is None:
+            return
+
+        self.Freeze()
+
+        if orient is not None and show:
+            if orient == wx.VERTICAL:
+                norient = wx.HORIZONTAL
+            else:
+                norient = wx.VERTICAL
+
+        if orient is not None and show:
+            origorient = self.bsizer.GetOrientation()
+            if origorient != norient:
+                for ii in xrange(self.GetPageCount()-1, -1, -1):
+                    self.bsizer.Detach(ii)
+
+                self.sizer.Detach(self.bsizer)
+                self.bsizer.Destroy()
+
+                self.bsizer = wx.BoxSizer(norient)
+
+                for ii in xrange(self.GetPageCount()):
+                    self.bsizer.Add(self._notebookpages[ii], 1, wx.EXPAND | wx.ALL, 2)
+
+                if self._style & NC_TOP:
+                    self.sizer.Add(self.bsizer, 1, wx.EXPAND)
+                else:
+                    self.sizer.Insert(0, self.bsizer, 1, wx.EXPAND)
+
+                self.bsizer.Layout()
+                self.sizer.Layout()
+
+        selection = self.GetSelection()
+
+        if show:
+            self._ShowTabCtrl(False)
+            if self._style & NC_TOP or self._style & NC_LEFT:
+                if len(self.nb._selectedtabs) > 0:
+                    for ii in xrange(self.GetPageCount()):
+                        if ii in self.nb._selectedtabs:
+                            self.bsizer.Show(ii, True)
+                        else:
+                            self.bsizer.Show(ii, False)
+                else:
+                    for ii in xrange(self.GetPageCount()):
+                        if self.IsPageEnabled(ii):
+                            if not self.nb._enablehiding or not self.nb._pages[ii]._ishidden:
+                                self.bsizer.Show(ii, True)
+                            else:
+                                self.bsizer.Show(ii, False)
+                        else:
+                            self.bsizer.Show(ii, False)
+            else:
+                if len(self.nb._selectedtabs) > 0:
+                    for ii in xrange(self.GetPageCount()):
+                        if ii in self.nb._selectedtabs:
+                            self.bsizer.Show(ii, True)
+                else:
+                    for ii in xrange(self.GetPageCount()):
+                        if self.IsPageEnabled(ii):
+                            if not self.nb._enablehiding or not self.nb._pages[ii]._ishidden:
+                                self.bsizer.Show(ii, True)
+                            else:
+                                self.bsizer.Show(ii, False)
+                        else:
+                            self.bsizer.Show(ii, False)
+        else:
+            self._ShowTabCtrl(True)
+            if self._style & NC_TOP or self._style & NC_LEFT:
+                for ii in xrange(self.GetPageCount()):
+                    self.bsizer.Show(ii, False)
+            else:
+                for ii in xrange(self.GetPageCount()):
+                    self.bsizer.Show(ii, False)
+
+            if selection < 0:
+                self.bsizer.Layout()
+                self.sizer.Layout()
+                return
+            else:
+                self.bsizer.Show(selection, True)
+                self.bsizer.Layout()
+
+        self._showcolumns = show
+
+        self.bsizer.Layout()
+        self.sizer.Layout()
+
+        self.Thaw()
+
+
+
+    def ShowTabs(self, show=True):
+        """ Shows/Hides Tabs On Request. """
+
+        if self._GetTabCtrlWindow().IsShown() == show:
+            return
+
+        if self.GetPageCount() == 0:
+            return
+
+        self.Freeze()
+
+        self._ShowTabCtrl(show)
+
+        self._showtabs = show
+
+        self.sizer.Layout()
+
+        self.Thaw()
+
+
+    def GetIndex(self, page):
+        """ Returns The Page Index (Position) Based On The NotebookCtrl Page Passed. """
+
+        if page in self._notebookpages:
+            return self._notebookpages.index(page)
+
+        return -1
+
+
+    def ReparentPage(self, nPage, newParent):
+        """ Reparents The NotebookCtrl Page nPage To A New Parent. """
+
+        if nPage < 0 or (self.GetSelection() >= 0 and nPage >= self.GetPageCount()):
+            raise "\nERROR: Invalid Notebook Page In ReparentPage: (" + str(nPage) + ")"
+
+        page = self.GetPage(nPage)
+        page.Reparent(newParent)
+
+
+    def ReparentToFrame(self, nPage, createNotebook=False):
+        """ Reparents The NotebookCtrl Page nPage To A New Frame. """
+
+        if nPage < 0 or (self.GetSelection() >= 0 and nPage >= self.GetPageCount()):
+            raise "\nERROR: Invalid Notebook Page In ReparentToFrame: (" + str(nPage) + ")"
+
+        self.Freeze()
+
+        infos = self.GetPageInfo(nPage)
+        panel = self.GetPage(nPage)
+        text = infos["text"]
+        oldparent = panel.GetParent()
+
+        frame = NCFrame(None, -1, text, nb=self, infos=infos, panel=panel, oldparent=oldparent)
+
+        if createNotebook:
+            nb = NotebookCtrl(frame, -1, style=self._style, sizer=self._sizerstyle)
+            nb.SetImageList(infos["imagelist"])
+            self.ReparentToNotebook(nPage, nb)
+        else:
+            self.ReparentPage(nPage, frame)
+
+            self.nb.DeletePage(nPage, False)
+
+            self.bsizer.Detach(nPage)
+            self.bsizer.Layout()
+            self.sizer.Layout()
+
+            self._notebookpages.pop(nPage)
+
+            self.AdvanceSelection()
+
+        if self.GetPageCount() == 0:
+            self._ShowTabCtrl(False)
+
+            self.sizer.Layout()
+
+        custom = self.GetCustomPage()
+        if custom is not None:
+            self.SetCustomPage(custom)
+
+        self.Thaw()
+
+        frame.Show()
+
+
+    def ReparentToNotebook(self, nPage, notebook, newPage=None):
+        """ Reparents The NotebookCtrl Page nPage To A New NotebookCtrl. """
+
+        if nPage < 0 or (self.GetSelection() >= 0 and nPage >= self.GetPageCount()):
+            raise "\nERROR: Invalid Notebook Page In ReparentToNotebook: (" + str(nPage) + ")"
+
+        if newPage is not None and newPage >= notebook.GetPageCount():
+            raise "\nERROR: Invalid Notebook New Page In ReparentToNotebook: (" + str(nPage) + ")"
+
+        self.Freeze()
+
+        infos = self.GetPageInfo(nPage)
+        panel = self.GetPage(nPage)
+
+        self.ReparentPage(nPage, notebook)
+
+        if newPage is None:
+            notebook.AddPage(panel, infos["text"], False, infos["image"])
+            notebook.SetPageInfo(0, infos)
+
+        for attr in attrs:
+            setattr(notebook, attr, getattr(self.nb, attr))
+
+        self.nb.DeletePage(nPage, False)
+
+        self.bsizer.Detach(nPage)
+        self.bsizer.Layout()
+        self.sizer.Layout()
+
+        self._notebookpages.pop(nPage)
+
+        self.AdvanceSelection()
+
+        if self.GetPageCount() == 0:
+            self._ShowTabCtrl(False)
+
+            self.sizer.Layout()
+
+        self.Thaw()
+
+
+    def GetPageInfo(self, nPage):
+        """ Returns All The Style Information For A Given Page. """
+
+        if nPage < 0 or (self.GetSelection() >= 0 and nPage >= self.GetPageCount()):
+            raise "\nERROR: Invalid Notebook Page In GetPageInfo: (" + str(nPage) + ")"
+
+        text = self.GetPageText(nPage)
+        image = self.GetPageImage(nPage)
+        font1 = self.GetPageTextFont(nPage)
+        font2 = self.GetPageTextSecondaryFont(nPage)
+        fontcolour = self.GetPageTextColour(nPage)
+        pagecolour = self.GetPageColour(nPage)
+        enabled = self.IsPageEnabled(nPage)
+        tooltip, ontime, winsize = self.GetPageToolTip(nPage)
+        menu = self.GetPagePopupMenu(nPage)
+        firstcol = self.GetPageFirstGradientColour(nPage)
+        secondcol = self.GetPageSecondGradientColour(nPage)
+        ishidden = self.nb._pages[nPage]._ishidden
+
+        isanimated = 0
+        timer = None
+
+        if self.nb._timers[nPage].IsRunning():
+            isanimated = 1
+            timer = self.nb._timers[nPage].GetInterval()
+
+        self.StopAnimation(nPage)
+        animatedimages = self.GetAnimationImages(nPage)
+
+        infos = {"text": text, "image": image, "font1": font1, "font2": font2,
+                 "fontcolour": fontcolour, "pagecolour": pagecolour, "enabled": enabled,
+                 "tooltip": tooltip, "ontime": ontime, "winsize": winsize,
+                 "menu": menu, "isanimated": isanimated, "timer": timer,
+                 "animatedimages": animatedimages, "imagelist": self.nb._imglist,
+                 "firstcol": firstcol, "secondcol": secondcol, "ishidden": ishidden}
+
+        return infos
+
+
+    def SetPageInfo(self, nPage, infos):
+        """ Sets All The Style Information For A Given Page. """
+
+        if nPage < 0 or (self.GetSelection() >= 0 and nPage >= self.GetPageCount()):
+            raise "\nERROR: Invalid Notebook Page In SetPageInfo: (" + str(nPage) + ")"
+
+        self.SetPageTextFont(nPage, infos["font1"])
+        self.SetPageTextSecondaryFont(nPage, infos["font2"])
+        self.SetPageTextColour(nPage, infos["fontcolour"])
+        self.SetPageColour(nPage, infos["pagecolour"])
+        self.EnablePage(nPage, infos["enabled"])
+        self.SetPageToolTip(nPage, infos["tooltip"], infos["ontime"], infos["winsize"])
+        self.SetPagePopupMenu(nPage, infos["menu"])
+        self.SetPageFirstGradientColour(nPage, infos["firstcol"])
+        self.SetPageSecondGradientColour(nPage, infos["secondcol"])
+        self.nb._pages[nPage]._ishidden = infos["ishidden"]
+
+        if infos["isanimated"] and len(infos["animatedimages"]) > 1:
+            self.SetAnimationImages(nPage, infos["animatedimages"])
+            self.StartAnimation(nPage, infos["timer"])
+
+
+    def SetCustomPage(self, panel):
+        """ Sets A Custom Panel To Show When There Are No Pages Left In NotebookCtrl. """
+
+        self.Freeze()
+
+        if panel is None:
+            if self._custompanel is not None:
+                self.bsizer.Detach(self._custompanel)
+                self._custompanel.Show(False)
+
+            if self.GetPageCount() == 0:
+                self._ShowTabCtrl(False)
+        else:
+            if self.GetPageCount() == 0:
+                if self._custompanel is not None:
+                    self.bsizer.Detach(self._custompanel)
+                    self._custompanel.Show(False)
+
+                self.bsizer.Add(panel, 1, wx.EXPAND | wx.ALL, 2)
+                panel.Show(True)
+                self._ShowTabCtrl(False)
+            else:
+                panel.Show(False)
+
+        self._custompanel = panel
+
+        self.bsizer.Layout()
+        self.sizer.Layout()
+        self.Thaw()
+
+
+    def GetCustomPage(self):
+        """ Gets A Custom Panel To Show When There Are No Pages Left In NotebookCtrl. """
+
+        return self._custompanel
+
+
+    def HideTab(self, nPage, hide=True):
+        """ Hides A Tab In The NotebookCtrl. """
+
+        self.nb.HideTab(nPage, hide)
+
+
+    def HitTest(self, point, flags=0):
+        """
+        Standard NotebookCtrl HitTest() Method. If Called With 2 Outputs, It
+        Returns The Page Clicked (If Any) And One Of These Flags:
+
+        NC_HITTEST_NOWHERE = 0   ==> Hit Not On Tab
+        NC_HITTEST_ONICON  = 1   ==> Hit On Icon
+        NC_HITTEST_ONLABEL = 2   ==> Hit On Label
+        NC_HITTEST_ONITEM  = 4   ==> Hit Generic, On Item
+        NC_HITTEST_ONX = 8       ==> Hit On Closing "X" On Every Page
+        """
+
+        return self.nb.HitTest(point, flags)
+
+    def _AddMargin(self, style, margin):
+        if style & NC_TOP or style & NC_BOTTOM:
+            self.tabsizer.Add((0, margin), 0)
+        elif style & NC_LEFT or style & NC_RIGHT:
+            self.tabsizer.Add((margin, 0), 0)
+
+    def _GetTabCtrlWindow(self):
+        if self._style & NC_TOP or self._style & NC_LEFT:
+            return self.tabsizer.GetItem(1)
+        else:
+            return self.tabsizer.GetItem(0)
+
+    def _ShowTabCtrl(self, show):
+        if self._style & NC_TOP:
+            self.sizer.Show(0, show)
+        else:
+            self.sizer.Show(1, show)
+
+
+# ---------------------------------------------------------------------------- #
+# Class TransientTipWindow
+# Auxiliary Help Class. Used To Build The Tip Window.
+# ---------------------------------------------------------------------------- #
+
+class _PopupWindow:
+
+    def _Fill(self, tip, winsize):
+
+        panel = wx.Panel(self, -1)
+        colour = self.GetParent().GetToolTipBackgroundColour()
+
+        panel.SetBackgroundColour(colour)
+
+        # border from sides and top to text (in pixels)
+        border = 5
+        # how much space between text lines
+        textPadding = 2
+        max_len = len(tip)
+        tw = winsize
+
+        mylines = tip.split("\n")
+
+        sts = wx.StaticText(panel, -1, "\n".join(mylines))
+        sx, sy = sts.GetBestSize()
+        sts.SetPosition((2, 2))
+
+        panel.SetSize((sx+6, sy+6))
+        self.SetSize(panel.GetSize())
+
+
+class TransientTipWindow(_PopupWindow, wx.PopupWindow):
+
+    def __init__(self, parent, tip, winsize):
+
+        wx.PopupWindow.__init__(self, parent, flags=wx.SIMPLE_BORDER)
+        self._Fill(tip,winsize)
+
+
+    def ProcessLeftDown(self, evt):
+
+        return False
+
+
+    def OnDismiss(self):
+
+        return False
+
+
+class macPopupWindow(wx.Frame):
+
+    def __init__(self, parent, flags):
+
+        wx.Frame.__init__(self, parent, id=-1, style=flags|wx.FRAME_NO_TASKBAR|wx.STAY_ON_TOP)
+        self._hideOnActivate = False
+        #Get the parent frame: could be improved maybe?
+        self._parentFrame = parent
+
+        while True:
+
+            parent = self._parentFrame.GetParent()
+
+            if parent:
+                self._parentFrame = parent
+            else:
+                break
+
+        self.Bind(wx.EVT_ACTIVATE, self.OnActivate)
+
+
+    def Show(self, show=True):
+
+        wx.Frame.Show(self,show)
+
+        if show:
+            self._parentFrame.Raise()
+            self._hideOnActivate = True
+
+
+    def OnActivate(self, evt):
+        """
+        Let The User Hide The Tooltip By Clicking On It.
+        NotebookCtrl Will Destroy It Later.
+        """
+
+        if self._hideOnActivate:
+            wx.Frame.Show(self,False)
+
+
+class macTransientTipWindow(_PopupWindow, macPopupWindow):
+
+    def __init__(self, parent, tip, winsize):
+
+        macPopupWindow.__init__(self, parent, flags=wx.SIMPLE_BORDER)
+        self._Fill(tip, winsize)
+
+
+class NCFrame(wx.Frame):
+
+    def __init__(self, parent, id=wx.ID_ANY, title="", pos=wx.DefaultPosition,
+                 size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, nb=None,
+                 panel=None, infos=None, oldparent=None):
+
+        wx.Frame.__init__(self, parent, id, title, pos, size, style)
+
+        self._infos = infos
+        self._nb = nb
+        self._panel = panel
+        self._oldparent = oldparent
+
+        self.Bind(wx.EVT_CLOSE, self.OnClose)
+
+
+    def OnClose(self, event):
+
+        try:
+            infos = self._infos
+            self._panel.Reparent(self._oldparent)
+            self._nb.AddPage(self._panel, infos["text"], False, infos["image"])
+
+            id = self._nb.GetPageCount() - 1
+
+            self._nb.SetPageTextFont(id, infos["font1"])
+            self._nb.SetPageTextSecondaryFont(id, infos["font2"])
+            self._nb.SetPageTextColour(id, infos["fontcolour"])
+            self._nb.SetPageColour(id, infos["pagecolour"])
+            self._nb.EnablePage(id, infos["enabled"])
+            self._nb.SetPageToolTip(id, infos["tooltip"], infos["ontime"], infos["winsize"])
+            self._nb.SetPagePopupMenu(id, infos["menu"])
+            self._nb.SetPageFirstGradientColour(id, infos["firstcol"])
+            self._nb.SetPageSecondGradientColour(id, infos["secondcol"])
+            self._nb._pages[id]._ishidden = infos["ishidden"]
+
+            if infos["isanimated"] and len(infos["animatedimages"]) > 1:
+                self._nb.SetAnimationImages(id, infos["animatedimages"])
+                self._nb.StartAnimation(id, infos["timer"])
+
+        except:
+            self.Destroy()
+            event.Skip()
+            return
+
+        self.Destroy()
+
+        event.Skip()
+
+class NotebookCtrlWindowHandler(xrc.XmlResourceHandler):
+    """
+    Create L{NotebookCtrl} windows defined in Xrc resources.
+
+    Below is an example of a resource definition::
+      <?xml version="1.0" encoding="ISO-8859-1"?>
+      <resource>
+        <object class="wxPanel" name="appPanel">
+          <object class="wxBoxSizer">
+            <orient>wxVERTICAL</orient>
+            <object class="sizeritem">
+              <option>1</option>
+              <flag>wxEXPAND</flag>
+              <object class="NotebookCtrl" name="notebook">
+                <style>wxNO_BORDER | NC_RIGHT | NC_ROTATE | NC_EXPANDABLE </style>
+                <focus>0</focus>
+                <highlight>1</highlight>
+                <tabstyle>NC_GRADIENT_HORIZONTAL | NC_GRADIENT_SELECTION</tabstyle>
+                <color1>#DCDCDC</color1>
+                <color2>#F5F5F5</color2>
+                <selectedcolor1>#C4DADB</selectedcolor1>
+                <selectedcolor2>#FFFFFF</selectedcolor2>
+                <custompagecolor>#C0C0C0</custompagecolor>
+              </object>
+            </object>
+          </object>
+        </object>
+      </resource>
+
+    @undocumented: CanHandle, DoCreateResource, SetupWindow
+    """
+    def __init__(self):
+        """
+        Create a NotebookCtrlWindowHandler instance.
+        """
+        xrc.XmlResourceHandler.__init__(self)
+        # Specify the window styles recognized by objects of this type
+        self.AddStyle("wxNO_BORDER", wx.NO_BORDER)
+        self.AddStyle("wxTAB_TRAVERSAL", wx.TAB_TRAVERSAL)
+        self.AddStyle("NC_TOP", NC_TOP)
+        self.AddStyle("NC_BOTTOM", NC_BOTTOM)
+        self.AddStyle("NC_LEFT", NC_LEFT)
+        self.AddStyle("NC_RIGHT", NC_RIGHT)
+        self.AddStyle("NC_FIXED_WIDTH", NC_FIXED_WIDTH)
+        self.AddStyle("NC_ROTATE", NC_ROTATE)
+        self.AddStyle("NC_EXPANDABLE", NC_EXPANDABLE)
+        # More styles, used in the tabstyle parameter
+        self.AddStyle("NC_AQUA_LIGHT", NC_AQUA_LIGHT)
+        self.AddStyle("NC_AQUA_DARK", NC_AQUA_DARK)
+        self.AddStyle("NC_AQUA", NC_AQUA)
+        self.AddStyle("NC_METAL", NC_METAL)
+        self.AddStyle("NC_SILVER", NC_SILVER)
+        self.AddStyle("NC_KDE", NC_KDE)
+        self.AddStyle("NC_GRADIENT_VERTICAL", NC_GRADIENT_VERTICAL)
+        self.AddStyle("NC_GRADIENT_HORIZONTAL", NC_GRADIENT_HORIZONTAL)
+        self.AddStyle("NC_GRADIENT_SELECTION", NC_GRADIENT_SELECTION)
+
+        self.AddWindowStyles()
+
+    def _CreateResourceInstance(self, parent, id, position, size, style, name):
+        window = NotebookCtrl(parent, id, position, size=size, style=style, name=name)
+        return window
+
+    def _GetColorParamValue(self, paramName, defaultValue=wx.WHITE):
+        paramValue = self.GetParamValue(paramName)
+        if paramValue:
+            return self.GetColour(paramName)
+        else:
+            return defaultValue
+
+    def _GetCustomPage(self, window):
+        customPage = wx.Window(window, -1, style = wx.STATIC_BORDER)
+        customPage.SetBackgroundColour(self._GetColorParamValue('custompagecolor'))
+        return customPage
+
+    def _GetIntParamValue(self, paramName, defaultValue=0):
+        paramValue = self.GetParamValue(paramName)
+        if paramValue:
+            return int(paramValue)
+        else:
+            return defaultValue
+
+    def _GetTabTheme(self):
+        tabstyle = self.GetStyle("tabstyle")
+
+        if tabstyle:
+            result = ThemeStyle()
+            if tabstyle & NC_GRADIENT_VERTICAL or tabstyle & NC_GRADIENT_HORIZONTAL:
+                result.EnableGradientStyle(True, tabstyle)
+                result.SetFirstGradientColour(self._GetColorParamValue('color1'))
+                result.SetSecondGradientColour(self._GetColorParamValue('color2'))
+                result.SetFirstGradientColourSelected(self._GetColorParamValue('selectedcolor1'))
+                result.SetSecondGradientColourSelected(self._GetColorParamValue('selectedcolor2'))
+            elif tabstyle & NC_AQUA_LIGHT or tabstyle & NC_AQUA_DARK:
+                result.EnableAquaTheme(True, tabstyle & NC_AQUA_LIGHT and 2 or 1)
+            elif tabstyle & NC_METAL:
+                result.EnableMetalTheme(True)
+            elif tabstyle & NC_KDE:
+                result.EnableKDETheme(True)
+            elif tabstyle & NC_SILVER:
+                result.EnableSilverTheme(True)
+        else:
+            result = GetDefaultTabStyle()
+        return result
+
+    # This method and the next one are required for XmlResourceHandlers
+    def CanHandle(self, node):
+        return self.IsOfClass(node, "NotebookCtrl")
+
+    def DoCreateResource(self):
+        # NOTE: wxWindows can be created in either a single-phase or
+        # in a two-phase way.  Single phase is what you normally do,
+        # and two-phase creates the instnace first, and then later
+        # creates the actual window when the Create method is called.
+        # (In wxPython the first phase is done using the wxPre*
+        # function, for example, wxPreFrame, wxPrePanel, etc.)
+        #
+        # wxXmlResource supports either method, a premade instance can
+        # be created and populated by xrc using the appropriate
+        # LoadOn* method (such as LoadOnPanel) or xrc can create the
+        # instance too, using the Load* method.  However this makes
+        # the handlers a bit more complex.  If you can be sure that a
+        # particular class will never be loaded using a pre-existing
+        # instance, then you can make the handle much simpler.  I'll
+        # show both methods below.
+
+        # The simple method assumes that there is no existing
+        # instance.  Be sure of that with an assert.
+        assert self.GetInstance() is None
+
+        # Now create the object
+        window = self._CreateResourceInstance(self.GetParentAsWindow(),
+                                              self.GetID(),
+                                              self.GetPosition(),
+                                              self.GetSize(),
+                                              self.GetStyle("style", NC_DEFAULT_STYLE),
+                                              self.GetName())
+
+        # Set standard window attributes
+        self.SetupWindow(window)
+        # Create any child windows of this node
+        self.CreateChildren(window)
+
+        return window
+
+    def SetupWindow(self, window):
+        super(NotebookCtrlWindowHandler, self).SetupWindow(window)
+        window.ApplyTabTheme(self._GetTabTheme())
+        window.SetHighlightSelection(self._GetIntParamValue("highlight", 0) != 0)
+        window.SetUseFocusIndicator(self._GetIntParamValue("focus", 1) != 0)
+        window.SetCustomPage(self._GetCustomPage(window))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/PyAUI.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4931 @@
+# --------------------------------------------------------------------------- #
+# PYAUI Library wxPython IMPLEMENTATION
+#
+# Original C++ Code From Kirix (wxAUI). You Can Find It At:
+#
+#    License: wxWidgets license
+#
+# http://www.kirix.com/en/community/opensource/wxaui/about_wxaui.html
+#
+# Current wxAUI Version Tracked: 0.9.2
+#
+#
+# Python Code By:
+#
+# Andrea Gavana, @ 23 Dec 2005
+# Latest Revision: 30 Jun 2006, 21.00 GMT
+#
+#
+# PyAUI version 0.9.2 Adds:
+#
+# * Fixes For Display Glitches;
+# * Fixes For Other Bugs Found In Previous Versions.
+#
+#
+# TODO List/Caveats
+#
+# 1. Using The New Versions Of wxPython (2.6.2.1pre.20060106 Or Higher) There
+#    Is A New Method Called wx.GetMouseState() That Gets Rid Of The Import Of
+#    win32all or ctypes. Moreover, It Should Make PyAUI Working On All
+#    Platforms (I Hope).
+#
+#
+# Latest Patches:
+#
+# 1) Reduced Flicker While Drawing The Dock Hint
+# 2) Made Impossoible To Drag The Sash Separator Outside The Main Frame
+# 3) Improved Repaint When Using The Active Pane Option
+# 4) Fixed The Mac Problem (Thanks To David Pratt) And Applied The wxGTK Patches
+#    Suggested By Robin Dunn To Correctly Draw The Dock Hint
+#
+# For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please
+# Write To Me At:
+#
+# andrea.gavana@agip.it
+# andrea_gavan@tin.it
+#
+# Or, Obviously, To The wxPython Mailing List!!!
+#
+# with OS X support and refactoring implemented by Chris Mellon (arkanes@gmail.com) -
+#    contact me directly or on wxPython ML for more info
+#
+#
+# End Of Comments
+# --------------------------------------------------------------------------- #
+
+"""
+PyAUI is an Advanced User Interface library that aims to implement "cutting-edge"
+interface usability and design features so developers can quickly and easily create
+beautiful and usable application interfaces.
+
+Vision and Design Principles
+
+PyAUI attempts to encapsulate the following aspects of the user interface:
+
+* Frame Management: Frame management provides the means to open, move and hide common
+controls that are needed to interact with the document, and allow these configurations
+to be saved into different perspectives and loaded at a later time.
+
+* Toolbars: Toolbars are a specialized subset of the frame management system and should
+behave similarly to other docked components. However, they also require additional
+functionality, such as "spring-loaded" rebar support, "chevron" buttons and end-user
+customizability.
+
+* Modeless Controls: Modeless controls expose a tool palette or set of options that
+float above the application content while allowing it to be accessed. Usually accessed
+by the toolbar, these controls disappear when an option is selected, but may also be
+"torn off" the toolbar into a floating frame of their own.
+
+* Look and Feel: Look and feel encompasses the way controls are drawn, both when shown
+statically as well as when they are being moved. This aspect of user interface design
+incorporates "special effects" such as transparent window dragging as well as frame animation.
+
+PyAUI adheres to the following principles:
+
+- Use native floating frames to obtain a native look and feel for all platforms;
+- Use existing wxPython code where possible, such as sizer implementation for frame management;
+- Use standard wxPython coding conventions.
+
+
+Usage:
+
+The following example shows a simple implementation that utilizes AuiManager to manage
+three text controls in a frame window:
+
+class MyFrame(wx.Frame):
+
+    def __init__(self, parent, id=-1, title="PyAUI Test", pos=wx.DefaultPosition,
+                 size=(800, 600), style=wx.DEFAULT_FRAME_STYLE):
+
+        wx.Frame.__init__(self, parent, id, title, pos, size, style)
+
+        self._mgr = PyAUI.AuiManager()
+
+        # notify PyAUI which frame to use
+        self._mgr.SetFrame(self)
+
+        # create several text controls
+        text1 = wx.TextCtrl(self, -1, "Pane 1 - sample text",
+                            wx.DefaultPosition, wx.Size(200,150),
+                            wx.NO_BORDER | wx.TE_MULTILINE)
+
+        text2 = wx.TextCtrl(self, -1, "Pane 2 - sample text",
+                            wx.DefaultPosition, wx.Size(200,150),
+                            wx.NO_BORDER | wx.TE_MULTILINE)
+
+        text3 = wx.TextCtrl(self, -1, "Main content window",
+                            wx.DefaultPosition, wx.Size(200,150),
+                            wx.NO_BORDER | wx.TE_MULTILINE)
+
+        # add the panes to the manager
+        self._mgr.AddPane(text1, wx.LEFT, "Pane Number One")
+        self._mgr.AddPane(text2, wx.BOTTOM, "Pane Number Two")
+        self._mgr.AddPane(text3, wx.CENTER)
+
+        # tell the manager to "commit" all the changes just made
+        self._mgr.Update()
+
+        self.Bind(wx.EVT_CLOSE, self.OnClose)
+
+
+    def OnClose(self, event):
+
+        # deinitialize the frame manager
+        self._mgr.UnInit()
+
+        self.Destroy()
+        event.Skip()
+
+
+# our normal wxApp-derived class, as usual
+
+app = wx.PySimpleApp()
+
+frame = MyFrame(None)
+app.SetTopWindow(frame)
+frame.Show()
+
+app.MainLoop()
+
+What's New:
+
+PyAUI version 0.9.2 Adds:
+
+* Fixes For Display Glitches;
+* Fixes For Other Bugs Found In Previous Versions.
+
+
+License And Version:
+
+PyAUI Library Is Freeware And Distributed Under The wxPython License.
+
+Latest Revision: Andrea Gavana @ 30 Jun 2006, 21.00 GMT
+Version 0.9.2.
+
+"""
+
+from orpg.orpg_wx import *
+import cStringIO, zlib
+import time
+
+_libimported = None
+_newversion = False
+
+# Check For The New wxVersion: It Should Be > 2.6.2.1pre.20060102
+# In Order To Let PyAUI Working On All Platforms
+
+wxver = wx.VERSION_STRING
+if wxver < "2.7":
+    wx.Rect.Contains = lambda self, point: wx.Rect.Inside(self, point)
+
+if hasattr(wx, "GetMouseState"):
+    _newversion = True
+    if wx.Platform == "__WXMSW__":
+        try:
+            import win32api
+            import win32con
+            import winxpgui
+            _libimported = "MH"
+        except:
+            try:
+                import ctypes
+                _libimported = "ctypes"
+            except:
+                pass
+
+else:
+    if wx.Platform == "__WXMSW__":
+        try:
+            import win32api
+            import win32con
+            import winxpgui
+            _libimported = "MH"
+        except:
+            try:
+                import ctypes
+                _libimported = "ctypes"
+            except:
+                raise "\nERROR: At Present, On Windows Machines, You Need To Install "\
+                      "Mark Hammond's pywin32 Extensions Or The ctypes Module, Or Download" \
+                      "The Latest wxPython Version."
+
+    else:
+        raise "\nSorry: I Still Don't Know How To Work On GTK/MAC Platforms... " \
+              "Please Download The Latest wxPython Version."
+
+
+if wx.Platform == "__WXMAC__":
+    try:
+        import ctypes
+        _carbon_dll = ctypes.cdll.LoadLibrary(r'/System/Frameworks/Carbon.framework/Carbon')
+    except:
+        _carbon_dll = None
+
+# Docking Styles
+AUI_DOCK_NONE = 0
+AUI_DOCK_TOP = 1
+AUI_DOCK_RIGHT = 2
+AUI_DOCK_BOTTOM = 3
+AUI_DOCK_LEFT = 4
+AUI_DOCK_CENTER = 5
+AUI_DOCK_CENTRE = AUI_DOCK_CENTER
+
+# Floating/Dragging Styles
+AUI_MGR_ALLOW_FLOATING        = 1
+AUI_MGR_ALLOW_ACTIVE_PANE     = 2
+AUI_MGR_TRANSPARENT_DRAG      = 4
+AUI_MGR_TRANSPARENT_HINT      = 8
+AUI_MGR_TRANSPARENT_HINT_FADE = 16
+
+AUI_MGR_DEFAULT = AUI_MGR_ALLOW_FLOATING | \
+                  AUI_MGR_TRANSPARENT_HINT | \
+                  AUI_MGR_TRANSPARENT_HINT_FADE | \
+                  AUI_MGR_TRANSPARENT_DRAG
+
+# Panes Customization
+AUI_ART_SASH_SIZE = 0
+AUI_ART_CAPTION_SIZE = 1
+AUI_ART_GRIPPER_SIZE = 2
+AUI_ART_PANE_BORDER_SIZE = 3
+AUI_ART_PANE_BUTTON_SIZE = 4
+AUI_ART_BACKGROUND_COLOUR = 5
+AUI_ART_SASH_COLOUR = 6
+AUI_ART_ACTIVE_CAPTION_COLOUR = 7
+AUI_ART_ACTIVE_CAPTION_GRADIENT_COLOUR = 8
+AUI_ART_INACTIVE_CAPTION_COLOUR = 9
+AUI_ART_INACTIVE_CAPTION_GRADIENT_COLOUR = 10
+AUI_ART_ACTIVE_CAPTION_TEXT_COLOUR = 11
+AUI_ART_INACTIVE_CAPTION_TEXT_COLOUR = 12
+AUI_ART_BORDER_COLOUR = 13
+AUI_ART_GRIPPER_COLOUR = 14
+AUI_ART_CAPTION_FONT = 15
+AUI_ART_GRADIENT_TYPE = 16
+
+# Caption Gradient Type
+AUI_GRADIENT_NONE = 0
+AUI_GRADIENT_VERTICAL = 1
+AUI_GRADIENT_HORIZONTAL = 2
+
+# Pane Button State
+AUI_BUTTON_STATE_NORMAL = 0
+AUI_BUTTON_STATE_HOVER = 1
+AUI_BUTTON_STATE_PRESSED = 2
+
+# Pane Insert Level
+AUI_INSERT_PANE = 0
+AUI_INSERT_ROW = 1
+AUI_INSERT_DOCK = 2
+
+# some built in bitmaps
+close_bits = '\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00' \
+             '\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00' \
+             '\xef\x00\x00\x00\xfb\x00\x00\x00\xcf\x00\x00\x00\xf9\x00\x00\x00' \
+             '\x9f\x00\x00\x00\xfc\x00\x00\x00?\x00\x00\x00\xfe\x00\x00\x00?\x00' \
+             '\x00\x00\xfe\x00\x00\x00\x9f\x00\x00\x00\xfc\x00\x00\x00\xcf\x00' \
+             '\x00\x00\xf9\x00\x00\x00\xef\x00\x00\x00\xfb\x00\x00\x00\xff\x00' \
+             '\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00' \
+             '\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00'
+
+pin_bits = '\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff' \
+           '\x00\x00\x00\xff\x00\x00\x00\x1f\x00\x00\x00\xfc\x00\x00\x00\xdf\x00' \
+           '\x00\x00\xfc\x00\x00\x00\xdf\x00\x00\x00\xfc\x00\x00\x00\xdf\x00\x00' \
+           '\x00\xfc\x00\x00\x00\xdf\x00\x00\x00\xfc\x00\x00\x00\xdf\x00\x00\x00' \
+           '\xfc\x00\x00\x00\x0f\x00\x00\x00\xf8\x00\x00\x00\x7f\x00\x00\x00\xff' \
+           '\x00\x00\x00\x7f\x00\x00\x00\xff\x00\x00\x00\x7f\x00\x00\x00\xff\x00' \
+           '\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00' \
+           '\x00\xff\x00\x00\x00\xff\x00\x00\x00'
+
+# PyAUI Event
+wxEVT_AUI_PANEBUTTON = wx.NewEventType()
+EVT_AUI_PANEBUTTON = wx.PyEventBinder(wxEVT_AUI_PANEBUTTON, 0)
+wxEVT_AUI_PANECLOSE = wx.NewEventType()
+EVT_AUI_PANECLOSE = wx.PyEventBinder(wxEVT_AUI_PANECLOSE, 0)
+
+
+def GetCloseData():
+    return zlib.decompress(
+'x\xda\xeb\x0c\xf0s\xe7\xe5\x92\xe2b``\xe0\xf5\xf4p\t\x02\xd2\x02 \xcc\xc1\
+\x06$\xe5?\xffO\x04R,\xc5N\x9e!\x1c@P\xc3\x91\xd2\x01\xe4Gy\xba8\x86X\xf4\
+\x9e\r:\xcd\xc7\xa0\xc0\xe1\xf5\xfb\xbf\xff\xbb\xc2\xacb6\xdbg\xaez\xb9|\x1c\
+\x9a\x82kU\x99xW\x16K\xf5\xdccS\xdad\xe9\xf3\xe0\xa4\x0f\x0f\xaf\xcb\xea\x88\
+\x8bV\xd7k\x1eoN\xdf\xb2\xdd\xc8\xd0\xe7Cw2{\xdd\\uf\xfd}3\x0f\xb0\xd4=\x0ff\
+\xdfr$\\\xe5\xcf\xa9\xfd3\xfa\xcdu\xa4\x7fk\xa6\x89\x03ma\xf0t\xf5sY\xe7\x94\
+\xd0\x04\x00\x1714z')
+
+
+def GetCloseBitmap():
+    return wx.BitmapFromImage(GetCloseImage())
+
+
+def GetCloseImage():
+    stream = cStringIO.StringIO(GetCloseData())
+    return wx.ImageFromStream(stream)
+
+
+def StepColour(c, percent):
+    """
+    StepColour() it a utility function that simply darkens
+    a color by the specified percentage.
+    """
+
+    r = c.Red()
+    g = c.Green()
+    b = c.Blue()
+
+    return wx.Colour(min((r*percent)/100, 255),
+                     min((g*percent)/100, 255),
+                     min((b*percent)/100, 255))
+
+
+def LightContrastColour(c):
+
+    amount = 120
+
+    # if the color is especially dark, then
+    # make the contrast even lighter
+    if c.Red() < 128 and c.Green() < 128 and c.Blue() < 128:
+        amount = 160
+
+    return StepColour(c, amount)
+
+
+def BitmapFromBits(color, type=0):
+    """
+    BitmapFromBits() is a utility function that creates a
+    masked bitmap from raw bits (XBM format).
+    """
+
+    if type == 0:   # Close Bitmap
+        img = GetCloseImage()
+    else:
+        # this should be GetClosePin()... but what the hell is a "pin"?
+        img = GetCloseImage()
+
+    img.Replace(255, 255, 255, 123, 123, 123)
+    img.Replace(0, 0, 0, color.Red(), color.Green(), color.Blue())
+
+    return img.ConvertToBitmap()
+
+
+def DrawGradientRectangle(dc, rect, start_color, end_color, direction):
+
+    rd = end_color.Red() - start_color.Red()
+    gd = end_color.Green() - start_color.Green()
+    bd = end_color.Blue() - start_color.Blue()
+
+    if direction == AUI_GRADIENT_VERTICAL:
+        high = rect.GetHeight() - 1
+    else:
+        high = rect.GetWidth() - 1
+
+    for ii in xrange(high+1):
+        r = start_color.Red() + ((ii*rd*100)/high)/100
+        g = start_color.Green() + ((ii*gd*100)/high)/100
+        b = start_color.Blue() + ((ii*bd*100)/high)/100
+
+        p = wx.Pen(wx.Colour(r, g, b))
+        dc.SetPen(p)
+
+        if direction == AUI_GRADIENT_VERTICAL:
+            dc.DrawLine(rect.x, rect.y+ii, rect.x+rect.width, rect.y+ii)
+        else:
+            dc.DrawLine(rect.x+ii, rect.y, rect.x+ii, rect.y+rect.height)
+
+
+class DockInfo:
+
+    def __init__(self):
+
+        self.dock_direction = 0
+        self.dock_layer = 0
+        self.dock_row = 0
+        self.size = 0
+        self.min_size = 0
+        self.resizable = True
+        self.fixed = False
+        self.toolbar = False
+        self.rect = wx.Rect()
+        self.panes = []
+
+
+    def IsOk(self):
+
+        return (self.dock_direction != 0 and [True] or [False])[0]
+
+
+    def IsHorizontal(self):
+
+        return ((self.dock_direction == AUI_DOCK_TOP or \
+                self.dock_direction == AUI_DOCK_BOTTOM) and \
+                [True] or [False])[0]
+
+
+    def IsVertical(self):
+
+        return ((self.dock_direction == AUI_DOCK_LEFT or \
+                self.dock_direction == AUI_DOCK_RIGHT or \
+                self.dock_direction == AUI_DOCK_CENTER) and [True] or [False])[0]
+
+
+class DockUIPart:
+
+    typeCaption = 0
+    typeGripper = 1
+    typeDock = 2
+    typeDockSizer = 3
+    typePane = 4
+    typePaneSizer = 5
+    typeBackground = 6
+    typePaneBorder = 7
+    typePaneButton = 8
+
+    def __init__(self):
+
+        self.orientation = wx.VERTICAL
+        self.type = 0
+        self.rect = wx.Rect()
+
+
+class PaneButton:
+
+    def __init__(self, button_id):
+
+        self.button_id = button_id
+
+
+# event declarations/classes
+
+class AuiManagerEvent(wx.PyCommandEvent):
+
+    def __init__(self, eventType, id=1):
+
+        wx.PyCommandEvent.__init__(self, eventType, id)
+
+        self.pane = None
+        self.button = 0
+
+
+    def SetPane(self, p):
+
+        self.pane = p
+
+
+    def SetButton(self, b):
+
+        self.button = b
+
+
+    def GetPane(self):
+
+        return self.pane
+
+
+    def GetButton(self):
+
+        return self.button
+
+
+# -- DefaultDockArt class implementation --
+#
+# DefaultDockArt is an art provider class which does all of the drawing for
+# AuiManager.  This allows the library caller to customize the dock art
+# (probably by deriving from this class), or to completely replace all drawing
+# with custom dock art. The active dock art class can be set via
+# AuiManager.SetDockArt()
+
+class DefaultDockArt:
+
+    def __init__(self):
+
+        base_color = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)
+        darker1_color = StepColour(base_color, 85)
+        darker2_color = StepColour(base_color, 70)
+        darker3_color = StepColour(base_color, 60)
+        darker4_color = StepColour(base_color, 50)
+        darker5_color = StepColour(base_color, 40)
+
+        self._active_caption_colour = LightContrastColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHT))
+        self._active_caption_gradient_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHT)
+        self._active_caption_text_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)
+        self._inactive_caption_colour = StepColour(darker1_color, 80)
+        self._inactive_caption_gradient_colour = darker1_color
+        self._inactive_caption_text_colour = wx.BLACK
+
+        sash_color = base_color
+        caption_color = darker1_color
+        paneborder_color = darker2_color
+        selectbutton_color = base_color
+        selectbuttonpen_color = darker3_color
+
+        self._sash_brush = wx.Brush(base_color)
+        self._background_brush = wx.Brush(base_color)
+        self._border_pen = wx.Pen(darker2_color)
+        self._gripper_brush = wx.Brush(base_color)
+        self._gripper_pen1 = wx.Pen(darker5_color)
+        self._gripper_pen2 = wx.Pen(darker3_color)
+        self._gripper_pen3 = wx.WHITE_PEN
+
+        self._caption_font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False)
+
+        self._inactive_close_bitmap = BitmapFromBits(self._inactive_caption_text_colour, 0)
+        self._inactive_pin_bitmap = BitmapFromBits(self._inactive_caption_text_colour, 1)
+        self._active_close_bitmap = BitmapFromBits(self._active_caption_text_colour, 0)
+        self._active_pin_bitmap = BitmapFromBits(self._active_caption_text_colour, 1)
+
+        # default metric values
+        self._sash_size = 4
+        self._caption_size = 17
+        self._border_size = 1
+        self._button_size = 14
+        self._gripper_size = 9
+        self._gradient_type = AUI_GRADIENT_VERTICAL
+
+
+    def GetMetric(self, id):
+
+        if id == AUI_ART_SASH_SIZE:
+            return self._sash_size
+        elif id == AUI_ART_CAPTION_SIZE:
+            return self._caption_size
+        elif id == AUI_ART_GRIPPER_SIZE:
+            return self._gripper_size
+        elif id == AUI_ART_PANE_BORDER_SIZE:
+            return self._border_size
+        elif id == AUI_ART_PANE_BUTTON_SIZE:
+            return self._button_size
+        elif id == AUI_ART_GRADIENT_TYPE:
+            return self._gradient_type
+        else:
+            raise "\nERROR: Invalid Metric Ordinal. "
+
+
+    def SetMetric(self, id, new_val):
+
+        if id == AUI_ART_SASH_SIZE:
+            self._sash_size = new_val
+        elif id == AUI_ART_CAPTION_SIZE:
+            self._caption_size = new_val
+        elif id == AUI_ART_GRIPPER_SIZE:
+            self._gripper_size = new_val
+        elif id == AUI_ART_PANE_BORDER_SIZE:
+            self._border_size = new_val
+        elif id == AUI_ART_PANE_BUTTON_SIZE:
+            self._button_size = new_val
+        elif id == AUI_ART_GRADIENT_TYPE:
+            self._gradient_type = new_val
+        else:
+            raise "\nERROR: Invalid Metric Ordinal. "
+
+
+    def GetColor(self, id):
+
+        if id == AUI_ART_BACKGROUND_COLOUR:
+            return self._background_brush.GetColour()
+        elif id == AUI_ART_SASH_COLOUR:
+            return self._sash_brush.GetColour()
+        elif id == AUI_ART_INACTIVE_CAPTION_COLOUR:
+            return self._inactive_caption_colour
+        elif id == AUI_ART_INACTIVE_CAPTION_GRADIENT_COLOUR:
+            return self._inactive_caption_gradient_colour
+        elif id == AUI_ART_INACTIVE_CAPTION_TEXT_COLOUR:
+            return self._inactive_caption_text_colour
+        elif id == AUI_ART_ACTIVE_CAPTION_COLOUR:
+            return self._active_caption_colour
+        elif id == AUI_ART_ACTIVE_CAPTION_GRADIENT_COLOUR:
+            return self._active_caption_gradient_colour
+        elif id == AUI_ART_ACTIVE_CAPTION_TEXT_COLOUR:
+            return self._active_caption_text_colour
+        elif id == AUI_ART_BORDER_COLOUR:
+            return self._border_pen.GetColour()
+        elif id == AUI_ART_GRIPPER_COLOUR:
+            return self._gripper_brush.GetColour()
+        else:
+            raise "\nERROR: Invalid Metric Ordinal. "
+
+
+    def SetColor(self, id, colour):
+
+        if id == AUI_ART_BACKGROUND_COLOUR:
+            self._background_brush.SetColour(colour)
+        elif id == AUI_ART_SASH_COLOUR:
+            self._sash_brush.SetColour(colour)
+        elif id == AUI_ART_INACTIVE_CAPTION_COLOUR:
+            self._inactive_caption_colour = colour
+        elif id == AUI_ART_INACTIVE_CAPTION_GRADIENT_COLOUR:
+            self._inactive_caption_gradient_colour = colour
+        elif id == AUI_ART_INACTIVE_CAPTION_TEXT_COLOUR:
+            self._inactive_caption_text_colour = colour
+        elif id == AUI_ART_ACTIVE_CAPTION_COLOUR:
+            self._active_caption_colour = colour
+        elif id == AUI_ART_ACTIVE_CAPTION_GRADIENT_COLOUR:
+            self._active_caption_gradient_colour = colour
+        elif id == AUI_ART_ACTIVE_CAPTION_TEXT_COLOUR:
+            self._active_caption_text_colour = colour
+        elif id == AUI_ART_BORDER_COLOUR:
+            self._border_pen.SetColour(colour)
+        elif id == AUI_ART_GRIPPER_COLOUR:
+            self._gripper_brush.SetColour(colour)
+            self._gripper_pen1.SetColour(StepColour(colour, 40))
+            self._gripper_pen2.SetColour(StepColour(colour, 60))
+        else:
+            raise "\nERROR: Invalid Metric Ordinal. "
+
+
+    GetColour = GetColor
+    SetColour = SetColor
+
+    def SetFont(self, id, font):
+
+        if id == AUI_ART_CAPTION_FONT:
+            self._caption_font = font
+
+
+    def GetFont(self, id):
+
+        if id == AUI_ART_CAPTION_FONT:
+            return self._caption_font
+
+        return wx.NoneFont
+
+
+    def DrawSash(self, dc, orient, rect):
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetBrush(self._sash_brush)
+        dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
+
+
+    def DrawBackground(self, dc, orient, rect):
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetBrush(self._background_brush)
+        dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
+
+
+    def DrawBorder(self, dc, rect, pane):
+
+        drect = wx.Rect()
+        drect.x = rect.x
+        drect.y = rect.y
+        drect.width = rect.width
+        drect.height = rect.height
+
+        dc.SetPen(self._border_pen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        border_width = self.GetMetric(AUI_ART_PANE_BORDER_SIZE)
+
+        if pane.IsToolbar():
+
+            for ii in xrange(0, border_width):
+
+                dc.SetPen(wx.WHITE_PEN)
+                dc.DrawLine(drect.x, drect.y, drect.x+drect.width, drect.y)
+                dc.DrawLine(drect.x, drect.y, drect.x, drect.y+drect.height)
+                dc.SetPen(self._border_pen)
+                dc.DrawLine(drect.x, drect.y+drect.height-1,
+                            drect.x+drect.width, drect.y+drect.height-1)
+                dc.DrawLine(drect.x+drect.width-1, drect.y,
+                            drect.x+drect.width-1, drect.y+drect.height)
+                drect.Deflate(1, 1)
+
+        else:
+
+            for ii in xrange(0, border_width):
+
+                dc.DrawRectangle(drect.x, drect.y, drect.width, drect.height)
+                drect.Deflate(1, 1)
+
+
+    def DrawCaptionBackground(self, dc, rect, active):
+
+        if self._gradient_type == AUI_GRADIENT_NONE:
+            if active:
+                dc.SetBrush(wx.Brush(self._active_caption_colour))
+            else:
+                dc.SetBrush(wx.Brush(self._inactive_caption_colour))
+
+            dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
+        else:
+            if active:
+                DrawGradientRectangle(dc, rect, self._active_caption_colour,
+                                      self._active_caption_gradient_colour,
+                                      self._gradient_type)
+            else:
+                DrawGradientRectangle(dc, rect, self._inactive_caption_colour,
+                                      self._inactive_caption_gradient_colour,
+                                      self._gradient_type)
+
+
+    def DrawCaption(self, dc, text, rect, pane):
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetFont(self._caption_font)
+
+        self.DrawCaptionBackground(dc, rect, ((pane.state & AuiPaneInfo.optionActive) and \
+                                              [True] or [False])[0])
+
+        if pane.state & AuiPaneInfo.optionActive:
+            dc.SetTextForeground(self._active_caption_text_colour)
+        else:
+            dc.SetTextForeground(self._inactive_caption_text_colour)
+
+        w, h = dc.GetTextExtent("ABCDEFHXfgkj")
+
+        dc.SetClippingRegion(rect.x, rect.y, rect.width, rect.height)
+        dc.DrawText(text, rect.x+3, rect.y+(rect.height/2)-(h/2)-1)
+        dc.DestroyClippingRegion()
+
+
+    def DrawGripper(self, dc, rect, pane):
+
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetBrush(self._gripper_brush)
+
+        dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
+
+        if not pane.HasGripperTop():
+            y = 5
+            while 1:
+                dc.SetPen(self._gripper_pen1)
+                dc.DrawPoint(rect.x+3, rect.y+y)
+                dc.SetPen(self._gripper_pen2)
+                dc.DrawPoint(rect.x+3, rect.y+y+1)
+                dc.DrawPoint(rect.x+4, rect.y+y)
+                dc.SetPen(self._gripper_pen3)
+                dc.DrawPoint(rect.x+5, rect.y+y+1)
+                dc.DrawPoint(rect.x+5, rect.y+y+2)
+                dc.DrawPoint(rect.x+4, rect.y+y+2)
+                y = y + 4
+                if y > rect.GetHeight() - 5:
+                    break
+        else:
+            x = 5
+            while 1:
+                dc.SetPen(self._gripper_pen1)
+                dc.DrawPoint(rect.x+x, rect.y+3)
+                dc.SetPen(self._gripper_pen2)
+                dc.DrawPoint(rect.x+x+1, rect.y+3)
+                dc.DrawPoint(rect.x+x, rect.y+4)
+                dc.SetPen(self._gripper_pen3)
+                dc.DrawPoint(rect.x+x+1, rect.y+5)
+                dc.DrawPoint(rect.x+x+2, rect.y+5)
+                dc.DrawPoint(rect.x+x+2, rect.y+4)
+                x = x + 4
+                if x > rect.GetWidth() - 5:
+                    break
+
+
+    def DrawPaneButton(self, dc, button, button_state, rect, pane):
+
+        drect = wx.Rect()
+        drect.x = rect.x
+        drect.y = rect.y
+        drect.width = rect.width
+        drect.height = rect.height
+
+        if button_state == AUI_BUTTON_STATE_PRESSED:
+
+            drect.x = drect.x + 1
+            drect.y = drect.y + 1
+
+        if button_state in [AUI_BUTTON_STATE_HOVER, AUI_BUTTON_STATE_PRESSED]:
+
+            if pane.state & AuiPaneInfo.optionActive:
+                dc.SetBrush(wx.Brush(StepColour(self._active_caption_colour, 120)))
+                dc.SetPen(wx.Pen(StepColour(self._active_caption_colour, 70)))
+            else:
+                dc.SetBrush(wx.Brush(StepColour(self._inactive_caption_colour, 120)))
+                dc.SetPen(wx.Pen(StepColour(self._inactive_caption_colour, 70)))
+
+            # draw the background behind the button
+            dc.DrawRectangle(drect.x, drect.y, 15, 15)
+
+        if button == AuiPaneInfo.buttonClose:
+            if pane.state & AuiPaneInfo.optionActive:
+
+                bmp = self._active_close_bitmap
+
+            else:
+                bmp = self._inactive_close_bitmap
+        elif button == AuiPaneInfo.buttonPin:
+            if pane.state & AuiPaneInfo.optionActive:
+
+                bmp = self._active_pin_bitmap
+
+            else:
+                bmp = self._inactive_pin_bitmap
+
+        # draw the button itself
+        dc.DrawBitmap(bmp, drect.x, drect.y, True)
+
+
+# -- AuiPaneInfo class implementation --
+#
+# AuiPaneInfo specifies all the parameters for a pane. These parameters specify where
+# the pane is on the screen, whether it is docked or floating, or hidden. In addition,
+# these parameters specify the pane's docked position, floating position, preferred
+# size, minimum size, caption text among many other parameters.
+
+class AuiPaneInfo:
+
+    optionFloating        = 2**0
+    optionHidden          = 2**1
+    optionLeftDockable    = 2**2
+    optionRightDockable   = 2**3
+    optionTopDockable     = 2**4
+    optionBottomDockable  = 2**5
+    optionFloatable       = 2**6
+    optionMovable         = 2**7
+    optionResizable       = 2**8
+    optionPaneBorder      = 2**9
+    optionCaption         = 2**10
+    optionGripper         = 2**11
+    optionDestroyOnClose  = 2**12
+    optionToolbar         = 2**13
+    optionActive          = 2**14
+    optionGripperTop      = 2**15
+
+    buttonClose           = 2**24
+    buttonMaximize        = 2**25
+    buttonMinimize        = 2**26
+    buttonPin             = 2**27
+    buttonCustom1         = 2**28
+    buttonCustom2         = 2**29
+    buttonCustom3         = 2**30
+    actionPane            = 2**31    # used internally
+
+    def __init__(self):
+
+        wx.DefaultSize = wx.Size(-1, -1)
+        self.window = None
+        self.frame = None
+        self.state = 0
+        self.dock_direction = AUI_DOCK_LEFT
+        self.dock_layer = 0
+        self.dock_row = 0
+        self.dock_pos = 0
+        self.floating_pos = wx.Point(-1, -1)
+        self.floating_size = wx.Size(-1, -1)
+        self.best_size = wx.Size(-1, -1)
+        self.min_size = wx.Size(-1, -1)
+        self.max_size = wx.Size(-1, -1)
+        self.dock_proportion = 0
+        self.caption = ""
+        self.buttons = []
+        self.name = ""
+        self.rect = wx.Rect()
+
+        self.DefaultPane()
+
+
+    def IsOk(self):
+        """ IsOk() returns True if the AuiPaneInfo structure is valid. """
+
+        return (self.window != None and [True] or [False])[0]
+
+
+    def IsFixed(self):
+        """ IsFixed() returns True if the pane cannot be resized. """
+
+        return not self.HasFlag(self.optionResizable)
+
+
+    def IsResizable(self):
+        """ IsResizeable() returns True if the pane can be resized. """
+
+        return self.HasFlag(self.optionResizable)
+
+
+    def IsShown(self):
+        """ IsShown() returns True if the pane should be drawn on the screen. """
+
+        return not self.HasFlag(self.optionHidden)
+
+
+    def IsFloating(self):
+        """ IsFloating() returns True if the pane is floating. """
+
+        return self.HasFlag(self.optionFloating)
+
+
+    def IsDocked(self):
+        """ IsDocked() returns True if the pane is docked. """
+
+        return not self.HasFlag(self.optionFloating)
+
+
+    def IsToolbar(self):
+        """ IsToolbar() returns True if the pane contains a toolbar. """
+
+        return self.HasFlag(self.optionToolbar)
+
+
+    def IsTopDockable(self):
+        """
+        IsTopDockable() returns True if the pane can be docked at the top
+        of the managed frame.
+        """
+
+        return self.HasFlag(self.optionTopDockable)
+
+
+    def IsBottomDockable(self):
+        """
+        IsBottomDockable() returns True if the pane can be docked at the bottom
+        of the managed frame.
+        """
+
+        return self.HasFlag(self.optionBottomDockable)
+
+
+    def IsLeftDockable(self):
+        """
+        IsLeftDockable() returns True if the pane can be docked at the left
+        of the managed frame.
+        """
+
+        return self.HasFlag(self.optionLeftDockable)
+
+
+    def IsRightDockable(self):
+        """
+        IsRightDockable() returns True if the pane can be docked at the right
+        of the managed frame.
+        """
+
+        return self.HasFlag(self.optionRightDockable)
+
+
+    def IsDockable(self):
+        """ IsDockable() returns True if the pane can be docked. """
+
+        return self.IsTopDockable() or self.IsBottomDockable() or self.IsLeftDockable() or \
+               self.IsRightDockable()
+
+
+    def IsFloatable(self):
+        """
+        IsFloatable() returns True if the pane can be undocked and displayed as a
+        floating window.
+        """
+
+        return self.HasFlag(self.optionFloatable)
+
+
+    def IsMovable(self):
+        """
+        IsMoveable() returns True if the docked frame can be undocked or moved to
+        another dock position.
+        """
+
+        return self.HasFlag(self.optionMovable)
+
+
+    def HasCaption(self):
+        """ HasCaption() returns True if the pane displays a caption. """
+
+        return self.HasFlag(self.optionCaption)
+
+
+    def HasGripper(self):
+        """ HasGripper() returns True if the pane displays a gripper. """
+
+        return self.HasFlag(self.optionGripper)
+
+
+    def HasBorder(self):
+        """ HasBorder() returns True if the pane displays a border. """
+
+        return self.HasFlag(self.optionPaneBorder)
+
+
+    def HasCloseButton(self):
+        """
+        HasCloseButton() returns True if the pane displays a button to close
+        the pane.
+        """
+
+        return self.HasFlag(self.buttonClose)
+
+
+    def HasMaximizeButton(self):
+        """
+        HasMaximizeButton() returns True if the pane displays a button to
+        maximize the pane.
+        """
+
+        return self.HasFlag(self.buttonMaximize)
+
+
+    def HasMinimizeButton(self):
+        """
+        HasMinimizeButton() returns True if the pane displays a button to
+        minimize the pane.
+        """
+
+        return self.HasFlag(self.buttonMinimize)
+
+
+    def HasPinButton(self):
+        """ HasPinButton() returns True if the pane displays a button to float the pane. """
+
+        return self.HasFlag(self.buttonPin)
+
+
+    def HasGripperTop(self):
+
+        return self.HasFlag(self.optionGripperTop)
+
+
+    def Window(self, w):
+
+        self.window = w
+        return self
+
+
+    def Name(self, n):
+        """ Name() sets the name of the pane so it can be referenced in lookup functions. """
+
+        self.name = n
+        return self
+
+
+    def Caption(self, c):
+        """ Caption() sets the caption of the pane. """
+
+        self.caption = c
+        return self
+
+
+    def Left(self):
+        """ Left() sets the pane dock position to the left side of the frame. """
+
+        self.dock_direction = AUI_DOCK_LEFT
+        return self
+
+
+    def Right(self):
+        """ Right() sets the pane dock position to the right side of the frame. """
+
+        self.dock_direction = AUI_DOCK_RIGHT
+        return self
+
+
+    def Top(self):
+        """ Top() sets the pane dock position to the top of the frame. """
+
+        self.dock_direction = AUI_DOCK_TOP
+        return self
+
+
+    def Bottom(self):
+        """ Bottom() sets the pane dock position to the bottom of the frame. """
+
+        self.dock_direction = AUI_DOCK_BOTTOM
+        return self
+
+
+    def Center(self):
+        """ Center() sets the pane to the center position of the frame. """
+
+        self.dock_direction = AUI_DOCK_CENTER
+        return self
+
+
+    def Centre(self):
+        """ Centre() sets the pane to the center position of the frame. """
+
+        self.dock_direction = AUI_DOCK_CENTRE
+        return self
+
+
+    def Direction(self, direction):
+        """ Direction() determines the direction of the docked pane. """
+
+        self.dock_direction = direction
+        return self
+
+
+    def Layer(self, layer):
+        """ Layer() determines the layer of the docked pane. """
+
+        self.dock_layer = layer
+        return self
+
+
+    def Row(self, row):
+        """ Row() determines the row of the docked pane. """
+
+        self.dock_row = row
+        return self
+
+
+    def Position(self, pos):
+        """ Position() determines the position of the docked pane. """
+
+        self.dock_pos = pos
+        return self
+
+
+    def MinSize(self, arg1=None, arg2=None):
+        """ MinSize() sets the minimum size of the pane. """
+
+        if isinstance(arg1, wx.Size):
+            ret = self.MinSize1(arg1)
+        else:
+            ret = self.MinSize2(arg1, arg2)
+
+        return ret
+
+
+    def MinSize1(self, size):
+
+        self.min_size = size
+        return self
+
+
+    def MinSize2(self, x, y):
+
+        self.min_size.Set(x,y)
+        return self
+
+
+    def MaxSize(self, arg1=None, arg2=None):
+        """ MaxSize() sets the maximum size of the pane. """
+
+        if isinstance(arg1, wx.Size):
+            ret = self.MaxSize1(arg1)
+        else:
+            ret = self.MaxSize2(arg1, arg2)
+
+        return ret
+
+
+    def MaxSize1(self, size):
+
+        self.max_size = size
+        return self
+
+
+    def MaxSize2(self, x, y):
+
+        self.max_size.Set(x,y)
+        return self
+
+
+    def BestSize(self, arg1=None, arg2=None):
+        """ BestSize() sets the ideal size for the pane. """
+
+        if isinstance(arg1, wx.Size):
+            ret = self.BestSize1(arg1)
+        else:
+            ret = self.BestSize2(arg1, arg2)
+
+        return ret
+
+
+    def BestSize1(self, size):
+
+        self.best_size = size
+        return self
+
+
+    def BestSize2(self, x, y):
+
+        self.best_size.Set(x,y)
+        return self
+
+
+    def FloatingPosition(self, pos):
+        """ FloatingPosition() sets the position of the floating pane. """
+
+        self.floating_pos = pos
+        return self
+
+
+    def FloatingSize(self, size):
+        """ FloatingSize() sets the size of the floating pane. """
+
+        self.floating_size = size
+        return self
+
+
+    def Fixed(self):
+        """ Fixed() forces a pane to be fixed size so that it cannot be resized. """
+
+        return self.SetFlag(self.optionResizable, False)
+
+
+    def Resizable(self, resizable=True):
+        """
+        Resizable() allows a pane to be resizable if resizable is True, and forces
+        it to be a fixed size if resizeable is False.
+        """
+
+        return self.SetFlag(self.optionResizable, resizable)
+
+
+    def Dock(self):
+        """ Dock() indicates that a pane should be docked. """
+
+        return self.SetFlag(self.optionFloating, False)
+
+
+    def Float(self):
+        """ Float() indicates that a pane should be floated. """
+
+        return self.SetFlag(self.optionFloating, True)
+
+
+    def Hide(self):
+        """ Hide() indicates that a pane should be hidden. """
+
+        return self.SetFlag(self.optionHidden, True)
+
+
+    def Show(self, show=True):
+        """ Show() indicates that a pane should be shown. """
+
+        return self.SetFlag(self.optionHidden, not show)
+
+
+    def CaptionVisible(self, visible=True):
+        """ CaptionVisible() indicates that a pane caption should be visible. """
+
+        return self.SetFlag(self.optionCaption, visible)
+
+
+    def PaneBorder(self, visible=True):
+        """ PaneBorder() indicates that a border should be drawn for the pane. """
+
+        return self.SetFlag(self.optionPaneBorder, visible)
+
+
+    def Gripper(self, visible=True):
+        """ Gripper() indicates that a gripper should be drawn for the pane. """
+
+        return self.SetFlag(self.optionGripper, visible)
+
+
+    def GripperTop(self, attop=True):
+        """ GripperTop() indicates that a gripper should be drawn for the pane. """
+
+        return self.SetFlag(self.optionGripperTop, attop)
+
+
+    def CloseButton(self, visible=True):
+        """ CloseButton() indicates that a close button should be drawn for the pane. """
+
+        return self.SetFlag(self.buttonClose, visible)
+
+
+    def MaximizeButton(self, visible=True):
+        """ MaximizeButton() indicates that a maximize button should be drawn for the pane. """
+
+        return self.SetFlag(self.buttonMaximize, visible)
+
+
+    def MinimizeButton(self, visible=True):
+        """ MinimizeButton() indicates that a minimize button should be drawn for the pane. """
+
+        return self.SetFlag(self.buttonMinimize, visible)
+
+
+    def PinButton(self, visible=True):
+        """ PinButton() indicates that a pin button should be drawn for the pane. """
+
+        return self.SetFlag(self.buttonPin, visible)
+
+
+    def DestroyOnClose(self, b=True):
+        """ DestroyOnClose() indicates whether a pane should be destroyed when it is closed. """
+
+        return self.SetFlag(self.optionDestroyOnClose, b)
+
+
+    def TopDockable(self, b=True):
+        """ TopDockable() indicates whether a pane can be docked at the top of the frame. """
+
+        return self.SetFlag(self.optionTopDockable, b)
+
+
+    def BottomDockable(self, b=True):
+        """ BottomDockable() indicates whether a pane can be docked at the bottom of the frame. """
+
+        return self.SetFlag(self.optionBottomDockable, b)
+
+
+    def LeftDockable(self, b=True):
+        """ LeftDockable() indicates whether a pane can be docked on the left of the frame. """
+
+        return self.SetFlag(self.optionLeftDockable, b)
+
+
+    def RightDockable(self, b=True):
+        """ RightDockable() indicates whether a pane can be docked on the right of the frame. """
+
+        return self.SetFlag(self.optionRightDockable, b)
+
+
+    def Floatable(self, b=True):
+        """ Floatable() indicates whether a frame can be floated. """
+
+        return self.SetFlag(self.optionFloatable, b)
+
+
+    def Movable(self, b=True):
+        """ Movable() indicates whether a frame can be moved. """
+
+        return self.SetFlag(self.optionMovable, b)
+
+
+    def Dockable(self, b=True):
+
+        return self.TopDockable(b).BottomDockable(b).LeftDockable(b).RightDockable(b)
+
+
+    def DefaultPane(self):
+        """ DefaultPane() specifies that the pane should adopt the default pane settings. """
+
+        state = self.state
+        state |= self.optionTopDockable | self.optionBottomDockable | \
+                 self.optionLeftDockable | self.optionRightDockable | \
+                 self.optionFloatable | self.optionMovable | self.optionResizable | \
+                 self.optionCaption | self.optionPaneBorder | self.buttonClose
+
+        self.state = state
+
+        return self
+
+
+    def CentrePane(self):
+        """ CentrePane() specifies that the pane should adopt the default center pane settings. """
+
+        return self.CenterPane()
+
+
+    def CenterPane(self):
+        """ CenterPane() specifies that the pane should adopt the default center pane settings. """
+
+        self.state = 0
+        return self.Center().PaneBorder().Resizable()
+
+
+    def ToolbarPane(self):
+        """ ToolbarPane() specifies that the pane should adopt the default toolbar pane settings. """
+
+        self.DefaultPane()
+        state = self.state
+
+        state |= (self.optionToolbar | self.optionGripper)
+        state &= ~(self.optionResizable | self.optionCaption)
+
+        if self.dock_layer == 0:
+            self.dock_layer = 10
+
+        self.state = state
+
+        return self
+
+
+    def SetFlag(self, flag, option_state):
+        """ SetFlag() turns the property given by flag on or off with the option_state parameter. """
+
+        state = self.state
+
+        if option_state:
+            state |= flag
+        else:
+            state &= ~flag
+
+        self.state = state
+
+        return self
+
+
+    def HasFlag(self, flag):
+        """ HasFlag() returns True if the the property specified by flag is active for the pane. """
+
+        return (self.state & flag and [True] or [False])[0]
+
+
+NoneAuiPaneInfo = AuiPaneInfo()
+
+# -- AuiFloatingPane class implementation --
+#
+# AuiFloatingPane implements a frame class with some special functionality
+# which allows the library to sense when the frame move starts, is active,
+# and completes.  Note that it contains it's own AuiManager instance,
+# which, in the future, would allow for nested managed frames.
+# For now, with wxMSW, the wx.MiniFrame window is used, but on wxGTK, wx.Frame
+
+if wx.Platform == "__WXGTK__":
+
+    class AuiFloatingPaneBaseClass(wx.Frame):
+        def __init__(self, parent, id=wx.ID_ANY, title="", pos=wx.DefaultPosition,
+                     size=wx.DefaultSize, style=0):
+            wx.Frame.__init__(self, parent, id, title, pos, size, style)
+
+else:
+
+    class AuiFloatingPaneBaseClass(wx.MiniFrame):
+        def __init__(self, parent, id=wx.ID_ANY, title="", pos=wx.DefaultPosition,
+                     size=wx.DefaultSize, style=0):
+            wx.MiniFrame.__init__(self, parent, id, title, pos, size, style)
+            if wx.Platform == "__WXMAC__":
+                self.MacSetMetalAppearance(True)
+
+
+class AuiFloatingPane(AuiFloatingPaneBaseClass):
+
+    def __init__(self, parent, owner_mgr, id=wx.ID_ANY, title="", pos=wx.DefaultPosition,
+                 size=wx.DefaultSize, style=wx.RESIZE_BORDER | wx.SYSTEM_MENU | wx.CAPTION |
+                                            wx.CLOSE_BOX | wx.FRAME_NO_TASKBAR |
+                                            wx.FRAME_FLOAT_ON_PARENT | wx.CLIP_CHILDREN,
+                 resizeborder=0):
+
+        if not resizeborder:
+            style = style & ~wx.RESIZE_BORDER
+
+        AuiFloatingPaneBaseClass.__init__(self, parent, id, title, pos, size, style)
+        self._owner_mgr = owner_mgr
+        self._moving = False
+        self._last_rect = wx.Rect()
+        self._mgr = AuiManager(None)
+        self._mgr.SetFrame(self)
+        self._mousedown = False
+        self.SetExtraStyle(wx.WS_EX_PROCESS_IDLE)
+
+        self.Bind(wx.EVT_CLOSE, self.OnClose)
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+        self.Bind(wx.EVT_MOVE, self.OnMoveEvent)
+        self.Bind(wx.EVT_MOVING, self.OnMoveEvent)
+        self.Bind(wx.EVT_IDLE, self.OnIdle)
+        self.Bind(wx.EVT_ACTIVATE, self.OnActivate)
+
+
+    def CopyAttributes(self, pane, contained_pane):
+
+        contained_pane.name = pane.name
+        contained_pane.caption = pane.caption
+        contained_pane.window = pane.window
+        contained_pane.frame = pane.frame
+        contained_pane.state = pane.state
+        contained_pane.dock_direction = pane.dock_direction
+        contained_pane.dock_layer = pane.dock_layer
+        contained_pane.dock_row = pane.dock_row
+        contained_pane.dock_pos = pane.dock_pos
+        contained_pane.best_size = pane.best_size
+        contained_pane.min_size = pane.min_size
+        contained_pane.max_size = pane.max_size
+        contained_pane.floating_pos = pane.floating_pos
+        contained_pane.floating_size = pane.floating_size
+        contained_pane.dock_proportion = pane.dock_proportion
+        contained_pane.buttons = pane.buttons
+        contained_pane.rect = pane.rect
+
+        return contained_pane
+
+
+    def SetPaneWindow(self, pane):
+
+        self._pane_window = pane.window
+        self._pane_window.Reparent(self)
+
+        contained_pane = AuiPaneInfo()
+
+        contained_pane = self.CopyAttributes(pane, contained_pane)
+
+        contained_pane.Dock().Center().Show(). \
+                       CaptionVisible(False). \
+                       PaneBorder(False). \
+                       Layer(0).Row(0).Position(0)
+
+        indx = self._owner_mgr._panes.index(pane)
+        self._owner_mgr._panes[indx] = pane
+
+        self._mgr.AddPane(self._pane_window, contained_pane)
+        self._mgr.Update()
+
+        if pane.min_size.IsFullySpecified():
+            tmp = self.GetSize()
+            self.GetSizer().SetSizeHints(self)
+            self.SetSize(tmp)
+
+        self.SetTitle(pane.caption)
+
+        if pane.floating_size != wx.DefaultSize:
+            self.SetSize(pane.floating_size)
+            self._owner_mgr._panes[indx] = pane
+        else:
+            size = pane.best_size
+            if size == wx.DefaultSize:
+                size = pane.min_size
+            if size == wx.DefaultSize:
+                size = self._pane_window.GetSize()
+            if pane.HasGripper():
+                if pane.HasGripperTop():
+                    size.y += self._owner_mgr._art.GetMetric(AUI_ART_GRIPPER_SIZE)
+                else:
+                    size.x += self._owner_mgr._art.GetMetric(AUI_ART_GRIPPER_SIZE)
+
+            pane.floating_size = size
+            self._owner_mgr._panes[indx] = pane
+            self.SetClientSize(size)
+
+
+    def OnSize(self, event):
+
+        self._owner_mgr.OnAuiFloatingPaneResized(self._pane_window, event.GetSize())
+
+
+    def OnClose(self, event):
+        self._owner_mgr.OnAuiFloatingPaneClosed(self._pane_window, event)
+        if event.GetSkipped():
+            self.Destroy()
+        self._mgr.UnInit()
+
+
+    def OnMoveEvent(self, event):
+
+        win_rect = self.GetRect()
+
+        # skip the first move event
+        if self._last_rect.IsEmpty():
+            self._last_rect = win_rect
+            return
+
+        # prevent frame redocking during resize
+        if self._last_rect.GetSize() != win_rect.GetSize():
+            self._last_rect = win_rect
+            return
+
+        self._last_rect = win_rect
+
+        if not self.IsMouseDown():
+            return
+
+        if not self._moving:
+            self.OnMoveStart()
+            self._moving = True
+
+        self.OnMoving(event.GetRect())
+
+
+    def IsMouseDown(self):
+
+        if _newversion:
+            ms = wx.GetMouseState()
+            return ms.leftDown
+        else:
+            if wx.Platform == "__WXMSW__":
+                if _libimported == "MH":
+                    return ((win32api.GetKeyState(win32con.VK_LBUTTON) & (1<<15))\
+                            and [True] or [False])[0]
+                elif _libimported == "ctypes":
+                    return ((ctypes.windll.user32.GetKeyState(1) & (1<<15)) and \
+                            [True] or [False])[0]
+
+
+    def OnIdle(self, event):
+
+        if self._moving:
+            if not self.IsMouseDown():
+                self._moving = False
+                self.OnMoveFinished()
+            else:
+                event.RequestMore()
+
+        event.Skip()
+
+
+    def OnMoveStart(self):
+
+        # notify the owner manager that the pane has started to move
+        self._owner_mgr.OnAuiFloatingPaneMoveStart(self._pane_window)
+
+
+    def OnMoving(self, window_rect):
+
+        # notify the owner manager that the pane is moving
+        self._owner_mgr.OnAuiFloatingPaneMoving(self._pane_window)
+
+
+    def OnMoveFinished(self):
+
+        # notify the owner manager that the pane has finished moving
+        self._owner_mgr.OnAuiFloatingPaneMoved(self._pane_window)
+
+
+    def OnActivate(self, event):
+
+        if event.GetActive():
+            self._owner_mgr.OnAuiFloatingPaneActivated(self._pane_window)
+
+
+# -- static utility functions --
+
+def PaneCreateStippleBitmap():
+
+    data = [0, 0, 0, 192, 192, 192, 192, 192, 192, 0, 0, 0]
+    img = wx.EmptyImage(2, 2)
+    counter = 0
+
+    for ii in xrange(2):
+        for jj in xrange(2):
+            img.SetRGB(ii, jj, data[counter], data[counter+1], data[counter+2])
+            counter = counter + 3
+
+    return img.ConvertToBitmap()
+
+
+def DrawResizeHint(dc, rect):
+
+    stipple = PaneCreateStippleBitmap()
+    brush = wx.BrushFromBitmap(stipple)
+    dc.SetBrush(brush)
+    dc.SetPen(wx.TRANSPARENT_PEN)
+
+    dc.SetLogicalFunction(wx.XOR)
+    dc.DrawRectangleRect(rect)
+
+
+def CopyDocksAndPanes(src_docks, src_panes):
+    """
+    CopyDocksAndPanes() - this utility function creates shallow copies of
+    the dock and pane info.  DockInfo's usually contain pointers
+    to AuiPaneInfo classes, thus this function is necessary to reliably
+    reconstruct that relationship in the new dock info and pane info arrays.
+    """
+
+    dest_docks = src_docks
+    dest_panes = src_panes
+
+    for ii in xrange(len(dest_docks)):
+        dock = dest_docks[ii]
+        for jj in xrange(len(dock.panes)):
+            for kk in xrange(len(src_panes)):
+                if dock.panes[jj] == src_panes[kk]:
+                    dock.panes[jj] = dest_panes[kk]
+
+    return dest_docks, dest_panes
+
+
+def CopyDocksAndPanes2(src_docks, src_panes):
+    """
+    CopyDocksAndPanes2() - this utility function creates full copies of
+    the dock and pane info.  DockInfo's usually contain pointers
+    to AuiPaneInfo classes, thus this function is necessary to reliably
+    reconstruct that relationship in the new dock info and pane info arrays.
+    """
+
+    dest_docks = []
+
+    for ii in xrange(len(src_docks)):
+        dest_docks.append(DockInfo())
+        dest_docks[ii].dock_direction = src_docks[ii].dock_direction
+        dest_docks[ii].dock_layer = src_docks[ii].dock_layer
+        dest_docks[ii].dock_row = src_docks[ii].dock_row
+        dest_docks[ii].size = src_docks[ii].size
+        dest_docks[ii].min_size = src_docks[ii].min_size
+        dest_docks[ii].resizable = src_docks[ii].resizable
+        dest_docks[ii].fixed = src_docks[ii].fixed
+        dest_docks[ii].toolbar = src_docks[ii].toolbar
+        dest_docks[ii].panes = src_docks[ii].panes
+        dest_docks[ii].rect = src_docks[ii].rect
+
+    dest_panes = []
+
+    for ii in xrange(len(src_panes)):
+        dest_panes.append(AuiPaneInfo())
+        dest_panes[ii].name = src_panes[ii].name
+        dest_panes[ii].caption = src_panes[ii].caption
+        dest_panes[ii].window = src_panes[ii].window
+        dest_panes[ii].frame = src_panes[ii].frame
+        dest_panes[ii].state = src_panes[ii].state
+        dest_panes[ii].dock_direction = src_panes[ii].dock_direction
+        dest_panes[ii].dock_layer = src_panes[ii].dock_layer
+        dest_panes[ii].dock_row = src_panes[ii].dock_row
+        dest_panes[ii].dock_pos = src_panes[ii].dock_pos
+        dest_panes[ii].best_size = src_panes[ii].best_size
+        dest_panes[ii].min_size = src_panes[ii].min_size
+        dest_panes[ii].max_size = src_panes[ii].max_size
+        dest_panes[ii].floating_pos = src_panes[ii].floating_pos
+        dest_panes[ii].floating_size = src_panes[ii].floating_size
+        dest_panes[ii].dock_proportion = src_panes[ii].dock_proportion
+        dest_panes[ii].buttons = src_panes[ii].buttons
+        dest_panes[ii].rect = src_panes[ii].rect
+
+    for ii in xrange(len(dest_docks)):
+        dock = dest_docks[ii]
+        for jj in xrange(len(dock.panes)):
+            for kk in xrange(len(src_panes)):
+                if dock.panes[jj] == src_panes[kk]:
+                    dock.panes[jj] = dest_panes[kk]
+
+        dest_docks[ii] = dock
+
+    return dest_docks, dest_panes
+
+
+def GetMaxLayer(docks, dock_direction):
+    """
+    GetMaxLayer() is an internal function which returns
+    the highest layer inside the specified dock.
+    """
+
+    max_layer = 0
+
+    for dock in docks:
+        if dock.dock_direction == dock_direction and dock.dock_layer > max_layer and not dock.fixed:
+            max_layer = dock.dock_layer
+
+    return max_layer
+
+
+def GetMaxRow(panes, direction, layer):
+    """
+    GetMaxRow() is an internal function which returns
+    the highest layer inside the specified dock.
+    """
+
+    max_row = 0
+
+    for pane in panes:
+        if pane.dock_direction == direction and pane.dock_layer == layer and \
+           pane.dock_row > max_row:
+            max_row = pane.dock_row
+
+    return max_row
+
+
+def DoInsertDockLayer(panes, dock_direction, dock_layer):
+    """
+    DoInsertDockLayer() is an internal function that inserts a new dock
+    layer by incrementing all existing dock layer values by one.
+    """
+
+    for ii in xrange(len(panes)):
+        pane = panes[ii]
+        if not pane.IsFloating() and pane.dock_direction == dock_direction and pane.dock_layer >= dock_layer:
+            pane.dock_layer = pane.dock_layer + 1
+
+        panes[ii] = pane
+
+    return panes
+
+
+def DoInsertDockRow(panes, dock_direction, dock_layer, dock_row):
+    """
+    DoInsertDockRow() is an internal function that inserts a new dock
+    row by incrementing all existing dock row values by one.
+    """
+
+    for ii in xrange(len(panes)):
+        pane = panes[ii]
+        if not pane.IsFloating() and pane.dock_direction == dock_direction and \
+           pane.dock_layer == dock_layer and pane.dock_row >= dock_row:
+            pane.dock_row = pane.dock_row + 1
+
+        panes[ii] = pane
+
+    return panes
+
+
+def DoInsertPane(panes, dock_direction, dock_layer, dock_row, dock_pos):
+
+    for ii in xrange(len(panes)):
+        pane = panes[ii]
+        if not pane.IsFloating() and pane.dock_direction == dock_direction and \
+           pane.dock_layer == dock_layer and  pane.dock_row == dock_row and \
+           pane.dock_pos >= dock_pos:
+            pane.dock_pos = pane.dock_pos + 1
+
+        panes[ii] = pane
+
+    return panes
+
+
+def FindDocks(docks, dock_direction, dock_layer=-1, dock_row=-1, arr=[]):
+    """
+    FindDocks() is an internal function that returns a list of docks which meet
+    the specified conditions in the parameters and returns a sorted array
+    (sorted by layer and then row).
+    """
+
+    begin_layer = dock_layer
+    end_layer = dock_layer
+    begin_row = dock_row
+    end_row = dock_row
+    dock_count = len(docks)
+    max_row = 0
+    max_layer = 0
+
+    # discover the maximum dock layer and the max row
+    for ii in xrange(dock_count):
+        max_row = max(max_row, docks[ii].dock_row)
+        max_layer = max(max_layer, docks[ii].dock_layer)
+
+    # if no dock layer was specified, search all dock layers
+    if dock_layer == -1:
+        begin_layer = 0
+        end_layer = max_layer
+
+    # if no dock row was specified, search all dock row
+    if dock_row == -1:
+        begin_row = 0
+        end_row = max_row
+
+    arr = []
+
+    for layer in xrange(begin_layer, end_layer+1):
+        for row in xrange(begin_row, end_row+1):
+            for ii in xrange(dock_count):
+                d = docks[ii]
+                if dock_direction == -1 or dock_direction == d.dock_direction:
+                    if d.dock_layer == layer and d.dock_row == row:
+                        arr.append(d)
+
+    return arr
+
+
+def FindPaneInDock(dock, window):
+    """
+    FindPaneInDock() looks up a specified window pointer inside a dock.
+    If found, the corresponding AuiPaneInfo pointer is returned, otherwise None.
+    """
+
+    for p in dock.panes:
+        if p.window == window:
+            return p
+
+    return None
+
+
+def RemovePaneFromDocks(docks, pane, exc=None):
+    """
+    RemovePaneFromDocks() removes a pane window from all docks
+    with a possible exception specified by parameter "except".
+    """
+
+    for ii in xrange(len(docks)):
+        d = docks[ii]
+        if d == exc:
+            continue
+        pi = FindPaneInDock(d, pane.window)
+        if pi:
+            d.panes.remove(pi)
+
+        docks[ii] = d
+
+    return docks
+
+
+def RenumberDockRows(docks):
+    """
+    RenumberDockRows() takes a dock and assigns sequential numbers
+    to existing rows.  Basically it takes out the gaps so if a
+    dock has rows with numbers 0, 2, 5, they will become 0, 1, 2.
+    """
+
+    for ii in xrange(len(docks)):
+        dock = docks[ii]
+        dock.dock_row = ii
+        for jj in xrange(len(dock.panes)):
+            dock.panes[jj].dock_row = ii
+
+        docks[ii] = dock
+
+    return docks
+
+
+def SetActivePane(panes, active_pane):
+
+    for ii in xrange(len(panes)):
+        pane = panes[ii]
+        pane.state &= ~AuiPaneInfo.optionActive
+
+        if pane.window == active_pane:
+            pane.state |= AuiPaneInfo.optionActive
+
+        panes[ii] = pane
+
+    return panes
+
+
+def PaneSortFunc(p1, p2):
+    """ This function is used to sort panes by dock position. """
+
+    return (p1.dock_pos < p2.dock_pos and [-1] or [1])[0]
+
+
+def EscapeDelimiters(s):
+    """
+    EscapeDelimiters() changes "" into "\" and "|" into "\|"
+    in the input string.  This is an internal functions which is
+    used for saving perspectives.
+    """
+
+    result = s.replace(";", "\\")
+    result = result.replace("|", "|\\")
+
+    return result
+
+
+actionNone = 0
+actionResize = 1
+actionClickButton = 2
+actionClickCaption = 3
+actionDragToolbarPane = 4
+actionDragAuiFloatingPane = 5
+
+auiInsertRowPixels = 10
+auiNewRowPixels = 40
+auiLayerInsertPixels = 40
+auiLayerInsertOffset = 5
+
+# -- AuiManager class implementation --
+#
+# AuiManager manages the panes associated with it for a particular wx.Frame,
+# using a pane's AuiPaneInfo information to determine each pane's docking and
+# floating behavior. AuiManager uses wxPython's sizer mechanism to plan the
+# layout of each frame. It uses a replaceable dock art class to do all drawing,
+# so all drawing is localized in one area, and may be customized depending on an
+# applications' specific needs.
+#
+# AuiManager works as follows: The programmer adds panes to the class, or makes
+# changes to existing pane properties (dock position, floating state, show state, etc.).
+# To apply these changes, AuiManager's Update() function is called. This batch
+# processing can be used to avoid flicker, by modifying more than one pane at a time,
+# and then "committing" all of the changes at once by calling Update().
+#
+# Panes can be added quite easily:
+#
+#   text1 = wx.TextCtrl(self, -1)
+#   text2 = wx.TextCtrl(self, -1)
+#   self._mgr.AddPane(text1, wx.LEFT, "Pane Caption")
+#   self._mgr.AddPane(text2, wx.BOTTOM, "Pane Caption")
+#   self._mgr.Update()
+#
+# Later on, the positions can be modified easily. The following will float an
+# existing pane in a tool window:
+
+#   self._mgr.GetPane(text1).Float()
+
+# Layers, Rows and Directions, Positions
+# Inside PyAUI, the docking layout is figured out by checking several pane parameters.
+# Four of these are important for determining where a pane will end up.
+#
+# Direction - Each docked pane has a direction, Top, Bottom, Left, Right, or Center.
+# This is fairly self-explanatory. The pane will be placed in the location specified
+# by this variable.
+#
+# Position - More than one pane can be placed inside of a "dock." Imagine to panes
+# being docked on the left side of a window. One pane can be placed over another.
+# In proportionally managed docks, the pane position indicates it's sequential position,
+# starting with zero. So, in our scenario with two panes docked on the left side, the
+# top pane in the dock would have position 0, and the second one would occupy position 1.
+#
+# Row - A row can allow for two docks to be placed next to each other. One of the most
+# common places for this to happen is in the toolbar. Multiple toolbar rows are allowed,
+# the first row being in row 0, and the second in row 1. Rows can also be used on
+# vertically docked panes.
+#
+# Layer - A layer is akin to an onion. Layer 0 is the very center of the managed pane.
+# Thus, if a pane is in layer 0, it will be closest to the center window (also sometimes
+# known as the "content window"). Increasing layers "swallow up" all layers of a lower
+# value. This can look very similar to multiple rows, but is different because all panes
+# in a lower level yield to panes in higher levels. The best way to understand layers
+# is by running the PyAUI sample (PyAUIDemo.py).
+
+class AuiManager(wx.EvtHandler):
+
+    def __init__(self, frame=None, flags=None):
+        """
+        Default Class Constructor. frame specifies the wx.Frame which should be managed.
+        flags specifies options which allow the frame management behavior to be modified.
+        """
+
+        wx.EvtHandler.__init__(self)
+        self._action = actionNone
+        self._last_mouse_move = wx.Point()
+        self._hover_button = None
+        self._art = DefaultDockArt()
+        self._hint_wnd = None
+        self._action_window = None
+        self._last_hint = wx.Rect()
+        self._hint_fadetimer = wx.Timer(self, wx.NewId())
+        self._hintshown = False
+
+        if flags is None:
+            flags = AUI_MGR_DEFAULT
+
+        self._flags = flags
+        self._active_pane = None
+
+        if frame:
+            self.SetFrame(frame)
+
+        self._panes = []
+        self._docks = []
+        self._uiparts = []
+
+        self.Bind(wx.EVT_PAINT, self.OnPaint)
+        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+        self.Bind(wx.EVT_SET_CURSOR, self.OnSetCursor)
+        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
+        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
+        self.Bind(wx.EVT_MOTION, self.OnMotion)
+        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
+        self.Bind(wx.EVT_TIMER, self.OnHintFadeTimer)
+        self.Bind(wx.EVT_CHILD_FOCUS, self.OnChildFocus)
+        self.Bind(EVT_AUI_PANEBUTTON, self.OnPaneButton)
+
+
+    def GetPaneByWidget(self, window):
+        """
+        This version of GetPane() looks up a pane based on a
+        'pane window', see below comment for more info.
+        """
+
+        for p in self._panes:
+            if p.window == window:
+                return p
+
+        return NoneAuiPaneInfo
+
+
+    def GetPaneByName(self, name):
+        """
+        This version of GetPane() looks up a pane based on a
+        'pane name', see below comment for more info.
+        """
+
+        for p in self._panes:
+            if p.name == name:
+                return p
+
+        return NoneAuiPaneInfo
+
+
+    def GetPane(self, item):
+        """
+        GetPane() looks up a AuiPaneInfo structure based
+        on the supplied window pointer.  Upon failure, GetPane()
+        returns an empty AuiPaneInfo, a condition which can be checked
+        by calling AuiPaneInfo.IsOk().
+
+        The pane info's structure may then be modified.  Once a pane's
+        info is modified, AuiManager.Update() must be called to
+        realize the changes in the UI.
+
+        AG: Added To Handle 2 Different Versions Of GetPane() For
+        wxPython/Python.
+        """
+
+        if isinstance(item, type("")):
+            return self.GetPaneByName(item)
+        else:
+            return self.GetPaneByWidget(item)
+
+
+    def GetAllPanes(self):
+        """ GetAllPanes() returns a reference to all the pane info structures. """
+
+        return self._panes
+
+
+    def HitTest(self, x, y):
+        """
+        HitTest() is an internal function which determines
+        which UI item the specified coordinates are over
+        (x,y) specify a position in client coordinates.
+        """
+
+        result = None
+
+        for item in self._uiparts:
+            # we are not interested in typeDock, because this space
+            # isn't used to draw anything, just for measurements
+            # besides, the entire dock area is covered with other
+            # rectangles, which we are interested in.
+            if item.type == DockUIPart.typeDock:
+                continue
+
+            # if we already have a hit on a more specific item, we are not
+            # interested in a pane hit.  If, however, we don't already have
+            # a hit, returning a pane hit is necessary for some operations
+            if (item.type == DockUIPart.typePane or \
+                item.type == DockUIPart.typePaneBorder) and result:
+                continue
+
+            # if the point is inside the rectangle, we have a hit
+            if item.rect.Contains((x, y)):
+                result = item
+
+        return result
+
+
+    # SetFlags() and GetFlags() allow the owner to set various
+    # options which are global to AuiManager
+
+    def SetFlags(self, flags):
+        """
+        SetFlags() is used to specify AuiManager's settings flags. flags specifies
+        options which allow the frame management behavior to be modified.
+        """
+
+        self._flags = flags
+
+
+    def GetFlags(self):
+        """ GetFlags() returns the current manager's flags. """
+
+        return self._flags
+
+
+    def SetFrame(self, frame):
+        """
+        SetFrame() is usually called once when the frame
+        manager class is being initialized.  "frame" specifies
+        the frame which should be managed by the frame manager.
+        """
+
+        if not frame:
+            raise "\nERROR: Specified Frame Must Be Non-Null. "
+
+        self._frame = frame
+        self._frame.PushEventHandler(self)
+
+        # if the owner is going to manage an MDI parent frame,
+        # we need to add the MDI client window as the default
+        # center pane
+        if isinstance(frame, wx.MDIParentFrame):
+            mdi_frame = frame
+            client_window = mdi_frame.GetClientWindow()
+
+            if not client_window:
+                raise "\nERROR: MDI Client Window Is Null. "
+
+            self.AddPane(client_window, AuiPaneInfo().Name("mdiclient").
+                         CenterPane().PaneBorder(False))
+
+
+    def GetFrame(self):
+        """ GetFrame() returns the frame pointer being managed by AuiManager. """
+
+        return self._frame
+
+
+    def UnInit(self):
+        """
+        UnInit() must be called, usually in the destructor
+        of the frame class.   If it is not called, usually this
+        will result in a crash upon program exit.
+        """
+
+        self._frame.RemoveEventHandler(self)
+
+
+    def GetArtProvider(self):
+        """ GetArtProvider() returns the current art provider being used. """
+
+        return self._art
+
+
+    def ProcessMgrEvent(self, event):
+
+        # first, give the owner frame a chance to override
+        if self._frame:
+            if self._frame.ProcessEvent(event):
+                return
+
+        if event.GetEventType() != wxEVT_AUI_PANECLOSE:
+            self.ProcessEvent(event)
+
+
+    def CanMakeWindowsTransparent(self):
+        if wx.Platform == "__WXMSW__":
+            version = wx.GetOsDescription()
+            found = version.find("XP") >= 0 or version.find("2000") >= 0 or version.find("NT") >= 0
+            return found and _libimported
+        elif wx.Platform == "__WXMAC__" and _carbon_dll:
+            return True
+        else:
+            return False
+
+# on supported windows systems (Win2000 and greater), this function
+# will make a frame window transparent by a certain amount
+    def MakeWindowTransparent(self, wnd, amount):
+
+        if wnd.GetSize() == (0, 0):
+            return
+
+        # this API call is not in all SDKs, only the newer ones, so
+        # we will runtime bind this
+        if wx.Platform == "__WXMSW__":
+            hwnd = wnd.GetHandle()
+
+            if not hasattr(self, "_winlib"):
+                if _libimported == "MH":
+                    self._winlib = win32api.LoadLibrary("user32")
+                elif _libimported == "ctypes":
+                    self._winlib = ctypes.windll.user32
+
+            if _libimported == "MH":
+                pSetLayeredWindowAttributes = win32api.GetProcAddress(self._winlib,
+                                                                      "SetLayeredWindowAttributes")
+
+                if pSetLayeredWindowAttributes == None:
+                    return
+
+                exstyle = win32api.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
+                if 0 == (exstyle & 0x80000):
+                    win32api.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, exstyle | 0x80000)
+
+                winxpgui.SetLayeredWindowAttributes(hwnd, 0, amount, 2)
+
+            elif _libimported == "ctypes":
+                style = self._winlib.GetWindowLongA(hwnd, 0xffffffecL)
+                style |= 0x00080000
+                self._winlib.SetWindowLongA(hwnd, 0xffffffecL, style)
+                self._winlib.SetLayeredWindowAttributes(hwnd, 0, amount, 2)
+
+        elif wx.Platform == "__WXMAC__" and _carbon_dll:
+            handle = _carbon_dll.GetControlOwner(wnd.GetHandle())
+            if amount == 0:
+                amnt = float(0)
+            else:
+                amnt = float(amount)/255.0  #convert from the 0-255 amount to the float that Carbon wants
+            _carbon_dll.SetWindowAlpha(handle, ctypes.c_float(amnt))
+        else:
+            #shouldn't be called, but just in case...
+            return
+
+
+    def SetArtProvider(self, art_provider):
+        """
+        SetArtProvider() instructs AuiManager to use the
+        specified art provider for all drawing calls.  This allows
+        plugable look-and-feel features.
+        """
+
+        # delete the last art provider, if any
+        del self._art
+
+        # assign the new art provider
+        self._art = art_provider
+
+
+    def AddPane(self, window, arg1=None, arg2=None):
+        """
+        AddPane() tells the frame manager to start managing a child window. There
+        are two versions of this function. The first verison allows the full spectrum
+        of pane parameter possibilities (AddPane1). The second version is used for
+        simpler user interfaces which do not require as much configuration (AddPane2).
+        In wxPython, simply call AddPane.
+        """
+
+        if type(arg1) == type(1):
+            # This Is Addpane2
+            if arg1 is None:
+                arg1 = wx.LEFT
+            if arg2 is None:
+                arg2 = ""
+            return self.AddPane2(window, arg1, arg2)
+        else:
+            return self.AddPane1(window, arg1)
+
+
+    def AddPane1(self, window, pane_info):
+
+        # check if the pane has a valid window
+        if not window:
+            return False
+
+        # check if the pane already exists
+        if self.GetPane(pane_info.window).IsOk():
+            return False
+
+        if isinstance(window, wx.ToolBar):
+            window.SetBestFittingSize()
+
+        self._panes.append(pane_info)
+
+        pinfo = self._panes[-1]
+
+        # set the pane window
+        pinfo.window = window
+
+        # if the pane's name identifier is blank, create a random string
+        if len(pinfo.name) == 0 or pinfo.name == "":
+            pinfo.name = ("%s%08x%08x%08x")%(pinfo.window.GetName(), time.time(),
+                                             time.clock(), len(self._panes))
+
+        # set initial proportion (if not already set)
+        if pinfo.dock_proportion == 0:
+            pinfo.dock_proportion = 100000
+
+        if pinfo.HasCloseButton() and len(pinfo.buttons) == 0:
+            button = PaneButton(None)
+            button.button_id = AuiPaneInfo.buttonClose
+            pinfo.buttons.append(button)
+
+        if pinfo.best_size == wx.DefaultSize and pinfo.window:
+            pinfo.best_size = pinfo.window.GetClientSize()
+
+            if isinstance(pinfo.window, wx.ToolBar):
+                # GetClientSize() doesn't get the best size for
+                # a toolbar under some newer versions of wxWidgets,
+                # so use GetBestSize()
+                pinfo.best_size = pinfo.window.GetBestSize()
+
+                # for some reason, wxToolBar::GetBestSize() is returning
+                # a size that is a pixel shy of the correct amount.
+                # I believe this to be the correct action, until
+                # wxToolBar::GetBestSize() is fixed.  Is this assumption
+                # correct?
+                pinfo.best_size.y = pinfo.best_size.y + 1
+
+                # this is needed for Win2000 to correctly fill toolbar backround
+                # it should probably be repeated once system colour change happens
+                if wx.Platform == "__WXMSW__" and pinfo.window.UseBgCol():
+                    pinfo.window.SetBackgroundColour(self.GetArtProvider().GetColour(AUI_ART_BACKGROUND_COLOUR))
+
+            if pinfo.min_size != wx.DefaultSize:
+                if pinfo.best_size.x < pinfo.min_size.x:
+                    pinfo.best_size.x = pinfo.min_size.x
+                if pinfo.best_size.y < pinfo.min_size.y:
+                    pinfo.best_size.y = pinfo.min_size.y
+
+        self._panes[-1] = pinfo
+
+        return True
+
+
+    def AddPane2(self, window, direction, caption):
+
+        pinfo = AuiPaneInfo()
+        pinfo.Caption(caption)
+
+        if direction == wx.TOP:
+            pinfo.Top()
+        elif direction == wx.BOTTOM:
+            pinfo.Bottom()
+        elif direction == wx.LEFT:
+            pinfo.Left()
+        elif direction == wx.RIGHT:
+            pinfo.Right()
+        elif direction == wx.CENTER:
+            pinfo.CenterPane()
+
+        return self.AddPane(window, pinfo)
+
+
+    def InsertPane(self, window, pane_info, insert_level=AUI_INSERT_PANE):
+        """
+        InsertPane() is used to insert either a previously unmanaged pane window
+        into the frame manager, or to insert a currently managed pane somewhere else.
+        InsertPane() will push all panes, rows, or docks aside and insert the window
+        into the position specified by insert_location. Because insert_location can
+        specify either a pane, dock row, or dock layer, the insert_level parameter is
+        used to disambiguate this. The parameter insert_level can take a value of
+        AUI_INSERT_PANE, AUI_INSERT_ROW or AUI_INSERT_DOCK.
+        """
+
+        # shift the panes around, depending on the insert level
+        if insert_level == AUI_INSERT_PANE:
+            self._panes = DoInsertPane(self._panes, pane_info.dock_direction,
+                                       pane_info.dock_layer, pane_info.dock_row,
+                                       pane_info.dock_pos)
+
+        elif insert_level == AUI_INSERT_ROW:
+            self._panes = DoInsertDockRow(self._panes, pane_info.dock_direction,
+                                          pane_info.dock_layer, pane_info.dock_row)
+
+        elif insert_level == AUI_INSERT_DOCK:
+            self._panes = DoInsertDockLayer(self._panes, pane_info.dock_direction,
+                                            pane_info.dock_layer)
+
+        # if the window already exists, we are basically just moving/inserting the
+        # existing window.  If it doesn't exist, we need to add it and insert it
+        existing_pane = self.GetPane(window)
+        indx = self._panes.index(existing_pane)
+
+        if not existing_pane.IsOk():
+
+            return self.AddPane(window, pane_info)
+
+        else:
+
+            if pane_info.IsFloating():
+                existing_pane.Float()
+                if pane_info.floating_pos != wx.DefaultPosition:
+                    existing_pane.FloatingPosition(pane_info.floating_pos)
+                if pane_info.floating_size != wx.DefaultSize:
+                    existing_pane.FloatingSize(pane_info.floating_size)
+            else:
+                existing_pane.Direction(pane_info.dock_direction)
+                existing_pane.Layer(pane_info.dock_layer)
+                existing_pane.Row(pane_info.dock_row)
+                existing_pane.Position(pane_info.dock_pos)
+
+            self._panes[indx] = existing_pane
+
+        return True
+
+
+    def DetachPane(self, window):
+        """
+        DetachPane() tells the AuiManager to stop managing the pane specified
+        by window. The window, if in a floated frame, is reparented to the frame
+        managed by AuiManager.
+        """
+
+        for p in self._panes:
+            if p.window == window:
+                if p.frame:
+                    # we have a floating frame which is being detached. We need to
+                    # reparent it to m_frame and destroy the floating frame
+
+                    # reduce flicker
+                    p.window.SetSize(1,1)
+                    p.frame.Show(False)
+
+                    # reparent to self._frame and destroy the pane
+                    p.window.Reparent(self._frame)
+                    p.frame.SetSizer(None)
+                    p.frame.Destroy()
+                    p.frame = None
+
+                self._panes.remove(p)
+                return True
+
+        return False
+
+
+    def SavePerspective(self):
+        """
+        SavePerspective() saves all pane information as a single string.
+        This string may later be fed into LoadPerspective() to restore
+        all pane settings.  This save and load mechanism allows an
+        exact pane configuration to be saved and restored at a later time.
+        """
+
+        result = "layout1|"
+        pane_count = len(self._panes)
+
+        for pane_i in xrange(pane_count):
+            pane = self._panes[pane_i]
+            result = result + "name=" + EscapeDelimiters(pane.name) + ";"
+            result = result + "caption=" + EscapeDelimiters(pane.caption) + ";"
+            result = result + "state=%u;"%pane.state
+            result = result + "dir=%d;"%pane.dock_direction
+            result = result + "layer=%d;"%pane.dock_layer
+            result = result + "row=%d;"%pane.dock_row
+            result = result + "pos=%d;"%pane.dock_pos
+            result = result + "prop=%d;"%pane.dock_proportion
+            result = result + "bestw=%d;"%pane.best_size.x
+            result = result + "besth=%d;"%pane.best_size.y
+            result = result + "minw=%d;"%pane.min_size.x
+            result = result + "minh=%d;"%pane.min_size.y
+            result = result + "maxw=%d;"%pane.max_size.x
+            result = result + "maxh=%d;"%pane.max_size.y
+            result = result + "floatx=%d;"%pane.floating_pos.x
+            result = result + "floaty=%d;"%pane.floating_pos.y
+            result = result + "floatw=%d;"%pane.floating_size.x
+            result = result + "floath=%d"%pane.floating_size.y
+            result = result + "|"
+
+        dock_count = len(self._docks)
+
+        for dock_i in xrange(dock_count):
+            dock = self._docks[dock_i]
+            result = result + ("dock_size(%d,%d,%d)=%d|")%(dock.dock_direction,
+                                                           dock.dock_layer,
+                                                           dock.dock_row,
+                                                           dock.size)
+
+        return result
+
+
+    def LoadPerspective(self, layout, update=True):
+        """
+        LoadPerspective() loads a layout which was saved with SavePerspective()
+        If the "update" flag parameter is True, the GUI will immediately be updated.
+        """
+
+        input = layout
+        # check layout string version
+        indx = input.index("|")
+        part = input[0:indx]
+        input = input[indx+1:]
+        part = part.strip()
+
+        if part != "layout1":
+            return False
+
+        olddocks = self._docks[:]
+        oldpanes = self._panes[:]
+
+        # mark all panes currently managed as docked and hidden
+        pane_count = len(self._panes)
+        for pane_i in xrange(pane_count):
+            pane = self._panes[pane_i]
+            pane.Dock().Hide()
+            self._panes[pane_i] = pane
+
+        # clear out the dock array this will be reconstructed
+        self._docks = []
+
+        # replace escaped characters so we can
+        # split up the string easily
+        input = input.replace("\\|", "\a")
+        input = input.replace("\\", "\b")
+
+        input = input.split("|")
+
+        for line in input:
+
+            if line.startswith("dock_size"):
+
+                indx = line.index("=")
+                size = int(line[indx+1:])
+                indx1 = line.index("(")
+                indx2 = line.index(")")
+                line2 = line[indx1+1:indx2]
+                vals = line2.split(",")
+                dir = int(vals[0])
+                layer = int(vals[1])
+                row = int(vals[2])
+                dock = DockInfo()
+                dock.dock_direction = dir
+                dock.dock_layer = layer
+                dock.dock_row = row
+                dock.size = size
+
+                self._docks.append(dock)
+
+            elif line.startswith("name"):
+
+                newline = line.split(";")
+                pane = AuiPaneInfo()
+
+                for newl in newline:
+                    myline = newl.strip()
+                    vals = myline.split("=")
+                    val_name = vals[0]
+                    value = vals[1]
+                    if val_name == "name":
+                        pane.name = value
+                    elif val_name == "caption":
+                        pane.caption = value
+                    elif val_name == "state":
+                        pane.state = int(value)
+                    elif val_name == "dir":
+                        pane.dock_direction = int(value)
+                    elif val_name == "layer":
+                        pane.dock_layer = int(value)
+                    elif val_name == "row":
+                        pane.dock_row = int(value)
+                    elif val_name == "pos":
+                        pane.dock_pos = int(value)
+                    elif val_name == "prop":
+                        pane.dock_proportion = int(value)
+                    elif val_name == "bestw":
+                        pane.best_size.x = int(value)
+                    elif val_name == "besth":
+                        pane.best_size.y = int(value)
+                        pane.best_size = wx.Size(pane.best_size.x, pane.best_size.y)
+                    elif val_name == "minw":
+                        pane.min_size.x = int(value)
+                    elif val_name == "minh":
+                        pane.min_size.y = int(value)
+                        pane.min_size = wx.Size(pane.min_size.x, pane.min_size.y)
+                    elif val_name == "maxw":
+                        pane.max_size.x = int(value)
+                    elif val_name == "maxh":
+                        pane.max_size.y = int(value)
+                        pane.max_size = wx.Size(pane.max_size.x, pane.max_size.y)
+                    elif val_name == "floatx":
+                        pane.floating_pos.x = int(value)
+                    elif val_name == "floaty":
+                        pane.floating_pos.y = int(value)
+                        pane.floating_pos = wx.Point(pane.floating_pos.x, pane.floating_pos.y)
+                    elif val_name == "floatw":
+                        pane.floating_size.x = int(value)
+                    elif val_name == "floath":
+                        pane.floating_size.y = int(value)
+                        pane.floating_size = wx.Size(pane.floating_size.x, pane.floating_size.y)
+                    else:
+                        raise "\nERROR: Bad Perspective String."
+
+                # replace escaped characters so we can
+                # split up the string easily
+                pane.name = pane.name.replace("\a", "|")
+                pane.name = pane.name.replace("\b", ";")
+                pane.caption = pane.caption.replace("\a", "|")
+                pane.caption = pane.caption.replace("\b", ";")
+
+                p = self.GetPane(pane.name)
+                if not p.IsOk():
+                    # the pane window couldn't be found
+                    # in the existing layout
+                    return False
+
+                indx = self._panes.index(p)
+                pane.window = p.window
+                pane.frame = p.frame
+                pane.buttons = p.buttons
+                self._panes[indx] = pane
+
+        if update:
+            self.Update()
+
+        return True
+
+
+    def GetPanePositionsAndSizes(self, dock):
+        """ Returns all the panes positions and sizes. """
+
+        caption_size = self._art.GetMetric(AUI_ART_CAPTION_SIZE)
+        pane_border_size = self._art.GetMetric(AUI_ART_PANE_BORDER_SIZE)
+        gripper_size = self._art.GetMetric(AUI_ART_GRIPPER_SIZE)
+
+        positions = []
+        sizes = []
+
+        action_pane = -1
+        pane_count = len(dock.panes)
+
+        # find the pane marked as our action pane
+        for pane_i in xrange(pane_count):
+            pane = dock.panes[pane_i]
+            if pane.state & AuiPaneInfo.actionPane:
+                action_pane = pane_i
+
+        # set up each panes default position, and
+        # determine the size (width or height, depending
+        # on the dock's orientation) of each pane
+        for pane in dock.panes:
+            positions.append(pane.dock_pos)
+            size = 0
+
+            if pane.HasBorder():
+                size  = size + pane_border_size*2
+
+            if dock.IsHorizontal():
+                if pane.HasGripper() and not pane.HasGripperTop():
+                    size = size + gripper_size
+
+                size = size + pane.best_size.x
+
+            else:
+                if pane.HasGripper() and pane.HasGripperTop():
+                    size = size + gripper_size
+
+                if pane.HasCaption():
+                    size = size + caption_size
+
+                size = size + pane.best_size.y
+
+            sizes.append(size)
+
+        # if there is no action pane, just return the default
+        # positions (as specified in pane.pane_pos)
+        if action_pane == -1:
+            return positions, sizes
+
+        offset = 0
+        for pane_i in xrange(action_pane-1, -1, -1):
+            amount = positions[pane_i+1] - (positions[pane_i] + sizes[pane_i])
+            if amount >= 0:
+                offset = offset + amount
+            else:
+                positions[pane_i] -= -amount
+
+            offset = offset + sizes[pane_i]
+
+        # if the dock mode is fixed, make sure none of the panes
+        # overlap we will bump panes that overlap
+        offset = 0
+        for pane_i in xrange(action_pane, pane_count):
+            amount = positions[pane_i] - offset
+            if amount >= 0:
+                offset = offset + amount
+            else:
+                positions[pane_i] += -amount
+
+            offset = offset + sizes[pane_i]
+
+        return positions, sizes
+
+
+    def LayoutAddPane(self, cont, dock, pane, uiparts, spacer_only):
+
+        sizer_item = wx.SizerItem()
+        caption_size = self._art.GetMetric(AUI_ART_CAPTION_SIZE)
+        gripper_size = self._art.GetMetric(AUI_ART_GRIPPER_SIZE)
+        pane_border_size = self._art.GetMetric(AUI_ART_PANE_BORDER_SIZE)
+        pane_button_size = self._art.GetMetric(AUI_ART_PANE_BUTTON_SIZE)
+
+        # find out the orientation of the item (orientation for panes
+        # is the same as the dock's orientation)
+
+        if dock.IsHorizontal():
+            orientation = wx.HORIZONTAL
+        else:
+            orientation = wx.VERTICAL
+
+        # this variable will store the proportion
+        # value that the pane will receive
+        pane_proportion = pane.dock_proportion
+
+        horz_pane_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        vert_pane_sizer = wx.BoxSizer(wx.VERTICAL)
+
+        if pane.HasGripper():
+
+            part = DockUIPart()
+            if pane.HasGripperTop():
+                sizer_item = vert_pane_sizer.Add((1, gripper_size), 0, wx.EXPAND)
+            else:
+                sizer_item = horz_pane_sizer.Add((gripper_size, 1), 0, wx.EXPAND)
+
+            part.type = DockUIPart.typeGripper
+            part.dock = dock
+            part.pane = pane
+            part.button = None
+            part.orientation = orientation
+            part.cont_sizer = horz_pane_sizer
+            part.sizer_item = sizer_item
+            uiparts.append(part)
+
+        if pane.HasCaption():
+
+            # create the caption sizer
+            part = DockUIPart()
+            caption_sizer = wx.BoxSizer(wx.HORIZONTAL)
+            sizer_item = caption_sizer.Add((1, caption_size), 1, wx.EXPAND)
+            part.type = DockUIPart.typeCaption
+            part.dock = dock
+            part.pane = pane
+            part.button = None
+            part.orientation = orientation
+            part.cont_sizer = vert_pane_sizer
+            part.sizer_item = sizer_item
+            caption_part_idx = len(uiparts)
+            uiparts.append(part)
+
+            # add pane buttons to the caption
+            for button in pane.buttons:
+                sizer_item = caption_sizer.Add((pane_button_size,
+                                               caption_size),
+                                               0, wx.EXPAND)
+                part = DockUIPart()
+                part.type = DockUIPart.typePaneButton
+                part.dock = dock
+                part.pane = pane
+                part.button = button
+                part.orientation = orientation
+                part.cont_sizer = caption_sizer
+                part.sizer_item = sizer_item
+                uiparts.append(part)
+
+            # add the caption sizer
+            sizer_item = vert_pane_sizer.Add(caption_sizer, 0, wx.EXPAND)
+            uiparts[caption_part_idx].sizer_item = sizer_item
+
+        # add the pane window itself
+        if spacer_only:
+            sizer_item = vert_pane_sizer.Add((1, 1), 1, wx.EXPAND)
+        else:
+            sizer_item = vert_pane_sizer.Add(pane.window, 1, wx.EXPAND)
+            vert_pane_sizer.SetItemMinSize(pane.window, (1, 1))
+
+        part = DockUIPart()
+        part.type = DockUIPart.typePane
+        part.dock = dock
+        part.pane = pane
+        part.button = None
+        part.orientation = orientation
+        part.cont_sizer = vert_pane_sizer
+        part.sizer_item = sizer_item
+        uiparts.append(part)
+
+        # determine if the pane should have a minimum size if the pane is
+        # non-resizable (fixed) then we must set a minimum size. Alternitavely,
+        # if the pane.min_size is set, we must use that value as well
+
+        min_size = pane.min_size
+        if pane.IsFixed():
+            if min_size == wx.DefaultSize:
+                min_size = pane.best_size
+                pane_proportion = 0
+
+        if min_size != wx.DefaultSize:
+            vert_pane_sizer.SetItemMinSize(
+                len(vert_pane_sizer.GetChildren())-1, (min_size.x, min_size.y))
+
+        # add the verticle sizer (caption, pane window) to the
+        # horizontal sizer (gripper, verticle sizer)
+        horz_pane_sizer.Add(vert_pane_sizer, 1, wx.EXPAND)
+
+        # finally, add the pane sizer to the dock sizer
+        if pane.HasBorder():
+            # allowing space for the pane's border
+            sizer_item = cont.Add(horz_pane_sizer, pane_proportion,
+                                  wx.EXPAND | wx.ALL, pane_border_size)
+            part = DockUIPart()
+            part.type = DockUIPart.typePaneBorder
+            part.dock = dock
+            part.pane = pane
+            part.button = None
+            part.orientation = orientation
+            part.cont_sizer = cont
+            part.sizer_item = sizer_item
+            uiparts.append(part)
+        else:
+            sizer_item = cont.Add(horz_pane_sizer, pane_proportion, wx.EXPAND)
+
+        return uiparts
+
+
+    def LayoutAddDock(self, cont, dock, uiparts, spacer_only):
+
+        sizer_item = wx.SizerItem()
+        part = DockUIPart()
+
+        sash_size = self._art.GetMetric(AUI_ART_SASH_SIZE)
+        orientation = (dock.IsHorizontal() and [wx.HORIZONTAL] or [wx.VERTICAL])[0]
+
+        # resizable bottom and right docks have a sash before them
+        if not dock.fixed and (dock.dock_direction == AUI_DOCK_BOTTOM or \
+                               dock.dock_direction == AUI_DOCK_RIGHT):
+
+            sizer_item = cont.Add((sash_size, sash_size), 0, wx.EXPAND)
+
+            part.type = DockUIPart.typeDockSizer
+            part.orientation = orientation
+            part.dock = dock
+            part.pane = None
+            part.button = None
+            part.cont_sizer = cont
+            part.sizer_item = sizer_item
+            uiparts.append(part)
+
+        # create the sizer for the dock
+        dock_sizer = wx.BoxSizer(orientation)
+
+        # add each pane to the dock
+        pane_count = len(dock.panes)
+
+        if dock.fixed:
+
+            # figure out the real pane positions we will
+            # use, without modifying the each pane's pane_pos member
+            pane_positions, pane_sizes = self.GetPanePositionsAndSizes(dock)
+
+            offset = 0
+            for pane_i in xrange(pane_count):
+
+                pane = dock.panes[pane_i]
+                pane_pos = pane_positions[pane_i]
+
+                amount = pane_pos - offset
+                if amount > 0:
+
+                    if dock.IsVertical():
+                        sizer_item = dock_sizer.Add((1, amount), 0, wx.EXPAND)
+                    else:
+                        sizer_item = dock_sizer.Add((amount, 1), 0, wx.EXPAND)
+
+                    part = DockUIPart()
+                    part.type = DockUIPart.typeBackground
+                    part.dock = dock
+                    part.pane = None
+                    part.button = None
+                    part.orientation = (orientation==wx.HORIZONTAL and \
+                                        [wx.VERTICAL] or [wx.HORIZONTAL])[0]
+                    part.cont_sizer = dock_sizer
+                    part.sizer_item = sizer_item
+                    uiparts.append(part)
+
+                    offset = offset + amount
+
+                uiparts = self.LayoutAddPane(dock_sizer, dock, pane, uiparts, spacer_only)
+
+                offset = offset + pane_sizes[pane_i]
+
+            # at the end add a very small stretchable background area
+            sizer_item = dock_sizer.Add((1, 1), 1, wx.EXPAND)
+            part = DockUIPart()
+            part.type = DockUIPart.typeBackground
+            part.dock = dock
+            part.pane = None
+            part.button = None
+            part.orientation = orientation
+            part.cont_sizer = dock_sizer
+            part.sizer_item = sizer_item
+            uiparts.append(part)
+
+        else:
+
+            for pane_i in xrange(pane_count):
+
+                pane = dock.panes[pane_i]
+
+                # if this is not the first pane being added,
+                # we need to add a pane sizer
+                if pane_i > 0:
+                    sizer_item = dock_sizer.Add((sash_size, sash_size), 0, wx.EXPAND)
+                    part = DockUIPart()
+                    part.type = DockUIPart.typePaneSizer
+                    part.dock = dock
+                    part.pane = dock.panes[pane_i-1]
+                    part.button = None
+                    part.orientation = (orientation==wx.HORIZONTAL and \
+                                        [wx.VERTICAL] or [wx.HORIZONTAL])[0]
+                    part.cont_sizer = dock_sizer
+                    part.sizer_item = sizer_item
+                    uiparts.append(part)
+
+                uiparts = self.LayoutAddPane(dock_sizer, dock, pane, uiparts, spacer_only)
+
+        if dock.dock_direction == AUI_DOCK_CENTER:
+            sizer_item = cont.Add(dock_sizer, 1, wx.EXPAND)
+        else:
+            sizer_item = cont.Add(dock_sizer, 0, wx.EXPAND)
+
+        part = DockUIPart()
+        part.type = DockUIPart.typeDock
+        part.dock = dock
+        part.pane = None
+        part.button = None
+        part.orientation = orientation
+        part.cont_sizer = cont
+        part.sizer_item = sizer_item
+        uiparts.append(part)
+
+        if dock.IsHorizontal():
+            cont.SetItemMinSize(dock_sizer, (0, dock.size))
+        else:
+            cont.SetItemMinSize(dock_sizer, (dock.size, 0))
+
+        #  top and left docks have a sash after them
+        if not dock.fixed and (dock.dock_direction == AUI_DOCK_TOP or \
+                               dock.dock_direction == AUI_DOCK_LEFT):
+
+            sizer_item = cont.Add((sash_size, sash_size), 0, wx.EXPAND)
+
+            part = DockUIPart()
+            part.type = DockUIPart.typeDockSizer
+            part.dock = dock
+            part.pane = None
+            part.button = None
+            part.orientation = orientation
+            part.cont_sizer = cont
+            part.sizer_item = sizer_item
+            uiparts.append(part)
+
+        return uiparts
+
+
+    def LayoutAll(self, panes, docks, uiparts, spacer_only=False, oncheck=True):
+
+        container = wx.BoxSizer(wx.VERTICAL)
+
+        pane_border_size = self._art.GetMetric(AUI_ART_PANE_BORDER_SIZE)
+        caption_size = self._art.GetMetric(AUI_ART_CAPTION_SIZE)
+        cli_size = self._frame.GetClientSize()
+
+        # empty all docks out
+        for ii in xrange(len(docks)):
+            docks[ii].panes = []
+
+        dock_count = len(docks)
+
+        # iterate through all known panes, filing each
+        # of them into the appropriate dock. If the
+        # pane does not exist in the dock, add it
+        for p in panes:
+
+            # find any docks in this layer
+            arr = FindDocks(docks, p.dock_direction, p.dock_layer, p.dock_row)
+
+            if len(arr) > 0:
+                dock = arr[0]
+            else:
+                # dock was not found, so we need to create a new one
+                d = DockInfo()
+                d.dock_direction = p.dock_direction
+                d.dock_layer = p.dock_layer
+                d.dock_row = p.dock_row
+                docks.append(d)
+                dock = docks[-1]
+
+            if p.IsDocked() and p.IsShown():
+                # remove the pane from any existing docks except this one
+                docks = RemovePaneFromDocks(docks, p, dock)
+
+                # pane needs to be added to the dock,
+                # if it doesn't already exist
+                if not FindPaneInDock(dock, p.window):
+                    dock.panes.append(p)
+            else:
+                # remove the pane from any existing docks
+                docks = RemovePaneFromDocks(docks, p)
+
+        # remove any empty docks
+        for ii in xrange(len(docks)-1, -1, -1):
+            if len(docks[ii].panes) == 0:
+                docks.pop(ii)
+
+        dock_count = len(docks)
+        # configure the docks further
+        for ii in xrange(len(docks)):
+            dock = docks[ii]
+            dock_pane_count = len(dock.panes)
+
+            # sort the dock pane array by the pane's
+            # dock position (dock_pos), in ascending order
+            dock.panes.sort(PaneSortFunc)
+
+            # for newly created docks, set up their initial size
+            if dock.size == 0:
+                size = 0
+                for jj in xrange(dock_pane_count):
+                    pane = dock.panes[jj]
+                    pane_size = pane.best_size
+                    if pane_size == wx.DefaultSize:
+                        pane_size = pane.min_size
+                    if pane_size == wx.DefaultSize:
+                        pane_size = pane.window.GetSize()
+
+                    if dock.IsHorizontal():
+                        size = max(pane_size.y, size)
+                    else:
+                        size = max(pane_size.x, size)
+
+                # add space for the border (two times), but only
+                # if at least one pane inside the dock has a pane border
+                for jj in xrange(dock_pane_count):
+                    if dock.panes[jj].HasBorder():
+                        size = size + pane_border_size*2
+                        break
+
+                # if pane is on the top or bottom, add the caption height,
+                # but only if at least one pane inside the dock has a caption
+                if dock.IsHorizontal():
+                    for jj in xrange(dock_pane_count):
+                        if dock.panes[jj].HasCaption():
+                            size = size + caption_size
+                            break
+
+                # new dock's size may not be more than 1/3 of the frame size
+                if dock.IsHorizontal():
+                    size = min(size, cli_size.y/3)
+                else:
+                    size = min(size, cli_size.x/3)
+
+                if size < 10:
+                    size = 10
+
+                dock.size = size
+
+            # determine the dock's minimum size
+            plus_border = False
+            plus_caption = False
+            dock_min_size = 0
+            for jj in xrange(dock_pane_count):
+                pane = dock.panes[jj]
+                if pane.min_size != wx.DefaultSize:
+                    if pane.HasBorder():
+                        plus_border = True
+                    if pane.HasCaption():
+                        plus_caption = True
+                    if dock.IsHorizontal():
+                        if pane.min_size.y > dock_min_size:
+                            dock_min_size = pane.min_size.y
+                    else:
+                        if pane.min_size.x > dock_min_size:
+                            dock_min_size = pane.min_size.x
+
+            if plus_border:
+                dock_min_size = dock_min_size + pane_border_size*2
+            if plus_caption and dock.IsHorizontal():
+                dock_min_size = dock_min_size + caption_size
+
+            dock.min_size = dock_min_size
+
+            # if the pane's current size is less than it's
+            # minimum, increase the dock's size to it's minimum
+            if dock.size < dock.min_size:
+                dock.size = dock.min_size
+
+            # determine the dock's mode (fixed or proportional)
+            # determine whether the dock has only toolbars
+            action_pane_marked = False
+            dock.fixed = True
+            dock.toolbar = True
+            for jj in xrange(dock_pane_count):
+                pane = dock.panes[jj]
+                if not pane.IsFixed():
+                    dock.fixed = False
+                if not pane.IsToolbar():
+                    dock.toolbar = False
+                if pane.state & AuiPaneInfo.actionPane:
+                    action_pane_marked = True
+
+            # if the dock mode is proportional and not fixed-pixel,
+            # reassign the dock_pos to the sequential 0, 1, 2, 3
+            # e.g. remove gaps like 1, 2, 30, 500
+            if not dock.fixed:
+                for jj in xrange(dock_pane_count):
+                    pane = dock.panes[jj]
+                    pane.dock_pos = jj
+                    dock.panes[jj] = pane
+
+            # if the dock mode is fixed, and none of the panes
+            # are being moved right now, make sure the panes
+            # do not overlap each other.  If they do, we will
+            # adjust the panes' positions
+            if dock.fixed and not action_pane_marked:
+                pane_positions, pane_sizes = self.GetPanePositionsAndSizes(dock)
+                offset = 0
+                for jj in xrange(dock_pane_count):
+                    pane = dock.panes[jj]
+                    pane.dock_pos = pane_positions[jj]
+                    amount = pane.dock_pos - offset
+                    if amount >= 0:
+                        offset = offset + amount
+                    else:
+                        pane.dock_pos += -amount
+
+                    offset = offset + pane_sizes[jj]
+                    dock.panes[jj] = pane
+
+            if oncheck:
+                self._docks[ii] = dock
+
+        # discover the maximum dock layer
+        max_layer = 0
+
+        for ii in xrange(dock_count):
+            max_layer = max(max_layer, docks[ii].dock_layer)
+
+        # clear out uiparts
+        uiparts = []
+
+        # create a bunch of box sizers,
+        # from the innermost level outwards.
+        cont = None
+        middle = None
+
+        if oncheck:
+            docks = self._docks
+
+        for layer in xrange(max_layer+1):
+            # find any docks in this layer
+            arr = FindDocks(docks, -1, layer, -1)
+            # if there aren't any, skip to the next layer
+            if len(arr) == 0:
+                continue
+
+            old_cont = cont
+
+            # create a container which will hold this layer's
+            # docks (top, bottom, left, right)
+            cont = wx.BoxSizer(wx.VERTICAL)
+
+            # find any top docks in this layer
+            arr = FindDocks(docks, AUI_DOCK_TOP, layer, -1, arr)
+            arr = RenumberDockRows(arr)
+            if len(arr) > 0:
+                for row in xrange(len(arr)):
+                    uiparts = self.LayoutAddDock(cont, arr[row], uiparts, spacer_only)
+
+            # fill out the middle layer (which consists
+            # of left docks, content area and right docks)
+
+            middle = wx.BoxSizer(wx.HORIZONTAL)
+
+            # find any left docks in this layer
+            arr = FindDocks(docks, AUI_DOCK_LEFT, layer, -1, arr)
+            arr = RenumberDockRows(arr)
+            if len(arr) > 0:
+                for row in xrange(len(arr)):
+                    uiparts = self.LayoutAddDock(middle, arr[row], uiparts, spacer_only)
+
+            # add content dock (or previous layer's sizer
+            # to the middle
+            if not old_cont:
+                # find any center docks
+                arr = FindDocks(docks, AUI_DOCK_CENTER, -1, -1, arr)
+                if len(arr) > 0:
+                    for row in xrange(len(arr)):
+                       uiparts = self.LayoutAddDock(middle, arr[row], uiparts, spacer_only)
+                else:
+                    # there are no center docks, add a background area
+                    sizer_item = middle.Add((1, 1), 1, wx.EXPAND)
+                    part = DockUIPart()
+                    part.type = DockUIPart.typeBackground
+                    part.pane = None
+                    part.dock = None
+                    part.button = None
+                    part.cont_sizer = middle
+                    part.sizer_item = sizer_item
+                    uiparts.append(part)
+            else:
+                middle.Add(old_cont, 1, wx.EXPAND)
+
+            # find any right docks in this layer
+            arr = FindDocks(docks, AUI_DOCK_RIGHT, layer, -1, arr)
+            arr = RenumberDockRows(arr)
+            if len(arr) > 0:
+                for row in xrange(len(arr)-1, -1, -1):
+                    uiparts = self.LayoutAddDock(middle, arr[row], uiparts, spacer_only)
+
+            cont.Add(middle, 1, wx.EXPAND)
+
+            # find any bottom docks in this layer
+            arr = FindDocks(docks, AUI_DOCK_BOTTOM, layer, -1, arr)
+            arr = RenumberDockRows(arr)
+            if len(arr) > 0:
+                for row in xrange(len(arr)-1, -1, -1):
+                    uiparts = self.LayoutAddDock(cont, arr[row], uiparts, spacer_only)
+
+        if not cont:
+            # no sizer available, because there are no docks,
+            # therefore we will create a simple background area
+            cont = wx.BoxSizer(wx.VERTICAL)
+            sizer_item = cont.Add((1, 1), 1, wx.EXPAND)
+            part = DockUIPart()
+            part.type = DockUIPart.typeBackground
+            part.pane = None
+            part.dock = None
+            part.button = None
+            part.cont_sizer = middle
+            part.sizer_item = sizer_item
+            uiparts.append(part)
+
+        if oncheck:
+            self._uiparts = uiparts
+            self._docks = docks
+
+        container.Add(cont, 1, wx.EXPAND)
+
+        if oncheck:
+            return container
+        else:
+            return container, panes, docks, uiparts
+
+
+    def Update(self):
+        """
+        Update() updates the layout.  Whenever changes are made to
+        one or more panes, this function should be called.  It is the
+        external entry point for running the layout engine.
+        """
+
+        pane_count = len(self._panes)
+        # delete old sizer first
+        self._frame.SetSizer(None)
+
+        # destroy floating panes which have been
+        # redocked or are becoming non-floating
+        for ii in xrange(pane_count):
+            p = self._panes[ii]
+            if not p.IsFloating() and p.frame:
+                # because the pane is no longer in a floating, we need to
+                # reparent it to self._frame and destroy the floating frame
+                # reduce flicker
+                p.window.SetSize((1, 1))
+                p.frame.Show(False)
+
+                # reparent to self._frame and destroy the pane
+                p.window.Reparent(self._frame)
+                p.frame.SetSizer(None)
+                p.frame.Destroy()
+                p.frame = None
+
+            self._panes[ii] = p
+
+        # create a layout for all of the panes
+        sizer = self.LayoutAll(self._panes, self._docks, self._uiparts, False)
+
+        # hide or show panes as necessary,
+        # and float panes as necessary
+
+        pane_count = len(self._panes)
+
+        for ii in xrange(pane_count):
+            p = self._panes[ii]
+            if p.IsFloating():
+                if p.frame == None:
+                    # we need to create a frame for this
+                    # pane, which has recently been floated
+                    resizeborder = 1
+                    if p.IsFixed():
+                        resizeborder = 0
+
+                    frame = AuiFloatingPane(self._frame, self, -1, "", p.floating_pos,
+                                         p.floating_size, resizeborder=resizeborder)
+
+                    # on MSW, if the owner desires transparent dragging, and
+                    # the dragging is happening right now, then the floating
+                    # window should have this style by default
+
+                    if self._action == actionDragAuiFloatingPane and self.UseTransparentDrag():
+                        self.MakeWindowTransparent(frame, 150)
+
+                    frame.SetPaneWindow(p)
+                    p.frame = frame
+                    if p.IsShown():
+                        frame.Show()
+                else:
+
+                    # frame already exists, make sure it's position
+                    # and size reflect the information in AuiPaneInfo
+                    if p.frame.GetPosition() != p.floating_pos:
+                        p.frame.SetDimensions(p.floating_pos.x, p.floating_pos.y,
+                                        -1, -1, wx.SIZE_USE_EXISTING)
+                    p.frame.Show(p.IsShown())
+            else:
+
+                p.window.Show(p.IsShown())
+
+            # if "active panes" are no longer allowed, clear
+            # any optionActive values from the pane states
+            if self._flags & AUI_MGR_ALLOW_ACTIVE_PANE == 0:
+                p.state &= ~AuiPaneInfo.optionActive
+
+            self._panes[ii] = p
+
+
+        old_pane_rects = []
+
+        for ii in xrange(pane_count):
+            r = wx.Rect()
+            p = self._panes[ii]
+
+            if p.window and p.IsShown() and p.IsDocked():
+                r = p.rect
+
+            old_pane_rects.append(r)
+
+        # apply the new sizer
+        self._frame.SetSizer(sizer)
+        self._frame.SetAutoLayout(False)
+        self.DoFrameLayout()
+
+        # now that the frame layout is done, we need to check
+        # the new pane rectangles against the old rectangles that
+        # we saved a few lines above here.  If the rectangles have
+        # changed, the corresponding panes must also be updated
+        for ii in xrange(pane_count):
+            p = self._panes[ii]
+            if p.window and p.IsShown() and p.IsDocked():
+                if p.rect != old_pane_rects[ii]:
+                    p.window.Refresh()
+                    p.window.Update()
+
+        self.Repaint()
+
+
+    def DoFrameLayout(self):
+        """
+        DoFrameLayout() is an internal function which invokes wxSizer.Layout
+        on the frame's main sizer, then measures all the various UI items
+        and updates their internal rectangles.  This should always be called
+        instead of calling self._frame.Layout() directly
+        """
+
+        self._frame.Layout()
+
+        for ii in xrange(len(self._uiparts)):
+            part = self._uiparts[ii]
+
+            # get the rectangle of the UI part
+            # originally, this code looked like this:
+            #    part.rect = wx.Rect(part.sizer_item.GetPosition(),
+            #                       part.sizer_item.GetSize())
+            # this worked quite well, with one exception: the mdi
+            # client window had a "deferred" size variable
+            # that returned the wrong size.  It looks like
+            # a bug in wx, because the former size of the window
+            # was being returned.  So, we will retrieve the part's
+            # rectangle via other means
+
+            part.rect = part.sizer_item.GetRect()
+            flag = part.sizer_item.GetFlag()
+            border = part.sizer_item.GetBorder()
+
+            if flag & wx.TOP:
+                part.rect.y -= border
+                part.rect.height += border
+            if flag & wx.LEFT:
+                part.rect.x -= border
+                part.rect.width += border
+            if flag & wx.BOTTOM:
+                part.rect.height += border
+            if flag & wx.RIGHT:
+                part.rect.width += border
+
+            if part.type == DockUIPart.typeDock:
+                part.dock.rect = part.rect
+            if part.type == DockUIPart.typePane:
+                part.pane.rect = part.rect
+
+            self._uiparts[ii] = part
+
+
+    def GetPanePart(self, wnd):
+        """
+        GetPanePart() looks up the pane border UI part of the
+        pane specified.  This allows the caller to get the exact rectangle
+        of the pane in question, including decorations like caption and border.
+        """
+
+        for ii in xrange(len(self._uiparts)):
+            part = self._uiparts[ii]
+            if part.type == DockUIPart.typePaneBorder and \
+               part.pane and part.pane.window == wnd:
+                return part
+
+        for ii in xrange(len(self._uiparts)):
+            part = self._uiparts[ii]
+            if part.type == DockUIPart.typePane and \
+               part.pane and part.pane.window == wnd:
+                return part
+
+        return None
+
+
+    def GetDockPixelOffset(self, test):
+        """
+        GetDockPixelOffset() is an internal function which returns
+        a dock's offset in pixels from the left side of the window
+        (for horizontal docks) or from the top of the window (for
+        vertical docks).  This value is necessary for calculating
+        fixel-pane/toolbar offsets when they are dragged.
+        """
+
+        # the only way to accurately calculate the dock's
+        # offset is to actually run a theoretical layout
+
+        docks, panes = CopyDocksAndPanes2(self._docks, self._panes)
+        panes.append(test)
+
+        sizer, panes, docks, uiparts = self.LayoutAll(panes, docks, [], True, False)
+        client_size = self._frame.GetClientSize()
+        sizer.SetDimension(0, 0, client_size.x, client_size.y)
+        sizer.Layout()
+
+        for ii in xrange(len(uiparts)):
+            part = uiparts[ii]
+            pos = part.sizer_item.GetPosition()
+            size = part.sizer_item.GetSize()
+            part.rect = wx.Rect(pos[0], pos[1], size[0], size[1])
+            if part.type == DockUIPart.typeDock:
+                part.dock.rect = part.rect
+
+        sizer.Destroy()
+
+        for ii in xrange(len(docks)):
+            dock = docks[ii]
+            if test.dock_direction == dock.dock_direction and \
+               test.dock_layer == dock.dock_layer and  \
+               test.dock_row == dock.dock_row:
+
+                if dock.IsVertical():
+                    return dock.rect.y
+                else:
+                    return dock.rect.x
+
+        return 0
+
+
+    def ProcessDockResult(self, target, new_pos):
+        """
+        ProcessDockResult() is a utility function used by DoDrop() - it checks
+        if a dock operation is allowed, the new dock position is copied into
+        the target info.  If the operation was allowed, the function returns True.
+        """
+
+        allowed = False
+        if new_pos.dock_direction == AUI_DOCK_TOP:
+            allowed = target.IsTopDockable()
+        elif new_pos.dock_direction == AUI_DOCK_BOTTOM:
+            allowed = target.IsBottomDockable()
+        elif new_pos.dock_direction == AUI_DOCK_LEFT:
+            allowed = target.IsLeftDockable()
+        elif new_pos.dock_direction == AUI_DOCK_RIGHT:
+            allowed = target.IsRightDockable()
+
+        if allowed:
+            target = new_pos
+
+        return allowed, target
+
+
+    def DoDrop(self, docks, panes, target, pt, offset=wx.Point(0,0)):
+        """
+        DoDrop() is an important function.  It basically takes a mouse position,
+        and determines where the panes new position would be.  If the pane is to be
+        dropped, it performs the drop operation using the specified dock and pane
+        arrays.  By specifying copy dock and pane arrays when calling, a "what-if"
+        scenario can be performed, giving precise coordinates for drop hints.
+        """
+
+        cli_size = self._frame.GetClientSize()
+
+        drop = AuiPaneInfo()
+        drop.name = target.name
+        drop.caption = target.caption
+        drop.window = target.window
+        drop.frame = target.frame
+        drop.state = target.state
+        drop.dock_direction = target.dock_direction
+        drop.dock_layer = target.dock_layer
+        drop.dock_row = target.dock_row
+        drop.dock_pos = target.dock_pos
+        drop.best_size = target.best_size
+        drop.min_size = target.min_size
+        drop.max_size = target.max_size
+        drop.floating_pos = target.floating_pos
+        drop.floating_size = target.floating_size
+        drop.dock_proportion = target.dock_proportion
+        drop.buttons = target.buttons
+        drop.rect = target.rect
+
+        # The result should always be shown
+        drop.Show()
+
+        # Check to see if the pane has been dragged outside of the window
+        # (or near to the outside of the window), if so, dock it along the edge
+
+        layer_insert_offset = auiLayerInsertOffset
+
+        if target.IsToolbar():
+            layer_insert_offset = 0
+
+        if pt.x < layer_insert_offset and \
+           pt.x > layer_insert_offset-auiLayerInsertPixels:
+            new_layer = max(max(GetMaxLayer(docks, AUI_DOCK_LEFT),
+                                GetMaxLayer(docks, AUI_DOCK_BOTTOM)),
+                            GetMaxLayer(docks, AUI_DOCK_TOP)) + 1
+
+            drop.Dock().Left().Layer(new_layer).Row(0). \
+                 Position(pt.y - self.GetDockPixelOffset(drop) - offset.y)
+
+            return self.ProcessDockResult(target, drop)
+
+        elif pt.y < layer_insert_offset and \
+              pt.y > layer_insert_offset-auiLayerInsertPixels:
+            new_layer = max(max(GetMaxLayer(docks, AUI_DOCK_TOP),
+                                GetMaxLayer(docks, AUI_DOCK_LEFT)),
+                            GetMaxLayer(docks, AUI_DOCK_RIGHT)) + 1
+
+            drop.Dock().Top().Layer(new_layer).Row(0). \
+                 Position(pt.x - self.GetDockPixelOffset(drop) - offset.x)
+
+            return self.ProcessDockResult(target, drop)
+
+        elif pt.x >= cli_size.x - layer_insert_offset and \
+              pt.x < cli_size.x - layer_insert_offset + auiLayerInsertPixels:
+
+            new_layer = max(max(GetMaxLayer(docks, AUI_DOCK_RIGHT),
+                                GetMaxLayer(docks, AUI_DOCK_TOP)),
+                            GetMaxLayer(docks, AUI_DOCK_BOTTOM)) + 1
+
+            drop.Dock().Right().Layer(new_layer).Row(0). \
+                 Position(pt.y - self.GetDockPixelOffset(drop) - offset.y)
+
+            return self.ProcessDockResult(target, drop)
+
+        elif pt.y >= cli_size.y - layer_insert_offset and \
+             pt.y < cli_size.y - layer_insert_offset + auiLayerInsertPixels:
+
+            new_layer = max(max(GetMaxLayer(docks, AUI_DOCK_BOTTOM),
+                                GetMaxLayer(docks, AUI_DOCK_LEFT)),
+                            GetMaxLayer(docks, AUI_DOCK_RIGHT)) + 1
+
+            drop.Dock().Bottom().Layer(new_layer).Row(0). \
+                 Position(pt.x - self.GetDockPixelOffset(drop) - offset.x)
+
+            return self.ProcessDockResult(target, drop)
+
+        part = self.HitTest(pt.x, pt.y)
+
+        if drop.IsToolbar():
+            if not part or not part.dock:
+                return False, target
+
+            # calculate the offset from where the dock begins
+            # to the point where the user dropped the pane
+            dock_drop_offset = 0
+            if part.dock.IsHorizontal():
+                dock_drop_offset = pt.x - part.dock.rect.x - offset.x
+            else:
+                dock_drop_offset = pt.y - part.dock.rect.y - offset.y
+
+            # toolbars may only be moved in and to fixed-pane docks,
+            # otherwise we will try to float the pane.  Also, the pane
+            # should float if being dragged over center pane windows
+            if not part.dock.fixed or part.dock.dock_direction == AUI_DOCK_CENTER:
+                if (self._flags & AUI_MGR_ALLOW_FLOATING) and (drop.IsFloatable() or (\
+                    part.dock.dock_direction != AUI_DOCK_CENTER and \
+                    part.dock.dock_direction != AUI_DOCK_NONE)):
+
+                    drop.Float()
+
+                return self.ProcessDockResult(target, drop)
+
+            drop.Dock(). \
+                 Direction(part.dock.dock_direction). \
+                 Layer(part.dock.dock_layer). \
+                 Row(part.dock.dock_row). \
+                 Position(dock_drop_offset)
+
+            if pt.y < part.dock.rect.y + 2 and len(part.dock.panes) > 1:
+                row = drop.dock_row
+                panes = DoInsertDockRow(panes, part.dock.dock_direction,
+                                        part.dock.dock_layer,
+                                        part.dock.dock_row)
+                drop.dock_row = row
+
+            if pt.y > part.dock.rect.y + part.dock.rect.height - 2 and \
+               len(part.dock.panes) > 1:
+                panes = DoInsertDockRow(panes, part.dock.dock_direction,
+                                        part.dock.dock_layer,
+                                        part.dock.dock_row+1)
+                drop.dock_row = part.dock.dock_row + 1
+
+            return self.ProcessDockResult(target, drop)
+
+        if not part:
+            return False, target
+
+        if part.type == DockUIPart.typePaneBorder or \
+            part.type == DockUIPart.typeCaption or \
+            part.type == DockUIPart.typeGripper or \
+            part.type == DockUIPart.typePaneButton or \
+            part.type == DockUIPart.typePane or \
+            part.type == DockUIPart.typePaneSizer or \
+            part.type == DockUIPart.typeDockSizer or \
+            part.type == DockUIPart.typeBackground:
+
+            if part.type == DockUIPart.typeDockSizer:
+                if len(part.dock.panes) != 1:
+                    return False, target
+
+                part = self.GetPanePart(part.dock.panes[0].window)
+
+                if not part:
+                    return False, target
+
+            # If a normal frame is being dragged over a toolbar, insert it
+            # along the edge under the toolbar, but over all other panes.
+            # (this could be done much better, but somehow factoring this
+            # calculation with the one at the beginning of this function)
+            if part.dock and (hasattr(part.dock, "toolbar") and part.dock.toolbar):
+                layer = 0
+
+                if part.dock.dock_direction == AUI_DOCK_LEFT:
+                    layer = max(max(GetMaxLayer(docks, AUI_DOCK_LEFT),
+                                    GetMaxLayer(docks, AUI_DOCK_BOTTOM)),
+                                GetMaxLayer(docks, AUI_DOCK_TOP))
+                elif part.dock.dock_direction == AUI_DOCK_TOP:
+                    layer = max(max(GetMaxLayer(docks, AUI_DOCK_TOP),
+                                    GetMaxLayer(docks, AUI_DOCK_LEFT)),
+                                GetMaxLayer(docks, AUI_DOCK_RIGHT))
+                elif part.dock.dock_direction == AUI_DOCK_RIGHT:
+                    layer = max(max(GetMaxLayer(docks, AUI_DOCK_RIGHT),
+                                    GetMaxLayer(docks, AUI_DOCK_TOP)),
+                                GetMaxLayer(docks, AUI_DOCK_BOTTOM))
+                elif part.dock.dock_direction == AUI_DOCK_BOTTOM:
+                    layer = max(max(GetMaxLayer(docks, AUI_DOCK_BOTTOM),
+                                    GetMaxLayer(docks, AUI_DOCK_LEFT)),
+                                GetMaxLayer(docks, AUI_DOCK_RIGHT))
+
+                panes = DoInsertDockRow(panes, part.dock.dock_direction,
+                                        layer, 0)
+                drop.Dock(). \
+                     Direction(part.dock.dock_direction). \
+                     Layer(layer).Row(0).Position(0)
+
+                return self.ProcessDockResult(target, drop)
+
+            if not part.pane:
+                return False, target
+
+            part = self.GetPanePart(part.pane.window)
+            if not part:
+                return False, target
+
+            insert_dock_row = False
+            insert_row = part.pane.dock_row
+            insert_dir = part.pane.dock_direction
+            insert_layer = part.pane.dock_layer
+
+            if part.pane.dock_direction == AUI_DOCK_TOP:
+                if pt.y >= part.rect.y and \
+                   pt.y < part.rect.y+auiInsertRowPixels:
+                    insert_dock_row = True
+
+            elif part.pane.dock_direction == AUI_DOCK_BOTTOM:
+                if pt.y > part.rect.y+part.rect.height-auiInsertRowPixels and \
+                   pt.y <= part.rect.y + part.rect.height:
+                    insert_dock_row = True
+
+            elif part.pane.dock_direction == AUI_DOCK_LEFT:
+                if pt.x >= part.rect.x and \
+                   pt.x < part.rect.x+auiInsertRowPixels:
+                    insert_dock_row = True
+
+            elif part.pane.dock_direction == AUI_DOCK_RIGHT:
+                if pt.x > part.rect.x+part.rect.width-auiInsertRowPixels and \
+                   pt.x <= part.rect.x+part.rect.width:
+                    insert_dock_row = True
+
+            elif part.pane.dock_direction == AUI_DOCK_CENTER:
+                # "new row pixels" will be set to the default, but
+                # must never exceed 20% of the window size
+                new_row_pixels_x = auiNewRowPixels
+                new_row_pixels_y = auiNewRowPixels
+
+                if new_row_pixels_x > part.rect.width*20/100:
+                    new_row_pixels_x = part.rect.width*20/100
+
+                if new_row_pixels_y > part.rect.height*20/100:
+                    new_row_pixels_y = part.rect.height*20/100
+
+                    # determine if the mouse pointer is in a location that
+                    # will cause a new row to be inserted.  The hot spot positions
+                    # are along the borders of the center pane
+
+                    insert_layer = 0
+                    insert_dock_row = True
+
+                    if pt.x >= part.rect.x and \
+                       pt.x < part.rect.x+new_row_pixels_x:
+                        insert_dir = AUI_DOCK_LEFT
+                    elif pt.y >= part.rect.y and \
+                         pt.y < part.rect.y+new_row_pixels_y:
+                        insert_dir = AUI_DOCK_TOP
+                    elif pt.x >= part.rect.x + part.rect.width-new_row_pixels_x and \
+                         pt.x < part.rect.x + part.rect.width:
+                        insert_dir = AUI_DOCK_RIGHT
+                    elif pt.y >= part.rect.y+ part.rect.height-new_row_pixels_y and \
+                         pt.y < part.rect.y + part.rect.height:
+                        insert_dir = AUI_DOCK_BOTTOM
+                    else:
+                        return False, target
+
+                    insert_row = GetMaxRow(panes, insert_dir, insert_layer) + 1
+
+            if insert_dock_row:
+
+                panes = DoInsertDockRow(panes, insert_dir, insert_layer,
+                                        insert_row)
+                drop.Dock().Direction(insert_dir). \
+                            Layer(insert_layer). \
+                            Row(insert_row). \
+                            Position(0)
+
+                return self.ProcessDockResult(target, drop)
+
+            # determine the mouse offset and the pane size, both in the
+            # direction of the dock itself, and perpendicular to the dock
+
+            if part.orientation == wx.VERTICAL:
+
+                offset = pt.y - part.rect.y
+                size = part.rect.GetHeight()
+
+            else:
+
+                offset = pt.x - part.rect.x
+                size = part.rect.GetWidth()
+
+            drop_position = part.pane.dock_pos
+
+            # if we are in the top/left part of the pane,
+            # insert the pane before the pane being hovered over
+            if offset <= size/2:
+
+                drop_position = part.pane.dock_pos
+                panes = DoInsertPane(panes,
+                                     part.pane.dock_direction,
+                                     part.pane.dock_layer,
+                                     part.pane.dock_row,
+                                     part.pane.dock_pos)
+
+            # if we are in the bottom/right part of the pane,
+            # insert the pane before the pane being hovered over
+            if offset > size/2:
+
+                drop_position = part.pane.dock_pos+1
+                panes = DoInsertPane(panes,
+                                     part.pane.dock_direction,
+                                     part.pane.dock_layer,
+                                     part.pane.dock_row,
+                                     part.pane.dock_pos+1)
+
+            drop.Dock(). \
+                 Direction(part.dock.dock_direction). \
+                 Layer(part.dock.dock_layer). \
+                 Row(part.dock.dock_row). \
+                 Position(drop_position)
+
+            return self.ProcessDockResult(target, drop)
+
+        return False, target
+
+
+    def UseTransparentHint(self):
+        return (self._flags & AUI_MGR_TRANSPARENT_HINT) and self.CanMakeWindowsTransparent()
+
+    def OnHintFadeTimer(self, event):
+        #sanity check
+        if not self.UseTransparentHint():
+            return
+
+        if not self._hint_wnd or self._hint_fadeamt >= 50:
+            self._hint_fadetimer.Stop()
+            return
+
+        self._hint_fadeamt = self._hint_fadeamt + 5
+        self.MakeWindowTransparent(self._hint_wnd, self._hint_fadeamt)
+
+
+    def ShowHint(self, rect):
+        self._hintshown = True
+        if self.UseTransparentHint():
+            if wx.Platform == "__WXMSW__":
+                if self._last_hint == rect:
+                    return
+                self._last_hint = rect
+
+                initial_fade = 50
+
+                if self._flags & AUI_MGR_TRANSPARENT_HINT_FADE:
+                    initial_fade = 0
+
+                if self._hint_wnd == None:
+
+                    pt = rect.GetPosition()
+                    size = rect.GetSize()
+                    self._hint_wnd = wx.Frame(self._frame, -1, "", pt, size,
+                                              wx.FRAME_TOOL_WINDOW |
+                                              wx.FRAME_FLOAT_ON_PARENT |
+                                              wx.FRAME_NO_TASKBAR |
+                                              wx.NO_BORDER)
+
+                    self.MakeWindowTransparent(self._hint_wnd, initial_fade)
+                    self._hint_wnd.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION))
+                    self._hint_wnd.Show()
+
+                    # if we are dragging a floating pane, set the focus
+
+                    # back to that floating pane (otherwise it becomes unfocused)
+
+                    if self._action == actionDragAuiFloatingPane and self._action_window:
+                        self._action_window.SetFocus()
+
+                else:
+
+                    pt = rect.GetPosition()
+                    size = rect.GetSize()
+                    self.MakeWindowTransparent(self._hint_wnd, initial_fade)
+                    self._hint_wnd.SetDimensions(pt.x, pt.y, rect.width, rect.height)
+
+                if self._flags & AUI_MGR_TRANSPARENT_HINT_FADE:
+                    # start fade in timer
+                    self._hint_fadeamt = 0
+                    self._hint_fadetimer.SetOwner(self, 101)
+                    self._hint_fadetimer.Start(5)
+                return
+            elif wx.Platform == "__WXMAC__":
+                if self._last_hint == rect:
+                    return  #same rect, already shown, no-op
+                if self._flags & AUI_MGR_TRANSPARENT_HINT_FADE:
+                    initial_fade = 0
+                else:
+                    initial_fade = 80
+
+                if not self._hint_wnd:
+                    self._hint_wnd = wx.MiniFrame(self._frame,
+                        style=wx.FRAME_FLOAT_ON_PARENT|wx.FRAME_TOOL_WINDOW
+                        |wx.CAPTION#|wx.FRAME_SHAPED
+                        #without wxCAPTION + wx.FRAME_TOOL_WINDOW, the hint window
+                        #gets focus & dims the main frames toolbar, which is both wrong
+                        #and distracting.
+                        #wx.CAPTION + wx.FRAME_TOOL_WINDOW cures the focus problem,
+                        #but then it draws the caption. Adding wx.FRAME_SHAPED takes
+                        #care of that, but then SetRect doesn't work to size - need to
+                        #create a bitmap or mask or something.
+                        )
+                    #can't set the background of a wx.Frame in OSX
+                    p = wx.Panel(self._hint_wnd)
+
+                    #the caption color is a light silver thats really hard to see
+                    #especially transparent. See if theres some other system
+                    #setting that is more appropriate, or just extend the art provider
+                    #to cover this
+                    #p.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION))
+                    p.SetBackgroundColour(wx.BLUE)
+
+                self.MakeWindowTransparent(self._hint_wnd, initial_fade)
+                self._hint_wnd.SetRect(rect)
+                self._hint_wnd.Show()
+
+                if self._action == actionDragAuiFloatingPane and self._action_window:
+                   self._action_window.SetFocus()
+
+                if self._flags & AUI_MGR_TRANSPARENT_HINT_FADE:
+                    # start fade in timer
+                    self._hint_fadeamt = 0
+                    self._hint_fadetimer.SetOwner(self, 101)
+                    self._hint_fadetimer.Start(5)
+                return
+
+
+        if self._last_hint != rect:
+            # remove the last hint rectangle
+            self._last_hint = rect
+            self._frame.Refresh()
+            self._frame.Update()
+
+
+
+        screendc = wx.ScreenDC()
+        clip = wx.Region(1, 1, 10000, 10000)
+
+        # clip all floating windows, so we don't draw over them
+        for pane in self._panes:
+            if pane.IsFloating() and pane.frame.IsShown():
+                recta = pane.frame.GetRect()
+                if wx.Platform == "__WXGTK__":
+                    # wxGTK returns the client size, not the whole frame size
+                    width, height = pane.frame.ClientToScreen((0,0)) - pane.frame.GetPosition()
+                    recta.width = recta.width + width
+                    recta.height = recta.height + height
+                    recta.Inflate(5, 5)
+                    #endif
+
+                clip.SubtractRect(recta)
+
+        screendc.SetClippingRegionAsRegion(clip)
+
+        screendc.SetBrush(wx.Brush(wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION)))
+        screendc.SetPen(wx.TRANSPARENT_PEN)
+
+        screendc.DrawRectangle(rect.x, rect.y, 5, rect.height)
+        screendc.DrawRectangle(rect.x+5, rect.y, rect.width-10, 5)
+        screendc.DrawRectangle(rect.x+rect.width-5, rect.y, 5, rect.height)
+        screendc.DrawRectangle(rect.x+5, rect.y+rect.height-5, rect.width-10, 5)
+
+
+    def HideHint(self):
+
+        self._hintshown = False
+
+        # hides a transparent window hint (currently wxMSW only)
+        if self.UseTransparentHint():
+            if self._hint_wnd:
+                self._hint_fadetimer.Stop()
+                #self._hint_wnd.Destroy()
+                self.MakeWindowTransparent(self._hint_wnd, 0)
+                self._last_hint = wx.Rect()
+
+            return
+
+        # hides a painted hint by redrawing the frame window
+        if not self._last_hint.IsEmpty():
+            self._frame.Refresh()
+            self._frame.Update()
+            self._last_hint = wx.Rect()
+
+
+    def DrawHintRect(self, pane_window, pt, offset):
+        """
+        DrawHintRect() draws a drop hint rectangle. First calls DoDrop() to
+        determine the exact position the pane would be at were if dropped.  If
+        the pame would indeed become docked at the specified drop point,
+        DrawHintRect() then calls ShowHint() to indicate this drop rectangle.
+        "pane_window" is the window pointer of the pane being dragged, pt is
+        the mouse position, in client coordinates.
+        """
+
+        # we need to paint a hint rectangle to find out the exact hint rectangle,
+        # we will create a new temporary layout and then measure the resulting
+        # rectangle we will create a copy of the docking structures (self._docks)
+        # so that we don't modify the real thing on screen
+
+        rect = wx.Rect()
+        pane = self.GetPane(pane_window)
+
+        attrs = self.GetAttributes(pane)
+        hint = AuiPaneInfo()
+        hint = self.SetAttributes(hint, attrs)
+
+        if hint.name != "__HINT__":
+            self._oldname = hint.name
+
+        hint.name = "__HINT__"
+
+        if not hint.IsOk():
+            hint.name = self._oldname
+            return
+
+        docks, panes = CopyDocksAndPanes2(self._docks, self._panes)
+
+        # remove any pane already there which bears the same window
+        # this happens when you are moving a pane around in a dock
+        for ii in xrange(len(panes)):
+            if panes[ii].window == pane_window:
+                docks = RemovePaneFromDocks(docks, panes[ii])
+                panes.pop(ii)
+                break
+
+        # find out where the new pane would be
+        allow, hint = self.DoDrop(docks, panes, hint, pt, offset)
+
+        if not allow:
+            self.HideHint()
+            return
+
+        panes.append(hint)
+
+        sizer, panes, docks, uiparts = self.LayoutAll(panes, docks, [], True, False)
+        client_size = self._frame.GetClientSize()
+        sizer.SetDimension(0, 0, client_size.x, client_size.y)
+        sizer.Layout()
+
+        for ii in xrange(len(uiparts)):
+            part = uiparts[ii]
+            if part.type == DockUIPart.typePaneBorder and \
+               part.pane and part.pane.name == "__HINT__":
+                pos = part.sizer_item.GetPosition()
+                size = part.sizer_item.GetSize()
+                rect = wx.Rect(pos[0], pos[1], size[0], size[1])
+                break
+
+        sizer.Destroy()
+
+        if rect.IsEmpty():
+            self.HideHint()
+            return
+
+        # actually show the hint rectangle on the screen
+        rect.x, rect.y = self._frame.ClientToScreen((rect.x, rect.y))
+        self.ShowHint(rect)
+
+
+    def GetAttributes(self, pane):
+
+        attrs = []
+        attrs.extend([pane.window, pane.frame, pane.state, pane.dock_direction,
+                     pane.dock_layer, pane.dock_pos, pane.dock_row, pane.dock_proportion,
+                     pane.floating_pos, pane.floating_size, pane.best_size,
+                     pane.min_size, pane.max_size, pane.caption, pane.name,
+                     pane.buttons, pane.rect])
+
+        return attrs
+
+
+    def SetAttributes(self, pane, attrs):
+
+        pane.window = attrs[0]
+        pane.frame = attrs[1]
+        pane.state = attrs[2]
+        pane.dock_direction = attrs[3]
+        pane.dock_layer = attrs[4]
+        pane.dock_pos = attrs[5]
+        pane.dock_row = attrs[6]
+        pane.dock_proportion = attrs[7]
+        pane.floating_pos = attrs[8]
+        pane.floating_size = attrs[9]
+        pane.best_size = attrs[10]
+        pane.min_size = attrs[11]
+        pane.max_size = attrs[12]
+        pane.caption = attrs[13]
+        pane.name = attrs[14]
+        pane.buttons = attrs[15]
+        pane.rect = attrs[16]
+
+        return pane
+
+    def UseTransparentDrag(self):
+
+        if self._flags & AUI_MGR_TRANSPARENT_DRAG:
+            return self.CanMakeWindowsTransparent()
+        else:
+            return False
+
+
+    def OnAuiFloatingPaneMoveStart(self, wnd):
+
+        # try to find the pane
+        pane = self.GetPane(wnd)
+        if not pane.IsOk():
+            raise "\nERROR: Pane Window Not Found"
+        if self.UseTransparentDrag() and pane.IsDockable():
+            self.MakeWindowTransparent(pane.frame, 150)
+
+
+    def OnAuiFloatingPaneMoving(self, wnd):
+
+        # try to find the pane
+        pane = self.GetPane(wnd)
+
+        if not pane.IsOk():
+            raise "\nERROR: Pane Window Not Found"
+
+        pt = wx.GetMousePosition()
+        client_pt = self._frame.ScreenToClient(pt)
+
+        # calculate the offset from the upper left-hand corner
+        # of the frame to the mouse pointer
+        frame_pos = pane.frame.GetPosition()
+        action_offset = wx.Point(pt[0]-frame_pos.x, pt[1]-frame_pos.y)
+
+##        # no hint for toolbar floating windows
+##        if pane.IsToolbar() and self._action == actionDragAuiFloatingPane:
+##
+##            oldname = pane.name
+##            indx = self._panes.index(pane)
+##            hint = pane
+##            docks, panes = CopyDocksAndPanes2(self._docks, self._panes)
+##
+##            # find out where the new pane would be
+##            ret, hint = self.DoDrop(docks, panes, hint, client_pt)
+##
+##            if not ret:
+##                return
+##
+##            if hint.IsFloating():
+##                return
+##
+##            pane = hint
+##            pane.name = oldname
+##
+##            self._panes[indx] = pane
+##            self._action = actionDragToolbarPane
+##            self._action_window = pane.window
+##
+##            self.Update()
+##
+##            return
+
+        # if a key modifier is pressed while dragging the frame,
+        # don't dock the window
+        if wx.GetKeyState(wx.WXK_CONTROL) or wx.GetKeyState(wx.WXK_ALT):
+            self.HideHint()
+            return
+
+        if pane.IsDockable():
+            self.DrawHintRect(wnd, client_pt, action_offset)
+
+        # reduces flicker
+        self._frame.Update()
+
+
+    def OnAuiFloatingPaneMoved(self, wnd):
+
+        # try to find the pane
+        pane = self.GetPane(wnd)
+
+        if not pane.IsOk():
+            raise "\nERROR: Pane Window Not Found"
+
+        pt = wx.GetMousePosition()
+        client_pt = self._frame.ScreenToClient(pt)
+
+        indx = self._panes.index(pane)
+
+        # calculate the offset from the upper left-hand corner
+        # of the frame to the mouse pointer
+        frame_pos = pane.frame.GetPosition()
+        action_offset = wx.Point(pt[0]-frame_pos.x, pt[1]-frame_pos.y)
+
+        # if a key modifier is pressed while dragging the frame,
+        # don't dock the window
+        if wx.GetKeyState(wx.WXK_CONTROL) or wx.GetKeyState(wx.WXK_ALT):
+            self.HideHint()
+            return
+
+        if not pane.IsToolbar() and pane.IsDockable() and not self._hintshown:
+            if not pane.IsFloating():
+                pane.Float()
+                pane.floating_pos = pane.frame.GetPosition()
+                self._panes[indx] = pane
+                if self.UseTransparentDrag():
+                    self.MakeWindowTransparent(pane.frame, 255)
+
+        # do the drop calculation
+        allow, pane = self.DoDrop(self._docks, self._panes, pane, client_pt, action_offset)
+
+        # if the pane is still floating, update it's floating
+        # position (that we store)
+        if pane.IsFloating():
+            pane.floating_pos = pane.frame.GetPosition()
+            if self.UseTransparentDrag():
+                self.MakeWindowTransparent(pane.frame, 255)
+
+        if not pane.IsToolbar() and pane.IsDockable():
+            pane.name = self._oldname
+
+        self._panes[indx] = pane
+
+        self.Update()
+        self.HideHint()
+
+
+    def OnAuiFloatingPaneResized(self, wnd, size):
+
+        # try to find the pane
+        pane = self.GetPane(wnd)
+        if not pane.IsOk():
+            raise "\nERROR: Pane Window Not Found"
+
+        indx = self._panes.index(pane)
+        pane.floating_size = size
+        self._panes[indx] = pane
+
+
+    def OnAuiFloatingPaneClosed(self, wnd, event):
+        # try to find the pane
+        pane = self.GetPane(wnd)
+        if not pane.IsOk():
+            raise "\nERROR: Pane Window Not Found"
+
+        e = AuiManagerEvent(wxEVT_AUI_PANECLOSE)
+        e.SetPane(pane)
+        self.ProcessMgrEvent(e)
+
+        if e.GetSkipped():
+            indx = self._panes.index(pane)
+            # reparent the pane window back to us and
+            # prepare the frame window for destruction
+            pane.window.Show(False)
+            pane.window.Reparent(self._frame)
+            pane.frame = None
+            pane.Hide()
+
+            self._panes[indx] = pane
+            event.Skip()
+
+
+
+    def OnAuiFloatingPaneActivated(self, wnd):
+
+        if self.GetFlags() & AUI_MGR_ALLOW_ACTIVE_PANE:
+            # try to find the pane
+            pane = self.GetPane(wnd)
+            if not pane.IsOk():
+                raise "\nERROR: Pane Window Not Found"
+
+            self._panes = SetActivePane(self._panes, wnd)
+            self.Repaint()
+
+
+    def Render(self, dc):
+        """
+        Render() draws all of the pane captions, sashes,
+        backgrounds, captions, grippers, pane borders and buttons.
+        It renders the entire user interface.
+        """
+
+        for part in self._uiparts:
+
+            # don't draw hidden pane items
+            if part.sizer_item and not part.sizer_item.IsShown():
+                continue
+
+            if part.type == DockUIPart.typeDockSizer or \
+               part.type == DockUIPart.typePaneSizer:
+                self._art.DrawSash(dc, part.orientation, part.rect)
+            elif part.type == DockUIPart.typeBackground:
+                self._art.DrawBackground(dc, part.orientation, part.rect)
+            elif part.type == DockUIPart.typeCaption:
+                self._art.DrawCaption(dc, part.pane.caption, part.rect, part.pane)
+            elif part.type == DockUIPart.typeGripper:
+                self._art.DrawGripper(dc, part.rect, part.pane)
+            elif part.type == DockUIPart.typePaneBorder:
+                self._art.DrawBorder(dc, part.rect, part.pane)
+            elif part.type == DockUIPart.typePaneButton:
+                self._art.DrawPaneButton(dc, part.button.button_id,
+                                         AUI_BUTTON_STATE_NORMAL, part.rect, part.pane)
+
+
+    def Repaint(self, dc=None):
+
+        w, h = self._frame.GetClientSize()
+        # figure out which dc to use if one
+        # has been specified, use it, otherwise
+        # make a client dc
+        client_dc = None
+
+        if not dc:
+            client_dc = wx.ClientDC(self._frame)
+            dc = client_dc
+
+        # if the frame has a toolbar, the client area
+        # origin will not be (0,0).
+        pt = self._frame.GetClientAreaOrigin()
+        if pt.x != 0 or pt.y != 0:
+            dc.SetDeviceOrigin(pt.x, pt.y)
+
+        # render all the items
+        self.Render(dc)
+
+        # if we created a client_dc, delete it
+        if client_dc:
+            del client_dc
+
+
+    def OnPaint(self, event):
+
+        dc = wx.PaintDC(self._frame)
+
+        if wx.Platform == "__WXMAC__":
+            #Macs paint optimizations clip the area we need to paint a log
+            #of the time, this is a dirty hack to always paint everything
+            self.Repaint(None)
+        else:
+            self.Repaint(dc)
+
+
+    def OnEraseBackground(self, event):
+
+        if wx.Platform == "__WXMAC__":
+            event.Skip()
+
+
+    def OnSize(self, event):
+
+        if self._frame:
+
+            self.DoFrameLayout()
+            wx.CallAfter(self.Repaint)
+
+            if not isinstance(self._frame, wx.MDIParentFrame):
+                event.Skip()
+
+
+    def OnSetCursor(self, event):
+
+        # determine cursor
+        part = self.HitTest(event.GetX(), event.GetY())
+        cursor = None
+
+        if part:
+            if part.type == DockUIPart.typeDockSizer or \
+               part.type == DockUIPart.typePaneSizer:
+
+                # a dock may not be resized if it has a single
+                # pane which is not resizable
+                if part.type == DockUIPart.typeDockSizer and part.dock and \
+                   len(part.dock.panes) == 1 and part.dock.panes[0].IsFixed():
+                    return
+
+                # panes that may not be resized do not get a sizing cursor
+                if part.pane and part.pane.IsFixed():
+                    return
+
+                if part.orientation == wx.VERTICAL:
+                    cursor = wx.StockCursor(wx.CURSOR_SIZEWE)
+                else:
+                    cursor = wx.StockCursor(wx.CURSOR_SIZENS)
+
+            elif part.type == DockUIPart.typeGripper:
+                cursor = wx.StockCursor(wx.CURSOR_SIZING)
+
+        if cursor is not None:
+            event.SetCursor(cursor)
+
+
+    def UpdateButtonOnScreen(self, button_ui_part, event):
+
+        hit_test = self.HitTest(event.GetX(), event.GetY())
+        state = AUI_BUTTON_STATE_NORMAL
+
+        if hit_test == button_ui_part:
+            if event.LeftDown():
+                state = AUI_BUTTON_STATE_PRESSED
+            else:
+                state = AUI_BUTTON_STATE_HOVER
+        else:
+            if event.LeftDown():
+                state = AUI_BUTTON_STATE_HOVER
+
+        # now repaint the button with hover state
+        cdc = wx.ClientDC(self._frame)
+
+        # if the frame has a toolbar, the client area
+        # origin will not be (0,0).
+        pt = self._frame.GetClientAreaOrigin()
+        if pt.x != 0 or pt.y != 0:
+            cdc.SetDeviceOrigin(pt.x, pt.y)
+
+        self._art.DrawPaneButton(cdc,
+                  button_ui_part.button.button_id,
+                  state,
+                  button_ui_part.rect, hit_test.pane)
+
+
+    def OnLeftDown(self, event):
+
+        part = self.HitTest(event.GetX(), event.GetY())
+
+        if part:
+            if part.dock and part.dock.dock_direction == AUI_DOCK_CENTER:
+                return
+
+            if part.type == DockUIPart.typeDockSizer or \
+               part.type == DockUIPart.typePaneSizer:
+
+                # a dock may not be resized if it has a single
+                # pane which is not resizable
+                if part.type == DockUIPart.typeDockSizer and part.dock and \
+                   len(part.dock.panes) == 1 and part.dock.panes[0].IsFixed():
+                    return
+
+                # panes that may not be resized should be ignored here
+                if part.pane and part.pane.IsFixed():
+                    return
+
+                self._action = actionResize
+                self._action_part = part
+                self._action_hintrect = wx.Rect()
+                self._action_start = wx.Point(event.GetX(), event.GetY())
+                self._action_offset = wx.Point(event.GetX() - part.rect.x,
+                                               event.GetY() - part.rect.y)
+                self._frame.CaptureMouse()
+
+            elif part.type == DockUIPart.typePaneButton:
+
+                self._action = actionClickButton
+                self._action_part = part
+                self._action_start = wx.Point(event.GetX(), event.GetY())
+                self._frame.CaptureMouse()
+
+                self.UpdateButtonOnScreen(part, event)
+
+            elif part.type == DockUIPart.typeCaption or \
+                  part.type == DockUIPart.typeGripper:
+
+                if self.GetFlags() & AUI_MGR_ALLOW_ACTIVE_PANE:
+                    # set the caption as active
+                    self._panes = SetActivePane(self._panes, part.pane.window)
+                    self.Repaint()
+
+                self._action = actionClickCaption
+                self._action_part = part
+                self._action_start = wx.Point(event.GetX(), event.GetY())
+                self._action_offset = wx.Point(event.GetX() - part.rect.x,
+                                               event.GetY() - part.rect.y)
+                self._frame.CaptureMouse()
+
+        if wx.Platform != "__WXMAC__":
+            event.Skip()
+
+
+    def OnLeftUp(self, event):
+
+        if self._action == actionResize:
+
+            self._frame.ReleaseMouse()
+
+            # get rid of the hint rectangle
+            dc = wx.ScreenDC()
+            DrawResizeHint(dc, self._action_hintrect)
+
+            # resize the dock or the pane
+            if self._action_part and self._action_part.type == DockUIPart.typeDockSizer:
+                rect = self._action_part.dock.rect
+                new_pos = wx.Point(event.GetX() - self._action_offset.x,
+                                   event.GetY() - self._action_offset.y)
+
+                if self._action_part.dock.dock_direction == AUI_DOCK_LEFT:
+                    self._action_part.dock.size = new_pos.x - rect.x
+                elif self._action_part.dock.dock_direction == AUI_DOCK_TOP:
+                    self._action_part.dock.size = new_pos.y - rect.y
+                elif self._action_part.dock.dock_direction == AUI_DOCK_RIGHT:
+                    self._action_part.dock.size = rect.x + rect.width - \
+                                                  new_pos.x - \
+                                                  self._action_part.rect.GetWidth()
+                elif self._action_part.dock.dock_direction == AUI_DOCK_BOTTOM:
+                    self._action_part.dock.size = rect.y + rect.height - \
+                                                  new_pos.y - \
+                                                  self._action_part.rect.GetHeight()
+
+                self.Update()
+                self.Repaint(None)
+
+            elif self._action_part and \
+                 self._action_part.type == DockUIPart.typePaneSizer:
+
+                dock = self._action_part.dock
+                pane = self._action_part.pane
+
+                total_proportion = 0
+                dock_pixels = 0
+                new_pixsize = 0
+
+                caption_size = self._art.GetMetric(AUI_ART_CAPTION_SIZE)
+                pane_border_size = self._art.GetMetric(AUI_ART_PANE_BORDER_SIZE)
+                sash_size = self._art.GetMetric(AUI_ART_SASH_SIZE)
+
+                new_pos = wx.Point(event.GetX() - self._action_offset.x,
+                                   event.GetY() - self._action_offset.y)
+
+                # determine the pane rectangle by getting the pane part
+                pane_part = self.GetPanePart(pane.window)
+                if not pane_part:
+                    raise "\nERROR: Pane border part not found -- shouldn't happen"
+
+                # determine the new pixel size that the user wants
+                # this will help us recalculate the pane's proportion
+                if dock.IsHorizontal():
+                    new_pixsize = new_pos.x - pane_part.rect.x
+                else:
+                    new_pixsize = new_pos.y - pane_part.rect.y
+
+                # determine the size of the dock, based on orientation
+                if dock.IsHorizontal():
+                    dock_pixels = dock.rect.GetWidth()
+                else:
+                    dock_pixels = dock.rect.GetHeight()
+
+                # determine the total proportion of all resizable panes,
+                # and the total size of the dock minus the size of all
+                # the fixed panes
+                dock_pane_count = len(dock.panes)
+                pane_position = -1
+
+                for ii in xrange(dock_pane_count):
+                    p = dock.panes[ii]
+                    if p.window == pane.window:
+                        pane_position = ii
+
+                    # while we're at it, subtract the pane sash
+                    # width from the dock width, because this would
+                    # skew our proportion calculations
+                    if ii > 0:
+                        dock_pixels = dock_pixels - sash_size
+
+                    # also, the whole size (including decorations) of
+                    # all fixed panes must also be subtracted, because they
+                    # are not part of the proportion calculation
+                    if p.IsFixed():
+                        if dock.IsHorizontal():
+                            dock_pixels = dock_pixels - p.best_size.x
+                        else:
+                            dock_pixels = dock_pixels - p.best_size.y
+                    else:
+                        total_proportion = total_proportion + p.dock_proportion
+
+                # find a pane in our dock to 'steal' space from or to 'give'
+                # space to -- this is essentially what is done when a pane is
+                # resized the pane should usually be the first non-fixed pane
+                # to the right of the action pane
+                borrow_pane = -1
+
+                for ii in xrange(pane_position+1, dock_pane_count):
+                    p = dock.panes[ii]
+                    if not p.IsFixed():
+                        borrow_pane = ii
+                        break
+
+                # demand that the pane being resized is found in this dock
+                # (this assert really never should be raised)
+                if pane_position == -1:
+                    raise "\nERROR: Pane not found in dock"
+
+                # prevent division by zero
+                if dock_pixels == 0 or total_proportion == 0 or borrow_pane == -1:
+                    self._action = actionNone
+                    return
+
+                # calculate the new proportion of the pane
+                new_proportion = new_pixsize*total_proportion/dock_pixels
+
+                # default minimum size
+                min_size = 0
+
+                # check against the pane's minimum size, if specified. please note
+                # that this is not enough to ensure that the minimum size will
+                # not be violated, because the whole frame might later be shrunk,
+                # causing the size of the pane to violate it's minimum size
+                if pane.min_size.IsFullySpecified():
+                    min_size = 0
+                    if pane.HasBorder():
+                        min_size = min_size + pane_border_size*2
+
+                    # calculate minimum size with decorations (border,caption)
+                    if pane_part.orientation == wx.VERTICAL:
+                        min_size = min_size + pane.min_size.y
+                        if pane.HasCaption():
+                            min_size = min_size + caption_size
+                    else:
+                        min_size = min_size + pane.min_size.x
+
+                # for some reason, an arithmatic error somewhere is causing
+                # the proportion calculations to always be off by 1 pixel
+                # for now we will add the 1 pixel on, but we really should
+                # determine what's causing this.
+                min_size = min_size + 1
+
+                min_proportion = min_size*total_proportion/dock_pixels
+
+                if new_proportion < min_proportion:
+                    new_proportion = min_proportion
+
+                prop_diff = new_proportion - pane.dock_proportion
+
+                # borrow the space from our neighbor pane to the
+                # right or bottom (depending on orientation)
+                dock.panes[borrow_pane].dock_proportion -= prop_diff
+                pane.dock_proportion = new_proportion
+
+                indxd = self._docks.index(dock)
+                indxp = self._panes.index(pane)
+
+                self._docks[indxd] = dock
+                self._panes[indxp] = pane
+
+                # repaint
+                self.Update()
+                self.Repaint(None)
+
+        elif self._action == actionClickButton:
+
+            self._hover_button = None
+            self._frame.ReleaseMouse()
+            self.UpdateButtonOnScreen(self._action_part, event)
+
+            # make sure we're still over the item that was originally clicked
+            if self._action_part == self.HitTest(event.GetX(), event.GetY()):
+                # fire button-click event
+                e = AuiManagerEvent(wxEVT_AUI_PANEBUTTON)
+                e.SetPane(self._action_part.pane)
+                e.SetButton(self._action_part.button.button_id)
+                self.ProcessMgrEvent(e)
+
+        elif self._action == actionClickCaption:
+
+            self._frame.ReleaseMouse()
+
+        elif self._action == actionDragAuiFloatingPane:
+
+            self._frame.ReleaseMouse()
+
+        elif self._action == actionDragToolbarPane:
+
+            self._frame.ReleaseMouse()
+
+            pane = self.GetPane(self._action_window)
+            if not pane.IsOk():
+                raise "\nERROR: Pane Window Not Found"
+
+            # save the new positions
+            docks = FindDocks(self._docks, pane.dock_direction,
+                              pane.dock_layer, pane.dock_row)
+
+            if len(docks) == 1:
+                dock = docks[0]
+                pane_positions, pane_sizes = self.GetPanePositionsAndSizes(dock)
+
+                dock_pane_count = len(dock.panes)
+                for ii in xrange(dock_pane_count):
+                    dock.panes[ii].dock_pos = pane_positions[ii]
+
+            pane.state &= ~AuiPaneInfo.actionPane
+            indx = self._panes.index(pane)
+            self._panes[indx] = pane
+
+            self.Update()
+
+        else:
+
+            event.Skip()
+
+        self._action = actionNone
+        self._last_mouse_move = wx.Point() # see comment in OnMotion()
+
+
+    def OnMotion(self, event):
+
+        # sometimes when Update() is called from inside this method,
+        # a spurious mouse move event is generated this check will make
+        # sure that only real mouse moves will get anywhere in this method
+        # this appears to be a bug somewhere, and I don't know where the
+        # mouse move event is being generated.  only verified on MSW
+
+        mouse_pos = event.GetPosition()
+        if self._last_mouse_move == mouse_pos:
+            return
+
+        self._last_mouse_move = mouse_pos
+
+        if self._action == actionResize:
+            pos = self._action_part.rect.GetPosition()
+            if self._action_part.orientation == wx.HORIZONTAL:
+                pos.y = max(0, mouse_pos.y - self._action_offset.y)
+                pos.y = min(pos.y, self._frame.GetClientSize().y - self._action_part.rect.GetSize().y)
+            else:
+                pos.x = max(0, mouse_pos.x - self._action_offset.x)
+                pos.x = min(pos.x, self._frame.GetClientSize().x - self._action_part.rect.GetSize().x)
+
+            mypos = self._frame.ClientToScreen(pos)
+            mysize = self._action_part.rect.GetSize()
+            rect = wx.Rect(mypos[0], mypos[1], mysize[0], mysize[1])
+
+            # is it the same as the old rectangle?
+            if self._action_hintrect == rect:
+                # heck, yes, no need to draw again, it will only bring about flicker
+                event.Skip()
+                return
+
+            # otherwise draw the hint
+
+            dc = wx.ScreenDC()
+
+            if not self._action_hintrect.IsEmpty() and self._action_hintrect != rect:
+                DrawResizeHint(dc, self._action_hintrect)
+
+            DrawResizeHint(dc, rect)
+            self._action_hintrect = rect
+
+        elif self._action == actionClickCaption:
+
+            drag_x_threshold = wx.SystemSettings_GetMetric(wx.SYS_DRAG_X)
+            drag_y_threshold = wx.SystemSettings_GetMetric(wx.SYS_DRAG_Y)
+
+            # caption has been clicked.  we need to check if the mouse
+            # is now being dragged. if it is, we need to change the
+            # mouse action to 'drag'
+            if abs(mouse_pos.x - self._action_start.x) > drag_x_threshold or \
+               abs(mouse_pos.y - self._action_start.y) > drag_y_threshold:
+
+                pane_info = self._action_part.pane
+                indx = self._panes.index(pane_info)
+
+                if not pane_info.IsToolbar():
+
+                    if self._flags & AUI_MGR_ALLOW_FLOATING and \
+                       pane_info.IsFloatable():
+
+                        self._action = actionDragAuiFloatingPane
+
+                        # set initial float position
+                        pt = self._frame.ClientToScreen(event.GetPosition())
+                        pane_info.floating_pos = wx.Point(pt.x - self._action_offset.x,
+                                                          pt.y - self._action_offset.y)
+                        # float the window
+                        pane_info.Float()
+                        self._panes[indx] = pane_info
+
+                        self.Update()
+
+                        self._action_window = pane_info.frame
+
+                        # action offset is used here to make it feel "natural" to the user
+                        # to drag a docked pane and suddenly have it become a floating frame.
+                        # Sometimes, however, the offset where the user clicked on the docked
+                        # caption is bigger than the width of the floating frame itself, so
+                        # in that case we need to set the action offset to a sensible value
+                        frame_size = self._action_window.GetSize()
+                        if frame_size.x <= self._action_offset.x:
+                            self._action_offset.x = 30
+
+                else:
+
+                    self._action = actionDragToolbarPane
+                    self._action_window = pane_info.window
+
+        elif self._action == actionDragAuiFloatingPane:
+
+            pt = self._frame.ClientToScreen(event.GetPosition())
+            if self._action_window:
+                self._action_window.Move((pt.x - self._action_offset.x,
+                                         pt.y - self._action_offset.y))
+
+        elif self._action == actionDragToolbarPane:
+
+            pane = self.GetPane(self._action_window)
+            if not pane.IsOk():
+                raise "\nERROR: Pane Window Not Found"
+
+            indx = self._panes.index(pane)
+            pane.state |= AuiPaneInfo.actionPane
+
+            pt = event.GetPosition()
+            ret, pane = self.DoDrop(self._docks, self._panes, pane, pt, self._action_offset)
+
+            if not ret:
+                return
+
+            # if DoDrop() decided to float the pane, set up
+            # the floating pane's initial position
+            if pane.IsFloating():
+
+                pt = self._frame.ClientToScreen(event.GetPosition())
+                pane.floating_pos = wx.Point(pt.x - self._action_offset.x,
+                                             pt.y - self._action_offset.y)
+
+            self._panes[indx] = pane
+
+            # this will do the actiual move operation
+            # in the case that the pane has been floated,
+            # this call will create the floating pane
+            # and do the reparenting
+            self.Update()
+
+            # if the pane has been floated, change the mouse
+            # action actionDragAuiFloatingPane so that subsequent
+            # EVT_MOTION() events will move the floating pane
+            if pane.IsFloating():
+
+                pane.state &= ~AuiPaneInfo.actionPane
+                self._action = actionDragAuiFloatingPane
+                self._action_window = pane.frame
+
+            self._panes[indx] = pane
+
+        else:
+
+            part = self.HitTest(event.GetX(), event.GetY())
+            if part and part.type == DockUIPart.typePaneButton:
+                if part != self._hover_button:
+                    # make the old button normal
+                    if self._hover_button:
+                        self.UpdateButtonOnScreen(self._hover_button, event)
+
+                    # mouse is over a button, so repaint the
+                    # button in hover mode
+                    self.UpdateButtonOnScreen(part, event)
+                    self._hover_button = part
+            else:
+                if self._hover_button:
+
+                    self._hover_button = None
+                    self.Repaint()
+
+                else:
+
+                    event.Skip()
+
+
+    def OnLeaveWindow(self, event):
+
+        if self._hover_button:
+            self._hover_button = None
+            self.Repaint()
+
+
+    def OnChildFocus(self, event):
+
+        # when a child pane has it's focus set, we should change the
+        # pane's active state to reflect this. (this is only true if
+        # active panes are allowed by the owner)
+        if self.GetFlags() & AUI_MGR_ALLOW_ACTIVE_PANE:
+            if self.GetPane(event.GetWindow()).IsOk():
+                self._panes = SetActivePane(self._panes, event.GetWindow())
+                self._frame.Refresh()
+
+        event.Skip()
+
+
+    def OnPaneButton(self, event):
+        """
+        OnPaneButton() is an event handler that is called
+        when a pane button has been pressed.
+        """
+
+        pane = event.pane
+        indx = self._panes.index(pane)
+
+        if event.button == AuiPaneInfo.buttonClose:
+            e = AuiManagerEvent(wxEVT_AUI_PANECLOSE)
+            e.SetPane(pane)
+            self.ProcessMgrEvent(e)
+
+            if e.GetSkipped():
+                pane.Hide()
+                self._panes[indx] = pane
+                self.Update()
+
+        elif event.button == AuiPaneInfo.buttonPin:
+
+            if self._flags & AUI_MGR_ALLOW_FLOATING and pane.IsFloatable():
+                pane.Float()
+
+            self._panes[indx] = pane
+            self.Update()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2 @@
+__all__ = ['inittool','inputValidatior', 'orpg_settings' , 'orpg_update', 'predTextCtrl',
+    'rgbhex', 'scriptkit', 'server_probe', 'toolBars' ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/aliaslib.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,956 @@
+# 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: aliaslib.py
+# Author: Dj Gilcrease
+# Maintainer:
+# Version:
+#   $Id: aliaslib.py,v 1.20 2007/08/09 05:23:21 digitalxero Exp $
+#
+# Description: nodehandler for alias.
+#
+
+__version__ = "$Id: aliaslib.py,v 1.20 2007/08/09 05:23:21 digitalxero Exp $"
+
+from orpg.orpg_wx import *
+from orpg.orpgCore import *
+from orpg.orpg_windows import createMaskedButton, orpgMultiCheckBoxDlg
+from orpg.tools.rgbhex import RGBHex
+import orpg.tools.orpg_log
+import orpg.dirpath
+import orpg.orpg_xml
+import orpg.tools.validate
+import orpg.tools.orpg_settings
+
+class AliasLib(wx.Frame):
+    def __init__(self):
+        self.orpgframe = open_rpg.get_component('frame')
+        self.log = open_rpg.get_component('log')
+        self.log.log("Enter AliasLib", ORPG_DEBUG)
+        wx.Frame.__init__(self, None, wx.ID_ANY, title="Alias Lib")
+        self.orpgframe.Freeze()
+        self.Freeze()
+        self.SetOwnBackgroundColour('#EFEFEF')
+        self.dir_struct = open_rpg.get_component('dir_struct')
+        self.settings = open_rpg.get_component('settings')
+        self.xml = open_rpg.get_component('xml')
+        self.validate = open_rpg.get_component('validate')
+        self.filename = self.settings.get_setting('aliasfile') + '.alias'
+        self.validate.config_file(self.filename, "default_alias.alias")
+        self.buildMenu()
+        self.buildButtons()
+        self.buildGUI()
+        wx.CallAfter(self.InitSetup)
+        wx.CallAfter(self.loadFile)
+        self.Thaw()
+        self.orpgframe.Thaw()
+        self.Bind(wx.EVT_CLOSE, self.OnMB_FileExit)
+        self.log.log("Exit AliasLib", ORPG_DEBUG)
+
+    def InitSetup(self):
+        self.chat = open_rpg.get_component('chat')
+        self.gametree = open_rpg.get_component('tree')
+        self.map = open_rpg.get_component('map')
+        self.session = open_rpg.get_component('session')
+
+    def buildMenu(self):
+        self.log.log("Enter AliasLib->buildMenu(self)", ORPG_DEBUG)
+        filemenu = wx.Menu()
+        item = wx.MenuItem(filemenu, wx.ID_ANY, "&New\tCtrl+N", "New ALias Lib")
+        self.Bind(wx.EVT_MENU, self.OnMB_FileNew, item)
+        filemenu.AppendItem(item)
+        item = wx.MenuItem(filemenu, wx.ID_ANY, "&Open\tCtrl+O", "Open Alias Lib")
+        self.Bind(wx.EVT_MENU, self.OnMB_FileOpen, item)
+        filemenu.AppendItem(item)
+        item = wx.MenuItem(filemenu, wx.ID_ANY, "&Save\tCtrl+S", "Save Alias Lib")
+        self.Bind(wx.EVT_MENU, self.OnMB_FileSave, item)
+        filemenu.AppendItem(item)
+        item = wx.MenuItem(filemenu, wx.ID_ANY, "&Export to Tree", "Export to Tree")
+        self.Bind(wx.EVT_MENU, self.OnMB_FileExportToTree, item)
+        filemenu.AppendItem(item)
+        item = wx.MenuItem(filemenu, wx.ID_ANY, "&Exit\tCtrl+X", "Exit")
+        self.Bind(wx.EVT_MENU, self.OnMB_FileExit, item)
+        filemenu.AppendItem(item)
+        aliasmenu = wx.Menu()
+        item = wx.MenuItem(aliasmenu, wx.ID_ANY, "New", "New")
+        self.Bind(wx.EVT_MENU, self.OnMB_AliasNew, item)
+        aliasmenu.AppendItem(item)
+        item = wx.MenuItem(aliasmenu, wx.ID_ANY, "Add Temporary", "Add Temporary")
+        self.Bind(wx.EVT_MENU, self.OnMB_AliasAddTemporary, item)
+        aliasmenu.AppendItem(item)
+        item = wx.MenuItem(aliasmenu, wx.ID_ANY, "Edit", "Edit")
+        self.Bind(wx.EVT_MENU, self.OnMB_AliasEdit, item)
+        aliasmenu.AppendItem(item)
+        item = wx.MenuItem(aliasmenu, wx.ID_ANY, "Delete", "Delete")
+        self.Bind(wx.EVT_MENU, self.OnMB_AliasDelete, item)
+        aliasmenu.AppendItem(item)
+        filtermenu = wx.Menu()
+        item = wx.MenuItem(filtermenu, wx.ID_ANY, "New", "New")
+        self.Bind(wx.EVT_MENU, self.OnMB_FilterNew, item)
+        filtermenu.AppendItem(item)
+        item = wx.MenuItem(filtermenu, wx.ID_ANY, "Edit", "Edit")
+        self.Bind(wx.EVT_MENU, self.OnMB_FilterEdit, item)
+        filtermenu.AppendItem(item)
+        item = wx.MenuItem(filtermenu, wx.ID_ANY, "Delete", "Delete")
+        self.Bind(wx.EVT_MENU, self.OnMB_FilterDelete, item)
+        filtermenu.AppendItem(item)
+        transmitmenu = wx.Menu()
+        item = wx.MenuItem(transmitmenu, wx.ID_ANY, "Send\tCtrl+Enter", "Send")
+        self.Bind(wx.EVT_MENU, self.OnMB_TransmitSend, item)
+        transmitmenu.AppendItem(item)
+        item = wx.MenuItem(transmitmenu, wx.ID_ANY, "Emote\tCtrl+E", "Emote")
+        self.Bind(wx.EVT_MENU, self.OnMB_TransmitEmote, item)
+        transmitmenu.AppendItem(item)
+        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")
+        menu.Append(filtermenu, "&Filter")
+        menu.Append(transmitmenu, "&Transmit")
+        self.SetMenuBar(menu)
+        self.log.log("Exit AliasLib->buildMenu(self)", ORPG_DEBUG)
+
+    def OnMB_FileNew(self, event):
+        self.log.log("Enter AliasLib->OnMB_FileNew(self, event)", ORPG_DEBUG)
+        oldfilename = self.filename
+        dlg = wx.TextEntryDialog(self, "Please Name This Alias Lib", "New Alias Lib")
+        if dlg.ShowModal() == wx.ID_OK:
+            self.filename = dlg.GetValue() + '.alias'
+        dlg.Destroy()
+        if oldfilename != self.filename:
+            self.OnMB_FileSave(None, oldfilename)
+            self.aliasList = []
+            self.filterList = []
+            self.OnMB_FileSave(None)
+        self.settings.set_setting('aliasfile', self.filename[:-6])
+        self.log.log("Exit AliasLib->OnMB_FileNew(self, event)", ORPG_DEBUG)
+
+    def OnMB_FileOpen(self, event):
+        self.log.log("Enter AliasLib->OnMB_FileOpen(self, event)", ORPG_DEBUG)
+        oldfilename = self.filename
+        dlg = wx.FileDialog(self, "Select an Alias Lib to Open", self.dir_struct["user"], wildcard="*.alias", style=wx.HIDE_READONLY|wx.OPEN)
+        if dlg.ShowModal() == wx.ID_OK:
+            self.filename = dlg.GetFilename()
+        dlg.Destroy()
+        if oldfilename != self.filename:
+            self.OnMB_FileSave(None, oldfilename)
+            self.loadFile()
+        self.settings.set_setting('aliasfile', self.filename[:-6])
+        self.log.log("Exit AliasLib->OnMB_FileOpen(self, event)", ORPG_DEBUG)
+
+    def OnMB_FileSave(self, event, file=None):
+        self.log.log("Enter AliasLib->OnMB_FileSave(self, event)", ORPG_DEBUG)
+        idx = self.aliasIdx
+        if file == None:
+            file = self.filename
+        xml = "<aliaslib>\n"
+        for n in xrange(self.selectAliasWnd.GetItemCount()):
+            self.alias = n
+            xml += "\t<alias "
+            xml += 'name="' + self.MakeHTMLSafe(self.alias[0]) + '" '
+            xml += 'color="' + self.alias[1] + '" '
+            xml += "/>\n"
+        for n in xrange(1, len(self.filterList)):
+            xml += "\t<filter "
+            xml += 'name="' + self.filterList[n] + '">' + "\n"
+            for rule in self.regExList[n-1]:
+                xml += "\t\t<rule "
+                xml += 'match="' + self.MakeHTMLSafe(rule[0]) + '" '
+                xml += 'sub="' + self.MakeHTMLSafe(rule[1]) + '" '
+                xml += "/>\n"
+            xml += "\t</filter>\n"
+        xml += "</aliaslib>"
+        self.alias = idx
+        f = open(self.dir_struct["user"] + file, "w")
+        f.write(xml)
+        f.close()
+        self.log.log("Exit AliasLib->OnMB_FileSave(self, event)", ORPG_DEBUG)
+
+    def OnMB_FileExportToTree(self, event):
+        self.log.log("Enter AliasLib->OnMB_FileExportToTree(self, event)", ORPG_DEBUG)
+        #tree = open_rpg.get_component("tree")
+        xml = '<nodehandler class="voxchat_handler" icon="player" module="voxchat" name="' + self.filename[:-6] + '" use.filter="0" version="1.0">' + "\n"
+        idx = self.aliasIdx
+        for n in xrange(self.selectAliasWnd.GetItemCount()):
+            self.alias = n
+            xml += "\t<voxchat.alias "
+            xml += 'name="' + self.MakeHTMLSafe(self.alias[0]) + '" '
+            xml += 'color="' + self.alias[1] + '" '
+            xml += "/>\n"
+        self.alias = idx
+        for n in xrange(1, len(self.filterList)):
+            xml += "\t<voxchat.filter "
+            xml += 'name="' + self.filterList[n] + '">' + "\n"
+            for rule in self.regExList[n-1]:
+                xml += "\t\t<rule "
+                xml += 'match="' + self.MakeHTMLSafe(rule[0]) + '" '
+                xml += 'sub="' + self.MakeHTMLSafe(rule[1]) + '" '
+                xml += "/>\n"
+            xml += "\t</voxchat.filter>\n"
+        xml += "</nodehandler>"
+        self.gametree.insert_xml(xml)
+        self.log.log("Exit AliasLib->OnMB_FileExportToTree(self, event)", ORPG_DEBUG)
+
+    def OnMB_FileExit(self, event):
+        self.log.log("Enter AliasLib->OnMB_FileExit(self, event)", ORPG_DEBUG)
+        self.OnMB_FileSave(0)
+        self.Hide()
+        top_frame = open_rpg.get_component('frame')
+        top_frame.mainmenu.Check(top_frame.mainmenu.FindMenuItem("Windows", "Alias Lib"), False)
+        self.log.log("Exit AliasLib->OnMB_FileExit(self, event)", ORPG_DEBUG)
+
+    def OnMB_AliasNew(self, event):
+        self.log.log("Enter AliasLib->OnMB_AliasNew(self, event)", ORPG_DEBUG)
+        self.NewEditAliasDialog("New")
+        self.log.log("Exit AliasLib->OnMB_AliasNew(self, event)", ORPG_DEBUG)
+
+    def OnMB_AliasAddTemporary(self, event):
+        self.log.log("Enter AliasLib->OnMB_AliasAddTemporary(self, event)", ORPG_DEBUG)
+        minis = self.map.canvas.layers['miniatures'].miniatures
+        for min in minis:
+            name = min.label
+            if name not in self.aliasList:
+                i = self.selectAliasWnd.InsertStringItem(self.selectAliasWnd.GetItemCount(), name)
+                self.selectAliasWnd.SetStringItem(i, 1, "Default")
+                self.selectAliasWnd.RefreshItem(i)
+        self.RefreshAliases()
+        self.log.log("Exit AliasLib->OnMB_AliasAddTemporary(self, event)", ORPG_DEBUG)
+
+    def OnMB_AliasEdit(self, event):
+        self.log.log("Enter AliasLib->OnMB_AliasEdit(self, event)", ORPG_DEBUG)
+        if self.aliasIdx != -1:
+            self.NewEditAliasDialog("Edit")
+        self.log.log("Exit AliasLib->OnMB_AliasEdit(self, event)", ORPG_DEBUG)
+
+    def NewEditAliasDialog(self, type):
+        self.log.log("Enter AliasLib->NewEditAliasDialog(self, type)", ORPG_DEBUG)
+        dlg = wx.Dialog(self, wx.ID_ANY, type + " Alias", style=wx.DEFAULT_DIALOG_STYLE|wx.STAY_ON_TOP)
+        txt = wx.TextCtrl(dlg, wx.ID_ANY)
+        if type == 'Edit':
+            txt.SetValue(self.alias[0])
+        self.colorbtn = wx.Button(dlg, wx.ID_ANY, "Default Color")
+        dlg.Bind(wx.EVT_BUTTON, self.ChangeAliasColor, self.colorbtn)
+        if self.alias[1] != 'Default':
+            self.colorbtn.SetLabel("Chat Color")
+            self.colorbtn.SetForegroundColour(self.alias[1])
+        okbtn = wx.Button(dlg, wx.ID_OK)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.StaticText(dlg, wx.ID_ANY, "Alias: "), 0, wx.EXPAND)
+        sizer.Add(txt, 1, wx.EXPAND)
+        sizer.Add(self.colorbtn, 0, wx.EXPAND)
+        sizer.Add(okbtn, 0, wx.EXPAND)
+        dlg.SetSizer(sizer)
+        dlg.SetAutoLayout(True)
+        dlg.Fit()
+        if dlg.ShowModal() == wx.ID_OK:
+            (r, g, b) = self.colorbtn.GetForegroundColour().Get()
+            if type == 'Edit':
+                self.selectAliasWnd.SetStringItem(self.aliasIdx, 0, txt.GetValue())
+                if self.colorbtn.GetLabel() != 'Default Color':
+                    self.aliasColor = RGBHex().hexstring(r, g, b)
+                self.selectAliasWnd.RefreshItem(self.aliasIdx)
+            else:
+                i = self.selectAliasWnd.InsertStringItem(self.selectAliasWnd.GetItemCount(), txt.GetValue())
+                if self.colorbtn.GetLabel() == 'Default Color':
+                    self.selectAliasWnd.SetStringItem(i, 1, "Default")
+                else:
+                    self.selectAliasWnd.SetStringItem(i, 1, RGBHex().hexstring(r, g, b))
+                    self.selectAliasWnd.SetItemTextColour(i, RGBHex().hexstring(r, g, b))
+                self.selectAliasWnd.RefreshItem(i)
+            self.RefreshAliases()
+        self.log.log("Exit AliasLib->NewEditAliasDialog(self, type)", ORPG_DEBUG)
+
+    def ChangeAliasColor(self, event):
+        self.log.log("Enter AliasLib->ChangeAliasColor(self, event)", ORPG_DEBUG)
+        color = RGBHex().do_hex_color_dlg(self)
+        self.colorbtn.SetLabel("Chat Color")
+        self.colorbtn.SetForegroundColour(color)
+        self.log.log("Exit AliasLib->ChangeAliasColor(self, event)", ORPG_DEBUG)
+
+    def OnMB_AliasDelete(self, event):
+        self.log.log("Enter AliasLib->OnMB_AliasDelete(self, event)", ORPG_DEBUG)
+        if self.aliasIdx != -1:
+            self.selectAliasWnd.DeleteItem(self.aliasIdx)
+        self.RefreshAliases()
+        self.log.log("Exit AliasLib->OnMB_AliasDelete(self, event)", ORPG_DEBUG)
+
+    def OnMB_FilterNew(self, event):
+        self.log.log("Enter AliasLib->OnMB_FilterNew(self, event)", ORPG_DEBUG)
+        dlg = wx.TextEntryDialog(self, 'Filter Name: ', 'Please name this filter')
+        if dlg.ShowModal() != wx.ID_OK:
+            dlg.Destroy()
+            return
+        filterName = dlg.GetValue()
+        i = self.selectFilterWnd.InsertStringItem(self.selectFilterWnd.GetItemCount(), filterName)
+        self.filter = i
+        self.regExList.append([])
+        self.OnMB_FilterEdit(None)
+        self.log.log("Exit AliasLib->OnMB_FilterNew(self, event)", ORPG_DEBUG)
+
+    def OnMB_FilterEdit(self, event):
+        self.log.log("Enter AliasLib->OnMB_FilterEdit(self, event)", ORPG_DEBUG)
+        wnd = FilterEditWnd(self, self.filter, self.filterRegEx)
+        wnd.MakeModal(True)
+        wnd.Show()
+        self.log.log("Exit AliasLib->OnMB_FilterEdit(self, event)", ORPG_DEBUG)
+
+    def OnMB_FilterDelete(self, event):
+        self.log.log("Enter AliasLib->OnMB_FilterDelete(self, event)", ORPG_DEBUG)
+        if self.filterIdx != -1:
+            self.selectFilterWnd.DeleteItem(self.filterIdx)
+        self.log.log("Exit AliasLib->OnMB_FilterDelete(self, event)", ORPG_DEBUG)
+
+    def OnMB_TransmitSend(self, event):
+        self.log.log("Enter AliasLib->OnMB_TransmitSend(self, event)", ORPG_DEBUG)
+        self.orpgframe.Freeze()
+        if self.alias[1] != 'Default':
+            defaultcolor = self.settings.get_setting("mytextcolor")
+            self.settings.set_setting("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)
+        if self.alias[1] != 'Default':
+            self.settings.set_setting("mytextcolor", defaultcolor)
+            self.chat.set_colors()
+        if self.checkClearText.IsChecked():
+            self.textWnd.SetValue("")
+        top_frame.Thaw()
+        self.log.log("Exit AliasLib->OnMB_TransmitSend(self, event)", ORPG_DEBUG)
+
+    def OnMB_TransmitEmote(self, event):
+        self.log.log("Enter AliasLib->OnMB_TransmitEmote(self, event)", ORPG_DEBUG)
+        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)
+        if self.checkClearText.IsChecked():
+            self.textWnd.SetValue("")
+        top_frame.Thaw()
+        self.log.log("Exit AliasLib->OnMB_TransmitEmote(self, event)", ORPG_DEBUG)
+
+    def OnMB_TransmitWhisper(self, event):
+        self.log.log("Enter AliasLib->OnMB_TransmitWhisper(self, event)", ORPG_DEBUG)
+        self.orpgframe.Freeze()
+        players = self.session.get_players()
+        if self.alias[1] != 'Default':
+            defaultcolor = self.settings.get_setting("mytextcolor")
+            self.settings.set_setting("mytextcolor", self.alias[1])
+            self.chat.set_colors()
+        opts = []
+        myid = session.get_id()
+        for p in players:
+            if p[2] != myid:
+                opts.append("(" + p[2] + ") " + self.chat.html_strip(p[0]))
+        dlg = orpgMultiCheckBoxDlg(self, opts, "Select Players:", "Whisper To", [])
+        sendto = []
+        if dlg.ShowModal() == wx.ID_OK:
+            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)
+        if len(sendto):
+            self.chat.whisper_to_players(line, sendto)
+        if self.alias[1] != 'Default':
+            self.settings.set_setting("mytextcolor", defaultcolor)
+            self.chat.set_colors()
+        if self.checkClearText.IsChecked():
+            self.textWnd.SetValue("")
+        top_frame.Thaw()
+        self.log.log("Exit AliasLib->OnMB_TransmitWhisper(self, event)", ORPG_DEBUG)
+
+    def OnMB_TransmitMacro(self, event):
+        self.log.log("Enter AliasLib->OnMB_TransmitMacro(self, event)", ORPG_DEBUG)
+        self.orpgframe.Freeze()
+        if self.alias[1] != 'Default':
+            defaultcolor = self.settings.get_setting("mytextcolor")
+            self.settings.set_setting("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)
+        if self.alias[1] != 'Default':
+            self.settings.set_setting("mytextcolor", defaultcolor)
+            self.chat.set_colors()
+        if self.checkClearText.IsChecked():
+            self.textWnd.SetValue("")
+        top_frame.Thaw()
+        self.log.log("Exit AliasLib->OnMB_TransmitMacro(self, event)", ORPG_DEBUG)
+
+    def buildButtons(self):
+        self.log.log("Enter AliasLib->buildButtons(self)", ORPG_DEBUG)
+        self.topBtnSizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.middleBtnSizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.bottomBtnSizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.addFromMapBtn = createMaskedButton(self, self.dir_struct["icon"] + 'install.gif', 'Add temporary aliases from map', wx.ID_ANY, "#C0C0C0")
+        self.newAliasBtn = createMaskedButton(self, self.dir_struct["icon"] + 'player.gif', 'Add a new Alias', wx.ID_ANY)
+        self.delAliasBtn = createMaskedButton(self, self.dir_struct["icon"] + 'noplayer.gif', 'Delete selected Alias', wx.ID_ANY)
+        self.editAliasBtn = createMaskedButton(self, self.dir_struct["icon"] + 'note.gif', 'Edit selected Alias', wx.ID_ANY)
+        self.Bind(wx.EVT_BUTTON, self.OnMB_AliasNew, self.newAliasBtn)
+        self.Bind(wx.EVT_BUTTON, self.OnMB_AliasAddTemporary, self.addFromMapBtn)
+        self.Bind(wx.EVT_BUTTON, self.OnMB_AliasEdit, self.editAliasBtn)
+        self.Bind(wx.EVT_BUTTON, self.OnMB_AliasDelete, self.delAliasBtn)
+        self.newFilterBtn = createMaskedButton(self, self.dir_struct["icon"] + 'add_filter.gif', 'Add a new Filter', wx.ID_ANY, "#0000FF")
+        self.editFilterBtn = createMaskedButton(self, self.dir_struct["icon"] + 'edit_filter.gif', 'Edit selected Filter', wx.ID_ANY, "#FF0000")
+        self.delFilterBtn = createMaskedButton(self, self.dir_struct["icon"] + 'delete_filter.gif', 'Delete selected Filter', wx.ID_ANY, "#0000FF")
+        self.Bind(wx.EVT_BUTTON, self.OnMB_FilterNew, self.newFilterBtn)
+        self.Bind(wx.EVT_BUTTON, self.OnMB_FilterEdit, self.editFilterBtn)
+        self.Bind(wx.EVT_BUTTON, self.OnMB_FilterDelete, self.delFilterBtn)
+        self.textBoldBtn = createMaskedButton(self, self.dir_struct["icon"] + 'bold.gif', 'Bold', wx.ID_ANY, "#BDBDBD")
+        self.textItalicBtn = createMaskedButton(self, self.dir_struct["icon"] + 'italic.gif', 'Italic', wx.ID_ANY, "#BDBDBD")
+        self.textUnderlineBtn = createMaskedButton(self, self.dir_struct["icon"] + 'underlined.gif', 'Underline', wx.ID_ANY, "#BDBDBD")
+        self.textColorBtn = wx.Button(self, wx.ID_ANY, "Color")
+        self.textColorBtn.SetForegroundColour(wx.BLACK)
+        self.exportBtn = createMaskedButton(self, self.dir_struct["icon"] + 'grid.gif', 'Export to Tree', wx.ID_ANY)
+        self.Bind(wx.EVT_BUTTON, self.FormatText, self.textBoldBtn)
+        self.Bind(wx.EVT_BUTTON, self.FormatText, self.textItalicBtn)
+        self.Bind(wx.EVT_BUTTON, self.FormatText, self.textUnderlineBtn)
+        self.Bind(wx.EVT_BUTTON, self.FormatText, self.textColorBtn)
+        self.Bind(wx.EVT_BUTTON, self.OnMB_FileExportToTree, self.exportBtn)
+        self.topBtnSizer.Add(self.newAliasBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.addFromMapBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.editAliasBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.delAliasBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.newFilterBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.editFilterBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.delFilterBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.textBoldBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.textItalicBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.textUnderlineBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.textColorBtn, 0, wx.EXPAND)
+        self.topBtnSizer.Add(self.exportBtn, 0, wx.EXPAND|wx.ALIGN_RIGHT)
+        self.checkFilterText = wx.CheckBox(self, wx.ID_ANY, "Filter Text")
+        self.Bind(wx.EVT_CHECKBOX, self.FilterTextChecked, self.checkFilterText)
+        self.checkClearText = wx.CheckBox(self, wx.ID_ANY, "Auto Clear Text")
+        self.middleBtnSizer.Add(self.checkFilterText, 0, wx.EXPAND)
+        self.middleBtnSizer.Add(self.checkClearText, 0, wx.EXPAND)
+        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)
+        self.log.log("Exit AliasLib->buildButtons(self)", ORPG_DEBUG)
+
+    def buildGUI(self):
+        self.log.log("Enter AliasLib->buildGUI(self)", ORPG_DEBUG)
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        rightwnd = wx.SplitterWindow(self, wx.ID_ANY, style=wx.SP_LIVE_UPDATE|wx.SP_NO_XP_THEME|wx.SP_3DSASH)
+        leftwnd = wx.SplitterWindow(rightwnd, wx.ID_ANY, style=wx.SP_LIVE_UPDATE|wx.SP_NO_XP_THEME|wx.SP_3DSASH)
+        self.selectAliasWnd = wx.ListCtrl(leftwnd, wx.ID_ANY, style=wx.LC_SINGLE_SEL|wx.LC_REPORT|wx.LC_HRULES)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.NewAliasSelection, self.selectAliasWnd)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.NoAliasSelection, self.selectAliasWnd)
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnMB_AliasEdit, self.selectAliasWnd)
+        self.selectFilterWnd = wx.ListCtrl(leftwnd, wx.ID_ANY, style=wx.LC_SINGLE_SEL|wx.LC_REPORT|wx.LC_HRULES)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.NewFilterSelection, self.selectFilterWnd)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.NoFilterSelection, self.selectFilterWnd)
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnMB_FilterEdit, self.selectFilterWnd)
+        self.textWnd = wx.TextCtrl(rightwnd, wx.ID_ANY, style=wx.TE_MULTILINE|wx.TE_BESTWRAP)
+        leftwnd.SplitHorizontally(self.selectAliasWnd, self.selectFilterWnd)
+        rightwnd.SplitVertically(leftwnd, self.textWnd)
+        self.sizer.Add(self.topBtnSizer, 0, wx.EXPAND)
+        self.sizer.Add(rightwnd, 1, wx.EXPAND)
+        self.sizer.Add(self.middleBtnSizer, 0, wx.EXPAND)
+        self.sizer.Add(self.bottomBtnSizer, 0, wx.EXPAND)
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        self.log.log("Exit AliasLib->buildGUI(self)", ORPG_DEBUG)
+
+    def loadFile(self):
+        self.log.log("Enter AliasLib->loadFile(self)", ORPG_DEBUG)
+        f = open(self.dir_struct["user"] + self.filename, "r")
+        data = f.read()
+        f.close()
+        self.alias = -1
+        self.filter = -1
+        xml_dom = self.xml.parseXml(data)
+        del data
+        aliases = xml_dom.getElementsByTagName("alias")
+        alist = []
+        for alias in aliases:
+            if alias.hasAttribute("color"):
+                color = alias.getAttribute("color")
+            else:
+                color = 'Default'
+            aname = self.MakeSafeHTML(alias.getAttribute("name"))
+            alist.append([aname, color])
+        alist.sort()
+        self.aliasList = alist
+        filters = xml_dom.getElementsByTagName("filter")
+        flist = []
+        self.regExList = []
+        for filter in filters:
+            flist.append(filter.getAttribute("name"))
+            rules = filter.getElementsByTagName("rule")
+            sub = []
+            for rule in rules:
+                sub.append([self.MakeSafeHTML(rule.getAttribute("match")), self.MakeSafeHTML(rule.getAttribute("sub"))])
+            self.regExList.append(sub)
+        self.filterList = flist
+        xml_dom.unlink()
+        self.alias = -1
+        self.filter = -1
+        self.log.log("Exit AliasLib->loadFile(self)", ORPG_DEBUG)
+
+    def MakeSafeHTML(self, str):
+        return str.replace("&amp;", "&").replace("&lt;", "<").replace("&quot;", '"').replace("&gt;", ">").replace("&#39;", "'")
+    def MakeHTMLSafe(self, str):
+        return str.replace("&", "&amp;").replace("<", "&lt;").replace('"', "&quot;").replace(">", "&gt;").replace("'", "&#39;")
+    def ImportFromTree(self, xml_dom):
+        self.log.log("Enter AliasLib->ImportFromTree(self, xml_dom)", ORPG_DEBUG)
+        oldfilename = self.filename
+        if xml_dom.getAttribute('name') == 'Alias Library':
+            dlg = wx.TextEntryDialog(self, "Please Name This Alias Lib", "New Alias Lib")
+            if dlg.ShowModal() == wx.ID_OK:
+                self.filename = dlg.GetValue() + '.alias'
+                dlg.Destroy()
+            else:
+                dlg.Destroy()
+                return
+        else:
+            self.filename = xml_dom.getAttribute('name') + '.alias'
+        self.settings.set_setting('aliasfile', self.filename[:-6])
+        if oldfilename != self.filename:
+            self.OnMB_FileSave(None, oldfilename)
+        f = open(self.dir_struct["user"] + self.filename, "w")
+        f.write(xml_dom.toxml().replace('nodehandler', 'aliaslib').replace('voxchat.', ''))
+        f.close()
+        wx.CallAfter(self.loadFile)
+        self.log.log("Exit AliasLib->ImportFromTree(self, xml_dom)", ORPG_DEBUG)
+
+    def NewAliasSelection(self, event):
+        self.log.log("Enter AliasLib->NewAliasSelection(self, event)", ORPG_DEBUG)
+        self.alias = event.GetIndex()
+        wx.CallAfter(self.chat.aliasList.SetStringSelection, self.alias[0])
+        self.log.log("Exit AliasLib->NewAliasSelection(self, event)", ORPG_DEBUG)
+
+    def NoAliasSelection(self, event):
+        self.log.log("Enter AliasLib->NoAliasSelection(self, event)", ORPG_DEBUG)
+        self.aliasIdx = -1
+        wx.CallAfter(self.chat.aliasList.SetStringSelection, self.alias[0])
+        self.log.log("Exit AliasLib->NoAliasSelection(self, event)", ORPG_DEBUG)
+
+    def GetSelectedAlias(self):
+        self.log.log("Enter AliasLib->GetSelectedAlias(self)", ORPG_DEBUG)
+        self.InitSetup()
+        if self.aliasIdx != -1:
+            self.log.log("Exit AliasLib->GetSelectedAlias(self) return " + str(self.aliasIdx), ORPG_DEBUG)
+            return [self.selectAliasWnd.GetItem(self.aliasIdx, 0).GetText(), self.selectAliasWnd.GetItem(self.aliasIdx, 1).GetText()]
+        self.log.log("Exit AliasLib->GetSelectedAlias(self) return " + str(self.aliasIdx), ORPG_DEBUG)
+        return [self.chat.defaultAliasName, "Default"]
+
+    def SetSelectedAlias(self, alias):
+        self.log.log("Enter AliasLib->SetSelectedAlias(self, aliasIdx)", ORPG_DEBUG)
+        found = False
+        if isinstance(alias, (int, long)):
+            self.aliasIdx = alias
+            found = True
+        else:
+            for n in xrange(self.selectAliasWnd.GetItemCount()):
+                if self.selectAliasWnd.GetItem(n, 0).GetText() == alias:
+                    self.aliasIdx = n
+                    found = True
+        if not found:
+            self.aliasIdx = -1
+        self.log.log("Exit AliasLib->SetSelectedAlias(self, aliasIdx)", ORPG_DEBUG)
+
+    def GetAliasList(self):
+        self.log.log("Enter AliasLib->GetAliasList(self)", ORPG_DEBUG)
+        alist = []
+        for n in xrange(0, self.selectAliasWnd.GetItemCount()):
+            self.alias = n
+            alist.append(self.alias[0])
+        self.log.log("Exit AliasLib->GetAliasList(self)", ORPG_DEBUG)
+        alist.sort()
+        alist.insert(0, self.chat.defaultAliasName)
+        return alist
+
+    def SetAliasList(self, alist):
+        self.log.log("Enter AliasLib->SetAliasList(self, list)", ORPG_DEBUG)
+        self.selectAliasWnd.ClearAll()
+        self.selectAliasWnd.InsertColumn(0, "Alias")
+        self.selectAliasWnd.InsertColumn(1, "Chat Color")
+        for item in alist:
+            i = self.selectAliasWnd.InsertStringItem(self.selectAliasWnd.GetItemCount(), item[0])
+            self.selectAliasWnd.SetStringItem(i, 1, item[1])
+            if item[1] != 'Default':
+                self.selectAliasWnd.SetItemTextColour(i, item[1])
+            self.selectAliasWnd.RefreshItem(i)
+        self.aliasIdx = -1
+        self.RefreshAliases()
+        self.log.log("Exit AliasLib->SetAliasList(self, list)", ORPG_DEBUG)
+
+    def GetAliasColor(self):
+        self.log.log("Enter/Exit AliasLib->GetAliasColor(self) return " + self.alias[1], ORPG_DEBUG)
+        return self.alias[1]
+
+    def RefreshAliases(self):
+        self.orpgframe.Freeze()
+        self.Freeze()
+        self.alias = -1
+        l1 = len(self.aliasList)
+        l2 = len(self.filterList)
+        if self.chat != None:
+            tmp = self.chat.aliasList.GetStringSelection()
+            self.alias = tmp
+            aidx = self.aliasIdx+1
+            if len(self.aliasList) <= aidx:
+                aidx = 0
+            self.chat.aliasList.Clear()
+            for n in xrange(l1):
+                self.chat.aliasList.Insert(self.aliasList[n], n)
+            self.chat.aliasList.SetStringSelection(self.aliasList[aidx])
+            fidx = self.chat.filterList.GetSelection()
+            if len(self.filterList) <= fidx:
+                fidx = 0
+            self.chat.filterList.Clear()
+            for n in xrange(l2):
+                self.chat.filterList.Insert(self.filterList[n], n)
+            self.chat.filterList.SetStringSelection(self.filterList[fidx])
+            if self.chat.parent.GMChatPanel != None:
+                aidx = self.chat.parent.GMChatPanel.aliasList.GetSelection()
+                if len(self.aliasList) <- aidx:
+                    aidx = 0
+                self.chat.parent.GMChatPanel.aliasList.Clear()
+                for n in xrange(l1):
+                    self.chat.parent.GMChatPanel.aliasList.Insert(self.aliasList[n], n)
+                self.chat.parent.GMChatPanel.aliasList.SetStringSelection(self.aliasList[aidx])
+                fidx = self.chat.parent.GMChatPanel.filterList.GetSelection()
+                self.chat.parent.GMChatPanel.filterList.Clear()
+                for n in xrange(l2):
+                    self.chat.parent.GMChatPanel.filterList.Insert(self.filterList[n], n)
+                self.chat.parent.GMChatPanel.filterList.SetStringSelection(self.filterList[fidx])
+            for tab in self.chat.parent.whisper_tabs:
+                aidx = tab.aliasList.GetSelection()
+                if len(self.aliasList) <= aidx:
+                    aidx = 0
+                tab.aliasList.Clear()
+                for n in xrange(l1):
+                    tab.aliasList.Insert(self.aliasList[n], n)
+                tab.aliasList.SetStringSelection(self.aliasList[aidx])
+                fidx = tab.filterList.GetSelection()
+                tab.filterList.Clear()
+                for n in xrange(l2):
+                    tab.filterList.Insert(self.filterList[n], n)
+                tab.filterList.SetStringSelection(self.filterList[fidx])
+
+            for tab in self.chat.parent.group_tabs:
+                aidx = tab.aliasList.GetSelection()
+                if len(self.aliasList) <= aidx:
+                    aidx = 0
+                tab.aliasList.Clear()
+                for n in xrange(l1):
+                    tab.aliasList.Insert(self.aliasList[n], n)
+                tab.aliasList.SetStringSelection(self.aliasList[aidx])
+                fidx = tab.filterList.GetSelection()
+                tab.filterList.Clear()
+                for n in xrange(l2):
+                    tab.filterList.Insert(self.filterList[n], n)
+                tab.filterList.SetStringSelection(self.filterList[fidx])
+
+            for tab in self.chat.parent.null_tabs:
+                aidx = tab.aliasList.GetSelection()
+                if len(self.aliasList) <= aidx:
+                    aidx = 0
+                tab.aliasList.Clear()
+                for n in xrange(l1):
+                    tab.aliasList.Insert(self.aliasList[n], n)
+                tab.aliasList.SetStringSelection(self.aliasList[aidx])
+                fidx = tab.filterList.GetSelection()
+                tab.filterList.Clear()
+                for n in xrange(l2):
+                    tab.filterList.Insert(self.filterList[n], n)
+                tab.filterList.SetStringSelection(self.filterList[fidx])
+        self.Thaw()
+        wx.CallAfter(self.orpgframe.Thaw)
+
+    def SetAliasColor(self, color):
+        self.log.log("Enter AliasLib->SetAliasColor(self, color)", ORPG_DEBUG)
+        if self.aliasIdx != -1:
+            self.selectAliasWnd.SetStringItem(self.aliasIdx, 1, color)
+            self.selectAliasWnd.SetItemTextColour(self.aliasIdx, color)
+        self.log.log("Exit AliasLib->SetAliasColor(self, color)", ORPG_DEBUG)
+
+    def FilterTextChecked(self, event):
+        if self.checkFilterText.IsChecked():
+            self.chat.filterList.SetStringSelection(self.filter)
+        else:
+            self.chat.filterList.SetStringSelection(self.chat.defaultFilterName)
+
+    def NewFilterSelection(self, event):
+        self.log.log("Enter AliasLib->NewFilterSelection(self, event)", ORPG_DEBUG)
+        self.filter = event.GetIndex()
+        if self.checkFilterText.IsChecked():
+            wx.CallAfter(self.chat.filterList.SetStringSelection, self.filter)
+        self.log.log("Exit AliasLib->NewFilterSelection(self, event)", ORPG_DEBUG)
+
+    def NoFilterSelection(self, event):
+        self.log.log("Enter AliasLib->NoFilterSelection(self, event)", ORPG_DEBUG)
+        self.filter = -1
+        wx.CallAfter(self.chat.filterList.SetStringSelection, self.filter)
+        self.log.log("Exit AliasLib->NoFilterSelection(self, event)", ORPG_DEBUG)
+
+    def GetSelectedFilter(self):
+        self.log.log("Enter AliasLib->GetSelectedFilter(self)", ORPG_DEBUG)
+        if self.filterIdx != -1:
+            self.log.log("Exit AliasLib->GetSelectedFilter(self) return " + str(self.filterIdx), ORPG_DEBUG)
+            return self.selectFilterWnd.GetItem(self.filterIdx, 0).GetText()
+        self.log.log("Exit AliasLib->GetSelectedFilter(self) return " + str(self.filterIdx), ORPG_DEBUG)
+        return self.chat.defaultFilterName
+
+    def SetSelectedFilter(self, idx):
+        self.log.log("Enter AliasLib->SetSelectedFilter(self, filter)", ORPG_DEBUG)
+        self.filterIdx = idx
+        self.log.log("Exit AliasLib->SetSelectedFilter(self, filter)", ORPG_DEBUG)
+
+    def GetFilterList(self):
+        self.log.log("Enter AliasLib->GetFilterList(self)", ORPG_DEBUG)
+        list = []
+        for n in xrange(-1, self.selectFilterWnd.GetItemCount()):
+            self.filter = n
+            list.append(self.filter)
+        self.log.log("Exit AliasLib->GetFilterList(self)", ORPG_DEBUG)
+        return list
+
+    def SetFilterList(self, list):
+        self.log.log("Enter AliasLib->SetFilterList(self, list)", ORPG_DEBUG)
+        self.selectFilterWnd.ClearAll()
+        self.selectFilterWnd.InsertColumn(0, "Filter Name")
+        for item in list:
+            i = self.selectFilterWnd.InsertStringItem(self.selectFilterWnd.GetItemCount(), item)
+            self.selectFilterWnd.RefreshItem(i)
+        self.selectFilterWnd.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+        self.filter = -1
+        self.RefreshAliases()
+        self.log.log("Exit AliasLib->SetFilterList(self, list)", ORPG_DEBUG)
+
+    def GetFilterRegEx(self):
+        if self.filterIdx == -1:
+            return []
+        return self.regExList[self.filterIdx]
+
+    def SetFilterRegEx(self, list):
+        self.regExList[self.filterIdx] = list
+
+    def FormatText(self, event):
+        #self.textColorBtn = wx.Button(self, wx.ID_ANY, "Color")
+        #self.textColorBtn.SetForegroundColour(wx.BLACK)
+        id = event.GetId()
+        txt = self.textWnd.GetValue()
+        (beg, end) = self.textWnd.GetSelection()
+        if beg != end:
+            sel_txt = txt[beg:end]
+        else:
+            sel_txt = txt
+        if id == self.textBoldBtn.GetId():
+            sel_txt = "<b>" + sel_txt + "</b>"
+        elif id == self.textItalicBtn.GetId():
+            sel_txt = "<i>" + sel_txt + "</i>"
+        elif id == self.textUnderlineBtn.GetId():
+            sel_txt = "<u>" + sel_txt + "</u>"
+        elif id == self.textColorBtn.GetId():
+            dlg = wx.ColourDialog(self)
+            if not dlg.ShowModal() == wx.ID_OK:
+                dlg.Destroy()
+                return
+            color = dlg.GetColourData().GetColour()
+            color = RGBHex().hexstring(color[0], color[1], color[2])
+            dlg.Destroy()
+            sel_txt = '<font color="' + color + '">' + sel_txt + '</font>'
+        if beg != end:
+            txt = txt[:beg] + sel_txt + txt[end:]
+        else:
+            txt = sel_txt
+        self.textWnd.SetValue(txt)
+        self.textWnd.SetInsertionPointEnd()
+        self.textWnd.SetFocus()
+
+    #Properties
+    alias = property(GetSelectedAlias, SetSelectedAlias)
+    aliasList = property(GetAliasList, SetAliasList)
+    aliasColor = property(GetAliasColor, SetAliasColor)
+
+    filter = property(GetSelectedFilter, SetSelectedFilter)
+    filterList = property(GetFilterList, SetFilterList)
+    filterRegEx = property(GetFilterRegEx, SetFilterRegEx)
+
+
+
+class FilterEditWnd(wx.Frame):
+    def __init__(self, parent, filterName, filterList):
+        wx.Frame.__init__(self, parent, wx.ID_ANY, "Edit Filter: " + filterName)
+
+        self.filterList = filterList
+        self.parent = parent
+
+        self.Freeze()
+        self.buildGUI()
+        self.fillList()
+        self.Layout()
+        self.grid.Select(0)
+        self.Thaw()
+
+        self.Bind(wx.EVT_CLOSE, self.OnExit)
+
+    def buildGUI(self):
+        bsizer = wx.BoxSizer(wx.VERTICAL)
+        self.panel = wx.Panel(self, wx.ID_ANY)
+        bsizer.Add(self.panel, 1, wx.EXPAND)
+        self.SetSizer(bsizer)
+        self.SetAutoLayout(True)
+
+        self.grid = wx.ListCtrl(self.panel, wx.ID_ANY, style=wx.LC_SINGLE_SEL|wx.LC_REPORT|wx.LC_HRULES)
+        self.grid.InsertColumn(0, "Replace")
+        self.grid.InsertColumn(1, "With")
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.selectRule, self.grid)
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.RuleEdit, self.grid)
+
+        self.addBtn = wx.Button(self.panel, wx.ID_ANY, 'Add')
+        self.editBtn = wx.Button(self.panel, wx.ID_ANY, 'Edit')
+        self.deleteBtn = wx.Button(self.panel, wx.ID_ANY, 'Delete')
+        self.okBtn = wx.Button(self.panel, wx.ID_OK, 'Done')
+
+        self.Bind(wx.EVT_BUTTON, self.RuleAdd, self.addBtn)
+        self.Bind(wx.EVT_BUTTON, self.RuleEdit, self.editBtn)
+        self.Bind(wx.EVT_BUTTON, self.RuleDelete, self.deleteBtn)
+        self.Bind(wx.EVT_BUTTON, self.OnDone, self.okBtn)
+
+        btsizer = wx.BoxSizer(wx.VERTICAL)
+        btsizer.Add(self.addBtn, 0, wx.EXPAND)
+        btsizer.Add(self.editBtn, 0, wx.EXPAND)
+        btsizer.Add(self.deleteBtn, 0, wx.EXPAND)
+        btsizer.Add(self.okBtn, 0, wx.EXPAND)
+
+        sizer = wx.GridBagSizer(5,5)
+
+        sizer.Add(self.grid, (0,0), flag=wx.EXPAND)
+        sizer.Add(btsizer, (0,1), flag=wx.EXPAND)
+
+        sizer.AddGrowableCol(0)
+        sizer.AddGrowableRow(0)
+        sizer.SetEmptyCellSize((0,0))
+
+        self.panel.SetSizer(sizer)
+        self.panel.SetAutoLayout(True)
+
+    def fillList(self):
+        for rule in self.filterList:
+            i = self.grid.InsertStringItem(self.grid.GetItemCount(), rule[0])
+            self.grid.SetStringItem(i, 1, rule[1])
+
+        self.grid.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+        self.grid.SetColumnWidth(1, wx.LIST_AUTOSIZE)
+
+    def selectRule(self, event):
+        self.currentIdx = event.GetIndex()
+        self.Freeze()
+        for i in xrange(0, self.grid.GetItemCount()):
+            self.grid.SetItemBackgroundColour(i, (255,255,255))
+
+        self.grid.SetItemBackgroundColour(self.currentIdx, (0,255,0))
+        self.grid.SetItemState(self.currentIdx, 0, wx.LIST_STATE_SELECTED)
+        self.grid.EnsureVisible(self.currentIdx)
+        self.Thaw()
+
+    def RuleEdit(self, event):
+        dlg = wx.Dialog(self, wx.ID_ANY, 'Edit Filter Rule')
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.StaticText(dlg, wx.ID_ANY, 'Replace: '), 0, wx.EXPAND)
+        rpltxt = wx.TextCtrl(dlg, wx.ID_ANY)
+        sizer.Add(rpltxt, 0, wx.EXPAND)
+        sizer.Add(wx.StaticText(dlg, wx.ID_ANY, 'With: '), 0, wx.EXPAND)
+        withtxt = wx.TextCtrl(dlg, wx.ID_ANY)
+        sizer.Add(withtxt, 0, wx.EXPAND)
+        sizer.Add(wx.Button(dlg, wx.ID_OK, 'Ok'), 0, wx.EXPAND)
+        sizer.Add(wx.Button(dlg, wx.ID_CANCEL, 'Cancel'), 0, wx.EXPAND)
+
+        dlg.SetSizer(sizer)
+        dlg.SetAutoLayout(True)
+        dlg.Fit()
+
+        rpltxt.SetValue(self.grid.GetItem(self.currentIdx, 0).GetText())
+        withtxt.SetValue(self.grid.GetItem(self.currentIdx, 1).GetText())
+
+        if dlg.ShowModal() != wx.ID_OK:
+            dlg.Destroy()
+            return
+
+        self.grid.SetStringItem(self.currentIdx, 0, rpltxt.GetValue())
+        self.grid.SetStringItem(self.currentIdx, 1, withtxt.GetValue())
+        self.grid.RefreshItem(self.currentIdx)
+        self.grid.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+        self.grid.SetColumnWidth(1, wx.LIST_AUTOSIZE)
+
+        dlg.Destroy()
+
+    def RuleAdd(self, event):
+        i = self.grid.InsertStringItem(self.grid.GetItemCount(), '')
+        self.grid.SetStringItem(i, 1, '')
+        self.grid.Select(i)
+        self.RuleEdit(None)
+
+    def RuleDelete(self, event):
+        self.grid.DeleteItem(self.currentIdx)
+        self.grid.Select(0)
+
+    def OnExit(self, event):
+        self.MakeModal(False)
+
+        list = []
+        for i in xrange(0, self.grid.GetItemCount()):
+            list.append([self.grid.GetItem(i, 0).GetText(), self.grid.GetItem(i, 1).GetText()])
+
+        self.parent.filterRegEx = list
+        event.Skip()
+
+    def OnDone(self, event):
+        self.Close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/inputValidator.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,234 @@
+# 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: inputValidator.py
+# Author: Greg Copeland
+# Maintainer:
+#
+# Description: Contains simple input validators to help reduce the amount of
+# user input generated text.
+#
+
+__version__ = "$Id: inputValidator.py,v 1.11 2006/11/04 21:24:22 digitalxero Exp $"
+
+
+##
+## Module Loading
+##
+from orpg.orpg_wx import *
+import string
+
+
+##
+## Text Only input (no numbers allowed)
+##
+class TextOnlyValidator(wx.PyValidator):
+    def __init__( self ):
+        wx.PyValidator.__init__( self )
+        self.Bind(wx.EVT_CHAR, self.onChar)
+
+
+
+    def Clone( self ):
+        return TextOnlyValidator()
+
+
+
+    def Validate( self, win ):
+        tc = self.GetWindow()
+        val = tc.GetValue()
+
+        retVal = True
+        for x in val:
+            if x not in string.letters:
+                retVal = False
+                break
+
+        return retVal
+
+
+
+    def onChar( self, event ):
+        key = event.GetKeyCode()
+        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255:
+            event.Skip()
+
+        elif chr(key) in string.letters:
+            event.Skip()
+
+        else:
+            if not wxValidator_IsSilent():
+                wxBell()
+
+        # Returning without calling even.  Skip eats the event before it
+        # gets to the text control
+        return
+
+
+
+##
+## Number Only input (no text allowed)
+##
+class NumberOnlyValidator(wx.PyValidator):
+    def __init__( self ):
+        wx.PyValidator.__init__( self )
+        self.Bind(wx.EVT_CHAR, self.onChar)
+
+
+
+    def Clone( self ):
+        return NumberOnlyValidator()
+
+
+
+    def Validate( self, win ):
+        tc = self.GetWindow()
+        val = tc.GetValue()
+
+        retVal = True
+        for x in val:
+            if x not in string.digits:
+                retVal = False
+                break
+
+        return retVal
+
+
+
+    def onChar( self, event ):
+        key = event.GetKeyCode()
+        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255:
+            event.Skip()
+
+        elif chr(key) in string.digits:
+            event.Skip()
+
+        else:
+            if not wxValidator_IsSilent():
+                wxBell()
+
+        # Returning without calling even.  Skip eats the event before it
+        # gets to the text control
+        return
+
+
+
+
+
+
+##
+## Math Only input (no text allowed, only numbers of math symbols)
+##
+class MathOnlyValidator(wx.PyValidator):
+    def __init__( self ):
+        wx.PyValidator.__init__( self )
+
+        # Build it as part of the class and not per Validate() call
+        self.allowedInput = "0123456789()*/+-<>"
+        self.Bind(wx.EVT_CHAR, self.onChar)
+
+
+
+    def Clone( self ):
+        return MathOnlyValidator()
+
+
+
+    def Validate( self, win ):
+        tc = self.GetWindow()
+        val = tc.GetValue()
+
+        retVal = True
+        for x in val:
+            if x not in self.allowedInput:
+                retVal = False
+                break
+
+        return retVal
+
+
+
+    def onChar( self, event ):
+        key = event.GetKeyCode()
+        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255:
+            event.Skip()
+
+        elif chr(key) in self.allowedInput:
+            event.Skip()
+
+        else:
+            if not wxValidator_IsSilent():
+                wxBell()
+
+        # Returning without calling even.  Skip eats the event before it
+        # gets to the text control
+        return
+
+
+
+
+
+
+##
+## Text and number input but DO NOT allow ANY HTML type input (no numbers allowed)
+##
+class NoHTMLValidator(wx.PyValidator):
+    def __init__( self ):
+        wx.PyValidator.__init__( self )
+
+        # Build it as part of the class and not per Validate() call
+        self.allowedInput = " 1234567890!@#$%^&*()_-+=`~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,[]{}|;:'\",./?\\"
+        self.Bind(wx.EVT_CHAR, self.onChar)
+
+
+
+    def Clone( self ):
+        return NoHTMLValidator()
+
+
+
+    def Validate( self, win ):
+        tc = self.GetWindow()
+        val = tc.GetValue()
+
+        retVal = True
+        for x in val:
+            if x not in self.allowedInput:
+                retVal = False
+                break
+
+        return retVal
+
+
+
+    def onChar( self, event ):
+        key = event.GetKeyCode()
+        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255:
+            event.Skip()
+
+        elif chr(key) in self.allowedInput:
+            event.Skip()
+
+        else:
+            if not wxValidator_IsSilent():
+                wxBell()
+
+        # Returning without calling even.  Skip eats the event before it
+        # gets to the text control
+        return
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/metamenus.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1418 @@
+__author__  = "E. A. Tacao <e.a.tacao |at| estadao.com.br>"
+__date__    = "21 Apr 2006, 13:15 GMT-03:00"
+__version__ = "0.07"
+__doc__     = """
+metamenus: classes that aim to simplify the use of menus in wxPython.
+
+MenuBarEx is a wx.MenuBar derived class for wxPython;
+MenuEx    is a wx.Menu derived class for wxPython.
+
+Some features:
+
+- Menus are created based on the indentation of items on a list. (See 'Usage'
+  below.)
+
+- Each menu item will trigger a method on the parent. The methods names may
+  be explicitly defined on the constructor or generated automatically. It's
+  also possible to define some names and let metamenus create the remaining.
+
+- Allows the user to enable or disable a menu item or an entire menu given
+  its label.
+
+- Supplies EVT_BEFOREMENU and EVT_AFTERMENU, events that are triggered right
+  before and after, respectively, the triggering of a EVT_MENU-bound method
+  on selection of some menu item.
+
+- MenuBarEx handles accelerators for numpad keys and also supports 'invisible
+  menu items'.
+
+- If your app is already i18n'd, menu items may be translated on the fly.
+  All you need to do is to write somewhere a .mo file containing the menu
+  translations.
+
+
+MenuEx Usage:
+
+The MenuEx usage is similar to MenuBarEx (please see below), except that it
+has an optional kwarg named show_title (boolean; controls whether the menu
+title will be shown) and doesn't have the MenuBarEx's xaccel kwarg:
+
+     MenuEx(self, menus, margin=wx.DEFAULT, show_title=True,
+            font=wx.NullFont, custfunc={}, i18n=True, style=0)
+
+
+MenuBarEx Usage:
+
+In order to put a MenuBarEx inside a frame it's enough to do this:
+     MenuBarEx(self, menus)
+
+or you can also use some few optional keyword arguments:
+     MenuBarEx(self, menus, margin=wx.DEFAULT, font=wx.NullFont,
+               xaccel=None, custfunc={}, i18n=True, style=0)
+
+  Arguments:
+    - self:  The frame in question.
+
+    - menus: A python list of 'menus', which are python lists of
+             'menu_entries'. Each 'menu_entry' is a python list that needs to
+             be in one of the following formats:
+
+              [label]
+              or [label, args]
+              or [label, kwargs]
+              or [label, args, kwargs]
+              or [label, kwargs, args]  (but please don't do this one).
+
+      . label: (string) The text for the menu item.
+
+               Leading whitespaces at the beginning of a label are used to
+               compute the indentation level of the item, which in turn is
+               used to determine the grouping of items. MenuBarEx determines
+               one indentation level for every group of two whitespaces.
+
+               If you want this item to be a sub-item, increase its
+               indentation. Top-level items must have no indentation.
+
+               Separators are items labeled with a "-" and may not have args
+               and kwargs.
+
+               Menu breaks (please see the wx.MenuItem.Break docs) are items
+               labeled with a "/" and may not have args and kwargs.
+
+               Accelerators are handled as usual; MenuBarEx also supports
+               numpad accelerators (e.g, "  &Forward\tCtrl+Num 8").
+
+               Please refer to the wxPython docs for wx.Menu.Append for more
+               information about them.
+
+      . args: (tuple) (helpString, wxItemKind)
+
+               - helpString is an optional help string that will be shown on
+                 the parent's status bar. If don't pass it, no help string
+                 for this item will appear on the statusbar.
+
+               - wxItemKind may be one of wx.ITEM_CHECK, "check",
+                 wx.ITEM_RADIO or "radio". It is also optional; if don't pass
+                 one, a default wx.ITEM_NORMAL will be used.
+
+               Note that if you have to pass only one argument, you can do
+               either:
+
+                   args=("", wxItemKind)
+                or args=(helpString,)
+                or helpString
+                or wxItemKind
+                or (helpString)
+                or (wxItemKind)
+
+                When you pass only one item, Metamenus will check if the
+                thing passed can be translated as an item kind (either
+                wx.RADIO, "radio", etc.) or not, and so will try to guess
+                what to do with the thing. So that if you want a status bar
+                showing something that could be translated as an item kind,
+                say, "radio", you'll have to pass both arguments: ("radio",).
+
+
+       . kwargs: (dict) wxBitmap bmpChecked, wxBitmap bmpUnchecked,
+                        wxFont font, int width,
+                        wxColour fgcolour, wxColour bgcolour
+
+               These options access wx.MenuItem methods in order to change
+               its appearance, and might not be present on all platforms.
+               They are internally handled as follows:
+
+                 key:                              item method:
+
+                 "bmpChecked" and "bmpUnchecked" : SetBitmaps
+                 "font"                          : SetFont
+                 "margin",                       : SetMarginWidth
+                 "fgColour",                     : SetTextColour
+                 "bgColour",                     : SetBackgroundColour
+
+               The "bmpChecked" and "bmpUnchecked" options accept a bitmap or
+               a callable that returns a bitmap when called. This is useful
+               if you created your bitmaps with encode_bitmaps.py and want to
+               pass something like {"bmpChecked": my_images.getSmilesBitmap}.
+
+               Please refer to the wxPython docs for wx.MenuItem for more
+               information about the item methods.
+
+    - margin:   (int) a value that will be used to do a SetMargin() for each
+                menubar item. Please refer to the wxPython docs for
+                wx.MenuItem.SetMargin for more information about this.
+
+    - font:     (wx.Font) a value that will be used to do a SetFont() for
+                each menu item. Please refer to the wxPython docs for
+                wx.MenuItem.SetFont for more information about this.
+
+    - xaccel:   (MenuBarEx only) allows one to bind events to 'items' that
+                are not actually menu items, rather methods or functions that
+                are triggered when some key or combination of keys is
+                pressed.
+
+                xaccel is a list of tuples (accel, function), where accel is
+                a string following the accelerator syntax described in the
+                wx.Menu.Append docs and function is the function/method to be
+                executed when the accelerator is triggered.
+
+                The events are managed in the same way as MenuBarEx events.
+
+    - custfunc: (dict) allows one to define explicitly what will be the
+                parent's method called on a menu event.
+
+                By default, all parent's methods have to start with "OnMB_"
+                (for menubars) or "OnM_" (for menus) plus the full menu
+                'path'. For a 'Save' menu item inside a 'File' top menu, e.g:
+
+                    def OnMB_FileSave(self):
+                        self.file.save()
+
+                However, the custfunc arg allows you to pass a dict of
+
+                    {menupath: method, menupath: method, ...}
+
+                so that if you want your File > Save menu triggering a
+                'onSave' method instead, you may pass
+
+                    {"FileSave": "onSave"}
+                 or {"FileSave": self.onSave}
+
+                as custfunc. This way, your parent's method should look like
+                this instead:
+
+                    def onSave(self):
+                        self.file.save()
+
+                You don't have to put all menu items inside custfunc. The
+                menupaths not found there will still trigger automatically
+                an OnMB_/OnM_-prefixed method.
+
+    - i18n:     (bool) Controls whether you want the items to be translated
+                or not. Default is True. For more info on i18n, please see
+                'More about i18n' below.
+
+    - style:    Please refer to the wxPython docs for wx.MenuBar/wx.Menu for
+                more information about this.
+
+
+The public methods:
+
+  The 'menu_string' arg on some of the public methods is a string that
+  refers to a menu item. For a File > Save menu, e. g., it may be
+  "OnMB_FileSave", "FileSave" or the string you passed via the custfunc
+  parameter (i. e., if you passed {"FileSave": "onSave"} as custfunc, the
+  string may also be "onSave").
+
+  The 'menu_string_list' arg on some of the public methods is a python list
+  of 'menu_string' strings described above. Please refer to the methods
+  themselves for more details.
+
+
+More about i18n:
+  If you want to get your menu items automatically translated, you'll need
+  to:
+
+  1. Create a directory named 'locale' under your app's directory, and under
+     the 'locale', create subdirectories named after the canonical names of
+     the languages you're going to use (e. g., 'pt_BR', 'es_ES', etc.)
+
+  2. Inside each of the subdirectories, write a gettext compiled catalog file
+     (e. g., "my_messages.mo") containing all of the menu labels translated
+     to the language represented by the subdirectory.
+
+  4. The language can be changed on the fly. Whenever you want to change the
+     menu language, execute these lines somewhere in your app:
+
+       l = wx.Locale(wx.LANGUAGE_PORTUGUESE_BRAZILIAN)
+       l.AddCatalogLookupPathPrefix("locale")
+       l.AddCatalog("my_messages.mo")
+       self.my_menu.UpdateMenus()
+
+  Unless you want your menus showing up in pt_BR, replace the
+  wx.LANGUAGE_PORTUGUESE_BRAZILIAN above by the proper language identifier.
+  For a list of supported identifiers please see the wxPython docs, under the
+  'Constants\Language identifiers' section.
+
+  Some items may show up in the selected language even though you didn't
+  create a .mo file for the translations. That's because wxPython looks for
+  them in the wxstd.mo file placed somewhere under the wxPython tree, and
+  maybe wxPython already uses some of the string you are using.
+
+  Note that if you're to distribute a standalone app the wxPython tree may
+  not be present, so it's a good idea to include a specific .mo file in your
+  package. On the other hand, if by any reason you _don't_ want the menu
+  items to be translated, you may pass a i18n=False kwarg to the constructor.
+
+  You can use metamenus itself directly from a command line to help on
+  creating a gettext-parseable file based on the menus you wrote. For more
+  info about this, please see the docs for the _mmprep class.
+
+  For more info about i18n, .mo files and gettext, please see
+  <http://wiki.wxpython.org/index.cgi/Internationalization>.
+
+
+Menu bar example:
+
+    a = [["File"],
+         ["  New",          "Creates a new file"],
+         ["  Save"],
+         ["  -"],
+         ["  Preview",      "Preview Document",
+                            {"bmpChecked": images.getSmilesBitmap(),
+                             "fgColour": wx.RED}],
+         ["  -"],
+         ["  Exit"]]
+
+    b = [["Edit"],
+         ["  Cut"],
+         ["  Copy"],
+         ["    Foo",         "check"],
+         ["    Bar",         "check"],
+         ["  Paste"]]
+
+    myMenuBar = MenuBarEx(self, [a, b])
+
+
+Context menu example:
+
+    a = [["Edit"],          # A 'top-level' menu item is used as title;
+         ["  Cut"],
+         ["  Copy"],
+         ["    Foo",        "radio"],
+         ["    Bar",        "radio"],
+         ["  Paste"]]
+
+    myContextMenu = MenuEx(self, a)
+
+
+If you don't want to show the title for the context menu:
+
+   myContextMenu = MenuEx(self, a, show_title=False)
+
+
+A very default 'File' menu example:
+
+       [
+        ['&File'],
+        ['  &New\tCtrl+N'],
+        ['  &Open...\tCtrl+O'],
+        ['  &Save\tCtrl+S'],
+        ['  Save &As...\tCtrl+Shift+S'],
+        ['  -'],
+        ['  Publis&h\tCtrl+Shift+P'],
+        ['  -'],
+        ['  &Close\tCtrl+W'],
+        ['  C&lose All'],
+        ['  -'],
+        ['  E&xit\tAlt+X']
+       ]
+
+
+Known Issues:
+
+These are wx.Menu issues, and since metamenus doesn't/can't work around them
+it's advisable to stay away from custom menus:
+
+- If you try to customize an item changing either its font, margin or
+colours, the following issues arise:
+
+  1. The item will appear shifted to the right when compared to default menu
+     items, although a GetMarginWidth() will return a default value;
+  2. wx.ITEM_RADIO items won't show their bullets.
+
+- If you try to change the bitmaps for wx.ITEM_RADIO items, the items will
+ignore the 2nd bitmap passed and will always show the checked bitmap,
+regardless of their state.
+
+
+About:
+
+metamenus is distributed under the wxWidgets license.
+
+This code should meet the wxPython Coding Guidelines
+<http://www.wxpython.org/codeguidelines.php> and the wxPython Style Guide
+<http://wiki.wxpython.org/index.cgi/wxPython_20Style_20Guide>.
+
+For all kind of problems, requests, enhancements, bug reports, etc,
+please drop me an e-mail.
+
+For updates please visit <http://j.domaindlx.com/elements28/wxpython/>.
+"""
+
+# History:
+#
+# Version 0.07:
+#    - Applied a patch from Michele Petrazzo which now allows the values
+#      passed via the custfunc dictionary to be either callable objects
+#      or strings representing the callable objects.
+#
+# Version 0.06:
+#    - Added i18n capabilities. Running metamenus.py from the command line
+#      also creates a gettext-parseable file than can in turn be used to
+#      create .po files.
+#
+#    - Some minor syntax fixes so that this code hopefully should meet the
+#      wxPython Coding Guidelines and the wxPython Style Guide.
+#
+#    - Changed EVT_BEFOREMENU_EVENT to EVT_BEFOREMENU and EVT_AFTERMENU_EVENT
+#      to EVT_AFTERMENU. If your app was using them, please update it.
+#
+#    - Fixed a test into OnMB_ that would raise an error on unicode systems;
+#      thanks to Michele Petrazzo for pointing this out.
+#
+#    - Fixed the EVT_MENU binding so that the accelerators now should work
+#      on Linux; thanks to Michele Petrazzo for pointing this out.
+#
+#    - Fixed a couple of bad names in the public methods (EnableMenuTop to
+#      EnableTopMenu, etc.) that would prevent the methods to work.
+#
+#    - Fixed a bug that would prevent checkable items to be created when
+#      only a tupleless wxItemKind was passed within a menu item.
+#
+#    - Fixed a couple of potential unicode bugs in _adjust that could arise
+#      if unicode objects were passed as menu items or help strings.
+#
+#    - Changes in _sItem: _adjust now is a method of _sItem; GetPath
+#      substituted _walkMenu/_walkMenuBar; _sItem now finds a translated
+#      label when using 18n, etc.
+#
+#    - All of the menu item strings passed to the public methods now may be
+#      in one of the following forms: (1) The full menu 'path' (e. g.,
+#      "FileSave"), (2) The prefix + the full menu 'path' (e. g.,
+#      "OnMB_FileSave"), (3) The method name passed as custfunc (e. g., if
+#      you passed {"FileSave": "onSave"} as custfunc, the string may also
+#      be "onSave").
+#
+#    - "bmpChecked" and "bmpUnchecked" options now may accept a bitmap or
+#      a callable that returns a bitmap when called. This is useful if your
+#      menu 'tree' is in another file and you import it _before_ your app is
+#      created, since BitmapFromImage can only be used if the app is already
+#      out there.
+#
+# Version 0.05:
+#    - Fixed the popup menu position on MenuEx.
+#
+#    - Applied a patch from Michele Petrazzo which implemented the custfunc
+#      funcionality, allowing one to choose arbitrary names for methods
+#      called on menu events.
+#
+# Version 0.04:
+#    - Changed the OnMB_, OnM_ code so that they won't shadow AttributeErrors
+#      raised on parent's code.
+#
+#    - Add the show_title kwarg to the MenuEx constructor.
+#
+# Version 0.03:
+#   - Added support for numpad accelerators; they must be passed as "Num x",
+#     where x may be into a [0-9] range.
+#
+#   - Added support for wx.MenuItem.Break(); if you want a menu break,
+#     now you can pass a "/" on a menu entry label.
+#
+#   - Added the EVT_BEFOREMENU_EVENT, which will be triggered right before
+#     the menu event.
+#
+#   - Those who believe that wx.UPPERCASE_STUFF_IS_UGLY 8^) now can pass
+#     "radio" instead of wx.ITEM_RADIO, "check" instead of wx.ITEM_CHECK, and
+#     "normal" (or "", or even nothing at all) instead of wx.ITEM_NORMAL.
+#
+#   - The args syntax has been extended. The previous version allowed one to
+#     pass either:
+#
+#          (helpString, wxItemKind)
+#       or ("", wxItemKind)
+#       or (helpString,)
+#
+#       Now its also possible to pass:
+#
+#          helpString
+#       or wxItemKind
+#       or (helpString)
+#       or (wxItemKind)
+#
+#     When you use this new style, Metamenus will check if the thing passed
+#     can be translated as an item kind (either wx.RADIO, "radio", etc.) or
+#     not, and so will try to guess what to do with the thing. Note that if
+#     you want a status bar showing something like "radio", you'll not be
+#     able to use this new style, but ("radio",) will still work for such
+#     purposes, though.
+#
+#   - xaccel, a new kwarg available in MenuBarEx, allows one to bind events
+#     to 'items' that are not actually menu items, rather methods or
+#     functions that are triggered when some key or combination of keys is
+#     pressed.
+#
+#     xaccel is a list of tuples (accel, function), where accel is a string
+#     following the accelerator syntax described in wx.Menu.Append docs and
+#     function is the function/method to be executed when the accelerator is
+#     triggered.
+#
+#     The events will be managed in the same way as MenuBarEx events. IOW,
+#     xaccel accelerators will provide some sort of 'invisible menu items'.
+#
+# Version 0.02: severe code clean-up; accelerators for submenus now work.
+#
+# Version 0.01: initial release.
+
+#----------------------------------------------------------------------------
+
+import wx
+from wx.lib.newevent import NewEvent
+
+# Events --------------------------------------------------------------------
+
+(MenuExBeforeEvent, EVT_BEFOREMENU) = NewEvent()
+(MenuExAfterEvent, EVT_AFTERMENU) = NewEvent()
+
+# Constants -----------------------------------------------------------------
+
+# If you're to use a different indentation level for menus, change
+# _ind here.
+_ind = 2 * " "
+
+# _sep is used internally only and is a substring that _cannot_
+# appear on any of the regular menu labels.
+_sep = " @@@ "
+
+# If you want to use different prefixes for methods called by this
+# menubar/menu, change them here.
+_prefixMB = "OnMB_"
+_prefixM  = "OnM_"
+
+#----------------------------------------------------------------------------
+
+class _sItem:
+    """
+    Internal use only. This provides a structure for parsing the 'trees'
+    supplied in a sane way.
+    """
+
+    def __init__(self, params):
+        self.parent = None
+        self.Id = wx.NewId()
+        self.params = self._adjust(params)
+        self.children = []
+
+        self.Update()
+
+
+    def _adjust(self, params):
+        """
+        This is responsible for formatting the args and kwargs for items
+        supplied within the 'tree'.
+        """
+
+        args = (); kwargs = {}
+        params = params + [None] * (3 - len(params))
+
+        if type(params[1]) == tuple:
+            args = params[1]
+        elif type(params[1]) in [str, unicode, int]:
+            args = (params[1],)
+        elif type(params[1]) == dict:
+            kwargs = params[1]
+
+        if type(params[2]) == tuple:
+            args = params[2]
+        elif type(params[2]) in [str, unicode, int]:
+            args = (params[2],)
+        elif type(params[2]) == dict:
+            kwargs = params[2]
+
+        args = list(args) + [""] * (2 - len(args))
+
+        # For those who believe wx.UPPERCASE_STUFF_IS_UGLY... 8^)
+        kind_conv = {"radio":  wx.ITEM_RADIO,
+                     "check":  wx.ITEM_CHECK,
+                     "normal": wx.ITEM_NORMAL}
+
+        if args[0] in kind_conv.keys() + kind_conv.values():
+            args = (args[1], args[0])
+
+        kind_conv.update({"normal": None, "": None})
+
+        if type(args[1]) in [str, unicode]:
+            kind = kind_conv.get(args[1])
+            if kind is not None:
+                args = (args[0], kind)
+            else:
+                args = (args[0],)
+
+        return (params[0], tuple(args), kwargs)
+
+
+    def Update(self):
+        # Members created/updated here:
+        #
+        # label:            "&New\tCtrl+N"
+        # label_text:       "&New"
+        # tlabel:           "&Novo\tCtrl+N"
+        # tlabel_text:      "&Novo"
+        # acc:              "Ctrl+N"
+        #
+        # I'm not actually using all of them right now, but maybe I will...
+
+        self.label = self.params[0].strip()
+        self.label_text = self.label.split("\t")[0].strip()
+        label, acc = (self.label.split("\t") + [''])[:2]
+        self.tlabel_text = wx.GetTranslation(label.strip())
+        self.acc = acc.strip()
+        if self.acc:
+            self.tlabel = "\t".join([self.tlabel_text, self.acc])
+        else:
+            self.tlabel = self.tlabel_text
+
+
+    def AddChild(self, Item):
+        Item.parent = self
+        self.children.append(Item)
+        return Item
+
+
+    def GetRealLabel(self, i18n):
+        if i18n:
+            label = self.GetLabelTranslation()
+        else:
+            label = self.GetLabel()
+        return label
+
+
+    def GetLabel(self):
+        return self.label
+
+
+    def GetLabelText(self):
+        return self.label_text
+
+
+    def GetLabelTranslation(self):
+        return self.tlabel
+
+
+    def GetLabelTextTranslation(self):
+        return self.tlabel_text
+
+
+    def GetAccelerator(self):
+        return self.acc
+
+
+    def GetId(self):
+        return self.Id
+
+
+    def GetParams(self):
+        return self.params
+
+
+    def GetParent(self):
+        return self.parent
+
+
+    def GetChildren(self, recursive=False):
+        def _walk(Item, r):
+            for child in Item.GetChildren():
+                r.append(child)
+                if child.HasChildren():
+                    _walk(child, r)
+            return r
+
+        if not recursive:
+            return self.children
+        else:
+            return _walk(self, [])
+
+
+    def HasChildren(self):
+        return bool(self.children)
+
+
+    def GetChildWithChildren(self):
+        def _walk(Item, r):
+            for child in Item.GetChildren():
+                if child.HasChildren():
+                    r.insert(0, child); _walk(child, r)
+            return r
+
+        return _walk(self, [])
+
+
+    def GetChildWithId(self, Id):
+        r = None
+        for child in self.GetChildren(True):
+            if child.GetId() == Id:
+                r = child; break
+        return r
+
+
+    def GetPath(self):
+        this = self; path = this.GetLabelText()
+
+        while this.GetParent() is not None:
+            this = this.GetParent()
+            path = "%s %s %s" % (this.GetLabelText(), _sep, path)
+
+        return path
+
+
+    def SetMethod(self, prefix, custfunc):
+        menuName = _clean(self.GetPath())
+
+        method_custom = custfunc.get(menuName)
+        method_default = prefix + menuName
+
+        # If a custfunc was passed here, use it; otherwise we'll use a
+        # default method name when this menu item is selected.
+        self.method = method_custom or method_default
+
+        # We also store a reference to all method names that the public
+        # methods can address.
+        self.all_methods = {method_custom: self.GetId(),
+                            method_default: self.GetId(),
+                            menuName: self.GetId()}
+
+
+    def GetMethod(self):
+        return self.method
+
+
+    def GetAllMethods(self):
+        return self.all_methods
+
+#----------------------------------------------------------------------------
+
+class _acceleratorTable:
+    """
+    Internal use only.
+
+    The main purposes here are to provide MenuBarEx support for accelerators
+    unhandled by the original wxMenu implementation (currently we only handle
+    numpad accelerators here) and to allow user to define accelerators
+    (passing the kwarg xaccel on MenuBarEx.__init__) that work even though
+    they're not associated to a menu item.
+    """
+
+    def __init__(self, xaccel=None):
+        """
+        Constructor.
+
+        xaccel is a list of tuples (accel, function), where accel is a string
+        following the accelerator syntax described in wx.Menu.Append docs and
+        function is the function/method to be executed when the
+        accelerator is triggered.
+        """
+
+        self.entries = []
+
+        self.flag_conv = {"alt"  : wx.ACCEL_ALT,
+                          "shift": wx.ACCEL_SHIFT,
+                          "ctrl" : wx.ACCEL_CTRL,
+                          ""     : wx.ACCEL_NORMAL}
+
+        xaccel = xaccel or (); n = []
+        for acc, fctn in xaccel:
+            flags, keyCode = self._parseEntry(acc)
+            if flags != None and keyCode != None:
+                n.append((flags, keyCode, fctn))
+        self.xaccel = n
+
+
+    def _parseEntry(self, acc):
+        """Support for unhandled accelerators."""
+
+        lacc = acc.lower()
+        flags, keyCode = None, None
+
+        # Process numpad keys...
+        if "num" in lacc:
+
+            # flags...
+            if "+" in lacc:
+                flag = lacc.split("+")[:-1]
+            elif "-" in acc:
+                flag = lacc.split("-")[:-1]
+            else:
+                flag = [""]
+
+            flags = 0
+            for rflag in flag:
+                flags |= self.flag_conv[rflag.strip()]
+
+            # keycode...
+            exec("keyCode = wx.WXK_NUMPAD%s" % lacc.split("num")[1].strip())
+
+        return flags, keyCode
+
+
+    def Convert(self, cmd, accel):
+        """
+        Converts id and accelerator supplied into wx.AcceleratorEntry
+        objects.
+        """
+
+        flags, keyCode = self._parseEntry(accel)
+        if flags != None and keyCode != None:
+            self.entries.append(wx.AcceleratorEntry(flags, keyCode, cmd))
+
+
+    def Assemble(self, MBIds):
+        """Assembles the wx.AcceleratorTable."""
+
+        for flags, keyCode, fctn in self.xaccel:
+            _id = wx.NewId(); MBIds[_id] = fctn
+            self.entries.append(wx.AcceleratorEntry(flags, keyCode, _id))
+
+        return MBIds, wx.AcceleratorTable(self.entries)
+
+#----------------------------------------------------------------------------
+
+def _process_kwargs(item, kwargs, margin, font):
+    """
+    Internal use only. This is responsible for setting font, margin and
+    colour for menu items.
+    """
+
+    if kwargs.has_key("bmpChecked"):
+        checked = kwargs["bmpChecked"]
+        unchecked = kwargs.get("bmpUnchecked", wx.NullBitmap)
+
+        if callable(checked):
+            checked = checked()
+        if callable(unchecked):
+            unchecked = unchecked()
+
+        item.SetBitmaps(checked, unchecked)
+
+    kwlist = [("font",     "SetFont"),
+              ("margin",   "SetMarginWidth"),
+              ("fgColour", "SetTextColour"),
+              ("bgColour", "SetBackgroundColour")]
+
+    for kw, m in kwlist:
+        if kwargs.has_key(kw):
+            getattr(item, m)(kwargs[kw])
+
+    if margin != wx.DEFAULT:
+        item.SetMarginWidth(margin)
+
+    if font != wx.NullFont:
+        item.SetFont(font)
+
+    return item
+
+#----------------------------------------------------------------------------
+
+def _evolve(a):
+    """Internal use only. This will parse the menu 'tree' supplied."""
+
+    top = _sItem(a[0]); il = 0; cur = {il: top}
+
+    for i in range(1, len(a)):
+        params = a[i]
+        level  = params[0].count(_ind) - 1
+
+        if level > il:
+            il += 1; cur[il] = new_sItem
+        elif level < il:
+            il = level
+
+        new_sItem = cur[il].AddChild(_sItem(params))
+
+    return top
+
+#----------------------------------------------------------------------------
+
+def _clean(s):
+    """Internal use only. Removes all non-alfanumeric chars from a string."""
+
+    return "".join([x for x in s if x.isalnum()])
+
+#----------------------------------------------------------------------------
+
+def _makeMenus(wxmenus, saccel, h, k, margin, font, i18n):
+    """Internal use only. Creates menu items."""
+
+    label = h.GetRealLabel(i18n); Id = h.GetId()
+    args, kwargs = h.GetParams()[1:]
+
+    if h.HasChildren():
+        args = (wxmenus[h], Id, label) + args
+        item = wx.MenuItem(*args, **{"subMenu": wxmenus[h]})
+        item = _process_kwargs(item, kwargs, margin, font)
+        wxmenus[k].AppendItem(item)
+        if saccel is not None:
+            saccel.Convert(item.GetId(), h.GetAccelerator())
+
+    else:
+        if label == "-":
+            wxmenus[k].AppendSeparator()
+
+        elif label == "/":
+            wxmenus[k].Break()
+
+        else:
+            args = (wxmenus[k], Id, label) + args
+            item = wx.MenuItem(*args)
+            item = _process_kwargs(item, kwargs, margin, font)
+            wxmenus[k].AppendItem(item)
+            if saccel is not None:
+                saccel.Convert(item.GetId(), h.GetAccelerator())
+
+#----------------------------------------------------------------------------
+
+class _mmprep:
+    """
+    Generates a temporary file that can be read by gettext utilities in order
+    to create a .po file with strings to be translated. This class is called
+    when you run metamenus from the command line.
+
+    Usage:
+     1. Make sure your menus are in a separate file and that the separate
+        file in question contain only your menus;
+
+     2. From a command line, type:
+          metamenus.py separate_file outputfile
+
+        where 'separate_file' is the python file containing the menu 'trees',
+        and 'outputfile' is the python-like file generated that can be parsed
+        by gettext utilities.
+
+    To get a .po file containing the translatable strings, put the
+    'outputfile' in the app.fil list of translatable files and run the
+    mki18n.py script. For more info please see
+    <http://wiki.wxpython.org/index.cgi/Internationalization>.
+    """
+
+    def __init__(self, filename, outputfile):
+        """Constructor."""
+
+        print "Parsing %s.py..." % filename
+
+        exec("import %s" % filename)
+        mod = eval(filename)
+
+        objs = []
+        for obj in dir(mod):
+            if type(getattr(mod, obj)) == list:
+                objs.append(obj)
+
+        all_lines = []
+        for obj in objs:
+            gerr = False; header = ["\n# Strings for '%s':\n" % obj]
+            err, lines = self.parseMenu(mod, obj)
+            if not err:
+                print "OK: parsed '%s'" % obj
+                all_lines += header + lines
+            else:
+                err, lines = self.parseMenuBar(mod, obj)
+                if not err:
+                    print "OK: parsed '%s'" % obj
+                    all_lines += header + lines
+                else:
+                    gerr = True
+            if gerr:
+                print "Warning: couldn't parse '%s'" % obj
+
+        try:
+            f = file("%s.py" % outputfile, "w")
+            f.writelines(all_lines)
+            f.close()
+            print "File %s.py succesfully written." % outputfile
+
+        except:
+            print "ERROR: File %s.py was NOT written." % outputfile
+            raise
+
+
+    def form(self, lines):
+        """Removes separators and breaks and adds gettext stuff."""
+
+        new_lines = []
+        for line in lines:
+            if line not in ["-", "/"]:
+                new_lines.append("_(" + `line` + ")\n")
+        return new_lines
+
+
+    def parseMenuBar(self, mod, obj):
+        """Tries to parse a MenuBarEx object."""
+
+        err = False; lines = []
+        try:
+            for menu in getattr(mod, obj):
+                top = _evolve(menu)
+                lines.append(top.GetLabelText())
+                for child in top.GetChildren(True):
+                    lines.append(child.GetLabelText())
+        except:
+            err = True
+
+        return err, self.form(lines)
+
+
+    def parseMenu(self, mod, obj):
+        """Tries to parse a MenuEx object."""
+
+        err = False; lines = []
+        try:
+            top = _evolve(getattr(mod, obj))
+            lines.append(top.GetLabelText())
+            for child in top.GetChildren(True):
+                lines.append(child.GetLabelText())
+        except:
+            err = True
+
+        return err, self.form(lines)
+
+
+# MenuBarEx Main stuff ------------------------------------------------------
+
+class MenuBarEx(wx.MenuBar):
+    def __init__(self, *args, **kwargs):
+        """
+        Constructor.
+        MenuBarEx(parent, menus, margin=wx.DEFAULT, font=wx.NullFont,
+                  xaccel=None, custfunc={}, i18n=True, style=0)
+        """
+
+        # Initializing...
+        self.parent, menus = args
+        margin = kwargs.pop("margin", wx.DEFAULT)
+        font = kwargs.pop("font", wx.NullFont)
+        xaccel = kwargs.pop("xaccel", None)
+        custfunc = kwargs.pop("custfunc", {})
+        i18n = self.i18n = kwargs.pop("i18n", True)
+
+        wx.MenuBar.__init__(self, **kwargs)
+
+        # An object to handle accelerators.
+        self.accel = _acceleratorTable(xaccel)
+
+        # A reference to all of the sItems involved.
+        tops = []
+
+        # For each menu...
+        for a in menus:
+            # Parse the menu 'tree' supplied.
+            top = _evolve(a)
+
+            # Create these menus first...
+            wxmenus = {top: wx.Menu()}
+            for k in top.GetChildWithChildren():
+                wxmenus[k] = wx.Menu()
+
+                # ...and append their respective children.
+                for h in k.GetChildren():
+                    _makeMenus(wxmenus, self.accel, h, k, margin, font, i18n)
+
+            # Now append these items to the top level menu.
+            for h in top.GetChildren():
+                _makeMenus(wxmenus, self.accel, h, top, margin, font, i18n)
+
+            # Now append the top menu to the menubar.
+            self.Append(wxmenus[top], top.GetRealLabel(i18n))
+
+            # Store a reference of this sItem.
+            tops.append(top)
+
+        # Now find out what are the methods that should be called upon
+        # menu items selection.
+        MBIds = {}; self.MBStrings = {}
+        for top in tops:
+            for child in top.GetChildren(True):
+                child.SetMethod(_prefixMB, custfunc)
+                MBIds[child.GetId()] = child
+                self.MBStrings.update(child.GetAllMethods())
+
+        # It won't hurt if we get rid of a None key, if any.
+        bogus = self.MBStrings.pop(None, None)
+
+        # We store the position of top-level menus rather than ids because
+        # wx.Menu.EnableTop uses positions...
+        for i, top in enumerate(tops):
+            self.MBStrings[_clean(top.GetLabelText())] = i
+            MBIds[i] = top
+
+        # Nice class. 8^) Will take care of this automatically.
+        self.parent.SetMenuBar(self)
+        self.parent.Bind(wx.EVT_MENU, self.OnMB_)
+
+        # Now do something about the accelerators...
+        self.MBIds, at = self.accel.Assemble(MBIds)
+        self.parent.SetAcceleratorTable(at)
+
+
+    def OnMB_(self, evt):
+        """
+        Called on all menu events for this menu. It will in turn call
+        the related method on parent, if any.
+        """
+
+        try:
+            attr = self.MBIds[evt.GetId()]
+
+            self.OnMB_before()
+
+            # Trigger everything except stuff passed via xaccel.
+            if isinstance(attr, _sItem):
+                attr_name = attr.GetMethod()
+
+                if callable(attr_name):
+                    attr_name()
+                elif hasattr(self.parent, attr_name) and \
+                     callable(getattr(self.parent, attr_name)):
+                    getattr(self.parent, attr_name)()
+                else:
+                    print "%s not found in parent." % attr_name
+
+            # Trigger something passed via xaccel.
+            elif callable(attr):
+                attr()
+
+            self.OnMB_after()
+
+        except KeyError:
+            # Maybe another menu was triggered elsewhere in parent.
+            pass
+
+        #evt.Skip() - removed. see http://www.archivum.info/comp.soft-sys.wxwindows/2008-07/msg00027.html
+
+
+    def OnMB_before(self):
+        """
+        If you need to execute something right before a menu event is
+        triggered, you can bind the EVT_BEFOREMENU.
+        """
+
+        evt = MenuExBeforeEvent(obj=self)
+        wx.PostEvent(self, evt)
+
+
+    def OnMB_after(self):
+        """
+        If you need to execute something right after a menu event is
+        triggered, you can bind the EVT_AFTERMENU.
+        """
+
+        evt = MenuExAfterEvent(obj=self)
+        wx.PostEvent(self, evt)
+
+
+    # Public methods --------------------------------------------------------
+
+    def UpdateMenus(self):
+        """
+        Call this to update menu labels whenever the current locale
+        changes.
+        """
+
+        if not self.i18n:
+            return
+
+        for k, v in self.MBIds.items():
+            # Update top-level menus
+            if not v.GetParent():
+                v.Update()
+                self.SetLabelTop(k, v.GetRealLabel(self.i18n))
+            # Update other menu items
+            else:
+                item = self.FindItemById(k)
+                if item is not None:   # Skip separators
+                    v.Update()
+                    self.SetLabel(k, v.GetRealLabel(self.i18n))
+
+
+    def GetMenuState(self, menu_string):
+        """Returns True if a checkable menu item is checked."""
+
+        this = self.MBStrings[menu_string]
+        return self.IsChecked(this)
+
+
+    def SetMenuState(self, menu_string, check=True):
+        """Toggles a checkable menu item checked or unchecked."""
+
+        this = self.MBStrings[menu_string]
+        self.Check(this, check)
+
+
+    def EnableItem(self, menu_string, enable=True):
+        """Enables or disables a menu item via its label."""
+
+        this = self.MBStrings[menu_string]
+        self.Enable(this, enable)
+
+
+    def EnableItems(self, menu_string_list, enable=True):
+        """Enables or disables menu items via a list of labels."""
+
+        for menu_string in menu_string_list:
+            self.EnableItem(menu_string, enable)
+
+
+    def EnableTopMenu(self, menu_string, enable=True):
+        """Enables or disables a top level menu via its label."""
+
+        this = self.MBStrings[menu_string]
+        self.EnableTop(this, enable)
+
+
+    def EnableTopMenus(self, menu_string_list, enable=True):
+        """Enables or disables top level menus via a list of labels."""
+
+        for menu_string in menu_string_list:
+            self.EnableTopMenu(menu_string, enable)
+
+
+# MenuEx Main stuff ---------------------------------------------------------
+
+class MenuEx(wx.Menu):
+    def __init__(self, *args, **kwargs):
+        """
+        Constructor.
+
+        MenuEx(parent, menu, margin=wx.DEFAULT, font=wx.NullFont,
+               show_title=True, custfunc={}, i18n=True, style=0)
+        """
+
+        # Initializing...
+        self.parent, menu = args
+        margin = kwargs.pop("margin", wx.DEFAULT)
+        font = kwargs.pop("font", wx.NullFont)
+        show_title = kwargs.pop("show_title", True)
+        custfunc = kwargs.pop("custfunc", {})
+        i18n = self.i18n = kwargs.pop("i18n", True)
+
+        wx.Menu.__init__(self, **kwargs)
+
+        self._title = menu[0][0]
+        if show_title:
+            if i18n:
+                self.SetTitle(wx.GetTranslation(self._title))
+            else:
+                self.SetTitle(self._title)
+
+        # Parse the menu 'tree' supplied.
+        top = _evolve(menu)
+
+        # Create these menus first...
+        wxmenus = {top: self}
+        for k in top.GetChildWithChildren():
+            wxmenus[k] = wx.Menu()
+
+            # ...and append their respective children.
+            for h in k.GetChildren():
+                _makeMenus(wxmenus, None, h, k, margin, font, i18n)
+
+        # Now append these items to the top level menu.
+        for h in top.GetChildren():
+            _makeMenus(wxmenus, None, h, top, margin, font, i18n)
+
+        # Now find out what are the methods that should be called upon
+        # menu items selection.
+        self.MenuIds = {}; self.MenuStrings = {}; self.MenuList = []
+        for child in top.GetChildren(True):
+            Id = child.GetId(); item = self.FindItemById(Id)
+            if item:
+                child.SetMethod(_prefixM, custfunc)
+                self.MenuIds[Id] = child
+                self.MenuStrings.update(child.GetAllMethods())
+                self.MenuList.append([Id, child.GetPath()])
+
+        # Initialize menu states.
+        self.MenuState = {}
+        for Id in self.MenuIds.keys():
+            if self.FindItemById(Id).IsCheckable():
+                is_checked = self.IsChecked(Id)
+            else:
+                is_checked = False
+            self.MenuState[Id] = is_checked
+
+        # Nice class. 8^) Will take care of this automatically.
+        self.parent.Bind(wx.EVT_MENU, self.OnM_)
+
+
+    def _update(self, i):
+        def _resetRadioGroup(i):
+            g = []; n = []
+
+            for Id, s in self.MenuList:
+                item = self.FindItemById(Id)
+                if item.GetKind() == wx.ITEM_RADIO:
+                    g.append(Id)
+                else:
+                    g.append(None)
+
+            for x in range(g.index(i), 0, -1):
+                if g[x] != None:
+                    n.append(g[x])
+                else:
+                    break
+
+            for x in range(g.index(i) + 1, len(g)):
+                if g[x] != None:
+                    n.append(g[x])
+                else:
+                    break
+
+            for i in n:
+                self.MenuState[i] = False
+
+        kind = self.FindItemById(i).GetKind()
+
+        if kind == wx.ITEM_CHECK:
+            self.MenuState[i] = not self.IsChecked(i)
+
+        elif kind == wx.ITEM_RADIO:
+            _resetRadioGroup(i)
+            self.MenuState[i] = True
+
+
+    def OnM_(self, evt):
+        """
+        Called on all menu events for this menu. It will in turn call
+        the related method on parent, if any.
+        """
+
+        try:
+            attr = self.MenuIds[evt.GetId()]
+
+            self.OnM_before()
+
+            if isinstance(attr, _sItem):
+                attr_name = attr.GetMethod()
+
+                if callable(attr_name):
+                    attr_name()
+                elif hasattr(self.parent, attr_name) and \
+                     callable(getattr(self.parent, attr_name)):
+                    getattr(self.parent, attr_name)()
+                else:
+                    print "%s not found in parent." % attr_name
+
+            self.OnM_after()
+
+        except KeyError:
+            # Maybe another menu was triggered elsewhere in parent.
+            pass
+
+        #evt.Skip() - removed. see http://www.archivum.info/comp.soft-sys.wxwindows/2008-07/msg00027.html
+
+
+    def OnM_before(self):
+        """
+        If you need to execute something right before a menu event is
+        triggered, you can bind the EVT_BEFOREMENU.
+        """
+
+        evt = MenuExBeforeEvent(obj=self)
+        wx.PostEvent(self, evt)
+
+
+    def OnM_after(self):
+        """
+        If you need to execute something right after a menu event is
+        triggered, you can bind the EVT_AFTERMENU.
+        """
+
+        evt = MenuExAfterEvent(obj=self)
+        wx.PostEvent(self, evt)
+
+
+    # Public methods --------------------------------------------------------
+
+    def UpdateMenus(self):
+        """
+        Call this to update menu labels whenever the current locale
+        changes.
+        """
+
+        if not self.i18n:
+            return
+
+        for k, v in MenuIds.items():
+            item = self.FindItemById(k)
+            if item is not None:   # Skip separators
+                v.Update()
+                self.SetLabel(k, v.GetRealLabel(self.i18n))
+
+
+    def Popup(self, evt):
+        """Pops this menu up."""
+
+        [self.Check(i, v) for i, v in self.MenuState.items() \
+         if self.FindItemById(i).IsCheckable()]
+
+        obj = evt.GetEventObject(); pos = evt.GetPosition()
+        obj.PopupMenu(self, pos)
+
+
+    def GetItemState(self, menu_string):
+        """Returns True if the item is checked."""
+
+        this = self.MenuStrings[menu_string]
+        return self.IsChecked(this)
+
+
+    def SetItemState(self, menu_string, check):
+        """Toggles a checkable menu item checked or unchecked."""
+
+        this = self.MenuStrings[menu_string]
+        self.MenuState[this] = check
+
+
+    def EnableItem(self, menu_string, enable=True):
+        """Enables or disables a menu item via its label."""
+
+        this = self.MenuStrings[menu_string]
+        self.Enable(this, enable)
+
+
+    def EnableItems(self, menu_string_list, enable=True):
+        """Enables or disables menu items via a list of labels."""
+
+        for menu_string in menu_string_list:
+            self.EnableItem(menu_string, enable)
+
+
+    def EnableAllItems(self, enable=True):
+        """Enables or disables all menu items."""
+
+        for Id in self.MenuIds.keys():
+            self.Enable(Id, enable)
+
+#----------------------------------------------------------------------------
+
+if __name__ == "__main__":
+    import sys, os.path
+    args = sys.argv[1:]
+    if len(args) == 2:
+        _mmprep(*[os.path.splitext(arg)[0] for arg in args])
+    else:
+        print """
+-----------------------------------------------------------------------------
+metamenus %s
+
+%s
+%s
+Distributed under the wxWidgets license.
+-----------------------------------------------------------------------------
+
+Usage:
+------
+
+metamenus.py separate_file outputfile
+
+- 'separate_file' is the python file containing the menu 'trees';
+- 'outputfile' is the output file generated that can be parsed by the gettext
+  utilities.
+
+Please see metamenus.__doc__ (under the 'More about i18n' section) and
+metamenus._mmprep.__doc__ for more details.
+-----------------------------------------------------------------------------
+""" % (__version__, __author__, __date__)
+
+
+#
+##
+### eof
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/orpg_log.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,64 @@
+# 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: orpg_log.py
+# Author: Dj Gilcrease
+# Maintainer:
+# Version:
+#   $Id: orpg_log.py,v 1.9 2007/05/06 16:43:02 digitalxero Exp $
+#
+# Description: classes for orpg log messages
+#
+
+from orpg.orpgCore import *
+
+class orpgLog:
+    def __init__(self, home_dir, filename='orpgRunLog '):
+        self.logToConsol = True
+        self.logLevel = 7
+        self.logName = home_dir + filename + time.strftime( '%m-%d-%Y.txt', time.localtime( time.time() ) )
+
+    def log(self, msg, type, to_consol=False):
+        if self.logToConsol or to_consol or type == ORPG_CRITICAL:
+            print msg
+
+        if type & self.logLevel:
+        #if type & self.logLevel or to_consol: #Arbitrary removal TaS.
+            logMsg = time.strftime( '[%x %X] ', time.localtime( time.time() ) ) + msg + "\n"
+            logFile = open(self.logName, "a")
+            logFile.write(logMsg)
+            logFile.close()
+
+    def setLogLevel(self, log_level):
+        self.logLevel = log_level
+
+    def getLogLevel(self):
+        return self.logLevel
+
+    def setLogName(self, log_name):
+        self.logName = log_name
+
+    def getLogName(self):
+        return self.logName
+
+    def setLogToConsol(self, bool):
+        self.logToConsol = bool
+
+    def getLogToConsol(self):
+        return self.logToConsol
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/orpg_settings.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,409 @@
+# 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: orpg_settings.py
+# Author: Dj Gilcrease
+# Maintainer:
+# Version:
+#   $Id: orpg_settings.py,v 1.51 2007/07/15 14:25:12 digitalxero Exp $
+#
+# Description: classes for orpg settings
+#
+
+from orpg.orpg_windows import *
+import orpg.dirpath
+from rgbhex import *
+import sys
+import os
+
+class orpgSettings:
+    def __init__(self):
+        self.validate = open_rpg.get_component("validate")
+        self.xml = open_rpg.get_component("xml")
+        self.log = open_rpg.get_component("log")
+        self.changes = []
+        self.validate.config_file("settings.xml","default_settings.xml")
+        self.filename = orpg.dirpath.dir_struct["user"] + "settings.xml"
+        temp_file = open(self.filename)
+        txt = temp_file.read()
+        temp_file.close()
+        self.xml_dom = self.xml.parseXml(txt)
+
+        if self.xml_dom is None:
+            self.rebuildSettings()
+        self.xml_dom = self.xml_dom._get_documentElement()
+
+    def rebuildSettings(self):
+        self.log.log("Settings file has be corrupted, rebuilding settings.", ORPG_INFO, True)
+        try:
+            os.remove(self.filename)
+        except:
+            pass
+
+        self.validate.config_file("settings.xml","default_settings.xml")
+        temp_file = open(self.filename)
+        txt = temp_file.read()
+        temp_file.close()
+        self.xml_dom = self.xml.parseXml(txt)
+
+    def get_setting(self, name):
+        try:
+            return self.xml_dom.getElementsByTagName(name)[0].getAttribute("value")
+        except:
+            return 0
+
+    def get_setting_keys(self):
+        keys = []
+        tabs = self.xml_dom.getElementsByTagName("tab")
+        for i in xrange(0, len(tabs)):
+            if tabs[i].getAttribute("type") == 'grid':
+                children = tabs[i]._get_childNodes()
+                for c in children:
+                    keys.append(c._get_tagName())
+        return keys
+
+    def set_setting(self, name, value):
+        self.xml_dom.getElementsByTagName(name)[0].setAttribute("value", value)
+
+    def add_setting(self, tab, setting, value, options, help):
+        if len(self.xml_dom.getElementsByTagName(setting)) > 0:
+            return False
+        tabs = self.xml_dom.getElementsByTagName("tab")
+        newsetting = self.xml.parseXml('<' + setting + ' value="' + value + '" options="' + options + '" help="' + help + '" />')._get_documentElement()
+        for i in xrange(0, len(tabs)):
+            if tabs[i].getAttribute("name") == tab and tabs[i].getAttribute("type") == 'grid':
+                tabs[i].appendChild(newsetting)
+                return True
+        return False
+
+    def add_tab(self, parent, tabname, tabtype):
+        tab_xml = '<tab '
+        if tabtype == 'text':
+            tab_xml += 'name="' + tabname + '" type="text" />'
+        else:
+            tab_xml += 'name="' + tabname + '" type="' + tabtype + '"></tab>'
+        newtab = self.xml.parseXml(tab_xml)._get_documentElement()
+        if parent != None:
+            tabs = self.xml_dom.getElementsByTagName("tab")
+            for i in xrange(0, len(tabs)):
+                if tabs[i].getAttribute("name") == parent and tabs[i].getAttribute("type") == 'tab':
+                    children = tabs[i]._get_childNodes()
+                    for c in children:
+                        if c.getAttribute("name") == tabname:
+                            return False
+                    tabs[i].appendChild(newtab)
+                    return True
+        else:
+            children = self.xml_dom._get_childNodes()
+            for c in children:
+                if c.getAttribute("name") == tabname:
+                    return False
+            self.xml_dom.appendChild(newtab)
+            return True
+        return False
+
+    def updateIni(self):
+        defaultFile = orpg.dirpath.dir_struct['template'] + 'default_settings.xml'
+        temp_file = open(defaultFile)
+        txt = temp_file.read()
+        temp_file.close()
+        default_dom = self.xml.parseXml(txt)._get_documentElement()
+        for child in default_dom.getChildren():
+            if child._get_tagName() == 'tab' and child.hasChildNodes():
+                self.proccessChildren(child)
+        default_dom.unlink()
+
+    def proccessChildren(self, dom, parent=None):
+        if dom._get_tagName() == 'tab':
+            self.add_tab(parent, dom.getAttribute("name"), dom.getAttribute("type"))
+
+        for child in dom.getChildren():
+            if child._get_tagName() == 'tab' and child.hasChildNodes():
+                self.proccessChildren(child, dom.getAttribute("name"))
+            else:
+                self.add_setting(dom.getAttribute("name"), child._get_tagName(), child.getAttribute("value"), child.getAttribute("options"), child.getAttribute("help"))
+
+    def save(self):
+        temp_file = open(self.filename, "w")
+        temp_file.write(self.xml.toxml(self.xml_dom,1))
+        temp_file.close()
+
+class orpgSettingsWnd(wx.Dialog):
+    def __init__(self, parent):
+        wx.Dialog.__init__(self,parent,-1,"OpenRPG Preferences",wx.DefaultPosition,size = wx.Size(-1,-1), style=wx.RESIZE_BORDER | wx.SYSTEM_MENU | wx.CAPTION)
+        self.Freeze()
+        self.validate = open_rpg.get_component("validate")
+        self.settings = open_rpg.get_component("settings")
+        self.chat = open_rpg.get_component("chat")
+        self.changes = []
+        self.SetMinSize((545,500))
+        self.tabber = orpgTabberWnd(self, style=FNB.FNB_NO_X_BUTTON)
+        self.build_gui()
+        self.tabber.SetSelection(0)
+        winsizer = wx.BoxSizer(wx.VERTICAL)
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(wx.Button(self, wx.ID_OK, "OK"), 1, wx.EXPAND)
+        sizer.Add(wx.Size(10,10))
+        sizer.Add(wx.Button(self, wx.ID_CANCEL, "Cancel"), 1, wx.EXPAND)
+        winsizer.Add(self.tabber, 1, wx.EXPAND)
+        winsizer.Add(sizer, 0, wx.EXPAND | wx.ALIGN_BOTTOM)
+        self.winsizer = winsizer
+        self.SetSizer(self.winsizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+        self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
+        self.Thaw()
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        self.winsizer.SetDimension(0,0,w,h-25)
+
+    def build_gui(self):
+        self.validate.config_file("settings.xml","default_settings.xml")
+        filename = open_rpg.get_component("dir_struct")["user"] + "settings.xml"
+        temp_file = open(filename)
+        temp_file.close()
+        children = self.settings.xml_dom._get_childNodes()
+        for c in children:
+            self.build_window(c,self.tabber)
+
+    def build_window(self, xml_dom, parent_wnd):
+        name = xml_dom._get_nodeName()
+        #container = 0
+        if name=="tab":
+            temp_wnd = self.do_tab_window(xml_dom,parent_wnd)
+        return temp_wnd
+
+    def do_tab_window(self, xml_dom, parent_wnd):
+        type = xml_dom.getAttribute("type")
+        name = xml_dom.getAttribute("name")
+
+        if type == "grid":
+            temp_wnd = self.do_grid_tab(xml_dom, parent_wnd)
+            parent_wnd.AddPage(temp_wnd, name, False)
+        elif type == "tab":
+            temp_wnd = orpgTabberWnd(parent_wnd, style=FNB.FNB_NO_X_BUTTON)
+            children = xml_dom._get_childNodes()
+            for c in children:
+                if c._get_nodeName() == "tab":
+                    self.do_tab_window(c, temp_wnd)
+            temp_wnd.SetSelection(0)
+            parent_wnd.AddPage(temp_wnd, name, False)
+        elif type == "text":
+            temp_wnd = wx.TextCtrl(parent_wnd,-1)
+            parent_wnd.AddPage(temp_wnd, name, False)
+        else:
+            temp_wnd = None
+        return temp_wnd
+
+    def do_grid_tab(self, xml_dom, parent_wnd):
+        settings = []
+        children = xml_dom._get_childNodes()
+        for c in children:
+            name = c._get_nodeName()
+            value = c.getAttribute("value")
+            help = c.getAttribute("help")
+            options = c.getAttribute("options")
+            settings.append([name, value, options, help])
+        temp_wnd = settings_grid(parent_wnd, settings, self.changes)
+        return temp_wnd
+
+    def onOk(self, evt):
+        #This will write the settings back to the XML
+        self.session = open_rpg.get_component("session")
+        tabbedwindows = open_rpg.get_component("tabbedWindows")
+        new = []
+        for wnd in tabbedwindows:
+            try:
+                style = wnd.GetWindowStyleFlag()
+                new.append(wnd)
+            except:
+                pass
+        tabbedwindows = new
+        open_rpg.add_component("tabbedWindows", tabbedwindows)
+        rgbconvert = RGBHex()
+
+        for i in xrange(0,len(self.changes)):
+            self.settings.set_setting(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':
+                self.chat.chatwnd.SetPage(self.chat.ResetPage())
+                self.chat.chatwnd.scroll_down()
+                if self.settings.get_setting('ColorTree') == '1':
+                    top_frame.tree.SetBackgroundColour(self.settings.get_setting('bgcolor'))
+                    top_frame.tree.SetForegroundColour(self.settings.get_setting('textcolor'))
+                    top_frame.tree.Refresh()
+                    top_frame.players.SetBackgroundColour(self.settings.get_setting('bgcolor'))
+                    top_frame.players.SetForegroundColour(self.settings.get_setting('textcolor'))
+                    top_frame.players.Refresh()
+                else:
+                    top_frame.tree.SetBackgroundColour('white')
+                    top_frame.tree.SetForegroundColour('black')
+                    top_frame.tree.Refresh()
+                    top_frame.players.SetBackgroundColour('white')
+                    top_frame.players.SetForegroundColour('black')
+                    top_frame.players.Refresh()
+
+            if self.changes[i][0] == 'ColorTree':
+                if self.changes[i][1] == '1':
+                    top_frame.tree.SetBackgroundColour(self.settings.get_setting('bgcolor'))
+                    top_frame.tree.SetForegroundColour(self.settings.get_setting('textcolor'))
+                    top_frame.tree.Refresh()
+                    top_frame.players.SetBackgroundColour(self.settings.get_setting('bgcolor'))
+                    top_frame.players.SetForegroundColour(self.settings.get_setting('textcolor'))
+                    top_frame.players.Refresh()
+                else:
+                    top_frame.tree.SetBackgroundColour('white')
+                    top_frame.tree.SetForegroundColour('black')
+                    top_frame.tree.Refresh()
+                    top_frame.players.SetBackgroundColour('white')
+                    top_frame.players.SetForegroundColour('black')
+                    top_frame.players.Refresh()
+
+            if self.changes[i][0] == 'GMWhisperTab' and self.changes[i][1] == '1':
+                self.chat.parent.create_gm_tab()
+            self.toggleToolBars(self.chat, self.changes[i])
+            try:
+                self.toggleToolBars(self.chat.parent.GMChatPanel, self.changes[i])
+            except:
+                pass
+            for panel in self.chat.parent.whisper_tabs:
+                self.toggleToolBars(panel, self.changes[i])
+            for panel in self.chat.parent.group_tabs:
+                self.toggleToolBars(panel, self.changes[i])
+            for panel in self.chat.parent.null_tabs:
+                self.toggleToolBars(panel, self.changes[i])
+
+            if self.changes[i][0] == 'player':
+                self.session.name = self.changes[i][1]
+
+            if (self.changes[i][0] == 'TabTheme' and (self.changes[i][1] == 'customflat' or self.changes[i][1] == 'customslant')) or\
+                (self.changes[i][0] == 'TabTextColor' and (self.settings.get_setting('TabTheme') == 'customflat' or self.settings.get_setting('TabTheme') == 'customslant')) or\
+                (self.changes[i][0] == 'TabGradientFrom' and (self.settings.get_setting('TabTheme') == 'customflat' or self.settings.get_setting('TabTheme') == 'customslant')) or\
+                (self.changes[i][0] == 'TabGradientTo' and (self.settings.get_setting('TabTheme') == 'customflat' or self.settings.get_setting('TabTheme') == 'customslant')):
+                gfrom = self.settings.get_setting('TabGradientFrom')
+                (fred, fgreen, fblue) = rgbconvert.rgb_tuple(gfrom)
+
+                gto = self.settings.get_setting('TabGradientTo')
+                (tored, togreen, toblue) = rgbconvert.rgb_tuple(gto)
+
+                tabtext = self.settings.get_setting('TabTextColor')
+                (tred, tgreen, tblue) = rgbconvert.rgb_tuple(tabtext)
+                for wnd in tabbedwindows:
+                    style = wnd.GetWindowStyleFlag()
+                    # remove old tabs style
+                    mirror = ~(FNB.FNB_VC71 | FNB.FNB_VC8 | FNB.FNB_FANCY_TABS | FNB.FNB_COLORFUL_TABS)
+                    style &= mirror
+                    if self.settings.get_setting('TabTheme') == 'customslant':
+                        style |= FNB.FNB_VC8
+                    else:
+                        style |= FNB.FNB_FANCY_TABS
+                    wnd.SetWindowStyleFlag(style)
+                    wnd.SetGradientColourTo(wx.Color(tored, togreen, toblue))
+                    wnd.SetGradientColourFrom(wx.Color(fred, fgreen, fblue))
+                    wnd.SetNonActiveTabTextColour(wx.Color(tred, tgreen, tblue))
+                    wnd.Refresh()
+
+            if self.changes[i][0] == 'TabBackgroundGradient':
+                for wnd in tabbedwindows:
+                    (red, green, blue) = rgbconvert.rgb_tuple(self.changes[i][1])
+                    wnd.SetTabAreaColour(wx.Color(red, green, blue))
+                    wnd.Refresh()
+        self.settings.save()
+        self.Destroy()
+
+    def toggleToolBars(self, panel, changes):
+        if changes[0] == 'AliasTool_On':
+            panel.toggle_alias(changes[1])
+        elif changes[0] == 'DiceButtons_On':
+            panel.toggle_dice(changes[1])
+        elif changes[0] == 'FormattingButtons_On':
+            panel.toggle_formating(changes[1])
+
+class settings_grid(wx.grid.Grid):
+    """grid for gen info"""
+    def __init__(self, parent, settings, changed = []):
+        wx.grid.Grid.__init__(self, parent, -1, style=wx.SUNKEN_BORDER | wx.WANTS_CHARS)
+        self.setting_data = changed
+        self.Bind(wx.EVT_SIZE, self.on_size)
+        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.on_cell_change)
+        self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.on_left_click)
+        self.CreateGrid(len(settings),3)
+        self.SetRowLabelSize(0)
+        self.SetColLabelValue(0,"Setting")
+        self.SetColLabelValue(1,"Value")
+        self.SetColLabelValue(2,"Available Options")
+        self.settings = settings
+        for i in range(len(settings)):
+            self.SetCellValue(i,0,settings[i][0])
+            self.SetCellValue(i,1,settings[i][1])
+            if settings[i][1] and settings[i][1][0] == '#':
+                self.SetCellBackgroundColour(i,1,settings[i][1])
+            self.SetCellValue(i,2,settings[i][2])
+
+    def on_left_click(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        if col == 2:
+            return
+        elif col == 0:
+            name = self.GetCellValue(row,0)
+            str = self.settings[row][3]
+            msg = wx.MessageBox(str,name)
+            return
+        elif col == 1:
+            setting = self.GetCellValue(row,0)
+            value = self.GetCellValue(row,1)
+            if value and value[0] == '#':
+                hexcolor = orpg.tools.rgbhex.RGBHex().do_hex_color_dlg(self)
+                if hexcolor:
+                    self.SetCellValue(row,2, hexcolor)
+                    self.SetCellBackgroundColour(row,1,hexcolor)
+                    self.Refresh()
+                    setting = self.GetCellValue(row,0)
+                    self.setting_data.append([setting, hexcolor])
+            else:
+                evt.Skip()
+
+    def on_cell_change(self,evt):
+        row = evt.GetRow()
+        col = evt.GetCol()
+        if col != 1:
+            return
+        setting = self.GetCellValue(row,0)
+        value = self.GetCellValue(row,1)
+        self.setting_data.append([setting, value])
+
+    def get_h(self):
+        (w,h) = self.GetClientSizeTuple()
+        rows = self.GetNumberRows()
+        minh = 0
+        for i in range (0,rows):
+            minh += self.GetRowSize(i)
+        minh += 120
+        return minh
+
+    def on_size(self,evt):
+        (w,h) = self.GetClientSizeTuple()
+        cols = self.GetNumberCols()
+        col_w = w/(cols)
+        for i in range(0,cols):
+            self.SetColSize(i,col_w)
+        self.Refresh()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/orpg_sound.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,278 @@
+from orpg.orpgCore import *
+from orpg.orpg_wx import *
+
+
+class orpgSound(wx.Panel):
+    def __init__(self, parent):
+        wx.Panel.__init__(self, parent, -1)
+        self.parent = parent
+
+        self.log = open_rpg.get_component("log")
+        self.settings = open_rpg.get_component('settings')
+
+        self.log.log("Enter orpgSound", ORPG_DEBUG)
+
+        # Create some controls
+        try:
+            self.mc = wx.media.MediaCtrl(self, wx.ID_ANY, size=(1,1))
+            self.OldPlayer = False
+            self.Bind(wx.media.EVT_MEDIA_LOADED, self.OnMediaLoaded)
+            self.log.log("wx.media Used", ORPG_DEBUG)
+        except NotImplementedError:
+            self.OldPlayer = True
+            self.log.log("Old Player Used", ORPG_DEBUG)
+        else:
+            btn2 = wx.Button(self, -1, "Play")
+            self.Bind(wx.EVT_BUTTON, self.OnPlay, btn2)
+            self.playBtn = btn2
+            self.playBtn.Disable()
+
+            btn3 = wx.Button(self, -1, "stop")
+            self.Bind(wx.EVT_BUTTON, self.OnStop, btn3)
+            self.stopBtn = btn3
+
+            self.playList = wx.Choice(self, wx.ID_ANY)
+            self.playDict = {}
+
+            self.pslider = wx.Slider(self, wx.ID_ANY, 0, 0, 100, size=wx.Size(100, -1))
+            self.Bind(wx.EVT_SLIDER, self.onSeek, self.pslider)
+
+            self.pos = wx.StaticText(self, wx.ID_ANY, "Position:")
+
+            self.vol = wx.StaticText(self, wx.ID_ANY, "Volume:")
+            self.vslider = wx.Slider(self, wx.ID_ANY, 100, 0, 100, size=wx.Size(50, -1))
+            self.Bind(wx.EVT_SLIDER, self.onVol, self.vslider)
+
+            self.log.log("Buttons Created", ORPG_DEBUG)
+
+            self.loopSound = False
+            self.seeking = False
+            self.lastlen = 0
+
+            # setup the layout
+            sizer = wx.GridBagSizer(hgap=1, vgap=1)
+            sizer.Add(self.mc, (0,20))
+            sizer.Add(self.playList, (0,0), span=(1,2))
+            sizer.Add(btn2, (0,2))
+            sizer.Add(btn3, (0,3))
+            sizer.Add(self.pos, (1,0), flag=wx.ALIGN_CENTER)
+            sizer.Add(self.pslider, (1,1), flag=wx.EXPAND)
+            sizer.Add(self.vol, (1,2), flag=wx.ALIGN_CENTER)
+            sizer.Add(self.vslider, (1,3), flag=wx.EXPAND)
+            sizer.AddGrowableCol(0)
+            sizer.AddGrowableCol(3)
+            sizer.SetEmptyCellSize((0,0))
+
+            self.SetSizer(sizer)
+            self.SetAutoLayout(True)
+
+            self.log.log("Sizer Set", ORPG_DEBUG)
+
+            self.Fit()
+
+            self.Bind(wx.EVT_CHOICE, self.PlaySelected, self.playList)
+            self.Bind(wx.EVT_CLOSE, self.OnClose)
+            self.timer = wx.Timer(self, wx.NewId())
+            self.Bind(wx.EVT_TIMER, self.OnTimer)
+            self.timer.Start(100)
+
+            self.log.log("Events Bound", ORPG_DEBUG)
+
+        self.log.log("Exit orpgSound", ORPG_DEBUG)
+
+    def play(self, sound_file, type="local", loop_sound=False):
+        self.log.log("Enter orpgSound->play(self, sound_file, type, loop_sound)", ORPG_DEBUG)
+
+        if self.OldPlayer:
+            if wx.Platform == '__WXMSW__':
+                import winsound
+                self.play_windows(sound_file.replace('&amp;', '&'))
+            elif wx.Platform == '__WXGTK__':
+                self.play_unix(sound_file.replace('&amp;', '&'))
+        else:
+            self.loopSound = loop_sound
+            self.LoadSound(sound_file.replace('&amp;', '&'), type)
+
+        self.log.log("Exit orpgSound->play(self, sound_file, type, loop_sound)", ORPG_DEBUG)
+
+    def play_windows(self,sound_file):
+        self.log.log("Enter orpgSound->play_windows(self,sound_file)", ORPG_DEBUG)
+
+        winsound.PlaySound(sound_file, winsound.SND_FILENAME)
+
+        self.log.log("Exit orpgSound->play_windows(self,sound_file)", ORPG_DEBUG)
+
+    def play_unix(self, sound_file):
+        self.log.log("Enter orpgSound->play_unix(self, sound_file)", ORPG_DEBUG)
+
+        unix_player = self.settings.get_setting('UnixSoundPlayer')
+        if unix_player != '':
+            os.system(unix_player + " " + sound_file)
+
+        self.log.log("Exit orpgSound->play_unix(self, sound_file)", ORPG_DEBUG)
+
+    def onSeek(self, evt):
+        self.log.log("Enter orpgSound->onSeek(self, evt)", ORPG_DEBUG)
+
+        offset = self.pslider.GetValue()
+        self.mc.Seek(offset)
+
+        self.log.log("Exit orpgSound->onSeek(self, evt)", ORPG_DEBUG)
+
+    def PlaySelected(self, evt):
+        self.log.log("Enter orpgSound->PlaySelected(self, evt)", ORPG_DEBUG)
+
+        name = self.playList.GetStringSelection()
+        info = self.playDict[name]
+        sound_file = info[0]
+        pos = info[1]
+
+        self.mc.LoadURI(sound_file)
+
+        pane = self.parent._mgr.GetPane("Sound Control Toolbar")
+        pane.window.SetInitialSize()
+        pane.BestSize(pane.window.GetEffectiveMinSize() + (0, 1))
+        self.parent._mgr.Update()
+
+        if pos > 0:
+            self.seeking = True
+            wx.CallAfter(self.mc.Pause)
+
+        self.log.log("Exit orpgSound->PlaySelected(self, evt)", ORPG_DEBUG)
+
+    def LoadSound(self, sound_file, type="local"):
+        self.log.log("Enter orpgSound->LoadSound(self, sound_file, type)", ORPG_DEBUG)
+
+        if self.mc.GetState() == wx.media.MEDIASTATE_PLAYING:
+            pos = self.mc.Tell()
+            len = self.mc.Length()
+            self.mc.Stop()
+            if (len-pos) <= 2000:
+                pos = 0
+            pname = self.playList.GetStringSelection()
+            self.playDict[pname][1] = pos
+
+        (path, name) = os.path.split(sound_file)
+        if type != "local":
+            self.mc.LoadURI(sound_file)
+        else:
+            self.mc.Load(sound_file)
+
+        if not self.playDict.has_key(name):
+            self.playList.Append(name)
+
+        pane = self.parent._mgr.GetPane("Sound Control Toolbar")
+        pane.window.SetInitialSize()
+        pane.BestSize(pane.window.GetEffectiveMinSize() + (0, 1))
+        self.parent._mgr.Update()
+
+        self.playDict[name] = [sound_file, 0]
+        self.playList.SetStringSelection(name)
+
+        self.log.log("Exit orpgSound->LoadSound(self, sound_file, type)", ORPG_DEBUG)
+        return
+
+    def OnMediaLoaded(self, evt):
+        self.log.log("Enter orpgSound->OnMediaLoaded(self, evt)", ORPG_DEBUG)
+
+        self.mc.Play()
+        self.playBtn.Enable()
+
+        self.log.log("Exit orpgSound->OnMediaLoaded(self, evt)", ORPG_DEBUG)
+
+    def OnPlay(self, evt):
+        self.log.log("Enter orpgSound->OnPlay(self, evt)", ORPG_DEBUG)
+
+        self.mc.Play()
+
+        self.log.log("Exit orpgSound->OnPlay(self, evt)", ORPG_DEBUG)
+
+    def OnStop(self, evt):
+        self.log.log("Enter orpgSound->OnStop(self, evt)", ORPG_DEBUG)
+
+        self.loopSound = False
+        self.mc.Stop()
+
+        self.log.log("Exit orpgSound->OnStop(self, evt)", ORPG_DEBUG)
+
+    def OnClose(self, evt):
+        self.log.log("Enter orpgSound->OnClose(self, evt)", ORPG_DEBUG)
+
+        self.timer.Stop()
+        del self.timer
+
+        self.log.log("Exit orpgSound->OnClose(self, evt)", ORPG_DEBUG)
+
+    def __del__(self):
+        self.timer.Stop()
+
+    def onVol(self, evt):
+        self.log.log("Enter orpgSound->onVol(self, evt)", ORPG_DEBUG)
+
+        vol = float(self.vslider.GetValue())/100
+        self.mc.SetVolume(vol)
+
+        self.log.log("Exit orpgSound->onVol(self, evt)", ORPG_DEBUG)
+
+    def OnTimer(self, args):
+        if not self.OldPlayer:
+            if self.mc.GetState() == wx.media.MEDIASTATE_PLAYING and not self.stopBtn.IsEnabled():
+                self.stopBtn.Enable()
+            elif self.mc.GetState() != wx.media.MEDIASTATE_PLAYING and self.stopBtn.IsEnabled():
+                self.stopBtn.Disable()
+
+            if self.mc.Length() > 0 and self.lastlen != self.mc.Length():
+                self.pslider.SetRange(0, self.mc.Length())
+                self.lastlen = self.mc.Length()
+
+            if not self.seeking:
+                self.pslider.SetValue(self.mc.Tell())
+
+            if self.mc.GetState() != wx.media.MEDIASTATE_PLAYING and self.loopSound:
+                self.mc.Play()
+
+            if self.seeking:
+                name = self.playList.GetStringSelection()
+                info = self.playDict[name]
+
+                if self.mc.Tell() >= info[1]:
+                    self.seeking = False
+
+                else:
+                    self.pslider.SetValue(info[1])
+                    wx.CallAfter(self.onSeek, None)
+
+        elif self.stopBtn.IsShown():
+            self.playBtn.Hide()
+            self.stopBtn.Hide()
+        return
+
+class SoundFrame(wx.Frame):
+    def __init__(self, openrpg):
+        wx.Frame.__init__(self, None, title="Sound Control Toolbar", style=wx.CAPTION | wx.SYSTEM_MENU | wx.STAY_ON_TOP)
+
+        self.soundCtrl = orpgSound(self, openrpg)
+
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(self.soundCtrl, 1, wx.EXPAND)
+        self.SetSizer(sizer)
+        self.SetSizer(sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+
+
+class BlankApp(wx.App):
+    def OnInit(self):
+        self.frame = SoundFrame()
+        self.frame.Show()
+        self.SetTopWindow(self.frame)
+        return True
+
+    def OnChar(self, event):
+        #Not sure how to determin if only Tab or Shift Tab was used to initiate
+        self.tabgroup.GoNext( self.frame1.FindFocus() )
+
+if __name__ == "__main__":
+    app = BlankApp(0)
+    app.MainLoop()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/passtool.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,181 @@
+# 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: passtool.py
+# Author: Todd "Snowdog" Faris
+# Maintainer:
+# Version:
+#   $Id: passtool.py,v 1.9 2006/11/04 21:24:22 digitalxero Exp $
+#
+# Description: password helper. remembers passwords so user
+#              doesn't have to type passwords over and over
+
+import orpg.orpg_windows
+from orpg.orpgCore import open_rpg
+import traceback
+
+#####################
+## Password Assistant
+#####################
+class PassSet:
+    "set of passwords for a given room id on a server"
+    def __init__(self):
+        #room admin password (aka boot password)
+        self.admin = None
+
+        #room password
+        self.room = None
+
+
+
+class PassTool:
+    "Password Management System"
+    def __init__(self):
+        self.settings = open_rpg.get_component("settings")
+        #server admin password
+        self.server = None
+        self.groups = {}
+        if self.settings.get_setting('PWMannager') == 'On':
+            self.enabled = 1
+        else:
+            self.enabled = 0
+
+
+    def DumpPasswords(self):
+        "Debugging Routine"
+        print("Password Manager Dump\nServer: \""+self.server+"\"")
+        for c in self.groups:
+            ad = self.groups[c].admin
+            rm = self.groups[c].room
+            print( " #"+str(c)+"  R:\""+str(rm)+"\"  A:\""+str(ad)+"\"")
+
+    def ClearPassword( self, type="ALL", groupid=0):
+        if type == "ALL" and groupid == 0:
+            self.server = None
+            self.groups={}
+        elif type == "ALL":
+            self.groups[ int(groupid) ].admin= None
+            self.groups[ int(groupid) ].room= None
+        elif type == "server": self.server = None
+        elif type == "admin":  self.groups[ int(groupid) ].admin = None
+        elif type == "room":
+            self.groups[ int(groupid) ].room = None
+        else: pass
+
+    def QueryUser(self,info_string):
+        pwd_dialog = orpg.orpg_windows.wx.TextEntryDialog(None,info_string,"Password Required")
+        if pwd_dialog.ShowModal() == orpg.orpg_windows.wx.ID_OK:
+            pwd_dialog.Destroy()
+            return str(pwd_dialog.GetValue())
+        else:
+            pwd_dialog.Destroy()
+            return None
+
+    def CheckGroupData(self, id ):
+        try: #see if group exists
+            group=self.groups[ int(id) ]
+        except: #group doesn't exist... create it
+            self.groups[ int(id) ] = PassSet()
+
+    def RemoveGroupData(self, id ):
+        try:
+            #if PassSet exists for group remove it.
+            del self.groups[int(id)]
+        except:
+            pass
+
+    def GetSilentPassword( self, type="server", groupid = 0):
+        try:
+            self.CheckGroupData( groupid )
+            if type == "admin":
+                if self.groups[int(groupid)].admin != None: return str(self.groups[int(groupid)].admin)
+                else: return None
+            elif type == "room":
+                if self.groups[int(groupid)].room != None: return str(self.groups[int(groupid)].room)
+                else: return None
+            elif type == "server":
+                if self.server != None: return str(self.server)
+                else: return None
+        except:
+            traceback.print_exc()
+            #return None
+
+    def GetPassword(self, type="room", groupid=0):
+        if self.Is_Enabled():
+            self.CheckGroupData( groupid )
+            if type == "admin": return self.AdminPass(int(groupid))
+            elif type == "room": return self.RoomPass(int(groupid))
+            elif type == "server": return self.ServerPass()
+            else:
+                querystring = "Enter password for \""+str(type)+"\""
+                return self.QueryUser( querystring )
+        else:
+            if type == "admin": return self.QueryUser( "Enter Admin(Boot) Password" )
+            elif type == "room": return self.QueryUser("Enter Room Password" )
+            elif type == "server": return self.QueryUser( "Enter Server Administrator Password" )
+            else:
+                querystring = "Enter password for \""+str(type)+"\""
+                return self.QueryUser( querystring )
+
+
+    def Is_Enabled(self):
+        return int(self.enabled)
+
+    def Enable(self):
+        self.enabled = 1
+        self.settings.set_setting('PWMannager', 'On')
+
+    def Disable(self):
+        self.enabled = 0
+        self.settings.set_setting('PWMannager', 'Off')
+
+
+    def AdminPass( self, groupid ):
+        self.CheckGroupData( groupid )
+        if self.groups[ int(groupid) ].admin != None: return str(self.groups[ int(groupid) ].admin)
+        else:
+            self.groups[ int(groupid) ].admin = self.QueryUser("Please enter the Room Administrator Password:")
+            return  str(self.groups[ int(groupid) ].admin)
+
+
+    def RoomPass( self, groupid):
+        self.CheckGroupData( groupid )
+        if self.groups[ int(groupid) ].room != None: return str(self.groups[ int(groupid) ].room)
+        else:
+            self.groups[ int(groupid) ].room =  self.QueryUser("Please enter the Room Password:")
+            return str(self.groups[ int(groupid) ].room)
+
+
+    def ServerPass( self ):
+        if self.server != None: return str(self.server)
+        else:
+            self.server = self.QueryUser("Please enter the Server Administrator password:")
+            return str(self.server)
+
+    def FailPassword( self, type="room", groupid=0):
+        self.CheckGroupData( groupid )
+        if type == "admin":
+            self.ClearPassword( type, groupid )
+            return self.AdminPass()
+        elif type == "room":
+            self.ClearPassword( type, groupid  )
+            return self.RoomPass()
+        elif type == "server":
+            self.ClearPassword( type, groupid  )
+            return self.ServerPass()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/pluginui.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,307 @@
+from orpg.orpg_wx import *
+from orpg.orpgCore import *
+import orpg.plugindb as plugindb
+import orpg.dirpath
+
+sys.path.append(orpg.dirpath.dir_struct["plugins"])
+
+class PluginFrame(wx.Frame):
+    def __init__(self, parent):
+        wx.Frame.__init__(self, parent, wx.ID_ANY, "Plugin Control Panel")
+        self.panel = wx.Panel(self, wx.ID_ANY)
+        self.plugindb = plugindb.PluginDB()
+        self.parent = parent
+        self.startplugs = self.plugindb.GetList("plugincontroller", "startup_plugins", [])
+        self.available_plugins = {}
+        self.enabled_plugins  = {}
+        self.pluginNames = []
+        self.SetMinSize((380, 480))
+        self._selectedPlugin = None
+        self.Bind(wx.EVT_CLOSE, self._close)
+
+    #Public Methods
+    def Start(self):
+        self.__buildGUI()
+        self._update(None)
+        self.base_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.base_sizer.Add(self.panel, 1, wx.EXPAND)
+        self.panel.SetSizer(self.main_sizer)
+        self.panel.SetAutoLayout(True)
+        self.panel.Fit()
+        self.SetSizer(self.base_sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+    def get_activeplugins(self):
+        return self.enabled_plugins
+
+    def get_startplugins(self):
+        return self.startplugs
+
+    #Private Methods
+    def __buildGUI(self):
+        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.btn_sizer2 = wx.BoxSizer(wx.HORIZONTAL)
+        self.head_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        #pnl = wx.Panel(self.panel, wx.ID_ANY)
+        self.err_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.err_sizer.Add(self.head_sizer, 0, wx.EXPAND)
+        self.errorMessage = wx.StaticText(self.panel, wx.ID_ANY, "")
+        self.err_sizer.Add(self.errorMessage, 0, wx.EXPAND)
+        self.main_sizer.Add(self.err_sizer, 0, wx.EXPAND)
+        self.pluginList = wx.ListCtrl(self.panel, wx.ID_ANY, style=wx.LC_SINGLE_SEL|wx.LC_REPORT|wx.LC_HRULES|wx.LC_SORT_ASCENDING)
+        self.pluginList.InsertColumn(1, "Name")
+        self.pluginList.InsertColumn(2, "Autostart")
+        self.pluginList.InsertColumn(3, "Author")
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._selectPlugin, self.pluginList)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self._deselectPlugin, self.pluginList)
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._togglePlugin, self.pluginList)
+        self.Bind(wx.EVT_LIST_COL_CLICK, self._sort, self.pluginList)
+        self.main_sizer.Add(self.pluginList, 1, wx.EXPAND)
+
+        self.enableAllBtn = wx.Button(self.panel, wx.ID_ANY, "Enable All")
+        self.enableBtn = wx.Button(self.panel, wx.ID_ANY, "Enable")
+        self.disableAllBtn = wx.Button(self.panel, wx.ID_ANY, "Disable All")
+        self.disableBtn = wx.Button(self.panel, wx.ID_ANY, "Disable")
+        self.autostartBtn = wx.Button(self.panel, wx.ID_ANY, "Autostart")
+        self.helpBtn = wx.Button(self.panel, wx.ID_ANY, "Plugin Info")
+        self.updateBtn = wx.Button(self.panel, wx.ID_ANY, "Update List")
+
+        self.Bind(wx.EVT_BUTTON, self._enableAll, self.enableAllBtn)
+        self.Bind(wx.EVT_BUTTON, self._enable, self.enableBtn)
+        self.Bind(wx.EVT_BUTTON, self._disableAll, self.disableAllBtn)
+        self.Bind(wx.EVT_BUTTON, self._disable, self.disableBtn)
+        self.Bind(wx.EVT_BUTTON, self._autostart, self.autostartBtn)
+        self.Bind(wx.EVT_BUTTON, self._help, self.helpBtn)
+        self.Bind(wx.EVT_BUTTON, self._update, self.updateBtn)
+
+        self.btn_sizer.Add(self.enableBtn, 0, wx.EXPAND)
+        self.btn_sizer.Add(self.disableBtn, 0, wx.EXPAND)
+        self.btn_sizer.Add(self.autostartBtn, 0, wx.EXPAND)
+        self.btn_sizer.Add(self.helpBtn, 0, wx.EXPAND)
+
+        self.btn_sizer2.Add(self.updateBtn, 0, wx.EXPAND)
+        self.btn_sizer2.Add(self.enableAllBtn, 0, wx.EXPAND)
+        self.btn_sizer2.Add(self.disableAllBtn, 0, wx.EXPAND)
+
+        self.__disablePluginBtns()
+
+        self.main_sizer.Add(self.btn_sizer, 0, wx.EXPAND)
+        self.main_sizer.Add(self.btn_sizer2, 0, wx.EXPAND)
+
+    def __disablePluginBtns(self):
+        self.enableBtn.Disable()
+        self.disableBtn.Disable()
+        self.autostartBtn.Disable()
+        self.helpBtn.Disable()
+
+    def __enablePluginBtns(self):
+        self.autostartBtn.Label = "Autostart"
+        self.enableBtn.Enable()
+        self.disableBtn.Enable()
+        self.autostartBtn.Enable()
+        self.helpBtn.Enable()
+
+    def __error(self, errMsg):
+        self.errorMessage.Label += "\n" + str(errMsg)
+        self.__doLayout()
+
+    def __clearError(self):
+        self.errorMessage.Label = ""
+        self.__doLayout()
+
+    def __checkIdx(self, evt):
+        if isinstance(evt, int):
+            return evt
+        elif self._selectedPlugin is not None:
+            return self._selectedPlugin
+        else:
+            dlg = wx.MessageDialog(None, "You need to select a plugin before you can use this!", 'ERROR', wx.OK)
+            dlg.ShowModal()
+            dlg.Destroy()
+            return None
+
+    def __impPlugin(self, pname):
+        try:
+            if "plugins." + pname in sys.modules:
+                del sys.modules["plugins." + pname]#to ensure that the newly-imported one will be used correctly. No, reload() is not a better way to do this.
+            mod = __import__("plugins." + pname)
+            plugin = getattr(mod, pname)
+            pdata = plugin.Plugin(self.plugindb, self.parent)
+            self.available_plugins[pdata.name] = [pname, pdata, pdata.author, pdata.help]
+            return plugin
+
+        except Exception, e:
+            self.__error(e)
+            traceback.print_exc()
+            print e
+
+    def __doLayout(self):
+        self.Freeze()
+        self.panel.Layout()
+        self.Fit()
+        self.Thaw()
+
+    def __pluginSort(self, item1, item2):
+        return cmp(self.pluginNames[item1], self.pluginNames[item2])
+
+    #Events
+    def _selectPlugin(self, evt):
+        self._selectedPlugin = evt.GetIndex()
+        self.__enablePluginBtns()
+        pname = self.pluginList.GetItem(self._selectedPlugin, 0).GetText()
+        info = self.available_plugins[pname]
+
+        if info[0] in self.enabled_plugins:
+            self.enableBtn.Disable()
+        else:
+            self.disableBtn.Disable()
+        if self.pluginList.GetItem(self._selectedPlugin, 1).GetText() == "X":
+            self.autostartBtn.Label = "Disable Autostart"
+
+        self.__doLayout()
+        self.pluginList.SetItemState(self._selectedPlugin, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED)
+
+    def _deselectPlugin(self, evt):
+        self.__disablePluginBtns()
+        self._selectedPlugin = None
+
+    def _togglePlugin(self, evt):
+        idx = evt.GetIndex()
+        pname = self.pluginList.GetItem(idx, 0).GetText()
+        info = self.available_plugins[pname]
+
+        if info[0] in self.enabled_plugins:
+            self._disable(idx)
+        else:
+            self._enable(idx)
+
+        self.pluginList.SetItemState(self._selectedPlugin, 0, wx.LIST_STATE_SELECTED)
+
+    def _enableAll(self, evt):
+        for pname in self.available_plugins.iterkeys():
+            info = self.available_plugins[pname]
+            if info[0] not in self.enabled_plugins:
+                idx = self.pluginList.FindItem(-1, pname)
+                self._enable(idx)
+
+    def _enable(self, evt):
+        idx = self.__checkIdx(evt)
+        if idx is None:
+            return
+        pname = self.pluginList.GetItem(idx, 0).GetText()
+        info = self.available_plugins[pname]
+        info[1].menu_start()
+
+        try:
+            info[1].plugin_enabled()
+        except Exception, e:
+            self.__error(e)
+            traceback.print_exc()
+            print e
+            self.pluginList.SetItemBackgroundColour(idx, (255,0,0))
+            info[1].menu_cleanup()
+            return
+
+        self.enabled_plugins[info[0]] = info[1]
+        self.pluginList.SetItemBackgroundColour(idx, (0,255,0))
+        self.enableBtn.Disable()
+        self.disableBtn.Enable()
+
+    def _disableAll(self, evt):
+        for entry in self.enabled_plugins.keys():
+            idx = self.pluginList.FindItem(0, self.enabled_plugins[entry].name)
+            self._disable(idx)
+
+    def _disable(self, evt):
+        idx = self.__checkIdx(evt)
+        if idx is None:
+            return
+        pname = self.pluginList.GetItem(idx, 0).GetText()
+        info = self.available_plugins[pname]
+        info[1].menu_cleanup()
+        try:
+            info[1].plugin_disabled()
+            del self.enabled_plugins[info[0]]
+        except Exception, e:
+            self.__error(e)
+            traceback.print_exc()
+            print e
+            self.pluginList.SetItemBackgroundColour(idx, (255,0,0))
+            return
+        self.pluginList.SetItemBackgroundColour(idx, (255,255,255))
+        self.disableBtn.Disable()
+        self.enableBtn.Enable()
+
+    def _autostart(self, evt):
+        idx = self.__checkIdx(evt)
+        if idx is None:
+            return
+        if self.pluginList.GetItem(idx, 1).GetText() == "X":
+            self.startplugs.remove(self.pluginList.GetItem(idx, 0).GetText())
+            self.pluginList.SetStringItem(idx, 1, "")
+            self.autostartBtn.Label = "Autostart"
+        else:
+            self.startplugs.append(self.pluginList.GetItem(idx, 0).GetText())
+            self.pluginList.SetStringItem(idx, 1, "X")
+            self.autostartBtn.Label = "Disable Autostart"
+
+        self.plugindb.SetList("plugincontroller", "startup_plugins", self.startplugs)
+        self.__doLayout()
+
+    def _help(self, evt):
+        if isinstance(evt, int):
+            idx = evt
+        elif self._selectedPlugin is not None:
+            idx = self._selectedPlugin
+        else:
+            dlg = wx.MessageDialog(None, "You need to select a plugin before you can use this!", 'ERROR', wx.OK)
+            dlg.ShowModal()
+            dlg.Destroy()
+            return
+
+        pname = self.pluginList.GetItem(idx, 0).GetText()
+        info = self.available_plugins[pname]
+
+        msg = "Author(s):\t" + info[2] + "\n\n" + info[3]
+
+        dlg = wx.MessageDialog(None, msg, 'Plugin Information: ' + pname, wx.OK)
+        dlg.ShowModal()
+        dlg.Destroy()
+
+    def _update(self, evt):
+        self.__clearError()
+        self._disableAll(None)
+        self.available_plugins = {}
+        self.errorMessage.Label = ""
+        self.pluginList.DeleteAllItems()
+        self.pluginNames = []
+
+        list_of_plugin_dir = os.listdir(orpg.dirpath.dir_struct["plugins"])
+        for p in list_of_plugin_dir:
+            #print p[:2]; print p[-4:]
+            if p[:2].lower()=="xx" and p[-3:]==".py":
+                self.__impPlugin(p[:-3])
+            elif p[:2].lower()=="xx" and p[-4:]==".pyc":
+                self.__impPlugin(p[:-4])
+
+        i = 0
+        for plugname, info in self.available_plugins.iteritems():
+            self.pluginNames.append(plugname)
+            idx = self.pluginList.InsertStringItem(self.pluginList.GetItemCount(), plugname)
+            self.pluginList.SetStringItem(idx, 2, info[2])
+            if plugname in self.startplugs:
+                self.pluginList.SetStringItem(idx, 1, "X")
+                self._enable(idx)
+            self.pluginList.SetItemData(idx, i)
+            i += 1
+        self.pluginList.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+        self.__doLayout()
+        self.__disablePluginBtns()
+
+    def _close(self, evt):
+        self.Hide()
+
+    def _sort(self, evt):
+        self.pluginList.SortItems(self.__pluginSort)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/predTextCtrl.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,613 @@
+# Copyright (C) 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: predTextCtrl.py
+# Author: Andrew Bennett
+# Maintainer: Andrew Bennett
+# Version:  $id:$
+#
+# Description: This file contains an extension to the wxPython wx.TextCtrl that provides predictive word completion
+#              based on a word file loaded at instantiation.  Also, it learns new words as you type, dynamically
+#              adjusting a weight for each word based on how often you type it.
+#
+
+##
+## Module Loading
+##
+
+import string
+from orpg.orpg_windows import *
+
+#  This line added to test CVS commit
+
+##
+## Class Definitions
+##
+
+# This class implements a tree node that represents a letter
+#
+# Defines:
+#   __init__(self,filename)
+#
+class Letter:
+    def __init__(self,asciiCharIn,parentNodeIn):
+        self.asciiChar =  asciiCharIn           # should be an ASCII char
+        self.parentNode = parentNodeIn          # should be ref to a class Letter
+        self.priority = 0                       # should be an int
+        self.mostCommon = self                  # should be a ref to a class Letter
+        self.children = {}                      # should be a ref to a dictionary of class Letter
+
+    def __str__(self):
+        return self.asciiChar
+
+
+# This class implements the tree structure to hold the words
+#
+# Defines:
+#    __init__(self,filename)
+#    updateMostCommon(self,target)
+#    setWord(self,wordText,priority,sumFlag)
+#    addWord(self,wordText)
+#    setWord(self,wordText)
+#    setWordPriority(self,wordText,priority)
+#    findWordNode(self,wordText) returns class Letter
+#    findWordPriority(self,wordText) returns int
+#    getPredition(self,k,cur) returns string
+class LetterTreeClass(object):
+
+    # Initialization subroutine.
+    #
+    # self : instance of self
+    # filename : name of word file to use
+    #
+    # returns None
+    #
+    # Purpose:  Constructor for LetterTree.  Basically, it initializes itself with a word file, if present.
+    def __init__(self, singletonKey):
+        if not isinstance(singletonKey, _SingletonKey):
+            raise invalid_argument(_("Use LetterTree() to get access to singleton"))
+
+        self.rootNode = Letter("",None)                # rootNode is a class Letter
+        self.rootNode.children = {}                    # initialize the children list
+
+
+    # updateMostCommon subroutine.
+    #
+    # self : instance of self
+    # target : class Letter that was updated
+    #
+    # Returns None
+    #
+    # Purpose:  Updates all of the parent nodes of the target, such that their mostCommon member
+    #           points to the proper class Letter, based on the newly updated priorities.
+    def updateMostCommon(self, target):
+                                                # cur is a class Letter
+        prev = target.parentNode                # prev is a class Letter
+        while prev:
+            if prev.mostCommon is None:
+                prev.mostCommon = target
+            else:
+                if target.priority > prev.mostCommon.priority:
+                    prev.mostCommon = target
+            prev = prev.parentNode
+
+
+
+    # setWord subroutine.
+    #
+    # self : instance of self
+    # wordText : string representing word to add
+    # priority : integer priority to set the word
+    # sumFlag : if True, add the priority to the existing, else assign the priority
+    #
+    # Returns:  None
+    #
+    # Purpose:  Sets or increments the priority of a word, adding the word if necessary
+    def setWord(self,wordText,priority = 1,sumFlag = 0):
+
+        cur = self.rootNode                     # start from the root
+
+        for ch in wordText:                     # for each character in the word
+            if cur.children.has_key(ch):        # check to see if we've found a new word
+
+                cur = cur.children[ch]            # if we haven't found a new word, move to the next letter and try again
+
+            else:                               # in this clause, we're creating a new branch, as the word is new
+                newLetter = Letter(ch,cur)        # create a new class Letter using this ascii code and the current letter as a parent
+
+                if cur is self.rootNode:          # special case:  Others expect the top level letters to point to None, not self.rootNode
+                    newLetter.parentNode = None
+
+                cur.children[ch] = newLetter      #  add the new letter to the list of children of the current letter
+                cur = newLetter                   #  make the new letter the current one for the next time through
+
+        #  at this point, cur is pointing to the last letter of either a new or existing word.
+
+        if sumFlag:                             #  if the caller wants to add to the existing (0 if new)
+            cur.priority += priority
+        else:                                   #  else, the set the priority directly
+            cur.priority = priority
+
+        self.updateMostCommon(cur)              # this will run back through the tree to fix up the mostCommon members
+
+
+    # addWord subroutine.
+    #
+    # self : instance of self
+    # wordText : string representing word to add
+    #
+    # Returns:  None
+    #
+    # Purpose:  Convenience method that wraps setWord.  Used to add words known not to exist.
+    def addWord(self,wordText):
+        self.setWord(wordText,priority = 1)
+
+
+    # incWord subroutine.
+    #
+    # self : instance of self
+    # wordText : string representing word to add
+    #
+    # Returns:  None
+    #
+    # Purpose:  Convenience method that wraps setWord.  Used to increment the priority of existing words and add new words.
+    # Note:     Generally, this method can be used instead of addWord.
+    def incWord(self,wordText):
+        self.setWord(wordText,priority = 1, sumFlag = 1)
+
+
+    # setWordPriority subroutine.
+    #
+    # self : instance of self
+    # wordText : string representing word to add
+    # priority:  int that is the new priority
+    #
+    # Returns:  None
+    #
+    # Purpose:  Convenience method that wraps setWord.  Sets existing words to priority or adds new words with priority = priority
+    def setWordPriority(self,wordText,priority):
+        self.setWord(wordText,priority = priority)
+
+
+    # findWordNode subroutine.
+    #
+    # self : instance of self
+    # wordText : string representing word to add
+    #
+    # Returns:  class Letter or None if word isn't found.
+    #
+    # Purpose:  Given a word, it returns the class Letter node that corresponds to the word.  Used mostly in prep for a call to
+    #           getPrediction()
+    def findWordNode(self,wordText):         #returns class Letter that represents the last letter in the word
+
+        cur = self.rootNode                  # start at the root
+
+        for ch in wordText:                  # move through each letter in the word
+            if cur.children.has_key(ch):     # if the next letter exists, make cur equal that letter and loop
+                cur = cur.children[ch]
+            else:
+                return None                  # return None if letter not found
+
+        return cur                           # return cur, as this points to the last letter if we got this far
+
+
+    # findWordPriority subroutine.
+    #
+    # self : instance of self
+    # wordText : string representing word to add
+    #
+    # Returns:  Int representing the word's priority or 0 if not found.
+    #
+    # Purpose:  Returns the priority of the given word
+    def findWordPriority(self,wordText):
+
+        cur = self.findWordNode(wordText)    #  find the class Letter node that corresponds to this word
+        if cur:
+            return cur.priority              #  if it was found, return it's priority
+        else:
+            return 0                         #  else, return 0, meaning word not found
+
+
+    def printTree(self, current=None):
+        letters = []
+        if current is None:
+            current = self.rootNode
+
+        for l, letter in current.children.iteritems():
+            letters.append(str(letter))
+            if letter.children != {}:
+                m = self.printTree(letter)
+                letters.append(m)
+
+        return letters
+
+
+
+    # getPrediction subroutine.
+    #
+    # self : instance of self
+    # k : ASCII char that was typed
+    # cur : class Letter that points to the current node in LetterTree
+    #
+    # Returns:  The predicted text or "" if none found
+    #
+    # Purpose:  This is the meat and potatoes of data structure.  It takes the "current" Letter node and the next key typed
+    #           and returns it's guess of the rest of the word, based on the highest priority letter in the rest of the branch.
+    def getPrediction(self,k,cur):
+
+        if cur.children.has_key(k) :                            #  check to see if the key typed is a sub branch
+                                                                #  If so, make a prediction.  Otherwise, do the else at the bottom of
+                                                                #  the method (see below).
+
+            cur = cur.children[k]                               #  set the cur to the typed key's class Letter in the sub-branch
+
+            backtrace = cur.mostCommon                          # backtrace is a class Letter.  It's used as a placeholder to back trace
+                                                                # from the last letter of the mostCommon word in the
+                                                                # sub-tree up through the tree until we meet ourself at cur.  We'll
+                                                                # build the guess text this way
+
+            returnText = ""                                     # returnText is a string.  This will act as a buffer to hold the string
+                                                                # we build.
+
+            while cur is not backtrace:                         #  Here's the loop.  We loop until we've snaked our way back to cur
+
+                returnText = backtrace.asciiChar + returnText   #  Create a new string that is the character at backtrace + everything
+                                                                #  so far.  So, for "tion"  we'll build "n","on","ion","tion" as we ..
+
+                backtrace = backtrace.parentNode                #  ... climb back towards cur
+
+            return returnText                                   #  And, having reached here, we've met up with cur, and returnText holds
+                                                                #  the string we built.  Return it.
+
+        else:                                                   #  This is the else to the original if.
+                                                                #  If the letter typed isn't in a sub branch, then
+                                                                #  the letter being typed isn't in our tree, so
+            return ""                                           #  return the empty string
+
+
+#  End of class LetterTree!
+
+
+
+class _SingletonKey(object):
+    def __new__(cls, *args, **kwargs):
+        if not hasattr(cls, '_inst'):
+            cls._inst = super(_SingletonKey, cls).__new__(cls, *args, **kwargs)
+        return cls._inst
+
+__key = _SingletonKey()
+LetterTree = LetterTreeClass(__key)
+
+# This class extends wx.TextCtrl
+#
+# Extends:  wx.TextCtrl
+#
+# Overrides:
+#    wx.TextCtrl.__init__(self,parent,id,value,size,style,name)
+#    wx.TextCtrl.OnChar(self,Event)
+#
+# Defines:
+#    findWord(self,insert,st)
+class predTextCtrl(wx.TextCtrl):
+
+    # Initialization subroutine.
+    #
+    # self : instance of self
+    # parent:  reference to parent window (wxWindow, me-thinks)
+    # id:      new Window Id, default to -1 to create new (I think: see docs for wxPython)
+    # value:   String that is the initial value the control holds, defaulting to ""
+    # size:    defaults to wx.DefaultSize
+    # style:   defaults to 0
+    # name:    defaults to "text"
+    # keyHook: must be a function pointer that takes self and a GetKeyCode object
+    # validator: defaults to None
+    #
+    # Note:  These parameters are essentially just passed back to the native wx.TextCtrl.
+    #        I basically just included (stole) enough of them from chatutils.py to make
+    #        it work.  Known missing args are pos and validator, which aren't used by
+    #        chatutils.py.
+    #
+    # Returns:  None
+    #
+    # Purpose:  Constructor for predTextCtrl.  Calls wx.TextCtrl.__init__ to get default init
+    #           behavior and then inits a LetterTree and captures the parent for later use in
+    #           passing events up the chain.
+    def __init__(self, parent, id = -1, value = "" , size = wx.DefaultSize, style = 0, name = "text",keyHook = None, validator=None):
+
+        #  Call super() for default behavior
+        if validator:
+            wx.TextCtrl.__init__(self, parent, id=id, value=value, size=size, style=style, name=name, validator=validator )
+        else:
+            wx.TextCtrl.__init__(self, parent, id=id, value=value, size=size, style=style, name=name)
+
+        self.tree = LetterTree      #  Instantiate a new LetterTree.
+        #  TODO:  make name of word file an argument.
+
+
+
+
+        self.parent = parent                               #  Save parent for later use in passing KeyEvents
+
+        self.cur = self.tree.rootNode                      # self.cur is a short cut placeholder for typing consecutive chars
+                                                           # It may be vestigal
+
+        self.keyHook = keyHook                             #  Save the keyHook passed in
+
+
+    # findWord subroutine.
+    #
+    # self :  instance of self
+    # insert: index of last char in st
+    # st :    string from insert to the left
+    #
+    # Note:  This implementation is about the third one for this method.  Originally,
+    #        st was an arbitrary string and insert was the point within
+    #        this string to begin looking left.  Since, I finally got it
+    #        to work right as it is around 2 in the morning, I'm not touching it, for now.
+    #
+    # Returns:  String that is the word or "" if none found.  This generally doesn't
+    #           happen, as st usually ends in a letter, which will be returned.
+    #
+    # Purpose:  This function is generally used to figure out the beginning of the
+    #           current word being typed, for later use in a LetterTree.getPrediction()
+    def findWord(self,insert,st):
+
+    #  Good luck reading this one.  Basically, I started out with an idea, and fiddled with the
+    #  constants as best I could until it worked.  It's not a piece of work of which I'm too
+    #  proud.  Basically, it's intent is to check each character to the left until it finds one
+    #  that isn't a letter.  If it finds such a character, it stops and returns the slice
+    #  from that point to insert.  Otherwise, it returns the whole thing, due to begin being
+    #  initialized to 0
+
+        begin = 0
+        for offset in range(insert - 1):
+            if st[-(offset + 2)] not in string.letters:
+                begin = insert - (offset + 1)
+                break
+        return st[begin:insert]
+
+
+    # OnChar subroutine.
+    #
+    # self :  instance of self
+    # event:  a GetKeyCode object
+    #
+    # Returns:  None
+    #
+    # Purpose:  This function is the key event handler for predTextCtrl.  It handles what it
+    #           needs to and passes the event on to it's parent's OnChar method.
+    def OnChar(self,event):
+
+        #  Before we do anything, call the keyHook handler, if not None
+        #    This is currently used to implement the typing/not_typing messages in a manner that
+        #         doesn't place the code here.  Maybe I should reconsider that.  :)
+        if(self.keyHook):
+            if self.keyHook(event) == 1:   #  if the passed in keyHook routine returns a one, it wants no predictive behavior
+                self.parent.OnChar(event)
+                return
+
+
+
+        #  This bit converts the GetKeyCode() return (int) to a char if it's in a certain range
+        asciiKey = ""
+        if (event.GetKeyCode() < 256) and (event.GetKeyCode() > 19):
+            asciiKey = chr(event.GetKeyCode())
+
+
+        if asciiKey == "":                                 #  If we didn't convert it to a char, then process based on the int GetKeyCodes
+
+            if  event.GetKeyCode() == wx.WXK_TAB:                #  We want to hook tabs to allow the user to signify acceptance of a
+                                                           #  predicted word.
+                #  Handle Tab key
+
+                fromPos = toPos = 0                        # get the current selection range
+                (fromPos,toPos) = self.GetSelection()
+
+                if (toPos - fromPos) == 0:                 # if there is no selection, pass tab on
+                    self.parent.OnChar(event)
+                    return
+
+                else:                                      #  This means at least one char is selected
+
+                    self.SetInsertionPoint(toPos)          #  move the insertion point to the end of the selection
+                    self.SetSelection(toPos,toPos)         #  and set the selection to no chars
+                                                           #  The prediction, if any, had been inserted into the text earlier, so
+                                                           #  moving the insertion point to the spot directly afterwards is
+                                                           #  equivalent to acceptance.  Without this, the next typed key would
+                                                           #  clobber the prediction.
+
+                    return                                 #  Don't pass tab on in this case
+
+            elif event.GetKeyCode() == wx.WXK_RETURN:            #  We want to hook returns, so that we can update the word list
+
+                st = self.GetValue()                       #  Grab the text from the control
+                newSt = ""                                 #  Init a buffer
+
+                #  This block of code, by popular demand, changes the behavior of the control to ignore any prediction that
+                #    hasn't been "accepted" when the enter key is struck.
+                (startSel,endSel) = self.GetSelection()                 #  get the curren selection
+
+                #
+                # Start update
+                # Changed the following to allow for more friendly behavior in
+                # a multilined predTextCtrl.
+                #
+                # front = st[:startSel]                                   #  Slice off the text to the front of where we are
+                # back = st[endSel:]                                      #  Slice off the text to the end from where we are
+                # st = front + back                          #  This expression creates a string that get rid of any selected text.
+                # self.SetValue(st)
+
+                self.Remove( startSel, endSel )
+                st = string.strip( self.GetValue() )
+                #
+                # End update
+                #
+
+                #  this loop will walk through every character in st and add it to
+                #  newSt if it's a letter.  If it's not a letter, (e.g. a comma or
+                #  hyphen) a space is added to newSt in it's place.
+                for ch in st:
+                    if ch not in string.letters:
+                        newSt += " "
+                    else:
+                        newSt += ch
+
+                #  Now that we've got a string of just letter sequences (words) and spaces
+                #  split it and to a LetterTree.incWord on the lowercase version of it.
+                #  Reminder:  incWord will increment the priority of existing words and add
+                #  new ones
+                for aWord in string.split(newSt):
+                    self.tree.incWord(string.lower(aWord))
+
+                self.parent.OnChar(event)                   #  Now that all of the words are added, pass the event and return
+                return
+
+            #  We want to capture the right arrow key to fix a slight UI bug that occurs when one right arrows
+            #  out of a selection.  I set the InsertionPoint to the beginning of the selection.  When the default
+            #  right arrow event occurs, the selection goes away, but the cursor is in an unexpected location.
+            #  This snippet fixes this behavior and then passes on the event.
+            elif event.GetKeyCode() == wx.WXK_RIGHT:
+                (startSel,endSel) = self.GetSelection()
+                self.SetInsertionPoint(endSel)
+                self.parent.OnChar(event)
+                return
+
+            #  Ditto as wx.WXK_RIGHT, but for completeness sake
+            elif event.GetKeyCode() == wx.WXK_LEFT:
+                (startSel,endSel) = self.GetSelection()
+                self.SetInsertionPoint(startSel)
+                self.parent.OnChar(event)
+                return
+
+
+            else:
+                #   Handle any other non-ascii events by calling parent's OnChar()
+                self.parent.OnChar(event)     #Call super.OnChar to get default behavior
+                return
+
+
+        elif asciiKey in string.letters:
+           #  This is the real meat and potatoes of predTextCtrl.  This is where most of the
+           #  wx.TextCtrl logic is changed.
+
+            (startSel,endSel) = self.GetSelection()                 #  get the curren selection
+            st = self.GetValue()                                    #  and the text in the control
+            front = st[:startSel]                                   #  Slice off the text to the front of where we are
+            back = st[endSel:]                                      #  Slice off the text to the end from where we are
+
+            st = front + asciiKey + back                            #  This expression creates a string that will insert the
+                                                                    #  typed character (asciiKey is generated at the
+                                                                    #  beginning of OnChar()) into the text.  If there
+                                                                    #  was text selected, that text will not be part
+                                                                    #  of the new string, due to the way front and back
+                                                                    #  were sliced.
+
+            insert = startSel + 1                                   #  creates an int that denotes where the new InsertionPoint
+                                                                    #  should be.
+
+            curWord = ""                                            #  Assume there's a problem with finding the curWord
+
+            if (len(back) == 0) or (back[0] not in string.letters): #  We should only insert a prediction if we are typing
+                                                                    #  at the end of a word, not in the middle.  There are
+                                                                    #  three cases: we are typing at the end of the string or
+                                                                    #  we are typing in the middle of the string and the next
+                                                                    #  char is NOT a letter or we are typing in the middle of the
+                                                                    #  string and the next char IS a letter.  Only the former two
+                                                                    #  cases denote that we should make a prediction
+                                                                    #  Note:  The order of the two logical clauses here is important!
+                                                                    #         If len(back) == 0, then the expression back[0] will
+                                                                    #         blow up with an out of bounds array subscript.  Luckily
+                                                                    #         the or operator is a short-circuit operator and in this
+                                                                    #         case will only evaluate back[0] if len(back) != 0, in
+                                                                    #         which we're safely in bounds.
+
+                curWord = self.findWord(insert,front + asciiKey)    #  Now that we know we're supposed to make a prediction,
+                                                                    #  let's find what word root to use in our prediction.
+                                                                    #  Note:  This is using the confusing findWord method.  I
+                                                                    #         send it insert and the text from the beginning
+                                                                    #         of the text through the key just entered.  This is
+                                                                    #         NOT the original usage, but it does work.  See
+                                                                    #         findWord() for more details.
+
+            else:                                                   #  Here, we've found we're in the middle of a word, so we're
+                                                                    #  going to call the parent's event handler.
+                self.parent.OnChar(event)
+                return
+
+            if curWord == "":                                       #  Here, we do a quick check to make sure we have a good root
+                                                                    #  word.  If not, allow the default thing to happen.  Of course,
+                                                                    #  now that I'm documenting this, it occurs to me to wonder why
+                                                                    #  I didn't do the same thing I just talked about.  Hmmmmmm.
+
+                self.parent.OnChar(event)                           # we're done here
+                return
+
+            self.cur = self.tree.findWordNode(string.lower(curWord[:-1]))  #  Still with me?  We're almost done.  At this point, we
+                                                                           #  need to convert our word string to a Letter node,
+                                                                           #  because that's what getPrediction expects.  Notice
+                                                                           #  that we're feeding in the string with the last
+                                                                           #  char sliced off.  For developmentally historical
+                                                                           #  reasons, getPrediction wants the node just before
+                                                                           #  the typed character and the typed char separately.
+
+
+            if self.cur is None:
+                self.parent.OnChar(event)                                  # if there's no word or no match, we're done
+                return
+
+
+            # get the prediction
+            predictText = self.tree.getPrediction(string.lower(asciiKey),self.cur)     #  This is the big prediction, as noted above
+                                                                                       #  Note the use of string.lower() because we
+                                                                                       #  keep the word list in all lower case,but we
+                                                                                       #  want to be able to match any capitalization
+
+            if predictText == "":
+                self.parent.OnChar(event)                                              # if there's no prediction, we're done
+                return
+
+            #  And now for the big finale.  We're going to take the string st
+            #  we created earlier and insert the prediction right after the
+            #  newly typed character.
+            front = st[:insert]                                 #  Grab a new front from st
+            back = st[insert:]                                  #  Grab a new back
+
+            st = front + predictText + back                     #  Insert the prediction
+
+            self.SetValue(st)                                   #  Now, overwrite the controls text with the new text
+            self.SetInsertionPoint(insert)                      #  Set the proper insertion point, directly behind the
+                                                                #  newly typed character and directly in front of the
+                                                                #  predicted text.
+
+            self.SetSelection(insert,insert+len(predictText))   #  Very important!  Set the selection to encompass the predicted
+                                                                #  text.  This way, the user can ignore the prediction by simply
+                                                                #  continuing to type.  Remember, if the user wants the prediction
+                                                                #  s/he must strike the tab key at this point.  Of course, one could
+                                                                #  just use the right arrow key as well, but that's not as easy to
+                                                                #  reach.
+
+            return                                              #  Done!  Do NOT pass the event on at this point, because it's all done.
+
+
+        else:
+            #   Handle every other non-letter ascii (e.g. semicolon) by passing the event on.
+            self.parent.OnChar(event)     #Call super.OnChar to get default behavior
+            return
+
+#  End of class predTextCtrl!
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/pubsub.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,837 @@
+
+#---------------------------------------------------------------------------
+"""
+This module provides a publish-subscribe component that allows
+listeners to subcribe to messages of a given topic. Contrary to the
+original wxPython.lib.pubsub module (which it is based on), it uses
+weak referencing to the subscribers so the lifetime of subscribers
+is not affected by Publisher. Also, callable objects can be used in
+addition to functions and bound methods. See Publisher class docs for
+more details.
+
+Thanks to Robb Shecter and Robin Dunn for having provided
+the basis for this module (which now shares most of the concepts but
+very little design or implementation with the original
+wxPython.lib.pubsub).
+
+The publisher is a singleton instance of the PublisherClass class. You
+access the instance via the Publisher object available from the module::
+
+    from wx.lib.pubsub import Publisher
+    Publisher().subscribe(...)
+    Publisher().sendMessage(...)
+    ...
+
+:Author:      Oliver Schoenborn
+:Since:       Apr 2004
+:Version:     $Id: pubsub.py,v 1.8 2006/06/11 00:12:59 RD Exp $
+:Copyright:   \(c) 2004 Oliver Schoenborn
+:License:     wxWidgets
+"""
+
+_implNotes = """
+Implementation notes
+--------------------
+
+In class Publisher, I represent the topics-listener set as a tree
+where each node is a topic, and contains a list of listeners of that
+topic, and a dictionary of subtopics of that topic. When the Publisher
+is told to send a message for a given topic, it traverses the tree
+down to the topic for which a message is being generated, all
+listeners on the way get sent the message.
+
+Publisher currently uses a weak listener topic tree to store the
+topics for each listener, and if a listener dies before being
+unsubscribed, the tree is notified, and the tree eliminates the
+listener from itself.
+
+Ideally, _TopicTreeNode would be a generic _TreeNode with named
+subnodes, and _TopicTreeRoot would be a generic _Tree with named
+nodes, and Publisher would store listeners in each node and a topic
+tuple would be converted to a path in the tree.  This would lead to a
+much cleaner separation of concerns. But time is over, time to move on.
+"""
+#---------------------------------------------------------------------------
+
+# for function and method parameter counting:
+from types   import InstanceType
+from inspect import getargspec, ismethod, isfunction
+# for weakly bound methods:
+from new     import instancemethod as InstanceMethod
+from weakref import ref as WeakRef
+
+# -----------------------------------------------------------------------------
+
+def _isbound(method):
+    """Return true if method is a bound method, false otherwise"""
+    assert ismethod(method)
+    return method.im_self is not None
+
+
+def _paramMinCountFunc(function):
+    """Given a function, return pair (min,d) where min is minimum # of
+    args required, and d is number of default arguments."""
+    assert isfunction(function)
+    (args, va, kwa, dflt) = getargspec(function)
+    lenDef = len(dflt or ())
+    lenArgs = len(args or ())
+    lenVA = int(va is not None)
+    return (lenArgs - lenDef + lenVA, lenDef)
+
+
+def _paramMinCount(callableObject):
+    """
+    Given a callable object (function, method or callable instance),
+    return pair (min,d) where min is minimum # of args required, and d
+    is number of default arguments. The 'self' parameter, in the case
+    of methods, is not counted.
+    """
+    if type(callableObject) is InstanceType:
+        min, d = _paramMinCountFunc(callableObject.__call__.im_func)
+        return min-1, d
+    elif ismethod(callableObject):
+        min, d = _paramMinCountFunc(callableObject.im_func)
+        return min-1, d
+    elif isfunction(callableObject):
+        return _paramMinCountFunc(callableObject)
+    else:
+        raise 'Cannot determine type of callable: '+repr(callableObject)
+
+
+def _tupleize(items):
+    """Convert items to tuple if not already one,
+    so items must be a list, tuple or non-sequence"""
+    if isinstance(items, list):
+        raise TypeError, 'Not allowed to tuple-ize a list'
+    elif isinstance(items, (str, unicode)) and items.find('.') != -1:
+        items = tuple(items.split('.'))
+    elif not isinstance(items, tuple):
+        items = (items,)
+    return items
+
+
+def _getCallableName(callable):
+    """Get name for a callable, ie function, bound
+    method or callable instance"""
+    if ismethod(callable):
+        return '%s.%s ' % (callable.im_self, callable.im_func.func_name)
+    elif isfunction(callable):
+        return '%s ' % callable.__name__
+    else:
+        return '%s ' % callable
+
+
+def _removeItem(item, fromList):
+    """Attempt to remove item from fromList, return true
+    if successful, false otherwise."""
+    try:
+        fromList.remove(item)
+        return True
+    except ValueError:
+        return False
+
+
+# -----------------------------------------------------------------------------
+
+class _WeakMethod:
+    """Represent a weak bound method, i.e. a method doesn't keep alive the
+    object that it is bound to. It uses WeakRef which, used on its own,
+    produces weak methods that are dead on creation, not very useful.
+    Typically, you will use the getRef() function instead of using
+    this class directly. """
+
+    def __init__(self, method, notifyDead = None):
+        """The method must be bound. notifyDead will be called when
+        object that method is bound to dies. """
+        assert ismethod(method)
+        if method.im_self is None:
+            raise ValueError, "We need a bound method!"
+        if notifyDead is None:
+            self.objRef = WeakRef(method.im_self)
+        else:
+            self.objRef = WeakRef(method.im_self, notifyDead)
+        self.fun = method.im_func
+        self.cls = method.im_class
+
+    def __call__(self):
+        """Returns a new.instancemethod if object for method still alive.
+        Otherwise return None. Note that instancemethod causes a
+        strong reference to object to be created, so shouldn't save
+        the return value of this call. Note also that this __call__
+        is required only for compatibility with WeakRef.ref(), otherwise
+        there would be more efficient ways of providing this functionality."""
+        if self.objRef() is None:
+            return None
+        else:
+            return InstanceMethod(self.fun, self.objRef(), self.cls)
+
+    def __eq__(self, method2):
+        """Two WeakMethod objects compare equal if they refer to the same method
+        of the same instance. Thanks to Josiah Carlson for patch and clarifications
+        on how dict uses eq/cmp and hashing. """
+        if not isinstance(method2, _WeakMethod):
+            return False
+        return      self.fun      is method2.fun \
+                and self.objRef() is method2.objRef() \
+                and self.objRef() is not None
+
+    def __hash__(self):
+        """Hash is an optimization for dict searches, it need not
+        return different numbers for every different object. Some objects
+        are not hashable (eg objects of classes derived from dict) so no
+        hash(objRef()) in there, and hash(self.cls) would only be useful
+        in the rare case where instance method was rebound. """
+        return hash(self.fun)
+
+    def __repr__(self):
+        dead = ''
+        if self.objRef() is None:
+            dead = '; DEAD'
+        obj = '<%s at %s%s>' % (self.__class__, id(self), dead)
+        return obj
+
+    def refs(self, weakRef):
+        """Return true if we are storing same object referred to by weakRef."""
+        return self.objRef == weakRef
+
+
+def _getWeakRef(obj, notifyDead=None):
+    """Get a weak reference to obj. If obj is a bound method, a _WeakMethod
+    object, that behaves like a WeakRef, is returned, if it is
+    anything else a WeakRef is returned. If obj is an unbound method,
+    a ValueError will be raised."""
+    if ismethod(obj):
+        createRef = _WeakMethod
+    else:
+        createRef = WeakRef
+
+    if notifyDead is None:
+        return createRef(obj)
+    else:
+        return createRef(obj, notifyDead)
+
+
+# -----------------------------------------------------------------------------
+
+def getStrAllTopics():
+    """Function to call if, for whatever reason, you need to know
+    explicitely what is the string to use to indicate 'all topics'."""
+    return ''
+
+
+# alias, easier to see where used
+ALL_TOPICS = getStrAllTopics()
+
+# -----------------------------------------------------------------------------
+
+
+class _NodeCallback:
+    """Encapsulate a weak reference to a method of a TopicTreeNode
+    in such a way that the method can be called, if the node is
+    still alive, but the callback does not *keep* the node alive.
+    Also, define two methods, preNotify() and noNotify(), which can
+    be redefined to something else, very useful for testing.
+    """
+
+    def __init__(self, obj):
+        self.objRef = _getWeakRef(obj)
+
+    def __call__(self, weakCB):
+        notify = self.objRef()
+        if notify is not None:
+            self.preNotify(weakCB)
+            notify(weakCB)
+        else:
+            self.noNotify()
+
+    def preNotify(self, dead):
+        """'Gets called just before our callback (self.objRef) is called"""
+        pass
+
+    def noNotify(self):
+        """Gets called if the TopicTreeNode for this callback is dead"""
+        pass
+
+
+class _TopicTreeNode:
+    """A node in the topic tree. This contains a list of callables
+    that are interested in the topic that this node is associated
+    with, and contains a dictionary of subtopics, whose associated
+    values are other _TopicTreeNodes. The topic of a node is not stored
+    in the node, so that the tree can be implemented as a dictionary
+    rather than a list, for ease of use (and, likely, performance).
+
+    Note that it uses _NodeCallback to encapsulate a callback for
+    when a registered listener dies, possible thanks to WeakRef.
+    Whenever this callback is called, the onDeadListener() function,
+    passed in at construction time, is called (unless it is None).
+    """
+
+    def __init__(self, topicPath, onDeadListenerWeakCB):
+        self.__subtopics = {}
+        self.__callables = []
+        self.__topicPath = topicPath
+        self.__onDeadListenerWeakCB = onDeadListenerWeakCB
+
+    def getPathname(self):
+        """The complete node path to us, ie., the topic tuple that would lead to us"""
+        return self.__topicPath
+
+    def createSubtopic(self, subtopic, topicPath):
+        """Create a child node for subtopic"""
+        return self.__subtopics.setdefault(subtopic,
+                    _TopicTreeNode(topicPath, self.__onDeadListenerWeakCB))
+
+    def hasSubtopic(self, subtopic):
+        """Return true only if topic string is one of subtopics of this node"""
+        return self.__subtopics.has_key(subtopic)
+
+    def getNode(self, subtopic):
+        """Return ref to node associated with subtopic"""
+        return self.__subtopics[subtopic]
+
+    def addCallable(self, callable):
+        """Add a callable to list of callables for this topic node"""
+        try:
+            id = self.__callables.index(_getWeakRef(callable))
+            return self.__callables[id]
+        except ValueError:
+            wrCall = _getWeakRef(callable, _NodeCallback(self.__notifyDead))
+            self.__callables.append(wrCall)
+            return wrCall
+
+    def getCallables(self):
+        """Get callables associated with this topic node"""
+        return [cb() for cb in self.__callables if cb() is not None]
+
+    def hasCallable(self, callable):
+        """Return true if callable in this node"""
+        try:
+            self.__callables.index(_getWeakRef(callable))
+            return True
+        except ValueError:
+            return False
+
+    def sendMessage(self, message):
+        """Send a message to our callables"""
+        deliveryCount = 0
+        for cb in self.__callables:
+            listener = cb()
+            if listener is not None:
+                listener(message)
+                deliveryCount += 1
+        return deliveryCount
+
+    def removeCallable(self, callable):
+        """Remove weak callable from our node (and return True).
+        Does nothing if not here (and returns False)."""
+        try:
+            self.__callables.remove(_getWeakRef(callable))
+            return True
+        except ValueError:
+            return False
+
+    def clearCallables(self):
+        """Abandon list of callables to caller. We no longer have
+        any callables after this method is called."""
+        tmpList = [cb for cb in self.__callables if cb() is not None]
+        self.__callables = []
+        return tmpList
+
+    def __notifyDead(self, dead):
+        """Gets called when a listener dies, thanks to WeakRef"""
+        #print 'TreeNODE', `self`, 'received death certificate for ', dead
+        self.__cleanupDead()
+        if self.__onDeadListenerWeakCB is not None:
+            cb = self.__onDeadListenerWeakCB()
+            if cb is not None:
+                cb(dead)
+
+    def __cleanupDead(self):
+        """Remove all dead objects from list of callables"""
+        self.__callables = [cb for cb in self.__callables if cb() is not None]
+
+    def __str__(self):
+        """Print us in a not-so-friendly, but readable way, good for debugging."""
+        strVal = []
+        for callable in self.getCallables():
+            strVal.append(_getCallableName(callable))
+        for topic, node in self.__subtopics.iteritems():
+            strVal.append(' (%s: %s)' %(topic, node))
+        return ''.join(strVal)
+
+
+class _TopicTreeRoot(_TopicTreeNode):
+    """
+    The root of the tree knows how to access other node of the
+    tree and is the gateway of the tree user to the tree nodes.
+    It can create topics, and and remove callbacks, etc.
+
+    For efficiency, it stores a dictionary of listener-topics,
+    so that unsubscribing a listener just requires finding the
+    topics associated to a listener, and finding the corresponding
+    nodes of the tree. Without it, unsubscribing would require
+    that we search the whole tree for all nodes that contain
+    given listener. Since Publisher is a singleton, it will
+    contain all topics in the system so it is likely to be a large
+    tree. However, it is possible that in some runs, unsubscribe()
+    is called very little by the user, in which case most unsubscriptions
+    are automatic, ie caused by the listeners dying. In this case,
+    a flag is set to indicate that the dictionary should be cleaned up
+    at the next opportunity. This is not necessary, it is just an
+    optimization.
+    """
+
+    def __init__(self):
+        self.__callbackDict  = {}
+        self.__callbackDictCleanup = 0
+        # all child nodes will call our __rootNotifyDead method
+        # when one of their registered listeners dies
+        _TopicTreeNode.__init__(self, (ALL_TOPICS,),
+                                _getWeakRef(self.__rootNotifyDead))
+
+    def addTopic(self, topic, listener):
+        """Add topic to tree if doesnt exist, and add listener to topic node"""
+        assert isinstance(topic, tuple)
+        topicNode = self.__getTreeNode(topic, make=True)
+        weakCB = topicNode.addCallable(listener)
+        assert topicNode.hasCallable(listener)
+
+        theList = self.__callbackDict.setdefault(weakCB, [])
+        assert self.__callbackDict.has_key(weakCB)
+        # add it only if we don't already have it
+        try:
+            weakTopicNode = WeakRef(topicNode)
+            theList.index(weakTopicNode)
+        except ValueError:
+            theList.append(weakTopicNode)
+        assert self.__callbackDict[weakCB].index(weakTopicNode) >= 0
+
+    def getTopics(self, listener):
+        """Return the list of topics for given listener"""
+        weakNodes = self.__callbackDict.get(_getWeakRef(listener), [])
+        return [weakNode().getPathname() for weakNode in weakNodes
+                if weakNode() is not None]
+
+    def isSubscribed(self, listener, topic=None):
+        """Return true if listener is registered for topic specified.
+        If no topic specified, return true if subscribed to something.
+        Use topic=getStrAllTopics() to determine if a listener will receive
+        messages for all topics."""
+        weakCB = _getWeakRef(listener)
+        if topic is None:
+            return self.__callbackDict.has_key(weakCB)
+        else:
+            topicPath = _tupleize(topic)
+            for weakNode in self.__callbackDict[weakCB]:
+                if topicPath == weakNode().getPathname():
+                    return True
+            return False
+
+    def unsubscribe(self, listener, topicList):
+        """Remove listener from given list of topics. If topicList
+        doesn't have any topics for which listener has subscribed,
+        nothing happens."""
+        weakCB = _getWeakRef(listener)
+        if not self.__callbackDict.has_key(weakCB):
+            return
+
+        cbNodes = self.__callbackDict[weakCB]
+        if topicList is None:
+            for weakNode in cbNodes:
+                weakNode().removeCallable(listener)
+            del self.__callbackDict[weakCB]
+            return
+
+        for weakNode in cbNodes:
+            node = weakNode()
+            if node is not None and node.getPathname() in topicList:
+                success = node.removeCallable(listener)
+                assert success == True
+                cbNodes.remove(weakNode)
+                assert not self.isSubscribed(listener, node.getPathname())
+
+    def unsubAll(self, topicList, onNoSuchTopic):
+        """Unsubscribe all listeners registered for any topic in
+        topicList. If a topic in the list does not exist, and
+        onNoSuchTopic is not None, a call
+        to onNoSuchTopic(topic) is done for that topic."""
+        for topic in topicList:
+            node = self.__getTreeNode(topic)
+            if node is not None:
+                weakCallables = node.clearCallables()
+                for callable in weakCallables:
+                    weakNodes = self.__callbackDict[callable]
+                    success = _removeItem(WeakRef(node), weakNodes)
+                    assert success == True
+                    if weakNodes == []:
+                        del self.__callbackDict[callable]
+            elif onNoSuchTopic is not None:
+                onNoSuchTopic(topic)
+
+    def sendMessage(self, topic, message, onTopicNeverCreated):
+        """Send a message for given topic to all registered listeners. If
+        topic doesn't exist, call onTopicNeverCreated(topic)."""
+        # send to the all-toipcs listeners
+        deliveryCount = _TopicTreeNode.sendMessage(self, message)
+        # send to those who listen to given topic or any of its supertopics
+        node = self
+        for topicItem in topic:
+            assert topicItem != ''
+            if node.hasSubtopic(topicItem):
+                node = node.getNode(topicItem)
+                deliveryCount += node.sendMessage(message)
+            else: # topic never created, don't bother continuing
+                if onTopicNeverCreated is not None:
+                    onTopicNeverCreated(topic)
+                break
+        return deliveryCount
+
+    def numListeners(self):
+        """Return a pair (live, dead) with count of live and dead listeners in tree"""
+        dead, live = 0, 0
+        for cb in self.__callbackDict:
+            if cb() is None:
+                dead += 1
+            else:
+                live += 1
+        return live, dead
+
+    # clean up the callback dictionary after how many dead listeners
+    callbackDeadLimit = 10
+
+    def __rootNotifyDead(self, dead):
+        #print 'TreeROOT received death certificate for ', dead
+        self.__callbackDictCleanup += 1
+        if self.__callbackDictCleanup > _TopicTreeRoot.callbackDeadLimit:
+            self.__callbackDictCleanup = 0
+            oldDict = self.__callbackDict
+            self.__callbackDict = {}
+            for weakCB, weakNodes in oldDict.iteritems():
+                if weakCB() is not None:
+                    self.__callbackDict[weakCB] = weakNodes
+
+    def __getTreeNode(self, topic, make=False):
+        """Return the tree node for 'topic' from the topic tree. If it
+        doesnt exist and make=True, create it first."""
+        # if the all-topics, give root;
+        if topic == (ALL_TOPICS,):
+            return self
+
+        # not root, so traverse tree
+        node = self
+        path = ()
+        for topicItem in topic:
+            path += (topicItem,)
+            if topicItem == ALL_TOPICS:
+                raise ValueError, 'Topic tuple must not contain ""'
+            if make:
+                node = node.createSubtopic(topicItem, path)
+            elif node.hasSubtopic(topicItem):
+                node = node.getNode(topicItem)
+            else:
+                return None
+        # done
+        return node
+
+    def printCallbacks(self):
+        strVal = ['Callbacks:\n']
+        for listener, weakTopicNodes in self.__callbackDict.iteritems():
+            topics = [topic() for topic in weakTopicNodes if topic() is not None]
+            strVal.append('  %s: %s\n' % (_getCallableName(listener()), topics))
+        return ''.join(strVal)
+
+    def __str__(self):
+        return 'all: %s' % _TopicTreeNode.__str__(self)
+
+
+# -----------------------------------------------------------------------------
+
+class _SingletonKey: pass
+
+class PublisherClass:
+    """
+    The publish/subscribe manager.  It keeps track of which listeners
+    are interested in which topics (see subscribe()), and sends a
+    Message for a given topic to listeners that have subscribed to
+    that topic, with optional user data (see sendMessage()).
+
+    The three important concepts for Publisher are:
+
+    - listener: a function, bound method or
+      callable object that can be called with one parameter
+      (not counting 'self' in the case of methods). The parameter
+      will be a reference to a Message object. E.g., these listeners
+      are ok::
+
+          class Foo:
+              def __call__(self, a, b=1): pass # can be called with only one arg
+              def meth(self,  a):         pass # takes only one arg
+              def meth2(self, a=2, b=''): pass # can be called with one arg
+
+          def func(a, b=''): pass
+
+          Foo foo
+          Publisher().subscribe(foo)           # functor
+          Publisher().subscribe(foo.meth)      # bound method
+          Publisher().subscribe(foo.meth2)     # bound method
+          Publisher().subscribe(func)          # function
+
+      The three types of callables all have arguments that allow a call
+      with only one argument. In every case, the parameter 'a' will contain
+      the message.
+
+    - topic: a single word, a tuple of words, or a string containing a
+      set of words separated by dots, for example: 'sports.baseball'.
+      A tuple or a dotted notation string denotes a hierarchy of
+      topics from most general to least. For example, a listener of
+      this topic::
+
+          ('sports','baseball')
+
+      would receive messages for these topics::
+
+          ('sports', 'baseball')                 # because same
+          ('sports', 'baseball', 'highscores')   # because more specific
+
+      but not these::
+
+           'sports'      # because more general
+          ('sports',)    # because more general
+          () or ('')     # because only for those listening to 'all' topics
+          ('news')       # because different topic
+
+    - message: this is an instance of Message, containing the topic for
+      which the message was sent, and any data the sender specified.
+
+    :note: This class is visible to importers of pubsub only as a
+           Singleton. I.e., every time you execute 'Publisher()', it's
+           actually the same instance of PublisherClass that is
+           returned. So to use, just do'Publisher().method()'.
+
+    """
+
+    __ALL_TOPICS_TPL = (ALL_TOPICS, )
+
+    def __init__(self, singletonKey):
+        """Construct a Publisher. This can only be done by the pubsub
+        module. You just use pubsub.Publisher()."""
+        if not isinstance(singletonKey, _SingletonKey):
+            raise invalid_argument("Use Publisher() to get access to singleton")
+        self.__messageCount  = 0
+        self.__deliveryCount = 0
+        self.__topicTree     = _TopicTreeRoot()
+
+    #
+    # Public API
+    #
+
+    def getDeliveryCount(self):
+        """How many listeners have received a message since beginning of run"""
+        return self.__deliveryCount
+
+    def getMessageCount(self):
+        """How many times sendMessage() was called since beginning of run"""
+        return self.__messageCount
+
+    def subscribe(self, listener, topic = ALL_TOPICS):
+        """
+        Subscribe listener for given topic. If topic is not specified,
+        listener will be subscribed for all topics (that listener will
+        receive a Message for any topic for which a message is generated).
+
+        This method may be called multiple times for one listener,
+        registering it with many topics.  It can also be invoked many
+        times for a particular topic, each time with a different
+        listener.  See the class doc for requirements on listener and
+        topic.
+
+        :note: The listener is held by Publisher() only by *weak*
+               reference.  This means you must ensure you have at
+               least one strong reference to listener, otherwise it
+               will be DOA ("dead on arrival"). This is particularly
+               easy to forget when wrapping a listener method in a
+               proxy object (e.g. to bind some of its parameters),
+               e.g.::
+
+                  class Foo:
+                      def listener(self, event): pass
+                  class Wrapper:
+                      def __init__(self, fun): self.fun = fun
+                      def __call__(self, *args): self.fun(*args)
+                  foo = Foo()
+                  Publisher().subscribe( Wrapper(foo.listener) ) # whoops: DOA!
+                  wrapper = Wrapper(foo.listener)
+                  Publisher().subscribe(wrapper) # good!
+
+        :note: Calling this method for the same listener, with two
+               topics in the same branch of the topic hierarchy, will
+               cause the listener to be notified twice when a message
+               for the deepest topic is sent. E.g.
+               subscribe(listener, 't1') and then subscribe(listener,
+               ('t1','t2')) means that when calling sendMessage('t1'),
+               listener gets one message, but when calling
+               sendMessage(('t1','t2')), listener gets message twice.
+
+        """
+        self.validate(listener)
+
+        if topic is None:
+            raise TypeError, 'Topic must be either a word, tuple of '\
+                             'words, or getStrAllTopics()'
+
+        self.__topicTree.addTopic(_tupleize(topic), listener)
+
+    def isSubscribed(self, listener, topic=None):
+        """Return true if listener has subscribed to topic specified.
+        If no topic specified, return true if subscribed to something.
+        Use topic=getStrAllTopics() to determine if a listener will receive
+        messages for all topics."""
+        return self.__topicTree.isSubscribed(listener, topic)
+
+    def validate(self, listener):
+        """Similar to isValid(), but raises a TypeError exception if not valid"""
+        # check callable
+        if not callable(listener):
+            raise TypeError, 'Listener '+`listener`+' must be a '\
+                             'function, bound method or instance.'
+        # ok, callable, but if method, is it bound:
+        elif ismethod(listener) and not _isbound(listener):
+            raise TypeError, 'Listener '+`listener`+\
+                             ' is a method but it is unbound!'
+
+        # check that it takes the right number of parameters
+        min, d = _paramMinCount(listener)
+        if min > 1:
+            raise TypeError, 'Listener '+`listener`+" can't"\
+                             ' require more than one parameter!'
+        if min <= 0 and d == 0:
+            raise TypeError, 'Listener '+`listener`+' lacking arguments!'
+
+        assert (min == 0 and d>0) or (min == 1)
+
+    def isValid(self, listener):
+        """Return true only if listener will be able to subscribe to
+        Publisher."""
+        try:
+            self.validate(listener)
+            return True
+        except TypeError:
+            return False
+
+    def unsubAll(self, topics=None, onNoSuchTopic=None):
+        """Unsubscribe all listeners subscribed for topics. Topics can
+        be a single topic (string or tuple) or a list of topics (ie
+        list containing strings and/or tuples). If topics is not
+        specified, all listeners for all topics will be unsubscribed,
+        ie. the Publisher singleton will have no topics and no listeners
+        left. If onNoSuchTopic is given, it will be called as
+        onNoSuchTopic(topic) for each topic that is unknown.
+        """
+        if topics is None:
+            del self.__topicTree
+            self.__topicTree = _TopicTreeRoot()
+            return
+
+        # make sure every topics are in tuple form
+        if isinstance(topics, list):
+            topicList = [_tupleize(x) for x in topics]
+        else:
+            topicList = [_tupleize(topics)]
+
+        # unsub every listener of topics
+        self.__topicTree.unsubAll(topicList, onNoSuchTopic)
+
+    def unsubscribe(self, listener, topics=None):
+        """Unsubscribe listener. If topics not specified, listener is
+        completely unsubscribed. Otherwise, it is unsubscribed only
+        for the topic (the usual tuple) or list of topics (ie a list
+        of tuples) specified. Nothing happens if listener is not actually
+        subscribed to any of the topics.
+
+        Note that if listener subscribed for two topics (a,b) and (a,c),
+        then unsubscribing for topic (a) will do nothing. You must
+        use getAssociatedTopics(listener) and give unsubscribe() the returned
+        list (or a subset thereof).
+        """
+        self.validate(listener)
+        topicList = None
+        if topics is not None:
+            if isinstance(topics, list):
+                topicList = [_tupleize(x) for x in topics]
+            else:
+                topicList = [_tupleize(topics)]
+
+        self.__topicTree.unsubscribe(listener, topicList)
+
+    def getAssociatedTopics(self, listener):
+        """Return a list of topics the given listener is registered with.
+        Returns [] if listener never subscribed.
+
+        :attention: when using the return of this method to compare to
+                expected list of topics, remember that topics that are
+                not in the form of a tuple appear as a one-tuple in
+                the return. E.g. if you have subscribed a listener to
+                'topic1' and ('topic2','subtopic2'), this method
+                returns::
+
+                associatedTopics = [('topic1',), ('topic2','subtopic2')]
+        """
+        return self.__topicTree.getTopics(listener)
+
+    def sendMessage(self, topic=ALL_TOPICS, data=None, onTopicNeverCreated=None):
+        """Send a message for given topic, with optional data, to
+        subscribed listeners. If topic is not specified, only the
+        listeners that are interested in all topics will receive message.
+        The onTopicNeverCreated is an optional callback of your choice that
+        will be called if the topic given was never created (i.e. it, or
+        one of its subtopics, was never subscribed to by any listener).
+        It will be called as onTopicNeverCreated(topic)."""
+        aTopic  = _tupleize(topic)
+        message = Message(aTopic, data)
+        self.__messageCount += 1
+
+        # send to those who listen to all topics
+        self.__deliveryCount += \
+            self.__topicTree.sendMessage(aTopic, message, onTopicNeverCreated)
+
+    #
+    # Private methods
+    #
+
+    def __call__(self):
+        """Allows for singleton"""
+        return self
+
+    def __str__(self):
+        return str(self.__topicTree)
+
+# Create the Publisher singleton. We prevent users from (inadvertently)
+# instantiating more than one object, by requiring a key that is
+# accessible only to module.  From
+# this point forward any calls to Publisher() will invoke the __call__
+# of this instance which just returns itself.
+#
+# The only flaw with this approach is that you can't derive a new
+# class from Publisher without jumping through hoops.  If this ever
+# becomes an issue then a new Singleton implementaion will need to be
+# employed.
+_key = _SingletonKey()
+Publisher = PublisherClass(_key)
+
+
+#---------------------------------------------------------------------------
+
+class Message:
+    """
+    A simple container object for the two components of a message: the
+    topic and the user data. An instance of Message is given to your
+    listener when called by Publisher().sendMessage(topic) (if your
+    listener callback was registered for that topic).
+    """
+    def __init__(self, topic, data):
+        self.topic = topic
+        self.data  = data
+
+    def __str__(self):
+        return '[Topic: '+`self.topic`+',  Data: '+`self.data`+']'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/rgbhex.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,105 @@
+# 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: rgbhex.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: rgbhex.py,v 1.10 2007/02/19 16:33:20 digitalxero Exp $
+#
+# Description: rgb to hex utility
+
+from orpg.orpg_wx import *
+
+
+#####################
+## RPGHex Tool
+#####################
+
+class RGBHex:
+    "Tools for Converting from hex to rgb and versa vicea"
+
+    def rgb_tuple(self,hexnum):
+        red = self.c2rgb(hexnum[1:3])
+        green = self.c2rgb(hexnum[3:5])
+        blue = self.c2rgb(hexnum[5:7])
+        #print "Converted %s to %s, %s, %s" % (hexnum, red, green, blue)
+        return (red, green, blue)
+
+    def hexstring(self, red, green, blue):
+        hexcolor = "#" + self.c2hex(red)
+        hexcolor = hexcolor + self.c2hex(green)
+        hexcolor = hexcolor + self.c2hex(blue)
+        return hexcolor
+
+    def c2rgb(self,num):
+        "Converts from hex to rgb"
+        first = num[0]
+        second = num[1]
+        s = 0
+        if first == 'a': s = 10 * 16
+        elif first == 'b': s = 11 * 16
+        elif first == 'c': s = 12 * 16
+        elif first == 'd': s = 13 * 16
+        elif first == 'e': s = 14 * 16
+        elif first == 'f': s = 15 * 16
+        else: s = s+ int(first) * 16
+        if second == 'a': s = s + 10
+        elif second == 'b': s = s + 11
+        elif second == 'c': s = s + 12
+        elif second == 'd': s = s + 13
+        elif second == 'e': s = s + 14
+        elif second == 'f': s = s + 15
+        else: s = s + int(second)
+        return s
+
+    def c2hex(self,num):
+        "Converts from RGB to Hex"
+        first = num/16
+        second = num%16
+        s = ""
+        if first == 10: s = s+"a"
+        elif first == 11: s = s+"b"
+        elif first == 12: s = s+"c"
+        elif first == 13: s = s+"d"
+        elif first == 14: s = s+"e"
+        elif first == 15: s = s+"f"
+        else: s = s+ str(first)
+        if second == 10: s = s+"a"
+        elif second == 11: s = s+"b"
+        elif second == 12: s = s+"c"
+        elif second == 13: s = s+"d"
+        elif second == 14: s = s+"e"
+        elif second == 15: s = s+"f"
+        else: s = s+ str(second)
+        return s
+
+    def do_hex_color_dlg(self, parent):
+        data = wx.ColourData()
+        data.SetChooseFull(True)
+        dlg = wx.ColourDialog(parent, data)
+        if dlg.ShowModal() == wx.ID_OK:
+            data = dlg.GetColourData()
+            (red,green,blue) = data.GetColour().Get()
+            hexcolor = self.hexstring(red, green, blue)
+            dlg.Destroy()
+            return hexcolor
+        else:
+            dlg.Destroy()
+            return None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/scriptkit.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,143 @@
+# 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: scriptkit.py
+# Author: Ted Berg
+# Maintainer:
+# Version:
+#   $Id: scriptkit.py,v 1.15 2006/11/04 21:24:22 digitalxero Exp $
+#
+# Description: Class that contains convenience methods for various operations.  Was created with the purpose
+#       of exposing a simple API to users of an as yet uncreated scripting interface.
+#
+
+
+import time
+from orpg.orpg_windows import *
+from orpg.orpg_xml import *
+from orpg.orpg_wx import *
+import orpg.chat.chat_msg
+
+class scriptkit:
+    def __init__(self):
+        """Simple constructor.  It currently only assigns the openrpg reference to a local variable.
+        <ul>
+            <li>openrpg - a reference to the application openrpg object.
+        </ul>
+        """
+        self.chat = open_rpg.get_component( 'chat' )
+        self.map = open_rpg.get_component( 'map' )
+        self.settings = open_rpg.get_component( 'settings' )
+        self.session = open_rpg.get_component('session')
+        self.xml = open_rpg.get_component('xml')
+
+    def addMiniatureToMap( self, min_label, min_url, unique=0 ):
+        """Adds a new miniature icon to the map.  Miniature <em>will</em> be labeled unless autolabel is
+        turned off on the map <em>and</em> the min_label field is blank.  Miniature will be numbered unless
+        the 'unique' argument evaluates to True ( i.e. nonzero or a non-empty string ).
+        <ul>
+            <li>min_label - text string to be used as a label for the miniature
+            <li>min_url - the URL for the image to be displayed on the map
+            <li>unique - the mini will be numbered if this evaluates to False.
+        </ul>
+        """
+
+        if min_url[:7] != "http://" :
+            min_url = "http://"+min_url
+
+        if self.map.canvas.auto_label:
+            if min_label == '':
+                start = min_url.rfind('/') + 1
+                min_label = min_url[ start : len( min_url ) - 4 ]
+
+            try:
+                unique = eval( unique )
+            except:
+                pass
+
+            if not unique:
+                min_label = '%s %d' % ( min_label, self.map.canvas.layers['miniatures'].next_serial() )
+        self.map.canvas.add_miniature( min_url, label, unique )
+
+    def become( self, name ):
+        try:
+            self.chat.aliasList.SetStringSelection(name)
+        except:
+            msg = 'Alias: %s Does not exist' % (name)
+            print msg
+
+    def sendToChat( self, text ):
+        """Broadcasts the specified text to the chatbuffer.
+        <ul>
+            <li>text - the text to send.
+        </ul>
+        """
+        if text[0] != "/":
+            self.chat.ParsePost(text, send=1, myself=1)
+        else:
+            self.chat.chat_cmds.docmd(text)
+
+    def sendToChatAs( self, name, text ):
+        """Broadcasts the specified text to the chatbuffer as the specified alias
+        <ul>
+            <li>name - The player's name will be temporarily changed to this value
+            <li>text - The text to broadcast to the chatbuffer
+        </ul>
+        """
+        self.become(name)
+        self.sendToChat( text )
+        self.become("Use Real Name")
+
+    def emoteToChat( self, text):
+        if text[0] != '/':
+            text = '/me ' + text
+        self.sendToChat( text )
+
+    def emoteToChatAs( self, name, text ):
+        text = '/me ' + text
+        self.become(name)
+        self.sendToChat( text )
+        self.become("Use Real Name")
+
+    def whisperToChat( self, who, text):
+        if text[0] != '/':
+            text = '/w %s=%s' % ( who, text )
+        self.sendToChat( text )
+
+    def whisperToChatAs( self, who, name, text ):
+        if text[0] != '/':
+            text = '/w %s=%s' % ( who, text )
+        self.become(name)
+        self.sendToChat( text )
+        self.become("Use Real Name")
+
+    def chatMessage( self, message ):
+        self.chat.Post( self.chat.colorize( self.chat.syscolor, message ) )
+
+    def get_input( self, message, title, default ):
+        dlg = wx.TextEntryDialog( self, message, title, default )
+        if dlg.ShowModal() == rpgutils.wx.ID_OK:
+            return dlg.GetValue()
+        dlg.Destroy()
+        return None
+
+    def show_info( self, title, message ):
+        dlg = wx.MessageDialog( None, message, title, wx.OK | wx.ICON_INFORMATION )
+        dlg.ShowModal()
+        dlg.Destroy()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/server_probe.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,147 @@
+#!/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: server_probe.py
+# Author: Chris Davis
+# Maintainer:
+# Version:
+#   $Id: server_probe.py,v 1.10 2006/11/04 21:24:22 digitalxero Exp $
+#
+# Description: This is a periodic maintenance script for removing
+# Unresponsive servers from the meta list.
+#
+
+from threading import Event
+from orpg.networking import mplay_client
+from orpg.networking import meta_server_lib
+import time
+
+
+
+class server_probe:
+    def __init__(self):
+        self.evts = { }
+        self.evts['on_receive'] = self.on_receive
+        self.evts['on_mplay_event'] = self.on_mplay_event
+        self.evts['on_group_event'] = self.on_group_event
+        self.evts['on_player_event'] = self.on_player_event
+        self.evts['on_status_event'] = self.on_status_event
+        self.session = mplay_client.mplay_client("Server Probe",self.evts)
+        self.removed=0
+        self.ok =0
+        self.lock = Event()
+
+
+    def probe_servers(self):
+        names = []
+        addresses = []
+        node_list = None
+        try:
+            xml_dom = meta_server_lib.get_server_list();
+            node_list = xml_dom.getElementsByTagName('server')
+            for n in node_list:
+                address = n.getAttribute('address')
+                name = n.getAttribute( 'name' )
+                id = n.getAttribute('id')
+                if address not in addresses and name not in names:
+                    names.append( name )
+                    addresses.append( address )
+                    self.probe_server(address,id)
+                else:
+                    # If we are here, we found a duplicate
+                    print "Duplicate entry, \"" + name + "\", is being removed."
+                    self.removed = self.removed + 1
+                    meta_server_lib.remove_server(id)
+
+        except:
+            print "An exception has occured.  Attempting to ignore it..."
+
+        print "\n\nServers probe done "
+        if node_list != None:
+            print "Total Servers:" + str(len(node_list))
+        print "servers removed: " + str(self.removed)
+        print "servers ok: " + str(self.ok)
+
+
+    def probe_server(self,address,id):
+        print "trying server: " + address
+
+        if address == "asdfasdf":   # replace with address of server to force from list
+            print "Forced removal of server!!!!!!!!!!"
+            meta_server_lib.remove_server(id)
+        else:
+            if self.session.connect(address):
+                self.lock.wait( timeout=20 )
+                self.session.start_disconnect()
+                while self.session.is_connected():
+                    time.sleep( 1 )
+                    self.session.check_my_status()
+                print "server: " + address + " ok\n"
+                self.ok = self.ok + 1
+                print "disconnected from valid server."
+            else:
+                print "**********>failed connnection!"
+                print "**********>removng server " + address + "\n"
+                self.removed = self.removed + 1
+                meta_server_lib.remove_server(id)
+ ##               meta_server_lib.post_failed_connection( id )
+
+        while self.session.is_connected():
+            time.sleep(1)
+
+
+    def on_receive( self, evt, data ):
+        """Not used
+        """
+        self.lock.set()
+
+
+
+    def on_mplay_event( self, evt ):
+        """Not used
+        """
+        self.lock.set()
+
+
+
+    def on_group_event( self, evt ):
+        """Not used
+        """
+        self.lock.set()
+
+
+
+    def on_player_event( self, evt ):
+        """Disconnects from the server if a 'new player' event is generated.
+        """
+        self.lock.set()
+
+    def on_status_event( self, evt ):
+        """Not used
+        """
+        self.lock.set()
+
+
+
+
+if __name__ == "__main__":
+    probe = server_probe()
+    probe.probe_servers()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/toolBars.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,159 @@
+# 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: toolBars.py
+# Author: Greg Copeland
+# Maintainer:
+#
+# Description: Contains all of the toolbars used in the application.
+#
+#
+
+__version__ = "$Id: toolBars.py,v 1.13 2006/11/04 21:24:22 digitalxero Exp $"
+
+
+##
+## Module Loading
+##
+from inputValidator import *
+import string
+import orpg.dirpath
+
+# DICE stuff
+TB_IDC_D4 = wx.NewId()
+TB_IDC_D6 = wx.NewId()
+TB_IDC_D8 = wx.NewId()
+TB_IDC_D10 = wx.NewId()
+TB_IDC_D12 = wx.NewId()
+TB_IDC_D20 = wx.NewId()
+TB_IDC_D100 = wx.NewId()
+TB_IDC_NUMDICE = wx.NewId()
+TB_IDC_MODS = wx.NewId()
+
+# MAP stuff
+TB_MAP_MODE = wx.NewId()
+# Caution: the use of wxFRAME_TOOL_WINDOW screws up the window on GTK.  Please don't use!!!
+
+class MapToolBar(wx.Panel):
+    """This is where all of the map related tools belong for quick reference."""
+    def __init__( self, parent, id=-1, title="Map Tool Bar", size= wx.Size(300, 45), callBack=None ):
+        wx.Panel.__init__(self, parent, id, size=size)
+        self.callback = callBack
+        self.mapmode = 1
+        self.modeicons = [orpg.dirpath.dir_struct["icon"]+"move.gif",
+            orpg.dirpath.dir_struct["icon"]+"draw.gif",
+            orpg.dirpath.dir_struct["icon"]+"tape.gif"]
+        # Make a sizer for everything to belong to
+        self.sizer = wx.BoxSizer( wx.HORIZONTAL )
+        bm = wx.Image(orpg.dirpath.dir_struct["icon"]+"move.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        self.butt = wx.BitmapButton( self, TB_MAP_MODE, bm )
+        self.sizer.Add( self.butt,0, wx.ALIGN_CENTER )
+        self.Bind(wx.EVT_BUTTON, self.onToolBarClick, id=TB_MAP_MODE)
+        # Build the toolbar now
+        # Stubbed, but nothing here yet!
+        # Now, attach the sizer to the panel and tell it to do it's magic
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+    def onToolBarClick(self,evt):
+        data = ""
+        id = evt.GetId()
+        data = ""
+        mode = 1
+        if id == TB_MAP_MODE:
+            mode = 1
+            self.mapmode +=1
+            if self.mapmode >3:
+                self.mapmode = 1
+            bm = wx.Image(self.modeicons[self.mapmode-1],wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+            self.butt= wx.BitmapButton(self,TB_MAP_MODE,bm)
+            data = self.mapmode
+        if self.callback != None:
+            self.callback(mode,data)
+
+class DiceToolBar(wx.Panel):
+    """This is where all of the dice related tools belong for quick reference."""
+    def __init__( self, parent, id=-1, title="Dice Tool Bar", size=wx.Size(300, 45), callBack=None ):
+        wx.Panel.__init__(self, parent, id, size=size)
+        # Save our post callback
+        self.callBack = callBack
+        # Make a sizer for everything to belong to
+        self.sizer = wx.BoxSizer( wx.HORIZONTAL )
+        # Build the toolbar now
+        self.numDieText = wx.TextCtrl( self, TB_IDC_NUMDICE, "1", size= wx.Size(50, 25),
+                                      validator=MathOnlyValidator() )
+        self.sizer.Add( self.numDieText, 1, wx.EXPAND | wx.ALIGN_LEFT )
+        bm = wx.Image(orpg.dirpath.dir_struct["icon"]+"b_d4.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        butt = wx.BitmapButton( self, TB_IDC_D4, bm, size=(bm.GetWidth(), bm.GetHeight()) )
+        self.sizer.Add( butt, 0, wx.ALIGN_CENTER )
+        self.Bind(wx.EVT_BUTTON, self.onToolBarClick, id=TB_IDC_D4)
+        bm = wx.Image(orpg.dirpath.dir_struct["icon"]+"b_d6.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        butt = wx.BitmapButton( self, TB_IDC_D6, bm, size=(bm.GetWidth(), bm.GetHeight()) )
+        self.sizer.Add( butt, 0, wx.ALIGN_CENTER )
+        self.Bind(wx.EVT_BUTTON, self.onToolBarClick, id=TB_IDC_D6)
+        bm = wx.Image(orpg.dirpath.dir_struct["icon"]+"b_d8.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        butt = wx.BitmapButton( self, TB_IDC_D8, bm, size=(bm.GetWidth(), bm.GetHeight()) )
+        self.sizer.Add( butt, 0, wx.ALIGN_CENTER )
+        self.Bind(wx.EVT_BUTTON, self.onToolBarClick, id=TB_IDC_D8)
+        bm = wx.Image(orpg.dirpath.dir_struct["icon"]+"b_d10.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        butt = wx.BitmapButton( self, TB_IDC_D10, bm, size=(bm.GetWidth(), bm.GetHeight()) )
+        self.sizer.Add( butt, 0, wx.ALIGN_CENTER )
+        self.Bind(wx.EVT_BUTTON, self.onToolBarClick, id=TB_IDC_D10)
+        bm = wx.Image(orpg.dirpath.dir_struct["icon"]+"b_d12.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        butt = wx.BitmapButton( self, TB_IDC_D12, bm, size=(bm.GetWidth(), bm.GetHeight()) )
+        self.sizer.Add( butt, 0, wx.ALIGN_CENTER )
+        self.Bind(wx.EVT_BUTTON, self.onToolBarClick, id=TB_IDC_D12)
+        bm = wx.Image(orpg.dirpath.dir_struct["icon"]+"b_d20.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        butt = wx.BitmapButton( self, TB_IDC_D20, bm, size=(bm.GetWidth(), bm.GetHeight()) )
+        self.sizer.Add( butt, 0, wx.ALIGN_CENTER )
+        self.Bind(wx.EVT_BUTTON, self.onToolBarClick, id=TB_IDC_D20)
+        bm = wx.Image(orpg.dirpath.dir_struct["icon"]+"b_d100.gif", wx.BITMAP_TYPE_GIF).ConvertToBitmap()
+        butt = wx.BitmapButton( self, TB_IDC_D100, bm, size=(bm.GetWidth(), bm.GetHeight()) )
+        self.sizer.Add( butt, 0, wx.ALIGN_CENTER )
+        self.Bind(wx.EVT_BUTTON, self.onToolBarClick, id=TB_IDC_D100)
+        # Add our other text control to the sizer
+        self.dieModText = wx.TextCtrl( self, TB_IDC_MODS, "+0", size= wx.Size(50, 25),
+                                      validator=MathOnlyValidator() )
+        self.sizer.Add( self.dieModText, 1, wx.EXPAND | wx.ALIGN_RIGHT )
+        # Now, attach the sizer to the panel and tell it to do it's magic
+        self.SetSizer(self.sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+    def onToolBarClick( self, evt ):
+        # Get our modifiers
+        numDie = self.numDieText.GetValue()
+        dieMod = self.dieModText.GetValue()
+        # Init the die roll text
+        if not len(numDie):
+            numDie = 1
+        dieRoll = str(numDie)
+        # Figure out which die roll was selected
+        id = evt.GetId()
+	recycle_bin = {TB_IDC_D4: "d4", TB_IDC_D6: "d6", TB_IDC_D8: "d8", TB_IDC_D10: "d10", TB_IDC_D12: "d12", TB_IDC_D20: "d20", TB_IDC_D100: "d100"}
+	dieType = recycle_bin[id]; recycle_bin = {}
+        # To appease tdb...I personally disagree with this!
+        if len(dieMod) and dieMod[0] not in "*/-+":
+            dieMod = "+" + dieMod
+        # Build the complete die roll text now
+        rollString = "[" + dieRoll + dieType + dieMod + "]"
+        # Now, call the post method to send everything off with
+        if self.callBack != None:
+            self.callBack( rollString,1,1 )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/tools/validate.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,37 @@
+# file: config_files.py
+#
+# Author: Todd Faris (Snowdog)
+# Date:   5/10/2005
+#
+# Misc. config file service methods
+#
+
+import orpg.dirpath
+import os
+
+class Validate:
+    def __init__(self, userpath=None):
+        if userpath is None:
+            userpath = orpg.dirpath.dir_struct["user"]
+        self.__loadUserPath = userpath
+
+    def config_file(self, user_file, template_file):
+        #STEP 1: verify the template exists
+        if (not os.path.exists(orpg.dirpath.dir_struct["template"] + template_file)):
+            return 0
+
+        #STEP 2: verify the user file exists. If it doesn't then create it from template
+        if (not os.path.exists(self.__loadUserPath + user_file)):
+            default = open(orpg.dirpath.dir_struct["template"] + template_file,"r")
+            file = default.read()
+            newfile = open(self.__loadUserPath + user_file,"w")
+            newfile.write(file)
+            default.close()
+            newfile.close()
+            return 2  #returning 2 (True) so calling method will know if file was created
+
+        #STEP 3: user file exists (is openable) return 1 indicating no-create operation required
+        else: return 1
+
+    def ini_entry(self, entry_name, ini_file):
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/orpg/xmltramp.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,323 @@
+"""xmltramp: Make XML documents easily accessible."""
+
+__version__ = "2.16"
+__author__ = "Aaron Swartz"
+__credits__ = "Many thanks to pjz, bitsko, and DanC."
+__copyright__ = "(C) 2003 Aaron Swartz. GNU GPL 2."
+
+if not hasattr(__builtins__, 'True'): True, False = 1, 0
+def isstr(f): return isinstance(f, type('')) or isinstance(f, type(u''))
+def islst(f): return isinstance(f, type(())) or isinstance(f, type([]))
+
+empty = {'http://www.w3.org/1999/xhtml': ['img', 'br', 'hr', 'meta', 'link', 'base', 'param', 'input', 'col', 'area']}
+
+def quote(x, elt=True):
+    if elt and '<' in x and len(x) > 24 and x.find(']]>') == -1: return "<![CDATA["+x+"]]>"
+    else: x = x.replace('&', '&amp;').replace('<', '&lt;').replace(']]>', ']]&gt;')
+    if not elt: x = x.replace('"', '&quot;')
+    return x
+
+class Element:
+    def __init__(self, name, attrs=None, children=None, prefixes=None):
+        if islst(name) and name[0] == None: name = name[1]
+        if attrs:
+            na = {}
+            for k in attrs.keys():
+                if islst(k) and k[0] == None: na[k[1]] = attrs[k]
+                else: na[k] = attrs[k]
+            attrs = na
+        self._name = name
+        self._attrs = attrs or {}
+        self._dir = children or []
+        prefixes = prefixes or {}
+        self._prefixes = dict(zip(prefixes.values(), prefixes.keys()))
+        if prefixes: self._dNS = prefixes.get(None, None)
+        else: self._dNS = None
+
+    def __repr__(self, recursive=0, multiline=0, inprefixes=None):
+        def qname(name, inprefixes):
+            if islst(name):
+                if inprefixes[name[0]] is not None:
+                    return inprefixes[name[0]]+':'+name[1]
+                else:
+                    return name[1]
+            else:
+                return name
+
+        def arep(a, inprefixes, addns=1):
+            out = ''
+            for p in self._prefixes.keys():
+                if not p in inprefixes.keys():
+                    if addns: out += ' xmlns'
+                    if addns and self._prefixes[p]: out += ':'+self._prefixes[p]
+                    if addns: out += '="'+quote(p, False)+'"'
+                    inprefixes[p] = self._prefixes[p]
+            for k in a.keys():
+                out += ' ' + qname(k, inprefixes)+ '="' + quote(a[k], False) + '"'
+            return out
+        inprefixes = inprefixes or {u'http://www.w3.org/XML/1998/namespace':'xml'}
+
+        # need to call first to set inprefixes:
+        attributes = arep(self._attrs, inprefixes, recursive)
+        out = '<' + qname(self._name, inprefixes)  + attributes
+        if not self._dir and (self._name[0] in empty.keys()
+          and self._name[1] in empty[self._name[0]]):
+            out += ' />'
+            return out
+        out += '>'
+        if recursive:
+            content = 0
+            for x in self._dir:
+                if isinstance(x, Element): content = 1
+            pad = '\n' + ('\t' * recursive)
+            for x in self._dir:
+                if multiline and content: out +=  pad
+                if isstr(x): out += quote(x)
+                elif isinstance(x, Element):
+                    out += x.__repr__(recursive+1, multiline, inprefixes.copy())
+                else:
+                    raise TypeError, "I wasn't expecting "+`x`+"."
+            if multiline and content: out += '\n' + ('\t' * (recursive-1))
+        else:
+            if self._dir: out += '...'
+        out += '</'+qname(self._name, inprefixes)+'>'
+        return out
+
+    def __unicode__(self):
+        text = ''
+        for x in self._dir:
+            text += unicode(x)
+        return ' '.join(text.split())
+
+    def __str__(self):
+        return self.__unicode__().encode('utf-8')
+
+    def __getattr__(self, n):
+        if n[0] == '_': raise AttributeError, "Use foo['"+n+"'] to access the child element."
+        if self._dNS: n = (self._dNS, n)
+        for x in self._dir:
+            if isinstance(x, Element) and x._name == n: return x
+        raise AttributeError, 'No child element named \''+n+"'"
+
+    def __hasattr__(self, n):
+        for x in self._dir:
+            if isinstance(x, Element) and x._name == n: return True
+        return False
+
+    def __setattr__(self, n, v):
+        if n[0] == '_': self.__dict__[n] = v
+        else: self[n] = v
+
+    def __getitem__(self, n):
+        if isinstance(n, type(0)): # d[1] == d._dir[1]
+            return self._dir[n]
+        elif isinstance(n, slice(0).__class__):
+            # numerical slices
+            if isinstance(n.start, type(0)): return self._dir[n.start:n.stop]
+            # d['foo':] == all <foo>s
+            n = n.start
+            if self._dNS and not islst(n): n = (self._dNS, n)
+            out = []
+            for x in self._dir:
+                if isinstance(x, Element) and x._name == n: out.append(x)
+            return out
+        else: # d['foo'] == first <foo>
+            if self._dNS and not islst(n): n = (self._dNS, n)
+            for x in self._dir:
+                if isinstance(x, Element) and x._name == n: return x
+            raise KeyError
+
+    def __setitem__(self, n, v):
+        if isinstance(n, type(0)): # d[1]
+            self._dir[n] = v
+        elif isinstance(n, slice(0).__class__):
+            # d['foo':] adds a new foo
+            n = n.start
+            if self._dNS and not islst(n): n = (self._dNS, n)
+            nv = Element(n)
+            self._dir.append(nv)
+
+        else: # d["foo"] replaces first <foo> and dels rest
+            if self._dNS and not islst(n): n = (self._dNS, n)
+            nv = Element(n); nv._dir.append(v)
+            replaced = False
+            todel = []
+            for i in range(len(self)):
+                if self[i]._name == n:
+                    if replaced:
+                        todel.append(i)
+                    else:
+                        self[i] = nv
+                        replaced = True
+            if not replaced: self._dir.append(nv)
+            for i in todel: del self[i]
+
+    def __delitem__(self, n):
+        if isinstance(n, type(0)): del self._dir[n]
+        elif isinstance(n, slice(0).__class__):
+            # delete all <foo>s
+            n = n.start
+            if self._dNS and not islst(n): n = (self._dNS, n)
+            for i in range(len(self)):
+                if self[i]._name == n: del self[i]
+        else:
+            # delete first foo
+            for i in range(len(self)):
+                if self[i]._name == n: del self[i]
+                break
+
+    def __call__(self, *_pos, **_set):
+        if _set:
+            for k in _set.keys(): self._attrs[k] = _set[k]
+        if len(_pos) > 1:
+            for i in range(0, len(_pos), 2):
+                self._attrs[_pos[i]] = _pos[i+1]
+        if len(_pos) == 1 is not None:
+            return self._attrs[_pos[0]]
+        if len(_pos) == 0:
+            return self._attrs
+
+    def __len__(self): return len(self._dir)
+
+class Namespace:
+    def __init__(self, uri): self.__uri = uri
+    def __getattr__(self, n): return (self.__uri, n)
+    def __getitem__(self, n): return (self.__uri, n)
+
+from xml.sax.handler import EntityResolver, DTDHandler, ContentHandler, ErrorHandler
+
+class Seeder(EntityResolver, DTDHandler, ContentHandler, ErrorHandler):
+    def __init__(self):
+        self.stack = []
+        self.ch = ''
+        self.prefixes = {}
+        ContentHandler.__init__(self)
+
+    def startPrefixMapping(self, prefix, uri):
+        if not self.prefixes.has_key(prefix): self.prefixes[prefix] = []
+        self.prefixes[prefix].append(uri)
+    def endPrefixMapping(self, prefix):
+        self.prefixes[prefix].pop()
+
+    def startElementNS(self, name, qname, attrs):
+        ch = self.ch; self.ch = ''
+        if ch and not ch.isspace(): self.stack[-1]._dir.append(ch)
+        attrs = dict(attrs)
+        newprefixes = {}
+        for k in self.prefixes.keys(): newprefixes[k] = self.prefixes[k][-1]
+        self.stack.append(Element(name, attrs, prefixes=newprefixes.copy()))
+
+    def characters(self, ch):
+        self.ch += ch
+
+    def endElementNS(self, name, qname):
+        ch = self.ch; self.ch = ''
+        if ch and not ch.isspace(): self.stack[-1]._dir.append(ch)
+        element = self.stack.pop()
+        if self.stack:
+            self.stack[-1]._dir.append(element)
+        else:
+            self.result = element
+
+from xml.sax import make_parser
+from xml.sax.handler import feature_namespaces
+
+def seed(fileobj):
+    seeder = Seeder()
+    parser = make_parser()
+    parser.setFeature(feature_namespaces, 1)
+    parser.setContentHandler(seeder)
+    parser.parse(fileobj)
+    return seeder.result
+
+def parse(text):
+    from StringIO import StringIO
+    return seed(StringIO(text))
+
+def load(url):
+    import urllib
+    return seed(urllib.urlopen(url))
+
+def unittest():
+    parse('<doc>a<baz>f<b>o</b>ob<b>a</b>r</baz>a</doc>').__repr__(1,1) == \
+      '<doc>\n\ta<baz>\n\t\tf<b>o</b>ob<b>a</b>r\n\t</baz>a\n</doc>'
+    assert str(parse("<doc />")) == ""
+    assert str(parse("<doc>I <b>love</b> you.</doc>")) == "I love you."
+    assert parse("<doc>\nmom\nwow\n</doc>")[0].strip() == "mom\nwow"
+    assert str(parse('<bing>  <bang> <bong>center</bong> </bang>  </bing>')) == "center"
+    assert str(parse('<doc>\xcf\x80</doc>')) == '\xcf\x80'
+    d = Element('foo', attrs={'foo':'bar'}, children=['hit with a', Element('bar'), Element('bar')])
+
+    try:
+        d._doesnotexist
+        raise "ExpectedError", "but found success. Damn."
+    except AttributeError: pass
+    assert d.bar._name == 'bar'
+    try:
+        d.doesnotexist
+        raise "ExpectedError", "but found success. Damn."
+    except AttributeError: pass
+    assert hasattr(d, 'bar') == True
+    assert d('foo') == 'bar'
+    d(silly='yes')
+    assert d('silly') == 'yes'
+    assert d() == d._attrs
+    assert d[0] == 'hit with a'
+    d[0] = 'ice cream'
+    assert d[0] == 'ice cream'
+    del d[0]
+    assert d[0]._name == "bar"
+    assert len(d[:]) == len(d._dir)
+    assert len(d[1:]) == len(d._dir) - 1
+    assert len(d['bar':]) == 2
+    d['bar':] = 'baz'
+    assert len(d['bar':]) == 3
+    assert d['bar']._name == 'bar'
+    d = Element('foo')
+    doc = Namespace("http://example.org/bar")
+    bbc = Namespace("http://example.org/bbc")
+    dc = Namespace("http://purl.org/dc/elements/1.1/")
+    d = parse("""<doc version="2.7182818284590451"
+      xmlns="http://example.org/bar"
+      xmlns:dc="http://purl.org/dc/elements/1.1/"
+      xmlns:bbc="http://example.org/bbc">
+            <author>John Polk and John Palfrey</author>
+            <dc:creator>John Polk</dc:creator>
+            <dc:creator>John Palfrey</dc:creator>
+            <bbc:show bbc:station="4">Buffy</bbc:show>
+    </doc>""")
+    assert repr(d) == '<doc version="2.7182818284590451">...</doc>'
+    assert d.__repr__(1) == '<doc xmlns:bbc="http://example.org/bbc" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://example.org/bar" version="2.7182818284590451"><author>John Polk and John Palfrey</author><dc:creator>John Polk</dc:creator><dc:creator>John Palfrey</dc:creator><bbc:show bbc:station="4">Buffy</bbc:show></doc>'
+    assert d.__repr__(1,1) == '<doc xmlns:bbc="http://example.org/bbc" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://example.org/bar" version="2.7182818284590451">\n\t<author>John Polk and John Palfrey</author>\n\t<dc:creator>John Polk</dc:creator>\n\t<dc:creator>John Palfrey</dc:creator>\n\t<bbc:show bbc:station="4">Buffy</bbc:show>\n</doc>'
+    assert repr(parse("<doc xml:lang='en' />")) == '<doc xml:lang="en"></doc>'
+    assert str(d.author) == str(d['author']) == "John Polk and John Palfrey"
+    assert d.author._name == doc.author
+    assert str(d[dc.creator]) == "John Polk"
+    assert d[dc.creator]._name == dc.creator
+    assert str(d[dc.creator:][1]) == "John Palfrey"
+    d[dc.creator] = "Me!!!"
+    assert str(d[dc.creator]) == "Me!!!"
+    assert len(d[dc.creator:]) == 1
+    d[dc.creator:] = "You!!!"
+    assert len(d[dc.creator:]) == 2
+    assert d[bbc.show](bbc.station) == "4"
+    d[bbc.show](bbc.station, "5")
+    assert d[bbc.show](bbc.station) == "5"
+    e = Element('e')
+    e.c = '<img src="foo">'
+    assert e.__repr__(1) == '<e><c>&lt;img src="foo"></c></e>'
+    e.c = '2 > 4'
+    assert e.__repr__(1) == '<e><c>2 > 4</c></e>'
+    e.c = 'CDATA sections are <em>closed</em> with ]]>.'
+    assert e.__repr__(1) == '<e><c>CDATA sections are &lt;em>closed&lt;/em> with ]]&gt;.</c></e>'
+    e.c = parse('<div xmlns="http://www.w3.org/1999/xhtml">i<br /><span></span>love<br />you</div>')
+    assert e.__repr__(1) == '<e><c><div xmlns="http://www.w3.org/1999/xhtml">i<br /><span></span>love<br />you</div></c></e>'
+    e = Element('e')
+    e('c', 'that "sucks"')
+    assert e.__repr__(1) == '<e c="that &quot;sucks&quot;"></e>'
+    assert quote("]]>") == "]]&gt;"
+    assert quote('< dkdkdsd dkd sksdksdfsd fsdfdsf]]> kfdfkg >') == '&lt; dkdkdsd dkd sksdksdfsd fsdfdsf]]&gt; kfdfkg >'
+    assert parse('<x a="&lt;"></x>').__repr__(1) == '<x a="&lt;"></x>'
+    assert parse('<a xmlns="http://a"><b xmlns="http://b"/></a>').__repr__(1) == '<a xmlns="http://a"><b xmlns="http://b"></b></a>'
+
+if __name__ == '__main__': unittest()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/platform.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1217 @@
+#!/usr/local/bin/python
+
+""" This module tries to retrieve as much platform identifying data as
+    possible. It makes this information available via function APIs.
+
+    If called from the command line, it prints the platform
+    information concatenated as single string to stdout. The output
+    format is useable as part of a filename.
+
+    Note that this module is a fast moving target. I plan to release
+    version 1.0 as the final version.
+
+    Still needed:
+    - more support for WinCE
+    - support for MS-DOS (PythonDX ?)
+    - support for Amiga and other still unsupported platforms running Python
+    - support for additional Linux distributions
+
+    Many thanks to all those who helped adding platform specific
+    checks (in no particular order):
+
+      Charles G Waldman, David Arnold, Gordon McMillan, Ben Darnell,
+      Jeff Bauer, Cliff Crawford, Ivan Van Laningham, Josef
+      Betancourt, Randall Hopper, Karl Putland, John Farrell, Greg
+      Andruk, Just van Rossum, Thomas Heller, Mark R. Levinson, Mark
+      Hammond, Bill Tutt, Hans Nowak, Uwe Zessin (OpenVMS support),
+      Colin Kong, Trent Mick
+
+    History:
+    0.8.0 - added sys.version parser and various new access
+            APIs (python_version(), python_compiler(), etc.)
+    0.7.2 - fixed architecture() to use sizeof(pointer) where available
+    0.7.1 - added support for Caldera OpenLinux
+    0.7.0 - some fixes for WinCE; untabified the source file
+    0.6.2 - support for OpenVMS - requires version 1.5.2-V006 or higher and
+            vms_lib.getsyi() configured
+    0.6.1 - added code to prevent 'uname -p' on platforms which are
+            known not to support it
+    0.6.0 - fixed win32_ver() to hopefully work on Win95,98,NT and Win2k;
+            did some cleanup of the interfaces - some APIs have changed
+    0.5.5 - fixed another type in the MacOS code... should have
+            used more coffee today ;-)
+    0.5.4 - fixed a few typos in the MacOS code
+    0.5.3 - added experimental MacOS support; added better popen()
+            workarounds in _syscmd_ver() -- still not 100% elegant
+            though
+    0.5.2 - fixed uname() to return '' instead of 'unkown' in all
+            return values (the system uname command tends to return
+            'unkown' instead of just leaving the field emtpy)
+    0.5.1 - included code for slackware dist; added exception handlers
+            to cover up situations where platforms don't have os.popen
+            (e.g. Mac) or fail on socket.gethostname(); fixed libc
+            detection RE
+    0.5.0 - changed the API names referring to system commands to *syscmd*;
+            added java_ver(); made syscmd_ver() a private
+            API (was system_ver() in previous versions) -- use uname()
+            instead; extended the win32_ver() to also return processor
+            type information
+    0.4.0 - added win32_ver() and modified the platform() output for WinXX
+    0.3.4 - fixed a bug in _follow_symlinks()
+    0.3.3 - fixed popen() and "file" command invokation bugs
+    0.3.2 - added architecture() API and support for it in platform()
+    0.3.1 - fixed syscmd_ver() RE to support Windows NT
+    0.3.0 - added system alias support
+    0.2.3 - removed 'wince' again... oh well.
+    0.2.2 - added 'wince' to syscmd_ver() supported platforms
+    0.2.1 - added cache logic and changed the platform string format
+    0.2.0 - changed the API to use functions instead of module globals
+            since some action take too long to be run on module import
+    0.1.0 - first release
+
+    You can always get the latest version of this module at:
+
+             http://www.egenix.com/files/python/platform.py
+
+    If that URL should fail, try contacting the author.
+
+    ----------------------------------------------------------------------
+
+    Copyright (c) 1999-2000, Marc-Andre Lemburg; mailto:mal@lemburg.com
+    Copyright (c) 2000-2001, eGenix.com Software GmbH; mailto:info@egenix.com
+
+    Permission to use, copy, modify, and distribute this software and its
+    documentation for any purpose and without fee or royalty is hereby granted,
+    provided that the above copyright notice appear in all copies and that
+    both that copyright notice and this permission notice appear in
+    supporting documentation or portions thereof, including modifications,
+    that you make.
+
+    EGENIX.COM SOFTWARE GMBH DISCLAIMS ALL WARRANTIES WITH REGARD TO
+    THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+    FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
+    INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+    FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+    NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+    WITH THE USE OR PERFORMANCE OF THIS SOFTWARE !
+
+"""
+
+__version__ = '0.8.0'
+
+import sys,string,os,re
+
+### Platform specific APIs
+
+def libc_ver(executable=sys.executable,lib='',version='',
+
+             chunksize=2048,
+             libc_search=re.compile('(__libc_init)'
+                                    '|'
+                                    '(GLIBC_([0-9.]+))'
+                                    '|'
+                                    '(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)'
+                                    )
+         ):
+
+    """ Tries to determine the libc version against which the
+        file executable (defaults to the Python interpreter) is linked.
+
+        Returns a tuple of strings (lib,version) which default to the
+        given parameters in case the lookup fails.
+
+        Note that the function has intimate knowledge of how different
+        libc versions add symbols to the executable is probably only
+        useable for executables compiled using gcc.
+
+        The file is read and scanned in chunks of chunksize bytes.
+
+    """
+    f = open(executable,'rb')
+    binary = f.read(chunksize)
+    pos = 0
+    while 1:
+        m = libc_search.search(binary,pos)
+        if not m:
+            binary = f.read(chunksize)
+            if not binary:
+                break
+            pos = 0
+            continue
+        libcinit,glibc,glibcversion,so,threads,soversion = m.groups()
+        if libcinit and not lib:
+            lib = 'libc'
+        elif glibc:
+            if lib != 'glibc':
+                lib = 'glibc'
+                version = glibcversion
+            elif glibcversion > version:
+                version = glibcversion
+        elif so:
+            if lib != 'glibc':
+                lib = 'libc'
+                if soversion > version:
+                    version = soversion
+                if threads and version[-len(threads):] != threads:
+                    version = version + threads
+        pos = m.end()
+    f.close()
+    return lib,version
+
+def _dist_try_harder(distname,version,id):
+
+    """ Tries some special tricks to get the distribution
+        information in case the default method fails.
+
+        Currently supports older SuSE Linux, Caldera OpenLinux and
+        Slackware Linux distributions.
+
+    """
+    if os.path.exists('/var/adm/inst-log/info'):
+        # SuSE Linux stores distribution information in that file
+        info = open('/var/adm/inst-log/info').readlines()
+        distname = 'SuSE'
+        for line in info:
+            tv = string.split(line)
+            if len(tv) == 2:
+                tag,value = tv
+            else:
+                continue
+            if tag == 'MIN_DIST_VERSION':
+                version = string.strip(value)
+            elif tag == 'DIST_IDENT':
+                values = string.split(value,'-')
+                id = values[2]
+        return distname,version,id
+
+    if os.path.exists('/etc/.installed'):
+        # Caldera OpenLinux has some infos in that file (thanks to Colin Kong)
+        info = open('/etc/.installed').readlines()
+        for line in info:
+            pkg = string.split(line,'-')
+            if len(pkg) >= 2 and pkg[0] == 'OpenLinux':
+                # XXX does Caldera support non Intel platforms ? If yes,
+                #     where can we find the needed id ?
+                return 'OpenLinux',pkg[1],id
+
+    if os.path.isdir('/usr/lib/setup'):
+        # Check for slackware verson tag file (thanks to Greg Andruk)
+        verfiles = os.listdir('/usr/lib/setup')
+        for n in range(len(verfiles)-1, -1, -1):
+            if verfiles[n][:14] != 'slack-version-':
+                del verfiles[n]
+        if verfiles:
+            verfiles.sort()
+            distname = 'slackware'
+            version = verfiles[-1][14:]
+            return distname,version,id
+
+    return distname,version,id
+
+def dist(distname='',version='',id='',
+
+         supported_dists=('SuSE','debian','redhat','mandrake'),
+         release_filename=re.compile('(\w+)[-_](release|version)'),
+         release_version=re.compile('([\d.]+)[^(]*(?:\((.+)\))?')):
+
+    """ Tries to determine the name of the OS distribution name
+
+        The function first looks for a distribution release file in
+        /etc and then reverts to _dist_try_harder() in case no
+        suitable files are found.
+
+        Returns a tuple distname,version,id which default to the
+        args given as parameters.
+
+    """
+    try:
+        etc = os.listdir('/etc')
+    except os.error:
+        # Probably not a Unix system
+        return distname,version,id
+    for file in etc:
+        m = release_filename.match(file)
+        if m:
+            _distname,dummy = m.groups()
+            if _distname in supported_dists:
+                distname = _distname
+                break
+    else:
+        return _dist_try_harder(distname,version,id)
+    f = open('/etc/'+file,'r')
+    firstline = f.readline()
+    f.close()
+    m = release_version.search(firstline)
+    if m:
+        _version,_id = m.groups()
+        if _version:
+            version = _version
+        if _id:
+            id = _id
+    else:
+        # Unkown format... take the first two words
+        l = string.split(string.strip(firstline))
+        if l:
+            version = l[0]
+            if len(l) > 1:
+                id = l[1]
+    return distname,version,id
+
+class _popen:
+
+    """ Fairly portable (alternative) popen implementation.
+
+        This is mostly needed in case os.popen() is not available, or
+        doesn't work as advertised, e.g. in Win9X GUI programs like
+        PythonWin or IDLE.
+
+        XXX Writing to the pipe is currently not supported.
+
+    """
+    tmpfile = ''
+    pipe = None
+    bufsize = None
+    mode = 'r'
+
+    def __init__(self,cmd,mode='r',bufsize=None):
+
+        if mode != 'r':
+            raise ValueError,'popen()-emulation only supports read mode'
+        import tempfile
+        self.tmpfile = tmpfile = tempfile.mktemp()
+        os.system(cmd + ' > %s' % tmpfile)
+        self.pipe = open(tmpfile,'rb')
+        self.bufsize = bufsize
+        self.mode = mode
+
+    def read(self):
+
+        return self.pipe.read()
+
+    def readlines(self):
+
+        if self.bufsize is not None:
+            return self.pipe.readlines()
+
+    def close(self,
+
+              remove=os.unlink,error=os.error):
+
+        if self.pipe:
+            rc = self.pipe.close()
+        else:
+            rc = 255
+        if self.tmpfile:
+            try:
+                remove(self.tmpfile)
+            except error:
+                pass
+        return rc
+
+    # Alias
+    __del__ = close
+
+def popen(cmd, mode='r', bufsize=None):
+
+    """ Portable popen() interface.
+    """
+    # Find a working popen implementation preferring win32pipe.popen
+    # over os.popen over _popen
+    popen = None
+    if os.environ.get('OS','') == 'Windows_NT':
+        # On NT win32pipe should work; on Win9x it hangs due to bugs
+        # in the MS C lib (see MS KnowledgeBase article Q150956)
+        try:
+            import win32pipe
+        except ImportError:
+            pass
+        else:
+            popen = win32pipe.popen
+    if popen is None:
+        if hasattr(os,'popen'):
+            popen = os.popen
+            # Check whether it works... it doesn't in GUI programs
+            # on Windows platforms
+            if sys.platform == 'win32': # XXX Others too ?
+                try:
+                    popen('')
+                except os.error:
+                    popen = _popen
+        else:
+            popen = _popen
+    if bufsize is None:
+        return popen(cmd,mode)
+    else:
+        return popen(cmd,mode,bufsize)
+
+def _norm_version(version,build=''):
+
+    """ Normalize the version and build strings and return a sinlge
+        vesion string using the format major.minor.build (or patchlevel).
+    """
+    l = string.split(version,'.')
+    if build:
+        l.append(build)
+    try:
+        ints = map(int,l)
+    except ValueError:
+        strings = l
+    else:
+        strings = map(str,ints)
+    version = string.join(strings[:3],'.')
+    return version
+
+def _syscmd_ver(system='',release='',version='',
+
+               supported_platforms=('win32','win16','dos','os2'),
+               ver_output=re.compile('(?:([\w ]+) ([\w.]+) '
+                                     '.*'
+                                     'Version ([\d.]+))')):
+
+    """ Tries to figure out the OS version used and returns
+        a tuple (system,release,version).
+
+        It uses the "ver" shell command for this which is known
+        to exists on Windows, DOS and OS/2. XXX Others too ?
+
+        In case this fails, the given parameters are used as
+        defaults.
+
+    """
+    if sys.platform not in supported_platforms:
+        return system,release,version
+
+    # Try some common cmd strings
+    for cmd in ('ver','command /c ver','cmd /c ver'):
+        try:
+            pipe = popen(cmd)
+            info = pipe.read()
+            if pipe.close():
+                raise os.error,'command failed'
+            # XXX How can I supress shell errors from being written
+            #     to stderr ?
+        except os.error,why:
+            #print 'Command %s failed: %s' % (cmd,why)
+            continue
+        except IOError,why:
+            #print 'Command %s failed: %s' % (cmd,why)
+            continue
+        else:
+            break
+    else:
+        return system,release,version
+
+    # Parse the output
+    info = string.strip(info)
+    m = ver_output.match(info)
+    if m:
+        system,release,version = m.groups()
+        # Strip trailing dots from version and release
+        if release[-1] == '.':
+            release = release[:-1]
+        if version[-1] == '.':
+            version = version[:-1]
+        # Normalize the version and build strings (eliminating additional
+        # zeros)
+        version = _norm_version(version)
+    return system,release,version
+
+def _win32_getvalue(key,name,default=''):
+
+    """ Read a value for name from the registry key.
+
+        In case this fails, default is returned.
+
+    """
+    from win32api import RegQueryValueEx
+    try:
+        return RegQueryValueEx(key,name)
+    except:
+        return default
+
+def win32_ver(release='',version='',csd='',ptype=''):
+
+    """ Get additional version information from the Windows Registry
+        and return a tuple (version,csd,ptype) referring to version
+        number, CSD level and OS type (multi/single
+        processor).
+
+        As a hint: ptype returns 'Uniprocessor Free' on single
+        processor NT machines and 'Multiprocessor Free' on multi
+        processor machines. The 'Free' refers to the OS version being
+        free of debugging code. It could also state 'Checked' which
+        means the OS version uses debugging code, i.e. code that
+        checks arguments, ranges, etc. (Thomas Heller).
+
+        Note: this functions only works if Mark Hammond's win32
+        package is installed and obviously only runs on Win32
+        compatible platforms.
+
+        XXX Is there any way to find out the processor type on WinXX ?
+
+        XXX Is win32 available on Windows CE ?
+
+        Adapted from code posted by Karl Putland to comp.lang.python.
+
+    """
+    # Import the needed APIs
+    try:
+        import win32api
+    except ImportError:
+        return release,version,csd,ptype
+    from win32api import RegQueryValueEx,RegOpenKeyEx,RegCloseKey,GetVersionEx
+    from win32con import HKEY_LOCAL_MACHINE,VER_PLATFORM_WIN32_NT,\
+                         VER_PLATFORM_WIN32_WINDOWS
+
+    # Find out the registry key and some general version infos
+    maj,min,buildno,plat,csd = GetVersionEx()
+    version = '%i.%i.%i' % (maj,min,buildno & 0xFFFF)
+    if csd[:13] == 'Service Pack ':
+        csd = 'SP' + csd[13:]
+    if plat == VER_PLATFORM_WIN32_WINDOWS:
+        regkey = 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion'
+        # Try to guess the release name
+        if maj == 4:
+            if min == 0:
+                release = '95'
+            else:
+                release = '98'
+        elif maj == 5:
+            release = '2000'
+    elif plat == VER_PLATFORM_WIN32_NT:
+        regkey = 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion'
+        if maj <= 4:
+            release = 'NT'
+        elif maj == 5:
+            release = '2000'
+    else:
+        if not release:
+            # E.g. Win3.1 with win32s
+            release = '%i.%i' % (maj,min)
+        return release,version,csd,ptype
+
+    # Open the registry key
+    try:
+        keyCurVer = RegOpenKeyEx(HKEY_LOCAL_MACHINE,regkey)
+        # Get a value to make sure the key exists...
+        RegQueryValueEx(keyCurVer,'SystemRoot')
+    except:
+        return release,version,csd,ptype
+
+    # Parse values
+    #subversion = _win32_getvalue(keyCurVer,
+    #                            'SubVersionNumber',
+    #                            ('',1))[0]
+    #if subversion:
+    #   release = release + subversion # 95a, 95b, etc.
+    build = _win32_getvalue(keyCurVer,
+                            'CurrentBuildNumber',
+                            ('',1))[0]
+    ptype = _win32_getvalue(keyCurVer,
+                           'CurrentType',
+                           (ptype,1))[0]
+
+    # Normalize version
+    version = _norm_version(version,build)
+
+    # Close key
+    RegCloseKey(keyCurVer)
+    return release,version,csd,ptype
+
+def _mac_ver_lookup(selectors,default=None):
+
+    from gestalt import gestalt
+    l = []
+    append = l.append
+    for selector in selectors:
+        try:
+            append(gestalt(selector))
+        except RuntimeError:
+            append(default)
+    return l
+
+def _bcd2str(bcd):
+
+    return hex(bcd)[2:]
+
+def mac_ver(release='',versioninfo=('','',''),machine=''):
+
+    """ Get MacOS version information and return it as tuple (release,
+        versioninfo, machine) with versioninfo being a tuple (version,
+        dev_stage, non_release_version).
+
+        Entries which cannot be determined are set to ''. All tuple
+        entries are strings.
+
+        Thanks to Mark R. Levinson for mailing documentation links and
+        code examples for this function. Documentation for the
+        gestalt() API is available online at:
+
+           http://www.rgaros.nl/gestalt/
+
+    """
+    # Check whether the version info module is available
+    try:
+        import gestalt
+    except ImportError:
+        return release,versioninfo,machine
+    # Get the infos
+    sysv,sysu,sysa = _mac_ver_lookup(('sysv','sysu','sysa'))
+    # Decode the infos
+    if sysv:
+        major = (sysv & 0xFF00) >> 8
+        minor = (sysv & 0x00F0) >> 4
+        patch = (sysv & 0x000F)
+        release = '%s.%i.%i' % (_bcd2str(major),minor,patch)
+    if sysu:
+        major =  (sysu & 0xFF000000) >> 24
+        minor =  (sysu & 0x00F00000) >> 20
+        bugfix = (sysu & 0x000F0000) >> 16
+        stage =  (sysu & 0x0000FF00) >> 8
+        nonrel = (sysu & 0x000000FF)
+        version = '%s.%i.%i' % (_bcd2str(major),minor,bugfix)
+        nonrel = _bcd2str(nonrel)
+        stage = {0x20:'development',
+                 0x40:'alpha',
+                 0x60:'beta',
+                 0x80:'final'}.get(stage,'')
+        versioninfo = (version,stage,nonrel)
+    if sysa:
+        machine = {0x1: '68k',
+                   0x2: 'PowerPC'}.get(sysa,'')
+    return release,versioninfo,machine
+
+def _java_getprop(self,name,default):
+
+    from java.lang import System
+    try:
+        return System.getProperty(name)
+    except:
+        return default
+
+def java_ver(release='',vendor='',vminfo=('','',''),osinfo=('','','')):
+
+    """ Version interface for JPython.
+
+        Returns a tuple (release,vendor,vminfo,osinfo) with vminfo being
+        a tuple (vm_name,vm_release,vm_vendor) and osinfo being a
+        tuple (os_name,os_version,os_arch).
+
+        Values which cannot be determined are set to the defaults
+        given as parameters (which all default to '').
+
+    """
+    # Import the needed APIs
+    try:
+        import java.lang
+    except ImportError:
+        return release,vendor,vminfo,osinfo
+
+    vendor = _java_getprop('java.vendor',vendor)
+    release = _java_getprop('java.version',release)
+    vm_name,vm_release,vm_vendor = vminfo
+    vm_name = _java_getprop('java.vm.name',vm_name)
+    vm_vendor = _java_getprop('java.vm.vendor',vm_vendor)
+    vm_release = _java_getprop('java.vm.version',vm_release)
+    vminfo = vm_name,vm_release,vm_vendor
+    os_name,os_version,os_arch = osinfo
+    os_arch = _java_getprop('java.os.arch',os_arch)
+    os_name = _java_getprop('java.os.name',os_name)
+    os_version = _java_getprop('java.os.version',os_version)
+    osinfo = os_name,os_version,os_arch
+
+    return release,vendor,vminfo,osinfo
+
+### System name aliasing
+
+def system_alias(system,release,version):
+
+    """ Returns (system,release,version) aliased to common
+        marketing names used for some systems.
+
+        It also does some reordering of the information in some cases
+        where it would otherwise cause confusion.
+
+    """
+    if system == 'Rhapsody':
+        # Apple's BSD derivative
+        # XXX How can we determine the marketing release number ?
+        return 'MacOS X Server',system+release,version
+
+    elif system == 'SunOS':
+        # Sun's OS
+        if release < '5':
+            # These releases use the old name SunOS
+            return system,release,version
+        # Modify release (marketing release = SunOS release - 3)
+        l = string.split(release,'.')
+        if l:
+            try:
+                major = int(l[0])
+            except ValueError:
+                pass
+            else:
+                major = major - 3
+                l[0] = str(major)
+                release = string.join(l,'.')
+        if release < '6':
+            system = 'Solaris'
+        else:
+            # XXX Whatever the new SunOS marketing name is...
+            system = 'Solaris'
+
+    elif system == 'IRIX64':
+        # IRIX reports IRIX64 on platforms with 64-bit support; yet it
+        # is really a version and not a different platform, since 32-bit
+        # apps are also supported..
+        system = 'IRIX'
+        if version:
+            version = version + ' (64bit)'
+        else:
+            version = '64bit'
+
+    elif system in ('win32','win16'):
+        # In case one of the other tricks
+        system = 'Windows'
+
+    return system,release,version
+
+### Various internal helpers
+
+def _platform(*args):
+
+    """ Helper to format the platform string in a filename
+        compatible format e.g. "system-version-machine".
+    """
+    # Format the platform string
+    platform = string.join(
+        map(string.strip,
+            filter(len,args)),
+        '-')
+
+    # Cleanup some possible filename obstacles...
+    replace = string.replace
+    platform = replace(platform,' ','_')
+    platform = replace(platform,'/','-')
+    platform = replace(platform,'\\','-')
+    platform = replace(platform,':','-')
+    platform = replace(platform,';','-')
+    platform = replace(platform,'"','-')
+    platform = replace(platform,'(','-')
+    platform = replace(platform,')','-')
+
+    # No need to report 'unkown' information...
+    platform = replace(platform,'unknown','')
+
+    # Fold '--'s and remove trailing '-'
+    while 1:
+        cleaned = replace(platform,'--','-')
+        if cleaned == platform:
+            break
+        platform = cleaned
+    while platform[-1] == '-':
+        platform = platform[:-1]
+
+    return platform
+
+def _node(default=''):
+
+    """ Helper to determine the node name of this machine.
+    """
+    try:
+        import socket
+    except ImportError:
+        # No sockets...
+        return default
+    try:
+        return socket.gethostname()
+    except socket.error:
+        # Still not working...
+        return default
+
+# os.path.abspath is new in Python 1.5.2:
+if not hasattr(os.path,'abspath'):
+
+    def _abspath(path,
+
+                 isabs=os.path.isabs,join=os.path.join,getcwd=os.getcwd,
+                 normpath=os.path.normpath):
+
+        if not isabs(path):
+            path = join(getcwd(), path)
+        return normpath(path)
+
+else:
+
+    _abspath = os.path.abspath
+
+def _follow_symlinks(filepath):
+
+    """ In case filepath is a symlink, follow it until a
+        real file is reached.
+    """
+    filepath = _abspath(filepath)
+    while os.path.islink(filepath):
+        filepath = os.path.normpath(
+            os.path.join(filepath,os.readlink(filepath)))
+    return filepath
+
+def _syscmd_uname(option,default=''):
+
+    """ Interface to the system's uname command.
+    """
+    if sys.platform in ('dos','win32','win16','os2'):
+        # XXX Others too ?
+        return default
+    try:
+        f = os.popen('uname %s 2> /dev/null' % option)
+    except (AttributeError,os.error):
+        return default
+    output = string.strip(f.read())
+    rc = f.close()
+    if not output or rc:
+        return default
+    else:
+        return output
+
+def _syscmd_file(target,default=''):
+
+    """ Interface to the system's file command.
+
+        The function uses the -b option of the file command to have it
+        ommit the filename in its output and if possible the -L option
+        to have the command follow symlinks. It returns default in
+        case the command should fail.
+
+    """
+    target = _follow_symlinks(target)
+    try:
+        f = os.popen('file %s 2> /dev/null' % target)
+    except (AttributeError,os.error):
+        return default
+    output = string.strip(f.read())
+    rc = f.close()
+    if not output or rc:
+        return default
+    else:
+        return output
+
+### Information about the used architecture
+
+# Default values for architecture; non-empty strings override the
+# defaults given as parameters
+_default_architecture = {
+    'win32': ('','WindowsPE'),
+    'win16': ('','Windows'),
+    'dos': ('','MSDOS'),
+}
+
+def architecture(executable=sys.executable,bits='',linkage='',
+
+                 split=re.compile('[\s,]').split):
+
+    """ Queries the given executable (defaults to the Python interpreter
+        binary) for various architecture informations.
+
+        Returns a tuple (bits,linkage) which contain information about
+        the bit architecture and the linkage format used for the
+        executable. Both values are returned as strings.
+
+        Values that cannot be determined are returned as given by the
+        parameter presets. If bits is given as '', the sizeof(pointer)
+        (or sizeof(long) on Python version < 1.5.2) is used as
+        indicator for the supported pointer size.
+
+        The function relies on the system's "file" command to do the
+        actual work. This is available on most if not all Unix
+        platforms. On some non-Unix platforms and then only if the
+        executable points to the Python interpreter defaults from
+        _default_architecture are used.
+
+    """
+    # Use the sizeof(pointer) as default number of bits if nothing
+    # else is given as default.
+    if not bits:
+        import struct
+        try:
+            size = struct.calcsize('P')
+        except struct.error:
+            # Older installations can only query longs
+            size = struct.calcsize('l')
+        bits = str(size*8) + 'bit'
+
+    # Get data from the 'file' system command
+    output = _syscmd_file(executable,'')
+
+    if not output and \
+       executable == sys.executable:
+        # "file" command did not return anything; we'll try to provide
+        # some sensible defaults then...
+        if _default_architecture.has_key(sys.platform):
+            b,l = _default_architecture[sys.platform]
+            if b:
+                bits = b
+            if l:
+                linkage = l
+        return bits,linkage
+
+    # Split the output into a list of strings omitting the filename
+    fileout = split(output)[1:]
+
+    if 'executable' not in fileout:
+        # Format not supported
+        return bits,linkage
+
+    # Bits
+    if '32-bit' in fileout:
+        bits = '32bit'
+    elif 'N32' in fileout:
+        # On Irix only
+        bits = 'n32bit'
+    elif '64-bit' in fileout:
+        bits = '64bit'
+
+    # Linkage
+    if 'ELF' in fileout:
+        linkage = 'ELF'
+    elif 'PE' in fileout:
+        # E.g. Windows uses this format
+        if 'Windows' in fileout:
+            linkage = 'WindowsPE'
+        else:
+            linkage = 'PE'
+    elif 'COFF' in fileout:
+        linkage = 'COFF'
+    elif 'MS-DOS' in fileout:
+        linkage = 'MSDOS'
+    else:
+        # XXX the A.OUT format also falls under this class...
+        pass
+
+    return bits,linkage
+
+### Portable uname() interface
+
+_uname_cache = None
+
+def uname():
+
+    """ Fairly portable uname interface. Returns a tuple
+        of strings (system,node,release,version,machine,processor)
+        identifying the underlying platform.
+
+        Note that unlike the os.uname function this also returns
+        possible processor information as additional tuple entry.
+
+        Entries which cannot be determined are set to ''.
+
+    """
+    global _uname_cache
+
+    if _uname_cache is not None:
+        return _uname_cache
+
+    # Get some infos from the builtin os.uname API...
+    try:
+        system,node,release,version,machine = os.uname()
+
+    except AttributeError:
+        # Hmm, no uname... we'll have to poke around the system then.
+        system = sys.platform
+        release = ''
+        version = ''
+        node = _node()
+        machine = ''
+        processor = ''
+        use_syscmd_ver = 1
+
+        # Try win32_ver() on win32 platforms
+        if system == 'win32':
+            release,version,csd,ptype = win32_ver()
+            if release and version:
+                use_syscmd_ver = 0
+
+        # Try the 'ver' system command available on some
+        # platforms
+        if use_syscmd_ver:
+            system,release,version = _syscmd_ver(system)
+
+        # In case we still don't know anything useful, we'll try to
+        # help ourselves
+        if system in ('win32','win16'):
+            if not version:
+                if system == 'win32':
+                    version = '32bit'
+                else:
+                    version = '16bit'
+            system = 'Windows'
+
+        elif system[:4] == 'java':
+            release,vendor,vminfo,osinfo = java_ver()
+            system = 'Java'
+            version = string.join(vminfo,', ')
+            if not version:
+                version = vendor
+
+        elif os.name == 'mac':
+            release,(version,stage,nonrel),machine = mac_ver()
+            system = 'MacOS'
+
+    else:
+        # System specific extensions
+        if system == 'OpenVMS':
+            # OpenVMS seems to have release and version mixed up
+            if not release or release == '0':
+                release = version
+                version = ''
+            # Get processor information
+            try:
+                import vms_lib
+            except ImportError:
+                pass
+            else:
+                csid, cpu_number = vms_lib.getsyi('SYI$_CPU',0)
+                if (cpu_number >= 128):
+                    processor = 'Alpha'
+                else:
+                    processor = 'VAX'
+        else:
+            # Get processor information from the uname system command
+            processor = _syscmd_uname('-p','')
+
+    # 'unkown' is not really any useful as information; we'll convert
+    # it to '' which is more portable
+    if system == 'unknown':
+        system = ''
+    if node == 'unknown':
+        node = ''
+    if release == 'unknown':
+        release = ''
+    if version == 'unknown':
+        version = ''
+    if machine == 'unknown':
+        machine = ''
+    if processor == 'unknown':
+        processor = ''
+    _uname_cache = system,node,release,version,machine,processor
+    return _uname_cache
+
+### Direct interfaces to some of the uname() return values
+
+def system():
+
+    """ Returns the system/OS name, e.g. 'Linux', 'Windows' or 'Java'.
+
+        An empty string is returned if the value cannot be determined.
+
+    """
+    return uname()[0]
+
+def node():
+
+    """ Returns the computer's network name (may not be fully qualified !)
+
+        An empty string is returned if the value cannot be determined.
+
+    """
+    return uname()[1]
+
+def release():
+
+    """ Returns the system's release, e.g. '2.2.0' or 'NT'
+
+        An empty string is returned if the value cannot be determined.
+
+    """
+    return uname()[2]
+
+def version():
+
+    """ Returns the system's release version, e.g. '#3 on degas'
+
+        An empty string is returned if the value cannot be determined.
+
+    """
+    return uname()[3]
+
+def machine():
+
+    """ Returns the machine type, e.g. 'i386'
+
+        An empty string is returned if the value cannot be determined.
+
+    """
+    return uname()[4]
+
+def processor():
+
+    """ Returns the (True) processor name, e.g. 'amdk6'
+
+        An empty string is returned if the value cannot be
+        determined. Note that many platforms do not provide this
+        information or simply return the same value as for machine(),
+        e.g.  NetBSD does this.
+
+    """
+    return uname()[5]
+
+### Various APIs for extracting information from sys.version
+
+_sys_version_parser = re.compile('([\w.+]+)\s*'
+                                 '\(#(\d+),\s*([\w ]+),\s*([\w :]+)\)\s*'
+                                 '\[([^\]]+)\]?')
+_sys_version_cache = None
+
+def _sys_version():
+
+    """ Returns a parsed version of Python's sys.version as tuple
+        (version, buildno, builddate, compiler) referring to the Python
+        version, build number, build date/time as string and the compiler
+        identification string.
+
+        Note that unlike the Python sys.version, the returned value
+        for the Python version will always include the patchlevel (it
+        defaults to '.0').
+
+    """
+    global _sys_version_cache
+    import sys, re, time
+
+    if _sys_version_cache is not None:
+        return _sys_version_cache
+    version, buildno, builddate, buildtime, compiler = \
+             _sys_version_parser.match(sys.version).groups()
+    buildno = int(buildno)
+    builddate = builddate + ' ' + buildtime
+    l = string.split(version, '.')
+    if len(l) == 2:
+        l.append('0')
+        version = string.join(l, '.')
+    _sys_version_cache = (version, buildno, builddate, compiler)
+    return _sys_version_cache
+
+def python_version():
+
+    """ Returns the Python version as string 'major.minor.patchlevel'
+
+        Note that unlike the Python sys.version, the returned value
+        will always include the patchlevel (it defaults to 0).
+
+    """
+    return _sys_version()[0]
+
+def python_build():
+
+    """ Returns a tuple (buildno, buildate) stating the Python
+        build number and date as strings.
+
+    """
+    return _sys_version()[1:3]
+
+def python_compiler():
+
+    """ Returns a string identifying the compiler used for compiling
+        Python.
+
+    """
+    return _sys_version()[3]
+
+### The Opus Magnum of platform strings :-)
+
+_platform_cache = None
+_platform_aliased_cache = None
+
+def platform(aliased=0, terse=0):
+
+    """ Returns a single string identifying the underlying platform
+        with as much useful information as possible (but no more :).
+
+        The output is intended to be human readable rather than
+        machine parseable. It may look different on different
+        platforms and this is intended.
+
+        If "aliased" is True, the function will use aliases for
+        various platforms that report system names which differ from
+        their common names, e.g. SunOS will be reported as
+        Solaris. The system_alias() function is used to implement
+        this.
+
+        Setting terse to True causes the function to return only the
+        absolute minimum information needed to identify the platform.
+
+    """
+    global _platform_cache,_platform_aliased_cache
+
+    if not aliased and (_platform_cache is not None):
+        return _platform_cache
+    elif _platform_aliased_cache is not None:
+        return _platform_aliased_cache
+
+    # Get uname information and then apply platform specific cosmetics
+    # to it...
+    system,node,release,version,machine,processor = uname()
+    if machine == processor:
+        processor = ''
+    if aliased:
+        system,release,version = system_alias(system,release,version)
+
+    if system == 'Windows':
+        # MS platforms
+        rel,vers,csd,ptype = win32_ver(version)
+        if terse:
+            platform = _platform(system,release)
+        else:
+            platform = _platform(system,release,version,csd)
+
+    elif system in ('Linux',):
+        # Linux based systems
+        distname,distversion,distid = dist('')
+        if distname and not terse:
+            platform = _platform(system,release,machine,processor,
+                                 'with',
+                                 distname,distversion,distid)
+        else:
+            # If the distribution name is unknown check for libc vs. glibc
+            libcname,libcversion = libc_ver(sys.executable)
+            platform = _platform(system,release,machine,processor,
+                                 'with',
+                                 libcname+libcversion)
+    elif system == 'Java':
+        # Java platforms
+        r,v,vminfo,(os_name,os_version,os_arch) = java_ver()
+        if terse:
+            platform = _platform(system,release,version)
+        else:
+            platform = _platform(system,release,version,
+                                 'on',
+                                 os_name,os_version,os_arch)
+
+    elif system == 'MacOS':
+        # MacOS platforms
+        if terse:
+            platform = _platform(system,release)
+        else:
+            platform = _platform(system,release,machine)
+
+    else:
+        # Generic handler
+        if terse:
+            platform = _platform(system,release)
+        else:
+            bits,linkage = architecture(sys.executable)
+            platform = _platform(system,release,machine,processor,bits,linkage)
+
+    if aliased:
+        _platform_aliased_cache = platform
+    elif terse:
+        pass
+    else:
+        _platform_cache = platform
+    return platform
+
+if __name__ == '__main__':
+    # Default is to print the aliased verbose platform string
+    terse = ('terse' in sys.argv or '--terse' in sys.argv)
+    aliased = (not 'nonaliased' in sys.argv and not '--nonaliased' in sys.argv)
+    print platform(aliased,terse)
+    sys.exit(0)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+__all__ = []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,1 @@
+__version__ = '2.0.0'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/_cpconfig.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,183 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import _cputil, ConfigParser, cpg
+
+        
+def setDefaultConfigOption():
+    """ Return an EmptyClass instance with the default config options """
+
+    cpg.configOption = _cputil.EmptyClass()
+
+    # Set default values for all options
+
+    # Parameters used for logging
+    cpg.configOption.logToScreen = 1
+    cpg.configOption.logFile = ''
+
+    # Parameters used to tell which socket the server should listen on
+    # Note that socketPort and socketFile conflict wich each
+    # other: if one has a non-null value, the other one should be null
+    cpg.configOption.socketHost = ''
+    cpg.configOption.socketPort = 8080
+    cpg.configOption.socketFile = '' # Used if server should listen on
+                                 # AF_UNIX socket
+    cpg.configOption.reverseDNS = 0
+    cpg.configOption.socketQueueSize = 5 # Size of the socket queue
+    cpg.configOption.protocolVersion = "HTTP/1.0"
+
+    # Parameters used to tell what kind of server we want
+    cpg.configOption.threadPool = 0 # Used if we want to create a pool
+                                # of threads at the beginning
+
+    # Variables used to tell if this is an SSL server
+    cpg.configOption.sslKeyFile = ""
+    cpg.configOption.sslCertificateFile = ""
+    cpg.configOption.sslClientCertificateVerification = 0
+    cpg.configOption.sslCACertificateFile = ""
+    cpg.configOption.sslVerifyDepth = 1
+
+    # Variable used to flush cache
+    cpg.configOption.flushCacheDelay=0
+
+    # Variable used for enabling debugging
+    cpg.configOption.debugMode=0
+
+    # Variable used to serve static content
+    cpg.configOption.staticContentList = []
+
+    # Variable used for session handling
+    cpg.configOption.sessionStorageType = ""
+    cpg.configOption.sessionTimeout = 60 # In minutes
+    cpg.configOption.sessionCleanUpDelay = 60 # In minutes
+    cpg.configOption.sessionCookieName = "CherryPySession"
+    cpg.configOption.sessionStorageFileDir = ""
+
+def parseConfigFile(configFile = None, parsedConfigFile = None):
+    """
+        Parse the config file and set values in cpg.configOption
+    """
+    _cpLogMessage = _cputil.getSpecialFunction('_cpLogMessage')
+    if configFile:
+        cpg.parsedConfigFile = ConfigParser.ConfigParser()
+        if hasattr(configFile, 'read'):
+            _cpLogMessage("Reading infos from configFile stream", 'CONFIG')
+            cpg.parsedConfigFile.readfp(configFile)
+        else:
+            _cpLogMessage("Reading infos from configFile: %s" % configFile, 'CONFIG')
+            cpg.parsedConfigFile.read(configFile)
+    else:
+        cpg.parsedConfigFile = parsedConfigFile
+
+    # Read parameters from configFile
+    for sectionName, optionName, valueType in [
+            ('server', 'logToScreen', 'int'),
+            ('server', 'logFile', 'str'),
+            ('server', 'socketHost', 'str'),
+            ('server', 'protocolVersion', 'str'),
+            ('server', 'socketPort', 'int'),
+            ('server', 'socketFile', 'str'),
+            ('server', 'reverseDNS', 'int'),
+            ('server', 'threadPool', 'int'),
+            ('server', 'sslKeyFile', 'str'),
+            ('server', 'sslCertificateFile', 'str'),
+            ('server', 'sslClientCertificateVerification', 'int'),
+            ('server', 'sslCACertificateFile', 'str'),
+            ('server', 'sslVerifyDepth', 'int'),
+            ('session', 'storageType', 'str'),
+            ('session', 'timeout', 'float'),
+            ('session', 'cleanUpDelay', 'float'),
+            ('session', 'cookieName', 'str'),
+            ('session', 'storageFileDir', 'str')
+            ]:
+        try:
+            value = cpg.parsedConfigFile.get(sectionName, optionName)
+            if valueType == 'int': value = int(value)
+            elif valueType == 'float': value = float(value)
+            if sectionName == 'session':
+                optionName = 'session' + optionName[0].upper() + optionName[1:]
+            setattr(cpg.configOption, optionName, value)
+        except:
+            pass
+
+    try:
+        staticDirList = cpg.parsedConfigFile.options('staticContent')
+        for staticDir in staticDirList:
+            staticDirTarget = cpg.parsedConfigFile.get('staticContent', staticDir)
+            cpg.configOption.staticContentList.append((staticDir, staticDirTarget))
+    except: pass
+
+def outputConfigOptions():
+    _cpLogMessage = _cputil.getSpecialFunction('_cpLogMessage')
+    _cpLogMessage("Server parameters:", 'CONFIG')
+    _cpLogMessage("  logToScreen: %s" % cpg.configOption.logToScreen, 'CONFIG')
+    _cpLogMessage("  logFile: %s" % cpg.configOption.logFile, 'CONFIG')
+    _cpLogMessage("  protocolVersion: %s" % cpg.configOption.protocolVersion, 'CONFIG')
+    _cpLogMessage("  socketHost: %s" % cpg.configOption.socketHost, 'CONFIG')
+    _cpLogMessage("  socketPort: %s" % cpg.configOption.socketPort, 'CONFIG')
+    _cpLogMessage("  socketFile: %s" % cpg.configOption.socketFile, 'CONFIG')
+    _cpLogMessage("  reverseDNS: %s" % cpg.configOption.reverseDNS, 'CONFIG')
+    _cpLogMessage("  socketQueueSize: %s" % cpg.configOption.socketQueueSize, 'CONFIG')
+    _cpLogMessage("  threadPool: %s" % cpg.configOption.threadPool, 'CONFIG')
+    _cpLogMessage("  sslKeyFile: %s" % cpg.configOption.sslKeyFile, 'CONFIG')
+    if cpg.configOption.sslKeyFile:
+        _cpLogMessage("  sslCertificateFile: %s" % cpg.configOption.sslCertificateFile, 'CONFIG')
+        _cpLogMessage("  sslClientCertificateVerification: %s" % cpg.configOption.sslClientCertificateVerification, 'CONFIG')
+        _cpLogMessage("  sslCACertificateFile: %s" % cpg.configOption.sslCACertificateFile, 'CONFIG')
+        _cpLogMessage("  sslVerifyDepth: %s" % cpg.configOption.sslVerifyDepth, 'CONFIG')
+        _cpLogMessage("  flushCacheDelay: %s min" % cpg.configOption.flushCacheDelay, 'CONFIG')
+    _cpLogMessage("  sessionStorageType: %s" % cpg.configOption.sessionStorageType, 'CONFIG')
+    if cpg.configOption.sessionStorageType:
+        _cpLogMessage("  sessionTimeout: %s min" % cpg.configOption.sessionTimeout, 'CONFIG')
+        _cpLogMessage("  cleanUpDelay: %s min" % cpg.configOption.sessionCleanUpDelay, 'CONFIG')
+        _cpLogMessage("  sessionCookieName: %s" % cpg.configOption.sessionCookieName, 'CONFIG')
+        _cpLogMessage("  sessionStorageFileDir: %s" % cpg.configOption.sessionStorageFileDir, 'CONFIG')
+    _cpLogMessage("  staticContent: %s" % cpg.configOption.staticContentList, 'CONFIG')
+
+def dummy():
+    # Check that parameters are correct and that they don't conflict with each other
+    if _protocolVersion not in ("HTTP/1.1", "HTTP/1.0"):
+        raise "CherryError: protocolVersion must be 'HTTP/1.1' or 'HTTP/1.0'"
+    if _reverseDNS not in (0,1): raise "CherryError: reverseDNS must be '0' or '1'"
+    if _socketFile and not hasattr(socket, 'AF_UNIX'): raise "CherryError: Configuration file has socketFile, but this is only available on Unix machines"
+    if _sslKeyFile:
+        try:
+            global SSL
+            from OpenSSL import SSL
+        except: raise "CherryError: PyOpenSSL 0.5.1 or later must be installed to use SSL. You can get it from http://pyopenssl.sourceforge.net"
+    if _socketPort and _socketFile: raise "CherryError: In configuration file: socketPort and socketFile conflict with each other"
+    if not _socketFile and not _socketPort: _socketPort=8000 # Default port
+    if _sslKeyFile and not _sslCertificateFile: raise "CherryError: Configuration file has sslKeyFile but no sslCertificateFile"
+    if _sslCertificateFile and not _sslKeyFile: raise "CherryError: Configuration file has sslCertificateFile but no sslKeyFile"
+    try: sys.stdout.flush()
+    except: pass
+
+    if _sessionStorageType not in ('', 'custom', 'ram', 'file', 'cookie'): raise "CherryError: Configuration file an invalid sessionStorageType: '%s'"%_sessionStorageType
+    if _sessionStorageType in ('custom', 'ram', 'cookie') and _sessionStorageFileDir!='': raise "CherryError: Configuration file has sessionStorageType set to 'custom, 'ram' or 'cookie' but a sessionStorageFileDir is specified"
+    if _sessionStorageType=='file' and _sessionStorageFileDir=='': raise "CherryError: Configuration file has sessionStorageType set to 'file' but no sessionStorageFileDir"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/_cpdefaults.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,207 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+
+"""
+A module containing a few utility classes/functions used by CherryPy
+"""
+
+import time, thread, os, cpg
+import cPickle as pickle
+
+def _cpLogMessage(msg, context = '', severity = 0):
+    """ Default method for logging messages """
+
+    nowTuple = time.localtime(time.time())
+    nowStr = '%04d/%02d/%02d %02d:%02d:%02d' % (nowTuple[:6])
+    if severity == 0:
+        level = "INFO"
+    elif severity == 1:
+        level = "WARNING"
+    elif severity == 2:
+        level = "ERROR"
+    else:
+        lebel = "UNKNOWN"
+    try:
+        logToScreen = int(cpg.configOption.logToScreen)
+    except:
+        logToScreen = True
+    s = nowStr + ' ' + context + ' ' + level + ' ' + msg
+    if logToScreen:
+        print s
+    if cpg.configOption.logFile:
+        f = open(cpg.configOption.logFile, 'ab')
+        f.write(s + '\n')
+        f.close()
+
+def _cpOnError():
+    """ Default _cpOnError method """
+
+    import traceback, StringIO
+    bodyFile = StringIO.StringIO()
+    traceback.print_exc(file = bodyFile)
+    cpg.response.body = [bodyFile.getvalue()]
+    cpg.response.headerMap['Content-Type'] = 'text/plain'
+
+def _cpSaveSessionData(sessionId, sessionData, expirationTime,
+        threadPool = None, sessionStorageType = None,
+        sessionStorageFileDir = None):
+    """ Save session data if needed """
+
+    if threadPool is None:
+        threadPool = cpg.configOption.threadPool
+    if sessionStorageType is None:
+        sessionStorageType =  cpg.configOption.sessionStorageType
+    if sessionStorageFileDir is None:
+        sessionStorageFileDir = cpg.configOption.sessionStorageFileDir
+
+    t = time.localtime(expirationTime)
+    if sessionStorageType == 'file':
+        fname=os.path.join(sessionStorageFileDir,sessionId)
+        if threadPool > 1:
+            cpg._sessionFileLock.acquire()
+        f = open(fname,"wb")
+        pickle.dump((sessionData, expirationTime), f)
+        f.close()
+        if threadPool > 1:
+            cpg._sessionFileLock.release()
+
+    elif sessionStorageType=="ram":
+        # Update expiration time
+        cpg._sessionMap[sessionId] = (sessionData, expirationTime)
+
+    """ TODO: implement cookie storage type
+    elif sessionStorageType == "cookie":
+        
+         TODO: set siteKey in _cpConfig
+            # Get site key from config file or compute it
+            try: cpg._SITE_KEY_ = configFile.get('server','siteKey')
+            except:
+                _SITE_KEY_ = ''
+                for i in range(30):
+                    _SITE_KEY_ += random.choice(string.letters)
+
+        # Update expiration time
+        sessionData = (sessionData, expirationTime)
+        dumpStr = pickle.dumps(_sessionData)
+        try: dumpStr = zlib.compress(dumpStr)
+        except: pass # zlib is not available in all python distros
+        dumpStr = binascii.hexlify(dumpStr) # Need to hexlify it because it will be stored in a cookie
+        cpg.response.simpleCookie['CSession'] = dumpStr
+        cpg.response.simpleCookie['CSession-sig'] = md5.md5(dumpStr + cpg.configOption.siteKey).hexdigest()
+        cpg.response.simpleCookie['CSession']['path'] = '/'
+        cpg.response.simpleCookie['CSession']['max-age'] = sessionTimeout * 60
+        cpg.response.simpleCookie['CSession-sig']['path'] = '/'
+        cpg.response.simpleCookie['CSession-sig']['max-age'] = sessionTimeout * 60
+    """
+
+def _cpLoadSessionData(sessionId, threadPool = None, sessionStorageType = None,
+        sessionStorageFileDir = None):
+    """ Return the session data for a given sessionId.
+        The _expirationTime will be checked by the caller of this function
+    """
+
+    if threadPool is None:
+        threadPool = cpg.configOption.threadPool
+    if sessionStorageType is None:
+        sessionStorageType =  cpg.configOption.sessionStorageType
+    if sessionStorageFileDir is None:
+        sessionStorageFileDir = cpg.configOption.sessionStorageFileDir
+
+    if sessionStorageType == "ram":
+        if cpg._sessionMap.has_key(sessionId):
+            return cpg._sessionMap[sessionId]
+        else: return None
+
+    elif sessionStorageType == "file":
+        fname = os.path.join(sessionStorageFileDir, sessionId)
+        if os.path.exists(fname):
+            if threadPool > 1:
+                cpg._sessionFileLock.acquire()
+            f = open(fname, "rb")
+            sessionData = pickle.load(f)
+            f.close()
+            if threadPool > 1:
+                cpg._sessionFileLock.release()
+            return sessionData
+        else: return None
+
+    """ TODO: implement cookie storage type
+    elif _sessionStorageType == "cookie":
+        if request.simpleCookie.has_key('CSession') and request.simpleCookie.has_key('CSession-sig'):
+            data = request.simpleCookie['CSession'].value
+            sig  = request.simpleCookie['CSession-sig'].value
+            if md5.md5(data + cpg.configOption.siteKey).hexdigest() == sig:
+                try:
+                    dumpStr = binascii.unhexlify(data)
+                    try: dumpStr = zlib.decompress(dumpStr)
+                    except: pass # zlib is not available in all python distros
+                    dumpStr = pickle.loads(dumpStr)
+                    return dumpStr
+                except: pass
+        return None
+    """
+
+def _cpCleanUpOldSessions(threadPool = None, sessionStorageType = None,
+        sessionStorageFileDir = None):
+    """ Clean up old sessions """
+
+    if threadPool is None:
+        threadPool = cpg.configOption.threadPool
+    if sessionStorageType is None:
+        sessionStorageType =  cpg.configOption.sessionStorageType
+    if sessionStorageFileDir is None:
+        sessionStorageFileDir = cpg.configOption.sessionStorageFileDir
+
+    # Clean up old session data
+    now = time.time()
+    if sessionStorageType == "ram":
+        sessionIdToDeleteList = []
+        for sessionId, (dummy, expirationTime) in cpg._sessionMap.items():
+            if expirationTime < now:
+                sessionIdToDeleteList.append(sessionId)
+        for sessionId in sessionIdToDeleteList:
+            del cpg._sessionMap[sessionId]
+
+    elif sessionStorageType=="file":
+        # This process is very expensive because we go through all files, parse them and then delete them if the session is expired
+        # One optimization would be to just store a list of (sessionId, expirationTime) in *one* file
+        sessionFileList = os.listdir(sessionStorageFileDir)
+        for sessionId in sessionFileList:
+            try:
+                dummy, expirationTime = _cpLoadSessionData(sessionId)
+                if expirationTime < now:
+                    os.remove(os.path.join(sessionStorageFileDir, sessionId))
+            except:
+                pass
+
+    elif sessionStorageType == "cookie":
+        # Nothing to do in this case: the session data is stored on the client
+        pass
+
+_cpFilterList = []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/_cphttpserver.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,350 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import cpg, sys, threading, SocketServer, _cphttptools
+import BaseHTTPServer, socket, Queue, _cputil
+
+def stop():
+    cpg._server.shutdown()
+
+def start():
+    """ Prepare the HTTP server and then run it """
+
+    # If sessions are stored in files and we
+    # use threading, we need a lock on the file
+    if (cpg.configOption.threadPool > 1) and \
+            cpg.configOption.sessionStorageType == 'file':
+        cpg._sessionFileLock = threading.RLock()
+
+
+    if cpg.configOption.socketFile:
+        # AF_UNIX socket
+        # TODO: Handle threading here
+        class MyCherryHTTPServer(CherryHTTPServer): address_family = socket.AF_UNIX
+    else:
+        # AF_INET socket
+        if cpg.configOption.threadPool > 1:
+            MyCherryHTTPServer = PooledThreadServer
+        else:
+            MyCherryHTTPServer = CherryHTTPServer
+
+    MyCherryHTTPServer.request_queue_size = cpg.configOption.socketQueueSize
+
+    # Set protocol_version
+    CherryHTTPRequestHandler.protocol_version = cpg.configOption.protocolVersion
+
+    run_server(CherryHTTPRequestHandler, MyCherryHTTPServer, \
+        (cpg.configOption.socketHost, cpg.configOption.socketPort), \
+        cpg.configOption.socketFile)
+
+def run_server(HandlerClass, ServerClass, server_address, socketFile):
+    """Run the HTTP request handler class."""
+
+    if cpg.configOption.socketFile:
+        try: os.unlink(cpg.configOption.socketFile) # So we can reuse the socket
+        except: pass
+        server_address = cpg.configOption.socketFile
+    if cpg.configOption.threadPool > 1:
+        myCherryHTTPServer = ServerClass(server_address, cpg.configOption.threadPool, HandlerClass)
+    else:
+        myCherryHTTPServer = ServerClass(server_address, HandlerClass)
+    cpg._server = myCherryHTTPServer
+    if cpg.configOption.socketFile:
+        try: os.chmod(socketFile, 0777) # So everyone can access the socket
+        except: pass
+    global _cpLogMessage
+    _cpLogMessage = _cputil.getSpecialFunction('_cpLogMessage')
+
+    servingWhat = "HTTP"
+    if cpg.configOption.socketPort: onWhat = "socket: ('%s', %s)" % (cpg.configOption.socketHost, cpg.configOption.socketPort)
+    else: onWhat = "socket file: %s" % cpg.configOption.socketFile
+    _cpLogMessage("Serving %s on %s" % (servingWhat, onWhat), 'HTTP')
+
+    try:
+        # Call the functions from cpg.server.onStartServerList
+        for func in cpg.server.onStartServerList:
+            func()
+        myCherryHTTPServer.serve_forever()
+    except KeyboardInterrupt:
+        _cpLogMessage("<Ctrl-C> hit: shutting down", "HTTP")
+        myCherryHTTPServer.server_close()
+    # Call the functions from cpg.server.onStartServerList
+    for func in cpg.server.onStopServerList:
+        func()
+
+class CherryHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+    """CherryPy HTTP request handler with the following commands:
+
+        o  GET
+        o  HEAD
+        o  POST
+        o  HOTRELOAD
+
+    """
+
+    def address_string(self):
+        """ Try to do a reverse DNS based on [server]reverseDNS in the config file """
+        if cpg.configOption.reverseDNS:
+            return BaseHTTPServer.BaseHTTPRequestHandler.address_string(self)
+        else:
+            return self.client_address[0]
+
+    def do_GET(self):
+        """Serve a GET request."""
+        cpg.request.method = 'GET'
+        _cphttptools.doRequest(
+            self.client_address[0],
+            self.address_string(),
+            self.raw_requestline,
+            self.headers,
+            self.rfile,
+            self.wfile
+        )
+
+    def do_HEAD(self): # Head is not implemented
+        """Serve a HEAD request."""
+        cpg.request.method = 'HEAD'
+        _cphttptools.doRequest(
+            self.client_address[0],
+            self.address_string(),
+            self.raw_requestline,
+            self.headers,
+            self.rfile,
+            self.wfile
+        )
+
+    def do_POST(self):
+        """Serve a POST request."""
+        cpg.request.method = 'POST'
+        _cphttptools.doRequest(
+            self.client_address[0],
+            self.address_string(),
+            self.raw_requestline,
+            self.headers,
+            self.rfile,
+            self.wfile
+        )
+
+        self.connection = self.request
+
+    def log_message(self, format, *args):
+        """ We have to override this to use our own logging mechanism """
+        _cputil.getSpecialFunction('_cpLogMessage')(format % args, "HTTP")
+
+
+class CherryHTTPServer(BaseHTTPServer.HTTPServer):
+    def server_activate(self):
+        """Override server_activate to set timeout on our listener socket"""
+        self.socket.settimeout(1)
+        BaseHTTPServer.HTTPServer.server_activate(self)
+
+    def server_bind(self):
+        # Removed getfqdn call because it was timing out on localhost when calling gethostbyaddr
+        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self.socket.bind(self.server_address)
+
+    def get_request(self):
+        # With Python 2.3 it seems that an accept socket in timeout (nonblocking) mode
+        #  results in request sockets that are also set in nonblocking mode. Since that doesn't play
+        #  well with makefile() (where wfile and rfile are set in SocketServer.py) we explicitly set
+        #  the request socket to blocking
+
+        request, client_address = self.socket.accept()
+        request.setblocking(1)
+        return request, client_address
+
+    def handle_request(self):
+        """Override handle_request to trap timeout exception."""
+        try:
+            BaseHTTPServer.HTTPServer.handle_request(self)
+        except socket.timeout:
+            # The only reason for the timeout is so we can notice keyboard
+            # interrupts on Win32, which don't interrupt accept() by default
+            return 1
+        except KeyboardInterrupt:
+            _cpLogMessage("<Ctrl-C> hit: shutting down", "HTTP")
+            self.shutdown()
+
+    def serve_forever(self):
+        """Override serve_forever to handle shutdown."""
+        self.__running = 1
+        while self.__running:
+            self.handle_request()
+
+    def shutdown(self):
+        self.__running = 0
+
+_SHUTDOWNREQUEST = (0,0)
+
+class ServerThread(threading.Thread):
+    def __init__(self, RequestHandlerClass, requestQueue, threadIndex):
+        threading.Thread.__init__(self)
+        self._RequestHandlerClass = RequestHandlerClass
+        self._requestQueue = requestQueue
+        self._threadIndex = threadIndex
+        
+    def run(self):
+        # Call the functions from cpg.server.onStartThreadList
+        for func in cpg.server.onStartThreadList:
+            func(self._threadIndex)
+        while 1:
+            request, client_address = self._requestQueue.get()
+            if (request, client_address) == _SHUTDOWNREQUEST:
+                # Call the functions from cpg.server.onStopThreadList
+                for func in cpg.server.onStopThreadList:
+                    func()
+                return
+            if self.verify_request(request, client_address):            
+                try:
+                    self.process_request(request, client_address)
+                except:
+                    self.handle_error(request, client_address)
+                    self.close_request(request)
+            else:
+                self.close_request(request)
+
+    def verify_request(self, request, client_address):
+        """ Verify the request.  May be overridden.
+            Return 1 if we should proceed with this request. """
+        return 1
+
+    def process_request(self, request, client_address):
+        self._RequestHandlerClass(request, client_address, self)        
+        self.close_request(request)
+
+    def close_request(self, request):
+        """ Called to clean up an individual request. """
+        request.close()
+
+    def handle_error(self, request, client_address):
+        """ Handle an error gracefully.  May be overridden.
+            The default is to print a traceback and continue.
+        """
+        import traceback, StringIO
+        bodyFile=StringIO.StringIO()
+        traceback.print_exc(file=bodyFile)
+        errorBody=bodyFile.getvalue()
+        bodyFile.close()
+        _cputil.getSpecialFunction('_cpLogMessage')(errorBody)
+        
+
+class PooledThreadServer(SocketServer.TCPServer):
+
+    allow_reuse_address = 1
+
+    """A TCP Server using a pool of worker threads. This is superior to the
+       alternatives provided by the Python standard library, which only offer
+       (1) handling a single request at a time, (2) handling each request in
+       a separate thread (via ThreadingMixIn), or (3) handling each request in
+       a separate process (via ForkingMixIn). It's also superior in some ways
+       to the pure async approach used by Twisted because it allows a more
+       straightforward and simple programming model in the face of blocking
+       requests (i.e. you don't have to bother with Deferreds).""" 
+    def __init__(self, serverAddress, numThreads, RequestHandlerClass, ThreadClass=ServerThread):
+        assert(numThreads > 0)
+        # I know it says "do not override", but I have to in order to implement SSL support !
+        SocketServer.BaseServer.__init__(self, serverAddress, RequestHandlerClass)
+        self.socket=socket.socket(self.address_family, self.socket_type)
+        self.server_bind()
+        self.server_activate()
+
+        self._numThreads = numThreads        
+        self._RequestHandlerClass = RequestHandlerClass
+        self._ThreadClass = ThreadClass
+        self._requestQueue = Queue.Queue()
+        self._workerThreads = []
+
+    def createThread(self, threadIndex):
+        return self._ThreadClass(self._RequestHandlerClass, self._requestQueue, threadIndex)
+            
+    def start(self):
+        if self._workerThreads != []:
+            return
+        for i in xrange(self._numThreads):
+            self._workerThreads.append(self.createThread(i))        
+        for worker in self._workerThreads:
+            worker.start()
+            
+    def server_close(self):
+        """Override server_close to shutdown thread pool"""
+        SocketServer.TCPServer.server_close(self)
+        for worker in self._workerThreads:
+            self._requestQueue.put(_SHUTDOWNREQUEST)
+        for worker in self._workerThreads:
+            worker.join()
+        self._workerThreads = []
+
+    def server_activate(self):
+        """Override server_activate to set timeout on our listener socket"""
+        self.socket.settimeout(1)
+        SocketServer.TCPServer.server_activate(self)
+
+    def server_bind(self):
+        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self.socket.bind(self.server_address)
+
+    def shutdown(self):
+        """Gracefully shutdown a server that is serve_forever()ing."""
+        self.__running = 0
+
+    def serve_forever(self):
+        """Handle one request at a time until doomsday (or shutdown is called)."""
+        if self._workerThreads == []:
+            self.start()
+        self.__running = 1
+        while self.__running:
+            if not self.handle_request():
+                break
+        self.server_close()            
+        
+    def handle_request(self):
+        """Override handle_request to enqueue requests rather than handle
+           them synchronously. Return 1 by default, 0 to shutdown the
+           server."""
+        try:
+            request, client_address = self.get_request()
+        except KeyboardInterrupt:
+            _cpLogMessage("<Ctrl-C> hit: shutting down", "HTTP")
+            return 0
+        except socket.error, e:
+            return 1
+        self._requestQueue.put((request, client_address))
+        return 1
+
+    def get_request(self):
+        # With Python 2.3 it seems that an accept socket in timeout (nonblocking) mode
+        #  results in request sockets that are also set in nonblocking mode. Since that doesn't play
+        #  well with makefile() (where wfile and rfile are set in SocketServer.py) we explicitly set
+        #  the request socket to blocking
+
+        request, client_address = self.socket.accept()
+        if hasattr(request,'setblocking'):
+            request.setblocking(1)
+        return request, client_address
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/_cphttptools.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,543 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import cpg, urllib, sys, time, traceback, types, StringIO, cgi, os
+import mimetypes, hashlib, random, string, _cputil, cperror, Cookie, urlparse
+from lib.filter import basefilter
+
+"""
+Common Service Code for CherryPy
+"""
+
+mimetypes.types_map['.dwg']='image/x-dwg'
+mimetypes.types_map['.ico']='image/x-icon'
+
+weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+
+class IndexRedirect(Exception): pass
+
+def parseFirstLine(data):
+    cpg.request.path = data.split()[1]
+    cpg.request.queryString = ""
+    cpg.request.browserUrl = cpg.request.path
+    cpg.request.paramMap = {}
+    cpg.request.paramList = [] # Only used for Xml-Rpc
+    cpg.request.filenameMap = {}
+    cpg.request.fileTypeMap = {}
+    i = cpg.request.path.find('?')
+    if i != -1:
+        # Parse parameters from URL
+        if cpg.request.path[i+1:]:
+            k = cpg.request.path[i+1:].find('?')
+            if k != -1:
+                j = cpg.request.path[:k].rfind('=')
+                if j != -1:
+                    cpg.request.path = cpg.request.path[:j+1] + \
+                        urllib.quote_plus(cpg.request.path[j+1:])
+            for paramStr in cpg.request.path[i+1:].split('&'):
+                sp = paramStr.split('=')
+                if len(sp) > 2:
+                    j = paramStr.find('=')
+                    sp = (paramStr[:j], paramStr[j+1:])
+                if len(sp) == 2:
+                    key, value = sp
+                    value = urllib.unquote_plus(value)
+                    if cpg.request.paramMap.has_key(key):
+                        # Already has a value: make a list out of it
+                        if type(cpg.request.paramMap[key]) == type([]):
+                            # Already is a list: append the new value to it
+                            cpg.request.paramMap[key].append(value)
+                        else:
+                            # Only had one value so far: start a list
+                            cpg.request.paramMap[key] = [cpg.request.paramMap[key], value]
+                    else:
+                        cpg.request.paramMap[key] = value
+        cpg.request.queryString = cpg.request.path[i+1:]
+        cpg.request.path = cpg.request.path[:i]
+
+def cookHeaders(clientAddress, remoteHost, headers, requestLine):
+    """Process the headers into the request.headerMap"""
+    cpg.request.headerMap = {}
+    cpg.request.requestLine = requestLine
+    cpg.request.simpleCookie = Cookie.SimpleCookie()
+
+    # Build headerMap
+    for item in headers.items():
+        # Warning: if there is more than one header entry for cookies (AFAIK, only Konqueror does that)
+        # only the last one will remain in headerMap (but they will be correctly stored in request.simpleCookie)
+        insertIntoHeaderMap(item[0],item[1])
+
+    # Handle cookies differently because on Konqueror, multiple cookies come on different lines with the same key
+    cookieList = headers.getallmatchingheaders('cookie')
+    for cookie in cookieList:
+        cpg.request.simpleCookie.load(cookie)
+
+    cpg.request.remoteAddr = clientAddress
+    cpg.request.remoteHost = remoteHost
+
+    # Set peer_certificate (in SSL mode) so the web app can examinate the client certificate
+    try: cpg.request.peerCertificate = self.request.get_peer_certificate()
+    except: pass
+
+    _cputil.getSpecialFunction('_cpLogMessage')("%s - %s" % (cpg.request.remoteAddr, requestLine[:-2]), "HTTP")
+
+
+def parsePostData(rfile):
+    # Read request body and put it in data
+    len = int(cpg.request.headerMap.get("Content-Length","0"))
+    if len: data = rfile.read(len)
+    else: data=""
+
+    # Put data in a StringIO so FieldStorage can read it
+    newRfile = StringIO.StringIO(data)
+    # Create a copy of headerMap with lowercase keys because
+    #   FieldStorage doesn't work otherwise
+    lowerHeaderMap = {}
+    for key, value in cpg.request.headerMap.items():
+        lowerHeaderMap[key.lower()] = value
+    forms = cgi.FieldStorage(fp = newRfile, headers = lowerHeaderMap, environ = {'REQUEST_METHOD':'POST'}, keep_blank_values = 1)
+    for key in forms.keys():
+        # Check if it's a list or not
+        valueList = forms[key]
+        if type(valueList) == type([]):
+            # It's a list of values
+            cpg.request.paramMap[key] = []
+            cpg.request.filenameMap[key] = []
+            cpg.request.fileTypeMap[key] = []
+            for item in valueList:
+                cpg.request.paramMap[key].append(item.value)
+                cpg.request.filenameMap[key].append(item.filename)
+                cpg.request.fileTypeMap[key].append(item.type)
+        else:
+            # It's a single value
+            # In case it's a file being uploaded, we save the filename in a map (user might need it)
+            cpg.request.paramMap[key] = valueList.value
+            cpg.request.filenameMap[key] = valueList.filename
+            cpg.request.fileTypeMap[key] = valueList.type
+
+def applyFilterList(methodName):
+    try:
+        filterList = _cputil.getSpecialFunction('_cpFilterList')
+        for filter in filterList:
+            method = getattr(filter, methodName, None)
+            if method:
+                method()
+    except basefilter.InternalRedirect:
+        # If we get an InternalRedirect, we start the filter list
+        #   from scratch. Is cpg.request.path or cpg.request.objectPath
+        #   has been modified by the hook, then a new filter list
+        #   will be applied.
+        # We use recursion so if there is an infinite loop, we'll
+        #   get the regular python "recursion limit exceeded" exception.
+        applyFilterList(methodName)
+
+
+def insertIntoHeaderMap(key,value):
+    normalizedKey = '-'.join([s.capitalize() for s in key.split('-')])
+    cpg.request.headerMap[normalizedKey] = value
+
+def initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile):
+    parseFirstLine(requestLine)
+    cookHeaders(clientAddress, remoteHost, headers, requestLine)
+
+    cpg.request.base = "http://" + cpg.request.headerMap['Host']
+    cpg.request.browserUrl = cpg.request.base + cpg.request.browserUrl
+    cpg.request.isStatic = False
+    cpg.request.parsePostData = True
+    cpg.request.rfile = rfile
+
+    # Change objectPath in filters to change the object that will get rendered
+    cpg.request.objectPath = None
+
+    applyFilterList('afterRequestHeader')
+
+    if cpg.request.method == 'POST' and cpg.request.parsePostData:
+        parsePostData(rfile)
+
+    applyFilterList('afterRequestBody')
+
+def doRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile):
+    # creates some attributes on cpg.response so filters can use them
+    cpg.response.wfile = wfile
+    cpg.response.sendResponse = 1
+    try:
+        initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile)
+    except basefilter.RequestHandled:
+        # request was already fully handled; it may be a cache hit
+        return
+
+    # Prepare response variables
+    now = time.time()
+    year, month, day, hh, mm, ss, wd, y, z = time.gmtime(now)
+    date = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (weekdayname[wd], day, monthname[month], year, hh, mm, ss)
+    cpg.response.headerMap = {
+        "protocolVersion": cpg.configOption.protocolVersion,
+        "Status": "200 OK",
+        "Content-Type": "text/html",
+        "Server": "CherryPy/" + cpg.__version__,
+        "Date": date,
+        "Set-Cookie": [],
+        "Content-Length": 0
+    }
+    cpg.response.simpleCookie = Cookie.SimpleCookie()
+
+    try:
+        handleRequest(cpg.response.wfile)
+    except:
+        # TODO: in some cases exceptions and filters are conflicting;
+        # error reporting seems to be broken in some cases. This code is
+        # a helper to check it
+        err = ""
+        exc_info_1 = sys.exc_info()[1]
+        if hasattr(exc_info_1, 'args') and len(exc_info_1.args) >= 1:
+            err = exc_info_1.args[0]
+
+        try:
+            _cputil.getSpecialFunction('_cpOnError')()
+
+            # Still save session data
+            if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
+                sessionId = cpg.response.simpleCookie[cpg.configOption.sessionCookieName].value
+                expirationTime = time.time() + cpg.configOption.sessionTimeout * 60
+                _cputil.getSpecialFunction('_cpSaveSessionData')(sessionId, cpg.request.sessionMap, expirationTime)
+
+            wfile.write('%s %s\r\n' % (cpg.response.headerMap['protocolVersion'], cpg.response.headerMap['Status']))
+
+            if (cpg.response.headerMap.has_key('Content-Length') and
+                    cpg.response.headerMap['Content-Length']==0):
+  	 	        buf = StringIO.StringIO()
+  	 	        [buf.write(x) for x in cpg.response.body]
+  	 	        buf.seek(0)
+  	 	        cpg.response.body = [buf.read()]
+  	 	        cpg.response.headerMap['Content-Length'] = len(cpg.response.body[0])
+
+            for key, valueList in cpg.response.headerMap.items():
+                if key not in ('Status', 'protocolVersion'):
+                    if type(valueList) != type([]): valueList = [valueList]
+                    for value in valueList:
+                        wfile.write('%s: %s\r\n'%(key, value))
+            wfile.write('\r\n')
+            for line in cpg.response.body:
+                wfile.write(line)
+        except:
+            bodyFile = StringIO.StringIO()
+            traceback.print_exc(file = bodyFile)
+            body = bodyFile.getvalue()
+            wfile.write('%s 200 OK\r\n' % cpg.configOption.protocolVersion)
+            wfile.write('Content-Type: text/plain\r\n')
+            wfile.write('Content-Length: %s\r\n' % len(body))
+            wfile.write('\r\n')
+            wfile.write(body)
+
+def sendResponse(wfile):
+    applyFilterList('beforeResponse')
+
+    # Set the content-length
+    if (cpg.response.headerMap.has_key('Content-Length') and
+            cpg.response.headerMap['Content-Length']==0):
+        buf = StringIO.StringIO()
+        [buf.write(x) for x in cpg.response.body]
+        buf.seek(0)
+        cpg.response.body = [buf.read()]
+        cpg.response.headerMap['Content-Length'] = len(cpg.response.body[0])
+
+    # Save session data
+    if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
+        sessionId = cpg.response.simpleCookie[cpg.configOption.sessionCookieName].value
+        expirationTime = time.time() + cpg.configOption.sessionTimeout * 60
+        _cputil.getSpecialFunction('_cpSaveSessionData')(sessionId, cpg.request.sessionMap, expirationTime)
+
+    wfile.write('%s %s\r\n' % (cpg.response.headerMap['protocolVersion'], cpg.response.headerMap['Status']))
+    for key, valueList in cpg.response.headerMap.items():
+        if key not in ('Status', 'protocolVersion'):
+            if type(valueList) != type([]): valueList = [valueList]
+            for value in valueList:
+                wfile.write('%s: %s\r\n' % (key, value))
+
+    # Send response cookies
+    cookie = cpg.response.simpleCookie.output()
+    if cookie:
+        wfile.write(cookie+'\r\n')
+    wfile.write('\r\n')
+
+    for line in cpg.response.body:
+        wfile.write(line)
+
+    # finalization hook for filter cleanup & logging purposes
+    applyFilterList('afterResponse')
+
+def handleRequest(wfile):
+    # Clean up expired sessions if needed:
+    now = time.time()
+    if cpg.configOption.sessionStorageType and cpg.configOption.sessionCleanUpDelay and cpg._lastSessionCleanUpTime + cpg.configOption.sessionCleanUpDelay * 60 <= now:
+        cpg._lastSessionCleanUpTime = now
+        _cputil.getSpecialFunction('_cpCleanUpOldSessions')()
+
+    # Save original values (in case they get modified by filters)
+    cpg.request.originalPath = cpg.request.path
+    cpg.request.originalParamMap = cpg.request.paramMap
+    cpg.request.originalParamList = cpg.request.paramList
+
+    path = cpg.request.path
+    if path.startswith('/'):
+        # Remove leading slash
+        path = path[1:]
+    if path.endswith('/'):
+        # Remove trailing slash
+        path = path[:-1]
+    path = urllib.unquote(path) # Replace quoted chars (eg %20) from url
+
+    # Handle static directories
+    for urlDir, fsDir in cpg.configOption.staticContentList:
+        if path == urlDir or path[:len(urlDir)+1]==urlDir+'/':
+
+            cpg.request.isStatic = 1
+
+            fname = fsDir + path[len(urlDir):]
+            start_url_var = cpg.request.browserUrl.find('?')
+            if start_url_var != -1: fname = fname + cpg.request.browserUrl[start_url_var:]
+            try:
+                stat = os.stat(fname)
+            except OSError:
+                raise cperror.NotFound
+            modifTime = stat.st_mtime
+
+            strModifTime = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(modifTime))
+
+            # Check if browser sent "if-modified-since" in request header
+            if cpg.request.headerMap.has_key('If-Modified-Since'):
+                # Check if if-modified-since date is the same as strModifTime
+                if cpg.request.headerMap['If-Modified-Since'] == strModifTime:
+                    cpg.response.headerMap = {
+                        'Status': 304,
+                        'protocolVersion': cpg.configOption.protocolVersion,
+                        'Date': cpg.response.headerMap['Date']}
+                    cpg.response.body = []
+                    sendResponse(wfile)
+                    return
+
+            cpg.response.headerMap['Last-Modified'] = strModifTime
+            # Set Content-Length and use an iterable (file object)
+            #   this way CP won't load the whole file in memory
+            cpg.response.headerMap['Content-Length'] = stat[6]
+            cpg.response.body = open(fname, 'rb')
+            # Set content-type based on filename extension
+            i = path.rfind('.')
+            if i != -1: ext = path[i:]
+            else: ext = ""
+            contentType = mimetypes.types_map.get(ext, "text/plain")
+            cpg.response.headerMap['Content-Type'] = contentType
+            sendResponse(wfile)
+            return
+
+    # Get session data
+    if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
+        now = time.time()
+        # First, get sessionId from cookie
+        try: sessionId = cpg.request.simpleCookie[cpg.configOption.sessionCookieName].value
+        except: sessionId=None
+        if sessionId:
+            # Load session data from wherever it was stored
+            sessionData = _cputil.getSpecialFunction('_cpLoadSessionData')(sessionId)
+            if sessionData == None:
+                sessionId = None
+            else:
+                cpg.request.sessionMap, expirationTime = sessionData
+                # Check that is hasn't expired
+                if now > expirationTime:
+                    # Session expired
+                    sessionId = None
+
+        # Create a new sessionId if needed
+        if not sessionId:
+            cpg.request.sessionMap = {}
+            sessionId = generateSessionId()
+            cpg.request.sessionMap['_sessionId'] = sessionId
+
+        cpg.response.simpleCookie[cpg.configOption.sessionCookieName] = sessionId
+        cpg.response.simpleCookie[cpg.configOption.sessionCookieName]['path'] = '/'
+        cpg.response.simpleCookie[cpg.configOption.sessionCookieName]['version'] = 1
+
+    try:
+        func, objectPathList, virtualPathList = mapPathToObject()
+    except IndexRedirect, inst:
+        # For an IndexRedirect, we don't go through the regular
+        #   mechanism: we return the redirect immediately
+        newUrl = urlparse.urljoin(cpg.request.base, inst.args[0])
+        wfile.write('%s 302\r\n' % (cpg.response.headerMap['protocolVersion']))
+        cpg.response.headerMap['Location'] = newUrl
+        for key, valueList in cpg.response.headerMap.items():
+            if key not in ('Status', 'protocolVersion'):
+                if type(valueList) != type([]): valueList = [valueList]
+                for value in valueList:
+                    wfile.write('%s: %s\r\n'%(key, value))
+        wfile.write('\r\n')
+        return
+
+    # Remove "root" from objectPathList and join it to get objectPath
+    cpg.request.objectPath = '/' + '/'.join(objectPathList[1:])
+    body = func(*(virtualPathList + cpg.request.paramList), **(cpg.request.paramMap))
+
+    # builds a uniform return type
+    if not isinstance(body, types.GeneratorType):
+        cpg.response.body = [body]
+    else:
+        cpg.response.body = body
+
+    if cpg.response.sendResponse:
+        sendResponse(wfile)
+
+def generateSessionId():
+    s = ''
+    for i in range(50):
+        s += random.choice(string.letters+string.digits)
+    s += '%s'%time.time()
+    return hashlib.hashlib(s).hexdigest()
+
+def getObjFromPath(objPathList, objCache):
+    """ For a given objectPathList (like ['root', 'a', 'b', 'index']),
+         return the object (or None if it doesn't exist).
+         Also keep a cache for maximum efficiency
+    """
+    # Let cpg be the first valid object.
+    validObjects = ["cpg"]
+
+    # Scan the objPathList in order from left to right
+    for index, obj in enumerate(objPathList):
+        # maps virtual filenames to Python identifiers (substitutes '.' for '_')
+        obj = obj.replace('.', '_')
+
+        # currentObjStr holds something like 'cpg.root.something.else'
+        currentObjStr = ".".join(validObjects)
+
+        #---------------
+        #   Cache check
+        #---------------
+        # Generate a cacheKey from the first 'index' elements of objPathList
+        cacheKey = tuple(objPathList[:index+1])
+        # Is this cacheKey in the objCache?
+        if cacheKey in objCache:
+            # And is its value not None?
+            if objCache[cacheKey]:
+                # Yes, then add it to the list of validObjects
+                validObjects.append(obj)
+                # OK, go to the next iteration
+                continue
+            # Its value is None, so we stop
+            # (This means it is not a valid object)
+            break
+
+        #-----------------
+        # Attribute check
+        #-----------------
+        if getattr(eval(currentObjStr), obj, None):
+            #  obj is a valid attribute of the current object
+            validObjects.append(obj)
+            #  Store it in the cache
+            objCache[cacheKey] = eval(".".join(validObjects))
+        else:
+            # obj is not a valid attribute
+            # Store None in the cache
+            objCache[cacheKey] = None
+            # Stop, we won't process the remaining objPathList
+            break
+
+    # Return the last cached object (even if its None)
+    return objCache[cacheKey]
+
+def mapPathToObject(path = None):
+    # Traverse path:
+    # for /a/b?arg=val, we'll try:
+    #   root.a.b.index -> redirect to /a/b/?arg=val
+    #   root.a.b.default(arg='val') -> redirect to /a/b/?arg=val
+    #   root.a.b(arg='val')
+    #   root.a.default('b', arg='val')
+    #   root.default('a', 'b', arg='val')
+
+    # Also, we ignore trailing slashes
+    # Also, a method has to have ".exposed = True" in order to be exposed
+
+    if path is None:
+        path = cpg.request.objectPath or cpg.request.path
+    if path.startswith('/'):
+        path = path[1:] # Remove leading slash
+    if path.endswith('/'):
+        path = path[:-1] # Remove trailing slash
+
+    if not path:
+        objectPathList = []
+    else:
+        objectPathList = path.split('/')
+    objectPathList = ['root'] + objectPathList + ['index']
+
+    # Try successive objects... (and also keep the remaining object list)
+    objCache = {}
+    isFirst = True
+    isSecond = False
+    isDefault = False
+    foundIt = False
+    virtualPathList = []
+    while objectPathList:
+        if isFirst or isSecond:
+            # Only try this for a.b.index() or a.b()
+            candidate = getObjFromPath(objectPathList, objCache)
+            if callable(candidate) and getattr(candidate, 'exposed', False):
+                foundIt = True
+                break
+        # Couldn't find the object: pop one from the list and try "default"
+        lastObj = objectPathList.pop()
+        if (not isFirst) or (not path):
+            virtualPathList.insert(0, lastObj)
+            objectPathList.append('default')
+            candidate = getObjFromPath(objectPathList, objCache)
+            if callable(candidate) and getattr(candidate, 'exposed', False):
+                foundIt = True
+                isDefault = True
+                break
+            objectPathList.pop() # Remove "default"
+        if isSecond:
+            isSecond = False
+        if isFirst:
+            isFirst = False
+            isSecond = True
+
+    # Check results of traversal
+    if not foundIt:
+        raise cperror.NotFound # We didn't find anything
+
+    if isFirst:
+        # We found the extra ".index"
+        # Check if the original path had a trailing slash (otherwise, do
+        #   a redirect)
+        if cpg.request.path[-1] != '/':
+            newUrl = cpg.request.path + '/'
+            if cpg.request.queryString: newUrl += cpg.request.queryString
+            raise IndexRedirect(newUrl)
+
+    return candidate, objectPathList, virtualPathList
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/_cpserver.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,94 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+Main CherryPy module:
+    - Parses config file
+    - Creates the HTTP server
+"""
+
+import cpg, thread, _cputil, _cpconfig, _cphttpserver, time, _cpthreadinglocal
+
+def start(configFile = None, parsedConfigFile = None, configMap = {}, initOnly = 0):
+    """
+        Main function. All it does is this:
+            - read/parse config file if any
+            - create response and request objects
+            - creates HTTP server based on configFile and configMap
+            - start HTTP server
+
+        Input: There are 2 ways to pass config options:
+            - Let CherryPy parse a config file (configFile)
+            - Pass the options as a dictionary (configMap)
+    """
+
+    # cpg.configOption contains an EmptyClass instance with all the configuration option
+    _cpconfig.setDefaultConfigOption()
+
+    # cpg.parsedConfigFile contains the ConfigParser instance with the parse config file
+    cpg.parsedConfigFile = None
+
+    if configFile:
+        _cpconfig.parseConfigFile(configFile = configFile)
+    elif parsedConfigFile:
+        _cpconfig.parseConfigFile(parsedConfigFile = parsedConfigFile)
+
+    if configMap:
+        for key, value in configMap.items():
+            setattr(cpg.configOption, key, value)
+
+    # Output config options
+    _cpconfig.outputConfigOptions()
+
+    # Check the config options
+    # TODO
+    # _cpconfig.checkConfigOptions()
+
+    # Create request and response object (the same objects will be used
+    #   throughout the entire life of the webserver)
+    cpg.request = _cpthreadinglocal.local()
+    cpg.response = _cpthreadinglocal.local()
+    # Create threadData object as a thread-specific all-purpose storage
+    cpg.threadData = _cpthreadinglocal.local()
+
+    # Initialize a few global variables
+    cpg._lastCacheFlushTime = time.time()
+    cpg._lastSessionCleanUpTime = time.time()
+    cpg._sessionMap = {} # Map of "cookie" -> ("session object", "expiration time")
+
+    if not initOnly:
+        _cphttpserver.start()
+
+def stop():
+    _cphttpserver.stop()
+
+# Set some special attributes for adding hooks
+onStartServerList = []
+onStartThreadList = []
+onStopServerList = []
+onStopThreadList = []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/_cpthreadinglocal.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,239 @@
+# This is a backport of Python-2.4's threading.local() implementation
+
+"""Thread-local objects
+
+(Note that this module provides a Python version of thread
+ threading.local class.  Depending on the version of Python you're
+ using, there may be a faster one available.  You should always import
+ the local class from threading.)
+
+Thread-local objects support the management of thread-local data.
+If you have data that you want to be local to a thread, simply create
+a thread-local object and use its attributes:
+
+  >>> mydata = local()
+  >>> mydata.number = 42
+  >>> mydata.number
+  42
+
+You can also access the local-object's dictionary:
+
+  >>> mydata.__dict__
+  {'number': 42}
+  >>> mydata.__dict__.setdefault('widgets', [])
+  []
+  >>> mydata.widgets
+  []
+
+What's important about thread-local objects is that their data are
+local to a thread. If we access the data in a different thread:
+
+  >>> log = []
+  >>> def f():
+  ...     items = mydata.__dict__.items()
+  ...     items.sort()
+  ...     log.append(items)
+  ...     mydata.number = 11
+  ...     log.append(mydata.number)
+
+  >>> import threading
+  >>> thread = threading.Thread(target=f)
+  >>> thread.start()
+  >>> thread.join()
+  >>> log
+  [[], 11]
+
+we get different data.  Furthermore, changes made in the other thread
+don't affect data seen in this thread:
+
+  >>> mydata.number
+  42
+
+Of course, values you get from a local object, including a __dict__
+attribute, are for whatever thread was current at the time the
+attribute was read.  For that reason, you generally don't want to save
+these values across threads, as they apply only to the thread they
+came from.
+
+You can create custom local objects by subclassing the local class:
+
+  >>> class MyLocal(local):
+  ...     number = 2
+  ...     initialized = False
+  ...     def __init__(self, **kw):
+  ...         if self.initialized:
+  ...             raise SystemError('__init__ called too many times')
+  ...         self.initialized = True
+  ...         self.__dict__.update(kw)
+  ...     def squared(self):
+  ...         return self.number ** 2
+
+This can be useful to support default values, methods and
+initialization.  Note that if you define an __init__ method, it will be
+called each time the local object is used in a separate thread.  This
+is necessary to initialize each thread's dictionary.
+
+Now if we create a local object:
+
+  >>> mydata = MyLocal(color='red')
+
+Now we have a default number:
+
+  >>> mydata.number
+  2
+
+an initial color:
+
+  >>> mydata.color
+  'red'
+  >>> del mydata.color
+
+And a method that operates on the data:
+
+  >>> mydata.squared()
+  4
+
+As before, we can access the data in a separate thread:
+
+  >>> log = []
+  >>> thread = threading.Thread(target=f)
+  >>> thread.start()
+  >>> thread.join()
+  >>> log
+  [[('color', 'red'), ('initialized', True)], 11]
+
+without affecting this thread's data:
+
+  >>> mydata.number
+  2
+  >>> mydata.color
+  Traceback (most recent call last):
+  ...
+  AttributeError: 'MyLocal' object has no attribute 'color'
+
+Note that subclasses can define slots, but they are not thread
+local. They are shared across threads:
+
+  >>> class MyLocal(local):
+  ...     __slots__ = 'number'
+
+  >>> mydata = MyLocal()
+  >>> mydata.number = 42
+  >>> mydata.color = 'red'
+
+So, the separate thread:
+
+  >>> thread = threading.Thread(target=f)
+  >>> thread.start()
+  >>> thread.join()
+
+affects what we see:
+
+  >>> mydata.number
+  11
+
+>>> del mydata
+"""
+
+# Threading import is at end
+
+class _localbase(object):
+    __slots__ = '_local__key', '_local__args', '_local__lock'
+
+    def __new__(cls, *args, **kw):
+        self = object.__new__(cls)
+        key = '_local__key', 'thread.local.' + str(id(self))
+        object.__setattr__(self, '_local__key', key)
+        object.__setattr__(self, '_local__args', (args, kw))
+        object.__setattr__(self, '_local__lock', RLock())
+
+        if args or kw and (cls.__init__ is object.__init__):
+            raise TypeError("Initialization arguments are not supported")
+
+        # We need to create the thread dict in anticipation of
+        # __init__ being called, to make sire we don't cal it
+        # again ourselves.
+        dict = object.__getattribute__(self, '__dict__')
+        currentThread().__dict__[key] = dict
+
+        return self
+
+def _patch(self):
+    key = object.__getattribute__(self, '_local__key')
+    d = currentThread().__dict__.get(key)
+    if d is None:
+        d = {}
+        currentThread().__dict__[key] = d
+        object.__setattr__(self, '__dict__', d)
+
+        # we have a new instance dict, so call out __init__ if we have
+        # one
+        cls = type(self)
+        if cls.__init__ is not object.__init__:
+            args, kw = object.__getattribute__(self, '_local__args')
+            cls.__init__(self, *args, **kw)
+    else:
+        object.__setattr__(self, '__dict__', d)
+
+class local(_localbase):
+
+    def __getattribute__(self, name):
+        lock = object.__getattribute__(self, '_local__lock')
+        lock.acquire()
+        try:
+            _patch(self)
+            return object.__getattribute__(self, name)
+        finally:
+            lock.release()
+
+    def __setattr__(self, name, value):
+        lock = object.__getattribute__(self, '_local__lock')
+        lock.acquire()
+        try:
+            _patch(self)
+            return object.__setattr__(self, name, value)
+        finally:
+            lock.release()
+
+    def __delattr__(self, name):
+        lock = object.__getattribute__(self, '_local__lock')
+        lock.acquire()
+        try:
+            _patch(self)
+            return object.__delattr__(self, name)
+        finally:
+            lock.release()
+
+
+    def __del__():
+        threading_enumerate = enumerate
+        __getattribute__ = object.__getattribute__
+
+        def __del__(self):
+            key = __getattribute__(self, '_local__key')
+
+            try:
+                threads = list(threading_enumerate())
+            except:
+                # if enumerate fails, as it seems to do during
+                # shutdown, we'll skip cleanup under the assumption
+                # that there is nothing to clean up
+                return
+
+            for thread in threads:
+                try:
+                    __dict__ = thread.__dict__
+                except AttributeError:
+                    # Thread is dying, rest in peace
+                    continue
+
+                if key in __dict__:
+                    try:
+                        del __dict__[key]
+                    except KeyError:
+                        pass # didn't have anything in this thread
+
+        return __del__
+    __del__ = __del__()
+
+from threading import currentThread, enumerate, RLock
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/_cputil.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,82 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+
+"""
+A module containing a few utility classes/functions used by CherryPy
+"""
+
+import time, thread, cpg, _cpdefaults, cperror
+
+try: import zlib
+except ImportError: pass
+
+class EmptyClass:
+    """ An empty class """
+    pass
+
+def getSpecialFunction(name):
+    """ Return the special function """
+
+    # First, we look in the right-most object if this special function is implemented.
+    # If not, then we try the previous object and so on until we reach cpg.root
+    # If it's still not there, we use the implementation from the
+    # "_cpdefaults.py" module
+    
+
+    moduleList = [_cpdefaults]
+    root = getattr(cpg, 'root', None)
+    if root:
+        moduleList.append(root)
+        # Try object path
+        try:
+            path = cpg.request.objectPath or cpg.request.path
+        except:
+            path = '/'
+        if path:
+            pathList = path.split('/')[1:]
+
+            obj = cpg.root
+            previousObj = None
+            # Successively get objects from the path
+            for newObj in pathList:
+                previousObj = obj
+                try:
+                    obj = getattr(obj, newObj)
+                    moduleList.append(obj)
+                except AttributeError:
+                    break
+
+    moduleList.reverse()
+    for module in moduleList:
+        func = getattr(module, name, None)
+        if func != None:
+            return func
+
+    raise cperror.InternalError, "Special function %s could not be found" % repr(name)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/cperror.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,48 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+Main CherryPy module:
+    - Parses config file
+    - Creates the HTTP server
+"""
+
+class Error(Exception):
+    pass
+
+class InternalError(Error):
+    """ Error that should never happen """
+    pass
+
+class NotFound(Error):
+    """ Happens when a URL couldn't be mapped to any class.method """
+    pass
+
+class WrongResponseType(Error):
+    """ Happens when the cpg.response.body is not a string """
+    pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/cpg.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,41 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+Global module that all modules developing with CherryPy should import.
+"""
+
+from __init__ import __version__
+
+# import server module
+import _cpserver as server
+
+# decorator function for exposing methods
+def expose(func):
+    func.exposed = True
+    return func
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/__init__.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,4 @@
+"""
+CherryPy Standard Library
+"""
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/aspect.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,87 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+# return codes for _before and _after aspect methods
+STOP = 0
+CONTINUE = 1
+
+class Aspect(object):
+    """
+    Base class for aspects. Derive new aspect classes from this, then
+    override one or both of _before and _after.
+    """
+
+    def __getattribute__(self, methodName):
+
+        # find method specified by methodName
+        try:
+            method = object.__getattribute__(self, methodName)
+        except:
+            raise
+
+        # if requested attribute is not a method, simply return it
+        if not callable(method):
+            return method
+
+        # define wrapper function
+        def _wrapper(*k, **kw):
+            # call _before method
+            status, value = object.__getattribute__(self, '_before')(methodName, method)
+            if status == STOP:
+                return value
+
+            # call wrapped method and append results
+            result = method(*k, **kw)
+            if value:
+                result = value + result
+
+            # call _after method
+            status, value =  object.__getattribute__(self, '_after')(methodName, method)
+            if status == STOP:
+                return value
+            if value:
+                result += value
+
+            # done!
+            return result
+
+        # expose wrapper function if wrapped method is exposed
+        if getattr(method, 'exposed', None):
+            _wrapper.exposed = True
+
+        # return wrapper function. It'll get called instead of the
+        # requested method.
+        return _wrapper
+
+
+    def _before(self, methodName, method):
+        return CONTINUE, None
+
+
+    def _after(self, methodName, method):
+        return CONTINUE, None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/cptools.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,51 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+Just a few convenient functions
+"""
+
+class ExposeItems:
+    """
+    Utility class that exposes a getitem-aware object. It does not provide
+    index() or default() methods, and it does not expose the individual item
+    objects - just the list or dict that contains them. User-specific index()
+    and default() methods can be implemented by inheriting from this class.
+    
+    Use case:
+    
+    from cherrypy.lib.cptools import ExposeItems
+    ...
+    cpg.root.foo = ExposeItems(mylist)
+    cpg.root.bar = ExposeItems(mydict)
+    """
+    exposed = True
+    def __init__(self, items):
+        self.items = items
+    def __getattr__(self, key):
+        return self.items[key]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/csauthenticate.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,140 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import time, whrandom
+from cherrypy import cpg
+
+from aspect import Aspect, STOP, CONTINUE
+
+class CSAuthenticate(Aspect):
+    timeoutMessage = "Session timed out"
+    wrongLoginPasswordMessage = "Wrong login/password"
+    noCookieMessage = "No cookie"
+    logoutMessage = "You have been logged out"
+    sessionIdCookieName = "CherrySessionId"
+    timeout = 60 # in minutes
+
+    def _before(self, methodName, method):
+        # If the method is not exposed, don't do anything
+        if not getattr(method, 'exposed', None):
+            return CONTINUE, None
+
+        cpg.request.login = ''
+        # If the method is one of these 4, do not try to find out who is logged in
+        if methodName in ["loginScreen", "logoutScreen", "doLogin", "doLogout"]:
+            return CONTINUE, None
+
+        # Check if a user is logged in:
+        #   - If they are, set request.login with the right value
+        #   - If not, return the login screen
+        if not cpg.request.simpleCookie.has_key(self.sessionIdCookieName):
+            return STOP, self.loginScreen(self.noCookieMessage, cpg.request.browserUrl)
+        sessionId = cpg.request.simpleCookie[self.sessionIdCookieName].value
+        now=time.time()
+
+        # Check that session exists and hasn't timed out
+        timeout=0
+        if not cpg.request.sessionMap.has_key(sessionId):
+            return STOP, self.loginScreen(self.noCookieMessage, cpg.request.browserUrl)
+        else:
+            login, expire = cpg.request.sessionMap[sessionId]
+            if expire < now: timeout=1
+            else:
+                expire = now + self.timeout*60
+                cpg.request.sessionMap[sessionId] = login, expire
+
+        if timeout:
+            return STOP, self.loginScreen(self.timeoutMessage, cpg.request.browserUrl)
+
+        cpg.request.login = login
+        return CONTINUE, None
+
+    def checkLoginAndPassword(self, login, password):
+        if (login,password) == ('login','password'): return ''
+        return 'Wrong login/password'
+
+    def generateSessionId(self, sessionIdList):
+        choice="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        while 1:
+            sessionId=""
+            for dummy in range(20): sessionId += whrandom.choice(choice)
+            if sessionId not in sessionIdList: return sessionId
+
+    def doLogin(self, login, password, fromPage):
+        # Check that login/password match
+        errorMsg = self.checkLoginAndPassword(login, password)
+        if errorMsg:
+            cpg.request.login = ''
+            return self.loginScreen(errorMsg, fromPage, login)
+        cpg.request.login = login
+        # Set session
+        newSessionId = self.generateSessionId(cpg.request.sessionMap.keys())
+        cpg.request.sessionMap[newSessionId] = login, time.time()+self.timeout*60
+
+        cpg.response.simpleCookie[self.sessionIdCookieName] = newSessionId
+        cpg.response.simpleCookie[self.sessionIdCookieName]['path'] = '/'
+        cpg.response.simpleCookie[self.sessionIdCookieName]['max-age'] = 31536000
+        cpg.response.simpleCookie[self.sessionIdCookieName]['version'] = 1
+        cpg.response.headerMap['Status'] = 302
+        cpg.response.headerMap['Location'] = fromPage
+        return ""
+    doLogin.exposed = True
+
+    def doLogout(self):
+        try:
+            sessionId = request.simpleCookie[self.sessionIdCookieName].value
+            del request.sessionMap[sessionId]
+        except: pass
+
+        cpg.response.simpleCookie[self.sessionIdCookieName] = ""
+        cpg.response.simpleCookie[self.sessionIdCookieName]['path'] = '/'
+        cpg.response.simpleCookie[self.sessionIdCookieName]['max-age'] = 0
+        cpg.response.simpleCookie[self.sessionIdCookieName]['version'] = 1
+        cpg.request.login = ''
+        cpg.response.headerMap['Status'] = 302
+        cpg.response.headerMap['Location'] = 'logoutScreen' # TBCTBC: may not be the right URL
+        return ""
+    doLogout.exposed = True
+
+    def logoutScreen(self):
+        return self.loginScreen(self.logoutMessage, '/index') # TBC
+    logoutScreen.exposed = True
+
+    def loginScreen(self, message, fromPage, login=''):
+        return """
+        <html><body>
+            Message: %s
+            <form method="post" action="doLogin">
+                Login: <input type=text name=login value="%s" size=10><br />
+                Password: <input type=password name=password size=10><br />
+                <input type=hidden name=fromPage value="%s"><br />
+                <input type=submit>
+            </form>
+        </body></html>
+        """ % (message, login, fromPage)
+    loginScreen.exposed = True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/defaultformmask.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,97 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+Default mask for the form.py module
+"""
+
+def defaultMask(field):
+    res="<tr><td valign=top>%s</td>"%field.label
+    if field.typ=='text':
+        res+='<td><input name="%s" type=text value="%s" size=%s></td>'%(field.name, field.currentValue, field.size)
+    elif field.typ=='forced':
+        res+='<td><input name="%s" type=hidden value="%s">%s</td>'%(field.name, field.currentValue, field.currentValue)
+    elif field.typ=='password':
+        res+='<td><input name="%s" type=password value="%s"></td>'%(field.name, field.currentValue)
+    elif field.typ=='select':
+        res+='<td><select name="%s">'%field.name
+        for option in field.optionList:
+            if type(option)==type(()):
+                optionId, optionLabel=option
+                if optionId==field.currentValue or str(optionId)==field.currentValue: res+="<option selected value=%s>%s</option>"%(optionId, optionLabel)
+                else: res+="<option value=%s>%s</option>"%(optionId, optionLabel)
+            else:
+                if option==field.currentValue: res+="<option selected>%s</option>"%option
+                else: res+="<option>%s</option>"%option
+        res+='</select></td>'
+    elif field.typ=='textarea':
+        # Size is colsxrows
+        if field.size==15: size="15x15"
+        else: size=field.size
+        cols, rows=size.split('x')
+        res+='<td><textarea name="%s" rows="%s" cols="%s">%s</textarea></td>'%(field.name, rows, cols, field.currentValue)
+    elif field.typ=='submit':
+        res+='<td><input type=submit value="%s"></td>'%field.name
+    elif field.typ=='hidden':
+        if type(field.currentValue)==type([]): currentValue=field.currentValue
+        else: currentValue=[field.currentValue]
+        res=""
+        for value in currentValue:
+            res+='<input name="%s" type=hidden value="%s">'%(field.name, value)
+        return res
+    elif field.typ=='checkbox' or field.typ=='radio':
+        res+='<td>'
+        # print "##### currentValue:", field.currentValue # TBC
+        for option in field.optionList:
+            if type(option)==type(()): optionValue, optionLabel=option
+            else: optionValue, optionLabel=option, option
+            res+='<input type="%s" name="%s" value="%s"'%(field.typ, field.name, optionValue)
+            if type(field.currentValue)==type([]):
+                if optionValue in field.currentValue: res+=' checked'
+            else:
+                if optionValue==field.currentValue: res+=' checked'
+            res+='>&nbsp;&nbsp;%s<br />'%optionLabel
+        res+='</td>'
+    if field.errorMessage:
+        res+="<td><font color=red>%s</font></td>"%field.errorMessage
+    else:
+        res+="<td>&nbsp;</td>"
+    return res+"</tr>"
+def hiddenMask(field):
+        if type(field.currentValue)==type([]): currentValue=field.currentValue
+        else: currentValue=[field.currentValue]
+        res=""
+        for value in currentValue:
+            res+='<input name="%s" type=hidden value="%s">'%(field.name, value)
+        return res
+def defaultHeader(label):
+    return "<table>"
+def defaultFooter(label):
+    return "</table>"
+def echoMask(label):
+    return label
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/basefilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,57 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+class InternalRedirect(Exception): pass
+class RequestHandled(Exception): pass
+
+class BaseInputFilter(object):
+    """
+    Base class for input filters. Derive new filter classes from this, then
+    override some of the methods to add some side-effects.
+    """
+    def afterRequestHeader(self):
+        """ Called after the request header has been read/parsed"""
+        pass
+
+    def afterRequestBody(self):
+        """ Called after the request body has been read/parsed"""
+        pass
+
+class BaseOutputFilter(object):
+    """
+    Base class for output filters. Derive new filter classes from this, then
+    override some of the methods to add some side-effects.
+    """
+    def beforeResponse(self):
+        """ Called before starting to write response """
+        pass
+
+    def afterResponse(self):
+        """ Called after writing the response (header & body included) """
+        pass
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/baseurlfilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,54 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+from basefilter import BaseInputFilter
+from cherrypy import cpg
+
+class BaseUrlFilter(BaseInputFilter):
+    """
+    Filter that changes the base URL.
+    Useful when running a CP server behind Apache.
+    """
+
+    def __init__(self, baseUrl = 'http://localhost', useXForwardedHost = True):
+        # New baseUrl
+        self.baseUrl = baseUrl
+        self.useXForwardedHost = useXForwardedHost
+
+    def afterRequestHeader(self):
+        if self.useXForwardedHost:
+            newBaseUrl = cpg.request.headerMap.get("X-Forwarded-Host", self.baseUrl)
+        else:
+            newBaseUrl = self.baseUrl
+        if newBaseUrl.find("://") == -1:
+            # add http:// or https:// if needed	
+            newBaseUrl = cpg.request.base[:cpg.request.base.find("://") + 3] + newBaseUrl
+
+        cpg.request.browserUrl = cpg.request.browserUrl.replace(
+            cpg.request.base, newBaseUrl)
+        cpg.request.base = newBaseUrl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/cachefilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,260 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import threading
+import Queue
+import time
+import cStringIO
+
+from basefilter import BaseInputFilter, RequestHandled
+from cherrypy import cpg
+
+def defaultCacheKey():
+    return cpg.request.browserUrl
+
+class Tee:
+    """
+    Wraps a stream object; chains the content that is written and keep a
+    copy in a StringIO for caching purposes.
+    """
+
+    def __init__(self, wfile, maxobjsize):
+        self.wfile = wfile
+        self.cache = cStringIO.StringIO()
+        self.maxobjsize = maxobjsize
+        self.caching = True
+        self.size = 0
+
+    def write(self, s):
+        self.wfile.write(s)
+        if self.caching:
+            self.size += len(s)
+            if self.size < self.maxobjsize:
+                self.cache.write(s)
+            else:
+                # exceeded the limit, aborts caching
+                self.stopCaching()
+
+    def flush(self):
+        self.wfile.flush()
+
+    def close(self):
+        self.wfile.close()
+        if self.caching:
+            self.stopCaching()
+
+    def stopCaching(self):
+        self.caching = False
+        self.cache.close()
+
+class MemoryCache:
+
+    def __init__(self, key, delay, maxobjsize, maxsize, maxobjects):
+        self.key = key
+        self.delay = delay
+        self.maxobjsize = maxobjsize
+        self.maxsize = maxsize
+        self.maxobjects = maxobjects
+        self.cursize = 0
+        self.cache = {}
+        self.expirationQueue = Queue.Queue()
+        self.expirationThread = threading.Thread(target=self.expireCache, name='expireCache')
+        self.expirationThread.setDaemon(True)
+        self.expirationThread.start()
+        self.totPuts = 0
+        self.totGets = 0
+        self.totHits = 0
+        self.totExpires = 0
+        self.totNonModified = 0
+
+    def expireCache(self):
+        while True:
+            expirationTime, objSize, objKey = self.expirationQueue.get(block=True, timeout=None)
+            while (time.time() < expirationTime):
+                time.sleep(0.1)
+            try:
+                del self.cache[objKey]
+                self.totExpires += 1
+                self.cursize -= objSize
+            except KeyError:
+                # the key may have been deleted elsewhere
+                pass
+
+    def get(self):
+        """
+        If the content is in the cache, returns a tuple containing the
+        expiration time, the lastModified response header and the object
+        (rendered as a string); returns None if the key is not found.
+        """
+        self.totGets += 1
+        cacheItem = self.cache.get(self.key(), None)
+        if cacheItem:
+            self.totHits += 1
+            return cacheItem
+        else:
+            return None
+
+    def put(self, lastModified, obj):
+        objSize = len(obj)
+        totalSize = self.cursize + objSize
+        # checks if there's space for the object
+        if ((objSize < self.maxobjsize) and
+            (totalSize < self.maxsize) and
+            (len(self.cache) < self.maxobjects)):
+            # add to the expirationQueue & cache
+            try:
+                expirationTime = time.time() + self.delay
+                objKey = self.key()
+                self.expirationQueue.put((expirationTime, objSize, objKey))
+                self.totPuts += 1
+                self.cursize += objSize
+            except Queue.Full:
+                # can't add because the queue is full
+                return
+            self.cache[objKey] = (expirationTime, lastModified, obj)
+
+class CacheInputFilter(BaseInputFilter):
+    """
+    Works on the input chain. If the page is already stored in the cache
+    serves the contents. If the page is not in the cache, it wraps the
+    cpg.response.wfile object; in this way, everything that is written is
+    recorded, independent if it was sent directly or not.
+    """
+
+    def __init__(
+            self,
+            CacheClass=MemoryCache,
+            key=defaultCacheKey,
+            delay=600,         # 10 minutes
+            maxobjsize=100000, # 100 KB
+            maxsize=10000000,  # 10 MB
+            maxobjects=1000    # 1000 objects
+            ):
+        cpg._cache = CacheClass(key, delay, maxobjsize, maxsize, maxobjects)
+
+    def afterRequestBody(self):
+        """ Checks if the page is already in the cache """
+        cacheData = cpg._cache.get()
+        if cacheData:
+            expirationTime, lastModified, obj = cacheData
+            # found a hit! check the if-modified-since request header
+            modifiedSince = cpg.request.headerMap.get('If-Modified-Since', None)
+            #print "Cache hit: If-Modified-Since=%s, lastModified=%s" % (modifiedSince, lastModified)
+            if modifiedSince == lastModified:
+                cpg._cache.totNonModified += 1
+                # the code below was borrowed from the sendResponse function
+                # it should be refactored & put into a function to allow reuse
+                cpg.response.wfile.write('%s %s\r\n' % (cpg.configOption.protocolVersion, 304))
+                # the code below doesn't work because the data isn't available at this point...
+                #cpg.response.wfile.write('%s: %s\r\n' % ('Date', cpg.request.headerMap['Date']))
+                # should the cache record & replay cookies it too?
+                cpg.response.wfile.write('\r\n')
+                raise RequestHandled
+            else:
+                # serve it & get out from the request
+                cpg.response.wfile.write(obj)
+                raise RequestHandled
+        else:
+            # sets a wrapper to cache the contents
+            cpg.response.wfile = Tee(cpg.response.wfile, cpg._cache.maxobjsize)
+            cpg.threadData.cacheable = True
+
+class CacheOutputFilter(object):
+    """
+    Works on the output chain. Stores the content of the page in the cache.
+    """
+
+    def beforeResponse(self):
+        """
+        Checks if the page is cacheable; if not so disables the cache.
+        Uses a flag that may be reset by intermediate filters. Note that
+        the output filter is usually the last filter in the chain, so
+        this method is probably the last one called before the response
+        is written.
+        """
+        if isinstance(cpg.response.wfile, Tee):
+            if cpg.threadData.cacheable:
+                return
+            # cancel caching
+            wrapper = cpg.response.wfile
+            wrapper.stopCaching()
+            cpg.response.wfile = wrapper.wfile
+
+    def afterResponse(self):
+        """
+        Close & fix the cache entry after content was fully written
+        """
+        if isinstance(cpg.response.wfile, Tee):
+            wrapper = cpg.response.wfile
+            if wrapper.caching:
+                if cpg.response.headerMap.get('Pragma', None) != 'no-cache':
+                    lastModified = cpg.response.headerMap.get('Last-Modified', None)
+                    # saves the cache data
+                    cpg._cache.put(lastModified, wrapper.cache.getvalue())
+                # closes the wrapper
+                wrapper.stopCaching()
+                cpg.response.wfile = wrapper.wfile
+
+def percentual(n,d):
+    """calculates the percentual, dealing with div by zeros"""
+    if d == 0:
+        return 0
+    else:
+        return (float(n)/float(d))*100
+
+def formatSize(n):
+    """formats a number as a memory size, in bytes, kbytes, MB, GB)"""
+    if n < 1024:
+        return "%4d bytes" % n
+    elif n < 1024*1024:
+        return "%4d kbytes" % (n / 1024)
+    elif n < 1024*1024*1024:
+        return "%4d MB" % (n / (1024*1024))
+    else:
+        return "%4d GB" % (n / (1024*1024*1024))
+
+class CacheStats:
+
+    def index(self):
+        cpg.response.headerMap['Content-Type'] = 'text/plain'
+        cpg.response.headerMap['Pragma'] = 'no-cache'
+        cache = cpg._cache
+        yield "Cache statistics\n"
+        yield "Maximum object size: %s\n" % formatSize(cache.maxobjsize)
+        yield "Maximum cache size: %s\n" % formatSize(cache.maxsize)
+        yield "Maximum number of objects: %d\n" % cache.maxobjects
+        yield "Current cache size: %s\n" % formatSize(cache.cursize)
+        yield "Approximated expiration queue size: %d\n" % cache.expirationQueue.qsize()
+        yield "Number of cache entries: %d\n" % len(cache.cache)
+        yield "Total cache writes: %d\n" % cache.totPuts
+        yield "Total cache read attempts: %d\n" % cache.totGets
+        yield "Total hits: %d (%1.2f%%)\n" % (cache.totHits, percentual(cache.totHits, cache.totGets))
+        yield "Total misses: %d (%1.2f%%)\n" % (cache.totGets-cache.totHits, percentual(cache.totGets-cache.totHits, cache.totGets))
+        yield "Total expires: %d\n" % cache.totExpires
+        yield "Total non-modified content: %d\n" % cache.totNonModified
+    index.exposed = True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/decodingfilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,53 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+from basefilter import BaseInputFilter
+from cherrypy import cpg
+import types
+
+class DecodingFilter(BaseInputFilter):
+    """
+    Filter that automatically decodes the request parameters (except files being uploaded).
+    """
+
+    def __init__(self, encoding = 'utf-8'):
+        self.encoding = encoding
+
+    def afterRequestBody(self):
+        for key, value in cpg.request.paramMap.items():
+            if key in cpg.request.filenameMap:
+                # This is a file being uploaded: skip it
+                continue
+            if isinstance(value, list):
+                # value is a list: decode each element
+                newValue = [v.decode(self.encoding) for v in value]
+            else:
+                # value is a regular string: decode it
+                newValue = value.decode(self.encoding)
+            cpg.request.paramMap[key] = newValue
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/encodingfilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,55 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+from basefilter import BaseOutputFilter
+from cherrypy import cpg
+import types
+
+class EncodingFilter(BaseOutputFilter):
+    """
+    Filter that automatically encodes the response.
+    """
+
+    def __init__(self, encoding = 'utf-8', mimeTypeList = ['text/html']):
+        self.encoding = encoding
+        self.mimeTypeList = mimeTypeList
+
+    def beforeResponse(self):
+        contentType = cpg.response.headerMap.get("Content-Type")
+        if contentType:
+            ctlist = contentType.split(';')[0]
+            if (ctlist in self.mimeTypeList):
+                # Add "charset=..." to response Content-Type header
+                if contentType and 'charset' not in contentType:
+                    cpg.response.headerMap["Content-Type"] += ";charset=%s" % self.encoding
+                # Return a generator that encodes the sequence
+                cpg.response.body = self.encode_body(cpg.response.body)
+
+    def encode_body(self, body):
+        for line in body:
+            yield line.encode(self.encoding)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/gzipfilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,85 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import zlib
+import struct
+import time
+from basefilter import BaseOutputFilter
+from cherrypy import cpg
+
+class GzipFilter(BaseOutputFilter):
+    """
+    Filter that gzips the response.
+    """
+
+    def __init__(self, mimeTypeList = ['text/html'], compresslevel=9):
+        # List of mime-types to compress
+        self.mimeTypeList = mimeTypeList
+        self.compresslevel = compresslevel
+
+    def beforeResponse(self):
+        if not cpg.response.body:
+            # Response body is empty (might be a 304 for instance)
+            return
+        ct = cpg.response.headerMap.get('Content-Type').split(';')[0]
+        ae = cpg.request.headerMap.get('Accept-Encoding', '')
+        if (ct in self.mimeTypeList) and ('gzip' in ae):
+            # Set header
+            cpg.response.headerMap['Content-Encoding'] = 'gzip'
+            # Return a generator that compresses the page
+            cpg.response.body = self.zip_body(cpg.response.body)
+
+    def write_gzip_header(self):
+        """
+        Adapted from the gzip.py standard module code
+        """
+        header = '\037\213'      # magic header
+        header += '\010'         # compression method
+        header += '\0'
+        header += struct.pack("<L", long(time.time()))
+        header += '\002'
+        header += '\377'
+        return header
+            
+    def write_gzip_trailer(self, crc, size):
+        footer = struct.pack("<l", crc)
+        footer += struct.pack("<L", size & 0xFFFFFFFFL)
+        return footer
+
+    def zip_body(self, body):
+        # Compress page
+        yield self.write_gzip_header()
+        crc = zlib.crc32("")
+        size = 0
+        zobj = zlib.compressobj(self.compresslevel, zlib.DEFLATED, -zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL, 0)
+        for line in body:
+            size += len(line)
+            crc = zlib.crc32(line, crc)
+            yield zobj.compress(line)
+        yield zobj.flush()
+        yield self.write_gzip_trailer(crc, size)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/logdebuginfofilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,81 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import time, StringIO, pickle
+from basefilter import BaseInputFilter, BaseOutputFilter
+from cherrypy import cpg
+from itertools import chain
+
+class LogDebugInfoStartFilter(BaseInputFilter, BaseOutputFilter):
+    """
+    Filter that adds debug information to the page
+    """
+
+    def __init__(self, mimeTypeList = ['text/html'], preTag = '<br /><br />',
+            logBuildTime = True, logPageSize = True,
+            logSessionSize = True, logAsComment = False):
+        # List of mime-types to which this applies
+        self.mimeTypeList = mimeTypeList
+        self.preTag = preTag
+        self.logBuildTime = logBuildTime
+        self.logPageSize = logPageSize
+        self.logSessionSize = logSessionSize
+        self.logAsComment = logAsComment
+
+    def afterRequestBody(self):
+        cpg.request.startBuilTime = time.time()
+
+    def beforeResponse(self):
+        ct = cpg.response.headerMap.get('Content-Type')
+        if (ct in self.mimeTypeList):
+            debuginfo = '\n'
+            if self.logAsComment:
+                debuginfo += '<!-- '
+            else:
+                debuginfo += self.preTag
+            logList = []
+            if self.logBuildTime:
+                logList.append("Build time: %.03fs" % (
+                    time.time() - cpg.request.startBuilTime))
+            if self.logPageSize:
+                logList.append("Page size: %.02fKB" % (
+                    len(cpg.response.body)/float(1024)))
+            if self.logSessionSize and cpg.configOption.sessionStorageType:
+                # Pickle session data to get its size
+                f = StringIO.StringIO()
+                pickle.dump(cpg.request.sessionMap, f, 1)
+                dumpStr = f.getvalue()
+                f.close()
+                logList.append("Session data size: %.02fKB" % (
+                    len(dumpStr)/float(1024)))
+
+            debuginfo += ', '.join(logList)
+            if self.logAsComment:
+                debuginfo += '-->'
+
+            cpg.response.body = chain(cpg.response.body, [debuginfo])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/tidyfilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,94 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import os, cgi
+from basefilter import BaseOutputFilter
+from cherrypy import cpg
+
+class TidyFilter(BaseOutputFilter):
+    """
+    Filter that runs the response through Tidy.
+    Note that we use the standalone Tidy tool rather than the python
+    mxTidy module. This is because this module doesn't seem to be
+    stable and it crashes on some HTML pages (which means that the
+    server would also crash)
+    """
+
+    def __init__(self, tidyPath, tmpDir, errorsToIgnore = []):
+        self.tidyPath = tidyPath
+        self.tmpDir = tmpDir
+        self.errorsToIgnore = errorsToIgnore
+
+    def beforeResponse(self):
+        # the tidy filter, by its very nature it's not generator friendly,
+        # so we just collect the body and work with it.
+        originalBody = ''.join(cpg.response.body)
+        cpg.response.body = [originalBody]
+
+        fct = cpg.response.headerMap.get('Content-Type', '')
+        ct = fct.split(';')[0]
+        if ct == 'text/html':
+            pageFile = os.path.join(self.tmpDir, 'page.html')
+            outFile = os.path.join(self.tmpDir, 'tidy.out')
+            errFile = os.path.join(self.tmpDir, 'tidy.err')
+            f = open(pageFile, 'wb')
+            f.write(originalBody)
+            f.close()
+            encoding = ''
+            i = fct.find('charset=')
+            if i != -1:
+                encoding = fct[i+8:]
+            encoding = encoding.replace('utf-8', 'utf8')
+            if encoding:
+                encoding = '-' + encoding
+            os.system('"%s" %s -f %s -o %s %s' % (
+                self.tidyPath, encoding, errFile, outFile, pageFile))
+            f = open(errFile, 'rb')
+            err = f.read()
+            f.close()
+
+            errList = err.splitlines()
+            newErrList = []
+            for err in errList:
+                if (err.find('Warning') != -1 or err.find('Error') != -1):
+                    ignore = 0
+                    for errIgn in self.errorsToIgnore:
+                        if err.find(errIgn) != -1:
+                            ignore = 1
+                            break
+                    if not ignore: newErrList.append(err)
+
+            if newErrList:
+                newBody = "Wrong HTML:<br />" + cgi.escape('\n'.join(newErrList)).replace('\n','<br />')
+                newBody += '<br /><br />'
+                i=0
+                for line in originalBody.splitlines():
+                    i += 1
+                    newBody += "%03d - "%i + cgi.escape(line).replace('\t','    ').replace(' ','&nbsp;') + '<br />'
+
+                cpg.response.body = [newBody]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/virtualhostfilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,57 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import basefilter
+from cherrypy import cpg, _cphttptools
+
+class VirtualHostFilter(basefilter.BaseInputFilter):
+    """
+    Filter that changes the ObjectPath based on the Host.
+    Useful when running multiple sites within one CP server.
+    See CherryPy recipes for the documentation.
+    """
+
+    def __init__(self, siteMap, useXForwardedHost = True):
+        self.siteMap = siteMap
+        self.useXForwardedHost = useXForwardedHost
+
+    def afterRequestHeader(self):
+        domain = cpg.request.base.split('//')[1]
+        if self.useXForwardedHost:
+            domain = cpg.request.headerMap.get( "X-Forwarded-Host", domain)
+        prefix = self.siteMap.get(domain)
+        if prefix:
+            # Re-use "mapPathToObject" function to find the actual
+            #   objectPath
+            candidate, objectPathList, virtualPathList = \
+                    _cphttptools.mapPathToObject(
+                        prefix + cpg.request.path
+                    )
+            cpg.request.objectPath = '/' + '/'.join(objectPathList[1:])
+            raise basefilter.InternalRedirect
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/filter/xmlrpcfilter.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,182 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+##########################################################################
+## Remco Boerma
+##
+## History:
+## 1.0.3   : 2005-01-28 Bugfix on content-length in 1.0.2 code fixed by
+##           Gian Paolo Ciceri
+## 1.0.2   : 2005-01-26 changed infile dox based on ticket #97
+## 1.0.1   : 2005-01-26 Speedup due to generator usage in CP2.
+##           The result is now converted to a list with length 1. So the complete
+##           xmlrpc result is written at once, and not per character. Thanks to
+##           Gian Paolo Ciceri for reporting the slowdown.
+## 1.0.0   : 2004-12-29 Released with CP2
+## 0.0.9   : 2004-12-23 made it CP2 #59 compatible (returns an iterable)
+##           Please note: as the xmlrpc doesn't know what you would want to return
+##           (and for the logic of marshalling) it will return Generator objects, as
+##           it is.. So it'll brake on that one!!
+##           NOTE: __don't try to return a Generator object to the caller__
+##           You could of course handle the generator usage internally, before sending
+##           the result. This breaks from the general cherrypy way of handling generators...
+## 0.0.8   : 2004-12-23 cpg.request.paramList should now be a filter. 
+## 0.0.7   : 2004-12-07 inserted in the experimental branch (all remco boerma till here)
+## 0.0.6   : 2004-12-02 Converted basefilter to baseinputfileter,baseoutputfilter
+## 0.0.5   : 2004-11-22 "RPC2/" now changed to "/RPC2/" with the new mapping function
+##           Gian paolo ciceri notified me with the lack of passing parameters.
+##           Thanks Gian, it's now implemented against the latest trunk.
+##           Gian also came up with the idea of lazy content-type checking: if it's sent
+##           as a header, it should be 'text/xml', if not sent at all, it should be
+##           accepted. (While this it not the xml/rpc standard, it's handy for those
+##           xml-rpc client implementations wich don't send this header)
+## 0.0.4   : 2004-11-20 in setting the path, the dot is replaces by a slash
+##           therefore the regular CP2 routines knows how to handle things, as 
+##           dots are not allowed in object names, it's varely easily adopted. 
+##           Path + method handling. The default path is 'RPC2', this one is 
+##           stripped. In case of path 'someurl' it is used for 'someurl' + method
+##           and 'someurl/someotherurl' is mapped to someurl.someotherurl + method.
+##           this way python serverproxies initialised with an url other than 
+##           just the host are handled well. I don't hope any other service would map
+##           it to 'RPC2/someurl/someotherurl', cause then it would break i think. .
+## 0.0.3   : 2004-11-19 changed some examples (includes error checking 
+##           wich returns marshalled Fault objects if the request is an RPC call.
+##           took testing code form afterRequestHeader and put it in 
+##           testValidityOfRequest to make things a little simpler. 
+##           simply log the requested function with parameters to stdout
+## 0.0.2   : 2004-11-19 the required cgi.py patch is no longer needed
+##           (thanks remi for noticing). Webbased calls to regular objects
+##           are now possible again ;) so it's no longer a dedicated xmlrpc
+##           server. The test script is also in a ready to run file named 
+##           testRPC.py along with the test server: filterExample.py
+## 0.0.1   : 2004-11-19 informing the public, dropping loads of useless
+##           tests and debugging
+## 0.0.0   : 2004-11-19 initial alpha
+## 
+##---------------------------------------------------------------------
+## 
+## EXAMPLE CODE FOR THE SERVER:
+##    from cherrypy.lib.filter.xmlrpcfilter import XmlRpcFilter
+##    from cherrypy import cpg
+##
+##    class Root:
+##        _cpFilterList = [XmlRpcFilter()]
+##        
+##        def longString(self,s,times):
+##            return s*times
+##        longString.exposed = True
+##
+##    cpg.root = Root()
+##    if __name__=='__main__':
+##        cpg.server.start(configMap = {'socketPort': 9001,
+##                                      'threadPool':0,
+##                                      'socketQueueSize':10 })
+## EXAMPLE CODE FOR THE CLIENT:
+## >>> import xmlrpclib
+## >>> server = xmlrpclib.ServerProxy('http://localhost:9001')
+## >>> assert server.longString('abc',3) == 'abcabcabc'
+## >>>
+######################################################################
+
+from basefilter import BaseInputFilter, BaseOutputFilter
+from cherrypy import cpg
+import xmlrpclib
+
+class XmlRpcFilter(BaseInputFilter,BaseOutputFilter):
+    """
+    Derivative of basefilter.
+    Test to convert XMLRPC to CherryPy2 object system and reverse
+
+    PLEASE NOTE:
+
+    afterRequestHeader:
+        Unmarshalls the posted data to a methodname and parameters.
+            - These are stored in cpg.request.rpcMethod and cpg.request.rpcParams
+            - The method is also stored in cpg.request.path, so CP2 will find the right
+              method to call for you. Based on the root's position
+    beforeResponse:
+        Marshalls the result of the excecuted function (in cpg.response.body) to xmlrpc.
+            - Until resolved: the result must be a python souce string with the results,
+              this string is 'eval'ed to return the results. This will be resolved in the
+              future.
+            - the Content-Type and -Length are set according to the new (marshalled) data. 
+              
+
+    """
+    def testValidityOfRequest(self):
+        # test if the content-length was sent
+        result = int(cpg.request.headerMap.get('Content-Length',0)) > 0
+        result = result and cpg.request.headerMap.get('Content-Type','text/xml').lower() in ['text/xml']
+        return result
+        
+    def afterRequestHeader(self):
+        """ Called after the request header has been read/parsed"""
+        cpg.request.isRPC = self.testValidityOfRequest()
+        if not cpg.request.isRPC: 
+            # used for debugging or more info
+            # print 'not a valid xmlrpc call'
+            return # break this if it's not for this filter!!
+        # used for debugging, or more info:
+        # print "xmlrpcmethod...",
+        cpg.request.parsePostData = 0
+        dataLength = int(cpg.request.headerMap.get('Content-Length',0))
+        data = cpg.request.rfile.read(dataLength)
+        try:
+            params, method = xmlrpclib.loads(data)
+        except Exception,e: 
+            params, method =  ('ERROR PARAMS',),'ERRORMETHOD'
+        cpg.request.rpcMethod, cpg.request.rpcParams = method,params
+        # patch the path. .there are only a few options:
+        # - 'RPC2' + method >> method
+        # - 'someurl' + method >> someurl.method
+        # - 'someurl/someother' + method >> someurl.someother.method
+        if not cpg.request.path.endswith('/'):
+            cpg.request.path+='/'
+        if cpg.request.path.startswith('/RPC2/'):
+            cpg.request.path=cpg.request.path[5:] ## strip the irst /rpc2
+        cpg.request.path+=str(method).replace('.','/')
+        cpg.request.paramList = list(params)
+        # used for debugging and more info
+        # print "XMLRPC Filter: calling '%s' with args: '%s' " % (cpg.request.path,params)
+
+    def beforeResponse(self):
+        """ Called before starting to write response """
+        if not cpg.request.isRPC: 
+            return # it's not an RPC call, so just let it go with the normal flow
+        try:
+            cpg.response.body = [xmlrpclib.dumps((cpg.response.body[0],), methodresponse=1,allow_none=1)]
+        except xmlrpclib.Fault,fault:
+            cpg.response.body = xmlrpclib.dumps(fault,allow_none=1)
+        except Exception,e:
+            print 'EXCEPTION: ',e
+        cpg.response.headerMap['Content-Type']='text/xml'
+        try:
+            cpg.response.headerMap['Content-Length']=`len(cpg.response.body[0])`
+        except TypeError:
+            # 1.0.3 : in case of an error, cpg.response.body is unscriptable
+            pass 
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/form.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,124 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+Simple form handling module.
+"""
+
+from cherrypy import cpg
+import defaultformmask
+
+class FormField:
+    def __init__(self, label, name, typ, mask=None, mandatory=0, size=15, optionList=[], defaultValue='', defaultMessage='', validate=None):
+        self.isField=1
+        self.label=label
+        self.name=name
+        self.typ=typ
+        if not mask: self.mask=defaultformmask.defaultMask
+        else: self.mask=mask
+        self.mandatory=mandatory
+        self.size=size
+        self.optionList=optionList
+        self.defaultValue=defaultValue
+        self.defaultMessage=defaultMessage
+        self.validate=validate
+        self.errorMessage=""
+    def render(self, leaveValues):
+        if leaveValues:
+            if self.typ!='submit':
+                if cpg.request.paramMap.has_key(self.name): self.currentValue=cpg.request.paramMap[self.name]
+                else: self.currentValue=""
+            else: self.currentValue=self.defaultValue
+        else:
+            self.currentValue=self.defaultValue
+            self.errorMessage=self.defaultMessage
+        return self.mask(self)
+
+class FormSeparator:
+    def __init__(self, label, mask):
+        self.isField=0
+        self.label=label
+        self.mask=mask
+    def render(self, dummy):
+        return self.mask(self.label)
+
+class Form:
+    method="post"
+    enctype=""
+    def formView(self, leaveValues=0):
+        if self.enctype: enctypeTag='enctype="%s"'%self.enctype
+        else: enctypeTag=""
+        res='<form method="%s" %s action="postForm">'%(self.method, enctypeTag)
+        for field in self.fieldList:
+            res+=field.render(leaveValues)
+        return res+"</form>"
+    def validateFields(self):
+        # Should be subclassed
+        # Update field's errorMessage value to set an error
+        pass
+    def validateForm(self):
+        # Reset errorMesage for each field
+        for field in self.fieldList:
+            if field.isField: field.errorMessage=""
+
+        # Validate mandatory fields
+        for field in self.fieldList:
+            if field.isField and field.mandatory and (not cpg.request.paramMap.has_key(field.name) or not cpg.request.paramMap[field.name]): field.errorMessage="Missing"
+
+        # Validate fields one by one
+        for field in self.fieldList:
+            if field.isField and field.validate and not field.errorMessage:
+                if cpg.request.paramMap.has_key(field.name): value=cpg.request.paramMap[field.name]
+                else: value=""
+                field.errorMessage=field.validate(value)
+
+        # Validate all fields together (ie: check that passwords match)
+        self.validateFields()
+        for field in self.fieldList:
+            if field.isField and field.errorMessage: return 0
+        return 1
+    def setFieldErrorMessage(self, fieldName, errorMessage):
+        for field in self.fieldList:
+            if field.isField and field.name==fieldName: field.errorMessage=errorMessage
+    def getFieldOptionList(self, fieldName):
+        for field in self.fieldList:
+            if field.isField and field.name==fieldName: return field.optionList
+    def getFieldDefaultValue(self, fieldName):
+        for field in self.fieldList:
+            if field.isField and field.name==fieldName: return field.defaultValue
+    def setFieldDefaultValue(self, fieldName, defaultValue):
+        for field in self.fieldList:
+            if field.isField and field.name==fieldName: field.defaultValue=defaultValue
+
+    def getFieldNameList(self, exceptList=[]):
+        fieldNameList=[]
+        for field in self.fieldList:
+            if field.isField and field.name and field.name not in exceptList: fieldNameList.append(field.name)
+        return fieldNameList
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/htmltools.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,51 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+Just a few convenient functions
+"""
+
+from cherrypy import cpg
+
+def redirect(newUrl):
+    """ Sends a redirect to the browser """
+    
+    if not newUrl.startswith('http://') and not newUrl.startswith('https://'):
+        # If newUrl is not canonical, we must make it canonical
+        if newUrl.startswith('/'):
+            # newUrl was absolute:
+            # we just add request.base in front of it
+            newUrl = cpg.request.base + newUrl
+        else:
+            # newUrl was relative:
+            # we remove the last bit from browserUrl and add newUrl to it
+            i = cpg.request.browserUrl.rfind('/')
+            newUrl = cpg.request.browserUrl[:i+1] + newUrl
+    cpg.response.headerMap['Status'] = 302
+    cpg.response.headerMap['Location'] = newUrl
+    return ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/lib/httptools.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,44 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+Just a few convenient functions
+"""
+
+from cherrypy import cpg
+import urlparse
+
+def canonicalizeUrl(url):
+    """ Canonicalize a URL. The URL might be relative, absolute or canonical """
+    return urlparse.urljoin(cpg.request.base, url)
+
+def redirect(url):
+    """ Sends a redirect to the browser (after canonicalizing the URL) """
+    cpg.response.headerMap['Status'] = 302
+    cpg.response.headerMap['Location'] = canonicalizeUrl(url)
+    return ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/wsgiapp.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,173 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+"""
+WSGI interface for CherryPy
+"""
+
+import StringIO, Cookie, time
+from cherrypy import cpg, _cphttptools, _cpserver
+
+def init(*a, **kw):
+    kw['initOnly'] = 1
+    _cpserver.start(*a, **kw)
+
+def wsgiApp(environ, start_response):
+    cpg.request.method = environ['REQUEST_METHOD']
+    # Rebuild first line of the request
+    pathInfo = environ['PATH_INFO']
+    qString = environ.get('QUERY_STRING')
+    if qString:
+        pathInfo += '?' + qString
+    firstLine = '%s %s %s' % (
+        environ['REQUEST_METHOD'],
+        pathInfo or '/',
+        environ['SERVER_PROTOCOL']
+    )
+    _cphttptools.parseFirstLine(firstLine)
+
+    # Initialize variables
+    now = time.time()
+    year, month, day, hh, mm, ss, wd, y, z = time.gmtime(now)
+    date = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (_cphttptools.weekdayname[wd], day, _cphttptools.monthname[month], year, hh, mm, ss)
+    cpg.request.headerMap = {}
+    cpg.request.simpleCookie = Cookie.SimpleCookie()
+    cpg.response.simpleCookie = Cookie.SimpleCookie()
+
+    # Rebuild headerMap
+    for cgiName, headerName in [
+        ('HTTP_HOST', 'Host'),
+        ('HTTP_USER_AGENT', 'User-Agent'),
+        ('HTTP_CGI_AUTHORIZATION', 'Authorization'),
+        ('CONTENT_LENGTH', 'Content-Length'),
+        ('CONTENT_TYPE', 'Content-Type'),
+        ('HTTP_COOKIE', 'Cookie'),
+        ('REMOTE_HOST', 'Remote-Host'),
+        ('REMOTE_ADDR', 'Remote-Addr'),
+        ('HTTP_REFERER', 'Referer'),
+        ('HTTP_ACCEPT_ENCODING', 'Accept-Encoding'),
+    ]:
+        if cgiName in environ:
+            _cphttptools.insertIntoHeaderMap(headerName, environ[cgiName])
+
+    #  TODO: handle POST
+
+    # set up stuff similar to initRequest
+    cpg.response.headerMap = {
+        "protocolVersion": cpg.configOption.protocolVersion,
+        "Status": "200 OK",
+        "Content-Type": "text/html",
+        "Server": "CherryPy/" + cpg.__version__,
+        "Date": date,
+        "Set-Cookie": [],
+        "Content-Length": 0
+    }
+    cpg.request.base = "http://" + cpg.request.headerMap['Host']
+    cpg.request.browserUrl = cpg.request.base + cpg.request.browserUrl
+    cpg.request.isStatic = False
+    cpg.request.parsePostData = True
+    cpg.request.rfile = environ["wsgi.input"]
+    cpg.request.objectPath = None
+    if 'Cookie' in cpg.request.headerMap:
+        cpg.request.simpleCookie.load(cpg.request.headerMap['Cookie'])
+
+    cpg.response.simpleCookie = Cookie.SimpleCookie()
+    cpg.response.sendResponse = 1
+
+    if cpg.request.method == 'POST' and cpg.request.parsePostData:
+        _cphttptools.parsePostData(cpg.request.rfile)
+
+    # Execute request
+    wfile = StringIO.StringIO()
+    cpg.response.wfile = wfile
+    _cphttptools.handleRequest(wfile)
+    response = wfile.getvalue()
+
+
+    # Extract header from response
+    headerLines = []
+    i = 0
+    while 1:
+        j = response.find('\n', i)
+        line = response[i:j]
+        if line[-1] == '\r':
+            line = line[:-1]
+        headerLines.append(line)
+        i = j+1
+        if not line:
+            break
+    response = response[i:]
+
+    status = headerLines[0]
+    # Remove "HTTP/1.0" at the beginning of status
+    i = status.find(' ')
+    status = status[i+1:]
+
+    responseHeaders = []
+    for line in headerLines[1:]:
+        i = line.find(':')
+        header = line[:i]
+        value = line[i+1:].lstrip()
+        responseHeaders.append((header,value))
+
+    start_response(status, responseHeaders)
+
+    return response
+
+if __name__ == '__main__':
+    from cherrypy import cpg, wsgiapp
+    class Root:
+        def index(self, name = "world"):
+            count = cpg.request.sessionMap.get('count', 0) + 1
+            cpg.request.sessionMap['count'] = count
+            return """
+                <html><body>
+                Hello, %s, count is %s:
+                <form action="/post" method="post">
+                    Post some data: <input name=myData type=text"> <input type=submit>
+                </form>
+            """ % (name, count)
+        index.exposed = True
+        def post(self, myData):
+            return "myData: " + myData
+        post.exposed = True
+    cpg.root = Root()
+
+    import sys
+    # This uses the WSGI HTTP server from PEAK.wsgiref
+    # sys.path.append(r"C:\Tmp\PEAK\src")
+    from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
+     # Read the CherryPy config file and initialize some variables
+    wsgiapp.init(configMap = {'socketPort': 8000, 'sessionStorageType': 'ram'})
+    server_address = ("", 8000)
+    httpd = WSGIServer(server_address, WSGIRequestHandler)
+    httpd.set_app(wsgiapp.wsgiApp)
+    sa = httpd.socket.getsockname()
+    #print "Serving HTTP on", sa[0], "port", sa[1], "..."
+    httpd.serve_forever()
+
Binary file plugins/heya.wav has changed
Binary file plugins/images/1up.gif has changed
Binary file plugins/images/chocobo.gif has changed
Binary file plugins/images/darkside.gif has changed
Binary file plugins/images/fairy.gif has changed
Binary file plugins/images/flyingspaghetti.gif has changed
Binary file plugins/images/gnome.gif has changed
Binary file plugins/images/hood.gif has changed
Binary file plugins/images/icon_arrow.gif has changed
Binary file plugins/images/icon_biggrin.gif has changed
Binary file plugins/images/icon_confused.gif has changed
Binary file plugins/images/icon_cool.gif has changed
Binary file plugins/images/icon_cry.gif has changed
Binary file plugins/images/icon_e_biggrin.gif has changed
Binary file plugins/images/icon_e_confused.gif has changed
Binary file plugins/images/icon_e_geek.gif has changed
Binary file plugins/images/icon_e_sad.gif has changed
Binary file plugins/images/icon_e_surprised.gif has changed
Binary file plugins/images/icon_e_ugeek.gif has changed
Binary file plugins/images/icon_e_wink.gif has changed
Binary file plugins/images/icon_eek.gif has changed
Binary file plugins/images/icon_evil.gif has changed
Binary file plugins/images/icon_exclaim.gif has changed
Binary file plugins/images/icon_frown.gif has changed
Binary file plugins/images/icon_idea.gif has changed
Binary file plugins/images/icon_lol.gif has changed
Binary file plugins/images/icon_mad.gif has changed
Binary file plugins/images/icon_mrgreen.gif has changed
Binary file plugins/images/icon_neutral.gif has changed
Binary file plugins/images/icon_question.gif has changed
Binary file plugins/images/icon_razz.gif has changed
Binary file plugins/images/icon_redface.gif has changed
Binary file plugins/images/icon_rolleyes.gif has changed
Binary file plugins/images/icon_sad.gif has changed
Binary file plugins/images/icon_smile.gif has changed
Binary file plugins/images/icon_smile2.gif has changed
Binary file plugins/images/icon_surprised.gif has changed
Binary file plugins/images/icon_twisted.gif has changed
Binary file plugins/images/icon_wink.gif has changed
Binary file plugins/images/link.gif has changed
Binary file plugins/images/medusa.gif has changed
Binary file plugins/images/mimic.gif has changed
Binary file plugins/images/mummy.gif has changed
Binary file plugins/images/ogre.gif has changed
Binary file plugins/images/ros.gif has changed
Binary file plugins/images/rupee.gif has changed
Binary file plugins/images/samurai.gif has changed
Binary file plugins/images/skeleton.gif has changed
Binary file plugins/images/skull.gif has changed
Binary file plugins/images/smiley0.gif has changed
Binary file plugins/images/smiley1.gif has changed
Binary file plugins/images/smiley12.gif has changed
Binary file plugins/images/smiley13.gif has changed
Binary file plugins/images/smiley14.gif has changed
Binary file plugins/images/smiley2.gif has changed
Binary file plugins/images/smiley3.gif has changed
Binary file plugins/images/smiley4.gif has changed
Binary file plugins/images/smiley5.gif has changed
Binary file plugins/images/smiley6.gif has changed
Binary file plugins/images/smiley7.gif has changed
Binary file plugins/images/smiley9.gif has changed
Binary file plugins/images/zombie.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/inittool.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,123 @@
+
+<nodehandler class="group_handler" module="containers" name="Initiative Tool Goodies" version="1.0">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="macro_handler" icon="oriental" module="chatmacro" name="Start/Clear tool" version="1.0">
+    <text>/init start</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Sort: High to Low" version="1.0">
+    <text>/init sorthigh</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Sort: Low to High" version="1.0">
+    <text>/init sortlow</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="help" module="chatmacro" name="List Initiatives" version="1.0">
+    <text>/init list</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="Initiative Tool Instructions" version="1.0">
+    <text multiline="1" send_button="0">Welcome to the new Initiative tool.
+
+Some might be a little disheartened to learn that the new initiative tool no longer has the nifty GUI that was seen briefly with version 0.9.8.  While we don't have that this initiative tools is still holds it's old functionality, as well as becoming more powerful and able to handle a more broad range of dicerolls.  So, without further adeu, let us get cracking.
+
+
+In this instruction set, you should have a set of nodes labeled &quot;Start/Clear tool&quot;, &quot;Sort: High to low&quot;, &quot;Sort: Low to High&quot;, and &quot;List Initiatives&quot;.  just by doubleclicking on each of these (and assuming you have imported the initiative tool plugin) the init tool command (as talked about below) are automatically run.  If you haven't, you'll get the &quot;** Sorry, don't understand what a / is...&quot; msg.
+
+Here's the basic method for using the tool:
+
+First, choose the type of roller to use depending on your gamesystem.  either doubleclick on the types in the gametree, or type in &quot;/type ___&quot;.  replacing &quot;___&quot; with std (standard), 3e (for d20 type games), and wod (for World of Darkness games).  You can also just type in &quot;/type&quot; to find out which type you are currently using.  &quot;/type std&quot; is the default
+
+the DM then starts the initiative tool with the button in the tree or the command &quot;/start&quot; or &quot;/clear&quot;.  all the players then type in a description of what they are doing (and it helps for them to put their name in it) along with their initiative dice and the word &quot;init&quot;
+
+example:
+Running across the roof, Woody dodges the dragon [1d10+6] init
+
+The reason for this is that the new tool will not record your name, only your msg.  it will also ignore anything after the diceroll so it is important to have the description first.  You can also use initiative nodes in almost any character sheet (as long as it has the word &quot;init&quot; or &quot;initiative&quot; in it). In addition to it all, the players can whisper their action to the DM instead.
+
+once all the inits have been inserted, the DM can then sort the list with these commands:
+low to high:
+/sort
+/sortlow
+/low
+
+high to low:
+/reverse
+/sorthigh
+/high
+
+to see the current initiative list at any time, you can type in &quot;/list&quot; or press the appropriate node in the gametree.
+
+now that the DM has the list sorted, he can then send the name on the top of the list to the chat with the command /run or /go.
+
+as soon as the all the players on the list have been sorted through it will display the msg &quot;End of Initiative Round&quot; when prompted again with /run.
+
+That's it.  Feel free to play around with it until you have a feel for it.  It will work BEST if you DISABLE IDs in chat.  Also, it will take a look at any message that it sees the four letters &quot;init&quot; in though it will only grab those that have both that word and a set of round brackets () in it.  In addition to those commands listed above, there are some additional extras that let the DM alter and manipulate the list as well.  they are:
+
+
+/del list_number
+-this allows you to delete whatever number list_number is.
+
+/change list_number new_init_value
+-this will change whatever number list_number's inititive value is to new_init_value
+
+/add new_init_value description
+-this will add a new specific initiative to the list with an inititive roll of new_init_value
+
+
+
+Here is the complete list of commands
+
+/init list
+
+/init start  /init clear
+
+description [diceroll] init
+
+/init sortlow
+
+/init sorthigh
+
+/init run /init go
+
+/init del
+
+/init change
+
+/init add
+
+/init ----this command is special, and will toggle on or off whether the init tool is looking for initiative rolls.  while not necessary to disable during normal play, it is suggested
+Note that you can use almost any node, but the List Box Checkbox node version doesn't work correctly and will only do the first selected item on the list.  other List Box versions do work though</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="grid" module="forms" name="-------------------------------------------------------" version="1.0">
+    <text multiline="0" send_button="0">just a standard divider</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="flask" module="chatmacro" name="Set Diceroller type: Standard" version="1.0">
+    <text>/init type std
+
+/# This is the standard basic rolling fashion.  It will erase each entry after they have been displayed and the players will roll initiative at the beginning of each new round.</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="flask" module="chatmacro" name="Set Diceroller type: World of Darkness" version="1.0">
+    <text>/type wod
+
+/# This diceroller type stores the initiatives and displays them in the traditional World of Darkness fashion.  That being that you will first sort them from low to high and display them in that order to let everyone declare what they are doing.  once everyone has done so, the init tool will reset itself and it will then go through the initiatives from high to low before ending the round.</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="flask" module="chatmacro" name="Set Diceroller type: 3e (and single init rolls only)" version="1.0">
+    <text>/type 3e
+
+This sets the tool to save the same list over and over again so that players and the DM will only roll once with their initiatives for combat.  At the end of each round the initiative tool will reset it's list to the original version.  This can be overrided and everyone can reroll if the DM just types &quot;/init clear&quot; (which clears the list)</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="flask" module="chatmacro" name="Set Diceroller type: ShadowRun" version="1.0">
+    <text>/type srun</text>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="grid" module="forms" name="-------------------------------------------------------" version="1.0">
+    <text multiline="0" send_button="0">just a standard divider</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="gear" module="chatmacro" name="Initiative macro node" version="1.0">
+    <text>Ted jumps up and down [1d10+6] init</text>
+  </nodehandler>
+  <nodehandler class="listbox_handler" icon="gear" module="forms" name="NPC" version="1.0">
+    <list send_button="1" type="1">
+      <option selected="0" value="">The Dragon swoops down on the peasants [1d10-6] init</option>
+      <option selected="0" value="">The peasant captain flees in terror [1d10+4] init</option>
+      <option selected="1" value="">The dragonhunter fires an arrow [1d10+3] init</option>
+    </list>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/inittool2.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,496 @@
+
+<nodehandler class="group_handler" icon="labtop" module="containers" name="GM's Initiative Tool" status="useful" version="1.0">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="macro_handler" icon="oriental" module="chatmacro" name="Start/Clear" version="1.0">
+    <text>/start</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="oriental" module="chatmacro" name="Clear" version="1.0">
+    <text>/clear</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Next Turn" version="1.0">
+    <text>/go</text>
+  </nodehandler>
+  <nodehandler class="tabber_handler" icon="questionhead" module="containers" name="Commands &amp; Help" version="1.0">
+    <nodehandler class="form_handler" icon="compass" module="forms" name="D20/RQ Cmds" version="1.0">
+      <form height="350" width="450"/>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="ADD INIT               " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/add 22 Aiur</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="ADD INIT (CHAT)  " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">Veggie-sama [1d20+1] init</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="ADD HIDDEN INIT" version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/addhidden 22 Zyhink</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="CHANGE INIT        " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/change 3 6</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="RANK UP               " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/rankup 2 1</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="RANK DOWN        " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/rankdown 1 1</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="DELETE                 " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/del 2</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="ADD EFFECT               " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/addfx 18 7 Hignar's Flaming Sphere</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="ADD EFFECT (CHAT)  " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">Tlasco's Bless [1] effect 6</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="ADD HIDDEN EFFECT" version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/addfxhidden 10 4 Vinzarino's Hold Person</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="CHANGE DURATION  " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/changedur 5 3</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="DELAY ID NOW" version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/initdelaynow 2</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="form_handler" icon="compass" module="forms" name="SR4 Cmds" version="1.0">
+      <form height="350" width="450"/>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="ADD INIT             " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/add 8 2 Sam</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="ADD INIT (CHAT)" version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">Alex [8d6.init(8)] init 3</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="CHANGE INIT      " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/change 2 6</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="CHANGE PASS    " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/changepass 2 4</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="DELETE               " version="1.0">
+        <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/del 1</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="tabber_handler" icon="compass" module="containers" name="Extra Cmds" version="1.0">
+      <nodehandler class="form_handler" icon="compass" module="forms" name="Other Commands" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Toggle Init System (D20, SR4 or RQ)" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/inittoggle</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Toggle Init Recording (chat cmds)     " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/init</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="List Initiatives to Self   " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/list</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="List Initiatives to Room" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/publiclist</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Show Round# to Room" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/rnd</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Show Pass# to Room   " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/pass</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Change Round#" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/rnd 2</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Change Pass#   " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/pass 2</text>
+        </nodehandler>
+        <form height="350" width="450"/>
+      </nodehandler>
+      <nodehandler class="form_handler" icon="compass" module="forms" name="Sandglass" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Start Sandglass (sec)" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/sandglass 1</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Cancel Sandglass     " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/sandglass 0</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Pause Sandglass   " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/sandglasspause</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Resume Sandglass" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/sandglassresume</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Force Skip Interval" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/sandglassforceskip 3</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Cancel Force Skip " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/sandglassforceskip 0</text>
+        </nodehandler>
+        <form height="350" width="450"/>
+      </nodehandler>
+      <nodehandler class="form_handler" icon="compass" module="forms" name="Save/Load Tool" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Save List (Slot 1-5) " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/initsave 1 My Battle</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Load List (Slot 1-5) " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/initload 1</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Turn on Autosave (5 mins)" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/initautosave 300</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Cancel Autosave                " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/initautosave 0</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Load Last Autosave List     " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/initautoload</text>
+        </nodehandler>
+        <form height="350" width="450"/>
+      </nodehandler>
+      <nodehandler class="form_handler" icon="compass" module="forms" name="System Config" version="1.0">
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Toggle Message Handler Warning" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/msghandwarning</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Toggle Hide IDs                             " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/togglehideid</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Toggle Hide 'On Deck'                   " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/togglehideondeck</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="[SR4-only] Toggle Hide 'Just Movement'" version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/togglehidejustmovement</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="[SR4-only] Init Reroll?                            " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/toggleroundreroll</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="[SR4-only] Toggle IP Order                    " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/toggleiporder</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Load Default Config  " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/initdefaultconfig</text>
+        </nodehandler>
+        <nodehandler class="textctrl_handler" icon="note" module="forms" name="Show Config             " version="1.0">
+          <text hide_title="0" multiline="0" raw_mode="1" send_button="1">/initshowconfig</text>
+        </nodehandler>
+        <form height="350" width="450"/>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="tabber_handler" icon="help" module="containers" name="HELP!" version="1.0">
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="D20 System" version="1.0">
+        <text hide_title="0" multiline="1" raw_mode="0" send_button="0">====================================================
+STARTING A BATTLE
+- The DM double-clicks &quot;Start/Clear&quot; (/start) in the gametree under the Initiative Tool node.
+- In the chat window, the players and the DM must type their name, then their initiative dice, ending with the word &quot;init&quot;.
+
+Example of Adding an Init:
+Veggie-sama [1d20+4] init
+
+TIP: If a player needs to re-add an initiative roll (because they accidentally forgot the word &quot;init&quot; or otherwise), they can use [Xd1] and substitute their original init roll into &quot;X&quot;.
+
+====================================================
+ADVANCING THE ROUND
+- After everyone is finished rolling, double-click &quot;Next Turn&quot; (/go) to start the round.
+- When the named character is finished with his/her turn, double-click &quot;Next Turn&quot; again to advance the initiative.
+- TIP: Check Quick Commands tab in this help for a shortcut (&quot;nextinit&quot;)
+- When all characters have taken their actions, the round will end and the next round will begin.
+
+====================================================
+VIEWING THE INITIATIVE LIST
+- To see a list of current initiatives, click the plus sign by &quot;Other&quot; in the game tree, then double-click &quot;List Initiatives to Self&quot; (/list) or &quot;List Initiatives to Room&quot; (/publiclist), depending on whether you want to spam yourself or everyone else, respectively.
+- TIP: Check Quick Commands in this Help for a shortcut (&quot;showinits&quot;)
+
+====================================================
+MANIPULATING THE INITs MANUALLY (DM-Only)
+TIP: The &quot;list#&quot; is the number to the left of the init number when you press &quot;List Initiatives to Self&quot;, and the first number in red text on each character/effect notification (note: the number in the parentheses in the initiative score value; the list# is to the left of that)
+- If you don't want any future inits to be recorded, double click &quot;Toggle Init Recording&quot;. Note this will also disable Quick Commands.
+
+/add (init#) (description)
+- adds a new initiative to the list with an initiative roll of (init#)
+
+/addhidden (init#) (description)
+- same as /add, except public will be unware of this character until it will first be asked play
+
+/del (list#)
+- deletes the character with a list number of (list#)
+
+/change (list#) (init#)
+- changes the initiative score of the character whose list number is (list#) to the new initiative value of (init#)
+
+/rankup (list#) (step#)
+- move up in the initiative list by the number of step that is (step#). You can only move within the same inititive score
+
+/rankdown (list#) (step#)
+- move down in the initiative list by the number of step that is (step#). You can only move within the same inititive score
+
+/initdelaynow (list#)
+- change the initiative score of the character whose list number is (list#) at the current initiative count and call the character to play NOW, before whichevercharacter was call to play previously.
+
+/initsetslot (list#)
+- set the given list# as the current selected slot. WARNING: You shouldn't use this function unless you really know what you are doing because any durations of the effects will be ignored.
+
+====================================================
+ROUNDS
+- The initiative tools keeps track of the number of rounds passed.
+Note: Any change in rounds will *not* affect Effect durations.
+
+/rnd
+- when used alone, will tell the room what round it is
+
+/rnd (round#)
+- when used with a number afterwards, will change the current round to the value of (round#)
+
+====================================================
+EFFECTS
+- Effects are non-characters that use initiative counts, usually magic spells or extraordinary abilities, which generally dissipate at the end of a certain duration. In D&amp;D, examples can include a wizard's Fire Wall spell, a barbarian's Rage, or the duration that enemy undead flee from a cleric's Turn Undead.
+- Effects are counted down between initiatives (after the caster's count, before the next count). They will persist until the specified duration, then automatically delete themselves. To add an effect, type the effect's name, followed by the init count it will occur on(within brackets), followed by the word &quot;effect&quot;, and lastly followed by its duration in rounds.
+- NOTE: Effects are denoted by a star (*) and in italics when you press &quot;List Initiatives to Self&quot;.
+- TIP: All values must be in rounds. To convert minutes into rounds, multiply the minutes by 10. To convert hours into rounds, multiply the hours by 600. To convert days into rounds... wait, your battles better not go on that long.
+
+Example:
+Hrothgar (count 12) casts Flaming Sphere, which will last for 7 rounds. He will type:
+Hrothgar's Flaming Sphere [12] effect 7
+
+The DM can manually add, delete, and change the Effects, because they are stored just like characters on the initiative list, like so:
+
+/addfx (init#) (duration#) (description)
+- adds a new effect to the list with an initiative roll of init#, and a duration (in rounds) of duration#
+- [effect-only]
+
+/addfxhidden (init#) (duration#) (description)
+- same as /addfx, except duration of the spell will not be print to public
+
+/del (list#)
+- same as /del from above
+
+/change (list#) (init#)
+- same as /change from above (also preserves the duration of the effect)
+
+/changedur (list#) (duration#)
+- changes the duration of the effect with a list number of (list#) to (duration#) in rounds
+- NOTE: You cannot change the duration to 0 or a negative number, so use /del instead
+- [effect-only]
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Shadowrun 4e" version="1.0">
+        <text hide_title="0" multiline="1" raw_mode="0" send_button="0">To switch to the Shadowrun Init Tool (or back to D20 or RuneQuest), the DM must double-click &quot;Toggle Init System&quot; (/inittoggle) in the &quot;Other&quot; node.
+
+The SR4 commands are otherwise identical to the D20 commands, with the notable exceptions mentioned below:
+
+====================================================
+STARTING A BATTLE
+- The DM must double-click &quot;Start/Clear&quot; in the gametree under the Initiative Tool node.
+- In the chat window, the players and the DM must type their name, then their initiative dice, then the word &quot;init&quot;, finally ending with the number of passes their character receives.
+- NOTE: The initiative tool for SR4 requires the SR4 dieroller to be installed and activated for all players as well, otherwise the system won't work correctly.
+
+Examples of Adding Inits:
+Veggie-sama has an Initiative attribute of 8 with 3 passes per round, so he types:
+Veggie-sama [8d6.init(8)] init 3
+
+If Veggie-sama added Edge dice for the round (with an Edge attribute of 5), he could type either:
+Veggie-sama [13d6.edgeinit(8)] init 3
+or
+Veggie-sama [13d6.initedge(8)] init 3
+NOTE: If you want to use Edge to automatically go first in the pass, then have the DM manually use an /add command (detailed below).
+
+TIP: If a player needs to re-add an initiative roll (because they accidentally forgot the word &quot;init&quot; or otherwise), the DM must manually use an /add command (detailed below).
+- Alternatively, the player could try Xd10000, as a hit will be any value 5 or higher, so there'd be a very low chance of rolling a four or lower on a ten thousand-sided die. *shrugs*
+
+====================================================
+ADVANCING THE ROUND
+- identical to D20, except with the addition of Initiative Passes and the fact that the inits are cleared at the end of every round to be rerolled for the next round
+
+====================================================
+VIEWING THE INITIATIVE LIST
+- identical to D20 (but it does show a super froody table)
+
+====================================================MANIPULATING THE INITs MANUALLY (DM-Only)
+- identical to D20
+
+====================================================RROUNDS &amp; PASSES
+- identical to D20, except for the two additional commands:
+
+/pass
+- when used alone, will tell the room what pass it is
+
+/pass (pass#)
+- when used with a number afterwards, will change the current pass to the value of (pass#)
+- NOTE: Only the numbers 1, 2, 3, or 4 are valid.
+
+====================================================
+EFFECTS
+- Effects are disabled in the Shadowrun init tool.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="RuneQuest" version="1.0">
+        <text hide_title="0" multiline="1" raw_mode="0" send_button="0">To switch to the RuneQuest Init Tool (or back to SR4 or D20), the DM must double-click &quot;Toggle Init System&quot; (/inittoggle) in the &quot;Other&quot; node. RuneQuest initiatives lists are very similar to D20 with the following change:
+
+- The list is broken into 10 Strike Ranks (SR)
+- The character with lowest total SR goes first
+
+Most commands used with D20 are usable with RQ, check out the 'D20 System Inits' help node for more informations.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Quick Commands" version="1.0">
+        <text hide_title="0" multiline="1" raw_mode="0" send_button="0">- There are certain words you (and other players in the room) can type to utilize the initiative tool to hasten combat. For instance, if a character is done with his/her turn, s/he may type &quot;nextinit&quot; to end their turn and start the next turn.
+- These can be turned off with &quot;Toggle Init Chat Commands&quot; (/init):
+
+&quot;nextinit&quot;
+- This will perform a &quot;Next Turn&quot; command (/go)
+
+&quot;showinits&quot;
+- This will perform a &quot;Show Inits to Room&quot; command (/publiclist)</text>
+        <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Toggle Init Chat Commands" version="1.0">
+          <text>/init</text>
+        </nodehandler>
+        <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Toggle Init System" version="1.0">
+          <text>/inittoggle</text>
+        </nodehandler>
+        <nodehandler class="macro_handler" icon="ccmap" module="chatmacro" name="List Initiatives to Self" version="1.0">
+          <text>/list</text>
+        </nodehandler>
+        <nodehandler class="macro_handler" icon="ccmap" module="chatmacro" name="List Initiatives to Room" version="1.0">
+          <text>/publiclist</text>
+        </nodehandler>
+        <nodehandler class="macro_handler" icon="questionhead" module="chatmacro" name="Display Round to Room" version="1.0">
+          <text>/rnd</text>
+        </nodehandler>
+        <nodehandler class="macro_handler" icon="questionhead" module="chatmacro" name="Display Pass# to Room (SR4)" version="1.0">
+          <text>/pass</text>
+        </nodehandler>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Sandglass" version="1.0">
+        <text hide_title="0" multiline="1" raw_mode="0" send_button="0">====================================================
+SANDGLASS
+- The sandglass is started at the beginning of each character turn. After the delay, a reminder is post to the chat.
+- The sandglass option is initially turned off. The DM can activate it by setting the delay to a value greater than zero.
+
+/sandglass 0
+- cancel the sandglass globally
+
+/sandglass 90
+- set the sandglass delay to 90 seconds. Reminder will be posted every 90 seconds of each character's turn.
+
+/sandglasspause
+- pause the sandglass
+
+/sandglassresume
+- resume the sandglass
+
+/sandglassforceskip 3
+- after 3 intervals, the current character will be loose his turn
+
+/sandglassforceskip 0
+- cancel the force skip feature
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="Saving/Loading" version="1.0">
+        <text hide_title="0" multiline="1" raw_mode="0" send_button="0">====================================================
+SAVING AND LOADING INITIATIVE LISTS
+
+The DM can save and load initiative lists. There is 5 saving slots and 1 autosaving slot.
+
+/initsave (slot_#) (optional description)
+- will save the current initiative list to the given slot. Default is to save on slot #1.
+
+/initload (slot_#)
+- will load the initiative list previously saved on the given slot. Default is to load slot #1.
+
+/initautosave 300
+- will auto save the current initiative list every 300 seconds on the special autosaving slot.
+
+/initautosave 0
+- cancel the autosaving
+
+/initautoload
+- load the last autosaved slot.
+</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="System Config" version="1.0">
+        <text hide_title="0" multiline="1" raw_mode="0" send_button="0">====================================================
+SETTING SYSTEM CONFIG
+
+/initdefaultconfig
+
+- reset to the default settings
+
+/initshowconfig
+
+- show system config
+
+/inittoggle
+
+- set the initiative system : D20, SR4 or RQ
+
+/togglehideid
+
+- to hide or not to players the IDs (for /publiclist, &quot;showlist&quot; commands only)
+  if you use /addhidden often, it might be a good idea to enable this setting because your players might notice the missing IDs when displaying the initiative list.
+
+/togglehideondeck
+- hides the &quot;On Deck&quot; messages generated each turn to show who should be ready next
+
+/togglehidejustmovement
+- hides the &quot;Just Movement&quot; messages in the Shadowrun 4th system, which are shown to help you speed up movement if you choose to divide it between passes (as in the rulebook)
+- Veggiesama wrote up this awfully verbose miniature guide for SR4 which works well with the &quot;Just Movement&quot; feature enabled: http://www.myfilehut.com/userfiles/17253/movement.pdf
+- Veggiesama likes to plug his own work
+
+/msghandwarning
+- disables the warning message that pops up every 60 seconds if someone else in the room has the xxinit2 plugin loaded
+- adding inits with two people in the room can lead to an ugly infinite loop, so it's advised that one of you save your current initiative list and turn it off
+
+/toggleroundreroll
+- optional SR4 toggle
+- allows you to change the behavior of initiatives at the end of the round
+- by default, you reroll new initiatives every round (like in the normal rules).
+- if you toggle it to &quot;Keep Inits Every Round&quot;, you can choose to keep the same initiatives in subsequent rounds; this leads to faster, less random (initiative-wise) battles
+
+/toggleiporder
+- optional SR4 toggle
+- allows you to change the order of IP execution
+- ex: 3/1/4/2 lets only characters with 3 IP act on the first pass, then 1 IP on second, 4 IP on third, etc.
+- some reasons for using staggered initiative passes can be found on this thread: http://forums.dumpshock.com/index.php?showtopic=11860</text>
+      </nodehandler>
+      <nodehandler class="textctrl_handler" icon="note" module="forms" name="About" version="1.0">
+        <text hide_title="0" multiline="1" raw_mode="0" send_button="0">========================================================================
+CREDITS
+
+Initiative Tool is the result of the work done by a number of people. The
+major contributors are listed here. (in no particular order)
+
+Woody
+        Wrote the original code
+
+Darloth
+        Wrote the original code
+
+mDuo13
+        Updates to version 1
+
+Veggie-sama [veggie-sama@cinci.rr.com]
+        Version 2.0, 2.1, 2.2.0, 2.2.5, 2.2.6, 2.2.7 with lots of new features and bugfixes
+
+magoo [legare@gmail.com]
+        Version 2.2.1, 2.2.2, 2.2.3, 2.2.4 (sandglass, save/load, ranking, RQ, etc.)
+
+========================================================================
+ADDITIONAL THANKS
+
+Hussar
+    Playtesting
+
+slyfax
+    Explanations for RuneQuest support and playtesting
+
+Zalarian
+    Bugfix with ending effects
+
+
+Thanks to the OpenRPG Dev Team and all the playtesters!
+
+========================================================================
+LICENSE
+
+Unless otherwise mentioned, this copyright applies to all files in
+this distribution.
+
+The license for all source code and documentation is the GNU GPL version 2:
+
+   http://www.gnu.org/licenses/gpl.txt
+
+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.
+</text>
+      </nodehandler>
+    </nodehandler>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/inittool2_player.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,66 @@
+
+<nodehandler class="group_handler" icon="player" module="containers" name="Player's Initiative Tool" status="useful" version="1.0">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="READ ME FIRST!!" version="1.0">
+    <text hide_title="0" multiline="1" raw_mode="0" send_button="0">=======================================
+
++++To GMs+++: Send a copy of the Player's Initiative Tool node to each of your players by right-clicking and pressing &quot;Send to Player&quot;. Advise them to open &quot;READ ME FIRST&quot;. Feel free to delete the node afterwards.
+
+This node should help speed things up a bit for the players, and hopefully end the nagging &quot;how do I roll my init again?&quot; questions.
+
+=======================================
+
++++To Players+++: The Player's Initiative Tool provides you a list of macros for the Initiative Tool v2, usable by anyone (not just the GM). What to do:
+
+1) Right-click and press &quot;design&quot; on each of the macro nodes.
+--- The title describes what the macro will do.
+--- The comment text (between the &quot;&lt;!---&quot; and &quot;---&gt;&quot;) describes how you should type it out in the chat or macro window. If you have trouble understanding, read the comment out loud to yourself.
+--- The last line shows an example of the macro.
+
+2) Replace the examples with your character's information.
+--- Feel free to delete any comments or macros that you definitely won't use.
+--- &quot;Show Inits&quot; and &quot;Next Turn&quot; don't need to be changed at all.
+
+3) Double-click the macro to activate it.
+--- If you need to make changes later on, just right-click and &quot;design&quot; again to change the macro.
+--- For effects in particular, sometimes it's easier just to memorize the formula rather than change it every time you want to add something.</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="die" module="chatmacro" name="Switch to D20 Dieroller" version="1.0">
+    <text>/dieroller d20</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="die" module="chatmacro" name="Switch to SR4 Dieroller" version="1.0">
+    <text>/dieroller sr4</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Next Turn" version="1.0">
+    <text>&lt;!---Advances the initiative to the next turn; use only at end of your turn and not other people's turns---&gt;
+nextinit</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Show Inits" version="1.0">
+    <text>&lt;!---Shows a public list of initiatives; can be quite spammy---&gt;
+showinits</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="d20" module="chatmacro" name="Roll Init (D20)" version="1.0">
+    <text>&lt;!---Name, space Initiative Roll in brackets, space the word &quot;init&quot;---&gt;
+Veggiesama [1d20+2] init</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="d20" module="chatmacro" name="Add Effect (D20)" version="1.0">
+    <text>&lt;!---Name, space Initiative Count of caster/user in brackets, space the word &quot;effect&quot;, space the Duration (in rounds) of the effect---&gt;
+&lt;!---Detailing the exact effects in the Name, though spammy, can be useful for yourself and the GM---&gt;
+Veggiesama's Awesome Aura (+4 STR/DEX/CON to all in 20' radius, +8 to self) [10] effect 5</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="gun2" module="chatmacro" name="Roll Init (SR4)" version="1.0">
+    <text>&lt;!---Name, space Initiative Roll in brackets, space the word &quot;init&quot;, space Initiative Passes---&gt;
+&lt;!---the Initiative Roll is [Xd6.init(Y)], where X is the number of dice you are rolling, and Y is your Initiative Score (usually both the same value)---&gt;
+Veggiesama [5d6.init(5)] init 2</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="shades" module="chatmacro" name="Roll Init (SR4 Virtual)" version="1.0">
+    <text>&lt;!---Name, space Initiative Roll in brackets, space the word &quot;init&quot;, space Initiative Passes---&gt;
+&lt;!---the Initiative Roll is [Xd6.init(Y)], where X is the number of dice you are rolling, and Y is your Initiative Score (usually both the same value)---&gt;
+Virtual Veggiesama [5d6.init(5)] init 2</text>
+  </nodehandler>
+  <nodehandler class="macro_handler" icon="wizard1" module="chatmacro" name="Roll Init (SR4 Astral)" version="1.0">
+    <text>&lt;!---Name, space Initiative Roll in brackets, space the word &quot;init&quot;, space Initiative Passes---&gt;
+&lt;!---the Initiative Roll is [Xd6.init(Y)], where X is the number of dice you are rolling, and Y is your Initiative Score (usually both the same value)---&gt;
+Astral Veggiesama [5d6.init(5)] init 2</text>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/quotebox.xml	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,59 @@
+
+<nodehandler class="group_handler" icon="image" module="containers" name="QuoteBox" version="1.0">
+  <group_atts border="1" cols="1"/>
+  <nodehandler class="group_handler" icon="image" module="containers" name="QuoteBox Settings" version="1.0">
+    <group_atts border="1" cols="1"/>
+    <nodehandler class="macro_handler" icon="8ball" module="chatmacro" name="Show Settings" version="1.0">
+      <text>/box</text>
+    </nodehandler>
+    <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set BG Color" version="1.0">
+      <text>/box_bgcol</text>
+    </nodehandler>
+    <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set Font Color" version="1.0">
+      <text>/box_fontcol</text>
+    </nodehandler>
+    <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Toggle Border" version="1.0">
+      <text>/box_border</text>
+    </nodehandler>
+    <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Toggle Italics" version="1.0">
+      <text>/box_italics</text>
+    </nodehandler>
+    <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Toggle Bold" version="1.0">
+      <text>/box_bold</text>
+    </nodehandler>
+    <nodehandler class="group_handler" icon="divider" module="containers" name="Change Size" version="1.0">
+      <group_atts border="1" cols="1"/>
+      <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set Size 1" version="1.0">
+        <text>/box_size 1</text>
+      </nodehandler>
+      <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set Size 2" version="1.0">
+        <text>/box_size 2</text>
+      </nodehandler>
+      <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set Size 3" version="1.0">
+        <text>/box_size 3</text>
+      </nodehandler>
+      <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set Size 4" version="1.0">
+        <text>/box_size 4</text>
+      </nodehandler>
+      <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set Size 5" version="1.0">
+        <text>/box_size 5</text>
+      </nodehandler>
+      <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set Size 6" version="1.0">
+        <text>/box_size 6</text>
+      </nodehandler>
+      <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set Size 7" version="1.0">
+        <text>/box_size 7</text>
+      </nodehandler>
+    </nodehandler>
+    <nodehandler class="macro_handler" icon="rome" module="chatmacro" name="Set All Default" version="1.0">
+      <text>/box_default</text>
+    </nodehandler>
+  </nodehandler>
+  <nodehandler class="textctrl_handler" icon="note" module="forms" name="QuoteBox Help!" version="1.0">
+    <text hide_title="0" multiline="1" raw_mode="0" send_button="0">To use the QuoteBox, simply type into the chat:
+
+    /box *yourmessage*
+
+To change your QuoteBox's font color, size, etc., look in Settings under this node of your gametree.</text>
+  </nodehandler>
+</nodehandler>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/server/base_plugin.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,130 @@
+import sys
+import os
+
+class BasePluginClass(object):
+    def __init__(self):
+        self.__name = ""
+        self.__author = ""
+        self.__help = ""
+        self.__file = __file__
+        self.__activated = False
+        self.__inputpriority = -1 # -1 = not used; 99 = priority doesn't matter
+                                  # Any other number is priority, lowest number
+                                  # executes first
+        self.__outputpriority = -1# -1 = not used; 99 = priority doesn't matter
+                                  # Any other number is priority, lowest number
+                                  # executes first
+        self.__pollpriority = -1  # -1 = not used; 99 = priority doesn't matter
+                                  # Any other number is priority, lowest number
+                                  # executes first
+
+
+    def preParseIncoming(self, xml_dom, data):
+        return xml_dom, data
+
+    def postParseIncoming(self, data):
+        return data
+
+    def getPlayer(self):
+        return None
+
+    def setPlayer(self, playerData):
+        return
+
+    def preParseOutgoing(self):
+        return []
+
+
+    def _getName(self):
+        return self.__name
+
+    def _setName(self, val):
+        if isinstance(val, basestring):
+            self.__name = val
+        else:
+            self.__name = str(val)
+
+
+    def _getAuthor(self):
+        return self.__author
+
+    def _setAuthor(self, val):
+        if isinstance(val, basestring):
+            self.__author = val
+        else:
+            self.__author = str(val)
+
+
+    def _getHelp(self):
+        return self.__help
+
+    def _setHelp(self, val):
+        if isinstance(val, basestring):
+            self.__help = val
+        else:
+            self.__help = str(val)
+
+
+    def _getFile(self):
+        return self.__file
+
+    def _setFile(self, val):
+        if isinstance(val, basestring):
+            self.__file = val
+        else:
+            self.__file = str(val)
+
+
+    def _getActivated(self):
+        return self.__activated
+
+    def _setActivated(self, val):
+        if isinstance(val, bool):
+            self.__activated = val
+        elif isinstance(val, int):
+            if val <= 0:
+                self.__activated = False
+            else:
+                self.__activated = True
+        else:
+            self.__activated = False
+
+
+    def _getInputPriority(self):
+        return self.__activated
+
+    def _setInputPriority(self, val):
+        if isinstance(val, int) and val in xrange(-1, 100):
+            self.__inputpriority = val
+        else:
+            self.__inputpriority = -1
+
+
+    def _getOutputPriority(self):
+        return self.__activated
+
+    def _setOutputPriority(self, val):
+        if isinstance(val, int) and val in xrange(-1, 100):
+            self.__outputpriority = val
+        else:
+            self.__outputpriority = -1
+
+
+    def _getPollPriority(self):
+        return self.__pollpriority
+
+    def _setPollPriority(self, val):
+        if isinstance(val, int) and val in xrange(-1, 100):
+            self.__pollpriority = val
+        else:
+            self.__pollpriority = -1
+
+    Name = property(_getName, _setName)
+    Author = property(_getAuthor, _setAuthor)
+    Help = property(_getHelp, _setHelp)
+    File = property(_getFile, _setFile)
+    Activated = property(_getActivated, _setActivated)
+    InputPriority = property(_getInputPriority, _setInputPriority)
+    OutputPriority = property(_getOutputPriority, _setOutputPriority)
+    PollPriority = property(_getPollPriority, _setPollPriority)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/server/examplePlugin.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,44 @@
+import os
+import sys
+from base_plugin import BasePluginClass
+
+class Plugin(BasePluginClass):
+    def __init__(self):
+        BasePluginClass.__init__(self)
+
+        self.Name = "examplePlugin"
+        self.File = __file__
+        self.Author = "Dj Gilcrease"
+        self.Help = "Help"
+        self.InputPriority = -1 # -1 = not used; 99 = priority doesn't matter
+                                # Any other number is priority, lowest number
+                                # executes first
+        self.OutputPriority = -1# -1 = not used; 99 = priority doesn't matter
+                                # Any other number is priority, lowest number
+                                # executes first
+        self.PollPriority = -1  # -1 = not used; 99 = priority doesn't matter
+                                # Any other number is priority, lowest number
+                                # executes first
+
+    def start(self):
+        #Do you DB connection here
+        pass
+
+    def stop(self):
+        #Close your DB connection here
+        pass
+
+    def preParseIncoming(self, xml_dom, data):
+        #Do something with the Data or Dom, and return it
+
+        return xml_dom, data
+
+    def postParseIncoming(self, data):
+        #Do something with the Data before it gets sent to the room
+
+        return data
+
+    def preParseOutgoing(self):
+        #Fetch messages from somewhere that need to be sent out
+
+        return []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxblank.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,126 @@
+import os
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = 'Example Plugin'
+        self.author = 'Your Name'
+        self.help = 'Info About your plugin'
+
+        #You can set variables below here. Always set them to a blank value in this section. Use plugin_enabled
+        #to set their proper values.
+        self.sample_variable = {}
+
+
+    def plugin_enabled(self):
+        #You can add new /commands like
+        # self.plugin_addcommand(cmd, function, helptext)
+        self.plugin_addcommand('/test', self.on_test, '- This is an example plugin command')
+
+        #If you want your plugin to have more then one way to call the same function you can
+        #use self.plugin_commandalias(alias name, command name)
+        #You can also make shortcut commands like the following
+        self.plugin_commandalias('/example', '/me is giving you an example')
+
+        #if you want your plugin to use custom messages to comunicate with other people using the same plugin
+        #you can add a message handler in a simmilar way to adding a new slash command. The first variable
+        #'xxblank' in this case is the tage name for your custom xml message. The second variable is the function
+        #you want to handle proccessing your messages when one is recived.
+        #Be sure to delete your handler in plugin_disabled
+        self.plugin_add_msg_handler('xxblank', self.on_xml_recive)
+
+        #if you want your plugin to store some settings in the settings window
+        #you can add them here, the system checks to make sure it does not already exist so you dont
+        #have to worry about it adding copies every time the plugin loads or it overwriting the users changes to it.
+        #This should be used for simple short settings that you would like the user to be able to change in the settings window
+        #variables:
+        #setting - The setting name, cannot contain spaces
+        #value - The default value
+        #options - The type of value that is expected
+        #help - a help message to explain what this variable does.
+        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'}
+
+    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
+        #such as closing windows created by the plugin
+        self.plugin_removecmd('/test')
+        self.plugin_removecmd('/example')
+
+        #This is the command to delete a message handler
+        self.plugin_delete_msg_handler('xxblank')
+
+        #This is how you should destroy a frame when the plugin is disabled
+        #This same method should be used in close_module as well
+        try:
+            self.frame.Destroy()
+        except:
+            pass
+
+    def on_test(self, cmdargs):
+        #this is just an example function for a command you create.
+        # cmdargs contains everything you typed after the command
+        # so if you typed /test this is a test, cmdargs = this is a test
+        # args are the individual arguments split. For the above example
+        # args[0] = this , args[1] = is , args[2] = a , args[3] = test
+        self.plugin_send_msg(cmdargs, '<xxblank>' + cmdargs + '</xxblank>')
+        args = cmdargs.split(None,-1)
+        msg = 'cmdargs = %s' % (cmdargs)
+        self.chat.InfoPost(msg)
+
+        if len(args) == 0:
+            self.chat.InfoPost("You have no args")
+        else:
+            i = 0
+            for n in args:
+                msg = 'args[' + str(i) + '] = ' + n
+                self.chat.InfoPost(msg)
+                i += 1
+
+    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))
+
+    def pre_parse(self, text):
+        #This is called just before a message is parsed by openrpg
+        return text
+
+    def send_msg(self, text, send):
+        #This is called when a message is about to be sent out.
+        #It covers all messages sent by the user, before they have been formatted.
+        #If send is set to 0, the message will not be sent out to other
+        #users, but it will still be posted to the user's chat normally.
+        #Otherwise, send defaults to 1. (The message is sent as normal)
+        return text, send
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        #This is called whenever a message from someone else is received, no matter
+        #what type of message it is.
+        #The text variable is the text of the message. If the type is a regular
+        #message, it is already formatted. Otherwise, it's not.
+        #The type variable is an integer which tells you the type: 1=chat, 2=whisper
+        #3=emote, 4=info, and 5=system.
+        #The name variable is the name of the player who sent you the message.
+        #The player variable contains lots of info about the player sending the
+        #message, including name, ID#, and currently-set role.
+        #Uncomment the following line to see the format for the player variable.
+        #print player
+        return text, type, name
+
+    def post_msg(self, text, myself):
+        #This is called whenever a message from anyone is about to be posted
+        #to chat; it doesn't affect the copy of the message that gets sent to others
+        #Be careful; system and info messages trigger this too.
+        return text
+
+    def refresh_counter(self):
+        #This is called once per second. That's all you need to know.
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxcac.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,52 @@
+import os
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = 'Command Alias Creator'
+        self.author = 'Dj Gilcrease'
+        self.help = "This plugin lets you add Command Aliases.\neg /sits insted of /me sits down"
+        self.newcmdaliases = {}
+
+
+    def plugin_enabled(self):
+        self.plugin_addcommand('/cmdalias', self.on_cmdalias, '[cmdalias_name fullcommand] [remove cmdalias_name] [clear] - (eg. <font color="#000000">/cmdalias /sits /me sits down</font> to add a command. OR <font color="#000000">/cmdalias remove /sits</font> to remove a single command. OR <font color="#000000">/cmdalias clear</font to clear the entire list)')
+        self.newcmdaliases = self.plugindb.GetDict("xxcac", "newcmdaliases", {})
+
+        for n in self.newcmdaliases:
+            if not self.shortcmdlist.has_key(n) and not self.cmdlist.has_key(n):
+                self.plugin_commandalias(n, self.newcmdaliases[n])
+
+
+    def plugin_disabled(self):
+        self.plugin_removecmd('/cmdalias')
+        for n in self.newcmdaliases:
+            self.plugin_removecmd(n)
+
+    def on_cmdalias(self, cmdargs):
+        args = cmdargs.split(" ",-1)
+        if len(args) == 0:
+            self.chat.InfoPost("USAGE: /cmdalias [cmdalias_name fullcommand] [remove cmdalias_name] [clear] - (eg. /sits /me sits down)")
+
+        elif args[0] == 'remove':
+            if self.newcmdaliases.has_key(args[1]):
+                del self.newcmdaliases[args[1]]
+                self.plugindb.SetDict("xxcac", "newcmdaliases", self.newcmdaliases)
+                self.plugin_removecmd(args[1])
+        elif args[0] == 'clear':
+            for n in self.newcmdaliases:
+                self.plugin_removecmd(n)
+            self.newcmdaliases = {}
+            self.plugindb.SetDict("xxcac", "newcmdaliases", self.newcmdaliases)
+        else:
+            oldcmd = cmdargs[len(args[0])+1:]
+            self.newcmdaliases[args[0]] = oldcmd
+            self.plugindb.SetDict("xxcac", "newcmdaliases", self.newcmdaliases)
+            self.plugin_commandalias(args[0], oldcmd)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxchatnotify.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,137 @@
+import os
+import orpg.pluginhandler
+import wx
+from orpg.orpgCore import *
+
+class Plugin(orpg.pluginhandler.PluginHandler): #Attempting to pass the orpgFrame so that we can Bind events, not working.
+    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 = 'Chat Notification'
+        self.author = 'Dj Gilcrease'
+        self.help = 'This plugin with either play a sound when a new chat message comes in, flash the taskbar or both'
+
+    def plugin_menu(self):
+	#Plugin Menu
+        self.menu = wx.Menu()
+        self.notifyToggle = self.menu.AppendCheckItem(wx.ID_ANY, 'On')
+        self.topframe.Bind(wx.EVT_MENU, self.on_settings, self.notifyToggle)
+
+        notify = wx.Menu()
+        self.notifyBeep = notify.AppendRadioItem(wx.ID_ANY, 'Beep')
+        self.notifyFlash = notify.AppendRadioItem(wx.ID_ANY, 'Flash')
+        self.notifyBoth = notify.AppendRadioItem(wx.ID_ANY, 'Both')
+        self.menu.AppendMenu(wx.ID_ANY, 'Notify', notify)
+        self.topframe.Bind(wx.EVT_MENU, self.on_settings, self.notifyBeep)
+        self.topframe.Bind(wx.EVT_MENU, self.on_settings, self.notifyFlash)
+        self.topframe.Bind(wx.EVT_MENU, self.on_settings, self.notifyBoth)
+
+        notifyType = wx.Menu()
+        self.notifyAll = notifyType.AppendRadioItem(wx.ID_ANY, 'All')
+        self.notifyWhisper = notifyType.AppendRadioItem(wx.ID_ANY, 'Whisper')
+        self.menu.AppendMenu(wx.ID_ANY, 'Type', notifyType)
+        self.topframe.Bind(wx.EVT_MENU, self.on_settings, self.notifyAll)
+        self.topframe.Bind(wx.EVT_MENU, self.on_settings, self.notifyWhisper)
+
+
+    def on_settings(self, evt):
+        if self.notifyToggle.IsChecked() == False:
+            self.notify = 'Off'
+            return
+        if self.notifyBeep.IsChecked() == True:
+            self.notify ='beep'
+        elif self.notifyFlash.IsChecked() == True:
+            self.notify = 'flash'
+        elif self.notifyBoth.IsChecked() == True:
+            self.notify = 'both'
+        if self.notifyAll.IsChecked() == True:
+            self.type = 'all'
+        elif self.notifyWhisper.IsChecked() == True:
+            self.type = 'whisper'
+
+
+    def plugin_enabled(self):
+        self.plugin_addcommand('/notify', self.on_notify, 'beep | flash | both | off | type all|whisper | clearsound | lsound soundfile [Local Sound Files only] | rsound http://to.sound.file [Remote Sound Files only] - This command turns on the chat notification. You can use sound files and flash by issuing /notify both')
+        self.notify = self.plugindb.GetString('xxchatnotify', 'notify', 'off')
+        self.type = self.plugindb.GetString('xxchatnotify', 'type', 'all')
+        self.mainframe = open_rpg.get_component('frame')
+        self.sound_player = open_rpg.get_component('sound')
+        self.soundloc = self.plugindb.GetString('xxchatnotify', 'soundloc', 'local')
+        self.soundfile = self.plugindb.GetString('xxchatnotify', 'soundfile', 'None')
+        self.chat_settings()
+
+    def chat_settings(self):
+        self.notifyToggle.Check(True)
+        if self.notify == 'beep':
+            self.notifyBeep.Check(True)
+        elif self.notify == 'flash':
+            self.notifyFlash.Check(True)
+        elif self.notify == 'both':
+            self.notifyBoth.Check(True)
+        else:
+            self.notifyToggle.Check(False)
+        if self.type == 'all':
+            self.notifyAll.Check(True)
+        elif self.type == 'whisper':
+            self.notifyWhisper.Check(True)
+
+    def plugin_disabled(self):
+        self.plugin_removecmd('/notify')
+
+    def on_notify(self, cmdargs):
+        args = cmdargs.split(None, 2)
+
+        if len(args) == 0:
+            self.chat.InfoPost('You must specify if you want it to beep, flash, both or be turned off or specify if you want to be notified for all messages or just whispers')
+
+        if args[0] == 'type':
+            self.type = args[1]
+            self.plugindb.SetString('xxchatnotify', 'type', self.type)
+            self.chat.InfoPost('Setting Notification on Message type to ' + args[1])
+            self.chat_settings()
+            return
+        elif args[0] == 'lsound':
+            self.soundloc = 'local'
+            self.soundfile = orpg.dirpath.dir_struct['plugins'] + args[1]
+            self.plugindb.SetString('xxchatnotify', 'soundfile', self.soundfile)
+            self.plugindb.GetString('xxchatnotify', 'soundloc', self.soundloc)
+            self.chat.InfoPost('Setting Sound file to ' + self.soundfile)
+            self.notify = 'beep'
+            return
+        elif args[0] == 'rsound':
+            self.soundloc = 'remote'
+            self.soundfile = args[1]
+            self.plugindb.SetString('xxchatnotify', 'soundfile', self.soundfile)
+            self.plugindb.GetString('xxchatnotify', 'soundloc', self.soundloc)
+            self.chat.InfoPost('Setting Sound file to ' + self.soundfile)
+            self.notify = 'beep'
+            return
+        elif args[0] == 'clearsound':
+            self.soundloc = 'local'
+            self.soundfile = 'None'
+            self.plugindb.SetString('xxchatnotify', 'soundfile', self.soundfile)
+            self.plugindb.GetString('xxchatnotify', 'soundloc', self.soundloc)
+            self.chat.InfoPost('Clearing Sound file')
+            self.notify = 'off'
+            return
+
+
+        self.notify = args[0]
+        self.plugindb.SetString('xxchatnotify', 'notify', self.notify)
+        self.chat.InfoPost('Setting Notification type to ' + args[0])
+        self.chat_settings()
+
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        if (self.notify == 'beep' or self.notify == 'both') and (self.type == 'all' or type == 2):
+            if self.soundfile == 'None':
+                wx.CallAfter(wx.Bell)
+                wx.CallAfter(wx.Bell)
+            else:
+                wx.CallAfter(self.sound_player.play, self.soundfile, self.soundloc)
+        if (self.notify == 'flash' or self.notify == 'both') and (self.type == 'all' or type == 2):
+            wx.CallAfter(self.mainframe.RequestUserAttention)
+        return text, type, name
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxcherrypy.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,128 @@
+import os
+import orpg.plugindb # VEG
+import orpg.pluginhandler
+import thread
+import cherrypy._cpserver as server
+import socket
+import wx
+
+ # VEG (march 21, 2007): Now remembers your last web server on/off setting
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = 'CherryPy Web Server'
+        self.author = 'Dj Gilcrease'
+        self.help = 'This plugin turns OpenRPG into a Web server\n'
+        self.help += 'allowing you to host your map and mini files localy'
+
+        #You can set variables below here. Always set them to a blank value in this section. Use plugin_enabled
+        #to set their proper values.
+        self.isServerRunning = 'off'
+        self.host = 0
+
+    def plugin_menu(self):
+        self.menu = wx.Menu()
+        self.toggle = self.menu.AppendCheckItem(wx.ID_ANY, 'On')
+        self.topframe.Bind(wx.EVT_MENU, self.cherrypy_toggle, self.toggle)
+
+        ports = wx.Menu()
+        self.portClient = ports.AppendRadioItem(wx.ID_ANY, 'Client:9557')
+        self.portServer = ports.AppendRadioItem(wx.ID_ANY, 'Server:9558')
+        self.topframe.Bind(wx.EVT_MENU, self.port_change, self.portClient)
+        self.topframe.Bind(wx.EVT_MENU, self.port_change, self.portServer)
+        self.menu.AppendMenu(wx.ID_ANY, 'Port', ports)
+
+    def port_change(self, evt):
+        if self.portClient.IsChecked() == True: self.on_cherrypy("port 9557")
+        if self.portServer.IsChecked() == True: self.on_cherrypy("port 9558")
+
+    def cherrypy_toggle(self, evt):
+        if self.toggle.IsChecked() == True: self.on_cherrypy("on")
+        if self.toggle.IsChecked() == False: self.on_cherrypy("off")
+
+    def plugin_enabled(self):
+        self.port = 9557
+        self.plugin_addcommand('/cherrypy', self.on_cherrypy, '[on | off | port | status] - This controls the CherryPy Web Server')
+        tmp = socket.gethostbyname_ex(socket.gethostname())
+        for ip in tmp[2]:
+            self.host = ip
+            if ip[:7] == '192.168' or ip[:3] == '10.' or ip == '127.0.0.1' or (ip[:3] == '172' and (int(ip[5:6]) >= 16 and int(ip[5:6]) <=32)) :
+                self.chat.InfoPost("[WARNING] Cherrypy has detected that you may be behind a router. This is your internal IP. For other users to properly connect, you may have to use your external IP, with port forwarding on port 80.<br />This feature is not suported in any way.")
+
+        #if str(self.plugindb.GetString("xxcherrypy", "auto_start", "off")) == "on":  # VEG
+        #    self.on_cherrypy("on")                                                   # VEG
+
+    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
+        #such as closing windows created by the plugin
+        self.plugin_removecmd('/cherrypy')
+        if self.isServerRunning == 'on':
+            server.stop()
+            self.isServerRunning = 'off'
+        else:
+            pass
+
+
+    def on_cherrypy(self, cmdargs):
+        args = cmdargs.split(None,-1)
+
+        if len(args) == 0 or args[0] == 'status':
+            self.chat.InfoPost("CherryPy Web Server is currently: " + self.isServerRunning)
+            self.chat.InfoPost("CherryPy Web Server address is: http://" + str(self.host) + ':' + str(self.port) + '/webfiles/')
+
+        elif args[0] == 'on' and self.isServerRunning == 'off':
+            self.webserver = thread.start_new_thread(self.startServer, (self.port,))
+            self.isServerRunning = 'on'
+            self.toggle.Check(True)
+            self.plugindb.SetString("xxcherrypy", "auto_start", "on") # VEG
+
+        elif args[0] == 'off' and self.isServerRunning == 'on':
+            server.stop()
+            self.isServerRunning = 'off'
+            self.toggle.Check(False)
+            self.chat.InfoPost("CherryPy Web Server is now disabled")
+            self.plugindb.SetString("xxcherrypy", "auto_start", "off") # VEG
+
+        elif args[0] == 'port':
+            if self.isServerRunning == 'on':
+                self.port = int(args[1])
+                server.stop()
+                self.webserver = thread.start_new_thread(self.startServer, (self.port,))
+            self.port = int(args[1])
+            self.chat.InfoPost("CherryPy Web Server is currently: " + self.isServerRunning)
+            self.chat.InfoPost("CherryPy Web Server address is: http://" + str(self.host) + ':' + str(self.port) + '/webfiles/')
+
+
+
+    def startServer(self, port):
+        try:
+            if self.host == 0:
+                raise Exception("Invalid IP address.<br />This error means you are behind a router or some other form of network that is giving you a Privet IP only (ie. 192.168.x.x, 10.x.x.x, 172.16 - 32.x.x)")
+
+            self.chat.InfoPost("CherryPy Web Server is now running on http://" + str(self.host) + ':' + str(self.port) + '/webfiles/')
+            server.start(configMap =
+                        {'staticContentList': [['', r''+orpg.dirpath.dir_struct["user"]+'webfiles/'],
+                                               ['webfiles', r''+orpg.dirpath.dir_struct["user"]+'webfiles/'],
+                                               ['Textures', r''+orpg.dirpath.dir_struct["user"]+'Textures/'],
+                                               ['Maps', r''+orpg.dirpath.dir_struct["user"]+'Maps/'],
+                                               ['Miniatures', r''+orpg.dirpath.dir_struct["user"]+'Miniatures']],
+                        'socketPort': port,
+                        'logToScreen': 0,
+                        'logFile':orpg.dirpath.dir_struct["user"]+'webfiles/log.txt',
+                        'sessionStorageType':'ram',
+                        'threadPool':10,
+                        'sessionTimeout':30,
+                        'sessionCleanUpDelay':30})
+
+        except Exception, e:
+            self.chat.InfoPost("FAILED to start server!")
+            self.chat.InfoPost(str(e))
+            self.isServerRunning = 'off'
+            self.toggle.Check(False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxfontchng.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,93 @@
+import os
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    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 = 'Font Tools'
+        self.author = 'mDuo13'
+        self.help = "You can change your font in chat by typing '/myfont *key*'\n"
+        self.help += "where *key* is this plugin's shorthand for a font, like 'c'\n"
+        self.help += "for Courier. You can type /myfont list for the whole list.\n"
+        self.help += "You can use another font by typing '/myfont custom=*name*', where\n"
+        self.help += "*name* is the EXACT name of the font, case-sensitive and all.\n"
+        self.help += "Font resizing is no longer supported by the plugin as it is in\n"
+        self.help += "the official code now. (Try typing '/fontsize *x*' to change your\n"
+        self.help += "font to size *x*.)"
+        #define any variables here, but always define them as ''. you will set thier values and proper type in plugin_enabled
+        self.fontstring = ''
+        self.fontlist = {}
+
+    def plugin_enabled(self):
+        #This is where you set any variables that need to be initalized when your plugin starts
+        #You can add new /commands like
+        self.plugin_addcommand('/myfont', self.on_myfont, '[list|custom (fontname)|fontname] - This lets you change the font type for your messages, but leaves other peoples alone')
+        #Argument 1 is the command it self
+        #Argument 2 is the function to be called when you issue the command
+        #Argument 3 is the help text for the command
+
+        self.fontstring = self.plugindb.GetString("xxfontchng", "fontstring", "")
+        self.fontlist = {"a" : "<font>",
+                        "arial" : "<font>",
+                        "c" : "<font face='Courier New, Courier, monospace'>",
+                        "courier" : "<font face='Courier New, Courier, monospace'>",
+                        "t" : "<font face='Times New Roman, Times, serif'>",
+                        "times" : "<font face='Times New Roman, Times, serif'>",
+                        "tempus" : "<font face='Tempus Sans ITC'>",
+                        "westminster" : "<font face='Westminster' size=+1>",
+                        "mistral" : "<font face='Mistral' size=+2>",
+                        "lucida sans" : "<font face='Lucida Sans'>",
+                        "lucida handwriting" : "<font face='Lucida Handwriting'>",
+                        "western" : "<font face='Rockwell'>",
+                        "rockwell" : "<font face='Rockwell'>"}
+
+    def plugin_disabled(self):
+        self.plugin_removecmd('/myfont')
+
+    def on_myfont(self, cmdargs):
+        args = cmdargs.split(None,1)
+
+        if len(args) == 0 or args[0] == 'list':
+            the_list = ''
+            for entry in self.fontlist.keys():
+                the_list += '<br />' + self.fontlist[entry] + entry + '</font>'
+            the_list += '<br />custom *anyfont*'
+            self.chat.InfoPost(the_list)
+        elif args[0] == 'custom':
+            self.fontstring = '<font face="' + args[1] + '">'
+        elif self.fontlist.has_key(args[0]):
+            self.fontstring = self.fontlist[args[0]]
+            self.chat.InfoPost("Your font now looks " + self.fontstring + "like this.</font>")
+            self.plugindb.SetString("xxfontchng", "fontstring", self.fontstring)
+        else:
+            self.chat.InfoPost("Invalid font name. Type /myfont list for a list of font names, or use custom preceding the font name")
+
+    def pre_parse(self, text):
+        #This is called just before a message is parsed by openrpg
+        cmdsearch = text.split(None,1)
+        if len(cmdsearch) == 0:
+            return text
+        cmd = cmdsearch[0].lower()
+        start = len(cmd)
+        end = len(text)
+        cmdargs = text[start:end]
+
+        if cmd == "/whisper" or cmd == "/w":
+            text = cmd + ' ' + cmdargs[:cmdargs.find("=")+1] + self.fontstring + cmdargs + '</font>'
+        elif cmd == "/me" or cmd == "/he" or cmd == "/she":
+            text = cmd + ' ' + self.fontstring + cmdargs + '</font>'
+        elif cmd[0] != '/':
+            text = self.fontstring + text + '</font>'
+        return text
+
+    def send_msg(self, text, send):
+        if self.fontstring.find("<font") > -1:
+            text = self.fontstring + text + '</font>'
+        return text, send
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxgsc.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,112 @@
+import os
+from orpg.orpg_wx import *
+import random
+import orpg.pluginhandler
+
+
+ID_ROLL = wx.NewId()
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+
+        self.name = 'Game Status Controller'
+        self.author = 'Woody, updated by mDuo13'
+        self.help = 'This plugin lets you quickly and easily manage a status that includes \n'
+        self.help += 'your HP and AC for AD&D 2nd Edition. Type /gsc to open up the manager\n'
+        self.help += 'window, and from there just change the values as necessary and the changes\n'
+        self.help += 'will be reflected in your status. To revert to your regular status, close\n'
+        self.help += 'the GSC window.'
+
+        self.frame = None
+
+    def plugin_enabled(self):
+        self.plugin_addcommand('/gsc', self.on_gsc, '- The GSC command')
+        self.frame = RollerFrame(None, -1, "Game Status Ctrl (GSC)", self)
+        self.frame.Hide()
+
+        item = wx.MenuItem(self.menu, wx.ID_ANY, "GSC Window", "GSC Window", wx.ITEM_CHECK)
+        self.topframe.Bind(wx.EVT_MENU, self._toggleWindow, item)
+        self.menu.AppendItem(item)
+
+    def plugin_disabled(self):
+        self.plugin_removecmd('/gsc')
+        try:
+            self.frame.TimeToQuit(1)
+        except:
+            pass
+
+    def on_gsc(self, cmdargs):
+        item = self.menu.FindItemById(self.menu.FindItem("GSC Window"))
+        item.Check(True)
+        self.frame.Show()
+        self.frame.Raise()
+
+    #Events
+    def _toggleWindow(self, evt):
+        id = evt.GetId()
+        item = self.menu.FindItemById(id)
+        if self.frame.IsShown():
+            self.frame.Hide()
+            item.Check(False)
+        else:
+            self.frame.Show()
+            item.Check(True)
+
+
+class RollerFrame(wx.Frame):
+    def __init__(self, parent, ID, title, plugin):
+        wx.Frame.__init__(self, parent, ID, title,
+                         wx.DefaultPosition, wx.Size(200, 70))
+
+        self.settings = plugin.settings
+        self.session = plugin.session
+        self.plugin = plugin
+
+        self.panel = wx.Panel(self,-1)
+        menu = wx.Menu()
+        menu.AppendSeparator()
+        menu.Append(wx.ID_EXIT, "&Close", "Close this window")
+        menuBar = wx.MenuBar()
+        menuBar.Append(menu, "&File");
+        self.SetMenuBar(menuBar)
+
+        self.old_idle = self.settings.get_setting('IdleStatusAlias')
+
+        wx.StaticText(self.panel, -1, "AC:", wx.Point(0, 5))
+        self.ac = wx.SpinCtrl(self.panel, ID_ROLL, "", wx.Point(18, 0), wx.Size(45, -1), min = -100, max = 100, initial = 10)
+        self.ac.SetValue(10)
+
+        wx.StaticText(self.panel, -1, "/", wx.Point(136, 5))
+        self.max_hp = wx.SpinCtrl(self.panel, ID_ROLL, "", wx.Point(144, 0), wx.Size(48, -1), min = -999, max = 999, initial = 10)
+        self.max_hp.SetValue(10)
+
+        wx.StaticText(self.panel, -1, "HP:", wx.Point(65, 5))
+        self.hp = wx.SpinCtrl(self.panel, ID_ROLL, "", wx.Point(83, 0), wx.Size(48, -1), min = -999, max = 999, initial = 10)
+        self.hp.SetValue(10)
+
+        self.Bind(wx.EVT_SPINCTRL, self.SetStatus, id=ID_ROLL)
+        self.Bind(wx.EVT_TEXT, self.SetStatus, id=ID_ROLL)
+        self.Bind(wx.EVT_MENU, self.TimeToQuit, id=wx.ID_EXIT)
+        self.Bind(wx.EVT_CLOSE, self._close)
+        self.SetStatus(None)
+
+    def SetStatus(self, evt):
+        new_status = "AC: " + str(self.ac.GetValue()) + "   HP: " + str(self.hp.GetValue()) + "/" + str(self.max_hp.GetValue())
+        self.settings.set_setting('IdleStatusAlias',new_status)
+        self.session.set_text_status(new_status)
+
+    def TimeToQuit(self, event):
+        self.settings.set_setting('IdleStatusAlias',self.old_idle)
+        self.session.set_text_status(self.old_idle)
+        self.frame = None
+        self.Destroy()
+
+    def _close(self, evt):
+        self.Hide()
+        item = self.plugin.menu.FindItemById(self.plugin.menu.FindItem("GSC Window"))
+        item.Check(False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxgvm.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,195 @@
+import os
+import orpg.pluginhandler
+import string
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = 'Game Variable Manager'
+        self.author = 'mDuo13'
+        self.help = 'This plugin is used to manage variables in your idle status. That way,\n'
+        self.help += 'you can keep as many numbers as you want in your idle status, and you can update \n'
+        self.help += 'them without having to go to the Settings menu.\n'
+        self.help += '\n'
+        self.help += 'First you must create some variables. You can do that by typing \n'
+        self.help += '"/gvm set *var*=*value*" where *var* is the variables name (no $ symbol), and\n'
+        self.help += '*value* is the variables starting value. The variables name can be any word,\n'
+        self.help += 'but dont put spaces between the variables name and value.\n'
+        self.help += '\n'
+        self.help += 'You can look up the value of a variable by typing "/gvm get *var*"\n'
+        self.help += '\n'
+        self.help += 'To set your status, type "/gvm status *message*" where *message* is the message \n'
+        self.help += 'you want your status to have. To include variables, simply the variables name, \n'
+        self.help += 'STARTING WITH A $ SYMBOL. For example, you could create a status with \n'
+        self.help += '"/gvm status HP:$hp MP:$mp AC:$ac" which, if $hp was 20, $mp was 10, and $ac\n'
+        self.help += 'was 5, would make your status "HP:20 MP:10 AC:5".\n'
+        self.help += '\n'
+        self.help += 'If you later change the values of any of the variables in your status, they will \n'
+        self.help += 'be automatically updated; to update a value, just type\n'
+        self.help += '"/gvm set *var*=*variable*" the same way you used it to create the variable.\n'
+        self.help += '\n'
+        self.help += 'You can list the current variables with the "/gvm list" command. You can also\n'
+        self.help += 'now roll dice with curly brackets, and have filters apply to them, like this:\n'
+        self.help += '"{4d6.takeHighest(3)+$bonus}". Any other filters you have (i.e. the Alias \n'
+        self.help += 'Library) will also apply to curly bracket rolls automatically.\n'
+        self.help += '\n'
+        self.help += 'Note that the GVM plugin conflicts with the NowPlaying and GSC plugins, so if\n'
+        self.help += 'you have both plugin enabled at the same time, they may not work quite right.\n'
+        self.help += '\n'
+        self.help += 'You can now save a group of variables with "/gvm save *name*". You can also load\n'
+        self.help += 'a group of previously saved variables with "/gvm open *name*"\n'
+
+        #You can set variables below here. Always set them to a blank value in this section. Use plugin_enabled
+        #to set their proper values.
+        self.vars = {}
+        self.status_scrip=""
+        self.status_on=0
+
+
+    def plugin_enabled(self):
+        #You can add new /commands like
+        # self.plugin_addcommand(cmd, function, helptext)
+        self.plugin_addcommand('/gvm', self.on_gvm, '[set name=value | calc name=value | get name | status | list | save set_name |  open set_name] - This plugin is used to manage variables that you can use in your status bar or in dice rolls')
+        self.plugin_addcommand('/gvmq', self.on_gvmq, '[set name=value | calc name=value | get name | status | list | save set_name |  open set_name] - This plugin is used to manage variables that you can use in your status bar or in dice rolls', False)
+        self.plugin_addcommand('/=', self.on_comment, '', False)
+
+    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
+        #such as closing windows created by the plugin
+        self.plugin_removecmd('/gvm')
+        self.plugin_removecmd('/gvmq')
+        self.plugin_removecmd('/=')
+
+    def on_comment(self, cmdargs):
+        pass
+
+    def on_gvm(self, cmdargs):
+        for key in self.vars.keys():
+            cmdargs = cmdargs.replace("$" + key, self.vars[key])
+
+        args = cmdargs.split(None,-1)
+        if len(args) == 0:
+            args = [' ']
+        if args[0]=="set":
+            cmd = cmdargs[len(args[0])+1:].strip().split("=")
+            self.vars[cmd[0]] = cmd[1]
+            self.chat.InfoPost("GVM: Set variable $" + cmd[0] + " to be: " + cmd[1])
+            if self.status_on:
+                self.gvm_status(self.status_scrip)
+
+        elif args[0]=="calc":
+            cmd = cmdargs[len(args[0])+1:].strip().split("=")
+            val = str(eval(cmd[1]))
+            self.vars[cmd[0]] = val
+            self.chat.InfoPost("GVM: Set variable $" + cmd[0] + " to be: " + val)
+            if self.status_on:
+                self.gvm_status(self.status_scrip)
+
+        elif args[0]=="get":
+            if args[1] in self.vars.keys():
+                self.chat.InfoPost("GVM: Variable $" + args[1] + " is: " + self.vars[args[1]])
+            else:
+                self.chat.InfoPost("GVM: Variable $" + args[1] + " does not exist!")
+
+        elif args[0]=="status":
+            self.status_scrip = cmdargs[len(args[0])+1:]
+            status_on = 1
+            self.gvm_status(self.status_scrip)
+
+        elif args[0]=="save":
+            fname = cmdargs[len(args[0])+1:]
+            self.plugindb.SetDict("xxgvm", fname, self.vars)
+            self.chat.InfoPost("GVM: Successfully saved variable set " + fname + "!")
+
+        elif args[0]=="open":
+            fname = cmdargs[len(args[0])+1:]
+            self.vars = self.plugindb.GetDict("xxgvm", fname, {})
+            self.chat.InfoPost("GVM: Loaded the file " + fname + ". Variables contained are:<br />" + self.make_list())
+
+        elif args[0] == "list":
+            self.chat.Post(self.make_list())
+
+        else:
+            self.chat.InfoPost("GVM: SYNTAX ERROR. <br />USEAGE: /gvm [set name=value | get name | status | list | save set_name |  open set_name]")
+
+    def on_gvmq(self, cmdargs):
+        for key in self.vars.keys():
+            cmdargs = cmdargs.replace("$" + key, self.vars[key])
+        args = cmdargs.split(None,-1)
+        if len(args) == 0:
+            args = [' ']
+        if args[0]=="set":
+            cmd = cmdargs[len(args[0])+1:].strip().split("=")
+            self.vars[cmd[0]] = cmd[1]
+            if self.status_on:
+                self.gvm_status(self.status_scrip)
+
+        elif args[0]=="calc":
+            cmd = cmdargs[len(args[0])+1:].strip().split("=")
+            self.vars[cmd[0]] = str(eval(cmd[1]))
+            if self.status_on:
+                self.gvm_status(self.status_scrip)
+
+        elif args[0]=="get":
+            if args[1] in self.vars.keys():
+                self.chat.InfoPost("GVM: Variable $" + args[1] + " is: " + self.vars[args[1]])
+            else:
+                self.chat.InfoPost("GVM: Variable $" + args[1] + " does not exist!")
+
+        elif args[0]=="status":
+            self.status_scrip = cmdargs[len(args[0])+1:]
+            status_on = 1
+            self.gvm_status(self.status_scrip)
+
+        elif args[0]=="save":
+            fname = cmdargs[len(args[0])+1:]
+            self.plugindb.SetDict("xxgvm", fname, self.vars)
+
+        elif args[0]=="open":
+            fname = cmdargs[len(args[0])+1:]
+            self.vars = self.plugindb.GetDict("xxgvm", fname, {})
+
+        elif args[0] == "list":
+            self.chat.Post(self.make_list())
+
+        else:
+            self.chat.InfoPost("GVM: SYNTAX ERROR. <br />USEAGE: /gvmq [set name=value | get name | status | list | save set_name |  open set_name]")
+
+
+    def pre_parse(self, text):
+        try:
+            for key in self.vars.keys():
+                text = text.replace("$" + key, self.vars[key])
+        except Exception, e:
+            print e
+            print key
+            self.vars[key]
+
+        return text
+
+    def gvm_status(self, cmd):
+        keychain = self.vars.keys()
+        keychain.sort()
+        newchain = []
+        for key in keychain:
+            newchain = [key] + newchain
+        for key in keychain:
+            cmd = cmd.replace("$" + key, self.vars[key])
+        self.settings.set_setting("IdleStatusAlias", cmd)
+        self.session.set_text_status(cmd)
+
+    def make_list(self):
+        keychain = self.vars.keys()
+        lister = ""
+        for key in keychain:
+            lister += "$" + key + "\t::\t" + self.vars[key] + "<br />"
+        if len(keychain)==0:
+            return "No variables!"
+        else:
+            return lister
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxheroinit.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,522 @@
+import os
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = 'Hero Turn Tool'
+        self.author = 'Heroman, based on xxxinit.py'
+        self.help = """This is the Hero Games turn turn. It is a bit more complex than other games."""
+
+
+    def plugin_enabled(self):
+        #You can add new /commands like
+        # self.plugin_addcommand(cmd, function, helptext)
+        self.plugin_addcommand('/hinit', self.on_hinit, '- Toggle Init Recording on or off')
+        self.plugin_addcommand('/hhelp', self.on_hhelp, '- List all of the Hero Init Commands')
+        self.plugin_addcommand('/hstart', self.on_hstart, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hact', self.on_hact, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hseg', self.on_hseg, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hdex', self.on_hdex, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hclear', self.on_hclear, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hadd', self.on_hadd, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hdel', self.on_hdel, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hown', self.on_hown, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hlist', self.on_hlist, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hsort', self.on_hsort, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hrun', self.on_hrun, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hchange', self.on_hchange, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hset', self.on_hset, '- Start the Hero Init Tool', False)
+        self.plugin_addcommand('/hchange', self.on_hchange, '- Start the Hero Init Tool', False)
+
+        #If you want your plugin to have more then one way to call the same function you can
+        #use self.plugin_commandalias(alias name, command name)
+        #You can also make shortcut commands like the following
+        self.plugin_commandalias('/hlow', '/hsort low', False)
+        self.plugin_commandalias('/hsortlow', '/hsort low', False)
+        self.plugin_commandalias('/hhigh', '/hsort high', False)
+        self.plugin_commandalias('/hsorthigh', '/hsort high', False)
+        self.plugin_commandalias('/hreverse', '/hsort high', False)
+        self.plugin_commandalias('/hgo', '/hrun', False)
+
+        #This is where you set any variables that need to be initalized when your plugin starts
+        self.toggle = True
+        self.init_list = []
+        self.backup_list = []
+        self.dexterity = 99
+        self.segment = 12
+        self.state_strings = ["0 Phase","1/2 Phase","Full Phase","Stunned","Not Gone", "Held Full", "Held Half", "Abort"]
+
+    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
+        #such as closing windows created by the plugin
+        self.plugin_removecmd('/test')
+        self.plugin_removecmd('/example')
+
+        #This is the command to delete a message handler
+        self.plugin_delete_msg_handler('xxblank')
+
+        #This is how you should destroy a frame when the plugin is disabled
+        #This same method should be used in close_module as well
+        try:
+            self.frame.Destroy()
+        except:
+            pass
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        #This is called whenever a message from someone else is received, no matter
+        #what type of message it is.
+        #The text variable is the text of the message. If the type is a regular
+        #message, it is already formatted. Otherwise, it's not.
+        #The type variable is an integer which tells you the type: 1=chat, 2=whisper
+        #3=emote, 4=info, and 5=system.
+        #The name variable is the name of the player who sent you the message.
+        #The player variable contains lots of info about the player sending the
+        #message, including name, ID#, and currently-set role.
+        #Uncomment the following line to see the format for the player variable.
+        #print player
+
+        if self.toggle:
+            if text.lower().find("heroinit") != -1:
+                command = text[text.rfind("(")+1:text.rfind(")")]
+                self.process_user(command, player)
+        return text, type, name
+
+    #Chat Commands
+    def on_hinit(self, cmdargs):
+        self.toggle = not self.toggle
+
+        if self.toggle:
+            self.chat.SystemPost("Init recording on.  Enter your info")
+        else:
+            self.chat.SystemPost("Init recording off")
+
+    def on_hhelp(self, cmdargs):
+            self.chat.InfoPost("Commands:<br>/hclear : Clears out list<br>" +
+                        "/hstart : Sets you to segment 12, highest dex<br>" +
+                        "/hgo : Proceed to the next DEX/segment<br>" +
+                        "/hact <i>#</i> (full|half|abort|stunned)<br>" +
+                        "/hseg SEGNO : Set segment to SEGNO<br>" +
+                        "/hdex DEXNO : Set dex to DEXNO<br>" +
+                        "/hadd DEX SPD NAME : Adds to the list<br>" +
+                        "/hdel # : Delete character #<br>" +
+                        "/hown # playerid : Change ownership of PC # to playerid<br>" +
+                        "/hclear : Clears out player list<br>" +
+                        "/hchange # NEWDEX NEWSPD : Change PC # to have Dex NEWDEX and SPD NEWSPD<br>" +
+                        "/hset list_# state : Change player's state (state:" + self.statelist() + ")")
+
+    def on_hstart(self, cmdargs):
+        self.dexterity = self.highestdex()
+        self.segment = 12
+
+        for player in self.init_list:
+            player[3] = 4
+            if player[0] == self.dexterity:
+                player[3] == 2
+
+        self.chat.Post("Combat Starting.  ")
+        self.call_time()
+
+    def on_hact(self, cmdargs):
+        cmds = cmdargs.split(None)
+
+        if len(cmds) == 1:
+            msg = self.do_action(-1, -1, cmds[0])
+        elif len(cmds) == 2:
+            msg = self.do_action(-1, int(cmds[0]), cmds[1])
+        else:
+            msg = "Error in command. See format with hero init"
+
+        self.chat.InfoPost(msg)
+
+    def on_hseg(self, cmdargs):
+        cmds = cmdargs.split(None, 3)
+
+        try:
+            new_seg = int(cmds[0])
+            self.segment = new_seg
+            self.call_time()
+        except:
+            self.chat.SystemPost("Invalid format.  correct command is: /hseg SEGMENT#")
+
+    def on_hdex(self, cmdargs):
+        cmds = cmdargs.split(None, 3)
+
+        try:
+            new_dex = int(cmds[0])
+            self.dexterity = new_dex
+            self.call_time()
+        except:
+            self.chat.SystemPost("Invalid format.  correct command is: /hdex DEX#")
+
+    def on_hclear(self, cmdargs):
+        self.init_list = []
+        self.backup_list = []
+        self.dexterity = 99
+        self.segment = 12
+        self.chat.Post("<hr><font color='#ff0000'>New Initiative</font><br><font color='#0000ff'>Set new Initiatives</font>", True)
+
+    def on_hadd(self, cmdargs):
+        cmds = cmdargs.split(None, 3)
+        try:
+            if len(cmds) == 3:
+                new_dex = int(txt[0])
+                new_spd = int(txt[1])
+                self.init_list.append([new_dex, new_spd, cmds[2], 0, -1])
+                self.backup_list.append([new_dex, new_spd, cmds[2], 0, -1])
+                self.list_inits()
+            else:
+                self.chat.SystemPost("Invalid format.  correct command is: /hadd dex spd description (" + str(len(cmds)) + " arguments give)")
+
+        except:
+            self.chat.SystemPost("Invalid format.  correct command is: /hadd dex spd description")
+
+    def on_hdel(self, cmdargs):
+        try:
+            cmds = cmdargs.split(None, 1)
+            self.init_list.remove(int(cmds[0]))
+            self.backup_list.remove(int(cmds[0]))
+            self.list_inits()
+        except:
+            self.chat.SystemPost("Invalid format.  correct command is: /del list_number")
+
+    def on_hown(self, cmdargs):
+        try:
+            cmds = cmdargs.split(None)
+            player = self.init_list[int(cmds[0])]
+            player[4] = int(cmds[1])
+            self.chat.InfoPost("Character " + player[2] + "(" + int(cmds[0]) + ") is now owned by ID " + int(cmds[1]))
+        except:
+            self.chat.SystemPost("Invalid format.  correct command is: /hown # playerid")
+
+    def on_hlist(self, cmdargs):
+        self.list_inits()
+
+    def on_hsort(self, cmdargs):
+        self.init_list.sort()
+        self.backup_list.sort()
+
+        if cmdargs == "high":
+            self.init_list.reverse()
+            self.backup_list.reverse()
+
+        self.list_inits()
+
+    def on_hrun(self, cmdargs):
+        advance = True
+        nextlowest = 0
+        heldactions = False
+
+        for player in self.init_list:
+            if self.actson(player[1]. self.segment):
+                if player[3] == 5 or player[3] == 6:
+                    heltactions = True
+                continue
+
+            if player[3] == 1 or player[3] == 2:
+                self.chat.InfoPost("Hero " + player[2] + " needs to act before we advance DEX")
+                advance = False
+
+            elif player[3] == 3 or player[3] == 4 or player[3] == 7:
+                if player[0] > nextlowest:
+                    nextlowest = player[0]
+
+            elif player[3] == 5 or player[3] == 6:
+                heltactions = True
+
+        if not advance:
+            return
+
+        advanceseg = False
+
+        if nextlowest == 0 and self.dexterity != 0:
+            if heldactions or self.segment == 12:
+                msg = "End of Segment " + str(self.segment) + "."
+                if heldactions:
+                    msg += " There are held actions."
+
+                msg += "  Issue command again to advance to next segment."
+
+                self.chat.InfoPost(msg)
+                self.dexterity = 0
+                return
+            else:
+                advanceseg = True
+        elif nextlowest == 0 and self.dexterify == 0:
+            advanceseg = True
+
+        if advanceseg:
+            self.dexterity = nextlowest
+            for player in self.init_list:
+                if player[0] == self.dexterity:
+                    if player[3] == 3 or player[3] == 7:
+                        player[3] = 0
+                    elif player[3] == 4:
+                        player[3] = 2
+        else:
+            nextsegment = 0
+            while nextsegment == 0:
+                self.segment += 1
+                if self.segment == 13:
+                    self.segment = 1
+
+                elif self.segment == 12:
+                    nextsegment = 12
+                    break
+
+                for player in self.init_list:
+                    if self.actson(player[1], self.segment):
+                        if player[3] == 0 or player[3] == 3 or player[3] == 7:
+                            nextsegment = self.segment
+
+                    if player[3] == 5 or player[3] == 6:
+                        nextsegment = self.segment
+
+            self.dexterity = self.highestdex()
+            for player in self.init_list:
+                heldactions = False
+                if self.actson(player[1], self.segment):
+                    if player[3] != 3 and player[3] != 7:
+                        player[3] = 4
+
+                    if player[0] == self.dexterity:
+                        if player[3] == 3:
+                            self.chat.InfoPost(player[2] + " unstuns.")
+                            player[3] = 0
+                        elif player[3] == 7:
+                            self.chat.InfoPost(player[2] + " recovers from the abort")
+                            player[3] = 0
+                        else:
+                            player[3] = 2
+
+                    if player[3] == 5 or player[3] == 6 and self.dexterity == 0:
+                        self.chat.InfoPost("There are held actions. Issue command again to advance to next segment.")
+        self.call_time()
+
+    def on_hchange(self, cmdargs):
+        try:
+            cmds = cmdargs.split()
+            self.init_list[int(cmds[0])][0] = cmds[1]
+            self.init_list[int(cmds[0])][1] = cmds[2]
+            self.backup_list[int(cmds[0])][0] = cmds[1]
+            self.backup_list[int(cmds[0])][1] = cmds[2]
+            self.list_inits()
+        except:
+            self.chat.SystemPost('Invalid format.  correct command is: /hchange list_# new_dex new_spd (example: /hchange 1 18 4)')
+
+    def on_hset(self, cmdargs):
+        try:
+            cmds = cmdargs.split()
+            self.init_list[int(cmds[0])][3] = cmds[1]
+            self.backup_list[int(cmds[0])][3] = cmds[1]
+            self.list_inits()
+        except:
+            msg = "Invalid format.  correct command is: /hset list_# state (state:" + self.statelist() + ")"
+            self.chat.InfoPost(msg)
+
+    #Other Methods
+    def statelist(self):
+        msg = ""
+        stateindex = 0
+        for state in self.state_strings:
+            if stateindex != 0:
+                msg += ", "
+            msg += str(stateindex) + "=" + state
+            stateindex += 1
+        return msg
+
+    def is_index(self, value):
+        try:
+            if value[:1] == "+":
+                offset = 1
+            return True
+        except:
+            return False
+
+    def actson(self, spd, segment):
+        speeds=[[0,0,0,0,0,0,0,0,0,0,0,1],
+                [0,0,0,0,0,1,0,0,0,0,0,1],
+                [0,0,0,1,0,0,0,1,0,0,0,1],
+                [0,0,1,0,0,1,0,0,1,0,0,1],
+                [0,0,1,0,1,0,0,1,0,1,0,1],
+                [0,1,0,1,0,1,0,1,0,1,0,1],
+                [0,1,0,1,0,1,1,0,1,0,1,1],
+                [0,1,1,0,1,1,0,1,1,0,1,1],
+                [0,1,1,1,0,1,1,1,0,1,1,1],
+                [0,1,1,1,1,1,0,1,1,1,1,1],
+                [0,1,1,1,1,1,1,1,1,1,1,1],
+                [1,1,1,1,1,1,1,1,1,1,1,1]]
+
+        if spd < 1 or spd > 12:
+            return False
+
+        if speeds[spd-1][segment-1] == 1:
+            return True
+
+        return False
+
+    def highestdex(self):
+        dex = 0
+        for player in self.init_list:
+            if self.actson(player[1], self.segment) and player[0] > dex:
+                dex = player[0]
+        return dex
+
+    def do_action(self, playerid, index, action):
+        if index == -1:
+            count = 1
+            for player in self.init_list:
+                if player[4] == playerid:
+                    index = count
+                count += 1
+
+        if index == -1:
+            return "You do not have any players in the combat list"
+
+        index -= 1
+        player = self.init_list[index]
+        if playerid != -1 and playerid != player[4]:
+            return "You do not own that player."
+
+        # You can only perform a full action if you have a full or held full
+        if action == "full":
+            if player[3] != 2 and player[3] != 5:
+                msg = player[2] + " cannot perform a full action."
+            else:
+                player[3] = 0
+                msg = player[2] + " performs a full action."
+        elif action == "half":
+            if player[3] != 1 and player[3] != 2 and player[3] != 5 and player[3] != 6:
+                msg = player[2] + " cannot perform a half action."
+            else:
+                if player[3] == 1 or player[3] == 6:
+                    player[3] = 0
+                    msg = player[2] + " performs a half action."
+                else:
+                    player[3] = 1
+                    msg = player[2] + " performs a half action (half remaining)"
+        elif action == "hold":
+            if player[3] != 1 and player[3] != 2:
+                msg = player[2] + " cannot hold an action."
+            else:
+                if player[3] == 2:
+                    player[3] = 5
+                    msg = player[2] + " holding a full action."
+                else:
+                    player[3] = 6
+                    msg = player[2] + " holding a half action."
+        elif action == "stunned":
+            player[3] = 3
+            msg = player[2] + " stunned!"
+        elif action == "abort":
+            # abort if full/half/not gone/held.  Those just lose this seg action
+            if player[3] == 1 or player[3] == 2 or player[3] == 4 or player[3] == 5 or player[3] == 6:
+                player[3] = 0
+                msg = player[2] + " aborts!"
+            # if you have 0 remaining and did not act this seg, regular abort
+            elif player[3] == 0 and not self.actson(player[1], self.segment):
+                player[3] = 7
+                msg = player[2] + " aborts!"
+            else:
+                msg = player[2] + " cannot abort yet."
+        else:
+            msg = "Unknown command."
+
+        return msg
+
+    def list_inits(self, player=0, send=False):
+        msg = "Combat Turn:<br>"
+        msg += "<table border=1 cellspacing=1 cellpadding=1><tr><th><th></th></th><th></th><th></th><th></th><th></th><th colspan=12>Segments</th></tr>"
+        msg += "<tr><th>#</th><th>Owner</th><th>Name</th><th>Spd</th><th>Dex</th><th>State</th>"
+        for x in xrange(1,13):
+            msg += "<th>" + str(x) + "</th>"
+        msg += "</tr>"
+        count=1
+        for m in self.init_list:
+            msg += "<tr><td align=center>"+str(count)+"</td>"
+            if m[4]==-1:
+                msg += "<td><font color=red>GM</font></td>"
+            else:
+                msg += "<td>" + m[4] + "</td>"
+            msg += "<td><font color='#0000ff'>" + m[2] + "</font></td>"
+            msg += "<td align=center><font color='#0000ff'>" + str(m[1]) + "</font></td>"
+            msg += "<td align=center><font color='#0000ff'>" + str(m[0]) + "</font></td>"
+            msg +="<td>" + self.state_strings[m[3]] + "</td>"
+
+            for segment in xrange(1,13):
+                if self.actson(m[1],segment):
+                    msg += "<td align=center>" + str(m[0]) + "</td>"
+                else:
+                    msg += "<td></td>"
+            msg += "</tr>"
+            count += 1
+        msg += "</table><br>"
+
+        msg += "It is currently Segment " + str(v.segment) + ", DEX " + str(v.dexterity) + "<br>"
+
+        if send and player != 0:
+            chat.whisper_to_players(msg, [player])
+        else:
+            self.chat.InfoPost(msg)
+
+    def call_time(self):
+        plist = ""
+        msg = "Segment is now " + str(self.segment) + ", DEX " + str(self.dexterity)
+        for player in self.init_list:
+            if player[3] == 1 or player[3] == 2 or player[3] == 5 or player[3] == 6:
+                if plist != "":
+                    plist += ", "
+                plist += player[2] + "(" + self.state_strings[player[3]] + ")"
+        if plist != "":
+            msg += ": " + plist
+        self.chat.Post(msg, True)
+
+    def process_user(self, command, player):
+        if command == "hadd":
+            txt = command.split(None, 3)
+            try:
+                if len(txt) == 4:
+                    new_dex = int(txt[1])
+                    new_spd = int(txt[2])
+                    self.init_list.append([new_dex, new_spd, txt[3], 0, player[2]])
+                    self.backup_list.append([new_dex, new_spd, txt[3], 0, player[2]])
+                    msg = "Character " + txt[3] + " added to initiative."
+                else:
+                    msg = "Error in command.  See format with heroinit"
+            except:
+                msg = "Error in command.  See format with heroinit"
+
+            self.chat.whisper_to_players(msg, [player[2]])
+
+        elif command == "hact":
+            txt = command.split(None)
+            if len(txt) == 2:
+                index = -1
+                action = txt[1]
+                msg = self.do_action(player[2], index, action)
+            elif len(txt) == 3:
+                index = int(txt[1])
+                action = txt[2]
+                msg = self.do_action(player[2], index, action)
+            else:
+                msg = "Error in command.  See format with heroinit"
+
+            self.chat.whisper_to_players(msg, [player[2]])
+
+        elif command == "hlist":
+            self.list_inits(player[2], True)
+
+        else:
+            msg = "Commands:<br>heroinit (hadd DEX SPEED CHARACTERNAME) : add character CHARACTERNAME with dex DEX and speed SPD to initiative list<br>"
+            msg += " Example:  heroinit (hadd 18 4 Joey)<br>"
+            msg += "heroinit (hact # (full|half|hold|stunned|abort) : Have character # perform an action.  # is the number from hlist.  If ommited, uses first owned character<br>"
+            msg += " Example:  heroinit (hact 1 full)   : Make character index 1 perform a full action.<br>"
+            msg += " Example:  heroinit (hact full)     : Have your first owned character perform a full action<br>"
+
+            self.chat.whisper_to_players(msg, [player[2]])
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxhiddendice.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,55 @@
+import os
+import re
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = 'Hidden Dice'
+        self.author = 'mDuo13'
+        self.help = 'Roll with curly brackets to hide your roll,\n'
+        self.help += 'having it display only for you. Other players will\n'
+        self.help += 'get a message that only says you are rolling.\n'
+        self.help += 'Useful for GMs who want to roll secretly.'
+
+        #You can set variables below here. Always set them to a blank value in this section. Use plugin_enabled
+        #to set their proper values.
+        self.hiddenrolls = []
+        self.dicere = "\{([0-9]*d[0-9]*.+)\}"
+
+
+    def plugin_enabled(self):
+        pass
+
+
+    def plugin_disabled(self):
+        pass
+
+
+    def pre_parse(self, text):
+        m = re.search(self.dicere, text)
+        while m:
+            roll = "[" + m.group(1) + "]"
+            self.hiddenrolls += [self.chat.ParseDice(roll)]
+            text = text[:m.start()] + "(hidden roll)" + text[m.end():]
+            m = re.search(self.dicere, text)
+        return text
+
+    def post_msg(self, text, myself):
+        print "post_msg:\n\t" + text
+        c = 0
+        a = text.find("(hidden roll)")
+
+        while len(self.hiddenrolls) > c and a > -1:
+            text = text[:a+14].replace("(hidden roll)", self.hiddenrolls[c]) + text[a+14:]
+            a = text.find("(hidden roll)")
+            c += 1
+        if c > 0:
+            self.hiddenrolls = []
+        return text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxinit.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,187 @@
+import os
+import orpg.pluginhandler
+from string import find, replace
+import orpg.dirpath
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    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 = 'Initiative Tool'
+        self.author = 'Woody, Darloth, updated by mDuo13'
+        self.help = "This is the ever-popular init tool. To learn how to use it, type\n"
+        self.help += "'/init help'. It will load the a help node into your game tree."
+
+        self.toggle = ''
+        self.init_list = ''
+        self.backup_list = ''
+        self.tool_type = ''
+        self.wod_counter = ''
+
+    def plugin_enabled(self):
+        #This is where you set any variables that need to be initalized when your plugin starts
+
+        self.plugin_addcommand('/init', self.on_init, '[help|type|clear|new|start|add|del|list|sortlow|sorthigh|run|go|change] - Init tool. Use /init help to get details about how to use the tool')
+
+
+        self.toggle = 1
+        self.init_list = []
+        self.backup_list = []
+        self.tool_type = 'std'
+        self.wod_counter = 0
+
+    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
+        #such as closing windows created by the plugin
+        self.plugin_removecmd('/init')
+
+    def on_init(self, cmdargs):
+        #this is just an example function for a command you create create your own
+        args = cmdargs.split(None,-1)
+
+        if len(args) == 0:
+            if self.toggle:
+                self.toggle = 0
+                self.post_my_msg("<font color='#ff0000'>Init recording off</font>")
+            else:
+                self.post_my_msg("<font color='#ff0000'>Init recording on</font>")
+                self.toggle = 1
+        elif args[0] == 'help':
+            f = open(orpg.dirpath.dir_struct["plugins"]+ "inittool.xml","r")
+            self.gametree.insert_xml(f.read())
+            f.close()
+        elif args[0] == 'type':
+            if len(args) == 2:
+                if args[1] == 'std' or args[1] == 'wod' or args[1] == '3e' or args[1] == 'srun':
+                    self.tool_type = args[1]
+                    self.post_my_msg("<font color='#ff0000'>Initiative tool now set to '" + self.tool_type + "'</font>")
+                else:
+                    self.post_my_msg("<font color='#ff0000'>Unknown Initiative tool type: " + args[1])
+            else:
+                self.chat.Post("<font color='#ff0000'>currently using the '" + self.tool_type + "' Initiative tool type</font>")
+        elif args[0] == 'clear' or args[0] == 'new' or args[0] == 'start':
+            self.init_list = []
+            self.backup_list = []
+            self.post_my_msg("<hr><font color='#ff0000'>New Initiative</font><br /><font color='#0000ff'>Roll new Initiatives</font>",1)
+        elif args[0] == 'add':
+            try:
+                new_init = int(args[1])
+                self.init_list += [[new_init, args[2]]]
+                self.backup_list += [[new_init, arge[2]]]
+                self.list_inits()
+            except:
+                self.post_my_msg("<font color='#ff0000'>Invalid format.  correct command is: /add init_number description</font>")
+        elif args[0] == 'del':
+            try:
+                del self.init_list[int(args[1])-1]
+                del self.backup_list[int(args[1])-1]
+                self.list_inits()
+            except:
+                self.post_my_msg("<font color='#ff0000'>Invalid format.  correct command is: /del list_number</font>")
+        elif args[0] == 'list':
+            self.list_inits()
+        elif args[0] == 'backuplist':
+            self.list_backups()
+        elif args[0] == 'sortlow':
+            self.init_list.sort()
+            self.backup_list.sort()
+            self.list_inits()
+        elif args[0] == 'sorthigh':
+            self.init_list.sort()
+            self.init_list.reverse()
+            self.backup_list.sort()
+            self.backup_list.reverse()
+            self.list_inits()
+        elif args[0] == 'run' or args[0] == 'go':
+            if len(self.init_list):
+                id = str(self.init_list[0][0])
+                player = str(self.init_list[0][1])
+                del self.init_list[0]
+                self.post_my_msg("<hr><font color='#ff0000'>Next init:</font><br /><font color='#0000ff'><b>("+id+")</b>: "+player+"</font>",1)
+            else:
+                if self.tool_type == 'std' or (self.tool_type == 'wod' and self.wod_counter == 1):
+                    self.backup_list = []
+                    self.init_list = []
+                    self.wod_counter = 0
+                    self.post_my_msg("<hr><font color='#ff0000'>End of Initiative Round</font>",1)
+                elif self.tool_type == '3e':
+                    self.init_list += self. backup_list
+                    self.post_my_msg("<hr><font color='#ff0000'>End of Initiative Round, Starting New Initiative Round</font>",1)
+                elif self.tool_type == 'wod' and self.wod_counter == 0 and len(self.backup_list) > 0:
+                    self.post_my_msg("<hr><font color='#ff0000'>Starting physical initiatives:</font>",1)
+                    self.wod_counter = 1
+                    self.init_list = self.backup_list
+                    self.init_list.sort()
+                    self.init_list.reverse()
+                elif self.tool_type == 'srun':
+                    for m in self.backup_list[:]:
+                        m[0] -= 10
+                        if m[0] <= 10:
+                            self.backup_list.remove(m)
+                    if len(self.backup_list):
+                        self.post_my_msg("<hr><font color='#ff0000'>End of Initiative Pass, starting next Pass</font>",1)
+                        self.init_list += self.backup_list
+                    else:
+                        self.post_my_msg("<hr><font color='#ff0000'>End of Combat Turn, roll new initiatives please</font>",1)
+                        self.init_list = []
+                        self.backup_list = []
+        elif args[0] == 'change':
+            try:
+                id = int(args[1])
+                new_init = int(args[2])
+                self.init_list[id][0] = new_init
+                self.backup_list[id][0] = new_init
+                self.list_inits()
+            except:
+                self.post_my_msg("<font color='#0000ff'>Invalid format.  correct command is: /change list_# new_init_# (example: /change 1 4)</font>")
+        else:
+            self.post_my_msg("<font color='#0000ff'>Invalid Command, type /init help and read the manual please</font>")
+
+    def post_msg(self, text, myself):
+        if self.toggle:
+            if myself == 1:
+                if text.lower().find("init") != -1:
+                    player = text[:text.find("[")]
+                    init = text[text.rfind("(")+1:text.rfind(")")]
+            else:
+                if text.lower().find("init") != -1:
+                    player=text[text.find("</B>")+4:text.find("[")]
+                    init=text[text.rfind("(")+1:text.rfind(")")]
+            try:
+                if text.lower().find("init") != -1:
+                    init = int(init)
+                    self.init_list += [[init,player]]
+                    self.backup_list += [[init,player]]
+            except:
+                pass
+        return text
+
+
+    def post_my_msg(self, msg, send=0):
+        tmp = self.toggle
+        self.toggle = 0
+        self.chat.Post(msg,send)
+        self.toggle = tmp
+
+
+    def list_inits(self):
+        msg = 'Initiatives:<br />'
+        for m in self.init_list:
+            msg += " <font color='#ff0000'>" + str(self.init_list.index(m) + 1) + "</font>"
+            msg += ": <font color='#0000ff'>(" + str(m[0]) + ") "
+            msg += m[1] + "</font><br />"
+        self.post_my_msg(msg)
+
+
+    def list_backups(self):
+        msg = 'backup list:<br />'
+        for m in self.backup_list:
+            msg += " <font color='#ff0000'>" + str(self.backup_list.index(m) + 1) + "</font>"
+            msg += ": <font color='#0000ff'>(" + str(m[0]) + ") "
+            msg += m[1] + "</font><br />"
+        self.post_my_msg(msg)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxinit2.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,2322 @@
+# 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.
+#
+##############################################################################
+
+from string import find, replace
+import wx
+
+import os
+import orpg.dirpath
+import orpg.orpg_version
+import orpg.plugindb
+import orpg.pluginhandler
+import hashlib
+import random
+
+__version__ = "2.2.8"
+
+class v:
+    init_list = []
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+
+        self.name = "Initiative Tool " + str(__version__)
+        self.author = "Woody, Darloth, updated by mDuo13, Veggiesama, magoo"
+        self.help =  "Type /inittool2 to load a help node into your game tree. Check out\n"
+        self.help += "the readme, and double-click Commands & Help if you want to see examples."
+
+        self.version = __version__
+        self.orpg_min_version="1.7.0"
+
+
+    def plugin_enabled(self):
+        # Setup variables.
+        self.loaded = 0     #  1 = config has been loaded on startup
+        self.init_debug = 0
+        v.init_list = [] # now an array of "Dict" (associative array):
+                       #   name : name of the effect of character
+                       #   type : 0 = character; 1 = effect
+                       #   init : init score
+                       #   duration : round remaining to the effect
+                       #   hidden : type 0 = shows up in public list only on init
+                       #            type 1 = does not print to public remaing rounds of effect
+                       #   pass : for SR4
+                       #   rank : when multiple character at same init count, the plugin
+                       #          use the 'rank' to order them
+                       #   tag  : unique tag assigned to each entry.
+        self.init_round = 1
+        self.init_recording = 1
+        self.init_title = ""
+        self.init_count = 0
+        self.init_pass = 0
+        self.init_listslot = 0
+        self.init_listtag = ""
+        self.init_end = 0
+        self.init_system = "D20" # D20 (default), SR4, RQ
+        self.sandglass_count = 0 # current count of the number of seconds of a character's turn
+                            # Well, more or less true, it is a count of the number of time the function
+                            # refresh_counter() has been called, which itself is supposed to be call each seconds.
+        self.sandglass_delay = 0 # 0 = off, anything else is the timer delay
+        self.sandglass_status = 0# 0 = running, 1 = pause
+        self.sandglass_skip_interval = 0 # after a number of interval, do a init_next(). 0 = disable
+        self.autosave_delay  = 60 # 0 = off, else, will autosave every 'autosave' seconds - VEG 2.2.8 (set to 60)
+        self.autosave_count  = 0 # current count of the number of seconds since last autosave
+        self.hide_id       = 0 # 1 = do not show IDs to everyone; 0 = show IDs to everyone
+        self.hide_ondeck   = 0 # 1 = do not show "on deck" message after every turn; 0 = show it
+        self.hide_justmovement = 0 # [SR4] 1 = do not show "just movement" messages
+        self.message_nowup = "NEXT UP FOR THE KILLING" # customizable "now up" message
+        self.msghand_timer = 0 # 0-60 = cycles the message handler to all players once every 60 seconds - VEG 2.2.6
+        self.msghand_warning = 1 # 1 = manual override, removes the message warning if someone else in room has xxinit2 loaded - VEG 2.2.6
+        self.reroll_type = 0 # 0 = reroll every round (normal), 1 = no reroll and keep last round inits - VEG 2.2.7
+        self.ip_order = 0 # 0 = 1/2/3/4 (normal), 1 = 3/1/4/2 (Serbitar's house rule), 2 = 4/1/3/2 (TinkerGnome's house rule) - VEG 2.2.7
+
+        # Add commands
+        self.plugin_addcommand("/inittool2",            self.CMD_inittool2,             "Loads the help/example node into your game tree")
+        self.plugin_addcommand("/inittoggle",           self.CMD_inittoggle,            "Toggles initiative system (D20/SR4/RuneQuest")
+        self.plugin_addcommand("/initdebug",            self.CMD_initdebug,             "???")
+        self.plugin_addcommand("/initgui",              self.CMD_initgui,               "???")
+        #self.plugin_addcommand("/initsaveconfig",       self.CMD_initsaveconfig,        "Saves current configuration") #VEG 2.2.6
+        #self.plugin_addcommand("/initloadconfig",       self.CMD_initloadconfig,        "Loads a previous configuration") #VEG 2.2.6
+        self.plugin_addcommand("/initdefaultconfig",    self.CMD_initdefaultconfig,     "Loads the default configuration")
+        self.plugin_addcommand("/initshowconfig",       self.CMD_initshowconfig,        "Displays current configuration")
+        self.plugin_addcommand("/togglehideid",         self.CMD_togglehideid,          "Shows IDs in a public list")
+        self.plugin_addcommand("/togglehideondeck",     self.CMD_togglehideondeck,          "Shows 'On Deck' message after every turn")
+        self.plugin_addcommand("/togglehidejustmovement", self.CMD_togglehidejustmovement,  "Shows 'Just Movement' messages in SR4")
+        self.plugin_addcommand("/init",                 self.CMD_init,                  "Toggles whether to record typed intiativies and textual initiative commands")
+        self.plugin_addcommand("/clear",                self.CMD_clear,                 "Clears current initiative list")
+        self.plugin_addcommand("/start",                self.CMD_start,                 "Clears current initiative list and prompts players to roll")
+        self.plugin_addcommand("/addfxhidden",          self.CMD_addfxhidden,           "Adds a hidden effect to the list")
+        self.plugin_addcommand("/addfx",                self.CMD_addfx,                 "Adds an effect to the list")
+        self.plugin_addcommand("/addhidden",            self.CMD_addhidden,             "Adds a hidden character to the list")
+        self.plugin_addcommand("/add",                  self.CMD_add,                   "Adds a character to the list")
+        self.plugin_addcommand("/del",                  self.CMD_del,                   "Deletes a character/effect from the list")
+        self.plugin_addcommand("/changepass",           self.CMD_changepass,            "Changes current initiative pass [SR4-only]")
+        self.plugin_addcommand("/changedur",            self.CMD_changedur,             "Changes duration of specific effect")
+        #self.plugin_addcommand("/changefx",             self.CMD_changefx,              "Changes the initiative count of a specific effect")
+        self.plugin_addcommand("/change",               self.CMD_change,                "Changes the initiative count of a specific character")
+        self.plugin_addcommand("/togglehidden",         self.CMD_togglehidden,          "Toggles whether to show hidden characters in initiative list or not")
+        self.plugin_addcommand("/initsetslot",          self.CMD_initsetslot,           "Sets the initiative turn to the designated list slot")
+        self.plugin_addcommand("/rankup",               self.CMD_rankup,                "Alters turn priority of one character/effect in the same initiative count as other characters/effects (higher)")
+        self.plugin_addcommand("/rankdown",             self.CMD_rankdown,              "Alters turn priority of one character/effect in the same initiative count as other characters/effects (lower)")
+        self.plugin_addcommand("/initdelaynow",         self.CMD_initdelaynow,          "???")
+        self.plugin_addcommand("/sandglass",            self.CMD_sandglass,             "Toggles sandglass functionality and sets duration of timer")
+        self.plugin_addcommand("/sandglasspause",       self.CMD_sandglasspause,        "Pauses the sandglass")
+        self.plugin_addcommand("/sandglassresume",      self.CMD_sandglassresume,       "Resumes the sandglass from pause")
+        self.plugin_addcommand("/sandglassforceskip",   self.CMD_sandglassforceskip,    "Forces the turn to skip after a number of sandglass duration intervals")
+        self.plugin_addcommand("/list",                 self.CMD_list,                  "Displays a list of characters, effects, and turn order to yourself")
+        self.plugin_addcommand("/publiclist",           self.CMD_publiclist,            "Displays a list of characters, effects, and turn order to the room")
+        self.plugin_addcommand("/rnd",                  self.CMD_rnd,                   "Displays the current round")
+        self.plugin_addcommand("/pass",                 self.CMD_pass,                  "Displays the current pass [SR4-only]")
+        self.plugin_addcommand("/go",                   self.CMD_go,                    "Advances the turn order to the next character/effect")
+        self.plugin_addcommand("/initsave",             self.CMD_initsave,              "Saves current list of initiatives")
+        self.plugin_addcommand("/initload",             self.CMD_initload,              "Loads previous list of initiatives")
+        self.plugin_addcommand("/initautosave",         self.CMD_initautosave,          "Toggles autosave")
+        self.plugin_addcommand("/initautoload",         self.CMD_initautoload,          "Loads previous autosave of initiative")
+        self.plugin_addcommand("/nowupmsg",             self.CMD_nowupmsg,              "Customize your own Now Up Message")
+        self.plugin_addcommand("/msghandwarning",       self.CMD_msghandwarning,        "Toggles the message handler warning when multiple xxinit2's are loaded in the room") #VEG 2.2.6
+        self.plugin_addcommand("/toggleroundreroll",    self.CMD_toggleroundreroll,     "Toggles method of round-by-round initiative in SR4") #VEG 2.2.7
+        self.plugin_addcommand("/toggleiporder",        self.CMD_toggleiporder,         "Toggles order of IP execution in SR4") #VEG 2.2.7
+
+        # Add message handlers
+        self.plugin_add_msg_handler("xxinit2_checkifloaded", self.received_checkifloaded) #VEG 2.2.6
+        self.plugin_add_msg_handler("xxinit2_returnloaded", self.received_returnloaded) #VEG 2.2.6
+
+        self.init_load(0) #VEG 2.2.8 - auto-loads last autosave at startup (helps if you crash)
+
+    def plugin_disabled(self):
+        self.plugin_removecmd("/inittool2")
+        self.plugin_removecmd("/inittoggle")
+        self.plugin_removecmd("/initdebug")
+        self.plugin_removecmd("/initgui")
+        #self.plugin_removecmd("/initsaveconfig") #VEG 2.2.6
+        #self.plugin_removecmd("/initloadconfig") #VEG 2.2.6
+        self.plugin_removecmd("/initdefaultconfig")
+        self.plugin_removecmd("/initshowconfig")
+        self.plugin_removecmd("/togglehideid")
+        self.plugin_removecmd("/togglehideondeck")
+        self.plugin_removecmd("/togglehidejustmovement")
+        self.plugin_removecmd("/init")
+        self.plugin_removecmd("/clear")
+        self.plugin_removecmd("/start")
+        self.plugin_removecmd("/addfxhidden")
+        self.plugin_removecmd("/addfx")
+        self.plugin_removecmd("/addhidden")
+        self.plugin_removecmd("/add")
+        self.plugin_removecmd("/del")
+        self.plugin_removecmd("/changepass")
+        self.plugin_removecmd("/changedur")
+        #self.plugin_removecmd("/changefx")
+        self.plugin_removecmd("/change")
+        self.plugin_removecmd("/togglehidden")
+        self.plugin_removecmd("/initsetslot")
+        self.plugin_removecmd("/rankup")
+        self.plugin_removecmd("/rankdown")
+        self.plugin_removecmd("/initdelaynow")
+        self.plugin_removecmd("/sandglass")
+        self.plugin_removecmd("/sandglasspause")
+        self.plugin_removecmd("/sandglassresume")
+        self.plugin_removecmd("/sandglassforceskip")
+        self.plugin_removecmd("/list")
+        self.plugin_removecmd("/publiclist")
+        self.plugin_removecmd("/rnd")
+        self.plugin_removecmd("/pass")
+        self.plugin_removecmd("/go")
+        self.plugin_removecmd("/initsave")
+        self.plugin_removecmd("/initload")
+        self.plugin_removecmd("/initautosave")
+        self.plugin_removecmd("/initautoload")
+        self.plugin_removecmd("/nowupmsg")
+        self.plugin_removecmd("/msghandwarning")
+        self.plugin_removecmd("/toggleroundreroll")
+        self.plugin_removecmd("/toggleiporder")
+
+        self.plugin_delete_msg_handler("xxinit2_checkifloaded") #VEG 2.2.6
+        self.plugin_delete_msg_handler("xxinit2_returnloaded") #VEG 2.2.6
+
+        self.loaded = 0
+
+        try:
+            self.frame.Destroy()
+        except:
+            pass
+
+    # Just received a query from another player; returning a "yes, I have xxinit2 loaded" message
+    def received_checkifloaded(self, playerid, data, xml_dom): #VEG 2.2.6
+        #print "receive_checkifloaded called: id=" + str(playerid) + "... data = " + str(data)
+        self.plugin_send_msg(playerid, "<xxinit2_returnloaded/>")
+
+    # Just received the "yes, I have xxinit2 loaded" message; printing a warning message
+    def received_returnloaded(self, playerid, data, xml_dom): #VEG 2.2.6
+        if self.msghand_warning:
+            self.post_syserror("Warning: xxinit2 plugin detected on "
+                               + str(self.session.get_player_by_player_id(playerid)[0]) + "'s system. \
+                               Having multiple copies of the xxinit2 plugin in the same room is not \
+                               advisable, because it may cause problems. It is suggested that only \
+                               one person in the room (the GM) have the xxinit2 plugin loaded. Go to \
+                               Tools-Plugins and uncheck the Initiative Tool 2.x to unload it. Type \
+                               /msghandwarning to simply turn this warning reminder off.")
+
+    def post_msg(self, text, myself):
+        #This is called whenever a message from anyone is about to be posted
+        #to chat; it doesn't affect the copy of the message that gets sent to others
+        #Be careful; system and info messages trigger this too.
+        if self.init_recording:
+            if text.lower().find("showinits") != -1:
+                self.inits_list(1)
+                return text
+            elif text.lower().find("nextinit") != -1:
+                self.init_next()
+                return text
+            # all collected, certain values only valid in following systems:
+            # d20: player, init, effect, duration
+            # sr4: player, init, passes
+            # VEG 2.2.7 - The lines that "won't work with macros" are unable to function properly because of the double-layers
+            #             of <font> tags that accompany macro sends. The string parsing gets screwed up. It's probably fixable
+            #             without changing the way macros are handled, but it's beyond my skill level. =( =( =(
+            if myself==1:
+                if text.find(" effect ") != -1:
+                    effect=text[text.rfind("'>")+2:text.find("[")]
+                    duration=text[text.rfind("effect ")+7:text.find("</font>")]
+                    init=text[text.rfind("=> ")+3:text.rfind(" effect")]
+                elif text.find(" init") != -1:
+                    player=text[:text.find("[")]
+                    passes=text[text.find("init ")+5:text.find("</font>")]
+                    init=text[text.rfind("(")+1:text.rfind(")")]
+            else:
+                if text.find(" effect ") != -1:
+                    effect=text[text.find("</b>: ")+6:text.find("[")]
+                    duration=text[text.find("effect ")+7:-7]
+                    init=text[text.rfind("=> ")+3:text.rfind(" effect")]
+                elif text.find(" init") != -1:
+                    player=text[text.find("</b>: ")+6:text.find("[")]
+                    passes=text[text.find("init ")+5:-7]
+                    init=text[text.rfind("(")+1:text.rfind(")")]
+            try:
+                if self.isD20() or self.isRQ():
+                    if text.find(" effect ") != -1:
+                        init=int(init)
+                        duration=int(duration)
+                        self.init_add([init,duration,effect], 1, 0)
+                    elif text.find(" init") != -1:
+                        init=int(init)
+                        self.init_add([init,player], 0, 0)
+                elif self.isSR4():
+                    if text.find(" init ") != -1:
+                        init=int(init)
+                        passes=int(passes)
+                        self.init_add([init,passes,player], 0, 0)
+            except:
+                print("Detected some sort of initiative input... failed!")
+
+        return text
+
+
+    def refresh_counter(self):
+        #This is called once per second. That's all you need to know.
+        # load system config once at startup
+        if not self.loaded:
+            self.loaded = 1
+            self.config_load()
+            self.config_show()
+            self.check_version()
+
+        # do sandglass stuff it has to do
+        if not self.isEnded():
+           self.sandglass()
+
+        # do the autosaving stuff if needed
+        self.autosave()
+
+        # VEG 2.2.6
+        self.msghand_timer += 1
+        if self.msghand_timer >= 60: # cycle playerlist only 1x every 60 sec
+            self.msghand_timer = 0
+            self.plugin_send_msg("all", "<xxinit2_checkifloaded/>")
+
+    ###############################################################
+    ####    Command Functions
+    ###############################################################
+
+    def CMD_inittool2(self, cmdargs):
+        f = open(orpg.dirpath.dir_struct["plugins"]+ "inittool2.xml","r")
+        f2 = open(orpg.dirpath.dir_struct["plugins"]+ "inittool2_player.xml","r")
+        self.gametree.insert_xml(f.read())
+        self.gametree.insert_xml(f2.read())
+        f.close()
+        f2.close()
+        return 1
+
+    def CMD_inittoggle(self, cmdargs):
+        if self.isSR4():
+            self.init_system = "RQ"
+            self.init_clear()
+            self.post_sysmsg("Now using <font color='#0000ff'><i>RuneQuest</font></i> system.", 1)
+        elif self.isD20():
+            self.init_system = "SR4"
+            self.init_clear()
+            self.post_sysmsg("Now using <font color='#0000ff'><i>Shadowrun 4th</font></i> system.", 1)
+        elif self.isRQ():
+            self.init_system = "D20"
+            self.init_clear()
+            self.post_sysmsg("Now using <font color='#0000ff'><i>d20</font></i> system.", 1)
+        self.config_save() # VEG 2.2.6
+
+    def CMD_initdebug(self, cmdargs):
+       cmdargs = cmdargs.split(None,-1)
+       if cmdargs[0] == "on":
+          self.init_debug = 1
+          self.post_sysmsg("Init debugging <font color='#0000ff'><i>on</i></font>.",1)
+       elif cmdargs[0] == "off":
+          self.init_debug = 0
+          self.post_sysmsg("Init debugging <font color='#0000ff'><i>off</i></font>.",1)
+
+    #def CMD_initsaveconfig(self, cmdargs):
+    #    self.config_save()
+
+    #def CMD_initloadconfig(self, cmdargs):
+    #    self.config_load()
+
+    def CMD_initdefaultconfig(self, cmdargs):
+        self.config_default()
+
+    def CMD_initshowconfig(self, cmdargs):
+        self.config_show()
+
+    def CMD_togglehideid(self, cmdargs):
+        self.hide_id = not self.hide_id
+        self.post_sysmsg("Hide IDs: " + str(self.hide_id), 1)
+        self.config_save() # VEG 2.2.6
+
+    def CMD_togglehideondeck(self, cmdargs):
+        self.hide_ondeck = not self.hide_ondeck
+        self.post_sysmsg("Hide 'On Deck': " + str(self.hide_ondeck), 1)
+        self.config_save() # VEG 2.2.6
+
+    def CMD_togglehidejustmovement(self, cmdargs):
+        self.hide_justmovement = not self.hide_justmovement
+        self.post_sysmsg("Hide 'Just Movement' [SR4-only]: " + str(self.hide_justmovement), 1)
+        self.config_save() # VEG 2.2.6
+
+    def CMD_init(self, cmdargs):
+        if self.init_recording:
+            self.init_recording=0
+            self.post_sysmsg("Init recording <font color='#0000ff'><i>off</i></font>.")
+        else:
+            self.init_recording=1
+            self.post_sysmsg("Init recording <font color='#0000ff'><i>on</i></font>.")
+        self.config_save() # VEG 2.2.6
+
+    def CMD_clear(self, cmdargs):
+        self.init_clear()
+        self.post_sysmsg("Clearing Initiative List !", 1)
+
+    def CMD_start(self, cmdargs):
+        self.init_clear()
+        self.post_my_msg("<hr><font color='#ff0000'><b>Starting Round</font> <font color='#0000ff'># <i>1</font> [" + self.init_system + "]</b>", 1)
+        self.post_my_msg("<font color='#0000ff'><b>Roll New Initiatives</b></font><hr>", 1)
+
+    def CMD_addfxhidden(self, cmdargs):
+        cmdargs = cmdargs.split(None,2)
+        if self.isD20() or self.isRQ():
+            self.init_add(cmdargs, 1, 1)
+        else:
+            self.post_syserror("Invalid input. Effects and durations do not work with " + str(self.init_system) + " system.")
+
+    def CMD_addfx(self, cmdargs):
+        cmdargs = cmdargs.split(None,2)
+        if self.isD20() or self.isRQ():
+            self.init_add(cmdargs, 1, 0)
+        else:
+            self.post_syserror("Invalid input. Effects and durations do not work with " + str(self.init_system) + " system.")
+
+    def CMD_addhidden(self, cmdargs):
+        if self.isD20() or self.isRQ():
+            cmdargs = cmdargs.split(None,1)
+        elif self.isSR4():
+            cmdargs = cmdargs.split(None,2)
+        self.init_add(cmdargs, 0, 1)
+
+    def CMD_add(self, cmdargs):
+        if self.isD20() or self.isRQ():
+            cmdargs = cmdargs.split(None,1)
+        elif self.isSR4():
+            cmdargs = cmdargs.split(None,2)
+        self.init_add(cmdargs, 0, 0)
+
+    def CMD_del(self, cmdargs):
+        id = int(cmdargs)
+        self.init_delete(id-1, 1) # subtract 1 because public lists shows with +1
+
+    def CMD_changepass(self, cmdargs):
+        cmdargs = cmdargs.split(None,-1)
+        if self.isSR4():
+            try:
+                id       = int(cmdargs[0])-1 # subtract 1 because public lists shows with +1
+                new_pass = int(cmdargs[1])
+                if new_pass < 1 or new_pass > 4:
+                    self.post_syserror("Invalid input [" + str(self.init_system) + "]. Passes must be greater than/equal to 1 and less than/equal to 4.")
+                else:
+                    v.init_list[id]["passes"] = new_pass
+                    self.post_sysmsg("<font color='#0000ff'><i>" + str(v.init_list[id]["name"]) + "</font></i>'s initiative \
+                        pass total has been changed to <font color='#0000ff'><i>" + str(new_pass) + "</font></i> passes !", 1)
+            except:
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. That command only works on passes. Correct command is /changepass init_# new_pass_#")
+
+        else:
+            self.post_syserror("Invalid input. There are no initiative passes in the " + str(self.init_system) + " system. Try SR4 !")
+
+    def CMD_changedur(self, cmdargs):
+        cmdargs = cmdargs.split(None,-1)
+        if self.isD20() or self.isRQ():
+            try:
+                id           = int(cmdargs[0])-1 # subtract 1 because public lists shows with +1
+                new_duration = int(cmdargs[1])
+                hidden       = v.init_list[id]["hidden"]
+
+                if self.init_debug == 1:
+                   print "id : " + str(id)
+                   print "new_dur : " + str(new_duration)
+                   print "hidden : " + str(hidden)
+
+                if new_duration == 0:
+                    self.post_syserror("Invalid input [" + str(self.init_system) + "]. Set duration to '1' if you want the effect to end next time it comes up. \
+                        Otherwise, delete the effect with the /del command.", not hidden)
+                    return
+                elif new_duration < 0:
+                    self.post_syserror("Invalid input [" + str(self.init_system) + "]. New duration must be greater than or equal to 1.", not hidden)
+                    return
+
+                if self.isEffect(id):
+                    v.init_list[id]["duration"] = new_duration
+                    self.post_sysmsg("<font color='#0000ff'><i>" + str(v.init_list[id]["name"]) + "</font></i> has been changed \
+                        to last <font color='#0000ff'><i>" + str(new_duration) + "</font></i> round(s) !", not hidden)
+                else:
+                    self.post_syserror("Invalid input [" + str(self.init_system) + "]. That command only works on effects. Correct command is /changedur init_# new_duration_#")
+
+            except Exception, e:
+                #print str(e)
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. That command only works on effects. Correct command is /changedur init_# new_duration_#")
+
+        else:
+            self.post_syserror("Invalid input. Effects and durations do not work with " + str(self.init_system) + ".")
+
+    #def CMD_changefx(self, cmdargs):
+    #    cmdargs = cmdargs.split(None,-1)
+    #    if self.isD20() or self.isRQ():
+    #        self.init_change(cmdargs,1)
+    #    else:
+    #        self.post_syserror(">Invalid input. Effects and durations do not work with " + str(self.init_system) + ".")
+
+    #def CMD_change(self, cmdargs):
+    #    cmdargs = cmdargs.split(None,-1)
+    #    self.init_change(cmdargs,0)
+
+    def CMD_change(self, cmdargs): # VEG 2.2.7
+        cmdargs = cmdargs.split(None,-1)
+        id = int(cmdargs[0])-1
+        new_init = int(cmdargs[1])
+        if self.isEffect(id):
+            if self.isD20() or self.isRQ():
+                self.init_change(id, new_init, 1)
+            else:
+                self.post_syserror(">Invalid input. Effects and durations do not work with " + str(self.init_system) + ".")
+        else:
+            self.init_change(id, new_init, 0)
+
+    def CMD_togglehidden(self, cmdargs):
+        id = int(cmdargs) - 1
+        self.toggle_hidden(id)
+
+    def CMD_initsetslot(self, cmdargs):
+        id = int(cmdargs) - 1
+        self.init_set_slot(id)
+
+    def CMD_rankup(self, cmdargs):
+        cmdargs = cmdargs.split(None,-1)
+        id = int(cmdargs[0]) - 1
+        try:
+            cmdargs[1]
+            n = 0 - int(cmdargs[1])
+        except:
+           # default num of steps to move
+           n = -1
+        self.init_move(id, n)
+
+    def CMD_rankdown(self, cmdargs):
+        cmdargs = cmdargs.split(None,-1)
+        id = int(cmdargs[0]) - 1
+        try:
+            cmdargs[1]
+            n = int(cmdargs[1])
+        except:
+            n = 1
+        self.init_move(id, n)
+
+    def CMD_initdelaynow(self, cmdargs):
+        id = int(cmdargs) - 1
+        self.init_delay_now(id)
+
+    def CMD_sandglass(self, cmdargs):
+        try:
+            delay = int(cmdargs)
+            self.set_sandglass(delay)
+        except:
+            self.post_syserror("Invalid input. /sandglass (#_of_secs)")
+
+    def CMD_sandglasspause(self, cmdargs):
+       self.sandglass_pause()
+       self.post_sysmsg("<br><b>Sandglass paused</b>", 1)
+
+    def CMD_sandglassresume(self, cmdargs):
+       self.sandglass_resume()
+       self.post_sysmsg("<br><b>Sandglass resumed</b>", 1)
+
+    def CMD_sandglassforceskip(self, cmdargs):
+        try:
+           interval = int(cmdargs)
+           self.set_sandglass_force_skip(interval)
+        except Exception, e:
+          #self.post_my_msg(str(e))
+          self.post_syserror("Invalid input. /sandglassforceskip (#_of_intervals)")
+
+    def CMD_list(self, cmdargs):
+        self.inits_list(0)
+
+    def CMD_publiclist(self, cmdargs):
+        self.inits_list(1)
+
+    def CMD_rnd(self, cmdargs):
+        try:
+            new_round = int(cmdargs)
+            self.init_round = new_round
+            self.post_sysmsg("Round number has been changed. It is now Round # <font color='#ff0000'>" + str(new_round) + "</font>",1)
+        except:
+            self.post_sysmsg("It is currently Round # <font color='#ff0000'>" + str(self.init_round) + "</font>",1)
+
+    def CMD_pass(self, cmdargs):
+        try:
+            new_pass = int(cmdargs)
+            if self.isSR4() and new_pass >= 1 and new_pass <= 4:
+                self.init_pass = new_pass
+                self.init_count = 0
+                self.init_listslot = 0
+                self.post_sysmsg("Initiative Pass has been changed. It is currently Pass # <font color='#ff0000'>" + str(self.init_pass) + "</font>",1)
+            elif not self.isSR4():
+                self.post_syserror("Invalid input. Initiative system must be set to SR4 for passes to function.")
+            else:
+                self.post_syserror("Invalid input. Passes must be greater than/equal to 1 and less than/equal to 4.")
+        except:
+            if self.isSR4():
+                self.post_sysmsg("It is currently Pass # <font color='#ff0000'>" + str(self.init_pass) + "</font>",1)
+            else:
+                self.post_syserror("Invalid input. Initiative system must be set to SR4 for passes to function.")
+
+    def CMD_go(self, cmdargs):
+        self.init_next()
+
+    def CMD_initsave(self, cmdargs):
+        cmdargs = cmdargs.split(None,1)
+        slot = int(cmdargs[0])
+        try:
+            self.init_title = cmdargs[1]
+        except:
+            self.init_title = "untitled"
+
+        if slot < 1 or slot > 5:
+            self.post_syserror("Invalid input. Slot # must be between 1 and 5.")
+        else:
+            self.init_save(slot, 1)
+
+    def CMD_initload(self, cmdargs):
+        try:
+            slot = int(cmdargs)
+        except:
+            slot = 1
+
+        if slot < 1 or slot > 5:
+            self.post_syserror("Invalid input. Slot # must be between 1 and 5.")
+        else:
+            self.init_load(slot)
+
+    def CMD_initautosave(self, cmdargs):
+        delay = int(cmdargs)
+        self.set_autosave(delay)
+
+    def CMD_initautoload(self, cmdargs):
+        self.init_load(0)
+
+    def CMD_nowupmsg(self, cmdargs):
+        self.message_nowup = str(cmdargs)
+        self.post_my_msg("<b>Now Up Message: </b>" + self.message_nowup)
+        self.config_save() # VEG 2.2.6
+
+    def CMD_msghandwarning(self, cmdargs): # VEG 2.2.6
+        if self.msghand_warning:
+            self.msghand_warning=0
+            self.post_sysmsg("Message handler warning <font color='#0000ff'><i>off</i></font>.")
+        else:
+            self.msghand_warning=1
+            self.post_sysmsg("Message handler warning <font color='#0000ff'><i>on</i></font>.")
+        self.config_save()
+
+    def CMD_toggleroundreroll(self, cmdargs): # VEG 2.2.7
+        if self.reroll_type:
+            self.reroll_type=0
+            self.post_sysmsg("Reroll toggle set to <font color='#0000ff'><i>Reroll Every Round (default)</i></font>.")
+        else:
+            self.reroll_type=1
+            self.post_sysmsg("Reroll toggle set to <font color='#0000ff'><i>Keep Same Inits Every Round (house rule)</i></font>.")
+        self.config_save()
+
+    def CMD_toggleiporder(self, cmdargs): # VEG 2.2.7
+        if self.ip_order == 0: # changes IP order to 3/1/4/2 (Serbitar's house rule)
+            self.ip_order=1
+            self.post_sysmsg("IP order set to <font color='#0000ff'><i>3/1/4/2 (Serbitar's)</i></font>.")
+        elif self.ip_order == 1: # changes IP order to 4/1/3/2 (TinkerGnome's house rule)
+            self.ip_order=2
+            self.post_sysmsg("IP order set to <font color='#0000ff'><i>4/1/3/2 (TinkerGnome's)</i></font>.")
+        elif self.ip_order == 2: # changes IP order to 1/2/3/4
+            self.ip_order=0
+            self.post_sysmsg("IP order set to <font color='#0000ff'><i>1/2/3/4 (normal)</i></font>.")
+        self.config_save()
+
+    ###############################################################
+    ####    Base Functions
+    ###############################################################
+
+    def init_clear(self):
+        v.init_list = []
+        self.init_round = 1
+        self.init_count = 0
+        self.init_pass = 0
+        self.init_listslot = 0
+        self.init_listtag = ""
+        self.init_end = 0
+        self.sandglass_count = 0
+        self.sandglass_status = 0
+
+
+    def init_add(self, text, effect, hidden):
+        if (effect==1) and (self.isD20() or self.isRQ()): #d20 effects
+            try:
+                if len(text) == 3:
+                    h = {}
+                    count = int(text[0]) + 1 # effects only decrement (and end)
+                                             # before the count in which they
+                                             # were added on, so add 1
+                    h["type"]     = 1 # effect
+                    h["init"]     = count
+                    h["name"]     = "*" + str(text[2])
+                    h["duration"] = int(text[1])
+                    h["rank"]     = self.get_next_rank(h["init"])
+                    h["hidden"]   = hidden
+                    h["tag"]      = self.get_unique_tag(h["name"])
+
+                    if hidden == 1:
+                       duration_str = "?"
+                    else:
+                       duration_str = str(h["duration"])
+
+                    v.init_list += [h]
+
+                    self.post_sysmsg("<i><font color='#0000ff'>" + h["name"] + "</font></i> effect added to list at init count <i><font color='#0000ff'>" + str(h["init"]) + "</font></i>, lasting <font color='#0000ff'><i>" + duration_str + "</i></font> round(s) !", 1)
+                    self.init_sort()
+            except:
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. Correct command is: /addfx init_# duration_# description")
+
+        elif self.isD20(): #d20 characters
+            try:
+                if len(text) == 2:
+                    h = {}
+                    h["type"]     = 0 # character
+                    h["init"]     = int(text[0])
+                    h["name"]     = str(text[1])
+                    h["rank"]     = self.get_next_rank(h["init"])
+                    h["hidden"]   = hidden
+                    h["tag"]      = self.get_unique_tag(h["name"])
+
+                    v.init_list += [h]
+
+                    self.post_sysmsg("<font color='#0000ff'><i>" + h["name"] + "</font></i> added to list at init count <i><font color='#0000ff'>" + str(h["init"]) + "</font></i> !", not hidden)
+                    self.init_sort()
+            except:
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. Correct command is: /add init_# description", not hidden)
+
+        elif self.isSR4(): #SR4 characters
+            try:
+                if len(text) == 3:
+                    h = {}
+                    h["type"]     = 0 # character
+                    h["init"]     = int(text[0])
+                    h["name"]     = str(text[2])
+                    h["passes"]   = int(text[1])
+                    h["rank"]     = self.get_next_rank(h["init"])
+                    h["hidden"]   = hidden
+                    h["tag"]      = self.get_unique_tag(h["name"])
+
+                    v.init_list+=[h]
+
+                    self.post_sysmsg("<font color='#0000ff'><i>" + h["name"] + "</font></i> added to list at init count \
+                        <font color='#0000ff'><i>" + str(h["init"]) + "</font></i> with <font color='#0000ff'><i>" + str(h["passes"])+ "</font></i> \
+                        passes !", not hidden)
+
+                    self.init_sort()
+
+            except:
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. Correct command is: /add init_# passes_# description", not hidden)
+
+        elif self.isRQ(): #RQ characters
+            try:
+                if len(text) == 2:
+                    h = {}
+                    h["type"]     = 0 # character
+                    h["init"]     = int(text[0])
+                    h["name"]     = str(text[1])
+                    h["rank"]     = self.get_next_rank(h["init"])
+                    h["hidden"]   = hidden
+                    h["tag"]      = self.get_unique_tag(h["name"])
+
+                    v.init_list += [h]
+
+                    self.post_sysmsg("<font color='#0000ff'><i>" + h["name"] + "</font></i> added to list at Strike Rank \
+                        <font color='#0000ff'><i>" + str(h["init"]) + "</font></i> !", not hidden)
+
+                    self.init_sort()
+            except:
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. Correct command is: /add SR description", not hidden)
+
+
+    def init_delete(self, id, public):
+        try:
+            name = v.init_list[id]["name"]
+            is_last = 0
+            is_self = 0
+            is_effect = 0
+            if public == 1:
+                self.post_sysmsg("<font color='#0000ff'><i>" + name + "</font></i> has been removed from the initiative list !", not v.init_list[id]["hidden"])
+            if self.isLast(id):
+                is_last = 1
+            if self.init_listslot == id:
+                is_self = 1
+            if self.isEffect(id):
+                is_effect = 1
+
+            del v.init_list[id]
+
+            if is_effect:
+                self.init_listslot -= 1
+            elif is_last and is_self:
+                self.init_next()
+            elif is_self == 1:
+                self.init_listslot -= 1
+                self.init_next()
+            self.init_sort()
+        except:
+            if public == 1:
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. Correct command is: /del list_number")
+
+
+    def init_change(self, id, new_init, effect):
+        try:
+            proceed = 0
+            old_name = v.init_list[id]["name"]
+
+            if v.init_list[id]["init"] == new_init:
+               self.post_syserror("Invalid input [" + str(self.init_system) + "]. Already at init count <font color='#0000ff'>" + str(new_init) + "</font>. Try /rankup or /rankdown instead.")
+               return
+
+            if effect == 1 and (self.isD20() or self.isRQ()): #d20 and RQ effects
+                if self.isEffect(id):
+                    proceed  = 1
+                    self.post_sysmsg("<font color='#0000ff'><i>" + old_name + "</font></i> has had its init count changed \
+                        to <font color='#0000ff'><i>" + str(new_init) + "</font></i>.", not v.init_list[id]["hidden"])
+                else:
+                    self.post_syserror("Invalid input [" + str(self.init_system) + "]. Not an effect. Try /change instead.")
+
+            else: #d20, SR4, RQ characters
+                if self.isEffect(id):
+                    self.post_syserror("Invalid input [" + str(self.init_system) + "]. Not a character. Try /changefx instead.")
+                else:
+                    proceed  = 1
+                    self.post_sysmsg("<font color='#0000ff'><i>" + old_name + "</font></i>'s init count has changed to \
+                        <font color='#0000ff'><i>" + str(new_init) + "</font></i>.", not v.init_list[id]["hidden"])
+            if proceed==1:
+
+                listslot_as_changed = 0
+                # hacks if currently selected listslot /change's, we select the next tag
+                if not self.isEnded() and id == self.init_listslot:
+                    if not self.isLast(id):
+                       self.init_listtag = v.init_list[id + 1]["tag"]
+                       listslot_as_changed = 1
+                       self.sandglass_reset()
+                    else:
+                       self.init_listtag = v.init_list[self.getLast()]["tag"]
+                       self.init_next()
+
+                v.init_list[id]["init"] = new_init
+
+                # set the rank to the lowest rank available...
+                v.init_list[id]["rank"] = self.get_next_rank(v.init_list[id]["init"])
+
+                self.init_sort()
+
+                # tell if listslot as changed
+                if listslot_as_changed == 1:
+                   if self.init_count >= v.init_list[id]['init']:
+                       self.init_listslot -= 1
+                       self.init_count = v.init_list[self.init_listslot]['init']
+                   else:
+                       self.init_listslot -= 1
+                       self.init_next()
+                   #self.post_now_up(self.init_listslot, self.message_nowup + ": ")
+                   #self.init_next()
+
+        except:
+            if effect == 1:
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. Correct command is: /changefx list_# new_init_# (example: /change 1 4)")
+            else:
+                self.post_syserror("Invalid input [" + str(self.init_system) + "]. Correct command \
+                    is: /change list_# new_init_# (example: /change 1 4)")
+
+
+    def init_sort(self):
+        if self.isD20():
+            self.sort_high()
+        elif self.isSR4():
+            self.sort_high()
+        elif self.isRQ():
+            self.sort_low()
+        else:
+            self.sort_high()
+
+
+    def sort_low(self):
+        v.init_list.sort(self.sort_low_initlist)
+        # this part readjusts the listslot when other objects are
+        # changed, added, moved or deleted from v.init_list
+        #count = self.init_count
+        if self.isEnded():
+           return
+        n = 0
+        last_tag = self.init_listtag
+        last_id = self.init_listslot
+        found = 0
+
+        for m in v.init_list:
+            id = v.init_list.index(m) # first listslot's id
+
+            if v.init_list[id]["tag"] == last_tag:
+               self.init_listslot = id
+               found = 1
+               break
+
+        # if can't find, it got /del'd
+        if found == 0 and self.init_listtag != "":
+            # next slot should've been bumped up, we re selecting it
+            try:
+               self.init_listtag = v.init_list[self.init_listslot]["tag"]
+            except:
+               #we del'd the last one... do nuttin
+               pass
+
+
+    def sort_high(self):
+        v.init_list.sort(self.sort_high_initlist)
+        v.init_list.reverse()
+        # this part readjusts the listslot when other objects are
+        # changed, added, moved or deleted from v.init_list
+        #count = self.init_count
+
+        if self.isEnded():
+           return
+        n = 0
+        last_tag = self.init_listtag
+        last_id = self.init_listslot
+        found = 0
+        for m in v.init_list:
+            id = v.init_list.index(m) # first listslot's id
+            #print "sid : " + str(id)
+            if v.init_list[id]["tag"] == last_tag:
+               self.init_listslot = id
+               found = 1
+               break
+        # if can't find, it got /del'd
+        if found == 0 and self.init_listtag != "":
+            # next slot should've been bumped up, we re selecting it
+            try:
+               self.init_listtag = v.init_list[self.init_listslot]["tag"]
+            except:
+               #we del'd the last one... do nuttin
+               pass
+
+
+    def sort_low_initlist(self, x, y):
+       if x['init'] < y['init']:
+          return -1
+       elif x['init'] > y['init']:
+          return 1
+       elif x['init'] == y['init']:
+          # if same init, we sort by rank
+          if x['rank'] > y['rank']:
+             return -1
+          elif x['rank'] < y['rank']:
+             return 1
+          else:
+                return 0
+       else: # shouldnt enter here
+          return 0
+
+
+    def sort_high_initlist(self, x, y):
+        if x['init'] < y['init']:
+            return -1
+        elif x['init'] > y['init']:
+            return 1
+        elif x['init'] == y['init']:
+            # if same init, we sort by rank
+            if x['rank'] < y['rank']:
+                return -1
+            elif x['rank'] > y['rank']:
+                return 1
+            else:
+                return 0
+        else: # shouldnt enter here
+            return 0
+
+
+    def init_next(self):
+       if self.isD20():
+          self.init_next_D20()
+       elif self.isSR4():
+          self.init_next_SR4()
+       elif self.isRQ():
+          self.init_next_RQ()
+
+
+    def init_next_D20(self):
+
+        if self.isEnded():
+            self.init_listslot = -1  # offsets +1 coming later, starts 0
+            self.init_listtag = ""
+            self.init_end = 1
+
+        if not self.isEnded():
+            try:
+                self.init_listslot += 1 #toldya
+                id   = self.init_listslot
+                init = str(v.init_list[id]["init"])
+                name = str(v.init_list[id]["name"])
+                self.init_listtag = v.init_list[id]["tag"]
+                self.init_count = v.init_list[id]["init"]
+
+                if self.isEffect(id):
+                    v.init_list[id]["duration"] -= 1     #subtract 1 rnd from duration
+                    if self.getRoundsLeftInEffect(id) == 0:
+                        self.post_effect(id, "EFFECT ENDING:")
+                        self.init_delete(id, 0)
+
+                        #nextslot = id #this effect got deleted, so id = former id+1
+
+                        # hack if two consecutive effects and this one has ended
+                        #if self.isEffect(nextslot):
+                        #    print "nextslot IS an effect"
+                        #    self.init_listslot -= 1 # zalarian
+                        #    id = self.init_listslot # zalarian
+                        #else:
+                        #    print "nextslot IS NOT an effect"
+                        #    v.init_list[id]["hidden"] = 0
+                        #    self.post_now_up(id, self.message_nowup + ": ")
+
+                        # reset the sandglass for this turn
+                        self.sandglass_reset()
+                    else:
+                        if v.init_list[id]["hidden"] == 1:
+                           rounds_left = "?"
+                        else:
+                           rounds_left = self.getRoundsLeftInEffect(id)
+
+                        if self.hide_id == 0:
+                           str_id = str(id+1) + ":"
+                        else:
+                           str_id = ""
+
+                        self.post_effect(id, "EFFECT:")
+
+                        nextslot = id + 1
+
+                        if self.isEffect(nextslot):
+                            self.init_next_D20()
+                else:   #normal character, not effect
+                    v.init_list[id]["hidden"] = 0
+
+                    self.post_now_up(id, self.message_nowup + ": ")
+
+                    # reset the sandglass for this turn
+                    self.sandglass_reset()
+
+            except Exception, e:
+
+                self.init_end = 0
+                self.init_round += 1
+                self.init_count = 0
+                self.post_sysmsg("<hr><font color='#ff0000'>End of Round<br>Starting Round <font color='#0000ff'><i># " + str(self.init_round) + "</font></i></font><hr>",1)
+
+
+    def init_next_SR4(self, do_not_advance_past_init=0):
+        if self.isEnded():
+            self.init_listslot = -1  # offsets +1 coming later, starts 0
+            self.init_listtag = ""
+            self.init_end = 1
+
+        if not self.isEnded():
+            if self.init_pass == 0:
+                self.init_pass +=1
+            if self.init_pass > 4:
+                self.init_end = 0
+                self.init_round += 1
+                self.init_pass = 0
+                msg = "<hr><font color='#ff0000'>End of Round<br>Starting Round <font color='#0000ff'><i># " + str(self.init_round) + "</font></i></font><hr>"
+
+                if self.reroll_type == 0: # Reroll Every Round (default) - VEG 2.2.7
+                    v.init_list = []
+                    msg += "<font color='#0000ff'>Roll New Initiatives</font><hr>"
+                else: # Keep Same Inits Every Round - VEG 2.2.7
+                    pass
+                self.post_sysmsg(msg,1)
+
+            else:
+                try:
+                    valid_char = 0
+                    just_movement = 0
+                    while valid_char != 1:
+                        self.init_listslot += 1 #toldya
+                        id   = self.init_listslot
+                        passes = v.init_list[id]["passes"] #exception raised here if noone left
+
+                        if self.ip_order == 0: # ip order: 1/2/3/4 (normal) - VEG 2.2.7
+                            if passes >= self.init_pass:
+                                valid_char = 1
+                            elif self.hide_justmovement != 1: # not enough passes, but still gain movement
+                                just_movement = 1
+                                valid_char = 1
+
+                        else: # ip order: 3/1/4/2 (Serbitar's house rule) or 4/1/3/2 (TinkerGnome's house rule) - VEG 2.2.7
+                            if self.init_pass == 1: # first pass: only people with 3+ (S) or 4+ (TG) IPs go
+                                if self.ip_order == 1 and passes >= 3: # ip order: 3/1/4/2
+                                    valid_char = 1
+                                elif self.ip_order == 2 and passes >= 4: # ip order: 4/1/3/2
+                                    valid_char = 1
+                                elif self.hide_justmovement != 1:
+                                    just_movement = 1
+                                    valid_char = 1
+                            elif self.init_pass == 2: # second pass: only people with 1+ IPs go
+                                if passes >= 1:
+                                    valid_char = 1
+                                elif self.hide_justmovement != 1:
+                                    just_movement = 1
+                                    valid_char = 1
+                            elif self.init_pass == 3: # third pass: only people with 4+ (S) or 3+ (TG) IPs go
+                                if self.ip_order == 1 and passes >= 4: # ip order: 4/1/4/2
+                                    valid_char = 1
+                                elif self.ip_order == 2 and passes >= 3: # ip order: 3/1/3/2
+                                    valid_char = 1
+                                elif self.hide_justmovement != 1:
+                                    just_movement = 1
+                                    valid_char = 1
+                            elif self.init_pass == 4: # fourth pass: only people with 2+ IPs go
+                                if passes >= 2:
+                                    valid_char = 1
+                                elif self.hide_justmovement != 1:
+                                    just_movement = 1
+                                    valid_char = 1
+
+                    init = str(v.init_list[id]["init"])
+                    name = str(v.init_list[id]["name"])
+                    self.init_listtag = v.init_list[id]["tag"]
+                    self.init_count = v.init_list[id]["init"]
+                    v.init_list[id]["hidden"] = 0  # no more hidden
+
+                    if just_movement == 0:
+                        self.post_now_up(id, self.message_nowup + ": ")
+                    else:
+                        self.post_movement(id, " JUST MOVEMENT:")
+
+                    # reset the sandglass for this turn
+                    self.sandglass_reset()
+
+                    if just_movement==1 and self.isMovement(id + 1) and do_not_advance_past_init==0:
+                        self.init_next_SR4() # for convenience sake, characters who only have movement
+                                        #     will be grouped together.
+                                        # do_not_advance_past_init is a hackfix to avoid some
+                                        #     problems found with this trick
+                    elif self.sharesInit(id + 1):  # rules say characters who share an init take
+                        self.init_next_SR4(1)      # actions simultaneously (for the most part)
+
+                except:
+                    old_pass = self.init_pass
+                    self.init_pass += 1
+                    self.init_count = 0
+                    self.init_end = 0
+                    if self.init_pass != 5:
+                        self.post_sysmsg("<hr><font color='#ff0000'>End of Pass <font color='#0000ff'><i>#"\
+                            + str(old_pass) + "</font></i><br>Starting New Pass <font color='#0000ff'><i># " \
+                            + str(self.init_pass) + "</font></i></font><hr>", 1)
+                    else:
+                        self.init_next_SR4()
+
+
+    def init_next_RQ(self):
+        if self.isEnded():
+            self.init_listslot = -1  # offsets +1 coming later, starts 0
+            self.init_listtag = ""
+            self.init_end = 1
+
+        if not self.isEnded():
+            try:
+                self.init_listslot += 1 #toldya
+                id   = self.init_listslot
+                init = str(v.init_list[id]["init"])
+                name = str(v.init_list[id]["name"])
+                self.init_listtag = v.init_list[id]["tag"]
+                self.init_count = v.init_list[id]["init"]
+
+                if self.isEffect(id):
+                    v.init_list[id]["duration"] -= 1     #subtract 1 rnd from duration
+                    if self.getRoundsLeftInEffect(id) == 0:
+                        self.post_effect(id, "EFFECT ENDING:")
+                        self.init_delete(id, 0)
+
+                        # hack if two consecutive effects and this one has ended
+                        #if self.isEffect(nextslot):
+                        #   self.init_listslot -= 1 # zalarian
+                        #   id = self.init_listslot # zalarian
+                        #else:
+                        #   v.init_list[id]["hidden"] = 0
+
+                        #   self.post_now_up(id, self.message_nowup + ": ")
+
+                        # reset the sandglass for this turn
+                        self.sandglass_reset()
+                    else:
+                        if v.init_list[id]["hidden"] == 1:
+                           rounds_left = "?"
+                        else:
+                           rounds_left = self.getRoundsLeftInEffect(id)
+
+
+                        if self.hide_id == 0:
+                           str_id = str(id+1) + ":"
+                        else:
+                           str_id = ""
+
+                        self.post_effect(id, "EFFECT:")
+
+                        nextslot = id + 1
+
+                    if self.isEffect(nextslot):
+                       self.init_next_RQ()
+                else:   #normal character, not effect
+                    v.init_list[id]["hidden"] = 0
+
+                    self.post_now_up(id, self.message_nowup + ": ")
+
+                    # reset the sandglass for this turn
+                    self.sandglass_reset()
+
+            except Exception, e:
+                self.init_end = 0
+                self.init_round += 1
+                self.init_count = 0
+                self.post_sysmsg("<hr><font color='#ff0000'>End of Round<br>Starting Round <font color='#0000ff'><i># " + str(self.init_round) + "</font></i></font><hr>",1)
+
+    def init_set_slot(self, id):
+       try:
+          if self.isEnded():
+             raise
+          else:
+             if id == self.init_listslot:
+                self.post_syserror("Slot <font color='#0000ff'>#" + str(id + 1) + "</font> already selected.")
+             else:
+                self.init_listtag = v.init_list[id]["tag"]
+                self.init_listslot = id
+                self.sandglass_reset()
+                self.post_sysmsg("<font color='#0000ff'><i>" + v.init_list[id]["name"] + "</font></i> selected !")
+
+       except:
+          self.post_syserror("Can't select slot <font color='#0000ff'>#" + str(id) + "</font>. Round must be started.")
+
+    def init_move(self, id, n):
+       try:
+          # how many inits we have so far
+          list_size = len(v.init_list)
+
+          if self.init_debug == 1:
+             print "list_size : " + str(list_size)
+             print "id : " + str(id)
+             print "pre n : " + str(n)
+
+          if id >= list_size:
+             self.post_syserror("Can't move an unknown ID (<font color='#0000ff'>" + str(id+1) + "</font>) !")
+             return
+
+          # are we going UP or DOWN ?
+          if n > 0:
+             up = 0
+             step = 1
+             # can't move down last object
+             if self.isLast(id):
+                self.post_syserror("Can't move <font color='#0000ff'>" + v.init_list[id]["name"] + "</font> this way! (Already at the last init)")
+                return
+
+             max_move = list_size - id # maximum num of moves we could possibly do
+             # cant move more than max_move position!!
+             if n > max_move:
+                n = max_move
+          elif n < 0:
+             up = 1
+             step = -1
+             #can't move up first object
+             if self.isFirst(id):
+                self.post_syserror("Can't move <font color='#0000ff'>" + v.init_list[id]["name"] + "</font> this way ! (Already at the first init)")
+                return
+
+             max_move = id # maximum num of move we could possibly do
+             # cant move more than max_move position!!
+             if n > max_move:
+                n = -max_move
+          else:
+             # shouldnt be here
+             self.post_syserror("Can't move <font color='#0000ff'>" + v.init_list[id]["name"] + "</font> !")
+             return 1
+
+          if self.init_debug == 1:
+             print "n : " + str(n)
+             print "step : " + str(step)
+             print "max_move : " + str(max_move)
+             print "up : " + str(up)
+             print "len : " + str(len(range(id , (id + n + step), step)))
+
+          # suppose we're not moving, id_n will be the target id we're moving to
+          id_n = id
+
+          # find the last init we can go with 'n' steps and staying on the init count of object 'id'
+          for k in range(id + step , (id + n + step), step):
+             #print "k... " + str(k)
+             if v.init_list[k]["init"] != v.init_list[id]["init"]:
+                # cant more further than k + step.. go away
+                break
+             else:
+                id_n = k
+
+          # we can't move if id_n is the same as id. probably only alone on this init count
+          if id_n == id:
+             self.post_syserror("Can't move <font color='#0000ff'>" + v.init_list[id]["name"] + "</font> ! (you may only rankup/rankdown within the same init value)</i>")
+             return
+
+          if self.init_debug == 1:
+             print "id_n : " + str(id_n)
+
+          # now we have to set the rank correctly
+          if up and self.isFirst(id_n):
+             # going up and we're moving in first position of all inits
+             v.init_list[id]["rank"] = v.init_list[id_n]["rank"] + 1
+          elif not up and self.isLast(id_n):
+             # going down and we're moving in last position of all inits
+             v.init_list[id]["rank"] = v.init_list[id_n]["rank"] - 1
+          elif v.init_list[id_n + step]["init"] != v.init_list[id]["init"]:
+             # moving in first (up) or last (down) position of the current init count
+             v.init_list[id]["rank"] = v.init_list[id_n]["rank"] - step
+          elif up and v.init_list[id_n + step]["init"] == v.init_list[id]["init"]:
+             # moving up somewhere between multiple id of the same init count
+             v.init_list[id]["rank"] = v.init_list[id_n]["rank"] - (step * (v.init_list[id_n + step]["rank"] - v.init_list[id_n]["rank"]) / 2.0)
+          elif not up and v.init_list[id_n + step]["init"] == v.init_list[id]["init"]:
+             # moving down somewhere between multiple id of the same init count
+             v.init_list[id]["rank"] = v.init_list[id_n]["rank"] + (step * (v.init_list[id_n + step]["rank"] - v.init_list[id_n]["rank"]) / 2.0)
+
+          self.post_sysmsg("<font color='#0000ff'><i>" + v.init_list[id]["name"] + "</font></i> moved !", not v.init_list[id]["hidden"])
+
+          # hack, if we're moving the selected slot or at the selected
+          # slot, we're staying at the same position.
+          if not self.isEnded():
+             if id_n == self.init_listslot:
+                # we're moving AT the selected slot
+                self.init_listtag = v.init_list[id]["tag"]
+                self.sandglass_reset()
+                self.post_now_up(id, self.message_nowup + ": ")
+                # works fine unless you try to rank by more than one step, so
+                # currently this section (or the other) is bugged
+             if id == self.init_listslot:
+                # we're moving THE selected slot
+                if up:
+                   self.init_listtag = v.init_list[id-1]["tag"]
+                else:
+                   self.init_listtag = v.init_list[id+1]["tag"]
+                self.post_now_up(id_n, self.message_nowup + ": ")
+                # works fine unless you try to rank by more than one step, so
+                # currently this section (or the other) is bugged
+                self.sandglass_reset()
+
+
+          # sort that list again
+          self.init_sort()
+
+       except:
+          self.post_syserror("Can't move <font color='#0000ff'>" + v.init_list[id]["name"] + "</font> ! (May only rankup/rankdown within the same init value)")
+
+
+    def init_delay_now(self, id):
+       try:
+          # can't delay if round ended
+          if self.isEnded():
+             self.post_syserror("Can't delay until the round has started.", 0)
+             return
+
+          if self.isD20() or self.isRQ():
+             # get current tag
+             tag_current = v.init_list[self.init_listslot]["tag"]
+             tag_delay = v.init_list[id]["tag"]
+             name_delay = v.init_list[id]["name"]
+
+             # can't delay current listslot
+             if self.init_listslot == id:
+                self.post_syserror("Can't delay current selected slot.", 0)
+                return
+
+             # if not on the same init count
+             if v.init_list[self.init_listslot]["init"] != v.init_list[id]["init"]:
+                # generate array needed by init_change() as first parameter
+                tab = [ 0, id + 1, v.init_list[self.init_listslot]["init"]]
+                self.init_change(tab, v.init_list[id]["type"])
+                self.init_sort()
+
+             #print "name_delay : " + str(name_delay)
+             #print "tag_delay : " + str(tag_delay)
+             #print "tag_current : " + str(tag_current)
+             #print "slot_tag : " + str(v.init_list[self.init_listslot]["tag"])
+             #print "listslot : " + str(self.init_listslot)
+
+             # must rankup if needed (init_change() move at the end of a group if at same init count
+             if not (v.init_list[self.init_listslot]["tag"] == tag_delay):
+                # must find where is tag_delay and move it before tag_current
+                for m in v.init_list:
+                   if m["tag"] == tag_delay:
+                      new_id_delay = v.init_list.index(m)
+                      break
+
+                step = (self.init_listslot - new_id_delay)
+                self.init_move(new_id_delay, step)
+                #self.init_listtag = tag_delay
+                self.init_sort()
+
+             self.post_now_up(self.init_listslot, "NOW UP FOR THE KILLING (DELAYED ACTION):")
+
+             self.sandglass_reset()
+
+          else:
+             self.post_syserror(">Can't delay in " + str(self.init_system) + ".")
+
+       except Exception, e:
+          print str(e)
+          self.post_syserror("Invalid input [" + str(self.init_system) + "]. Correct command is: /initdelaynow list_number")
+
+    ###############################################################
+    ####    Sandglass Functions
+    ###############################################################
+
+    # will send reminder to the chat if needed
+    def sandglass(self):
+       try:
+          if self.sandglass_delay == 0 or self.sandglass_status == 1:
+             pass
+          # send reminder except for effect
+          elif not self.isEnded() and not self.isEffect(self.init_listslot):
+             self.sandglass_count += 1
+             if self.sandglass_skip_interval and not (self.sandglass_count % (self.sandglass_delay * self.sandglass_skip_interval)):
+                uname = v.init_list[self.init_listslot]["name"]
+                self.post_sysmsg("TIME'S UP <font color='#0000ff'><i>" + uname + "</font></i>!!! (<i>" + str(self.sandglass_count) + " seconds elapsed</i>)", 1)
+                self.init_next()
+             elif not (self.sandglass_count % self.sandglass_delay):
+                self.post_sysmsg("REMINDER to <font color='#0000ff'><i>" + v.init_list[self.init_listslot]["name"] + "</font></i> : IT'S YOUR TURN! (<i>" + str(self.sandglass_count) + " seconds elapsed</i>)", 1)
+
+       except:
+          pass
+
+
+    def sandglass_reset(self):
+       try:
+          if self.sandglass_delay > 0:
+             self.sandglass_count = 0
+             self.sandglass_resume()
+       except:
+          pass
+
+
+    # status :  0 = running
+    #           1 = pause
+    def sandglass_pause(self):
+       try:
+          if self.sandglass_delay > 0:
+             self.sandglass_status = 1
+       except:
+          pass
+
+
+    def sandglass_resume(self):
+       try:
+          if self.sandglass_delay > 0:
+             self.sandglass_status = 0
+       except:
+          pass
+
+    def set_sandglass_force_skip(self,interval):
+       self.sandglass_skip_interval = interval
+       if interval == 0:
+          self.post_sysmsg("Sandglass force turn skip is now off.", 1)
+       else:
+          self.post_sysmsg("Sandglass force turn skip is now set to " + str(interval) + " interval(s).", 1)
+       self.config_save() # VEG 2.2.6
+
+    def set_sandglass(self, delay):
+       self.sandglass_delay = delay
+       if delay == 0:
+          self.post_sysmsg("Sandglass is now off.", 1)
+       else:
+          self.post_sysmsg("Sandglass is now set to " + str(delay) + " seconds.", 1)
+       self.config_save() # VEG 2.2.6
+
+    ###############################################################
+    ####    Config/Save/Load Functions
+    ###############################################################
+
+    def init_save(self, slot, public):
+       try:
+          modname = "xxinit2-" + str(slot)
+
+          self.plugindb.SetString(modname, "init_recording",  str(self.init_recording))
+          self.plugindb.SetString(modname, "init_round",      str(self.init_round))
+          self.plugindb.SetString(modname, "init_title",      str(self.init_title))
+          self.plugindb.SetString(modname, "init_count",      str(self.init_count))
+          self.plugindb.SetString(modname, "init_pass",       str(self.init_pass))
+          self.plugindb.SetString(modname, "init_listslot",   str(self.init_listslot))
+          self.plugindb.SetString(modname, "init_listtag",    str(self.init_listtag))
+          self.plugindb.SetString(modname, "init_end",        str(self.init_end))
+          self.plugindb.SetString(modname, "init_system",     str(self.init_system))
+          self.plugindb.SetString(modname, "init_list",       repr(v.init_list))
+          self.plugindb.SetString(modname, "sandglass_status",       repr(self.sandglass_status))
+
+          if self.init_title != "":
+             title_str = " <i>[" + self.init_title + "]</i>"
+          else:
+             title_str = ""
+
+          if slot == 0:
+             #self.post_sysmsg("Initiative list autosaved successfully.", public)
+             # this is annoying, so i disabled the text output
+             pass
+          else:
+             self.post_sysmsg("Initiative list saved successfully on slot #" + str(slot) + title_str + ".", public)
+
+       except Exception, e:
+          print "err saving : " + str(e)
+
+
+    def init_load(self, slot):
+       try:
+          self.init_clear()
+
+          modname = "xxinit2-" + str(slot)
+
+          self.init_recording  = int(self.plugindb.GetString(modname, "init_recording","0"))
+          self.init_round      = int(self.plugindb.GetString(modname, "init_round", "0"))
+          self.init_title      = str(self.plugindb.GetString(modname, "init_title", ""))
+          self.init_count      = int(self.plugindb.GetString(modname, "init_count", "0"))
+          self.init_pass       = int(self.plugindb.GetString(modname, "init_pass", "0"))
+          self.init_listslot   = int(self.plugindb.GetString(modname, "init_listslot", "0"))
+          self.init_listtag    = str(self.plugindb.GetString(modname, "init_listtag", ""))
+          self.init_end        = int(self.plugindb.GetString(modname, "init_end", "0"))
+          self.init_system     = str(self.plugindb.GetString(modname, "init_system", ""))
+          v.init_list          = eval(self.plugindb.GetString(modname, "init_list", ""))
+          self.sandglass_status = eval(self.plugindb.GetString(modname, "sandglass_status", "0"))
+
+          if self.init_title != "":
+             title_str = " <i>[" + self.init_title + "]</i>"
+          else:
+             title_str = ""
+
+          if slot == 0:
+             self.post_sysmsg("Last autosaved initiative list loaded successfully.")
+          else:
+             self.post_sysmsg("Initiative list slot #" + str(slot) + title_str + " loaded successfully.")
+          self.config_save() # VEG 2.2.6
+
+       except Exception, e:
+          #print "err loading: " + e
+          self.post_syserror("Error loading Initiative list.")
+
+
+    def autosave(self):
+       try:
+          slot = 0
+          if self.autosave_delay != 0:
+             self.autosave_count += 1
+             if not (self.autosave_count % self.autosave_delay):
+                self.autosave_count = 0
+                self.init_save(slot, 0)
+
+       except Exception, e:
+          print str(e)
+
+
+    def set_autosave(self, delay):
+
+       self.autosave_delay = delay
+
+       if delay == 0:
+          self.post_sysmsg("Autosave is now off.")
+       else:
+          self.post_sysmsg("Autosave is now done every " + str(delay) + " seconds.")
+
+
+    def config_save(self):
+       try:
+          modname = "xxinit2-config"
+
+          # don't want python to convert this to the strings "TRUE" or "FALSE"
+          if self.init_recording == 0:
+             self.init_recording = 0
+          else:
+             self.init_recording = 1
+
+          if self.hide_id == 0:
+             self.hide_id = 0
+          else:
+             self.hide_id = 1
+
+          if self.hide_ondeck == 0:
+             self.hide_ondeck = 0
+          else:
+             self.hide_ondeck = 1
+
+          if self.hide_justmovement == 0:
+             self.hide_justmovement = 0
+          else:
+             self.hide_justmovement = 1
+
+          self.plugindb.SetString(modname, "init_recording",  str(self.init_recording))
+          self.plugindb.SetString(modname, "init_system",     str(self.init_system))
+          self.plugindb.SetString(modname, "sandglass_count", str(self.sandglass_count))
+          self.plugindb.SetString(modname, "sandglass_delay", str(self.sandglass_delay))
+          self.plugindb.SetString(modname, "sandglass_skip_interval", str(self.sandglass_skip_interval))
+          self.plugindb.SetString(modname, "autosave_delay",  str(self.autosave_delay))
+          self.plugindb.SetString(modname, "autosave_count",  str(self.autosave_count))
+          self.plugindb.SetString(modname, "hide_id",         str(self.hide_id))
+          self.plugindb.SetString(modname, "hide_ondeck",     str(self.hide_ondeck))
+          self.plugindb.SetString(modname, "hide_justmovement",str(self.hide_justmovement))
+          self.plugindb.SetString(modname, "message_nowup",   str(self.message_nowup))
+          self.plugindb.SetString(modname, "msghand_timer",   str(self.msghand_timer)) # VEG 2.2.6
+          self.plugindb.SetString(modname, "msghand_warning", str(self.msghand_warning)) # VEG 2.2.6
+          self.plugindb.SetString(modname, "reroll_type",     str(self.reroll_type)) # VEG 2.2.7
+          self.plugindb.SetString(modname, "ip_order",      str(self.ip_order)) # VEG 2.2.7
+
+          #self.post_sysmsg("Configuration saved successfully.",0)
+          print "Initiative configuration saved successfully."
+
+       except:
+          self.post_syserror("Error saving configuration.",0)
+
+
+    def config_load(self):
+
+       try:
+          modname = "xxinit2-config"
+
+          self.init_recording  = int(self.plugindb.GetString(modname, "init_recording","0"))
+          self.init_system     = str(self.plugindb.GetString(modname, "init_system", ""))
+          self.sandglass_count = int(self.plugindb.GetString(modname, "sandglass_count", "0"))
+          self.sandglass_delay = int(self.plugindb.GetString(modname, "sandglass_delay", "0"))
+          self.sandglass_skip_interval = int(self.plugindb.GetString(modname, "sandglass_skip_interval", "0"))
+          self.autosave_delay  = int(self.plugindb.GetString(modname, "autosave_delay", "0"))
+          self.autosave_count  = int(self.plugindb.GetString(modname, "autosave_count", "0"))
+          self.hide_id         = int(self.plugindb.GetString(modname, "hide_id", "0"))
+          self.hide_ondeck     = int(self.plugindb.GetString(modname, "hide_ondeck", "0"))
+          self.hide_justmovement = int(self.plugindb.GetString(modname, "hide_justmovement", "0"))
+          self.message_nowup   = str(self.plugindb.GetString(modname, "message_nowup", ""))
+          self.msghand_timer   = int(self.plugindb.GetString(modname, "msghand_timer", "0")) # VEG 2.2.6
+          self.msghand_warning = int(self.plugindb.GetString(modname, "msghand_warning", "0")) # VEG 2.2.6
+          self.reroll_type     = int(self.plugindb.GetString(modname, "reroll_type", "0")) # VEG 2.2.7
+          self.ip_order      = int(self.plugindb.GetString(modname, "ip_order", "0")) # VEG 2.2.7
+
+          # if system blank... no config was loaded
+          if self.init_system == "":
+             raise
+
+          self.post_sysmsg("Configuration loaded successfully.",0)
+          self.config_save() # VEG 2.2.6
+
+       except:
+          self.config_default()
+
+
+    def config_default(self):
+
+          self.init_recording = 1
+          self.init_system = "D20"
+          self.sandglass_count = 0
+          self.sandglass_delay = 0
+          self.sandglass_status = 0
+          self.sandglass_skip_interval = 0
+          self.autosave_delay = 60
+          self.autosave_count = 0
+          self.hide_id = 0
+          self.hide_ondeck = 0
+          self.hide_justmovement = 0
+          self.message_nowup = "NEXT UP FOR THE KILLING"
+          self.msghand_timer = 0
+          self.msghand_warning = 1
+          self.reroll_type = 0
+          self.ip_order = 0
+
+          self.post_sysmsg("Default configuration loaded successfully.",0)
+          self.config_save() # VEG 2.2.6
+
+    def config_show(self):
+
+       try:
+          self.post_sysmsg("<br><u>Init Tool system config v" + str(self.version) + "</u><br>")
+
+          if self.init_recording == 0:
+             buf = "disabled";
+          else:
+             buf = "enabled";
+          self.post_my_msg("<b>Init Recording:</b> " + str(buf))
+
+          self.post_my_msg("<b>System:</b> " + str(self.init_system))
+
+          if self.sandglass_delay == 0:
+             buf = "disabled";
+          else:
+             buf = str(self.sandglass_delay) + " seconds"
+             if self.sandglass_status == 1:
+                buf += str(" [ paused ]")
+
+          self.post_my_msg("<b>Sandglass:</b> " + str(buf))
+
+          if self.sandglass_skip_interval == 0:
+             buf = "disabled";
+          else:
+             buf = "every " + str(self.sandglass_skip_interval) + " interval(s)."
+          self.post_my_msg("<b>Sandglass force skip:</b> " + str(buf))
+
+          if self.autosave_delay == 0:
+             buf = "disabled";
+          else:
+             buf = str(self.autosave_delay) + " seconds"
+          self.post_my_msg("<b>Autosave:</b> " + str(buf))
+
+          if self.msghand_warning == 0: # VEG 2.2.6
+             buf = "disabled";
+          else:
+             buf = "enabled";
+          self.post_my_msg("<b>Show Message Handler Warning:</b> " + str(buf))
+
+          if self.hide_id == 0:
+             buf = "disabled";
+          else:
+             buf = "enabled";
+          self.post_my_msg("<b>Hide IDs:</b> " + str(buf))
+
+          if self.hide_ondeck == 0:
+             buf = "disabled";
+          else:
+             buf = "enabled";
+          self.post_my_msg("<b>Hide 'On Deck':</b> " + str(buf))
+
+          if self.hide_justmovement == 0:
+             buf = "disabled";
+          else:
+             buf = "enabled";
+          self.post_my_msg("<b>[SR4-only] Hide 'Just Movement':</b> " + str(buf))
+
+          if self.reroll_type == 0: # VEG 2.2.7
+             buf = "reroll inits every round (normal)";
+          else:
+             buf = "keep same inits every round (house rule)";
+          self.post_my_msg("<b>[SR4-only] Init Reroll?:</b> " + str(buf))
+
+          if self.ip_order == 0: # VEG 2.2.7
+             buf = "1/2/3/4 (normal)";
+          elif self.ip_order == 1:
+             buf = "3/1/4/2 (Serbitar's house rule)"
+          elif self.ip_order == 2:
+             buf = "4/1/3/2 (TinkerGnome's house rule)";
+          self.post_my_msg("<b>[SR4-only] IP Order:</b> " + str(buf))
+
+          self.post_my_msg("<b>Now Up Message:</b> " + str(self.message_nowup))
+
+       except:
+          self.post_syserror("Error reading configuration.",0)
+
+    ###############################################################
+    ####    Display Functions
+    ###############################################################
+
+    def inits_list(self, public):
+       if self.isD20():
+          self.inits_list_D20(public)
+       elif self.isSR4():
+          self.inits_list_SR4(public)
+       elif self.isRQ():
+          self.inits_list_RQ(public)
+
+    def inits_list_D20(self, public):
+
+        current_count = str(self.init_count)
+
+        if self.sandglass_delay == 0:
+           current_sandglass = "off"
+        else:
+           current_sandglass = str(self.sandglass_delay) + " sec."
+           if self.sandglass_status == 1:
+              current_sandglass += str(" [ paused ]")
+
+        msg = "<br><br><b>Initiatives (Current Count: " + current_count + "; Sandglass: " + current_sandglass + "):</b><br>"
+        for m in v.init_list:
+
+            # dont show public if character (type 0)
+            if public and m["type"] == 0 and m["hidden"] == 1:
+               continue
+
+            # id in the list appears as 1 to (N+1), in reality it's 0 to N
+            id   = v.init_list.index(m)
+            idplusone = str(id+1)
+
+            if not self.isEnded() and self.init_listslot == id:
+               msg += "<b>"
+
+            # we dont want to show IDs to everyone
+            if public and self.hide_id:
+               msg += " "
+            else:
+               msg += " <font color='#ff0000'>" + idplusone + ") :</font>"
+
+            if self.isEffect(id):
+                # example
+                #   5: (*14) <i>Effect: Tlasco's Bless (4)</i>
+                if public and m["hidden"] == 1:
+                   duration = "?"
+                else:
+                   duration = m["duration"]
+                msg += " [" + str(m["init"])+"] "
+                msg += "<font color='#0000ff'><i>Effect: " + str(m["name"]) + " (" + str(duration) + ")</i>"
+            else:
+                # example
+                #   6: (14) Tlasco
+                msg+= " ["+str(m["init"])+"] <font color='#0000ff'>" + str(m["name"])
+            if self.init_debug:
+               #msg+= " [rank:" + str(m["rank"]) + "; tag: " + str(m["tag"]) + "]</font>"
+               msg+= " [rank:" + str(m["rank"]) + "; type: " + str(m["type"]) + "]</font>"
+               #msg+= " [rank:" + str(m["rank"]) + "]</font>"
+            else:
+               msg+= "</font>"
+
+            if not public and m["hidden"] == 1:
+               msg += " <i>[H]</i>"
+
+            if not self.isEnded() and self.init_listslot == id:
+               msg += "</b>"
+
+            msg += "<br>"
+        self.post_my_msg(msg,public)
+
+
+    def inits_list_SR4(self, public):
+
+        if self.sandglass_delay == 0:
+           current_sandglass = "off"
+        else:
+           current_sandglass = str(self.sandglass_delay) + " sec."
+           if self.sandglass_status == 1:
+              current_sandglass += str(" [ paused ]")
+
+        if self.ip_order == 0:
+            current_ip_order = "1/2/3/4 (normal)"
+        elif self.ip_order == 1:
+            current_ip_order = "3/1/4/2 (Serbitar's house rule)"
+        else:
+            current_ip_order = "4/1/3/2 (TinkerGnome's house rule)"
+        current_count = str(self.init_count)
+        current_pass  = str(self.init_pass)
+
+        msg = "<br><br><b>Initiatives (Current Pass: " + current_pass \
+            + "; Current Count: " + current_count + "), Sandglass: " \
+            + current_sandglass + ", IP Order: " + current_ip_order \
+            + "</b><br>"
+        msg += "<table border='1'><tr>&nbsp;<th></th>"
+
+        if self.init_pass==1:
+            msg += "<th><b><font color='#ff0000'>Pass 1</font></b></th>"
+        else:
+            msg += "<th>Pass 1</th>"
+
+        if self.init_pass==2:
+            msg += "<th><b><font color='#ff0000'>Pass 2</font></b></th>"
+        else:
+            msg += "<th>Pass 2</th>"
+
+        if self.init_pass==3:
+            msg += "<th><b><font color='#ff0000'>Pass 3</font></b></th>"
+        else:
+            msg += "<th>Pass 3</th>"
+
+        if self.init_pass==4:
+            msg += "<th><b><font color='#ff0000'>Pass 4</font></b></th></tr>"
+        else:
+            msg += "<th>Pass 4</th></tr>"
+
+        for m in v.init_list:
+            # id in the list appears as 1 to (N+1), in reality it's 0 to N
+            id   = v.init_list.index(m)
+            idplusone = str(id+1)
+            # example
+            #             | Init Pass 1 | Init Pass 2 | etc...  |          |
+            #   ---------------------------
+            #   6: Tlasco |     13      |     13      | (blank) |  (blank) |
+
+            # we dont want to show IDs to everyone
+            if public and self.hide_id:
+               msg += " <tr><th>"
+            else:
+               msg += " <tr><td><font color='#ff0000'>" + idplusone + ":</font> "
+
+            # bold the current player's name
+            if not self.isEnded() and self.init_listslot == id:
+               msg += "<b><font color='#0000ff'>" + str(m["name"]) + "</font></b></td>"
+            else:
+               msg += "<font color='#0000ff'>" + str(m["name"]) + "</font></td>"
+
+            ip = self.ip_order #ip==0 is 1/2/3/4 (normal) - VEG 2.2.7
+                               #ip==1 is 3/1/4/2 (Serbitar's house rule)
+                               #ip==2 is 4/1/3/2 (TinkerGnome's house rule)
+            passes = m["passes"]
+            # pass one
+            if (ip==0 and passes >= 1) or (ip==1 and passes >= 3) or (ip==2 and passes >= 4):
+                msg += "<td align=center>"+str(m["init"]) + "</td>"
+            else:
+                msg += "<td>&nbsp;</td>"
+            # pass two
+            if (ip==0 and passes >= 2) or (ip==1 and passes >= 1) or (ip==2 and passes >= 1):
+                msg += "<td align=center>"+str(m["init"]) + "</td>"
+            else:
+                msg += "<td>&nbsp;</td>"
+            # pass three
+            if (ip==0 and passes >= 3) or (ip==1 and passes >= 4) or (ip==2 and passes >= 3):
+                msg += "<td align=center>" + str(m["init"]) + "</td>"
+            else:
+                msg += "<td>&nbsp;</td>"
+            # pass four
+            if (ip==0 and passes >= 4) or (ip==1 and passes >= 2) or (ip==2 and passes >= 2):
+                msg += "<td align=center>" + str(m["init"]) + "</td>"
+            else:
+                msg += "<td>&nbsp;</td>"
+
+            msg += "</tr>"
+
+        msg += "</table>"
+        self.post_my_msg(msg, public)
+
+
+    def inits_list_RQ(self, public):
+        current_count = str(self.init_count)
+
+        if self.sandglass_delay == 0:
+           current_sandglass = "off"
+        else:
+           current_sandglass = str(self.sandglass_delay) + " sec."
+           if self.sandglass_status == 1:
+              current_sandglass += str(" [ paused ]")
+
+        msg = "<br><br><b>Initiatives (Current Strike Rank: " + current_count + "; Sandglass: " + current_sandglass + "):</b><br>"
+        for m in v.init_list:
+
+            # dont show public if character (type 0)
+            if public and m["type"] == 0 and m["hidden"] == 1:
+               continue
+
+            # id in the list appears as 1 to (N+1), in reality it's 0 to N
+            id   = v.init_list.index(m)
+            idplusone = str(id+1)
+
+            if not self.isEnded() and self.init_listslot == id:
+               msg += "<b>"
+
+            # we dont want to show IDs to everyone
+            if public and self.hide_id:
+               msg += " "
+            else:
+               msg += " <font color='#ff0000'>" + idplusone + ") :</font>"
+
+            if self.isEffect(id):
+                # example
+                #   5: (*14) <i>Effect: Tlasco's Bless (4)</i>
+                if public and m["hidden"] == 1:
+                   duration = "?"
+                else:
+                   duration = m["duration"]
+                msg += " [" + str(m["init"])+"] "
+                msg += "<font color='#0000ff'><i>Effect: " + str(m["name"]) + " (" + str(duration) + ")</i>"
+            else:
+                # example
+                #   6: (14) Tlasco
+                msg+= " ["+str(m["init"])+"]  <font color='#0000ff'>" + str(m["name"])
+            if self.init_debug:
+               #msg+= " [rank:" + str(m["rank"]) + "; tag: " + str(m["tag"]) + "]</font>"
+               msg+= " [rank:" + str(m["rank"]) + "; type: " + str(m["type"]) + "]</font>"
+               #msg+= " [rank:" + str(m["rank"]) + "]</font>"
+            else:
+               msg+= "</font>"
+
+            if not public and m["hidden"] == 1:
+               msg += " <i>[H]</i>"
+
+            if not self.isEnded() and self.init_listslot == id:
+               msg += "</b>"
+
+            msg += "<br>"
+        self.post_my_msg(msg, public)
+
+
+    def post_now_up(self, id, text):
+
+       #if v.init_list[id]["hidden"] == 0:
+       if self.hide_id == 0:
+          id_str = "<font color='#000000'><b>" + str(id + 1) + ")</b></font>"
+       else:
+          id_str = ""
+
+       try:
+          id_next = self.getNext(id)
+          str_on_deck = ""
+          if id_next != -1:
+             if self.hide_ondeck != 1:
+                 str_on_deck = "<br><i><font color='#000000'>(on deck: [" + str(v.init_list[id_next]["init"]) + "] " + str(v.init_list[id_next]["name"]) + ")</font></i>"
+
+       except Exception, e:
+          print str(e)
+
+       self.post_my_msg("<table border=1 width=100% align='center'>\
+                     <tr>\
+                      <td>" + str(id_str) + " <font color='#ff0000'><b><u>" + str(text)\
+                            + "</u></b></font><font color='#0000ff'><b>"\
+                            + " <font color='#000000'></b>[" + str(v.init_list[id]["init"]) + "]<b></font> "\
+                             + str(v.init_list[id]["name"]) + "</b></font> " + str_on_deck + "\
+                      </td>\
+                     </tr>\
+                    </table>", 1)
+
+    def post_movement(self, id, text):
+
+       #if v.init_list[id]["hidden"] == 0:
+       if self.hide_id == 0:
+          id_str = "<font color='#000000'><b>" + str(id + 1) + ")</b></font>"
+       else:
+          id_str = ""
+
+       self.post_my_msg("<table border=1 width=100% align='center'>\
+                     <tr>\
+                      <td>" + str(id_str) + " <font color='#0000ff'><b><i>" + str(text)\
+                            + "</i></b></font><font color='#0000ff'><b>"\
+                            + " <font color='#000000'></b>[" + str(v.init_list[id]["init"]) + "]<b></font> "\
+                             + str(v.init_list[id]["name"]) + "</b></font> \
+                      </td>\
+                     </tr>\
+                    </table>", 1)
+
+    def post_effect(self, id, text):
+
+       if self.hide_id == 0:
+          id_str = "<font color='#000000'><b>" + str(id + 1) + ")</b></font>"
+       else:
+          id_str = ""
+
+       if v.init_list[id]["hidden"] == 1:
+          rounds_left = "?"
+       else:
+          rounds_left = self.getRoundsLeftInEffect(id)
+
+       if(self.getRoundsLeftInEffect(id) > 0):
+          self.post_my_msg("<table border=1 width=100% align='center'>\
+                     <tr>\
+                      <td>" + str(id_str) + " <font color='#0000ff'><b><i>" + str(text)\
+                             + "</i></b></font><font color='#0000ff'>"\
+                             + " <font color='#000000'>[" + str(v.init_list[id]["init"]) + "]</font> <i>"\
+                             + str(v.init_list[id]["name"]) + "</i></font> : <b>" + str(rounds_left) + "</b> round(s) remaining.\
+                      </td>\
+                     </tr>\
+                    </table>", 1)
+       else:
+          self.post_my_msg("<table border=1 width=100% align='center'>\
+                     <tr>\
+                      <td>" + str(id_str) + " <font color='#0000ff'><b><i>" + str(text)\
+                             + "</i></b></font><i>"\
+                             + " [" + str(v.init_list[id]["init"]) + "] <font color='#0000ff'>"\
+                             + str(v.init_list[id]["name"]) + "</font></i>\
+                      </td>\
+                     </tr>\
+                    </table>", 1)
+
+
+    def post_my_msg(self, msg,send=0):
+        tmp = self.init_recording
+
+        # have to disable the tool in order to post or else you get a clone
+        self.init_recording = 0
+        self.chat.Post(msg,send)
+
+        self.init_recording = tmp
+
+
+    def post_syserror(self, msg, send=0):
+       self.post_my_msg("<font color='#ff0000'><b><i>" + msg + "</i></b></font>", send)
+
+
+    def post_sysmsg(self, msg,send=0):
+       self.post_my_msg("<b>" + msg + "</b>", send)
+
+
+    def toggle_hidden(self, id):
+       try:
+          #print "id : " + str(id)
+          v.init_list[id]["hidden"] = not v.init_list[id]["hidden"]
+          if v.init_list[id]["hidden"] == 1:
+             tmp = "hidden"
+          else:
+             tmp = "visible"
+          self.post_sysmsg(str(v.init_list[id]["name"]) + " now " + tmp + ".")
+
+       except:
+          self.post_syserror("Invalid format. Correct command is: /togglehidden init_#")
+
+    ###############################################################
+    ####    Conditional + Other Functions
+    ###############################################################
+
+    def check_version(self):
+       if(int(replace(orpg.orpg_version.VERSION, ".", "")) < int(replace(self.orpg_min_version, ".", ""))):
+          self.post_sysmsg("<br><font color='#ff0000'>WARNING</font>: You are currently using OpenRPG " + str(orpg.orpg_version.VERSION) + " but you need OpenRPG " + str(self.orpg_min_version) + " or greater to run the Initiative Tool " + str(self.version) + "<br>Use it at your own risk!</b><br>")
+
+    def isD20(self):
+        if self.init_system == "D20":
+            return 1
+        else:
+            return 0
+
+    def isSR4(self):
+        if self.init_system == "SR4":
+            return 1
+        else:
+            return 0
+
+    def isRQ(self):
+        if self.init_system == "RQ":
+            return 1
+        else:
+            return 0
+
+    def isEnded(self):
+       if self.init_end == 0:
+          return 1
+       else:
+          return 0
+
+    def getFirst(self):
+       return(0)
+
+    def getLast(self):
+       return(len(v.init_list)-1)
+
+    def isEffect(self, id):
+        # if it's type 1, then it's an effect
+        try:
+            if (self.isD20() or self.isRQ()) and v.init_list[id]["type"] == 1:
+                return 1
+            else:
+                return 0
+        except:
+            return 0
+
+    def getRoundsLeftInEffect(self, id):
+        rounds_left = v.init_list[id]["duration"]
+        return rounds_left
+
+    def isMovement(self, id): # sr4 only, used to spam movement-only characters
+        try:                  # in passes where they receive no actions
+            passes = v.init_list[id]["passes"]
+            if self.ip_order == 0: # Normal 1/2/3/4 --- VEG 2.2.7
+                if passes < self.init_pass:
+                    return 1
+                else:
+                    return 0
+            elif self.ip_order == 1: # Serbitar's 3/1/4/2 --- VEG 2.2.7
+                if (self.init_pass == 1) and (passes < 3):
+                    return 1
+                elif (self.init_pass == 2) and (passes < 1):
+                    return 1
+                elif (self.init_pass == 3) and (passes < 4):
+                    return 1
+                elif (self.init_pass == 4) and (passes < 2):
+                    return 1
+                else:
+                    return 0
+            elif self.ip_order == 2: # TinkerGnome's 4/1/3/2 --- VEG 2.2.7
+                if (self.init_pass == 1) and (passes < 4):
+                    return 1
+                elif (self.init_pass == 2) and (passes < 1):
+                    return 1
+                elif (self.init_pass == 3) and (passes < 3):
+                    return 1
+                elif (self.init_pass == 4) and (passes < 2):
+                    return 1
+                else:
+                    return 0
+        except:
+            return 0
+
+    def sharesInit(self,id):
+        try:
+            if v.init_list[id]["init"] == v.init_list[self.init_listslot]["init"]:
+                return 1
+            else:
+                return 0
+        except: # last init will be an error, don't start new round yet
+            return 0
+
+    # get the id of the next (non effect) non-hidden character after the given id
+    # return -1 : no character is next
+    def getNext(self, id):
+       lst_size = len(v.init_list)
+
+       # list empty
+       if lst_size < 2:
+          return(-1)
+
+       lst = []
+       id_found = -1
+
+       # if we'r at the last init, the next one is at the beginning of the list
+       if self.isLast(id):
+          for m in v.init_list:
+             id_m = v.init_list.index(m)
+             if self.isHidden(id_m) or self.isEffect(id_m):
+                pass
+             else:
+                id_found = id_m
+                break;
+       else:
+          try:
+             for id_m in range(id+1, self.getLast()+1):
+                if self.isHidden(id_m) or self.isEffect(id_m):
+                   pass
+                else:
+                   id_found = id_m
+                   break;
+
+          except Exception, e:
+             return(-1)
+
+          # maybe we haven't found yet, must search at the beginning of the list
+          if id_found == -1 and not self.isFirst(id):
+             for id_m in range(0, id):
+                if self.isHidden(id_m) or self.isEffect(id_m):
+                   pass
+                else:
+                   id_found = id_m
+                   break;
+
+       if id_found != id:
+          return(id_found)
+       else:
+          return(-1)
+
+    def isHidden(self, id):
+       try:
+          if v.init_list[id]["hidden"] == 1:
+             return(1)
+          else:
+             return(0)
+
+       except:
+             return(0)
+
+    def isFirst(self, id):
+       if id == 0:
+          return 1
+       else:
+          return 0
+
+    def isLast(self, id):
+       if id == (len(v.init_list)-1):
+          return 1
+       else:
+          return 0
+
+    def get_next_rank(self, init):
+       # when adding/changing an object, this function is used to get a unique rank of this init count
+       # new object are added at the end of an init count, so we need a 'rank' lower than the
+       # other object of the same init count.
+       rank = 0;
+       for m in v.init_list:
+          if m["init"] == init:
+             x = m["rank"] - 1
+             if rank > x:
+                rank = x
+       return rank
+
+    def get_unique_tag(self, name):
+        m = hashlib.new()
+        m.update(str(random.random()) + str(name))
+        return m.hexdigest()
+
+### VEG 2.2.6
+### Init GUI stuff below
+
+    def CMD_initgui(self, cmdargs):
+        self.frame = InitToolFrame(NULL, -1, "Initiative Tool GUI")
+        self.frame.Show(true)
+
+class InitToolFrame(wx.Frame):
+    def __init__(self, parent, ID, title):
+        wx.Frame.__init__(self, parent, ID, title, size=(400, 300))
+
+        self.panel = wx.Panel(self,-1) #, style=wx.SUNKEN_BORDER)
+
+        #self.panel.SetBackgroundColour("RED")
+
+        self.x_id = 5
+        self.x_name = 25
+        self.x_init = 200
+        self.x_change = 225
+        self.x_delete = 275
+        self.y = 0
+        b = 0
+
+        wx.StaticText ( self.panel, -1, "ID", (self.x_id, self.y) )
+        wx.StaticText ( self.panel, -1, "Name", (self.x_name, self.y) )
+        wx.StaticText ( self.panel, -1, "Init#", (self.x_init, self.y) )
+        self.y+=20
+
+        var_ID=0
+        var_Name=0
+        var_Init=0
+
+        for m in v.init_list:
+            var_ID = str(v.init_list.index(m)+1) + ")"
+            var_Name = self.strip_html(m["name"])
+            var_Init = str(m["init"])
+
+            wx.StaticText ( self.panel, -1, var_ID, (self.x_id, self.y) )
+            wx.StaticText ( self.panel, -1, var_Name, (self.x_name, self.y) )
+            wx.TextCtrl ( self.panel, -1, var_Init, (self.x_init, self.y), (20,20), wx.TE_READONLY)
+            wx.Button(self.panel, -1, "Change", (self.x_change, self.y), (50,20) )
+            wx.Button(self.panel, -1, "Delete", (self.x_delete, self.y), (50,20) )
+            self.y+=20
+
+        #for i in range(0,4):
+        #    if i == 0:
+        #        var_ID   = str(0)
+        #        var_Name = "Hignar"
+        #        var_Init = str(14)
+        #    elif i == 1:
+        #        var_ID   = str(1)
+        #        var_Name = "Aiur"
+        #        var_Init = str(12)
+        #    elif i == 2:
+        #        var_ID   = str(2)
+        #        var_Name = "Effect: Tlasco's Bless"
+        #        var_Init = str(4)
+        #    elif i == 3:
+        #        var_ID   = str(3)
+        #        var_Name = "Tlasco"
+        #        var_Init = str(3)
+        #    wxStaticText ( self.panel, -1, var_ID, (self.x_id, self.y) )
+        #    wxStaticText ( self.panel, -1, var_Name, (self.x_name, self.y) )
+        #    wxStaticText ( self.panel, -1, var_Init, (self.x_init, self.y) )
+        #    wxButton(self.panel, -1, "Change", (self.x_change, self.y), (50,20) )
+        #    wxButton(self.panel, -1, "Delete", (self.x_delete, self.y), (50,20) )
+        #    self.y+=20
+
+    def strip_html(self, text):
+        finished = 0
+        while not finished:
+            finished = 1
+            # check if there is an open tag left
+            start = text.find("<")
+            if start >= 0:
+                # if there is, check if the tag gets closed
+                stop = text[start:].find(">")
+                if stop >= 0:
+                    # if it does, strip it, and continue loop
+                    text = text[:start] + text[start+stop+1:]
+                    finished = 0
+        return text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxnamesound.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,137 @@
+import os
+import orpg.pluginhandler
+import orpg.dirpath
+import re
+import string
+from orpg.orpgCore import open_rpg
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = 'Name Sound'
+        self.author = 'mDuo13'
+        self.help = "This plays a 'hey!' sound whenever your name is said in chat. It is\n"
+        self.help += "not HTML- or case-sensitive. You can also create nicknames to which the plugin\n"
+        self.help += "will also respond. To add a nickname, type '/xxnick add *name*', where *name*\n"
+        self.help += "is the nickname you want to add. Then, whenever *name* is said in chat, you'll\n"
+        self.help += "hear the sound also. You can remove your nicknames by typing\n"
+        self.help += "'/xxnick del*name*' where *name* is the nickname you wish to delete. Neither is\n"
+        self.help += "case sensitive. Additionally, you can see what nicknames you currently have\n"
+        self.help += "with '/xxnick list'."
+
+        self.antispam = 0
+        self.names = []
+        self.soundfile = ''
+        self.soundplayer = ''
+        self.notify = False
+
+
+    def plugin_enabled(self):
+        self.plugin_addcommand('/xxnick', self.on_xxnick, 'add name|del name|list - This is the command for the namesound plugin')
+        self.plugin_addcommand('/wnotify', self.on_wnotify, 'This will toggle notification on incoming whispers')
+        self.plugin_addcommand('/sfile', self.on_sfile, 'This will set the sound file to use for notification')
+
+        self.names = self.plugindb.GetList("xxnamesound", "names", [])
+
+        self.soundplayer = self.sound_player = open_rpg.get_component('sound')
+
+        tmp = self.plugindb.GetString('xxnamesound', 'wnotify', str(self.notify))
+        if tmp == 'True':
+            self.on_wnotify(None)
+
+        self.soundfile = self.plugindb.GetString('xxnamesound', 'soundfile', orpg.dirpath.dir_struct['plugins'] + 'heya.wav')
+
+
+        reg = []
+
+        if not self.chat.html_strip(self.session.name.lower()) in self.names:
+            self.names.append(self.chat.html_strip(self.session.name.lower()))
+
+        for name in self.names:
+            reg.append("(?<![a-zA-Z0-9>/\#\-])" + name + "(?!\w+|[<])")
+
+        reg = string.join(reg, "|")
+        self.nameReg = re.compile(reg, re.I)
+
+
+    def plugin_disabled(self):
+        self.plugin_removecmd('/xxnick')
+        self.plugin_removecmd('/wnotify')
+        self.plugin_removecmd('/sfile')
+
+
+    def on_wnotify(self, cmdargs):
+        self.notify = not self.notify
+
+        self.plugindb.SetString('xxnamesound', 'wnotify', str(self.notify))
+
+        if self.notify:
+            self.chat.InfoPost("Whisper Notification is On!")
+        else:
+            self.chat.InfoPost("Whisper Notification is Off!")
+
+
+    def on_sfile(self, cmdargs):
+        self.soundfile = cmdargs
+        self.plugindb.SetString('xxnamesound', 'soundfile', cmdargs)
+
+    def on_xxnick(self, cmdargs):
+        args = cmdargs.split(None,-1)
+        reg = []
+        if len(args):
+            name = cmdargs[len(args[0])+1:].lower().strip()
+
+        if len(args) == 0 or args[0] == 'list':
+            name_list = ''
+            i = 0
+            for name in self.names:
+                name_list += name
+                if i < len(self.names)-1:
+                    name_list += ', '
+                i += 1
+            self.chat.InfoPost('Currently chacking for ' + name_list)
+        elif args[0] == 'add':
+            if name not in self.names and name != '':
+                self.names.append(name)
+                self.plugindb.SetList('xxnamesound', 'names', self.names)
+                self.chat.InfoPost('The name ' + name + ' has been added to your nickname list. You will now hear a sound when someone says it in chat.')
+            else:
+                self.chat.InfoPost('The name ' + name + ' is already in your nickname list.')
+        elif args[0] == 'del':
+            if name in self.names:
+                self.names.remove(name)
+                self.plugindb.SetList('xxnamesound', 'names', self.names)
+                self.chat.InfoPost('The name ' + name + ' has been removed from your nickname list.')
+            else:
+                self.chat.InfoPost('The name ' + name + ' is not in your nickname list.')
+
+        for name in self.names:
+            reg.append("(?<![a-zA-Z0-9>/\#\-])" + name + "['s]*(?!\w+|[<])")
+
+        reg = string.join(reg, "|")
+        self.nameReg = re.compile(reg, re.I)
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        if self.antispam > 0:
+            return text, type, name
+
+        if self.nameReg.search(self.chat.html_strip(text.lower())) != None:
+            self.soundplayer.play(self.soundfile)
+            self.antispam = 1
+        elif self.notify and type == 2:
+            self.soundplayer.play(self.soundfile)
+            self.antispam = 1
+
+
+        return text, type, name
+
+    def refresh_counter(self):
+        #This is called once per second. That's all you need to know.
+        if self.antispam:
+            self.antispam -= 0.04
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxnote.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,180 @@
+import os
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = 'Quick Notes'
+        self.author = 'Dj Gilcrease'
+        self.help = "This plugin lets you take quick notes on a subject"
+        self.notes = {}
+
+
+    def plugin_enabled(self):
+        self.notes = self.plugindb.GetDict("xxnote", "notes", {})
+
+        self.plugin_addcommand('/newnote', self.on_newnote, '{Subject} - Will create a new note subject')
+        self.plugin_addcommand('/appendnote', self.on_appendnote, '{Subject}={Note} - Will append a note to {Subject}')
+        self.plugin_addcommand('/delnote', self.on_delnote, '{Subject}={Note_id}|all - Will delete {Note_id} from {Subject} or will delete the subject compleatly')
+        self.plugin_addcommand('/editnote', self.on_editnote, '{Subject}={Note_id} {Note} - Will replace {Note_id} with {Note} for {Subject}')
+        self.plugin_addcommand('/clearnotes', self.on_clearnotes, '- Will clear all of your notes')
+        self.plugin_addcommand('/listnotes', self.on_listnotes, '- Will list all of the note subjects you have')
+        self.plugin_addcommand('/viewnote', self.on_viewnote, '{Subject} - Will display the notes you have for {Subject}')
+        self.plugin_addcommand('/notetonode', self.on_notetonode, '{Subject} - Will create a text node containing your notes for {Subject}')
+        self.plugin_addcommand('/notehelp', self.on_notehelp, '- Will Display the help for each command of this plugin!')
+
+
+    def plugin_disabled(self):
+        self.plugin_removecmd('/newnote')
+        self.plugin_removecmd('/appendnote')
+        self.plugin_removecmd('/editnote')
+        self.plugin_removecmd('/delnote')
+        self.plugin_removecmd('/listnotes')
+        self.plugin_removecmd('/viewnote')
+        self.plugin_removecmd('/notetonode')
+        self.plugin_removecmd('/notehelp')
+        self.plugin_removecmd('/clearnotes')
+
+    def on_newnote(self, cmdargs):
+        if len(cmdargs) == 0:
+            self.on_notehelp('')
+            return
+
+        subject = cmdargs
+        if not self.notes.has_key(subject):
+            self.notes[subject] = []
+            self.plugindb.SetDict("xxnote", "notes", self.notes)
+            self.chat.InfoPost("Created new note for " + subject)
+        else:
+            self.chat.InfoPost("You already have a note with the subject " + subject)
+
+    def on_appendnote(self, cmdargs):
+        args = cmdargs.split("=",-1)
+        if len(args) < 2:
+            self.on_notehelp('')
+            return
+
+        subject = args[0]
+        note = args[1]
+        if not self.notes.has_key(subject):
+            self.notes[subject] = []
+            self.notes[subject].append(note)
+            self.plugindb.SetDict("xxnote", "notes", self.notes)
+            self.chat.InfoPost("Created new note for " + subject)
+        else:
+            self.notes[subject].append(note)
+            self.plugindb.SetDict("xxnote", "notes", self.notes)
+            self.chat.InfoPost("Appended <i>" + note + "</i> to <b>" + subject + "</b>")
+
+        self.on_viewnote(subject)
+
+    def on_editnote(self, cmdargs):
+        args = cmdargs.split("=",-1)
+        if len(args) < 2:
+            self.on_notehelp('')
+            return
+
+        s = args[1].find(" ")
+        subject = args[0]
+        note_id = int(args[1][:s])
+        note = args[1][s:]
+        if not self.notes.has_key(subject):
+            self.notes[subject] = []
+            self.notes[subject].append(note)
+            self.plugindb.SetDict("xxnote", "notes", self.notes)
+            self.chat.InfoPost("Created new note for " + subject)
+        else:
+            self.notes[subject][note_id] = note
+            self.plugindb.SetDict("xxnote", "notes", self.notes)
+            self.chat.InfoPost("Edited <i>" + str(note_id) + ': ' + note + "</i> to <b>" + subject + "</b>")
+
+        self.on_viewnote(subject)
+
+    def on_delnote(self, cmdargs):
+        args = cmdargs.split("=",-1)
+        if len(args) < 2:
+            self.on_notehelp('')
+            return
+
+        subject = args[0]
+        noteid = args[1]
+
+        if noteid == 'all':
+            del self.notes[subject]
+            self.plugindb.SetDict("xxnote", "notes", self.notes)
+            self.chat.InfoPost("Removed subject " + subject)
+        else:
+            del self.notes[subject][int(noteid)]
+            self.plugindb.SetDict("xxnote", "notes", self.notes)
+            self.chat.InfoPost("Removed note " + noteid + " from subject " + subject)
+            self.on_viewnote(subject)
+
+    def on_clearnotes(self, cmdargs):
+        self.notes = {}
+        self.plugindb.SetDict("xxnote", "notes", self.notes)
+        self.chat.InfoPost("Cleared all of your notes")
+
+    def on_listnotes(self, cmdargs):
+        if len(self.notes) == 0:
+            self.chat.InfoPost("You have no notes at this time, use /newnote to create one")
+            return
+        self.chat.InfoPost("The subjects you have notes on are:")
+        for subject in self.notes.keys():
+            self.chat.InfoPost("** " + subject)
+
+    def on_viewnote(self, cmdargs):
+        if len(cmdargs) == 0:
+            self.on_notehelp('')
+            return
+
+        subject = cmdargs
+        if self.notes.has_key(subject):
+            msg = '<table bgcolor="#cccccc"><tr><td colspan="2"><b><font color="#000000">Notes for '
+            msg += subject
+            msg += ':</font></b></td></tr><tr><td width="10">ID</td><td>Note Text</td></tr>'
+            noteid = 0
+            for n in self.notes[subject]:
+                msg += '<tr><td width="10"><font color="#000000">'
+                msg += str(noteid)
+                msg += '</font></td>'
+                msg += '<td><font color="#000000">'
+                msg += n
+                msg += '</font></td></tr>'
+                noteid += 1
+
+            msg += '</table>'
+            self.chat.InfoPost(msg)
+        else:
+            self.chat.InfoPost("You have no notes for " + subject)
+
+    def on_notetonode(self, cmdargs):
+        if len(cmdargs) == 0:
+            self.on_notehelp('')
+            return
+
+        subject = cmdargs
+        if self.notes.has_key(subject):
+            node = '<nodehandler class="textctrl_handler" icon="note" module="forms" name="'
+            node += subject
+            node += '" version="1.0"><text multiline="1" send_button="1">'
+            for note in self.notes[subject]:
+                node += note + "\n"
+            node += '</text></nodehandler>'
+            self.gametree.insert_xml(node)
+        else:
+            self.chat.InfoPost("You have no notes for " + subject)
+
+    def on_notehelp(self, cmdargs):
+        self.chat.InfoPost('/newnote ' + self.cmdlist['/newnote']['help'])
+        self.chat.InfoPost('/appendnote ' + self.cmdlist['/appendnote']['help'])
+        self.chat.InfoPost('/delnote ' + self.cmdlist['/delnote']['help'])
+        self.chat.InfoPost('/listnotes ' + self.cmdlist['/listnotes']['help'])
+        self.chat.InfoPost('/clearnotes ' + self.cmdlist['/clearnotes']['help'])
+        self.chat.InfoPost('/notetonode ' + self.cmdlist['/notetonode']['help'])
+        self.chat.InfoPost('/viewnote ' + self.cmdlist['/viewnote']['help'])
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxooc.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,32 @@
+import os
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    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 = 'OOC Comments Tool'
+        self.author = 'mDuo13'
+        self.help = "Type '/ooc *message*' to send '(( *message* ))' -- it just preformats\n"
+        self.help += "out of character comments for you."
+
+    def plugin_enabled(self):
+        #This is where you set any variables that need to be initalized when your plugin starts
+
+        self.plugin_addcommand('/ooc', self.on_ooc, 'message - This puts (( message )) to let other players know you are talking out of character')
+
+
+    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
+        #such as closing windows created by the plugin
+
+        self.plugin_removecmd('/ooc')
+
+    def on_ooc(self, cmdargs):
+        #this is just an example function for a command you create create your own
+        self.chat.ParsePost('(( ' + cmdargs + ' ))', 1, 1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxquotebox.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,243 @@
+import os
+import orpg.dirpath
+import orpg.plugindb
+import orpg.pluginhandler
+from orpg.tools.rgbhex import RGBHex
+import wx
+
+__version__ = "2.0" # updated for OpenRPG+ 1.7.0
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    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 = "Quote Box"
+        self.author = "frobnic8 (erskin@eldritch.org), mDuo13, Veggiesama"
+        self.help = """This plugin allows you to put your text in a special box. Again, while
+            it could (except for the color selection) be easily done with a text node,
+            this plugin makes it faster, presents a consistant appearance, and saves
+            you from silly HTML errors. Just type "/box" and some text to get started.
+
+            Type /quotebox to load a help node into your game tree.
+
+            Advanced commands:
+            '/box': Displays the current settings.
+            '/box *text*': Displays *text* in the box.
+            '/box_size *size*': Lets you set the size of the text
+            \tin the box, where *size* is a number from 1 to 7. The default is 3.
+            '/box_bgcol': Brings up a color selection dialog for picking the background
+            \tcolor of the box. The default is pale grey.
+            '/box_fontcol': Brings up a color selection dialog for picking the color of
+            \tthe font in the box. The default is black.
+            '/box_border': Toggles a small black border around the box.
+            '/box_italics': Toggles whether the text in the box should be italicized or not.
+            '/box_bold': Toggles whether the text in the box should be bolded or not.
+            '/box_type': Toggles between the old and new versions of the box. Defaults to new.
+            '/box_default': Sets plugin to default settings.
+
+            Credits go out to Travis for the original HTML code and Sunless DM for
+            bringing it to mDuo13's attention."""
+
+        self.version = __version__
+        self.orpg_min_version="1.7.0"
+
+        self.fontcolor = "#000000"
+        self.bgcolor="#aaaaaa"
+        self.size = 3
+        self.border = 0
+        self.italics = 0
+        self.bold = 1
+        self.boxtype = 1
+        self.r_h = RGBHex()
+
+    def plugin_enabled(self):
+        self.fontcolor  = str(self.plugindb.GetString("xxquotebox", "fontcolor", "#000000"))
+        self.bgcolor    = str(self.plugindb.GetString("xxquotebox", "bgcolor", "#aaaaaa"))
+        self.size       = int(self.plugindb.GetString("xxquotebox", "size", "3"))
+        self.border     = int(self.plugindb.GetString("xxquotebox", "border", "0"))
+        self.italics    = int(self.plugindb.GetString("xxquotebox", "italics", "0"))
+        self.bold       = int(self.plugindb.GetString("xxquotebox", "bold", "1"))
+        self.boxtype    = int(self.plugindb.GetString("xxquotebox", "boxtype", "1"))
+
+        self.plugin_addcommand('/box',          self.on_box,        "'/box': Displays the current settings <b>OR</b> '/box *text*': Displays *text* in the box.")
+        self.plugin_addcommand('/box_size',     self.on_box_size,   "'/box_size *size*': Lets you set the size of the text in the box, where *size* is a number from 1 to 7. The default is 3.", False)
+        self.plugin_addcommand('/box_bgcol',    self.on_box_bgcol,  "'/box_bgcol': Brings up a color selection dialog for picking the background color of the box. The default is pale grey.", False)
+        self.plugin_addcommand('/box_fontcol',  self.on_box_fontcol,"'/box_fontcol': Brings up a color selection dialog for picking the color of the font in the box. The default is black.", False)
+        self.plugin_addcommand('/box_border',   self.on_box_border, "'/box_border': Toggles a small black border around the box.", False)
+        self.plugin_addcommand('/box_italics',  self.on_box_italics,"'/box_italics': Toggles whether the text in the box should be italicized or not.", False)
+        self.plugin_addcommand('/box_bold',     self.on_box_bold,   "'/box_bold': Toggles whether the text in the box should be bolded or not.", False)
+        self.plugin_addcommand('/box_type',     self.on_box_type,   "'/box_type': Toggles between the old and new versions of the box. Defaults to new.", False)
+        self.plugin_addcommand('/box_default',  self.on_box_default,"'/box_default': Sets plugin to default settings.", False)
+        self.plugin_addcommand('/quotebox',     self.on_quotebox,   "<b>TYPE /QUOTEBOX TO BEGIN.</b> Loads up a help node into gametree.")
+
+    def plugin_disabled(self):
+        self.plugin_removecmd('/box')
+        self.plugin_removecmd('/box_size')
+        self.plugin_removecmd('/box_bgcol')
+        self.plugin_removecmd('/box_fontcol')
+        self.plugin_removecmd('/box_border')
+        self.plugin_removecmd('/box_italics')
+        self.plugin_removecmd('/box_bold')
+        self.plugin_removecmd('/box_type')
+        self.plugin_removecmd('/box_default')
+        self.plugin_removecmd('/box_quotebox')
+
+    def save_changes(self):
+        self.plugindb.SetString("xxquotebox", "fontcolor",str(self.fontcolor))
+        self.plugindb.SetString("xxquotebox", "bgcolor",  str(self.bgcolor))
+        self.plugindb.SetString("xxquotebox", "size",     str(self.size))
+        self.plugindb.SetString("xxquotebox", "border",   str(self.border))
+        self.plugindb.SetString("xxquotebox", "italics",  str(self.italics))
+        self.plugindb.SetString("xxquotebox", "bold",     str(self.bold))
+        self.plugindb.SetString("xxquotebox", "boxtype",  str(self.boxtype))
+
+    def on_box(self, cmdargs):
+        #this is just an example function for a command you create.
+        # cmdargs contains everything you typed after the command
+        # so if you typed /test this is a test, cmdargs = this is a test
+        # args are the individual arguments split. For the above example
+        # args[0] = this , args[1] = is , args[2] = a , args[3] = test
+        args = cmdargs.split(None,-1)
+
+        # shows status information of the plugin in a dialog window
+        if len(args) == 0:
+            if self.boxtype == 0:
+                msg_boxtype = "old"
+            else:
+                msg_boxtype = "new"
+
+            self.dlg = wx.MessageDialog(None,
+                 'Current settings used by the Quote Box plugin:\n'+
+                 '\nsize: '+str(self.size)+'\nbgcolor: '+self.bgcolor+
+                 '\nfontcolor: '+self.fontcolor+'\nborder: '+str(self.border)+
+                 '\nitalics: '+str(self.italics)+'\nbold: '+str(self.bold)+
+                 '\nboxtype: '+msg_boxtype+'\n'+
+                 '\nSee the Plugin Info from the Tools/Plugin menu for '+
+                 'more information.', 'Quote Box Current Settings', wx.OK)
+            self.dlg.ShowModal()
+            self.dlg.Destroy()
+
+        # making a box and using the cmdargs as the text inside
+        else:
+            msg = cmdargs
+            if self.boxtype == 0: #old
+                box = '<table bgcolor="' + self.bgcolor + '" border="0" cellpadding="0" cellspacing="0" width="100%">'
+                box += '<tr><td><font size="' + str(self.size) + '" color="' + self.fontcolor + '">'
+                box += '<b>' + msg + '</b></font></table>'
+                self.chat.Post(box, True, True)
+            else: #new
+                if self.border:
+                    border = " border='1'"
+                else:
+                    border = ""
+
+                if self.italics:
+                    italics = "<i>"
+                    enditalics = "</i>"
+                else:
+                    italics = ""
+                    enditalics = ""
+
+                if self.bold:
+                    bold = "<b>"
+                    endbold = "</b>"
+                else:
+                    bold = ""
+                    endbold = ""
+
+                box = '<br><center><table bgcolor="' + self.bgcolor + '" width="80%"'
+                box += 'cellpadding="' + str(int(self.size * 5)) + '" cellspacing="0" ' + border + '>'
+                box += '<tr><td><font size="' + str(self.size) + '" color="' + self.fontcolor + '">'
+                box += bold + italics + msg + enditalics + endbold
+                box += '</font></td></tr></table></center>'
+                self.chat.Post(box, True, True)
+
+    # changes size of font, as well as cell-padding size
+    # cell padding size = font size * 5
+    def on_box_size(self, cmdargs):
+        try:
+            self.size = int(cmdargs)
+            self.save_changes()
+            self.chat.InfoPost("Box size set to <b>" + str(self.size) + "</b>.")
+        except:
+            self.chat.InfoPost("That is not a valid font size.")
+
+    # opens a color-choosing dialog for background color of table
+    def on_box_bgcol(self, cmdargs):
+        color = self.r_h.do_hex_color_dlg(None)
+        if color != None:
+            self.bgcolor = color
+        self.save_changes()
+
+    # opens a color-choosing dialog for font color of text
+    def on_box_fontcol(self, cmdargs):
+        color = self.r_h.do_hex_color_dlg(None)
+        if color != None:
+            self.fontcolor = color
+        self.save_changes()
+
+    # toggles whether border should be on or off
+    def on_box_border(self, cmdargs):
+        if self.border:
+            self.border=0
+            self.chat.InfoPost("No longer using border on table.")
+        else:
+            self.border=1
+            self.chat.InfoPost("Using border on table.")
+        self.save_changes()
+
+    # toggles whether text should be italics or not
+    def on_box_italics(self, cmdargs):
+        if self.italics:
+            self.italics=0
+            self.chat.InfoPost("No longer using italic text.")
+        else:
+            self.italics=1
+            self.chat.InfoPost("Using italic text.")
+        self.save_changes()
+
+    # toggles whether text should be bold or not
+    def on_box_bold(self, cmdargs):
+        if self.bold:
+            self.bold=0
+            self.chat.InfoPost("No longer using bold text.")
+        else:
+            self.bold=1
+            self.chat.InfoPost("Using bold text.")
+        self.save_changes()
+
+    # toggles between old-style and new-style boxes
+    def on_box_type(self, cmdargs):
+        if self.boxtype:
+            self.boxtype=0
+            self.chat.InfoPost("Now using old-style boxes (thin, full-width, left-aligned)")
+        else:
+            self.boxtype=1
+            self.chat.InfoPost("Now using new-style boxes (in middle, with thick borders)")
+        self.save_changes()
+
+    # reverts all quotebox settings back to default
+    def on_box_default(self, cmdargs):
+        self.fontcolor = "#000000"
+        self.bgcolor="#aaaaaa"
+        self.size = 3
+        self.border = 0
+        self.italics = 0
+        self.bold = 1
+        self.boxtype = 1
+
+        self.chat.InfoPost("Quotebox plugin reverted to default settings.")
+        self.save_changes()
+
+    # loads up quotebox.xml as a node in the gametree
+    def on_quotebox(self, cmdargs):
+        f = open(orpg.dirpath.dir_struct["plugins"]+ "quotebox.xml","r")
+        self.gametree.insert_xml(f.read())
+        f.close()
+        return 1
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxsimpleinit.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,816 @@
+import os
+import orpg.pluginhandler
+from orpg.orpgCore import *
+import wx
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !openrpg : instance of the the base openrpg control
+    def __init__(self, plugindb, parent):
+        orpg.pluginhandler.PluginHandler.__init__(self, plugindb, parent)
+        self.orpgframe = open_rpg.get_component('frame')
+
+        # The Following code should be edited to contain the proper information
+        self.name = 'Simple Init'
+        self.author = 'Dj Gilcrease'
+        self.help = 'This is a simplistic Init tool that does not relie on Chat message parsing'
+
+        #You can set variables below here. Always set them to a blank value in this section. Use plugin_enabled
+        #to set their proper values.
+
+    def plugin_menu(self):
+        self.menu = wx.Menu()
+
+        self.toggle = self.menu.AppendCheckItem(wx.ID_ANY, 'Show')
+        self.topframe.Bind(wx.EVT_MENU, self.on_init, self.toggle)
+        self.toggle.Check(True)
+
+
+    def plugin_enabled(self):
+        self.plugin_addcommand('/inittoggle', self.on_init, '- This will Show or Hide the Init window')
+        self.plugin_addcommand('/rollinit', self.rollInit, '- This will roll a new set of Inits', False)
+        self.plugin_addcommand('/startcombat', self.startInit, '- This will start the combat', False)
+        self.plugin_addcommand('/nextplayer', self.advanceInit, '- This will advance the Initiative to the next Player', False)
+        self.plugin_addcommand('/stopcombat', self.stopInit, '- This will end the combat', False)
+        self.plugin_addcommand('/pausecombat', self.pauseInit, '- This will pause the auto Advancer', False)
+
+        self.InitSaves = self.plugindb.GetDict("xxsimpleinit", "InitSaves", {})
+        self.frame = InitFrame(self)
+        self.frame.Show()
+        self.advanceTimer = wx.Timer(self.frame, wx.NewId())
+        self.frame.Bind(wx.EVT_TIMER, self.advanceInit, self.advanceTimer)
+        self.buttonTimer = wx.Timer(self.frame, wx.NewId())
+        self.frame.Bind(wx.EVT_TIMER, self.buttonCheck, self.buttonTimer)
+        self.buttonTimer.Start(250)
+        self.autoAdvancePaused = False
+        #self.PluginMenu()
+
+    def plugin_disabled(self):
+        self.plugin_removecmd('/inittoggle')
+        self.plugin_removecmd('/rollinit')
+        self.plugin_removecmd('/startcombat')
+        self.plugin_removecmd('/nextplayer')
+        self.plugin_removecmd('/stopcombat')
+        self.plugin_removecmd('/pausecombat')
+
+        self.advanceTimer.Stop()
+        del self.advanceTimer
+
+        self.buttonTimer.Stop()
+        del self.buttonTimer
+
+        try:
+            self.frame.Destroy()
+        except:
+            pass
+
+    def on_init(self, cmdargs):
+        if self.frame.IsShown():
+            self.toggle.Check(False)
+            self.frame.Hide()
+        else:
+            self.toggle.Check(True)
+            self.frame.Show()
+        print self.toggle.IsChecked()
+
+    def startInit(self, evt=None):
+        if self.frame.initList.GetItemCount() == 0:
+            self.chat.InfoPost('You need to add some stuff to the Init list before you start')
+            return
+
+        if not self.frame.startButton.IsEnabled():
+            self.chat.InfoPost('Combat Round has already started! You must Stop the current combat to start a new one.')
+            return
+
+        self.orpgframe.Freeze()
+        self.frame.Freeze()
+        self.frame.nextButton.Enable()
+        self.frame.stopButton.Enable()
+        self.frame.startButton.Disable()
+        self.frame.rollButton.Disable()
+        self.frame.autoAdvanceCheck.Disable()
+        self.frame.autoAdvanceList.Disable()
+        self.frame.autoAdvanceToggle.Enable()
+        self.frame.addButton.Disable()
+        self.frame.editButton.Disable()
+        self.frame.deleteButton.Disable()
+        self.frame.saveButton.Disable()
+        self.frame.loadButton.Disable()
+        self.frame.clearButton.Disable()
+
+        self.frame.currentInit = -1
+        self.round = 0
+
+        self.chat.Post('<font color="#00ff00" size="4"><b>============ START COMBAT ============</b></font>', True, True)
+        self.advanceInit()
+        self.frame.Thaw()
+
+        if self.frame.autoAdvanceCheck.IsChecked():
+            time = int(60000*int(self.frame.autoAdvanceList.GetStringSelection()))
+            self.advanceTimer.Start(time)
+        else:
+            self.frame.addButton.Enable()
+            self.frame.editButton.Enable()
+            self.frame.deleteButton.Enable()
+
+        if self.frame.IsShown():
+            wx.CallAfter(self.frame.SetFocus)
+
+        wx.CallAfter(self.orpgframe.Thaw)
+
+    def stopInit(self, evt=None):
+        if not self.frame.stopButton.IsEnabled():
+            self.chat.InfoPost('Combat is already ended!')
+            return
+
+        self.frame.Freeze()
+        self.frame.nextButton.Disable()
+        self.frame.stopButton.Disable()
+        self.frame.startButton.Enable()
+        self.frame.rollButton.Enable()
+        self.frame.autoAdvanceCheck.Enable()
+        self.frame.autoAdvanceList.Enable()
+        self.frame.autoAdvanceToggle.Disable()
+        self.frame.addButton.Enable()
+        self.frame.editButton.Enable()
+        self.frame.deleteButton.Enable()
+        self.frame.saveButton.Enable()
+        self.frame.loadButton.Enable()
+        self.frame.clearButton.Enable()
+
+        for i in xrange(0, self.frame.initList.GetItemCount()):
+            self.frame.currentInit = i
+            if self.frame.currentInit.type == 'Effect':
+                self.frame.initList.DeleteItem(i)
+
+        self.frame.currentInit = 0
+
+        if self.autoAdvancePaused and self.frame.autoAdvanceCheck.IsChecked():
+            self.pauseInit('No Adv')
+        self.frame.Thaw()
+
+        self.advanceTimer.Stop()
+        self.chat.Post('<font color="#ff0000" size="4"><b>============ END COMBAT ============</b></font>', True, True)
+
+    def advanceInit(self, evt=None):
+        if not self.frame.nextButton.IsEnabled():
+            self.chat.InfoPost('You must first Start combat inorder to advance the initiative turn!')
+            return
+
+        self.frame.currentInit = self.frame.initIdx+1
+        if self.frame.currentInit.type == 'Effect':
+            newDur = str(int(self.frame.currentInit.duration)-1)
+            self.frame.initList.SetStringItem(self.frame.initIdx, 2, newDur)
+            msg = '<br><table width="100%" border="1"><tr><td align="center"><center><u><b><font color="#ff0000" size="4">EFFECT NOTICE</font></b></u></center></td></tr>'
+            msg += '<tr><td align="center"><font color="#000000">' + self.frame.currentInit.name + ' has ' + newDur + ' rounds remaining</font></td></tr></table>'
+            self.chat.Post(msg, True, True)
+            wx.CallAfter(self.advanceInit)
+        else:
+            msg = '<br><table width="100%" border="1"><tr><td align="center"><center><u><font color="#ff0000" size="4"><b>' + self.frame.nextMessage.GetValue() + '</b></font></u></center></td></tr>'
+            msg += '<tr><td align="center"><b><font size="3"><font color="#ff0000">' + str(self.frame.initIdx+1) + ':</font> '
+            msg += '<font color="#0000ff">(' + self.frame.currentInit.init + ')</font> '
+            msg += '<font color="#000000">' + self.frame.currentInit.name + '</b></font></font></td></tr></table>'
+            self.chat.Post(msg, True, True)
+
+        if self.frame.currentInit.type == 'Effect' and int(self.frame.currentInit.duration) <= 0:
+            self.frame.Freeze()
+            cidx = self.frame.initIdx
+            self.frame.initList.DeleteItem(cidx)
+            self.frame.currentInit = cidx
+            self.frame.Thaw()
+
+    def pauseInit(self, evt=None):
+        self.frame.Freeze()
+        if not self.autoAdvancePaused:
+            self.frame.autoAdvanceToggle.SetLabel('Resume Auto Advance')
+            self.autoAdvancePaused = True
+            self.advanceTimer.Stop()
+            self.frame.nextButton.Disable()
+            self.frame.addButton.Enable()
+            self.frame.editButton.Enable()
+            self.frame.deleteButton.Enable()
+        else:
+            self.frame.autoAdvanceToggle.SetLabel('Pause Auto Advance')
+            self.frame.nextButton.Enable()
+            self.frame.addButton.Disable()
+            self.frame.editButton.Disable()
+            self.frame.deleteButton.Disable()
+            self.autoAdvancePaused = False
+            if evt != 'No Adv':
+                self.advanceInit()
+            self.advanceTimer.Start()
+        self.frame.Thaw()
+
+    def newRound(self):
+        self.round += 1
+        msg = '<br><hr><font color="#ff0000" size="4"><b>End of Round #' + str(self.round-1) + ', Starting Round #' + str(self.round) + '</b></font><hr>'
+        self.chat.Post(msg, True, True)
+
+    def rollD20Init(self):
+        if not self.frame.rollButton.IsEnabled():
+            self.chat.InfoPost("You cannot roll new Inits at this time")
+            return
+        self.orpgframe.Freeze()
+        self.frame.Freeze()
+        msg = '<br><font color="#0000ff" size="4"><b>============ START INIT LIST ============</b></font><br>'
+        msg += '<font color="#000000"><b>'
+        for i in xrange(0, self.frame.initList.GetItemCount()):
+            self.frame.currentInit = i
+            if self.frame.currentInit.manual == 'No':
+                initRoll = self.chat.ParseDice('[1d20' + self.frame.currentInit.initMod + ']')
+
+                initRoll = initRoll.split('(')
+                initRoll = initRoll[1].replace(')','')
+                self.frame.initList.SetStringItem(i, 4, initRoll)
+                self.frame.initList.SetItemData(i, int(initRoll))
+            else:
+                initRoll = self.frame.currentInit.init
+
+            if self.frame.currentInit.type != 'Effect':
+                msg += '<br>' + self.frame.currentInit.name + ' = <font color="#ff0000">' + initRoll + '</font>'
+
+        msg += '</b></font><br>'
+
+        self.frame.initList.SortItems(self.frame.initSort)
+        msg += '<font color="#0000ff" size="4"><b>============ END INIT LIST ============</b></font>'
+        self.chat.Post(msg, True, True)
+
+        self.frame.Thaw()
+        if self.frame.IsShown():
+            wx.CallAfter(self.frame.SetFocus)
+        wx.CallAfter(self.orpgframe.Thaw)
+
+    #Events
+    def rollInit(self, evt):
+        if self.frame.initRollType.GetStringSelection() == 'd20':
+            self.rollD20Init()
+
+    def buttonCheck(self, evt):
+        if self.autoAdvancePaused:
+            return
+        if self.frame.initList.GetItemCount() == 0:
+            self.frame.Freeze()
+            self.advanceTimer.Stop()
+            self.frame.startButton.Disable()
+            self.frame.rollButton.Disable()
+            self.frame.nextButton.Disable()
+            self.frame.stopButton.Disable()
+            self.frame.editButton.Disable()
+            self.frame.deleteButton.Disable()
+            self.frame.saveButton.Disable()
+            self.frame.clearButton.Disable()
+            self.frame.Thaw()
+        elif not self.frame.stopButton.IsEnabled():
+                self.frame.startButton.Enable()
+                self.frame.rollButton.Enable()
+                self.frame.editButton.Enable()
+                self.frame.deleteButton.Enable()
+                self.frame.saveButton.Enable()
+                self.frame.clearButton.Enable()
+
+        if not self.frame.autoAdvanceCheck.IsChecked():
+            self.frame.autoAdvanceToggle.Disable()
+            self.autoAdvancePaused = True
+        elif self.frame.autoAdvanceCheck.IsChecked() and self.frame.stopButton.IsEnabled():
+            self.frame.autoAdvanceToggle.Enable()
+
+class InitFrame(wx.Frame):
+    def __init__(self, plugin):
+        self.plugin = plugin
+	self.toggle = plugin.toggle
+        self.log = open_rpg.get_component('log')
+        self.log.log("Enter InitFrame", ORPG_DEBUG)
+
+        wx.Frame.__init__(self, None, wx.ID_ANY, title="Simple Init", style=wx.DEFAULT_FRAME_STYLE)
+        self.SetOwnBackgroundColour('#EFEFEF')
+
+        self.dir_struct = open_rpg.get_component('dir_struct')
+        self.settings = open_rpg.get_component('settings')
+        self.xml = open_rpg.get_component('xml')
+        self.validate = open_rpg.get_component('validate')
+
+        self.Freeze()
+        self.buildMenu()
+        self.buildButtons()
+        self.buildGUI()
+        wx.CallAfter(self.InitSetup)
+
+        self.Bind(wx.EVT_CLOSE, self.onCloseWindow)
+
+        self.log.log("Exit InitFrame", ORPG_DEBUG)
+
+    def InitSetup(self):
+        self.chat = open_rpg.get_component('chat')
+        self.gametree = open_rpg.get_component('tree')
+        self.map = open_rpg.get_component('map')
+        self.session = open_rpg.get_component('session')
+
+        self.initIdx = -1
+        self.Thaw()
+
+    def initSort(self, item1, item2):
+        if item1 == item2:
+            return 0
+        elif item1 > item2:
+            return -1
+        elif item1 < item2:
+            return 1
+
+        return 0
+
+    def onCloseWindow(self, evt):
+        self.Hide()
+	self.toggle.Check(False)
+
+    def buildMenu(self):
+        self.log.log("Enter InitFrame->buildMenu(self)", ORPG_DEBUG)
+
+        initmenu = wx.Menu()
+
+        item = wx.MenuItem(initmenu, wx.ID_ANY, "Add\tInsert", "Add")
+        self.Bind(wx.EVT_MENU, self.OnMB_InitAdd, item)
+        initmenu.AppendItem(item)
+
+        item = wx.MenuItem(initmenu, wx.ID_ANY, "Edit\tCtrl+E", "Edit")
+        self.Bind(wx.EVT_MENU, self.OnMB_InitEdit, item)
+        initmenu.AppendItem(item)
+
+        item = wx.MenuItem(initmenu, wx.ID_ANY, "Delete\tCtrl+D", "Delete")
+        self.Bind(wx.EVT_MENU, self.OnMB_InitDelete, item)
+        initmenu.AppendItem(item)
+
+        item = wx.MenuItem(initmenu, wx.ID_ANY, "Clear\tCtrl+C", "Clear")
+        self.Bind(wx.EVT_MENU, self.OnMB_InitClear, item)
+        initmenu.AppendItem(item)
+
+        initmenu.AppendSeparator()
+
+        item = wx.MenuItem(initmenu, wx.ID_ANY, "Next\tEnter", "Next")
+        self.Bind(wx.EVT_MENU, self.plugin.advanceInit, item)
+        initmenu.AppendItem(item)
+
+        initmenu.AppendSeparator()
+
+        item = wx.MenuItem(initmenu, wx.ID_ANY, "Save\tCtrl+S", "Save Current Init List")
+        self.Bind(wx.EVT_MENU, self.OnMB_InitSave, item)
+        initmenu.AppendItem(item)
+
+        item = wx.MenuItem(initmenu, wx.ID_ANY, "Load", "Load New Init List")
+        self.Bind(wx.EVT_MENU, self.OnMB_InitLoad, item)
+        initmenu.AppendItem(item)
+
+        menu = wx.MenuBar()
+        menu.Append(initmenu, "&Init")
+
+        self.SetMenuBar(menu)
+
+        self.log.log("Exit InitFrame->buildMenu(self)", ORPG_DEBUG)
+
+    def buildButtons(self):
+        self.log.log("Enter InitFrame->buildButtons(self)", ORPG_DEBUG)
+
+        self.autoAdvanceCheck = wx.CheckBox(self, wx.ID_ANY, "")
+        self.autoAdvanceCheck.SetValue(True)
+        self.autoAdvanceList = wx.Choice(self, wx.ID_ANY, choices=['1','2','3','4','5'])
+        self.autoAdvanceList.SetSelection(2)
+        self.autoAdvanceToggle = wx.Button(self, wx.ID_ANY, "Pause Auto Advance")
+        self.Bind(wx.EVT_BUTTON, self.plugin.pauseInit, self.autoAdvanceToggle)
+        self.autoAdvanceToggle.Disable()
+
+        self.psizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.psizer.Add(wx.StaticText(self, wx.ID_ANY, "Auto Advance"), 0, wx.ALIGN_CENTER|wx.ALL, 5)
+        self.psizer.Add(self.autoAdvanceCheck, 0, wx.ALIGN_CENTER|wx.ALL, 0)
+        self.psizer.Add(wx.StaticText(self, wx.ID_ANY, "Every"), 0, wx.ALIGN_CENTER|wx.ALL, 5)
+        self.psizer.Add(self.autoAdvanceList, 0, wx.ALIGN_CENTER|wx.ALL, 0)
+        self.psizer.Add(wx.StaticText(self, wx.ID_ANY, "Minutes"), 0, wx.ALIGN_CENTER|wx.ALL, 5)
+        self.psizer.Add(self.autoAdvanceToggle, 0, wx.ALIGN_CENTER|wx.ALL, 10)
+
+        self.addButton = wx.Button(self, wx.ID_ANY, "Add")
+        self.Bind(wx.EVT_BUTTON, self.OnMB_InitAdd, self.addButton)
+        self.editButton = wx.Button(self, wx.ID_ANY, "Edit")
+        self.editButton.Disable()
+        self.Bind(wx.EVT_BUTTON, self.OnMB_InitEdit, self.editButton)
+        self.deleteButton = wx.Button(self, wx.ID_ANY, "Delete")
+        self.deleteButton.Disable()
+        self.Bind(wx.EVT_BUTTON, self.OnMB_InitDelete, self.deleteButton)
+        self.saveButton = wx.Button(self, wx.ID_ANY, "Save")
+        self.saveButton.Disable()
+        self.Bind(wx.EVT_BUTTON, self.OnMB_InitSave, self.saveButton)
+        self.loadButton = wx.Button(self, wx.ID_ANY, "Load")
+        self.Bind(wx.EVT_BUTTON, self.OnMB_InitLoad, self.loadButton)
+        self.clearButton = wx.Button(self, wx.ID_ANY, "Clear")
+        self.clearButton.Disable()
+        self.Bind(wx.EVT_BUTTON, self.OnMB_InitClear, self.clearButton)
+
+
+        self.esizer = wx.BoxSizer(wx.VERTICAL)
+        self.esizer.Add(self.addButton, 0, wx.EXPAND|wx.ALL, 2)
+        self.esizer.Add(self.editButton, 0, wx.EXPAND|wx.ALL, 3)
+        self.esizer.Add(self.deleteButton, 0, wx.EXPAND|wx.ALL, 2)
+        self.esizer.Add(self.saveButton, 0, wx.EXPAND|wx.ALL, 3)
+        self.esizer.Add(self.loadButton, 0, wx.EXPAND|wx.ALL, 2)
+        self.esizer.Add(self.clearButton, 0, wx.EXPAND|wx.ALL, 3)
+
+        self.rollButton = wx.Button(self, wx.ID_ANY, "Roll New Inits")
+        self.Bind(wx.EVT_BUTTON, self.plugin.rollInit, self.rollButton)
+        self.startButton = wx.Button(self, wx.ID_ANY, "Start")
+        self.Bind(wx.EVT_BUTTON, self.plugin.startInit, self.startButton)
+        self.nextButton = wx.Button(self, wx.ID_ANY, "Next")
+        self.nextButton.Disable()
+        self.Bind(wx.EVT_BUTTON, self.plugin.advanceInit, self.nextButton)
+        self.stopButton = wx.Button(self, wx.ID_ANY, "Stop")
+        self.stopButton.Disable()
+        self.Bind(wx.EVT_BUTTON, self.plugin.stopInit, self.stopButton)
+
+        self.asizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.asizer.Add(self.rollButton, 0, wx.EXPAND|wx.ALL, 5)
+        self.asizer.Add(self.startButton, 0, wx.EXPAND|wx.ALL, 5)
+        self.asizer.Add(self.nextButton, 0, wx.EXPAND|wx.ALL, 5)
+        self.asizer.Add(self.stopButton, 0, wx.EXPAND|wx.ALL, 5)
+
+        self.log.log("Exit InitFrame->buildButtons(self)", ORPG_DEBUG)
+
+    def buildGUI(self):
+        self.log.log("Enter InitFrame->buildGUI(self)", ORPG_DEBUG)
+
+        sizer = wx.GridBagSizer(hgap=1, vgap=1)
+
+        self.initList = wx.ListCtrl(self, wx.ID_ANY, style=wx.LC_SINGLE_SEL|wx.LC_REPORT|wx.LC_HRULES)
+        self.initList.InsertColumn(0, "Name")
+        self.initList.InsertColumn(1, "Type")
+        self.initList.InsertColumn(2, "Duration")
+        self.initList.InsertColumn(3, "Init Mod")
+        self.initList.InsertColumn(4, "Init")
+        self.initList.InsertColumn(5, "Manualy Set")
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.selectInit, self.initList)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.deselectInit, self.initList)
+        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnMB_InitEdit, self.initList)
+        self.Bind(wx.EVT_LIST_ITEM_FOCUSED, self.Test, self.initList)
+
+        self.nextMessage = wx.TextCtrl(self, wx.ID_ANY, "UP NEXT FOR THE KILLING")
+        msgsizer = wx.BoxSizer(wx.HORIZONTAL)
+        msgsizer.Add(wx.StaticText(self, wx.ID_ANY, "Next Message"), 0, wx.ALIGN_CENTER|wx.ALL, 5)
+        msgsizer.Add(self.nextMessage, 1, wx.EXPAND|wx.ALL, 5)
+
+        self.initRollType = wx.Choice(self, wx.ID_ANY, choices=['d20'])
+        self.initRollType.SetSelection(0)
+
+        sizer.Add(self.psizer, (0,0), flag=wx.EXPAND)
+        sizer.Add(wx.StaticText(self, wx.ID_ANY, 'Init Roll Type'), (0,1), flag=wx.ALIGN_BOTTOM|wx.ALIGN_CENTER_HORIZONTAL)
+        sizer.Add(msgsizer, (1,0), flag=wx.EXPAND)
+        sizer.Add(self.initRollType, (1,1), flag=wx.ALIGN_CENTER)
+        sizer.Add(self.initList, (2,0), flag=wx.EXPAND)
+        sizer.Add(self.esizer, (2,1), flag=wx.EXPAND)
+        sizer.Add(self.asizer, (3,0), flag=wx.EXPAND)
+        sizer.AddGrowableCol(0)
+        sizer.AddGrowableRow(2)
+
+        self.SetSizer(sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+        self.log.log("Exit InitFrame->buildGUI(self)", ORPG_DEBUG)
+
+    def updateInit(self, init, type):
+        reset = False
+        if not self.startButton.IsEnabled() and self.initList.GetItemCount() > 0:
+            reset = True
+            name = self.currentInit.name
+
+
+        self.Freeze()
+
+        if type == 'Add':
+            i = self.initList.InsertStringItem(self.initList.GetItemCount(), init.name)
+            self.initList.SetStringItem(i, 1, init.type)
+            self.initList.SetStringItem(i, 2, init.duration)
+            self.initList.SetStringItem(i, 3, init.initMod)
+            self.initList.SetStringItem(i, 4, init.init)
+            self.initList.SetStringItem(i, 5, init.manual)
+            self.initList.SetItemData(i, init)
+        else:
+            self.initList.SetStringItem(self.initIdx, 0, init.name)
+            self.initList.SetStringItem(self.initIdx, 1, init.type)
+            self.initList.SetStringItem(self.initIdx, 2, init.duration)
+            self.initList.SetStringItem(self.initIdx, 3, init.initMod)
+            self.initList.SetStringItem(self.initIdx, 4, init.init)
+            self.initList.SetStringItem(self.initIdx, 5, init.manual)
+            self.initList.SetItemData(self.initIdx, init)
+
+        self.initList.SortItems(self.initSort)
+        self.initList.SetColumnWidth(0, wx.LIST_AUTOSIZE)
+        self.initList.SetColumnWidth(1, wx.LIST_AUTOSIZE)
+        self.initList.SetColumnWidth(2, wx.LIST_AUTOSIZE)
+        self.initList.SetColumnWidth(3, wx.LIST_AUTOSIZE)
+
+        if reset:
+            for i in xrange(0, self.initList.GetItemCount()):
+                self.currentInit = i
+                if self.currentInit.name == name and type == 'Add':
+                    break
+                elif  self.currentInit.name == name and type == 'Edit':
+                    self.currentInit = self.initIdx-1
+                    break
+
+            self.initList.Select(self.initIdx)
+        else:
+            self.currentInit = 0
+        self.Thaw()
+
+    #Events
+    def Test(self, evt):
+        pass
+
+    def selectInit(self, evt):
+        self.currentInit = evt.GetIndex()
+        self.initList.SetItemState(self.initIdx, 0, wx.LIST_STATE_SELECTED)
+
+    def deselectInit(self, evt):
+        #self.currentInit = -1
+        evt.Skip()
+
+    def OnMB_InitAdd(self, evt):
+        addeditwnd = AddEditWnd(self, Init('New'), 'Add')
+        addeditwnd.Show()
+
+    def OnMB_InitEdit(self, evt):
+        addeditwnd = AddEditWnd(self, self.currentInit, 'Edit')
+        addeditwnd.Show()
+
+    def OnMB_InitDelete(self, evt):
+        if self.initIdx != -1:
+            self.initList.DeleteItem(self.initIdx)
+            self.currentInit = 0
+
+    def OnMB_InitClear(self, evt):
+        self.initList.DeleteAllItems()
+        self.initIdx = -1
+
+    def OnMB_InitSave(self, evt):
+        dlg = wx.TextEntryDialog(self, "Please Name This Init List", "New Init List")
+        if dlg.ShowModal() == wx.ID_OK:
+            saveas = dlg.GetValue()
+            dlg.Destroy()
+        else:
+            dlg.Destroy()
+            return
+
+        self.Freeze()
+        cidx = self.initIdx
+        self.plugin.InitSaves[saveas] = []
+        for i in xrange(0, self.initList.GetItemCount()):
+            self.currentInit = i
+            cinit = {}
+            cinit['name'] = self.currentInit.name
+            cinit['type'] = self.currentInit.type
+            cinit['duration'] = self.currentInit.duration
+            cinit['initMod'] = self.currentInit.initMod
+            cinit['init'] = self.currentInit.init
+            cinit['manual'] = self.currentInit.manual
+            self.plugin.InitSaves[saveas].append(cinit)
+
+        self.currentInit = cidx
+        self.Thaw()
+
+        self.plugin.plugindb.SetDict("xxsimpleinit", "InitSaves", self.plugin.InitSaves)
+
+
+    def OnMB_InitLoad(self, evt):
+        dlg = wx.Dialog(self, wx.ID_ANY, "Load Init List")
+        sz = wx.BoxSizer(wx.HORIZONTAL)
+        sz.Add(wx.StaticText(dlg, wx.ID_ANY, 'Select the Init List to Load'), 0, wx.ALIGN_CENTER)
+        selectList = wx.Choice(dlg, wx.ID_ANY, choices=self.plugin.InitSaves.keys())
+        selectList.SetSelection(0)
+        sz.Add(selectList, 0, wx.EXPAND)
+        sz.Add(wx.Button(dlg, wx.ID_OK, 'Load'), 0)
+        sz.Add(wx.Button(dlg, wx.ID_CANCEL, 'Cancel'), 0)
+        dlg.Show()
+
+        dlg.SetSizer(sz)
+        dlg.SetAutoLayout(True)
+        dlg.Fit()
+
+        if dlg.ShowModal() == wx.ID_OK:
+            load = selectList.GetStringSelection()
+            dlg.Destroy()
+        else:
+            dlg.Destroy()
+            return
+
+        self.Freeze()
+        self.OnMB_InitClear(None)
+        for newInit in self.plugin.InitSaves[load]:
+            init = Init(newInit['name'], newInit['type'], newInit['duration'], newInit['initMod'], newInit['init'], newInit['manual'])
+            self.updateInit(init, 'Add')
+            del init
+
+        self.initList.SortItems(self.initSort)
+        self.currentInit = 0
+        self.Thaw()
+
+    #Getter/Setter
+    def GetSelectedInit(self):
+        return Init(self.initList.GetItem(self.initIdx, 0).GetText(),\
+                self.initList.GetItem(self.initIdx, 1).GetText(),\
+                self.initList.GetItem(self.initIdx, 2).GetText(),\
+                self.initList.GetItem(self.initIdx, 3).GetText(),\
+                self.initList.GetItem(self.initIdx, 4).GetText(),\
+                self.initList.GetItem(self.initIdx, 5).GetText())
+
+    def SetSelectedInit(self, idx):
+        for i in xrange(0, self.initList.GetItemCount()):
+            self.initList.SetItemBackgroundColour(i, (255,255,255))
+
+        if idx >= self.initList.GetItemCount():
+            self.initIdx = 0
+            self.plugin.newRound()
+        else:
+            self.initIdx = idx
+
+        if self.initIdx != -1:
+            self.initList.SetItemBackgroundColour(self.initIdx, (0,255,0))
+
+    #Properties
+    currentInit = property(GetSelectedInit, SetSelectedInit)
+
+class Init:
+    def __init__(self, *args, **kwargs):
+        """__init__(self, name, type='Player', duration='0', initMod='+0', init='0', manual='No')"""
+        if kwargs.has_key('name'):
+            self.name = kwargs['name']
+        else:
+            try:
+                self.name = args[0]
+            except:
+                print 'Exception: Invalid initilization of ' + __name__ + ' Message. Missing name'
+
+        try:
+            self.type = args[1]
+        except:
+            if kwargs.has_key('type'):
+                self.type = kwargs['type']
+            else:
+                self.type = 'Player'
+
+        try:
+            self.duration = args[2]
+        except:
+            if kwargs.has_key('duration'):
+                self.duration = kwargs['duration']
+            else:
+                self.duration = '0'
+
+        try:
+            self.initMod = args[3]
+        except:
+            if kwargs.has_key('initMod'):
+                self.initMod = kwargs['initMod']
+            else:
+                self.initMod = '0'
+
+        try:
+            self.init = args[4]
+        except:
+            if kwargs.has_key('init'):
+                self.init = kwargs['init']
+            else:
+                self.init = '0'
+
+        try:
+            self.manual = args[5]
+        except:
+            if kwargs.has_key('manual'):
+                self.manual = kwargs['manual']
+            else:
+                self.manual = 'No'
+
+    def __int__(self):
+        return int(self.init)
+
+
+class AddEditWnd(wx.Frame):
+    def __init__(self, parent, Init, edittype="Add"):
+        self.parent = parent
+        self.edittype = edittype
+        if edittype == 'Add':
+            wndTitle = 'Add New Initiative Member'
+        else:
+            wndTitle = 'Edit Initive Member'
+
+        wx.Frame.__init__(self, parent, wx.ID_ANY, title=wndTitle, style=wx.DEFAULT_FRAME_STYLE|wx.STAY_ON_TOP)
+        #self.MakeModal(True)
+        self.SetOwnBackgroundColour('#EFEFEF')
+
+        self.currentInit = Init
+
+        self.Freeze()
+        self.buildGUI()
+        self.Thaw()
+
+    def buildGUI(self):
+        sizer = wx.GridBagSizer(hgap=1, vgap=1)
+        sizer.SetEmptyCellSize((0,0))
+
+        self.stDuration = wx.StaticText(self, wx.ID_ANY, 'Duration')
+        self.stInitiative = wx.StaticText(self, wx.ID_ANY, 'Initiative')
+
+        sizer.Add(wx.StaticText(self, wx.ID_ANY, 'Name'), (0,0), flag=wx.ALIGN_BOTTOM)
+        sizer.Add(wx.StaticText(self, wx.ID_ANY, 'Type'), (0,1), flag=wx.ALIGN_BOTTOM)
+        sizer.Add(self.stDuration, (0,2), flag=wx.ALIGN_BOTTOM)
+        sizer.Add(wx.StaticText(self, wx.ID_ANY, 'Int Mod'), (0,3), flag=wx.ALIGN_BOTTOM)
+        sizer.Add(self.stInitiative, (0,4), flag=wx.ALIGN_BOTTOM)
+        sizer.Add(wx.StaticText(self, wx.ID_ANY, 'Manualy Set'), (0,5), flag=wx.ALIGN_BOTTOM)
+
+        self.name = wx.TextCtrl(self, wx.ID_ANY)
+        self.type = wx.Choice(self, wx.ID_ANY, choices=['Player', 'NPC', 'Effect'])
+        self.type.SetSelection(0)
+        self.Bind(wx.EVT_CHOICE, self.typeSelect, self.type)
+        self.duration = wx.TextCtrl(self, wx.ID_ANY, '', size=(40,-1))
+        self.duration.SetMaxLength(4)
+        self.duration.Bind(wx.EVT_CHAR, self.validateNumber)
+        self.initMod = wx.Choice(self, wx.ID_ANY, choices=['-20', '-19', '-18', '-17', '-16', '-15', '-14', '-13', '-12', '-11', '-10', '-9', '-8', '-7', '-6', '-5', '-4', '-3', '-2', '-1', '+0', '+1', '+2', '+3', '+4', '+5', '+6', '+7', '+8', '+9','+10', '+11', '+12', '+13', '+14', '+15', '+16', '+17', '+18', '+19', '+20'])
+        self.initMod.SetSelection(20)
+        self.initiative = wx.TextCtrl(self, wx.ID_ANY, '0', size=(40,-1))
+        self.initiative.SetMaxLength(4)
+        self.initiative.Bind(wx.EVT_CHAR, self.validateNumber)
+        self.manual = wx.Choice(self, wx.ID_ANY, choices=['No', 'Yes'])
+        self.manual.SetSelection(0)
+        self.Bind(wx.EVT_CHOICE, self.manualSelect, self.manual)
+
+        self.initiative.Hide()
+        self.stInitiative.Hide()
+        self.stDuration.Hide()
+        self.duration.Hide()
+
+        if self.edittype == 'Edit':
+            self.name.SetValue(self.currentInit.name)
+            self.type.SetStringSelection(self.currentInit.type)
+            self.duration.SetValue(self.currentInit.duration)
+            self.initMod.SetStringSelection(self.currentInit.initMod)
+            self.initiative.SetValue(self.currentInit.init)
+            self.manual.SetStringSelection(self.currentInit.manual)
+
+            self.manualSelect(None)
+            self.typeSelect(None)
+
+        sizer.Add(self.name, (1,0), flag=wx.ALIGN_CENTER)
+        sizer.Add(self.type, (1,1), flag=wx.ALIGN_CENTER)
+        sizer.Add(self.duration, (1,2), flag=wx.ALIGN_CENTER)
+        sizer.Add(self.initMod, (1,3), flag=wx.ALIGN_CENTER)
+        sizer.Add(self.initiative, (1,4), flag=wx.ALIGN_CENTER)
+        sizer.Add(self.manual, (1,5), flag=wx.ALIGN_CENTER)
+
+        self.okButton = wx.Button(self, wx.ID_OK, 'Ok')
+        self.cancelButton = wx.Button(self, wx.ID_CANCEL, 'Cancel')
+
+        sizer.Add(self.okButton, (0,6), flag=wx.EXPAND)
+        sizer.Add(self.cancelButton, (1,6))
+        self.Bind(wx.EVT_BUTTON, self.onOK, self.okButton)
+        self.Bind(wx.EVT_BUTTON, self.onCancel, self.cancelButton)
+
+        self.SetSizer(sizer)
+        self.SetAutoLayout(True)
+        self.Fit()
+
+
+    #Events
+    def validateNumber(self, evt):
+        if evt.GetKeyCode() == wx.WXK_DELETE or evt.GetKeyCode() == wx.WXK_BACK or (evt.GetKeyCode() < 256 and chr(evt.GetKeyCode()) in string.digits):
+            evt.Skip()
+        else:
+            wx.Bell()
+
+    def manualSelect(self, evt):
+        if self.manual.GetStringSelection() == 'Yes':
+            self.initiative.Show()
+            self.stInitiative.Show()
+        else:
+            self.initiative.Hide()
+            self.stInitiative.Hide()
+
+        self.Fit()
+
+    def typeSelect(self, evt):
+        if self.type.GetStringSelection() == 'Effect':
+            self.stDuration.Show()
+            self.duration.Show()
+        else:
+            self.stDuration.Hide()
+            self.duration.Hide()
+
+        self.Fit()
+
+    def onOK(self, evt):
+        self.currentInit.name = self.name.GetValue()
+        self.currentInit.type = self.type.GetStringSelection()
+        self.currentInit.duration = self.duration.GetValue()
+        self.currentInit.initMod = self.initMod.GetStringSelection()
+        self.currentInit.init = self.initiative.GetValue()
+        self.currentInit.manual = self.manual.GetStringSelection()
+        self.parent.updateInit(self.currentInit, self.edittype)
+        self.Close()
+
+    def onCancel(self, evt):
+        self.Close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxsmiley.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,168 @@
+import os
+import orpg.pluginhandler
+import orpg.dirpath
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    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 = 'Smilies!'
+        self.author = 'mDuo13'
+        self.help = "This plugin turns text smilies like >=) or :D into images. There are 15\n"
+        self.help += "images. Also, you can type '/smiley' to get a list of what emoticons are\n"
+        self.help += "converted to what images."
+
+        self.smileylist = {}
+
+    def plugin_enabled(self):
+        #This is where you set any variables that need to be initalized when your plugin starts
+
+        self.plugin_addcommand('/smiley', self.on_smiley, '- [add|remove|help] The Smiley command')
+
+        smlist = {
+                    '>:-('   :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley7.gif" /> ',
+                    ':/'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley5.gif" /> ',
+                    ':|'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley6.gif" /> ',
+                    ':('     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley9.gif" /> ',
+                    ' />:('  :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley7.gif" /> ',
+                    ' />=('  :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley7.gif" /> ',
+                    '=)'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley0.gif" /> ',
+                    '=D'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley1.gif" /> ',
+                    ';)'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley3.gif" /> ',
+                    '=/'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley5.gif" /> ',
+                    '=|'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley6.gif" /> ',
+                    '=('     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley9.gif" /> ',
+                    ':)'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley0.gif" /> ',
+                    ':D'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley1.gif" /> ',
+                    'B)'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley2.gif" /> ',
+                    ':p'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley4.gif" /> ',
+                    '=\\'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley5.gif" /> ',
+                    ':P'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley4.gif" /> ',
+                    '=P'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley4.gif" /> ',
+                    '^_^'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley12.gif" /> ',
+                    '^-^'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley12.gif" /> ',
+                    '^.^'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley12.gif" /> ',
+                    'n_n'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley12.gif" /> ',
+                    'n.n'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley12.gif" /> ',
+                    'n,n'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley12.gif" /> ',
+                    'I-)'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley13.gif" /> ',
+                    'n.n;'   :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley14.gif" /> ',
+                    'n.n;;'  :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley14.gif" /> ',
+                    'n_n;'   :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley14.gif" /> ',
+                    ':-)'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley0.gif" /> ',
+                    ':-D'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley1.gif" /> ',
+                    ':-P'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley2.gif" /> ',
+                    ':-p'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley4.gif" /> ',
+                    ':-/'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley5.gif" /> ',
+                    ':-|'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley6.gif" /> ',
+                    ':-('    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley9.gif" /> ',
+                    ':-\\'   :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/smiley5.gif" /> ',
+                    '-)'          :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_smile.gif" /> ',
+                    ';-)'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_wink.gif" /> ',
+                    ':->'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_smile2.gif" /> ',
+                    ':-D'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_biggrin.gif" /> ',
+                    ':-P'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_razz.gif" /> ',
+                    ':-o'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_surprised.gif" /> ',
+                    ':mrgreen:'   :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_mrgreen.gif" /> ',
+                    ':lol:'       :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_lol.gif" /> ',
+                    ':-('         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_sad.gif" /> ',
+                    ':-|'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_neutral.gif" /> ',
+                    ':-?'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_confused.gif" /> ',
+                    ':-x'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_mad.gif" /> ',
+                    ':shock:'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_eek.gif" /> ',
+                    ':cry:'       :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_cry.gif" /> ',
+                    ';_;'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_cry.gif" /> ',
+                    ':oops:'      :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_redface.gif" /> ',
+                    '8-)'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_cool.gif" /> ',
+                    ':evil:'      :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_evil.gif" /> ',
+                    ':twisted:'      :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_twisted.gif" /> ',
+                    ':roll:'      :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_rolleyes.gif" /> ',
+                    ':!:'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_exclaim.gif" /> ',
+                    ':?:'         :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_question.gif" /> ',
+                    ':idea:'      :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_idea.gif" /> ',
+                    ':arrow:'     :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_arrow.gif" /> ',
+                    ':ubergeek:'  :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_e_ugeek.gif" /> ',
+                    ':geek:'      :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/icon_e_geek.gif" /> ',
+                    ':fairy:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/fairy.gif" /> ',
+                    ':hood:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/hood.gif" /> ',
+                    ':gnome:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/gnome.gif" /> ',
+                    ':link:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/link.gif" /> ',
+                    ':mummy:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/mummy.gif" /> ',
+                    ':ogre:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/ogre.gif" /> ',
+                    ':medusa:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/medusa.gif" /> ',
+                    ':mimic:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/mimic.gif" /> ',
+                    ':skull:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/skull.gif" /> ',
+                    ':zombie:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/zombie.gif" /> ',
+                    ':chocobo:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/chocobo.gif" /> ',
+                    ':darkside:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/darkside.gif" /> ',
+                    ':flyingspaghetti:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/flyingspaghetti.gif" /> ',
+                    ':rupee:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/rupee.gif" /> ',
+                    ':ros:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/ros.gif" /> ',
+                    ':skeleton:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/skeleton.gif" /> ',
+                    ':samurai:'    :       ' <img src="' + orpg.dirpath.dir_struct['plugins'] + 'images/samurai.gif" /> '}
+
+        self.smileylist = self.plugindb.GetDict("xxsmiley", "smileylist", smlist)
+
+    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
+        #such as closing windows created by the plugin
+
+        self.plugin_removecmd('/smiley')
+        self.plugindb.SetDict("xxsmiley", "smileylist", self.smileylist)
+
+    def on_smiley(self, cmdargs):
+        #this is just an example function for a command you create create your own
+        if not len(cmdargs):
+            self.chat.InfoPost("Available Smilies:")
+            the_list = ' <table border="1">'
+            for key in self.smileylist.keys():
+                the_list += ' <tr><td>' + key + ' </td><td>' + self.smileylist[key] + ' </td></tr>'
+            the_list += "</table>"
+            self.chat.InfoPost(the_list)
+            return
+
+        args = cmdargs.split(None, -1)
+
+        if args[0] == 'add' and len(args) == 3:
+            if args[2].find('http') > -1:
+                self.smileylist[args[1]] = ' <img src="' + args[2] + '" alt="' + args[1] + '" />'
+            else:
+                self.smileylist[args[1]] = ' <img src="' + orpg.dirpath.dir_struct["plugins"] + 'images/' + args[2] + '" />' + "\n"
+
+            self.chat.InfoPost('Added ' + args[1] + '&nbsp&nbsp&nbsp : &nbsp&nbsp&nbsp' + self.smileylist[args[1]])
+
+        elif args[0] == 'remove' and len(args) == 2:
+            if self.smileylist.has_key(args[1]):
+                del self.smileylist[args[1]]
+                self.chat.InfoPost('Removed ' + args[1])
+            else:
+                self.chat.InfoPost(args[1] + ' was not a smiley!')
+
+        else:
+            self.chat.InfoPost('/smiley - Lists all avaliable smilies')
+            self.chat.InfoPost('/smiley add {smiley} {imagefile} - Add a smily to the list. The {smiley} can be any string of text that does not contain a space. The {imagefile} should be an image in the openrpg/plugins/images directory')
+            self.chat.InfoPost('/smiley remove {smiley} - Remove {smiley} from the list')
+
+        self.plugindb.SetDict("xxsmiley", "smileylist", self.smileylist)
+
+    def doSmiley(self, text):
+        for key, value in self.smileylist.iteritems():
+            text = text.replace(key, value)
+
+        return text
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        text = self.doSmiley(text)
+
+        return text, type, name
+
+    def post_msg(self, text, myself):
+        if myself:
+            text = self.doSmiley(text)
+
+        return text
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxspell.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,86 @@
+import os
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    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 = 'Spelling Checker (aka Autoreplace)'
+        self.author = 'Woody, mDuo13'
+        self.help = "This plugin automatically replaces certain keys with other ones wheneve\nr"
+        self.help += "it sees them. You can edit this plugin to change what it replaces. This one\n"
+        self.help += "even corrects other people's spelling."
+        self.checklist = {}
+
+    def plugin_enabled(self):
+        #This is where you set any variables that need to be initalized when your plugin starts
+        #You can add new /commands like
+        self.plugin_addcommand("/spell", self.on_spell, "[add {bad_word} {new_word} | list | remove {bad_word}] - This function allows you to add words to be auto replaced when they are sent or recived. If you want either word to have a space in it you will need to use the _ (underscore) as a space, it will be replaced with a space before it is added to the check list")
+        self.checklist = self.plugindb.GetDict("xxspell", "checklist", {})
+
+    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
+        #such as closing windows created by the plugin
+        self.plugin_removecmd('/spell')
+
+    def replace(self, text):
+        for a in self.checklist.keys():
+            text = text.replace(a, self.checklist[a])
+        return text
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        text = self.replace(text)
+        return text, type, name
+
+    def pre_parse(self, text):
+        text = self.replace(text)
+        return text
+
+    def on_spell(self, cmdargs):
+        args = cmdargs.split(None,-1)
+        if len(args) == 0:
+            args = [' ']
+        if args[0] == 'list':
+            self.chat.InfoPost(self.make_list())
+        elif args[0] == 'remove':
+            args = self.create_spaces(args)
+            if self.checklist.has_key(args[1]):
+                del self.checklist[args[1]]
+                self.plugindb.SetDict("xxspell", "checklist", self.checklist)
+                self.chat.InfoPost("%s removed from the check list" % args[1])
+            else:
+                self.chat.InfoPost("%s was not in the check list" % args[1])
+        elif args[0] == 'add':
+            args = self.create_spaces(args)
+            self.checklist[args[1]] = args[2]
+            self.plugindb.SetDict("xxspell", "checklist", self.checklist)
+            self.chat.InfoPost("%s will now be replaced by %s" % (args[1], args[2]))
+        else:
+            helptxt = "Spelling Command Help:<br />"
+            helptxt += "<b>/spell add bad_word new_word</b> - Here it is important to remember that any spaces in your bad or new words needs to be type as _ (an underscore).<br />"
+            helptxt += "<b>/spell remove bad_word</b> - Here it is important to remember that any spaces in your bad word needs to be type as _ (an underscore).<br />"
+            helptxt += "<b>/spell list</b> - This will list all of the bad words you are checking for, along with thier replacements.<br />"
+            self.chat.InfoPost(helptxt)
+
+
+    def create_spaces(self, list):
+        i = 0
+        for n in list:
+            list[i] = n.replace("_"," ")
+            i += 1
+        return list
+
+    def make_list(self):
+        keychain = self.checklist.keys()
+        lister = ""
+        for key in keychain:
+            lister += "<b>" + key.replace("<","&lt;").replace(">","&gt;") + "</b> :: <b>" + self.checklist[key].replace("<","&lt;").replace(">","&gt;") + "</b><br />"
+        if len(keychain)==0:
+            return "No variables!"
+        else:
+            return lister
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxsrdlinker.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,950 @@
+import os
+import re
+import string
+import orpg.pluginhandler
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    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 = 'SRD Auto Linker'
+        self.author = 'Digitalxero, Ulf'
+        self.help = "This plugin automatically Links words to the proper section of the Online SRD"
+        baseURL = 'http://d20srd.org/srd/'
+        self.checklist = {
+
+            # Skills
+            'appraise':                         baseURL + "skills/appraise.htm",
+            'autohypnosis':                     baseURL + "psionic/skills/autohypnosis.htm",
+            'balance':                          baseURL + "skills/balance.htm",
+            'bluff':                            baseURL + "skills/bluff.htm",
+            'climb':                            baseURL + "skills/climb.htm",
+            'concentration':                    baseURL + "psionic/skills/concentration.htm",
+            'craft':                            baseURL + "skills/craft.htm",
+            'decipher script':                  baseURL + "skills/decipherScript.htm",
+            'diplomacy':                        baseURL + "skills/diplomacy.htm",
+            'disable device':                   baseURL + "skills/disableDevice.htm",
+            'disguise':                         baseURL + "skills/disguise.htm",
+            'escape artist':                    baseURL + "skills/escapeArtist.htm",
+            'forgery':                          baseURL + "skills/forgery.htm",
+            'gather information':               baseURL + "skills/gatherInformation.htm",
+            'handle animal':                    baseURL + "skills/handleAnimal.htm",
+            'heal':                             baseURL + "skills/heal.htm",
+            'hide':                             baseURL + "skills/hide.htm",
+            'intimidate':                       baseURL + "skills/intimidate.htm",
+            'jump':                             baseURL + "skills/jump.htm",
+            'knowledge':                        baseURL + "skills/knowledge.htm",
+            'listen':                           baseURL + "skills/listen.htm",
+            'move silently':                    baseURL + "skills/moveSilently.htm",
+            'open lock':                        baseURL + "skills/openLock.htm",
+            'perform':                          baseURL + "skills/perform.htm",
+            'profession':                       baseURL + "skills/profession.htm",
+            'psicraft':                         baseURL + "psionic/skills/psicraft.htm",
+            'ride':                             baseURL + "skills/ride.htm",
+            'search':                           baseURL + "skills/search.htm",
+            'sense motive':                     baseURL + "skills/senseMotive.htm",
+            'sleight of hand':                  baseURL + "skills/sleightOfHand.htm",
+            'speak language':                   baseURL + "skills/speakLanguage.htm",
+            'spellcraft':                       baseURL + "skills/spellcraft.htm",
+            'spot':                             baseURL + "skills/spot.htm",
+            'survival':                         baseURL + "skills/survival.htm",
+            'swim':                             baseURL + "skills/swim.htm",
+            'tumble':                           baseURL + "skills/tumble.htm",
+            'use magic device':                 baseURL + "skills/useMagicDevice.htm",
+            'use psionic device':               baseURL + "psionic/skills/usePsionicDevice.htm",
+            'use rope':                         baseURL + "skills/useRope.htm",
+
+            # Feats
+            'acrobatic':                        baseURL + 'feats.htm#acrobatic',
+            'agile':                            baseURL + 'feats.htm#agile',
+            'alertness':                        baseURL + 'feats.htm#alertness',
+            'animal affinity':                  baseURL + 'feats.htm#animalAffinity',
+            'antipsionic magic':                baseURL + 'psionic/psionicFeats.htm#antipsionicMagic',
+            'armor proficiency (heavy)':        baseURL + 'feats.htm#armorProficiencyHeavy',
+            'armor proficiency (light)':        baseURL + 'feats.htm#armorProficiencyLight',
+            'armor proficiency (medium)':       baseURL + 'feats.htm#armorProficiencyMedium',
+            'armor proficiency':                baseURL + 'feats.htm#armorProficiencyLight',
+            'athletic':                         baseURL + 'feats.htm#athletic',
+            'augment summoning':                baseURL + 'feats.htm#augmentSummoning',
+            'autonomous':                       baseURL + 'psionic/psionicFeats.htm#autonomous',
+            'blind-fight':                      baseURL + 'feats.htm#blindFight',
+            'brew potion':                      baseURL + 'feats.htm#brewPotion',
+            'chaotic mind':                                 baseURL + 'psionic/psionicFeats.htm#chaoticMind',
+            'cleave':                           baseURL + 'feats.htm#cleave',
+            'cloak dance':                                  baseURL + 'psionic/psionicFeats.htm#cloakDance',
+            'combat casting':                   baseURL + 'feats.htm#combatCasting',
+            'combat expertise':                 baseURL + 'feats.htm#combatExpertise',
+            'combat reflexes':                  baseURL + 'feats.htm#combatReflexes',
+            'craft magic arms and armor':       baseURL + 'feats.htm#craftMagicArmsAndArmor',
+            'craft rod':                        baseURL + 'feats.htm#craftRod',
+            'craft staff':                      baseURL + 'feats.htm#craftStaff',
+            'craft wand':                       baseURL + 'feats.htm#craftWand',
+            'craft wondrous item':              baseURL + 'feats.htm#craftWondrousItem',
+            'deceitful':                        baseURL + 'feats.htm#deceitful',
+            'deflect arrows':                   baseURL + 'feats.htm#deflectArrows',
+            'deft hands':                       baseURL + 'feats.htm#deftHands',
+            'diehard':                          baseURL + 'feats.htm#diehard',
+            'diligent':                         baseURL + 'feats.htm#diligent',
+            'dodge':                            baseURL + 'feats.htm#dodge',
+            'empower spell':                    baseURL + 'feats.htm#empowerSpell',
+            'endurance':                        baseURL + 'feats.htm#endurance',
+            'enlarge spell':                    baseURL + 'feats.htm#enlargeSpell',
+            'eschew materials':                 baseURL + 'feats.htm#eschewMaterials',
+            'exotic weapon proficiency':        baseURL + 'feats.htm#exoticWeaponProficiency',
+            'extend spell':                     baseURL + 'feats.htm#extendSpell',
+            'extra turning':                    baseURL + 'feats.htm#extraTurning',
+            'far shot':                         baseURL + 'feats.htm#farShot',
+            'forge ring':                       baseURL + 'feats.htm#forgeRing',
+            'great cleave':                     baseURL + 'feats.htm#greatCleave',
+            'great fortitude':                  baseURL + 'feats.htm#greatFortitude',
+            'greater spell focus':              baseURL + 'feats.htm#greaterSpellFocus',
+            'greater spell penetration':        baseURL + 'feats.htm#greaterSpellPenetration',
+            'greater two-weapon fighting':      baseURL + 'feats.htm#greaterTwoWeaponFighting',
+            'greater weapon focus':             baseURL + 'feats.htm#greaterWeaponFocus',
+            'greater weapon specialization':    baseURL + 'feats.htm#greaterWeaponSpecialization',
+            'heighten spell':                   baseURL + 'feats.htm#heightenSpell',
+            'improved bull rush':               baseURL + 'feats.htm#improvedBullRush',
+            'improved counterspell':            baseURL + 'feats.htm#improvedCounterspell',
+            'improved critical':                baseURL + 'feats.htm#improvedCritical',
+            'improved disarm':                  baseURL + 'feats.htm#improvedDisarm',
+            'improved familiar':                baseURL + 'feats.htm#improvedFamiliar',
+            'improved feint':                   baseURL + 'feats.htm#improvedFeint',
+            'improved grapple':                 baseURL + 'feats.htm#improvedGrapple',
+            'improved initiative':              baseURL + 'feats.htm#improvedInitiative',
+            'improved overrun':                 baseURL + 'feats.htm#improvedOverrun',
+            'improved precise shot':            baseURL + 'feats.htm#improvedPreciseShot',
+            'improved shield bash':             baseURL + 'feats.htm#improvedShieldBash',
+            'improved sunder':                  baseURL + 'feats.htm#improvedSunder',
+            'improved trip':                    baseURL + 'feats.htm#improvedTrip',
+            'improved turning':                 baseURL + 'feats.htm#improvedTurning',
+            'improved two-weapon fighting':     baseURL + 'feats.htm#improvedTwoWeaponFighting',
+            'improved unarmed strike':          baseURL + 'feats.htm#improvedUnarmedStrike',
+            'investigator':                     baseURL + 'feats.htm#investigator',
+            'iron will':                        baseURL + 'feats.htm#ironWill',
+            'leadership':                       baseURL + 'feats.htm#leadership',
+            'lightning reflexes':               baseURL + 'feats.htm#lightningReflexes',
+            'magical aptitude':                 baseURL + 'feats.htm#magicalAptitude',
+            'manyshot':                         baseURL + 'feats.htm#manyshot',
+            'martial weapon proficiency':       baseURL + 'feats.htm#martialWeaponProficiency',
+            'maximize spell':                   baseURL + 'feats.htm#maximizeSpell',
+            'mobility':                         baseURL + 'feats.htm#mobility',
+            'mounted archery':                  baseURL + 'feats.htm#mountedArchery',
+            'mounted combat':                   baseURL + 'feats.htm#mountedCombat',
+            'natural spell':                    baseURL + 'feats.htm#naturalSpell',
+            'negotiator':                       baseURL + 'feats.htm#negotiator',
+            'nimble fingers':                   baseURL + 'feats.htm#nimbleFingers',
+            'persuasive':                       baseURL + 'feats.htm#persuasive',
+            'point blank shot':                 baseURL + 'feats.htm#pointBlankShot',
+            'power attack':                     baseURL + 'feats.htm#powerAttack',
+            'precise shot':                     baseURL + 'feats.htm#preciseShot',
+            'quick draw':                       baseURL + 'feats.htm#quickDraw',
+            'quicken spell':                    baseURL + 'feats.htm#quickenSpell',
+            'rapid reload':                     baseURL + 'feats.htm#rapidReload',
+            'rapid shot':                       baseURL + 'feats.htm#rapidShot',
+            'ride-by attack':                   baseURL + 'feats.htm#rideByAttack',
+            'run':                              baseURL + 'feats.htm#run',
+            'scribe scroll':                    baseURL + 'feats.htm#scribeScroll',
+            'self-sufficient':                  baseURL + 'feats.htm#selfSufficient',
+            'shield proficiency':               baseURL + 'feats.htm#shieldProficiency',
+            'shot on the run':                  baseURL + 'feats.htm#shotOnTheRun',
+            'silent spell':                     baseURL + 'feats.htm#silentSpell',
+            'simple weapon proficiency':        baseURL + 'feats.htm#simpleWeaponProficiency',
+            'skill focus':                      baseURL + 'feats.htm#skillFocus',
+            'snatch arrows':                    baseURL + 'feats.htm#snatchArrows',
+            'spell focus':                      baseURL + 'feats.htm#spellFocus',
+            'spell mastery':                    baseURL + 'feats.htm#spellMasterySpecial',
+            'spell penetration':                baseURL + 'feats.htm#spellPenetration',
+            'spirited charge':                  baseURL + 'feats.htm#spiritedCharge',
+            'spring attack':                    baseURL + 'feats.htm#springAttack',
+            'stealthy':                         baseURL + 'feats.htm#stealthy',
+            'still spell':                      baseURL + 'feats.htm#stillSpell',
+            'stunning fist':                    baseURL + 'feats.htm#stunningFist',
+            'toughness':                        baseURL + 'feats.htm#toughness',
+            'tower shield proficiency':         baseURL + 'feats.htm#towerShieldProficiency',
+            'track':                            baseURL + 'feats.htm#track',
+            'trample':                          baseURL + 'feats.htm#trample',
+            'two-weapon defense':               baseURL + 'feats.htm#twoWeaponDefense',
+            'two-weapon fighting':              baseURL + 'feats.htm#twoWeaponFighting',
+            'weapon finesse':                   baseURL + 'feats.htm#weaponFinesse',
+            'weapon focus':                     baseURL + 'feats.htm#weaponFocus',
+            'weapon specialization':            baseURL + 'feats.htm#weaponSpecialization',
+            'whirlwind attack':                 baseURL + 'feats.htm#whirlwindAttack',
+            'widen spell':                      baseURL + 'feats.htm#widenSpell',
+
+            # Druid special abilities.
+            'animal companion' :                baseURL + "classes/druid.htm#animalCompanion",
+            'wild empathy'     :                baseURL + "classes/druid.htm#wildEmpathy",
+            'woodland stride'  :                baseURL + "classes/druid.htm#woodlandStride",
+            'nature sense'     :                baseURL + "classes/druid.htm#natureSense",
+            'trackless step'   :                baseURL + "classes/druid.htm#tracklessStep",
+            "resist nature's lure":             baseURL + "classes/druid.htm#resistNaturesLure",
+            'wild shape'       :                baseURL + "classes/druid.htm#wildShape",
+            'venom immunity'   :                baseURL + "classes/druid.htm#venomImmunity",
+            'a thousand faces' :                baseURL + "classes/druid.htm#aThousandFaces",
+            'timeless body'    :                baseURL + "classes/druid.htm#druidTimelessBody",
+
+            # Animal companion abilities.
+            'link'             :                baseURL + "classes/druid.htm#link",
+            'share spells'     :                baseURL + "classes/druid.htm#shareSpells",
+            'devotion'         :                baseURL + "classes/druid.htm#devotion",
+            'multiattack'      :                baseURL + "classes/druid.htm#multiattack",
+
+            # Ranger special abilities.
+            'wild empathy'     :                baseURL + "classes/ranger.htm#wildEmpathy",
+            'favored enemy'    :                baseURL + "classes/ranger.htm#favoredEnemy",
+            'combat style'     :                baseURL + "classes/ranger.htm#combatStyle",
+            'animal companion' :                baseURL + "classes/druid.htm#animalCompanion",
+            'improved combat style':            baseURL + "classes/ranger.htm#improvedCombatStyle",
+            'woodland stride'  :                baseURL + "classes/ranger.htm#woodlandStride",
+            'swift tracker'    :                baseURL + "classes/ranger.htm#swiftTracker",
+            'combat style mastery':             baseURL + "classes/ranger.htm#combatStyleMastery",
+            'camouflage'       :                baseURL + "classes/ranger.htm#camouflage",
+            'hide in plain sight':              baseURL + "classes/ranger.htm#hideinPlainSight",
+
+            # Rogue special abilities.
+            'sneak attack'     :                baseURL + "classes/rogue.htm#sneakAttack",
+            'trapfinding'      :                baseURL + "classes/rogue.htm#trapfinding",
+            'trap sense'       :                baseURL + "classes/rogue.htm#trapSense",
+            'uncanny dodge'    :                baseURL + "classes/rogue.htm#uncannyDodge",
+            'improved uncanny dodge':           baseURL + "classes/rogue.htm#improvedUncannyDodge",
+
+            # Wizard special abilities.
+            'summon familiar'  :                baseURL + "classes/sorcererWizard.htm#wizardFamiliar",
+            'scribe scroll'    :                baseURL + "classes/sorcererWizard.htm#scribeScroll",
+
+            # Spells
+            "acid arrow":                       baseURL + "spells/acidArrow.htm",
+            "acid fog":                         baseURL + "spells/acidFog.htm",
+            "acid splash":                      baseURL + "spells/acidSplash.htm",
+            "aid":                              baseURL + "spells/aid.htm",
+            "air walk":                         baseURL + "spells/airWalk.htm",
+            "alarm":                            baseURL + "spells/alarm.htm",
+            "align weapon":                     baseURL + "spells/alignWeapon.htm",
+            "alter self":                       baseURL + "spells/alterSelf.htm",
+            "analyze dweomer":                  baseURL + "spells/analyzeDweomer.htm",
+            "animal growth":                    baseURL + "spells/animalGrowth.htm",
+            "animal messenger":                 baseURL + "spells/animalMessenger.htm",
+            "animal shapes":                    baseURL + "spells/animalShapes.htm",
+            "animal trance":                    baseURL + "spells/animalTrance.htm",
+            "animate dead":                     baseURL + "spells/animateDead.htm",
+            "animate objects":                  baseURL + "spells/animateObjects.htm",
+            "animate plants":                   baseURL + "spells/animatePlants.htm",
+            "animate rope":                     baseURL + "spells/animateRope.htm",
+            "antilife shell":                   baseURL + "spells/antilifeShell.htm",
+            "antimagic field":                  baseURL + "spells/antimagicField.htm",
+            "antipathy":                        baseURL + "spells/antipathy.htm",
+            "antiplant shell":                  baseURL + "spells/antiplantShell.htm",
+            "arcane eye":                       baseURL + "spells/arcaneEye.htm",
+            "arcane lock":                      baseURL + "spells/arcaneLock.htm",
+            "arcane mark":                      baseURL + "spells/arcaneMark.htm",
+            "arcane sight":                     baseURL + "spells/arcaneSight.htm",
+            "arcane sight, greater":            baseURL + "spells/arcaneSightGreater.htm",
+            "astral projection":                baseURL + "spells/astralProjection.htm",
+            "atonement":                        baseURL + "spells/atonement.htm",
+            "augury":                           baseURL + "spells/augury.htm",
+            "awaken":                           baseURL + "spells/awaken.htm",
+            "baleful polymorph":                baseURL + "spells/balefulPolymorph.htm",
+            "bane":                             baseURL + "spells/bane.htm",
+            "banishment":                       baseURL + "spells/banishment.htm",
+            "barkskin":                         baseURL + "spells/barkskin.htm",
+            "bear's endurance":                 baseURL + "spells/bearsEndurance.htm",
+            "bear's endurance, mass":           baseURL + "spells/bearsEnduranceMass.htm",
+            "bestow curse":                     baseURL + "spells/bestowCurse.htm",
+            "binding":                          baseURL + "spells/binding.htm",
+            "black tentacles":                  baseURL + "spells/blackTentacles.htm",
+            "blade barrier":                    baseURL + "spells/bladeBarrier.htm",
+            "blasphemy":                        baseURL + "spells/blasphemy.htm",
+            "bless":                            baseURL + "spells/bless.htm",
+            "bless water":                      baseURL + "spells/blessWater.htm",
+            "bless weapon":                     baseURL + "spells/blessWeapon.htm",
+            "blight":                           baseURL + "spells/blight.htm",
+            "blindness/deafness":               baseURL + "spells/blindnessDeafness.htm",
+            "blink":                            baseURL + "spells/blink.htm",
+            "blur":                             baseURL + "spells/blur.htm",
+            "break enchantment":                baseURL + "spells/breakEnchantment.htm",
+            "bull's strength":                  baseURL + "spells/bullsStrength.htm",
+            "bull's strength, mass":            baseURL + "spells/bullsStrengthMass.htm",
+            "burning hands":                    baseURL + "spells/burningHands.htm",
+            "call lightning":                   baseURL + "spells/callLightning.htm",
+            "call lightning storm":             baseURL + "spells/callLightningStorm.htm",
+            "calm animals":                     baseURL + "spells/calmAnimals.htm",
+            "calm emotions":                    baseURL + "spells/calmEmotions.htm",
+            "cat's grace":                      baseURL + "spells/catsGrace.htm",
+            "cat's grace, mass":                baseURL + "spells/catsGraceMass.htm",
+            "cause fear":                       baseURL + "spells/causeFear.htm",
+            "chain lightning":                  baseURL + "spells/chainLightning.htm",
+            "changestaff":                      baseURL + "spells/changestaff.htm",
+            "chaos hammer":                     baseURL + "spells/chaosHammer.htm",
+            "charm animal":                     baseURL + "spells/charmAnimal.htm",
+            "charm monster":                    baseURL + "spells/charmMonster.htm",
+            "charm monster, mass":              baseURL + "spells/charmMonsterMass.htm",
+            "charm person":                     baseURL + "spells/charmPerson.htm",
+            "chill metal":                      baseURL + "spells/chillMetal.htm",
+            "chill touch":                      baseURL + "spells/chillTouch.htm",
+            "circle of death":                  baseURL + "spells/circleofDeath.htm",
+            "clairaudience/clairvoyance":       baseURL + "spells/clairaudienceClairvoyance.htm",
+            "clenched fist":                    baseURL + "spells/clenchedFist.htm",
+            "cloak of chaos":                   baseURL + "spells/cloakofChaos.htm",
+            "clone":                            baseURL + "spells/clone.htm",
+            "cloudkill":                        baseURL + "spells/cloudkill.htm",
+            "color spray":                      baseURL + "spells/colorSpray.htm",
+            "command":                          baseURL + "spells/command.htm",
+            "command, greater":                 baseURL + "spells/commandGreater.htm",
+            "command plants":                   baseURL + "spells/commandPlants.htm",
+            "command undead":                   baseURL + "spells/commandUndead.htm",
+            "commune":                          baseURL + "spells/commune.htm",
+            "commune with nature":              baseURL + "spells/communewithNature.htm",
+            "comprehend languages":             baseURL + "spells/comprehendLanguages.htm",
+            "cone of cold":                     baseURL + "spells/coneofCold.htm",
+            "confusion":                        baseURL + "spells/confusion.htm",
+            "confusion, lesser":                baseURL + "spells/confusionLesser.htm",
+            "consecrate":                       baseURL + "spells/consecrate.htm",
+            "contact other plane":              baseURL + "spells/contactOtherPlane.htm",
+            "contagion":                        baseURL + "spells/contagion.htm",
+            "contingency":                      baseURL + "spells/contingency.htm",
+            "continual flame":                  baseURL + "spells/continualFlame.htm",
+            "control plants":                   baseURL + "spells/controlPlants.htm",
+            "control undead":                   baseURL + "spells/controlUndead.htm",
+            "control water":                    baseURL + "spells/controlWater.htm",
+            "control weather":                  baseURL + "spells/controlWeather.htm",
+            "control winds":                    baseURL + "spells/controlWinds.htm",
+            "create food and water":            baseURL + "spells/createFoodandWater.htm",
+            "create greater undead":            baseURL + "spells/createGreaterUndead.htm",
+            "create undead":                    baseURL + "spells/createUndead.htm",
+            "create water":                     baseURL + "spells/createWater.htm",
+            "creeping doom":                    baseURL + "spells/creepingDoom.htm",
+            "crushing despair":                 baseURL + "spells/crushingDespair.htm",
+            "crushing hand":                    baseURL + "spells/crushingHand.htm",
+            "cure critical wounds":             baseURL + "spells/cureCriticalWounds.htm",
+            "cure critical wounds, mass":       baseURL + "spells/cureCriticalWoundsMass.htm",
+            "cure light wounds":                baseURL + "spells/cureLightWounds.htm",
+            "cure light wounds, mass":          baseURL + "spells/cureLightWoundsMass.htm",
+            "cure minor wounds":                baseURL + "spells/cureMinorWounds.htm",
+            "cure moderate wounds":             baseURL + "spells/cureModerateWounds.htm",
+            "cure moderate wounds, mass":       baseURL + "spells/cureModerateWoundsMass.htm",
+            "cure serious wounds":              baseURL + "spells/cureSeriousWounds.htm",
+            "cure serious wounds, mass":        baseURL + "spells/cureSeriousWoundsMass.htm",
+            "curse water":                      baseURL + "spells/curseWater.htm",
+            "dancing lights":                   baseURL + "spells/dancingLights.htm",
+            "darkness":                         baseURL + "spells/darkness.htm",
+            "darkvision":                       baseURL + "spells/darkvision.htm",
+            "daylight":                         baseURL + "spells/daylight.htm",
+            "daze":                             baseURL + "spells/daze.htm",
+            "daze monster":                     baseURL + "spells/dazeMonster.htm",
+            "death knell":                      baseURL + "spells/deathKnell.htm",
+            "death ward":                       baseURL + "spells/deathWard.htm",
+            "deathwatch":                       baseURL + "spells/deathwatch.htm",
+            "deep slumber":                     baseURL + "spells/deepSlumber.htm",
+            "deeper darkness":                  baseURL + "spells/deeperDarkness.htm",
+            "delay poison":                     baseURL + "spells/delayPoison.htm",
+            "delayed blast fireball":           baseURL + "spells/delayedBlastFireball.htm",
+            "demand":                           baseURL + "spells/demand.htm",
+            "desecrate":                        baseURL + "spells/desecrate.htm",
+            "destruction":                      baseURL + "spells/destruction.htm",
+            "detect animals or plants":         baseURL + "spells/detectAnimalsorPlants.htm",
+            "detect chaos":                     baseURL + "spells/detectChaos.htm",
+            "detect evil":                      baseURL + "spells/detectEvil.htm",
+            "detect good":                      baseURL + "spells/detectGood.htm",
+            "detect law":                       baseURL + "spells/detectLaw.htm",
+            "detect magic":                     baseURL + "spells/detectMagic.htm",
+            "detect poison":                    baseURL + "spells/detectPoison.htm",
+            "detect scrying":                   baseURL + "spells/detectScrying.htm",
+            "detect secret doors":              baseURL + "spells/detectSecretDoors.htm",
+            "detect snares and pits":           baseURL + "spells/detectSnaresandPits.htm",
+            "detect thoughts":                  baseURL + "spells/detectThoughts.htm",
+            "detect undead":                    baseURL + "spells/detectUndead.htm",
+            "dictum":                           baseURL + "spells/dictum.htm",
+            "dimension door":                   baseURL + "spells/dimensionDoor.htm",
+            "dimensional anchor":               baseURL + "spells/dimensionalAnchor.htm",
+            "dimensional lock":                 baseURL + "spells/dimensionalLock.htm",
+            "diminish plants":                  baseURL + "spells/diminishPlants.htm",
+            "discern lies":                     baseURL + "spells/discernLies.htm",
+            "discern location":                 baseURL + "spells/discernLocation.htm",
+            "disguise self":                    baseURL + "spells/disguiseSelf.htm",
+            "disintegrate":                     baseURL + "spells/disintegrate.htm",
+            "dismissal":                        baseURL + "spells/dismissal.htm",
+            "dispel chaos":                     baseURL + "spells/dispelChaos.htm",
+            "dispel evil":                      baseURL + "spells/dispelEvil.htm",
+            "dispel good":                      baseURL + "spells/dispelGood.htm",
+            "dispel law":                       baseURL + "spells/dispelLaw.htm",
+            "dispel magic":                     baseURL + "spells/dispelMagic.htm",
+            "dispel magic, greater":            baseURL + "spells/dispelMagicGreater.htm",
+            "displacement":                     baseURL + "spells/displacement.htm",
+            "disrupt undead":                   baseURL + "spells/disruptUndead.htm",
+            "disrupting weapon":                baseURL + "spells/disruptingWeapon.htm",
+            "divination":                          baseURL + "spells/divination.htm",
+            "divine favor":                        baseURL + "spells/divineFavor.htm",
+            "divine power":                        baseURL + "spells/divinePower.htm",
+            "dominate animal":                     baseURL + "spells/dominateAnimal.htm",
+            "dominate monster":                    baseURL + "spells/dominateMonster.htm",
+            "dominate person":                     baseURL + "spells/dominatePerson.htm",
+            "doom":                                baseURL + "spells/doom.htm",
+            "dream":                               baseURL + "spells/dream.htm",
+            "eagle's splendor":                    baseURL + "spells/eaglesSplendor.htm",
+            "eagle's splendor, mass":              baseURL + "spells/eaglesSplendorMass.htm",
+            "earthquake":                          baseURL + "spells/earthquake.htm",
+            "elemental swarm":                     baseURL + "spells/elementalSwarm.htm",
+            "endure elements":                     baseURL + "spells/endureElements.htm",
+            "energy drain":                        baseURL + "spells/energyDrain.htm",
+            "enervation":                          baseURL + "spells/enervation.htm",
+            "enlarge person":                      baseURL + "spells/enlargePerson.htm",
+            "enlarge person, mass":                baseURL + "spells/enlargePersonMass.htm",
+            "entangle":                            baseURL + "spells/entangle.htm",
+            "enthrall":                            baseURL + "spells/enthrall.htm",
+            "entropic shield":                     baseURL + "spells/entropicShield.htm",
+            "erase":                               baseURL + "spells/erase.htm",
+            "ethereal jaunt":                      baseURL + "spells/etherealJaunt.htm",
+            "etherealness":                        baseURL + "spells/etherealness.htm",
+            "expeditious retreat":                 baseURL + "spells/expeditiousRetreat.htm",
+            "explosive runes":                     baseURL + "spells/explosiveRunes.htm",
+            "eyebite":                             baseURL + "spells/eyebite.htm",
+            "fabricate":                           baseURL + "spells/fabricate.htm",
+            "faerie fire":                         baseURL + "spells/faerieFire.htm",
+            "false life":                          baseURL + "spells/falseLife.htm",
+            "false vision":                        baseURL + "spells/falseVision.htm",
+            "fear":                                baseURL + "spells/fear.htm",
+            "feather fall":                        baseURL + "spells/featherFall.htm",
+            "feeblemind":                          baseURL + "spells/feeblemind.htm",
+            "find the path":                       baseURL + "spells/findthePath.htm",
+            "find traps":                          baseURL + "spells/findTraps.htm",
+            "finger of death":                     baseURL + "spells/fingerofDeath.htm",
+            "fire seeds":                          baseURL + "spells/fireSeeds.htm",
+            "fire shield":                         baseURL + "spells/fireShield.htm",
+            "fire storm":                          baseURL + "spells/fireStorm.htm",
+            "fire trap":                           baseURL + "spells/fireTrap.htm",
+            "fireball":                            baseURL + "spells/fireball.htm",
+            "flame arrow":                         baseURL + "spells/flameArrow.htm",
+            "flame blade":                         baseURL + "spells/flameBlade.htm",
+            "flame strike":                        baseURL + "spells/flameStrike.htm",
+            "flaming sphere":                      baseURL + "spells/flamingSphere.htm",
+            "flare":                               baseURL + "spells/flare.htm",
+            "flesh to stone":                      baseURL + "spells/fleshtoStone.htm",
+            "fly":                                 baseURL + "spells/fly.htm",
+            "floating disk":                       baseURL + "spells/floatingDisk.htm",
+            "fog cloud":                           baseURL + "spells/fogCloud.htm",
+            "forbiddance":                         baseURL + "spells/forbiddance.htm",
+            "forcecage":                           baseURL + "spells/forcecage.htm",
+            "forceful hand":                       baseURL + "spells/forcefulHand.htm",
+            "foresight":                           baseURL + "spells/foresight.htm",
+            "fox's cunning":                       baseURL + "spells/foxsCunning.htm",
+            "fox's cunning, mass":                 baseURL + "spells/foxsCunningMass.htm",
+            "freedom":                             baseURL + "spells/freedom.htm",
+            "freedom of movement":                 baseURL + "spells/freedomofMovement.htm",
+            "freezing sphere":                     baseURL + "spells/freezingSphere.htm",
+            "gaseous form":                        baseURL + "spells/gaseousForm.htm",
+            "gate":                                baseURL + "spells/gate.htm",
+            "geas/quest":                          baseURL + "spells/geasQuest.htm",
+            "geas, lesser":                        baseURL + "spells/geasLesser.htm",
+            "gentle repose":                       baseURL + "spells/gentleRepose.htm",
+            "ghost sound":                         baseURL + "spells/ghostSound.htm",
+            "ghoul touch":                         baseURL + "spells/ghoulTouch.htm",
+            "giant vermin":                        baseURL + "spells/giantVermin.htm",
+            "glibness":                            baseURL + "spells/glibness.htm",
+            "glitterdust":                         baseURL + "spells/glitterdust.htm",
+            "globe of invulnerability":            baseURL + "spells/globeofInvulnerability.htm",
+            "globe of invulnerability, lesser":    baseURL + "spells/globeofInvulnerabilityLesser.htm",
+            "glyph of warding":                    baseURL + "spells/glyphofWarding.htm",
+            "glyph of warding, greater":           baseURL + "spells/glyphofWardingGreater.htm",
+            "goodberry":                           baseURL + "spells/goodberry.htm",
+            "good hope":                           baseURL + "spells/goodHope.htm",
+            "grasping hand":                       baseURL + "spells/graspingHand.htm",
+            "grease":                              baseURL + "spells/grease.htm",
+            "greater (spell name)":                baseURL + "spells/greaterSpellName.htm",
+            "guards and wards":                    baseURL + "spells/guardsandWards.htm",
+            "guidance":                            baseURL + "spells/guidance.htm",
+            "gust of wind":                        baseURL + "spells/gustofWind.htm",
+            "hallow":                              baseURL + "spells/hallow.htm",
+            "hallucinatory terrain":               baseURL + "spells/hallucinatoryTerrain.htm",
+            "halt undead":                         baseURL + "spells/haltUndead.htm",
+            "harm":                                baseURL + "spells/harm.htm",
+            "haste":                               baseURL + "spells/haste.htm",
+            "heal":                                baseURL + "spells/heal.htm",
+            "heal, mass":                          baseURL + "spells/healMass.htm",
+            "heal mount":                          baseURL + "spells/healMount.htm",
+            "heat metal":                          baseURL + "spells/heatMetal.htm",
+            "helping hand":                        baseURL + "spells/helpingHand.htm",
+            "heroes' feast":                       baseURL + "spells/heroesFeast.htm",
+            "heroism":                             baseURL + "spells/heroism.htm",
+            "heroism, greater":                    baseURL + "spells/heroismGreater.htm",
+            "hide from animals":                   baseURL + "spells/hidefromAnimals.htm",
+            "hide from undead":                    baseURL + "spells/hidefromUndead.htm",
+            "hideous laughter":                    baseURL + "spells/hideousLaughter.htm",
+            "hold animal":                         baseURL + "spells/holdAnimal.htm",
+            "hold monster":                        baseURL + "spells/holdMonster.htm",
+            "hold monster, mass":                  baseURL + "spells/holdMonsterMass.htm",
+            "hold person":                         baseURL + "spells/holdPerson.htm",
+            "hold person, mass":                   baseURL + "spells/holdPersonMass.htm",
+            "hold portal":                         baseURL + "spells/holdPortal.htm",
+            "holy aura":                           baseURL + "spells/holyAura.htm",
+            "holy smite":                          baseURL + "spells/holySmite.htm",
+            "holy sword":                          baseURL + "spells/holySword.htm",
+            "holy word":                           baseURL + "spells/holyWord.htm",
+            "horrid wilting":                          baseURL + "spells/horridWilting.htm",
+            "hypnotic pattern":                    baseURL + "spells/hypnoticPattern.htm",
+            "hypnotism":                           baseURL + "spells/hypnotism.htm",
+            "ice storm":                           baseURL + "spells/iceStorm.htm",
+            "identify":                                baseURL + "spells/identify.htm",
+            "illusory script":                     baseURL + "spells/illusoryScript.htm",
+            "illusory wall":                       baseURL + "spells/illusoryWall.htm",
+            "imbue with spell ability":        baseURL + "spells/imbuewithSpellAbility.htm",
+            "implosion":                           baseURL + "spells/implosion.htm",
+            "imprisonment":                        baseURL + "spells/imprisonment.htm",
+            "incendiary cloud":                    baseURL + "spells/incendiaryCloud.htm",
+            "inflict critical wounds":             baseURL + "spells/inflictCriticalWounds.htm",
+            "inflict critical wounds, mass":    baseURL + "spells/inflictCriticalWoundsMass.htm",
+            "inflict light wounds":                baseURL + "spells/inflictLightWounds.htm",
+            "inflict light wounds, mass":          baseURL + "spells/inflictLightWoundsMass.htm",
+            "inflict minor wounds":                baseURL + "spells/inflictMinorWounds.htm",
+            "inflict moderate wounds":             baseURL + "spells/inflictModerateWounds.htm",
+            "inflict moderate wounds, mass":    baseURL + "spells/inflictModerateWoundsMass.htm",
+            "inflict serious wounds":              baseURL + "spells/inflictSeriousWounds.htm",
+            "inflict serious wounds, mass":     baseURL + "spells/inflictSeriousWoundsMass.htm",
+            "insanity":                                baseURL + "spells/insanity.htm",
+            "insect plague":                       baseURL + "spells/insectPlague.htm",
+            "instant summons":                     baseURL + "spells/instantSummons.htm",
+            "interposing hand":                    baseURL + "spells/interposingHand.htm",
+            "invisibility":                        baseURL + "spells/invisibility.htm",
+            "invisibility, greater":           baseURL + "spells/invisibilityGreater.htm",
+            "invisibility, mass":                  baseURL + "spells/invisibilityMass.htm",
+            "invisibility purge":                  baseURL + "spells/invisibilityPurge.htm",
+            "invisibility sphere":                 baseURL + "spells/invisibilitySphere.htm",
+            "iron body":                           baseURL + "spells/ironBody.htm",
+            "ironwood":                                baseURL + "spells/ironwood.htm",
+            "irresistible dance":                  baseURL + "spells/irresistibleDance.htm",
+            "jump":                                    baseURL + "spells/jump.htm",
+            "keen edge":                           baseURL + "spells/keenEdge.htm",
+            "knock":                                   baseURL + "spells/knock.htm",
+            "know direction":                          baseURL + "spells/knowDirection.htm",
+            "legend lore":                             baseURL + "spells/legendLore.htm",
+            "lesser (spell name)":                 baseURL + "spells/lesserSpellName.htm",
+            "levitate":                                baseURL + "spells/levitate.htm",
+            "light":                                   baseURL + "spells/light.htm",
+            "lightning bolt":                          baseURL + "spells/lightningBolt.htm",
+            "limited wish":                        baseURL + "spells/limitedWish.htm",
+            "liveoak":                                 baseURL + "spells/liveoak.htm",
+            "locate creature":                     baseURL + "spells/locateCreature.htm",
+            "locate object":                       baseURL + "spells/locateObject.htm",
+            "longstrider":                             baseURL + "spells/longstrider.htm",
+            "lullaby":                                 baseURL + "spells/lullaby.htm",
+            "mage armor":                              baseURL + "spells/mageArmor.htm",
+            "mage hand":                           baseURL + "spells/mageHand.htm",
+            "mage's disjunction":                  baseURL + "spells/magesDisjunction.htm",
+            "mage's faithful hound":           baseURL + "spells/magesFaithfulHound.htm",
+            "mage's lucubration":                  baseURL + "spells/magesLucubration.htm",
+            "mage's magnificent mansion":          baseURL + "spells/magesMagnificentMansion.htm",
+            "mage's private sanctum":              baseURL + "spells/magesPrivateSanctum.htm",
+            "mage's sword":                        baseURL + "spells/magesSword.htm",
+            "magic aura":                              baseURL + "spells/magicAura.htm",
+            "magic circle against chaos":          baseURL + "spells/magicCircleagainstChaos.htm",
+            "magic circle against evil":       baseURL + "spells/magicCircleagainstEvil.htm",
+            "magic circle against good":       baseURL + "spells/magicCircleagainstGood.htm",
+            "magic circle against law":        baseURL + "spells/magicCircleagainstLaw.htm",
+            "magic fang":                              baseURL + "spells/magicFang.htm",
+            "magic fang, greater":                 baseURL + "spells/magicFangGreater.htm",
+            "magic jar":                           baseURL + "spells/magicJar.htm",
+            "magic missile":                       baseURL + "spells/magicMissile.htm",
+            "magic mouth":                             baseURL + "spells/magicMouth.htm",
+            "magic stone":                             baseURL + "spells/magicStone.htm",
+            "magic vestment":                          baseURL + "spells/magicVestment.htm",
+            "magic weapon":                        baseURL + "spells/magicWeapon.htm",
+            "magic weapon, greater":           baseURL + "spells/magicWeaponGreater.htm",
+            "major creation":                          baseURL + "spells/majorCreation.htm",
+            "major image":                             baseURL + "spells/majorImage.htm",
+            "make whole":                              baseURL + "spells/makeWhole.htm",
+            "mark of justice":                     baseURL + "spells/markofJustice.htm",
+            "mass (spell name)":                   baseURL + "spells/massSpellName.htm",
+            "maze":                                    baseURL + "spells/maze.htm",
+            "meld into stone":                     baseURL + "spells/meldintoStone.htm",
+            "mending":                                 baseURL + "spells/mending.htm",
+            "message":                                 baseURL + "spells/message.htm",
+            "meteor swarm":                        baseURL + "spells/meteorSwarm.htm",
+            "mind blank":                              baseURL + "spells/mindBlank.htm",
+            "mind fog":                                baseURL + "spells/mindFog.htm",
+            "minor creation":                          baseURL + "spells/minorCreation.htm",
+            "minor image":                             baseURL + "spells/minorImage.htm",
+            "miracle":                                 baseURL + "spells/miracle.htm",
+            "mirage arcana":                       baseURL + "spells/mirageArcana.htm",
+            "mirror image":                        baseURL + "spells/mirrorImage.htm",
+            "misdirection":                        baseURL + "spells/misdirection.htm",
+            "mislead":                                 baseURL + "spells/mislead.htm",
+            "mnemonic enhancer":                   baseURL + "spells/mnemonicEnhancer.htm",
+            "modify memory":                       baseURL + "spells/modifyMemory.htm",
+            "moment of prescience":                baseURL + "spells/momentofPrescience.htm",
+            "mount":                                   baseURL + "spells/mount.htm",
+            "move earth":                              baseURL + "spells/moveEarth.htm",
+            "neutralize poison":                   baseURL + "spells/neutralizePoison.htm",
+            "nightmare":                           baseURL + "spells/nightmare.htm",
+            "nondetection":                        baseURL + "spells/nondetection.htm",
+            "obscure object":                          baseURL + "spells/obscureObject.htm",
+            "obscuring mist":                          baseURL + "spells/obscuringMist.htm",
+            "open/close":                              baseURL + "spells/openClose.htm",
+            "order's wrath":                       baseURL + "spells/ordersWrath.htm",
+            "overland flight":                     baseURL + "spells/overlandFlight.htm",
+            "owl's wisdom":                        baseURL + "spells/owlsWisdom.htm",
+            "owl's wisdom, mass":                  baseURL + "spells/owlsWisdomMass.htm",
+            "passwall":                                baseURL + "spells/passwall.htm",
+            "pass without trace":                  baseURL + "spells/passwithoutTrace.htm",
+            "permanency":                              baseURL + "spells/permanency.htm",
+            "permanent image":                     baseURL + "spells/permanentImage.htm",
+            "persistent image":                    baseURL + "spells/persistentImage.htm",
+            "phantasmal killer":                   baseURL + "spells/phantasmalKiller.htm",
+            "phantom steed":                       baseURL + "spells/phantomSteed.htm",
+            "phantom trap":                        baseURL + "spells/phantomTrap.htm",
+            "phase door":                              baseURL + "spells/phaseDoor.htm",
+            "planar ally":                             baseURL + "spells/planarAlly.htm",
+            "planar ally, greater":                baseURL + "spells/planarAllyGreater.htm",
+            "planar ally, lesser":                 baseURL + "spells/planarAllyLesser.htm",
+            "planar binding":                          baseURL + "spells/planarBinding.htm",
+            "planar binding, greater":             baseURL + "spells/planarBindingGreater.htm",
+            "planar binding, lesser":              baseURL + "spells/planarBindingLesser.htm",
+            "plane shift":                             baseURL + "spells/planeShift.htm",
+            "plant growth":                        baseURL + "spells/plantGrowth.htm",
+            "poison":                                  baseURL + "spells/poison.htm",
+            "polar ray":                           baseURL + "spells/polarRay.htm",
+            "polymorph":                           baseURL + "spells/polymorph.htm",
+            "polymorph any object":                baseURL + "spells/polymorphAnyObject.htm",
+            "power word blind":                    baseURL + "spells/powerWordBlind.htm",
+            "power word kill":                     baseURL + "spells/powerWordKill.htm",
+            "power word stun":                     baseURL + "spells/powerWordStun.htm",
+            "prayer":                                  baseURL + "spells/prayer.htm",
+            "prestidigitation":                    baseURL + "spells/prestidigitation.htm",
+            "prismatic sphere":                    baseURL + "spells/prismaticSphere.htm",
+            "prismatic spray":                     baseURL + "spells/prismaticSpray.htm",
+            "prismatic wall":                          baseURL + "spells/prismaticWall.htm",
+            "produce flame":                       baseURL + "spells/produceFlame.htm",
+            "programmed image":                    baseURL + "spells/programmedImage.htm",
+            "project image":                       baseURL + "spells/projectImage.htm",
+            "protection from arrows":              baseURL + "spells/protectionfromArrows.htm",
+            "protection from chaos":           baseURL + "spells/protectionfromChaos.htm",
+            "protection from energy":              baseURL + "spells/protectionfromEnergy.htm",
+            "protection from evil":                baseURL + "spells/protectionfromEvil.htm",
+            "protection from good":                baseURL + "spells/protectionfromGood.htm",
+            "protection from law":                 baseURL + "spells/protectionfromLaw.htm",
+            "protection from spells":              baseURL + "spells/protectionfromSpells.htm",
+            "prying eyes":                             baseURL + "spells/pryingEyes.htm",
+            "prying eyes, greater":                baseURL + "spells/pryingEyesGreater.htm",
+            "purify food and drink":           baseURL + "spells/purifyFoodandDrink.htm",
+            "pyrotechnics":                        baseURL + "spells/pyrotechnics.htm",
+            "quench":                                  baseURL + "spells/quench.htm",
+            "rage":                                    baseURL + "spells/rage.htm",
+            "rainbow pattern":                     baseURL + "spells/rainbowPattern.htm",
+            "raise dead":                              baseURL + "spells/raiseDead.htm",
+            "ray of enfeeblement":                 baseURL + "spells/rayofEnfeeblement.htm",
+            "ray of exhaustion":                   baseURL + "spells/rayofExhaustion.htm",
+            "ray of frost":                        baseURL + "spells/rayofFrost.htm",
+            "read magic":                              baseURL + "spells/readMagic.htm",
+            "reduce animal":                       baseURL + "spells/reduceAnimal.htm",
+            "reduce person":                       baseURL + "spells/reducePerson.htm",
+            "reduce person, mass":                 baseURL + "spells/reducePersonMass.htm",
+            "refuge":                                  baseURL + "spells/refuge.htm",
+            "regenerate":                              baseURL + "spells/regenerate.htm",
+            "reincarnate":                             baseURL + "spells/reincarnate.htm",
+            "remove blindness/deafness":       baseURL + "spells/removeBlindnessDeafness.htm",
+            "remove curse":                        baseURL + "spells/removeCurse.htm",
+            "remove disease":                          baseURL + "spells/removeDisease.htm",
+            "remove fear":                             baseURL + "spells/removeFear.htm",
+            "remove paralysis":                    baseURL + "spells/removeParalysis.htm",
+            "repel metal or stone":                baseURL + "spells/repelMetalorStone.htm",
+            "repel vermin":                        baseURL + "spells/repelVermin.htm",
+            "repel wood":                              baseURL + "spells/repelWood.htm",
+            "repulsion":                           baseURL + "spells/repulsion.htm",
+            "resilient sphere":                    baseURL + "spells/resilientSphere.htm",
+            "resistance":                              baseURL + "spells/resistance.htm",
+            "resist energy":                       baseURL + "spells/resistEnergy.htm",
+            "restoration":                             baseURL + "spells/restoration.htm",
+            "restoration, greater":                baseURL + "spells/restorationGreater.htm",
+            "restoration, lesser":                 baseURL + "spells/restorationLesser.htm",
+            "resurrection":                        baseURL + "spells/resurrection.htm",
+            "reverse gravity":                     baseURL + "spells/reverseGravity.htm",
+            "righteous might":                     baseURL + "spells/righteousMight.htm",
+            "rope trick":                              baseURL + "spells/ropeTrick.htm",
+            "rusting grasp":                       baseURL + "spells/rustingGrasp.htm",
+            "sanctuary":                           baseURL + "spells/sanctuary.htm",
+            "scare":                                   baseURL + "spells/scare.htm",
+            "scintillating pattern":           baseURL + "spells/scintillatingPattern.htm",
+            "scorching ray":                       baseURL + "spells/scorchingRay.htm",
+            "screen":                                  baseURL + "spells/screen.htm",
+            "scrying":                                 baseURL + "spells/scrying.htm",
+            "scrying, greater":                    baseURL + "spells/scryingGreater.htm",
+            "sculpt sound":                        baseURL + "spells/sculptSound.htm",
+            "searing light":                       baseURL + "spells/searingLight.htm",
+            "secret chest":                        baseURL + "spells/secretChest.htm",
+            "secret page":                             baseURL + "spells/secretPage.htm",
+            "secure shelter":                          baseURL + "spells/secureShelter.htm",
+            "see invisibility":                    baseURL + "spells/seeInvisibility.htm",
+            "seeming":                                 baseURL + "spells/seeming.htm",
+            "sending":                                 baseURL + "spells/sending.htm",
+            "sepia snake sigil":                   baseURL + "spells/sepiaSnakeSigil.htm",
+            "sequester":                           baseURL + "spells/sequester.htm",
+            "shades":                                  baseURL + "spells/shades.htm",
+            "shadow conjuration":                  baseURL + "spells/shadowConjuration.htm",
+            "shadow conjuration, greater":      baseURL + "spells/shadowConjurationGreater.htm",
+            "shadow evocation":                    baseURL + "spells/shadowEvocation.htm",
+            "shadow evocation, greater":       baseURL + "spells/shadowEvocationGreater.htm",
+            "shadow walk":                             baseURL + "spells/shadowWalk.htm",
+            "shambler":                                baseURL + "spells/shambler.htm",
+            "shapechange":                             baseURL + "spells/shapechange.htm",
+            "shatter":                                 baseURL + "spells/shatter.htm",
+            "shield":                                  baseURL + "spells/shield.htm",
+            "shield of faith":                     baseURL + "spells/shieldofFaith.htm",
+            "shield of law":                       baseURL + "spells/shieldofLaw.htm",
+            "shield other":                        baseURL + "spells/shieldOther.htm",
+            "shillelagh":                              baseURL + "spells/shillelagh.htm",
+            "shocking grasp":                          baseURL + "spells/shockingGrasp.htm",
+            "shout":                                   baseURL + "spells/shout.htm",
+            "shout, greater":                          baseURL + "spells/shoutGreater.htm",
+            "shrink item":                             baseURL + "spells/shrinkItem.htm",
+            "silence":                                 baseURL + "spells/silence.htm",
+            "silent image":                        baseURL + "spells/silentImage.htm",
+            "simulacrum":                              baseURL + "spells/simulacrum.htm",
+            "slay living":                             baseURL + "spells/slayLiving.htm",
+            "sleep":                                   baseURL + "spells/sleep.htm",
+            "sleet storm":                             baseURL + "spells/sleetStorm.htm",
+            "slow":                                    baseURL + "spells/slow.htm",
+            "snare":                                   baseURL + "spells/snare.htm",
+            "soften earth and stone":              baseURL + "spells/softenEarthandStone.htm",
+            "solid fog":                           baseURL + "spells/solidFog.htm",
+            "song of discord":                     baseURL + "spells/songofDiscord.htm",
+            "soul bind":                           baseURL + "spells/soulBind.htm",
+            "sound burst":                             baseURL + "spells/soundBurst.htm",
+            "speak with animals":                  baseURL + "spells/speakwithAnimals.htm",
+            "speak with dead":                     baseURL + "spells/speakwithDead.htm",
+            "speak with plants":                   baseURL + "spells/speakwithPlants.htm",
+            "spectral hand":                       baseURL + "spells/spectralHand.htm",
+            "spell immunity":                          baseURL + "spells/spellImmunity.htm",
+            "spell immunity, greater":             baseURL + "spells/spellImmunityGreater.htm",
+            "spell resistance":                    baseURL + "spells/spellResistance.htm",
+            "spellstaff":                              baseURL + "spells/spellstaff.htm",
+            "spell turning":                       baseURL + "spells/spellTurning.htm",
+            "spider climb":                        baseURL + "spells/spiderClimb.htm",
+            "spike growth":                        baseURL + "spells/spikeGrowth.htm",
+            "spike stones":                        baseURL + "spells/spikeStones.htm",
+            "spiritual weapon":                    baseURL + "spells/spiritualWeapon.htm",
+            "statue":                                  baseURL + "spells/statue.htm",
+            "status":                                  baseURL + "spells/status.htm",
+            "stinking cloud":                          baseURL + "spells/stinkingCloud.htm",
+            "stone shape":                             baseURL + "spells/stoneShape.htm",
+            "stoneskin":                           baseURL + "spells/stoneskin.htm",
+            "stone tell":                              baseURL + "spells/stoneTell.htm",
+            "stone to flesh":                          baseURL + "spells/stonetoFlesh.htm",
+            "storm of vengeance":                  baseURL + "spells/stormofVengeance.htm",
+            "suggestion":                              baseURL + "spells/suggestion.htm",
+            "suggestion, mass":                    baseURL + "spells/suggestionMass.htm",
+            "summon instrument":                   baseURL + "spells/summonInstrument.htm",
+            "summon monster i":                    baseURL + "spells/summonMonsterI.htm",
+            "summon monster ii":                   baseURL + "spells/summonMonsterII.htm",
+            "summon monster iii":                  baseURL + "spells/summonMonsterIII.htm",
+            "summon monster iv":                   baseURL + "spells/summonMonsterIV.htm",
+            "summon monster v":                    baseURL + "spells/summonMonsterV.htm",
+            "summon monster vi":                   baseURL + "spells/summonMonsterVI.htm",
+            "summon monster vii":                  baseURL + "spells/summonMonsterVII.htm",
+            "summon monster viii":                 baseURL + "spells/summonMonsterVIII.htm",
+            "summon monster ix":                   baseURL + "spells/summonMonsterIX.htm",
+            "summon nature's ally i":              baseURL + "spells/summonNaturesAllyI.htm",
+            "summon nature's ally ii":             baseURL + "spells/summonNaturesAllyII.htm",
+            "summon nature's ally iii":        baseURL + "spells/summonNaturesAllyIII.htm",
+            "summon nature's ally iv":             baseURL + "spells/summonNaturesAllyIV.htm",
+            "summon nature's ally v":              baseURL + "spells/summonNaturesAllyV.htm",
+            "summon nature's ally vi":             baseURL + "spells/summonNaturesAllyVI.htm",
+            "summon nature's ally vii":        baseURL + "spells/summonNaturesAllyVII.htm",
+            "summon nature's ally viii":       baseURL + "spells/summonNaturesAllyVIII.htm",
+            "summon nature's ally ix":             baseURL + "spells/summonNaturesAllyIX.htm",
+            "summon swarm":                        baseURL + "spells/summonSwarm.htm",
+            "sunbeam":                                 baseURL + "spells/sunbeam.htm",
+            "sunburst":                                baseURL + "spells/sunburst.htm",
+            "symbol of death":                     baseURL + "spells/symbolofDeath.htm",
+            "symbol of fear":                          baseURL + "spells/symbolofFear.htm",
+            "symbol of insanity":                  baseURL + "spells/symbolofInsanity.htm",
+            "symbol of pain":                          baseURL + "spells/symbolofPain.htm",
+            "symbol of persuasion":                baseURL + "spells/symbolofPersuasion.htm",
+            "symbol of sleep":                     baseURL + "spells/symbolofSleep.htm",
+            "symbol of stunning":                  baseURL + "spells/symbolofStunning.htm",
+            "symbol of weakness":                  baseURL + "spells/symbolofWeakness.htm",
+            "sympathetic vibration":           baseURL + "spells/sympatheticVibration.htm",
+            "sympathy":                                baseURL + "spells/sympathy.htm",
+            "telekinesis":                             baseURL + "spells/telekinesis.htm",
+            "telekinetic sphere":                  baseURL + "spells/telekineticSphere.htm",
+            "telepathic bond":                     baseURL + "spells/telepathicBond.htm",
+            "teleport":                                baseURL + "spells/teleport.htm",
+            "teleport object":                     baseURL + "spells/teleportObject.htm",
+            "teleport, greater":                   baseURL + "spells/teleportGreater.htm",
+            "teleportation circle":                baseURL + "spells/teleportationCircle.htm",
+            "temporal stasis":                     baseURL + "spells/temporalStasis.htm",
+            "time stop":                           baseURL + "spells/timeStop.htm",
+            "tiny hut":                                baseURL + "spells/tinyHut.htm",
+            "tongues":                                 baseURL + "spells/tongues.htm",
+            "touch of fatigue":                    baseURL + "spells/touchofFatigue.htm",
+            "touch of idiocy":                     baseURL + "spells/touchofIdiocy.htm",
+            "transformation":                          baseURL + "spells/transformation.htm",
+            "transmute metal to wood":             baseURL + "spells/transmuteMetaltoWood.htm",
+            "transmute mud to rock":           baseURL + "spells/transmuteMudtoRock.htm",
+            "transmute rock to mud":           baseURL + "spells/transmuteRocktoMud.htm",
+            "transport via plants":                baseURL + "spells/transportviaPlants.htm",
+            "trap the soul":                       baseURL + "spells/traptheSoul.htm",
+            "tree shape":                              baseURL + "spells/treeShape.htm",
+            "tree stride":                             baseURL + "spells/treeStride.htm",
+            "true resurrection":                   baseURL + "spells/trueResurrection.htm",
+            "true seeing":                             baseURL + "spells/trueSeeing.htm",
+            "true strike":                             baseURL + "spells/trueStrike.htm",
+            "undeath to death":                    baseURL + "spells/undeathtoDeath.htm",
+            "undetectable alignment":              baseURL + "spells/undetectableAlignment.htm",
+            "unhallow":                                baseURL + "spells/unhallow.htm",
+            "unholy aura":                             baseURL + "spells/unholyAura.htm",
+            "unholy blight":                       baseURL + "spells/unholyBlight.htm",
+            "unseen servant":                          baseURL + "spells/unseenServant.htm",
+            "vampiric touch":                          baseURL + "spells/vampiricTouch.htm",
+            "veil":                                    baseURL + "spells/veil.htm",
+            "ventriloquism":                       baseURL + "spells/ventriloquism.htm",
+            "virtue":                                  baseURL + "spells/virtue.htm",
+            "vision":                                  baseURL + "spells/vision.htm",
+            "wail of the banshee":                 baseURL + "spells/wailoftheBanshee.htm",
+            "wall of fire":                        baseURL + "spells/wallofFire.htm",
+            "wall of force":                       baseURL + "spells/wallofForce.htm",
+            "wall of ice":                             baseURL + "spells/wallofIce.htm",
+            "wall of iron":                        baseURL + "spells/wallofIron.htm",
+            "wall of stone":                       baseURL + "spells/wallofStone.htm",
+            "wall of thorns":                          baseURL + "spells/wallofThorns.htm",
+            "warp wood":                           baseURL + "spells/warpWood.htm",
+            "water breathing":                     baseURL + "spells/waterBreathing.htm",
+            "water walk":                              baseURL + "spells/waterWalk.htm",
+            "waves of exhaustion":                 baseURL + "spells/wavesofExhaustion.htm",
+            "waves of fatigue":                    baseURL + "spells/wavesofFatigue.htm",
+            "web":                                     baseURL + "spells/web.htm",
+            "weird":                                   baseURL + "spells/weird.htm",
+            "whirlwind":                           baseURL + "spells/whirlwind.htm",
+            "whispering wind":                     baseURL + "spells/whisperingWind.htm",
+            "wind walk":                           baseURL + "spells/windWalk.htm",
+            "wind wall":                           baseURL + "spells/windWall.htm",
+            "wish":                                    baseURL + "spells/wish.htm",
+            "wood shape":                              baseURL + "spells/woodShape.htm",
+            "word of chaos":                       baseURL + "spells/wordofChaos.htm",
+            "word of recall":                          baseURL + "spells/wordofRecall.htm",
+            "zone of silence":                     baseURL + "spells/zoneofSilence.htm",
+            "zone of truth":                       baseURL + "spells/zoneofTruth.htm",
+
+            # Abilities
+            'ability damage':                   baseURL + "naturalSpecialAbilities.htm#abilityDamage",
+            'ability drain':                    baseURL + "naturalSpecialAbilities.htm#abilityDrain",
+            'ability score loss':               baseURL + "naturalSpecialAbilities.htm#abilityScoreLoss",
+            'alternate form':                   baseURL + "naturalSpecialAbilities.htm#alternateForm",
+            'antimagic':                        baseURL + "naturalSpecialAbilities.htm#antimagic",
+            'bite':                             baseURL + "naturalSpecialAbilities.htm#bite",
+            'blindsense':                       baseURL + "naturalSpecialAbilities.htm#blindsightAndBlindsense",
+            'blindsight':                       baseURL + "naturalSpecialAbilities.htm#blindsightAndBlindsense",
+            'breath weapon':                    baseURL + "naturalSpecialAbilities.htm#breathWeapon",
+            'burrow':                           baseURL + "naturalSpecialAbilities.htm#burrow",
+            'change shape':                     baseURL + "naturalSpecialAbilities.htm#changeShape",
+            'charm':                            baseURL + "naturalSpecialAbilities.htm#charmAndCompulsion",
+            'claw':                             baseURL + "naturalSpecialAbilities.htm#claworTalon",
+            'climb':                            baseURL + "naturalSpecialAbilities.htm#climb",
+            'cold immunity':                    baseURL + "naturalSpecialAbilities.htm#coldImmunity",
+            'compulsion':                       baseURL + "naturalSpecialAbilities.htm#charmAndCompulsion",
+            'constrict':                        baseURL + "naturalSpecialAbilities.htm#constrict",
+            'damage reduction':                 baseURL + "naturalSpecialAbilities.htm#damageReduction",
+            'darkvision':                       baseURL + "naturalSpecialAbilities.htm#darkvision",
+            'death attacks':                    baseURL + "naturalSpecialAbilities.htm#deathAttacks",
+            'disease':                          baseURL + "naturalSpecialAbilities.htm#disease",
+            'energy drain':                     baseURL + "naturalSpecialAbilities.htm#energyDrainAndNegativeLevels",
+            'etherealness':                     baseURL + "naturalSpecialAbilities.htm#etherealness",
+            'evasion':                          baseURL + "naturalSpecialAbilities.htm#evasionAndImprovedEvasion",
+            'fast healing':                     baseURL + "naturalSpecialAbilities.htm#fastHealing",
+            'fear aura':                        baseURL + "naturalSpecialAbilities.htm#fearAura",
+            'fear cones':                       baseURL + "naturalSpecialAbilities.htm#fearConesSpandRays",
+            'fear rays':                        baseURL + "naturalSpecialAbilities.htm#fearConesSpandRays",
+            'fear':                             baseURL + "naturalSpecialAbilities.htm#fear",
+            'fire immunity':                    baseURL + "naturalSpecialAbilities.htm#fireImmunity",
+            'fly':                              baseURL + "naturalSpecialAbilities.htm#fly",
+            'frightful presence':               baseURL + "naturalSpecialAbilities.htm#frightfulPresence",
+            'gaseous form':                     baseURL + "naturalSpecialAbilities.htm#gaseousForm",
+            'gaze attacks':                     baseURL + "naturalSpecialAbilities.htm#gazeAttacks",
+            'gore':                             baseURL + "naturalSpecialAbilities.htm#gore",
+            'improved evasion':                 baseURL + "naturalSpecialAbilities.htm#evasionAndImprovedEvasion",
+            'improved grab':                    baseURL + "naturalSpecialAbilities.htm#improvedGrab",
+            'incorporeality':                   baseURL + "naturalSpecialAbilities.htm#incorporeality",
+            'invisibility':                     baseURL + "naturalSpecialAbilities.htm#invisibility",
+            'level loss':                       baseURL + "naturalSpecialAbilities.htm#levelLoss",
+            'low-light vision':                 baseURL + "naturalSpecialAbilities.htm#lowLightVision",
+            'manufactured weapons':             baseURL + "naturalSpecialAbilities.htm#manufacturedWeapons",
+            'movement modes':                   baseURL + "naturalSpecialAbilities.htm#movementModes",
+            'natural weapons':                  baseURL + "naturalSpecialAbilities.htm#naturalWeapons",
+            'negative levels':                  baseURL + "naturalSpecialAbilities.htm#energyDrainAndNegativeLevels",
+            'nonabilities':                     baseURL + "naturalSpecialAbilities.htm#nonabilities",
+            'paralysis':                        baseURL + "naturalSpecialAbilities.htm#paralysis",
+            'poison':                           baseURL + "naturalSpecialAbilities.htm#poison",
+            'polymorph':                        baseURL + "naturalSpecialAbilities.htm#polymorph",
+            'pounce':                           baseURL + "naturalSpecialAbilities.htm#pounce",
+            'powerful charge':                  baseURL + "naturalSpecialAbilities.htm#powerfulCharge",
+            'psionics':                         baseURL + "naturalSpecialAbilities.htm#psionics",
+            'rake':                             baseURL + "naturalSpecialAbilities.htm#rake",
+            'rays':                             baseURL + "naturalSpecialAbilities.htm#rays",
+            'regeneration':                     baseURL + "naturalSpecialAbilities.htm#regeneration",
+            'resistance to energy':             baseURL + "naturalSpecialAbilities.htm#resistanceToEnergy",
+            'scent':                            baseURL + "naturalSpecialAbilities.htm#scent",
+            'slam':                             baseURL + "naturalSpecialAbilities.htm#slaporSlam",
+            'slap':                             baseURL + "naturalSpecialAbilities.htm#slaporSlam",
+            'sonic attacks':                    baseURL + "naturalSpecialAbilities.htm#sonicAttacks",
+            'spell immunity':                   baseURL + "naturalSpecialAbilities.htm#spellImmunity",
+            'spell resistance':                 baseURL + "naturalSpecialAbilities.htm#spellResistance",
+            'spells':                           baseURL + "naturalSpecialAbilities.htm#spells",
+            'sting':                            baseURL + "naturalSpecialAbilities.htm#sting",
+            'summon':                           baseURL + "naturalSpecialAbilities.htm#summon",
+            'swallow whole':                    baseURL + "naturalSpecialAbilities.htm#swallowWhole",
+            'swim':                             baseURL + "naturalSpecialAbilities.htm#swim",
+            'talon':                            baseURL + "naturalSpecialAbilities.htm#claworTalon",
+            'telepathy':                        baseURL + "naturalSpecialAbilities.htm#telepathy",
+            'tentacle':                         baseURL + "naturalSpecialAbilities.htm#tentacle",
+            'trample':                          baseURL + "naturalSpecialAbilities.htm#trample",
+            'tremorsense':                      baseURL + "naturalSpecialAbilities.htm#tremorsense",
+            'turn resistance':                  baseURL + "naturalSpecialAbilities.htm#turnResistance",
+            'vulnerability to energy':          baseURL + "naturalSpecialAbilities.htm#vulnerabilitytoEnergy",
+
+            #Psionics
+
+        }
+
+    def plugin_enabled(self):
+        reg = []
+        reg2 = []
+        reg3 = []
+        for search_phrase in self.checklist.keys():
+            if len(reg) < 300:
+                reg.append("(?<![a-zA-Z0-9>/\#\-])" + search_phrase + "[s]*(?!\w+|[<])")
+            elif len(reg2) < 300:
+                reg2.append("(?<![a-zA-Z0-9>/\#\-])" + search_phrase + "[s]*(?!\w+|[<])")
+            elif len(reg3) < 400:
+                reg3.append("(?<![a-zA-Z0-9>/\#\-])" + search_phrase + "[s]*(?!\w+|[<])")
+
+        reg = string.join(reg, "|")
+        reg2 = string.join(reg2, "|")
+        reg3 = string.join(reg3, "|")
+        self.regex = re.compile(reg, re.I)
+        self.regex2 = re.compile(reg2, re.I)
+        self.regex3 = re.compile(reg3, re.I)
+
+    def plugin_disabled(self):
+        pass
+
+    def regsub(self, m):
+        term = m.group(0).lower()
+        while not self.checklist.has_key(term) and len(term) > 1:
+            term = term[:-1]
+        return '<a href="' + self.checklist[term] + '">' + m.group(0) + '</a>'
+
+    def replace(self, text):
+        text = self.regex.sub(self.regsub, text)
+        text = self.regex2.sub(self.regsub, text)
+        text = self.regex3.sub(self.regsub, text)
+        return text
+
+    def pre_parse(self, text):
+        text = self.replace(text)
+        return text
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        text = self.replace(text)
+        return text, type, name
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxstatus.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,69 @@
+import os
+import orpg.pluginhandler
+from random import randint
+from time import time
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    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 = 'Idle Time'
+        self.author = 'Woody, mDuo13'
+        self.help = "When you haven't sent a message to chat for a minute or more, this\n"
+        self.help += "plugin sets your status to end with '(* Mins)' where the * is however many\n"
+        self.help += "minutes you've been inactive. You can also set a custom message for the timed\n"
+        self.help += "idle by typing '/idle status *text*' where *text* is that message."
+
+        self.idle_timer_status = ''
+        self.start_time = ''
+        self.minutes = ''
+        self.last_update = ''
+
+    def plugin_enabled(self):
+        #This is where you set any variables that need to be initalized when your plugin starts
+        self.plugin_addcommand('/idle', self.on_idle, 'status - This sets your status to what ever you type')
+        self.reset_time()
+
+    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
+        #such as closing windows created by the plugin
+        self.plugin_removecmd('/idle')
+
+    def on_idle(self, cmdargs):
+        args = cmdargs.split(None,1)
+
+        if args[0] == 'status':
+            self.idle_timer_status = cmdargs[7:]
+        else:
+            self.chat.InfoPost("Invalid syntax for /idle command")
+
+    def reset_time(self):
+        self.start_time = time()
+        self.minutes = 0
+        self.last_update = 0
+
+    def pre_parse(self, text):
+        #This is called just before a message is parsed by openrpg
+        self.reset_time()
+        return text
+
+    def refresh_counter(self):
+        if self.idle_timer_status == '':
+            self.idle_timer_status = self.settings.get_setting("IdleStatusAlias")
+
+        current_time = time()
+        self.minutes = round((current_time - self.start_time)/60,1)
+
+        if self.minutes > 1:
+            plur = 's'
+        else:
+            plur = ''
+
+        if current_time - self.last_update >= 30:
+            self.session.set_text_status(self.idle_timer_status + ' (' + str(self.minutes) + ' min' + plur + ')')
+            self.last_update = time()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/xxurl2link.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,52 @@
+import os
+import orpg.pluginhandler
+import re
+
+class Plugin(orpg.pluginhandler.PluginHandler):
+    # Initialization subroutine.
+    #
+    # !self : instance of self
+    # !chat : instance of the chat window to write to
+    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 = 'URL to link conversion'
+        self.author = 'tdb30 tbaleno@wrathof.com'
+        self.help = "This plugin automaticaly wraps urls in link tags\n"
+        self.help += "making them clickable."
+
+        self.url_regex = None
+        self.mailto_regex = None
+
+    def plugin_enabled(self):
+        #This is where you set any variables that need to be initalized when your plugin starts
+        self.url_regex = re.compile("(?<![\[=\"a-z0-9:/.])((?:http|ftp|gopher)://)?(?<![@a-z])((?:[a-z0-9\-]+[-.]?[a-z0-9]+)*\.(?:[a-z]{2,4})(?:[a-z0-9_=\?\#\&~\%\.\-/\:\+;]*))", re.I)
+
+        self.mailto_regex = re.compile("(?<![=\"a-z0-9:/.])((?:[a-z0-9]+[_]?[a-z0-9]*)+@{1}(?:[a-z0-9]+[-.]?[a-z0-9]+)*\.(?:[a-z]{2,4}))", re.I)
+
+    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
+        #such as closing windows created by the plugin
+        pass
+
+    def pre_parse(self, text):
+        text = self.mailto_regex.sub(self.regmailsub, text)
+        text = self.url_regex.sub(self.regurlsub, text)
+        return text
+
+    def plugin_incoming_msg(self, text, type, name, player):
+        text = self.mailto_regex.sub(self.regmailsub, text)
+        text = self.url_regex.sub(self.regurlsub, text)
+        return text, type, name
+
+    def regmailsub(self, m):
+        term = m.group(0).lower()
+        return '<a href="mailto:' + term + '">' + m.group(0) + '</a>'
+
+    def regurlsub(self, m):
+        link = m.group(2)
+        if m.group(1) != None:
+            return '<a href="' + m.group(1).lower() + link + '">' + m.group(0) + '</a>'
+        else:
+            return '<a href="http://' + link + '">' + link + '</a>'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyver.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,56 @@
+import sys  # Needed for version
+import string # Needed for split
+from orpg.orpg_version import * # To get NEEDS_PYTHON_MAJOR, MINOR, and MICRO
+
+def getNumber(numberstringtoconvert):
+    currentnumberstring = ""
+    for number in numberstringtoconvert:
+        if number >= "0" and number <="9":
+            currentnumberstring += number
+        else:
+            break
+    if currentnumberstring == "":
+        return 0
+    else:
+        return int(currentnumberstring)
+# This checks to make sure a certain version of python or later is in use
+# The actual version requested is set in orpg/openrpg_version
+def checkPyVersion():
+
+    #  taking the first split on whitespace of sys.version gives us the version info without the build stuff
+    vernumstring = string.split(sys.version)[0]
+
+    #  This splits the version string into (major,minor,micro).  Actually, a complicating factor
+    #  is that there sometimes isn't a micro, e.g. 2.0.  We'll just do it the hard way to build
+    #  the numbers instead of tuple unpacking.
+    splits = string.split(vernumstring,'.')
+
+    #  Assign default values
+    micro = 0
+    minor = 0
+    major = 0
+    # Assign the integer conversion of each, assuming that it was found.  If not found, we assumed 0 just above.
+    if len(splits) > 0:
+        major = getNumber(splits[0])
+    if len(splits) > 1:
+        minor = getNumber(splits[1])
+    if len(splits) > 2:
+        micro = getNumber(splits[2])
+    # Check against min version info from orpg/orpg_version
+    if major >= NEEDS_PYTHON_MAJOR:
+        if major > NEEDS_PYTHON_MAJOR:  # If it's greater, there's no need to check the minor
+            return
+        if minor >= NEEDS_PYTHON_MINOR:
+            if minor > NEEDS_PYTHON_MINOR:  # If it's greater, there's no need to check the micro
+                return
+            if micro >= NEEDS_PYTHON_MICRO:
+                return
+
+    # If we get here, then the version check failed so we inform the user of the required version and exit
+    print "Invalid python version being used.  Detected version %s,"  % (vernumstring)
+    print "but version %i.%i.%i or better is required!" % (NEEDS_PYTHON_MAJOR,NEEDS_PYTHON_MINOR,NEEDS_PYTHON_MICRO)
+    print "You either have the wrong version of Python installed or you"
+    print "have multiple versions installed.  If you have multiple versions,"
+    print "please make sure Python %i.%i.%i or better is found first in your path or explicitly" % (NEEDS_PYTHON_MAJOR,NEEDS_PYTHON_MINOR,NEEDS_PYTHON_MICRO)
+    print "start using, \"<path>\python <program>\"."
+    sys.exit( 1 )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/readme.txt	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,18 @@
+How to use OpenRPG version 1.7.x
+Make sure you have installed python 2.5+ and wxPython 2.8+!
+
+Launching OpenRPG:
+OpenRPG can be launch by executing the OpenRPG.pyw script
+located in the openrpg folder.  On windows, Macs, and
+Unix with a GUI, this can be accomplished by double
+clicking OpenRPG.pyw. From a shell, type: python OpenRPG.pyw
+
+Launching a OpenRPG game server:
+You want to launch your own server execute the
+Server.py.
+
+For more info on how to use OpenRPG,
+visit http://www.openrpg.com
+
+
+-OpenRPG Team
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rollback.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+
+import sys
+import os
+
+HG = os.environ["HG"]
+
+os.system(HG + " rollback")
+os.system(HG + " revert --all")
+print "Since you reverted, I am guessing there are issues with the last update"
+print "PLEASE notify me on either the openrpg.com boards, or by email"
+print "digitalxero@gmail.com"
+print "You can continue using your rolled back version by launching openrpg from"
+print "the command line with the command: OpenRPG*.py -n"
+print "the -n tells the system not to update, it will still run the downloader"
+print "but it will not change your files"
+raw_input("press <enter> key to terminate program")
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/start_developer.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+import sys
+import os
+
+import pyver
+pyver.checkPyVersion()
+
+
+for key in sys.modules.keys():
+    if 'orpg' in key:
+        del sys.modules[key]
+
+from orpg.orpg_wx import *
+import orpg.main
+
+if WXLOADED:
+    mainapp = orpg.main.orpgApp(0)
+    mainapp.MainLoop()
+else:
+    print "You really really need wx!"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/start_noupdate.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import sys
+import os
+
+#HG = os.environ["HG"]
+
+import pyver
+pyver.checkPyVersion()
+
+#os.system(HG + ' pull "http://hg.assembla.com/traipse"')
+
+for key in sys.modules.keys():
+    if 'orpg' in key:
+        del sys.modules[key]
+
+from orpg.orpg_wx import *
+import orpg.main
+
+if WXLOADED:
+    mainapp = orpg.main.orpgApp(0)
+    mainapp.MainLoop()
+else:
+    print "You really really need wx!"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/start_release.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+
+import sys
+import os
+
+HG = os.environ["HG"]
+
+import pyver
+pyver.checkPyVersion()
+
+
+os.system(HG + ' pull "http://hg.assembla.com/traipse"')
+os.system(HG + " update -C grumpy-goblin")
+
+for key in sys.modules.keys():
+    if 'orpg' in key:
+        del sys.modules[key]
+
+from orpg.orpg_wx import *
+import orpg.main
+
+if WXLOADED:
+    mainapp = orpg.main.orpgApp(0)
+    mainapp.MainLoop()
+else:
+    print "You really really need wx!"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/start_server.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+import os
+import string
+import sys
+import time
+import gc
+import getopt
+import traceback
+
+# Simple usuage text for request via help or command line errors
+def usage( retValue ):
+    print "\nUsage: \n[-d --directory directory] - Directory of where to load config files\n" + \
+    "[-n Server Name]\n" + \
+    "[-p]\n" + \
+    "[-l Lobby Boot Password]\n" + \
+    "[-r Run From???]\n" + \
+    "[-h --help]\n\n" + \
+    "[-m --manual]\n\n" +\
+    "Where -p is used to request meta registration.  If -p is given, the boot\n" + \
+    "password and server name MUST be provided.  If no options are given, user\n" + \
+    "will be prompted for information.\n\n"
+    sys.exit( retValue )
+
+HG = os.environ["HG"]
+
+import pyver
+pyver.checkPyVersion()
+
+os.system(HG + ' pull "http://hg.assembla.com/openrpg_rc"')
+os.system(HG + ' update')
+
+import orpg.networking.mplay_server
+import orpg.networking.meta_server_lib
+
+if __name__ == '__main__':
+    gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
+    gc.enable()
+
+    orpg_server = orpg.networking.mplay_server.mplay_server()
+    lobby_boot_pwd = orpg_server.boot_pwd
+    server_directory = orpg_server.userPath
+    server_reg = orpg_server.reg
+    server_name = orpg_server.name
+    manualStart = False
+
+    # See if we have command line arguments in need of processing
+    try:
+        (opts, args) = getopt.getopt( sys.argv[1:], "n:phml:m:d:", ["help","manual","directory="] )
+        for o, a in opts:
+            if o in ("-d", "--directory"):
+                if (a[(len(a)-1):len(a)] != os.sep):
+                    a = a + os.sep
+                    if not (os.access(a, os.W_OK) and os.path.isdir(a)):
+                        print "*** ERROR *** Directory '" + a + "' Either doesn't exist, or you don't have permission to write to it."
+                        sys.exit(1)
+                server_directory = a
+            # Server Name
+            if o in ( "-n", ):
+                server_name = a
+            # Post server to meta
+            if o in ( "-p", ):
+                server_reg = 'Y'
+            # Lobby Password
+            if o in ( "-l", ):
+                lobby_boot_pwd = a
+            # Help
+            if o in ( "-h", "--help" ):
+                usage( 0 )
+            #Dont Auto Init Server
+            if o in ("-m", "--manual"):
+                manualStart = True
+    except:
+        print
+        usage( 1 )
+
+
+    # start server!
+    orpg_server.name = server_name
+    orpg_server.reg = server_reg
+    orpg_server.boot_pwd = lobby_boot_pwd
+    orpg_server.userPath = server_directory
+    orpg_server.remoteadmin = 1
+
+    if not manualStart:
+        orpg_server.initServer()
+
+    print "-----------------------------------------------------"
+    print "Type 'help' or '?' or 'h' for server console commands"
+    print "-----------------------------------------------------"
+
+    opt = "None"
+    orpg_server.console_log()
+    try:
+        while (opt != "kill") and ( opt != "quit"):
+            opt = raw_input("action?:")
+            words = opt.split()
+            if opt == "initserver":
+                userpath = raw_input("Please enter the directory you wish to load files from [myfile]:")
+                orpg_server.initServer(userPath=userpath)
+            elif opt == "broadcast":
+                msg = raw_input("Message:")
+                orpg_server.broadcast(msg)
+            elif opt == "dump":
+                orpg_server.player_dump()
+            elif opt == "dump groups":
+                orpg_server.groups_list()
+            elif opt == "get lobby boot password":
+                print "Lobby boot password is:  " + orpg_server.groups['0'].boot_pwd
+                print
+            elif opt == "register":
+                msg = raw_input("Enter server name:  ")
+                orpg_server.register(msg)
+            elif opt == "unregister":
+                orpg_server.unregister()
+            elif opt == "set lobby boot password":
+                lobby_boot_pwd = raw_input("Enter boot password for the Lobby:  ")
+                orpg_server.groups['0'].boot_pwd = lobby_boot_pwd
+            elif len(words) == 2 and words[0] == "group":
+                orpg_server.group_dump(words[1])
+            elif opt == "help" or opt == "?" or opt == "h":
+                orpg_server.print_help()
+            elif opt == "search":
+                msg = raw_input("Pattern:")
+                orpg_server.search(msg)
+            elif opt == "remove room":
+                print "Removing a room will kick everyone in that room off your server."
+                print "You might consider going to that room and letting them know what you are about to do."
+                groupnumber = raw_input("Room group number:")
+                orpg_server.remove_room(groupnumber)
+            elif opt == "remotekill":
+                if orpg_server.toggleRemoteKill():
+                    print "Remote Kill has been allowed!"
+                else:
+                    print "Remote Kill has been disallowed"
+            elif opt == "uptime":
+                orpg_server.uptime()
+            elif opt == "roompasswords":
+                print orpg_server.RoomPasswords()
+            elif opt == "list":
+                orpg_server.player_list()
+            elif opt == "log":
+                orpg_server.console_log()
+            elif opt == "log meta":
+                orpg_server.toggleMetaLogging()
+            elif len(words) > 0 and words[0] == "logfile":
+                if len(words) > 1:
+                    if words[1] == "off":
+                        orpg_server.NetworkLogging(0)
+                    elif words[1] == "on":
+                        orpg_server.NetworkLogging(1)
+                    elif words[1] == "split":
+                        orpg_server.NetworkLogging(2)
+                    else:
+                        print "<command useage> logfile [off|on|split]"
+                else:
+                    print orpg_server.NetworkLoggingStatus()
+            elif (len(words) > 0 and words[0]) == "monitor":
+                if len(words) >1:
+                    print "Attempting to monitor client \""+str(words[1])+"\""
+                    orpg_server.monitor(words[1])
+                else: print "<command useage> monitor (player id #)"
+            elif opt == "purge clients":
+                try:
+                    orpg_server.kick_all_clients()
+                except Exception, e:
+                    traceback.print_exc()
+            elif len(words)>0 and words[0] == "zombie":
+                if len(words) > 1:
+                    if words[1] == "set":
+                        if len(words) > 2:
+                            try:
+                                t = int(words[2])
+                                orpg_server.zombie_time = t
+                                print ("--> Zombie auto-kick time set to "+str(t)+" minutes");
+                            except Exception, e:
+                                print "Invalid zombie time!"
+                                traceback.print_exc()
+                        else:
+                            orpg_server.zombie_time = 480
+                            print "--> Zombie auto-kick time set to default (480 mins)";
+                    else:
+                        print "<command useage> zombie [set [mins]]"
+                else:
+                    timeout = int(orpg_server.zombie_time)
+                    print ("--> Zombie auto-kick time set to "+str(timeout)+" minutes.  Use \"zombie set [min]\" to change.");
+            elif opt == "kick":
+                kick_id = raw_input("Kick Player #  ")
+                kick_msg = raw_input("Reason(optional):  ")
+                orpg_server.admin_kick(kick_id,kick_msg)
+            elif opt == "ban":
+                ban_id = raw_input("Ban Player #  ")
+                ban_msg = raw_input("Reason(optional):  ")
+                orpg_server.admin_ban(ban_id, ban_msg)
+            elif opt == "sendsize":
+                send_len = raw_input("Send Size #  ")
+                orpg_server.admin_setSendSize(send_len)
+            elif opt == "remoteadmin":
+                if orpg_server.toggleRemoteAdmin():
+                    print "Remote Admin has been allowed!"
+                else:
+                    print "Remote Admin has been disallowed"
+            elif opt == "togglelobbysound":
+                if orpg_server.admin_toggleSound():
+                    print "Sending a lobby sound has been enabled!"
+                    print 'Playing: ' + orpg_server.lobbySound
+                else:
+                    print "Sending a lobby sound has been disabled"
+            elif opt == "lobbysound":
+                soundfile = raw_input("Sound File URL: ")
+                orpg_server.admin_soundFile(soundfile)
+            else:
+                if (opt == "kill") or (opt == "quit"):
+                    orpg_server.saveBanList()
+                    print ("Closing down OpenRPG server. Please wait...")
+                else:
+                    print ("[UNKNOWN COMMAND: \""+opt+"\" ]")
+
+    except Exception, e:
+        print "EXCEPTION: "+str(e)
+        traceback.print_exc()
+        raw_input("press <enter> key to terminate program")
+
+    orpg_server.kill_server()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/start_server_gui.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+import os
+import sys
+
+HG = os.environ["HG"]
+
+import pyver
+pyver.checkPyVersion()
+
+os.system(HG + ' pull "http://hg.assembla.com/openrpg_rc"')
+os.system(HG + ' update')
+
+from orpg.orpg_wx import *
+
+if WXLOADED:
+    import orpg.networking.mplay_server_gui
+    app = orpg.networking.mplay_server_gui.ServerGUIApp(0)
+    app.MainLoop()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system_check.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,35 @@
+import sys
+import time
+import platform
+import wx
+import orpg.orpg_version
+
+class system_check:
+
+    def start(self,log_file='openrpg_sysinfo.txt'):
+        self.log_file = open(log_file,'w')
+        self.log_file.write("OpenRPG System Info " + time.strftime( '%m-%d-%y', time.localtime( time.time() ) ))
+        self.check_openrpg()
+        self.check_py()
+        self.check_wxpython()
+        self.check_platform()
+        self.log_file.close()
+
+    def check_wxpython(self):
+        self.log_file.write("\nwxPython Version: " + wx.__version__)
+
+    def check_py(self):
+        self.log_file.write("\nPython: " + sys.version)
+
+    def check_platform(self):
+        self.log_file.write("\nPlatform: " + platform.platform())
+
+    def check_openrpg(self):
+        self.log_file.write("\nOpenRPG Version: " + orpg.orpg_version.VERSION)
+        self.log_file.write("\nOpenRPG Build: " + orpg.orpg_version.BUILD)
+
+
+
+if __name__ == "__main__":
+    syscheck = system_check()
+    syscheck.start()