changeset 0:1fd2201f5c36

Initial commit of parpg-core.
author M. George Hansen <technopolitica@gmail.com>
date Sat, 14 May 2011 01:12:35 -0700
parents
children 43787e2ca831 e2a8e3805b04
files .hgignore AUTHORS.txt LICENSE.txt README.txt SConscript bin/unix/parpg bin/unix/parpg.in nsis/AdvUninstLog2.nsh nsis/download_mirror.nsh nsis/python-module.nsh nsis/setup.nsi pylintrc run_tests.py setup.py src/parpg/__init__.py src/parpg/application.py src/parpg/charactercreationcontroller.py src/parpg/charactercreationview.py src/parpg/characterstatistics.py src/parpg/common/__init__.py src/parpg/common/listeners/__init__.py src/parpg/common/listeners/command_listener.py src/parpg/common/listeners/console_executor.py src/parpg/common/listeners/event_listener.py src/parpg/common/listeners/key_listener.py src/parpg/common/listeners/mouse_listener.py src/parpg/common/listeners/widget_listener.py src/parpg/common/ordereddict.py src/parpg/common/utils.py src/parpg/console.py src/parpg/controllerbase.py src/parpg/dialogue.py src/parpg/dialogueactions.py src/parpg/dialoguecontroller.py src/parpg/dialogueparsers.py src/parpg/dialogueprocessor.py src/parpg/font.py src/parpg/gamemap.py src/parpg/gamemodel.py src/parpg/gamescenecontroller.py src/parpg/gamesceneview.py src/parpg/gamestate.py src/parpg/gui/__init__.py src/parpg/gui/actionsbox.py src/parpg/gui/charactercreationview.py src/parpg/gui/containergui.py src/parpg/gui/containergui_base.py src/parpg/gui/dialogs.py src/parpg/gui/dialoguegui.py src/parpg/gui/drag_drop_data.py src/parpg/gui/filebrowser.py src/parpg/gui/hud.py src/parpg/gui/inventorygui.py src/parpg/gui/menus.py src/parpg/gui/popups.py src/parpg/gui/spinners.py src/parpg/gui/tabwidget.py src/parpg/inventory.py src/parpg/main.py src/parpg/mainmenucontroller.py src/parpg/mainmenuview.py src/parpg/objects/__init__.py src/parpg/objects/action.py src/parpg/objects/actors.py src/parpg/objects/base.py src/parpg/objects/composed.py src/parpg/objects/containers.py src/parpg/objects/doors.py src/parpg/objects/items.py src/parpg/quest_engine.py src/parpg/serializers.py src/parpg/settings.py src/parpg/sounds.py src/parpg/viewbase.py system.cfg.in tests/__init__.py tests/test_carryable_container.py tests/test_console.py tests/test_container.py tests/test_crate.py tests/test_dialogueprocessor.py tests/test_game_object.py tests/test_inventory.py tests/test_living.py tests/test_lockable.py tests/test_npc.py tests/test_objects_base.py tests/test_openable.py tests/test_scriptable.py tests/test_single_item_container.py tests/test_wearable.py
diffstat 88 files changed, 14727 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,2 @@
+glob:**.py[co]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AUTHORS.txt	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,57 @@
+List of contributors, sorted by their estimated amount of contributions and impact on the development (descending order). For individual attribution, check the <asset>.license files.
+
+== Audio ==
+* davematney
+* meinmartini
+* timong
+* sindwiller
+* semolina
+* qubodup
+* gary
+
+== Graphics ==
+* sirren
+* gaspard
+* q_x
+* justinoperable
+* lamoot
+* zimble
+* infrared
+* border
+* shrew81
+* comscar
+* continuum
+
+== Mechanics ==
+* zenbitz
+
+== Programming ==
+* maximinus
+* orlandov
+* kaydeth
+* eliedebrauwer
+* b0rland
+* vaporice
+* beliar
+* saritor
+* bretzel13
+* atomic
+* tie
+* meggie
+* pirum
+* andrewbc
+* icelus
+* zenbitz
+* Technomage
+* Aspidites
+
+== Project management ==
+* mvbarracuda
+* shevy
+
+== Writing ==
+* zenbitz
+* shevy
+* nineofhearts
+* dk
+* eleazzaar
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE.txt	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,619 @@
+                      GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  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
+them 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 prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  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.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including parpg to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey 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;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  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.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+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.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.txt	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,120 @@
+=================================================================================
+                            PARPG TECHDEMO 2 README
+                          Last Updated: 6 March 2011
+=================================================================================
+
+== Introduction ==
+Welcome to the second official release of the open source post-apocalyptic RPG undertaking PARPG. This release is based on PARPG SVN trunk revison 877. Built against and tested with FIFE SVN trunk revision 3586.
+
+== Table of contents ==
+01 Important note
+02 Preamble
+03 License
+04 Supported operating systems
+05 How to run PARPG
+06 Keybindings
+07 Settings
+08 Map editor
+09 Known bugs / limitations
+10 How to provide feedback
+11 Acknowlegements
+
+== 01 Important note ==
+The install instructions outlined in this README only apply to official stable releases of PARPG! In case you're reading this file but want to test an SVN version of PARPG, please read the install instructions for SVN versions that found at our project wiki instead: http://wiki.parpg.net/Download#SVN
+
+== 02 Preamble ==
+This preamble has been written to give you an upfront overview of the current status of the project. In the last months the project slowed down considerably and we basically decided that we'll rather release what we have right now instead of trying to implement more features and add more content, risking to collapse before we reach the finish line of the first release.
+
+This said: we didn't manage to fix a number of bugs and didn't reach the original aim to implement a couple of quests that you can actually play through. You can walk around, talk to other characters, change to different (placeholder) maps, listen to background audio tracks and play around with the settings. That's it. It's possible that this is the first and last official PARPG release due the issues we are facing lately. Hopefully this release will help to attract some fresh blood that will get the project back on track. If not, it was at least fun for us to spend a year of our life on such a project and release what we have achieved to the public. And move on after that.
+
+== 03 License ==
+* All Python code is licensed under GPL 3.0. For the full license see: <PARPG>/game/license/gpl30.license
+* Assets are either licensed under Creative Commons 3.0 BY-SA or public domain. For the full licenses see: <PARPG>/game/license/cc30_by_sa.license & <PARPG>/game/license/public_domain.license
+* Most directories contain an <asset>.license file that states under which license the specific assets in the directory are released under and who created them. Use this file to properly attribute developers in case you reuse PARPG assets in your project
+
+== 04 Supported operating systems ==
+PARPG is officially supported in combination with these operating systems:
+* Linux (32 & 64bit)
+* Win32 (32 & 64bit)
+
+PARPG should nevertheless also work with (prolly some tweaks required):
+* FreeBSD (use Linux install instructions)
+* Mac OSX
+* OpenBSD (use Linux install instructions)
+
+== 05 How to run PARPG ==
+=== 05.1 Linux ===
+* Unpack the fife_r3236_src.tar.gz archive to a folder of your choice (called <FIFE> from now on)
+* Install scons using your package manager
+* Install swig using your package manager
+* Install Python 2.5 or 2.6 and a matching version of PyYAML if you don't have them installed yet
+* cd into the <FIFE> directory and run: scons ext && sudo scons install-python
+* In case you encounter any build errors, install the necessary libraries and try these pointers http://wiki.fifengine.de/Building:Linux:SCons
+* Unpack the parpg_td1_r522_src.tar.gz archive to a folder of your choice (called <PARPG> from now on)
+* cd into the <PARPG> directory and run: ./run.py
+
+=== 05.2 Mac ===
+* Unpack the fife_r3236_src.tar.gz archive to a folder of your choice (called <FIFE> from now on)
+* Install scons using macports
+* Install swig using macports
+* Install guichan using macports
+* Install Python 2.5 or 2.6 and a matching version of PyYAML if you don't have them installed yet
+* cd into the <FIFE> directory and run: scons ext && sudo scons install-python
+* In case you encounter any build errors, install the necessary libraries and try these pointers http://wiki.fifengine.de/Building:Mac:Scons
+* Unpack the parpg_td1_r522_src.tar.gz archive to a folder of your choice (called <PARPG> from now on)
+* cd into the <PARPG> directory and run: ./run.py
+
+=== 05.3 Win32 ===
+* Run parpg_td1_r522_win32.exe and install PARPG to a location of your choice (called <PARPG> from now on)
+* Make sure that you've installed (Active)Python 2.6, PyYAML and FIFE (they either ship with the installer or can be downloaded with the help of it)
+* To run PARPG itself, cd into the <PARPG> directory and execute run.py (should be automatically associated with your Python 2.6 interpreter)
+* To run PARPG with file logging, cd into the <PARPG> directory and execute log_parpg.bat; the log will be written to logfile.txt
+
+== 06 Keybindings ==
+* <i> toggles the inventory on and off
+* <t> shows the grid layout
+* <m> toggles music on and off
+* <q> shows quit dialog
+* <F5> shows the grid coordinates
+* <F7> takes a screenshot
+* <F10> shows a debug console
+* <ESC> shows the main menu
+* <PAUSE> (un)pauses the game
+
+== 07 Settings ==
+There are two ways to change the ingame settings like used renderer, resolution, etc.:
+* Press <ESC> and select Options from the menu
+* Edit <PARPG>/game/settings.xml manually, entries are mostly self-explanatory
+
+== 08 Map editor ==
+* To run the PARPG map editor, cd into the PARPG directory and execute parpg_editor.py
+* A work in progress map editor tutorial can be found at http://wiki.parpg.net/Map_editor_tutorial
+
+== 09 Known bugs / limitations ==
+* FPS rate tends to be rather slow on some systems; the FIFE team is working to address this in their view_performance branch, for details see http://forums.parpg.net/index.php?topic=570.0
+* While it's possible to talk to the NPCs, you can't solve any of the quests without using the ingame console (see ticket #229)
+* Some unit tests are broken (see ticket #253)
+* Random music playback hasn't been implemented (see ticket #250)
+* PARPG crashes on exit in combination with Windows Vista 64bit if FIFE is built via MSVC 2005/2008 (see ticket #114 for a workaround)
+* There is no regular main menu, you'll have to use the <ESC> key to access the placeholder menu (see ticket #118)
+* Containers don't support taking everything out of them at once (see ticket #225)
+* You can't drop items onto the ground (see ticket #223)
+* Some map objects are semi-transparent while they should have been rendered opaque (see ticket #251)
+* Full list of open tickets can be found at http://parpg-trac.cvsdude.com/parpg/report/1
+
+== 10 How to provide feedback ==
+You can reach the development team in a couple of ways and provide feedback:
+* PARPG forums: http://forums.parpg.net/
+* PARPG IRC channel: #parpg @ irc.freenode.net (visit http://irc.parpg.net to join the channel via your web browser)
+* PARPG bug tracker: http://bugs.parpg.net
+* PARPG blog: http://blog.parpg.net/
+
+All feedback is welcome!
+
+== 11 Acknowlegements ==
+We thank everyone who supported the project and believed in the idea of creating an old school isometric 2d RPG despite the general trend of more and more higher budget cross platform (read: consoles and win32) 3d RPGs. We do especially want to thank qubodup, a loyal follower who supported the project since its first steps and has helped us by spreading the word about PARPG with his ingame videos and his continued news coverage at http://freegamer.blogspot.com/
+
+Furthermore we want to thank the developers of FIFE, the engine of our choice. Without you this project would not have been possible at all and we're glad that the project is finally in good hands again now. If you want to find out more about FIFE, check out their website http://www.fifengine.net/
+
+The PARPG developers,
+http://www.parpg.net
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SConscript	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,16 @@
+Import('environment', 'config_dict')
+
+environment['PY_PACKAGES'] += [
+    Dir('src/parpg'),
+]
+config_file_template = File('system.cfg.in')
+environment['CONFIG_FILES'] += [
+    environment.Substfile(config_file_template, SUBST_DICT=config_dict)
+]
+environment['DATA_FILES'] += []
+# TODO M. George Hansen 2011-05-12: Implement windows executable.
+executable_template = File('bin/unix/parpg.in')
+environment['EXECUTABLES'] += [
+    environment.Substfile(executable_template, SUBST_DICT=config_dict)
+]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/unix/parpg	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+/usr/bin/python "/home/george/Workspace/Games/parpg/build/lib/python2.6/dist-packages/main.py" -m "/home/george/Workspace/Games/parpg/build/lib/python2.6/dist-packages"  "/home/george/Workspace/Games/parpg/build/etc/parpg/system.cfg"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/unix/parpg.in	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+@PYTHON@ "@PY_LIB_DIR@/main.py" -m "@PY_LIB_DIR@"  "@SYS_CONF_DIR@/system.cfg"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nsis/AdvUninstLog2.nsh	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,427 @@
+     ;_____________________________ HEADER FILE BEGIN ____________________________
+
+     # Advanced Uninstall Log NSIS header
+     # Version 1.0 2007-01-31
+     # By Red Wine (http://nsis.sf.net/User:Red_Wine)
+     
+     # Usage: See included examples Uninstall_Log_Default_UI.nsi - Uninstall_Log_Modern_UI.nsi
+
+!verbose push
+   !verbose 3
+
+!ifndef ADVANCED_UNINSTALL.LOG_NSH
+   !define ADVANCED_UNINSTALL.LOG_NSH
+
+!ifndef INSTDIR_REG_ROOT | INSTDIR_REG_KEY
+   !error "You must properly define both INSTDIR_REG_ROOT and INSTDIR_REG_KEY"
+!endif
+
+!ifndef UNINSTALL_LOG
+   !define UNINSTALL_LOG         "Uninstall"
+!endif
+
+!ifndef UNINST_LOG_VERBOSE
+   !define UNINST_LOG_VERBOSE    "3"
+!endif
+
+!verbose pop
+
+!echo "Advanced Uninstall Log NSIS header v1.0 2007-01-31 by Red Wine (http://nsis.sf.net/User:Red_Wine)"
+
+!verbose push
+   !verbose ${UNINST_LOG_VERBOSE}
+
+!define UNINST_EXE     "$INSTDIR\${UNINSTALL_LOG}.exe"
+!define UNINST_DAT     "$INSTDIR\${UNINSTALL_LOG}.dat"
+!define UNLOG_PART     "$PLUGINSDIR\part."
+!define UNLOG_TEMP     "$PLUGINSDIR\unlog.tmp"
+!define EXCLU_LIST     "$PLUGINSDIR\exclude.tmp"
+!define UNLOG_HEAD     "=========== Uninstaller Log please do not edit this file ==========="
+
+Var TargetDir
+
+ var unlog_tmp_0
+ var unlog_tmp_1
+ var unlog_tmp_2
+ var unlog_tmp_3
+ var unlog_error
+
+!include FileFunc.nsh
+!include TextFunc.nsh
+
+!insertmacro Locate
+!insertmacro un.Locate
+!insertmacro DirState
+!insertmacro un.DirState
+!insertmacro FileJoin
+!insertmacro TrimNewLines
+!insertmacro un.TrimNewLines
+
+;.............................. Uninstaller Macros ..............................
+
+/* Used to get rid of pesky newline characters that prevent installations from happenning */
+Function un.TrimNewlines
+ Exch $R0 ; removes whatever was stored into the stack with the file data
+ Push $R1
+ Push $R2
+ StrCpy $R1 0
+ 
+ loop:
+   IntOp $R1 $R1 - 1
+   StrCpy $R2 $R0 1 $R1
+   StrCmp $R2 "$\r" loop
+   StrCmp $R2 "$\n" loop
+   IntOp $R1 $R1 + 1
+   IntCmp $R1 0 no_trim_needed
+   StrCpy $R0 $R0 $R1
+ 
+ no_trim_needed:
+   Pop $R2
+   Pop $R1
+   Exch $R0
+FunctionEnd
+
+Function un.RemoveEmptyDirs
+  Pop $9
+
+  FindFirst $0 $1 "$TargetDir$9*"
+  StrCmp $0 "" End
+  Loop:
+    StrCmp $1 "" End
+    StrCmp $1 "." Next
+    StrCmp $1 ".." Next
+      Push $0
+      Push $1
+      Push $9
+      Push "$9$1\"
+      Call un.RemoveEmptyDirs
+      Pop $9
+      Pop $1
+      Pop $0
+    RMDir "$TargetDir$9$1"
+    Next:
+    FindNext $0 $1
+    Goto Loop
+  End:
+  FindClose $0
+FunctionEnd
+
+!macro UNINSTALL.NEW_UNINSTALL TempDir
+	StrCpy $TargetDir ${TempDir}
+  FileOpen $unlog_tmp_0 "$TargetDir\Uninstall.dat" r
+  
+  UninstallLoop:
+    ClearErrors #ClearErrors from displaying
+    FileRead $unlog_tmp_0 $R0 #Reads string lines from a file and stores them in $R0
+		IfErrors UninstallEnd #If EOF has been reached
+    Push $R0 #throw contents of $R0 into stack
+	    Call un.TrimNewLines
+    Pop $R0 #takes contents of stack and places into $R0
+    Delete "$R0" #delete file
+    Goto UninstallLoop
+  UninstallEnd:
+  FileClose $unlog_tmp_0
+
+  Delete "$TargetDir\Uninstall.dat"
+  Delete "$TargetDir\Uninstall.exe"
+  
+  Push "\"
+  Call un.RemoveEmptyDirs
+  RMDir "$TargetDir"
+!macroend
+
+!macro INTERACTIVE_UNINSTALL
+  !verbose push
+     !verbose ${UNINST_LOG_VERBOSE}
+     
+     !ifdef INTERACTIVE_UNINSTALL
+        !error "INTERACTIVE_UNINSTALL is already defined"
+     !endif
+
+        var unlog_tmp_4
+        var unlog_tmp_5
+
+     !define INTERACTIVE_UNINSTALL
+
+     !ifdef INTERACTIVE_UNINSTALL & UNATTENDED_UNINSTALL
+        !error "You must insert either Interactive or Unattended Uninstall neither both, neither none."
+     !endif
+
+     !ifdef UnLog_Uninstall_CallBackFunc
+        !undef UnLog_Uninstall_CallBackFunc
+     !endif
+
+     !ifndef UnLog_Uninstall_CallBackFunc
+        !insertmacro UNINSTALL.LOG_UNINSTALL_INTERACTIVE
+        !define UnLog_Uninstall_CallBackFunc "un._LocateCallBack_Function_Interactive"
+     !endif
+
+  !verbose pop
+!macroend
+
+
+!macro UNATTENDED_UNINSTALL
+  !verbose push
+     !verbose ${UNINST_LOG_VERBOSE}
+
+     !ifdef UNATTENDED_UNINSTALL
+        !error "UNATTENDED_UNINSTALL is already defined"
+     !endif
+
+     !define UNATTENDED_UNINSTALL
+
+     !ifdef INTERACTIVE_UNINSTALL & UNATTENDED_UNINSTALL
+        !error "You must insert either Interactive or Unattended Uninstall neither both, neither none."
+     !endif
+
+     !ifdef UnLog_Uninstall_CallBackFunc
+        !undef UnLog_Uninstall_CallBackFunc
+     !endif
+
+     !ifndef UnLog_Uninstall_CallBackFunc
+        !insertmacro UNINSTALL.LOG_UNINSTALL_UNATTENDED
+        !define UnLog_Uninstall_CallBackFunc "un._LocateCallBack_Function_Unattended"
+     !endif
+
+  !verbose pop
+!macroend
+
+
+!macro UNINSTALL.LOG_UNINSTALL_UNATTENDED
+
+  Function un._LocateCallBack_Function_Unattended
+    start:
+        FileRead $unlog_tmp_2 "$unlog_tmp_3" ${NSIS_MAX_STRLEN}
+        ${un.TrimNewLines} "$unlog_tmp_3" "$unlog_tmp_3"
+        StrCmp "$unlog_tmp_3" "$R9" islog
+        IfErrors nolog
+        goto start
+
+    islog:
+        IfFileExists "$R9\*.*" isdir
+
+    isfile:
+        Delete "$R9"
+        goto end
+
+    isdir:
+        RmDir "$R9"
+	IntOp $unlog_tmp_1 $unlog_tmp_1 + 1
+        goto end
+
+    nolog:
+        ClearErrors
+        StrCmp "$R9" "${UNINST_EXE}" isfile
+        StrCmp "$R9" "${UNINST_DAT}" isfile
+
+    end:
+        FileSeek $unlog_tmp_2 0 SET
+	Push $unlog_tmp_0
+  FunctionEnd
+
+!macroend
+
+
+!macro UNINSTALL.LOG_UNINSTALL_INTERACTIVE
+
+  Function un._LocateCallBack_Function_Interactive
+    start:
+        FileRead $unlog_tmp_2 "$unlog_tmp_3" ${NSIS_MAX_STRLEN}
+        ${un.TrimNewLines} "$unlog_tmp_3" "$unlog_tmp_3"
+        StrCmp "$unlog_tmp_3" "$R9" islog
+        IfErrors nolog
+        goto start
+
+    islog:
+        IfFileExists "$R9\*.*" isdir
+
+    isfile:
+        Delete "$R9"
+        goto end
+
+    isdir:
+        RmDir "$R9"
+	IntOp $unlog_tmp_1 $unlog_tmp_1 + 1
+        goto end
+
+    nolog:
+        ClearErrors
+	FileSeek $unlog_tmp_4 0 SET
+    read:
+        FileRead $unlog_tmp_4 "$unlog_tmp_3"
+        ${un.TrimNewLines} "$unlog_tmp_3" "$unlog_tmp_3"
+        StrCmp "$unlog_tmp_3" "$R9" end
+        IfErrors +2
+        goto read
+        ClearErrors 
+        StrCmp "$R9" "${UNINST_EXE}" isfile
+        StrCmp "$R9" "${UNINST_DAT}" isfile
+        IfFileExists "$R9\*.*" msgdir
+
+	MessageBox MB_ICONQUESTION|MB_YESNO \
+        'Delete File "$R9"?' /SD IDNO IDYES isfile IDNO nodel
+
+    msgdir:
+        MessageBox MB_ICONQUESTION|MB_YESNO \
+        'Delete Directory "$R9"?' /SD IDNO IDYES isdir IDNO nodel
+
+    nodel:
+	FileSeek $unlog_tmp_4 0 END
+	FileWrite $unlog_tmp_4 "$R9$\r$\n"
+
+    end:
+        FileSeek $unlog_tmp_2 0 SET
+	Push $unlog_tmp_0
+  FunctionEnd
+
+!macroend
+
+;................................. Installer Macros .................................
+
+!macro UNINSTALL.LOG_INSTALL_UNATTENDED
+
+  Function _LocateCallBack_Function_Install
+    loop:
+        FileRead $unlog_tmp_2 "$unlog_tmp_3" ${NSIS_MAX_STRLEN}
+        ${TrimNewLines} "$unlog_tmp_3" "$unlog_tmp_3"
+        IfErrors 0 +4
+        ClearErrors
+        FileSeek $unlog_tmp_2 0 SET
+        goto next
+        StrCmp "$R9" "$unlog_tmp_3" end
+        goto loop
+    next:
+	FileWrite $unlog_tmp_1 "$R9$\r$\n"
+    end:
+	Push $unlog_tmp_0
+  FunctionEnd
+
+!macroend
+
+
+!ifdef UnLog_Install_Func_CallBack
+    !undef UnLog_Install_Func_CallBack
+!endif
+
+!ifndef UnLog_Install_Func_CallBack
+    !insertmacro UNINSTALL.LOG_INSTALL_UNATTENDED
+    !define UnLog_Install_Func_CallBack "_LocateCallBack_Function_Install"
+!endif
+
+
+!macro UNINSTALL.LOG_PREPARE_INSTALL
+  !verbose push
+     !verbose ${UNINST_LOG_VERBOSE}
+
+      Push $0
+      Push $1
+        ClearErrors
+        ReadRegStr "$0"  ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "${UNINSTALL_LOG}Directory"
+        IfErrors next
+        ${DirState} "$0" $1
+        StrCmp "$1" "-1" next
+        StrCmp "$1" "0" next
+        IfFileExists "$0\${UNINSTALL_LOG}.dat" next
+        MessageBox MB_ICONEXCLAMATION|MB_OK \
+        "Previous installation detected at $0.$\n\
+        Required file ${UNINSTALL_LOG}.dat is missing.$\n$\nIt is highly recommended \
+        to select an empty directory and perform a fresh installation." /SD IDOK
+        StrCpy $unlog_error "error"
+
+    next:
+        ClearErrors
+        StrCmp "$PLUGINSDIR" "" 0 +2
+           InitPluginsDir
+
+        GetTempFileName "$1"
+        FileOpen $0 "$1" w
+        FileWrite $0 "${UNLOG_HEAD}$\r$\n"
+        FileClose $0
+        Rename "$1" "${UNLOG_TEMP}"
+      Pop $1
+      Pop $0
+
+  !verbose pop
+!macroend
+
+
+!macro UNINSTALL.LOG_UPDATE_INSTALL
+  !verbose push
+     !verbose ${UNINST_LOG_VERBOSE}
+
+        Delete "${UNINST_DAT}"
+        Rename "${UNLOG_TEMP}" "${UNINST_DAT}"
+        WriteUninstaller "${UNINST_EXE}"
+        WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "${UNINSTALL_LOG}.dat" "${UNINST_DAT}"
+        WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "${UNINSTALL_LOG}Directory" "$INSTDIR"
+
+  !verbose pop
+!macroend
+
+
+!define uninstall.log_install "!insertmacro UNINSTALL.LOG_INSTALL"
+
+!macro UNINSTALL.LOG_INSTALL FileOpenWrite FileOpenRead TargetDir
+  !verbose push
+     !verbose ${UNINST_LOG_VERBOSE}
+
+	FileOpen $unlog_tmp_1 "${FileOpenWrite}" w
+	FileOpen $unlog_tmp_2 "${FileOpenRead}" r
+
+	${Locate} "${TargetDir}" "/L=FD" "${UnLog_Install_Func_CallBack}"
+
+        StrCmp $unlog_error "error" 0 +2
+        ClearErrors
+
+	IfErrors 0 +2
+	MessageBox MB_ICONEXCLAMATION|MB_OK "Error creating ${UNINSTALL_LOG} Log." /SD IDOK
+
+	FileClose $unlog_tmp_1
+	FileClose $unlog_tmp_2
+
+  !verbose pop
+!macroend
+
+
+!define uninstall.log_mergeID "!insertmacro UNINSTALL.LOG_MERGE"
+
+!macro UNINSTALL.LOG_MERGE UnlogPart
+  !verbose push
+     !verbose ${UNINST_LOG_VERBOSE}
+
+        ${FileJoin} "${UNLOG_TEMP}" "${UnlogPart}" "${UNLOG_TEMP}"
+
+  !verbose pop
+!macroend
+
+
+!macro UNINSTALL.LOG_OPEN_INSTALL
+  !verbose push
+     !verbose ${UNINST_LOG_VERBOSE}
+
+        StrCmp $unlog_error "error" +2
+        ${uninstall.log_install} "${EXCLU_LIST}" "${UNINST_DAT}" "$OUTDIR"
+
+  !verbose pop
+!macroend
+
+
+!macro UNINSTALL.LOG_CLOSE_INSTALL
+  !verbose push
+     !verbose ${UNINST_LOG_VERBOSE}
+
+   !define ID ${__LINE__}
+
+        ${uninstall.log_install} "${UNLOG_PART}${ID}" "${EXCLU_LIST}" "$OUTDIR"
+        ${uninstall.log_mergeID} "${UNLOG_PART}${ID}"
+
+   !undef ID ${__LINE__}
+
+  !verbose pop
+!macroend
+
+!endif
+
+!verbose pop
+     ;_____________________________ HEADER FILE END ____________________________
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nsis/download_mirror.nsh	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,249 @@
+var RandomSeed		# The seed for the random number generator
+ 
+###############################################################
+#
+# Returns a random number
+#
+# Usage:
+# 	Push Seed (or previously generated number)
+#	Call RandomNumber
+#	Pop Generated Random Number
+Function RandomNumber
+	Exch $R0
+ 
+	IntOp $R0 $R0 * "13"
+	IntOp $R0 $R0 + "3"
+	IntOp $R0 $R0 % "1048576" # Values goes from 0 to 1048576 (2^20)
+ 
+	Exch $R0
+FunctionEnd
+ 
+####################################################
+#
+# Returns a random number between 0 and MaxValue-1
+#
+# Usage:
+# 	Push Seed (or previously generated number)
+#	Push MaxValue
+#	Call RandomNumber
+#	Pop Generated Random Number
+#	Pop NewSeed
+Function LimitedRandomNumber
+	Exch $R0
+	Exch
+	Exch $R1
+	Push $R2
+	Push $R3
+ 
+	StrLen $R2 $R0
+	Push $R1
+RandLoop:
+	Call RandomNumber
+	Pop $R1	#Random Number
+	IntCmp $R1 $R0 0 NewRnd
+	StrLen $R3 $R1	
+	IntOp $R3 $R3 - $R2
+	IntOp $R3 $R3 / "2"
+	StrCpy $R3 $R1 $R2 $R3
+	IntCmp $R3 $R0 0 RndEnd
+NewRnd:
+	Push $R1
+	Goto RandLoop
+RndEnd:
+	StrCpy $R0 $R3
+	IntOp $R0 $R0 + "0" #removes initial 0's
+	Pop $R3
+	Pop $R2
+	Exch $R1
+	Exch
+	Exch $R0
+FunctionEnd
+
+###################################################
+# 
+# Downloads a file from a list of mirrors
+# (the fist mirror is selected at random)
+#
+# Usage:
+# 	Push Mirror1
+# 	[Push Mirror2]
+# 	...
+# 	[Push Mirror10]
+#	Push NumMirrors		# 10 Max
+#	Push FileName
+#	Call DownloadFromRandomMirror
+#	Pop Return
+#
+#	Returns the last NSISdl result
+Function DownloadFromRandomMirror
+	Exch $R1 #File name
+	Exch
+	Exch $R0 #Number of Mirrors
+	Push $0
+	Exch 3
+	Pop $0	#Mirror 1
+	IntCmpU "2" $R0 0 0 +4
+		Push $1
+		Exch 4
+		Pop $1	#Mirror 2
+	IntCmpU "3" $R0 0 0 +4
+		Push $2
+		Exch 5
+		Pop $2	#Mirror 3
+	IntCmpU "4" $R0 0 0 +4
+		Push $3
+		Exch 6
+		Pop $3	#Mirror 4
+	IntCmpU "5" $R0 0 0 +4
+		Push $4
+ 
+		Exch 7
+		Pop $4	#Mirror 5
+	IntCmpU "6" $R0 0 0 +4
+		Push $5
+		Exch 8
+		Pop $5	#Mirror 6
+	IntCmpU "7" $R0 0 0 +4
+		Push $6
+		Exch 9
+		Pop $6	#Mirror 7
+	IntCmpU "8" $R0 0 0 +4
+		Push $7
+		Exch 10
+		Pop $7	#Mirror 8
+	IntCmpU "9" $R0 0 0 +4
+		Push $8
+		Exch 11
+		Pop $8	#Mirror 9
+	IntCmpU "10" $R0 0 0 +4
+		Push $9
+		Exch 12
+		Pop $9	#Mirror 10
+	Push $R4
+	Push $R2
+	Push $R3
+	Push $R5
+	Push $R6
+ 
+	StrCmp $RandomSeed "" 0 +2
+		StrCpy $RandomSeed $HWNDPARENT   #init RandomSeed
+ 
+	Push $RandomSeed
+	Push $R0
+	Call LimitedRandomNumber
+	Pop $R3
+	Pop $RandomSeed
+ 
+	StrCpy $R5 "0"
+MirrorsStart:
+	IntOp $R5 $R5 + "1"
+	StrCmp $R3 "0" 0 +3
+		StrCpy $R2 $0
+		Goto MirrorsEnd
+	StrCmp $R3 "1" 0 +3
+		StrCpy $R2 $1
+		Goto MirrorsEnd
+	StrCmp $R3 "2" 0 +3
+		StrCpy $R2 $2
+		Goto MirrorsEnd
+	StrCmp $R3 "3" 0 +3
+		StrCpy $R2 $3
+		Goto MirrorsEnd
+	StrCmp $R3 "4" 0 +3
+		StrCpy $R2 $4
+		Goto MirrorsEnd
+	StrCmp $R3 "5" 0 +3
+		StrCpy $R2 $5
+		Goto MirrorsEnd
+	StrCmp $R3 "6" 0 +3
+		StrCpy $R2 $6
+		Goto MirrorsEnd
+	StrCmp $R3 "7" 0 +3
+		StrCpy $R2 $7
+		Goto MirrorsEnd
+	StrCmp $R3 "8" 0 +3
+		StrCpy $R2 $8
+		Goto MirrorsEnd
+	StrCmp $R3 "9" 0 +3
+		StrCpy $R2 $9
+		Goto MirrorsEnd
+	StrCmp $R3 "10" 0 +3
+		StrCpy $R2 $10
+		Goto MirrorsEnd
+ 
+MirrorsEnd:
+	IntOp $R6 $R3 + "1"
+	DetailPrint "Downloading from mirror $R6: $R2"
+ 
+	NSISdl::download "$R2" "$R1"
+	Pop $R4
+	StrCmp $R4 "success" Success
+	StrCmp $R4 "cancel" DownloadCanceled
+	IntCmp $R5 $R0 NoSuccess
+	DetailPrint "Download failed (error $R4), trying with other mirror"
+	IntOp $R3 $R3 + "1"
+	IntCmp $R3 $R0 0 MirrorsStart
+	StrCpy $R3 "0"
+	Goto MirrorsStart
+ 
+DownloadCanceled:
+	DetailPrint "Download Canceled: $R2"
+	Goto End
+NoSuccess:		
+	DetailPrint "Download Failed: $R1"
+	Goto End
+Success:
+	DetailPrint "Download completed."
+End:
+	Pop $R6
+	Pop $R5
+	Pop $R3
+	Pop $R2
+	Push $R4
+	Exch
+	Pop $R4
+	Exch 2
+	Pop $R1
+	Exch 2
+	Pop $0
+	Exch
+ 
+	IntCmpU "2" $R0 0 0 +4
+		Exch 2	
+		Pop $1
+		Exch
+	IntCmpU "3" $R0 0 0 +4
+		Exch 2	
+		Pop $2
+		Exch
+	IntCmpU "4" $R0 0 0 +4
+		Exch 2	
+		Pop $3
+		Exch
+	IntCmpU "5" $R0 0 0 +4
+		Exch 2	
+		Pop $4
+		Exch
+	IntCmpU "6" $R0 0 0 +4
+		Exch 2	
+		Pop $5
+		Exch
+	IntCmpU "7" $R0 0 0 +4
+		Exch 2	
+		Pop $6
+		Exch
+	IntCmpU "8" $R0 0 0 +4
+		Exch 2	
+		Pop $7
+		Exch
+	IntCmpU "9" $R0 0 0 +4
+		Exch 2	
+		Pop $8
+		Exch
+	IntCmpU "10" $R0 0 0 +4
+		Exch 2	
+		Pop $9
+		Exch
+	Pop $R0
+FunctionEnd
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nsis/python-module.nsh	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,106 @@
+!include nsDialogs.nsh
+!include LogicLib.nsh
+!macro _FileExists2 _a _b _t _f
+	StrCmp `${_b}` `` `${_f}` 0
+	IfFileExists `${_b}` `0` `${_f}` ;returns true if this is a directory
+	IfFileExists `${_b}\*.*` `${_f}` `${_t}` ;so if it is a directory, jump to false
+!macroend
+!undef FileExists
+!define FileExists `"" FileExists2`
+!macro _DirExists _a _b _t _f
+	StrCmp `${_b}` `` `${_f}` 0
+	IfFileExists `${_b}\*.*` `${_t}` `${_f}`
+!macroend
+!define DirExists `"" DirExists`
+!include mui2.nsh
+!include WinMessages.nsh
+
+Var PythonList
+Var Directory
+Var Browse
+Var PythonPath
+Var PythonVer
+!macro PAGE_PYTHON_MODULE 
+Page custom PythonModulePage PythonModulePage_OnLeave
+!macroend
+
+Function PythonModulePage
+	!insertmacro MUI_HEADER_TEXT_PAGE "Install Python Module" "Please select where to install the Python module"
+    nsDialogs::Create 1018
+	Pop $0
+
+	${NSD_CreateListBox} 0 0 300u 120u ""
+	Pop $PythonList
+
+    StrCpy $0 0
+    EnumRegKey $1 HKLM SOFTWARE\Python\PythonCore $0
+    ${DoWhile} $1 != ""
+        ReadRegStr $2 HKLM SOFTWARE\Python\PythonCore\$1\InstallPath ""
+        ${if} $2 != "" 
+            ${NSD_LB_AddString} $PythonList "$1" 
+        ${endif}
+        IntOp $0 $0 + 1
+        EnumRegKey $1 HKLM SOFTWARE\Python\PythonCore $0
+    ${Loop}
+    ${NSD_LB_GetCount} $PythonList $0
+    ${if} $0 == 0
+    MessageBox MB_ICONINFORMATION|MB_OK "No Python version detected. If you don't have Python installed you should install it first. Otherwise you can input the python directory below."
+    ${endif}
+    ${NSD_LB_AddString} $PythonList "Custom"
+    ${NSD_LB_SelectString} $PythonList $PythonVer
+    ${NSD_OnChange} $PythonList PythonModulePage_OnLeave_OnChange_PythonList
+    
+	${NSD_CreateDirRequest} 0 121u 280u 15u ""
+	Pop $Directory  
+    
+    ${NSD_CreateButton} 281u 121u 19u 15u "..."
+    Pop $Browse
+    GetFunctionAddress $0 PythonModulePage_OnClick_Browse
+    
+    
+    nsDialogs::OnClick /NOUNLOAD $Browse $0    
+    
+    nsDialogs::Show
+FunctionEnd
+
+Function PythonModulePage_OnLeave
+${NSD_GetText} $Directory $PythonPath
+${NSD_LB_GetSelection} $PythonList $PythonVer
+${If} $PythonPath == ""
+${OrIfNot} ${DirExists} $PythonPath 
+    Abort
+${endif}
+FunctionEnd
+
+Function PythonModulePage_OnLeave_OnChange_PythonList
+${NSD_LB_GetSelection} $PythonList $0
+${if} $0 != "Custom"
+    EnableWindow $Directory 0
+    EnableWindow $Browse 0
+    ${if} $0 != ""
+        ReadRegStr $1 HKLM SOFTWARE\Python\PythonCore\$0\InstallPath ""
+        ${NSD_SetText} $Directory $1
+    ${endif}
+${else}
+    EnableWindow $Directory 1
+    EnableWindow $Browse 1
+${endif}
+
+FunctionEnd
+
+Function PythonModulePage_OnClick_Browse
+    
+    ${NSD_GetText} $Directory $0
+
+	nsDialogs::SelectFolderDialog /NOUNLOAD "Please select a target directory" "$0"
+
+	Pop $0
+	${If} $0 == error
+
+		Abort
+
+	${EndIf}
+
+	${NSD_SetText} $Directory $0
+
+FunctionEnd
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nsis/setup.nsi	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,436 @@
+###############################################################
+#NSIS script for PARPG
+#
+# Non standard plugins
+#
+# ZipDLL : http://nsis.sourceforge.net/ZipDLL_plug-in
+# Please note the install instructions
+#
+# Advanced Uninstall Log 2
+# Header for that is in the same directory as this script - AdvUninstLog2.nsh
+#
+# Python module installer
+# Header for that is in the same directory as this script - python-module.nsh
+#
+!define PRODUCT_NAME "PARPG Techdemo 2"
+!define PRODUCT_VERSION "SVN trunk r788"
+!define PRODUCT_PUBLISHER "PARPG Development Team"
+!define PRODUCT_WEB_SITE "http://www.parpg.net/"
+!define INSTDIR_REG_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
+!define INSTDIR_REG_ROOT "HKLM"
+
+!define PARPG_DIR "game"
+!define EXEC_SCRIPT_NAME "parpg-run.py"
+# MUI 1.67 compatible ------
+!include "MUI2.nsh"
+!include "AdvUninstLog2.nsh"
+!include "python-module.nsh"
+!include "download_mirror.nsh"
+
+# MUI Settings
+!define MUI_ABORTWARNING
+!define MUI_ICON "${PARPG_DIR}\gui\icons\window_icon.ico"
+!define MUI_UNICON "${PARPG_DIR}\gui\icons\window_icon.ico"
+
+# Welcome page
+!insertmacro MUI_PAGE_WELCOME
+!define MUI_PAGE_CUSTOMFUNCTION_PRE SelectFilesCheck
+!insertmacro MUI_PAGE_COMPONENTS
+
+# License page
+!insertmacro MUI_PAGE_LICENSE "${PARPG_DIR}\license\gpl30.license"
+# Instfiles page Externals
+!define MUI_PAGE_CUSTOMFUNCTION_PRE SelectFilesExternals
+!insertmacro MUI_PAGE_INSTFILES
+# Directory page PARPG
+!define MUI_PAGE_CUSTOMFUNCTION_PRE SelectFilesPARPG
+!insertmacro MUI_PAGE_DIRECTORY
+!insertmacro PAGE_PYTHON_MODULE
+# Instfiles page PARPG
+!insertmacro MUI_PAGE_INSTFILES
+
+
+# Finish page
+!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README.txt"
+!define MUI_PAGE_CUSTOMFUNCTION_LEAVE DeleteSectionsINI
+!insertmacro MUI_PAGE_FINISH
+
+# Uninstaller pages
+!insertmacro MUI_UNPAGE_INSTFILES
+
+# Language files
+!insertmacro MUI_LANGUAGE "English"
+
+# ZipFile Support
+!include "ZipDLL.nsh"
+
+# MUI end ------
+
+RequestExecutionLevel admin #For Vista. Admin is needed to install in program files directory
+
+Name "${PRODUCT_NAME}"
+OutFile "parpg_td2_r877_win32.exe"
+InstallDir "$PROGRAMFILES\PARPG"
+ShowInstDetails show
+ShowUnInstDetails show
+
+!insertmacro UNATTENDED_UNINSTALL
+
+#Externals, at least Python, have to be installed first
+SectionGroup Externals Externals
+#---------- DOWNLOAD PYTHON -------
+Section "ActivePython (required)" Python
+  DetailPrint "Downloading Python"
+  NSISdl::download http://downloads.activestate.com/ActivePython/releases/2.7.1.4/ActivePython-2.7.1.4-win32-x86.msi $TEMP\pysetup.msi
+  Pop $R0 #Get the return value
+    StrCmp $R0 "success" +3
+      MessageBox MB_OK "Failed to download Python installer: $R0"
+      Quit
+
+  DetailPrint "Installing Python"
+  ExecWait '"msiexec" /i "$TEMP\pysetup.msi"'
+
+  DetailPrint "Deleting Python installer"
+  Delete $TEMP\pysetup.msi
+SectionEnd
+
+#------------ PyYAML --------------
+Section "PyYAML (required)" PyYAML
+  DetailPrint "Downloading PyYAML"
+  NSISdl::download http://pyyaml.org/download/pyyaml/PyYAML-3.09.win32-py2.7.exe $TEMP\pyaml_setup.exe
+  Pop $R0 #Get the return value
+    StrCmp $R0 "success" +3
+      MessageBox MB_OK "Failed to download PyYAML installer: $R0"
+      Quit
+
+  DetailPrint "Installing PyYAML"
+  ExecWait "$TEMP\pyaml_setup.exe"
+
+  DetailPrint "Deleting PyYAML installer"
+  Delete "$TEMP\PyYAML_setup.exe"
+SectionEnd
+
+#----------- OPEN AL --------------
+Section "OpenAL (required)" OpenAL
+  SetDetailsPrint textonly
+  NSISdl::download http://connect.creativelabs.com/openal/Downloads/oalinst.zip $TEMP\oalinst.zip
+  
+  DetailPrint "Extracting OpenAL archive"
+  !insertmacro ZIPDLL_EXTRACT $TEMP\oalinst.zip $TEMP oalinst.exe
+
+  DetailPrint "Installing OpenAL"
+  ExecWait "$TEMP\oalinst.exe"
+
+  DetailPrint "Deleting OpenAL installer"
+  Delete "$TEMP\oalinst.exe"
+  Delete "$TEMP\oalinst.zip"
+SectionEnd
+
+Section "FIFE (required)" FIFE
+    DetailPrint "Downloading FIFE installer"
+	Push "http://puzzle.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://mesh.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://aarnet.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://cdnetworks-us-1.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://ovh.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://ignum.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://tenet.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://jaist.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://garr.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push "http://cdnetworks-kr-2.dl.sourceforge.net/project/fife/active/packages/FIFE-0.3.2r2_installer_win32.exe"
+	Push 10
+	Push "$TEMP\FIFE-0.3.2r2_installer_win32.exe"
+	Call DownloadFromRandomMirror
+	Pop $0
+    
+	StrCmp $0 "cancel" 0 +3
+		MessageBox MB_OK "Download canceled"
+		Goto End
+	StrCmp $0 "success" 0 +3
+        DetailPrint "Installing FIFE"
+		ExecWait "$TEMP\FIFE-0.3.2r2_installer_win32.exe"
+        Goto End
+	MessageBox MB_OK "Error $0"
+	End:
+    DetailPrint "Deleting FIFE Installer"
+    Delete "$TEMP\FIFE-0.3.2r2_installer_win32.exe"
+SectionEnd
+
+#--------- SECTION END ------------
+SectionGroupEnd
+
+SectionGroup PARPG PARPG
+Section "PARPG Module" PARPG-module
+  SectionIn RO
+  DetailPrint "Installing PARPG python package"
+  SetOutPath "$PythonPath\lib\site-packages"
+  SetOverwrite try
+  FILE /r /x *svn* "${PARPG_DIR}\parpg" 
+  SetAutoClose true
+SectionEnd
+#------------ Main. Packages PARPG code --------------
+Section "PARPG Datafiles" PARPG-data
+  SectionIn RO
+  SetOverwrite try
+  
+  # get all the core PARPG files
+  SetOutPath "$INSTDIR\dialogue"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\dialogue\" 
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetOutPath "$INSTDIR\fonts"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\fonts\" 
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetOutPath "$INSTDIR\gui"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\gui\" 
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetOutPath "$INSTDIR\maps"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\maps\" 
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetOutPath "$INSTDIR\music"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\music\" 
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetOutPath "$INSTDIR\objects"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\objects\" 
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetOutPath "$INSTDIR\quests"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\quests\" 
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetOutPath "$INSTDIR\character_scripts"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\character_scripts\"
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetOutPath "$INSTDIR\license"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE /r /x *svn* "${PARPG_DIR}\license\"
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+
+  SetOutPath "$INSTDIR"
+  !insertmacro UNINSTALL.LOG_OPEN_INSTALL
+  FILE "${PARPG_DIR}\README"
+  FILE "${PARPG_DIR}\AUTHORS"
+  FILE "${PARPG_DIR}\run.py" 
+  FILE "${PARPG_DIR}\system.cfg"
+  
+  RENAME "README" "README.txt"
+  RENAME "AUTHORS" "AUTHORS.txt"
+  RENAME "run.py" "${EXEC_SCRIPT_NAME}"
+
+  !insertmacro UNINSTALL.LOG_CLOSE_INSTALL
+  SetAutoClose true
+SectionEnd
+# Tools not included as they aren't ready for distribution
+#Section -Tools
+  #SetOutPath "$INSTDIR\tools\map_editor"
+  #SetOverwrite try
+#SectionEnd
+
+Section "-Additional" -AdditionalIcons
+  SectionIn RO
+  #avoid shortcuts headaches on vista by doing everything in the all users start menu
+  SetShellVarContext all
+  SetOutPath $INSTDIR
+  WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}"
+  CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
+  CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Website.lnk" "$INSTDIR\${PRODUCT_NAME}.url"
+  CreateShortcut '$SMPROGRAMS\${PRODUCT_NAME}\uninstall.lnk' '${UNINST_EXE}'
+  SetOutPath "$INSTDIR" #this makes the following shortcut run in the installed directory
+  CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\PARPG.lnk" "$INSTDIR\${EXEC_SCRIPT_NAME}"
+SectionEnd
+
+Section "-Post" -Post
+  SectionIn RO
+  WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayName" "$(^Name)"
+  WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "UninstallString" "${UNINST_EXE}"
+  WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayIcon" "$INSTDIR\gui\icons\window_icon.ico"
+  WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
+  WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
+  WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
+SectionEnd
+SectionGroupEnd
+
+##===========================================================================
+## Settings
+##===========================================================================
+ 
+!define PARPG_StartIndex ${PARPG}
+!define PARPG_EndIndex   ${-Post}
+ 
+!define EXT_StartIndex ${Externals}
+!define EXT_EndIndex   ${Fife}
+
+Function .OnInit
+  !insertmacro UNINSTALL.LOG_PREPARE_INSTALL
+  !insertmacro SetSectionFlag ${PARPG} ${SF_RO}
+  StrCpy $PythonPath ""
+  StrCpy $PythonVer "Custom"
+  InitPluginsDir
+FunctionEnd
+
+## If user goes back to this page from 1st Directory page
+## we need to put the sections back to how they were before
+Var IfBack
+Function SelectFilesCheck
+ StrCmp $IfBack 1 0 NoCheck
+  Call ResetFiles
+ NoCheck:
+FunctionEnd
+
+Function IsExternalsSelected
+Push $R0
+Push $R1
+ 
+ StrCpy $R0 ${EXT_StartIndex}    # Group 2 start
+ 
+  Loop:
+   IntOp $R0 $R0 + 1
+   SectionGetFlags $R0 $R1			# Get section flags
+    IntOp $R1 $R1 & ${SF_SELECTED}
+    StrCmp $R1 ${SF_SELECTED} 0 +3		# If section is selected, done
+     StrCpy $R0 1
+     Goto Done
+    StrCmp $R0 ${EXT_EndIndex} 0 Loop
+ 
+ Done:
+Pop $R1
+Exch $R0
+FunctionEnd
+
+## Here we are selecting first sections to install
+## by unselecting all the others!
+Function SelectFilesExternals
+ 
+ # If user clicks Back now, we will know to reselect Group 2's sections for
+ # Components page
+ StrCpy $IfBack 1
+ 
+ # We need to save the state of the Group 2 Sections
+ # for the next InstFiles page
+Push $R0
+Push $R1
+ 
+ StrCpy $R0 ${PARPG_StartIndex} # Group 2 start
+ 
+  Loop:
+   IntOp $R0 $R0 + 1
+   SectionGetFlags $R0 $R1				    # Get section flags
+    WriteINIStr "$PLUGINSDIR\sections.ini" Sections $R0 $R1 # Save state
+    !insertmacro UnselectSection $R0			    # Then unselect it
+    StrCmp $R0 ${PARPG_EndIndex} 0 Loop
+ 
+ # Don't install prog 1?
+ Call IsExternalsSelected
+ Pop $R0
+ StrCmp $R0 1 +4
+  Pop $R1
+  Pop $R0
+  Abort
+ 
+Pop $R1
+Pop $R0
+FunctionEnd
+
+## Here we need to unselect all Group 1 sections
+## and then re-select those in Group 2 (that the user had selected on
+## Components page)
+Function SelectFilesPARPG
+Push $R0
+Push $R1
+ 
+ StrCpy $R0 ${EXT_StartIndex}    # Group 1 start
+ 
+  Loop:
+   IntOp $R0 $R0 + 1
+    !insertmacro UnselectSection $R0		# Unselect it
+    StrCmp $R0 ${EXT_EndIndex} 0 Loop
+ 
+ Call ResetFiles
+ 
+Pop $R1
+Pop $R0
+FunctionEnd
+
+## This will set all sections to how they were on the components page
+## originally
+Function ResetFiles
+Push $R0
+Push $R1
+ 
+ StrCpy $R0 ${PARPG_StartIndex}    # Group 2 start
+ 
+  Loop:
+   IntOp $R0 $R0 + 1
+   ReadINIStr "$R1" "$PLUGINSDIR\sections.ini" Sections $R0 # Get sec flags
+    SectionSetFlags $R0 $R1				  # Re-set flags for this sec
+    StrCmp $R0 ${PARPG_EndIndex} 0 Loop
+ 
+Pop $R1
+Pop $R0
+FunctionEnd
+
+## Here we are deleting the temp INI file at the end of installation
+Function DeleteSectionsINI
+ FlushINI "$PLUGINSDIR\Sections.ini"
+ Delete "$PLUGINSDIR\Sections.ini"
+FunctionEnd
+
+Function .onInstSuccess
+  !insertmacro UNINSTALL.LOG_UPDATE_INSTALL
+FunctionEnd
+
+Function un.onUninstSuccess
+  HideWindow
+  MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) was successfully removed from your computer."
+FunctionEnd
+
+Function un.onInit
+  MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove $(^Name) and all of its components?" IDYES +2
+  Abort
+FunctionEnd
+
+Section Uninstall
+  #avoid shortcuts headaches on vista by doing everything in the all users start menu
+  SetShellVarContext all
+  
+  # remove data files
+  !insertmacro UNINSTALL.NEW_UNINSTALL "$INSTDIR"
+  
+  Delete "$SMPROGRAMS\${PRODUCT_NAME}\Website.lnk"
+  Delete "$SMPROGRAMS\${PRODUCT_NAME}\PARPG.lnk"
+  Delete "$SMPROGRAMS\${PRODUCT_NAME}\uninstall.lnk"
+  RmDir "$SMPROGRAMS\${PRODUCT_NAME}"
+  
+  Delete "$INSTDIR\*.log"
+  Delete "$INSTDIR\system.cfg"
+  Delete "$INSTDIR\${PRODUCT_NAME}.url"
+  
+  RMDir "$INSTDIR"
+
+  # Remove shortcuts
+  RMDir /r "$SMPROGRAMS\PARPG"
+ 
+  # Remove Registry keys
+  DeleteRegKey ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}"
+  SetAutoClose true
+SectionEnd
+
+
+LangString DESC_PARPG ${LANG_ENGLISH} "PARPG - Techdemo 1 SVN r788"
+LangString DESC_Python ${LANG_ENGLISH} "ActivePython 2.6.4.8 - Required to run PARPG. Requires an active internet connection to install."
+LangString DESC_FIFE ${LANG_ENGLISH} "FIFE-0.3.2r2 - Required to run PARPG."
+LangString DESC_PyYAML ${LANG_ENGLISH} "PyYAML 3.09 - Required Python Module. Requires an active internet connection to install."
+LangString DESC_OpenAL ${LANG_ENGLISH} "OpenAL - Required for sound effects and music playback."
+
+!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
+  !insertmacro MUI_DESCRIPTION_TEXT ${PARPG} $(DESC_PARPG)
+  !insertmacro MUI_DESCRIPTION_TEXT ${Python} $(DESC_Python)
+  !insertmacro MUI_DESCRIPTION_TEXT ${FIFE} $(DESC_FIFE)
+  !insertmacro MUI_DESCRIPTION_TEXT ${PyYAML} $(DESC_PyYAML)
+  !insertmacro MUI_DESCRIPTION_TEXT ${OpenAL} $(DESC_OpenAL)
+!insertmacro MUI_FUNCTION_DESCRIPTION_END
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylintrc	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,309 @@
+# lint Python modules using external checkers.
+# 
+# This is the main checker controlling the other ones and the reports
+# generation. It is itself both a raw checker and an astng checker in order
+# to:
+# * handle message activation / deactivation at the module level
+# * handle some basic but necessary stats'data (number of classes, methods...)
+# 
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add <file or directory> to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Set the cache size for astng objects.
+cache-size=500
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable only checker(s) with the given id(s). This option conflicts with the
+# disable-checker option
+#enable-checker=
+
+# Enable all checker(s) except those with the given id(s). This option
+# conflicts with the enable-checker option
+#disable-checker=
+
+# Enable all messages in the listed categories (IRCWEF).
+#enable-msg-cat=
+
+# Disable all messages in the listed categories (IRCWEF).
+disable-msg-cat=I
+
+# Enable the message(s) with the given id(s).
+#enable-msg=
+
+# Disable the message(s) with the given id(s).
+disable-msg=W0704,W0105,W0142,R0901,R0913,W0221,W0613,R0903,R0902,R0201,R0904,R0914,R0912,W0511,R0915,R0911
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells wether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectivly contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (R0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (R0004).
+comment=no
+
+# Enable the report(s) with the given id(s).
+#enable-report=R0004
+
+# Disable the report(s) with the given id(s).
+disable-report=R0001,R0002,R0003,R0101,R0401,R0402,R0701,R0801
+
+
+# checks for :
+# * doc strings
+# * modules / classes / functions / methods / arguments / variables name
+# * number of arguments, local variables, branchs, returns and statements in
+# functions, methods
+# * required module attributes
+# * dangerous default values as arguments
+# * redefinition of function / method / class
+# * uses of the global statement
+# 
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-zA-Z0-9]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-zA-Z0-9]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=k,ex,Run,_,__init__,__getstate__,__setstate__,ID,__repr__,x,y,z,X,Y,Z,__str__,__getitem__
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+
+# try to find bugs in the code using type inference
+# 
+[TYPECHECK]
+
+# Tells wether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamicaly set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+# checks for
+# * unused variables / imports
+# * undefined variables
+# * redefinition of variable from builtins or from an outer scope
+# * use of variable before assigment
+# 
+[VARIABLES]
+
+# Tells wether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching names used for dummy variables (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+# checks for :
+# * methods without self as first argument
+# * overridden methods signature
+# * access only to existant members via self
+# * attributes not defined in the __init__ method
+# * supported interfaces implementation
+# * unreachable code
+# 
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+# checks for sign of poor/misdesign:
+# * number of methods, attributes, local variables...
+# * size, complexity of functions, methods
+# 
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+# checks for
+# * external modules dependencies
+# * relative / wildcard imports
+# * cyclic imports
+# * uses of deprecated modules
+# 
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report R0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report R0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report R0402 must
+# not be disabled)
+int-import-graph=
+
+
+# checks for :
+# * unauthorized constructions
+# * strict indentation
+# * line length
+# * use of <> instead of !=
+# 
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+
+# checks for:
+# * warning notes in the code like FIXME, XXX
+# * PEP 263: source code with non ascii character but no encoding declaration
+# 
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+# checks for similarities and duplicated code. This computation may be
+# memory / CPU intensive, so you should disable it if you experiments some
+# problems.
+# 
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/run_tests.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,40 @@
+#!/usr/bin/env python2
+
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+
+
+import sys, os, unittest
+
+#Check if config.py exists. Get 'fife_path' from config
+try:
+    import config
+    sys.path.append(config.fife_path)
+except:
+    pass
+
+def _jp(path):
+    return os.path.sep.join(path.split('/'))
+
+_paths = ('../../engine/swigwrappers/python', '../../engine/extensions','tests')
+test_suite = unittest.TestSuite()
+
+for p in _paths:
+    if p not in sys.path:
+        sys.path.append(_jp(p))
+
+for p in os.listdir("tests") :
+    if p[-3:] == ".py" :
+        test_suite.addTest(unittest.TestLoader().loadTestsFromName(p[:-3]))
+
+unittest.TextTestRunner(verbosity=2).run(test_suite)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,292 @@
+#!/usr/bin/env python2
+"""
+Cross-system setup script responsible for creating source and binary
+packages for Linux, Windows, and Mac
+"""
+import sys
+import os
+import string
+from distutils.core import setup, Command, Distribution as _Distribution
+from distutils.command import (install as distutils_install,
+    build as distutils_build)
+from distutils.util import subst_vars, convert_path
+
+distutils_install.SCHEME_KEYS += ('config',)
+
+# Update existing installation schemes with the new config key.
+distutils_install.INSTALL_SCHEMES['unix_prefix'].update(
+    {
+        'data'  : '$base/share/$dist_name',
+        'config': '$base/etc/$dist_name'
+    }
+)
+distutils_install.INSTALL_SCHEMES['unix_local'].update(
+    {
+        'data'  : '$base/local/share/$dist_name',
+        'config': '$base/local/etc/$dist_name'
+    }
+)
+distutils_install.INSTALL_SCHEMES['deb_system'].update(
+    {
+        'data'  : '$base/share/$dist_name',
+        'config': '$base/etc/$dist_name'
+    }
+)
+distutils_install.INSTALL_SCHEMES['unix_home'].update(
+    {
+        'data'  : '$base/share/$dist_name',
+        'config': '$base/etc/$dist_name'
+    }
+)
+distutils_install.INSTALL_SCHEMES['unix_user'].update(
+    {
+        'data'  : '$userbase/share/$dist_name',
+        'config': '$userbase/etc/$dist_name'
+    }
+)
+distutils_install.WINDOWS_SCHEME.update(
+    {
+        'config': '$base'
+    }
+)
+distutils_install.INSTALL_SCHEMES['nt_user'].update(
+    {
+        'config': '$userbase',
+    }
+)
+distutils_install.INSTALL_SCHEMES['mac'].update(
+    {
+        'config': '$base',
+    }
+)
+distutils_install.INSTALL_SCHEMES['mac_user'].update(
+    {
+        'config': '$userbase',
+    }
+)
+distutils_install.INSTALL_SCHEMES['os2'].update(
+    {
+        'config': '$base',
+    }
+)
+distutils_install.INSTALL_SCHEMES['os2_home'].update(
+    {
+        'config': '$userbase',
+    }
+)
+
+
+class build_templates(Command):
+    description = '"build" template files (copy to build directory, ' \
+                  'substituting build variables)'
+    user_options = [
+        ('build-dir=', 'd', 'directory to "build" (copy) to'),
+        ('force', 'f', 'forcibly build everything (ignore file timestamps)'),
+    ]
+    boolean_options = ['force']
+    
+    def initialize_options(self):
+        self.build_base = None
+        self.build_dir = None
+        self.force = None
+        self.config_vars = None
+        self.templates = None
+    
+    def finalize_options(self):
+        self.set_undefined_options(
+            'build',
+            ('build_base', 'build_base'),
+            ('force', 'force'),
+        )
+        self.build_dir = os.path.join(self.build_base, 'templates')
+        self.set_undefined_options('install', ('config_vars', 'config_vars'))
+        self.templates = self.distribution.templates
+    
+    def run(self):
+        if self.has_templates():
+            self.build_templates()
+    
+    def build_templates(self):
+        build_dir = self.build_dir
+        for template_path, outfile_rel_path in self.templates.items():
+            outfile_path = os.path.join(build_dir, outfile_rel_path)
+            self.copy_file(template_path, outfile_path, preserve_mode=False)
+            self.substitute_template(outfile_path)
+    
+    def substitute_template(self, file_path):
+        with file(file_path, 'r') as template_file:
+            template_content = template_file.read()
+        template = string.Template(template_content)
+        config_vars = self.config_vars
+        file_content = template.substitute(config_vars)
+        with file(file_path, 'w') as config_file:
+            config_file.write(file_content)
+    
+    def has_templates(self):
+        return self.distribution.has_templates()
+
+
+class install_config(Command):
+    description = 'install configuration files'
+    user_options = [
+        ('install-dir=', 'd', 'directory to install configuration files to'),
+        ('force', 'f', 'force installation (overwrite existing files)'),
+        ('skip-build', None, 'skip the build steps'),
+    ]
+    boolean_options = ['force', 'skip-build']
+    
+    def initialize_options(self):
+        self.install_dir = None
+        self.force = 0
+        self.skip_build = None
+        self.config_files = None
+        self.config_templates = None
+        self.config_vars = None
+    
+    def finalize_options(self):
+        self.set_undefined_options(
+            'install',
+            ('install_config', 'install_dir'),
+            ('force', 'force'),
+            ('skip_build', 'skip_build'),
+            ('config_vars', 'config_vars'),
+        )
+        self.config_files = self.distribution.config_files
+        self.config_templates = self.distribution.config_templates
+    
+    def run(self):
+        install_dir = self.install_dir
+        outfiles = []
+        for file_path in self.config_files:
+            output_file_path = os.path.join(install_dir, file_path)
+            output_dir_path = os.path.dirname(output_file_path)
+            self.mkpath(output_dir_path)
+            outfile = self.copy_file(file_path, output_dir_path,
+                                     preserve_mode=0)
+            outfiles.append(outfile)
+        self.outfiles = outfiles
+    
+    def get_inputs(self):
+        return self.distribution.config_files or []
+    
+    def get_outputs(self):
+        return self.outfiles or []
+
+
+class install(distutils_install.install):
+    user_options = distutils_install.install.user_options + [
+        ('install-config=', None,
+         'installation directory for system-wide configuration files')
+    ]
+    
+    def has_config(self):
+        return self.distribution.has_config()
+    
+    def has_templates(self):
+        return self.distribution.has_templates()
+
+    def initialize_options(self):
+        self.install_config = None
+        distutils_install.install.initialize_options(self)
+    
+    def finalize_options(self):
+        distutils_install.install.finalize_options(self)
+        # FIXME M. George Hansen 2011-05-11: Probably shouldn't be using a
+        #     private method here...
+        self._expand_attrs(['install_config'])
+        self.convert_paths('config')
+        self.config_vars['config'] = self.install_config
+        if self.root is not None:
+            self.change_roots('config')
+        install_scheme = self.install_scheme
+        for key, value in install_scheme.items():
+            if os.name in ['posix', 'nt']:
+                value = os.path.expanduser(value)
+            value = subst_vars(value, self.config_vars)
+            value = convert_path(value)
+            self.config_vars[key] = value
+    
+    def select_scheme(self, name):
+        # Store the selected scheme for later processing.
+        distutils_install.install.select_scheme(self, name)
+        self.install_scheme = distutils_install.INSTALL_SCHEMES[name]
+
+    def get_program_files_path(self):
+        assert sys.platform == 'win32'
+        try:
+            program_files_path = os.environ['ProgramFiles']
+        except KeyError:
+            # ProgramFiles environmental variable isn't set, so we'll have to
+            # find it ourselves. Bit of a hack, but better than nothing.
+            program_files_path = ''
+            try:
+                import win32api
+            except ImportError:                
+                  program_files_path = ''
+            else:
+                drive_names = \
+                    [drive_name for drive_name in
+                     win32api.GetLogicalDriveStrings().split('\000') if
+                     drive_name]
+                for drive_name in drive_names:
+                    search_path = os.path.join(drive_name, 'Program Files')
+                    if os.path.isdir(search_path):
+                        program_files_path = search_path
+            if not program_files_path:
+                error_message = 'unable to determine path to Program Files'
+                raise error_message
+        
+        return program_files_path
+    
+    sub_commands = distutils_install.install.sub_commands + [
+        ('install_config', has_config),
+    ]
+
+
+class build(distutils_build.build):
+    def has_templates(self):
+        return self.distribution.has_templates()
+    
+    sub_commands = [('build_templates', has_templates)] + \
+        distutils_build.build.sub_commands
+
+
+class Distribution(_Distribution):
+    def __init__(self, attrs=None):
+        self.templates = {}
+        self.config_files = []
+        _Distribution.__init__(self, attrs)
+    
+    def has_config(self):
+        return self.config_files and len(self.config_files) > 0
+
+    def has_templates(self):
+        return self.templates and len(self.templates) > 0
+
+
+setup(
+    name='parpg',
+    scripts=['parpg'],
+    templates={'system.cfg': 'system.cfg.in', 'parpg': 'parpg.in'},
+    config_files=['system.cfg'],
+    packages=['parpg'],
+    modules=['src/main.py'],
+    package_dir={'parpg': 'src/parpg'},
+    version='0.2.0',
+    url='http://www.parpg.net',
+    description='',
+    long_description='',
+    cmdclass={'install': install, 'install_config': install_config,
+              'build': build, 'build_templates': build_templates},
+    distclass=Distribution,
+    classifiers=[
+        'Programming Language :: Python',
+        'License :: OSI Approved :: GNU General Public License (GPL)',
+        'Intended Audience :: End Users/Desktop',
+        'Intended Audience :: Developers',
+        'Development Status :: 2 - Pre-Alpha',
+        'Operating System :: OS Independent',
+        'Natural Language :: English',
+        'Topic :: Games/Entertainment :: Role-Playing',
+    ],
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/__init__.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,31 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+COPYRIGHT_HEADER = """\
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/application.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,230 @@
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+"""This module contains the main Application class 
+and the basic Listener for PARPG """
+
+import os
+import sys
+
+from fife import fife
+from fife.extensions import pychan
+from fife.extensions.serializers.xmlanimation import XMLAnimationLoader
+from fife.extensions.basicapplication import ApplicationBase
+
+from parpg import console
+from parpg.font import PARPGFont
+from parpg.gamemodel import GameModel
+from parpg.mainmenuview import MainMenuView
+from parpg.mainmenucontroller import MainMenuController
+from parpg.common.listeners.event_listener import EventListener
+from parpg.common.listeners.key_listener import KeyListener
+from parpg.common.listeners.mouse_listener import MouseListener
+from parpg.common.listeners.command_listener import CommandListener
+from parpg.common.listeners.console_executor import ConsoleExecuter
+from parpg.common.listeners.widget_listener import WidgetListener
+
+class KeyFilter(fife.IKeyFilter):
+    """
+    This is the implementation of the fife.IKeyFilter class.
+    
+    Prevents any filtered keys from being consumed by guichan.
+    """
+    def __init__(self, keys):
+        fife.IKeyFilter.__init__(self)
+        self._keys = keys
+
+    def isFiltered(self, event):
+        """Checks if an key is filtered"""
+        return event.getKey().getValue() in self._keys
+
+class ApplicationListener(KeyListener,
+                        MouseListener,
+                        ConsoleExecuter,
+                        CommandListener,
+                        WidgetListener):    
+    """Basic listener for PARPG"""
+        
+    def __init__(self, event_listener, engine, view, model):
+        """Initialize the instance.
+           @type engine: fife.engine
+           @param engine: ???
+           @type view: viewbase.ViewBase
+           @param view: View that draws the current state
+           @type model: GameModel
+           @param model: The game model"""
+
+        KeyListener.__init__(self, event_listener)
+        MouseListener.__init__(self, event_listener)
+        ConsoleExecuter.__init__(self, event_listener)
+        CommandListener.__init__(self, event_listener)
+        WidgetListener.__init__(self, event_listener)        
+        self.engine = engine
+        self.view = view
+        self.model = model
+        keyfilter = KeyFilter([fife.Key.ESCAPE])
+        keyfilter.__disown__()        
+        
+        engine.getEventManager().setKeyFilter(keyfilter)
+        self.quit = False
+        self.about_window = None
+        self.console = console.Console(self)
+
+    def quitGame(self):
+        """Forces a quit game on next cycle.
+           @return: None"""
+        self.quit = True
+
+    def onConsoleCommand(self, command):
+        """
+        Called on every console comand, delegates calls  to the a console
+        object, implementing the callbacks
+        @type command: string
+        @param command: the command to run
+        @return: result
+        """
+        return self.console.handleConsoleCommand(command)
+
+    def onCommand(self, command):
+        """Enables the game to be closed via the 'X' button on the window frame
+           @type command: fife.Command
+           @param command: The command to read.
+           @return: None"""
+        if(command.getCommandType() == fife.CMD_QUIT_GAME):
+            self.quit = True
+            command.consume()
+
+class PARPGApplication(ApplicationBase):
+    """Main Application class
+       We use an MVC model model
+       self.gamesceneview is our view,self.model is our model
+       self.controller is the controller"""
+       
+    def __init__(self, setting):
+        """Initialise the instance.
+           @return: None"""
+        self._setting = setting
+        self.engine = fife.Engine()
+        self.loadSettings()
+        self.engine.init()
+        self._animationloader = XMLAnimationLoader(self.engine.getImagePool(),
+                                                   self.engine.getVFS())
+        self.engine.getAnimationPool().addResourceLoader(self._animationloader)
+
+        pychan.init(self.engine, debug = True)
+        pychan.setupModalExecution(self.mainLoop,self.breakFromMainLoop)
+
+        self.quitRequested = False
+        self.breakRequested = False
+        self.returnValues = []
+        #self.engine.getModel(self)
+        self.model = GameModel(self.engine, setting)
+        self.model.readMapFiles()
+        self.model.readObjectDB()
+        self.model.getAgentImportFiles()
+        self.model.readAllAgents()
+        self.model.getDialogues()
+        self.view = MainMenuView(self.engine, self.model)
+        self.loadFonts()
+        self.event_listener = EventListener(self.engine)
+        self.controllers = []
+        controller = MainMenuController(self.engine, self.view, self.model, 
+                                        self)
+        #controller.initHud()
+        self.controllers.append(controller)
+        self.listener = ApplicationListener(self.event_listener,
+                                            self.engine, 
+                                            self.view, 
+                                            self.model)
+        #start_map = self._setting.fife.get("PARPG", "Map")
+        #self.model.changeMap(start_map)
+
+    def loadFonts(self):
+        # add the fonts path to the system path to import font definitons
+        sys.path.insert(0, os.path.join(self._setting.system_path, 
+                                        self._setting.fife.FontsPath))
+        from oldtypewriter import fontdefs
+
+        for fontdef in fontdefs:
+            pychan.internal.get_manager().addFont(PARPGFont(fontdef,
+                                                            self._setting))
+                
+
+    def loadSettings(self):
+        """
+        Load the settings from a python file and load them into the engine.
+        Called in the ApplicationBase constructor.
+        """
+
+        engineSetting = self.engine.getSettings()
+        engineSetting.setDefaultFontGlyphs(self._setting.fife.FontGlyphs)
+        engineSetting.setDefaultFontPath(os.path.join(self._setting.system_path,
+                                                   self._setting.fife.FontsPath,
+                                                   self._setting.fife.Font))
+        engineSetting.setDefaultFontSize(self._setting.fife.DefaultFontSize)
+        engineSetting.setBitsPerPixel(self._setting.fife.BitsPerPixel)
+        engineSetting.setInitialVolume(self._setting.fife.InitialVolume)
+        engineSetting.setSDLRemoveFakeAlpha(self._setting.fife.SDLRemoveFakeAlpha)
+        engineSetting.setScreenWidth(self._setting.fife.ScreenWidth)
+        engineSetting.setScreenHeight(self._setting.fife.ScreenHeight)
+        engineSetting.setRenderBackend(self._setting.fife.RenderBackend)
+        engineSetting.setFullScreen(self._setting.fife.FullScreen)
+        engineSetting.setVideoDriver(self._setting.fife.VideoDriver)
+        engineSetting.setLightingModel(self._setting.fife.Lighting)
+        engineSetting.setColorKeyEnabled(self._setting.fife.ColorKeyEnabled)
+
+        engineSetting.setColorKey(*[int(digit) 
+                                    for digit in self._setting.fife.ColorKey])
+
+        engineSetting.setWindowTitle(self._setting.fife.WindowTitle)
+        engineSetting.setWindowIcon(os.path.join(self._setting.system_path, 
+                                                 self._setting.fife.IconsPath,
+                                                 self._setting.fife.WindowIcon))
+
+    def createListener(self):
+        """ __init__ takes care of creating an event listener, so
+            basicapplication's createListener is harmful. Without 
+            overriding it, the program quit's on esc press, rather than
+            invoking the main menu
+        """
+        pass
+
+    def pushController(self, controller):
+        """Adds a controller to the list to be the current active one."""
+        self.controllers[-1].pause(True)
+        self.controllers.append(controller)
+    
+    def popController(self):
+        """Removes and returns the current active controller, unless its the last one"""
+        ret_controller = None
+        if self.controllers.count > 1:
+            ret_controller = self.controllers.pop()
+            self.controllers[-1].pause(False)
+        ret_controller.onStop()
+        return ret_controller
+    
+    def switchController(self, controller):
+        """Clears the controller list and adds a controller to be the current active one"""
+        for old_controller in self.controllers:
+            old_controller.onStop()
+        self.controllers = []
+        self.controllers.append(controller)
+    
+    def _pump(self):
+        """Main game loop.
+           There are in fact 2 main loops, this one and the one in GameSceneView.
+           @return: None"""
+        if self.listener.quit:
+            self.breakRequested = True #pylint: disable-msg=C0103
+        else:
+            for controller in self.controllers:
+                controller.pump()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/charactercreationcontroller.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,394 @@
+#   This file is part of PARPG.
+#
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""Provides the controller that defines the behaviour of the character creation
+   screen."""
+
+import characterstatistics as char_stats
+from serializers import XmlSerializer
+from controllerbase import ControllerBase
+from gamescenecontroller import GameSceneController
+from gamesceneview import GameSceneView
+from parpg.inventory import Inventory
+
+DEFAULT_STAT_VALUE = 50
+
+
+def getStatCost(offset):
+    """Gets and returns the cost to increase stat based on the offset"""
+    if offset < 0:
+        offset *= -1
+    if offset < 22:
+        return 1
+    elif offset < 29:
+        return 2
+    elif offset < 32:
+        return 3
+    elif offset < 35:
+        return 4
+    elif offset < 36:
+        return 5
+    elif offset < 38:
+        return 6
+    elif offset < 39:
+        return 7
+    elif offset < 40:
+        return 8
+    elif offset < 41:
+        return 9
+    else:
+        return 10    
+
+#TODO: Should be replaced with the real character class once its possible
+class SimpleCharacter(object):
+    """This is a simple class that is used to store the data during the
+    character creation"""
+    
+    def __init__(self, name, gender, origin, age, picture, traits,
+                 primary_stats, secondary_stats, inventory):
+        self.name = name
+        self.gender = gender
+        self.origin = origin
+        self.age = age
+        self.picture = picture
+        self.traits = traits
+        self.statistics = {}
+        for primary_stat in primary_stats:
+            short_name = primary_stat.short_name
+            self.statistics[short_name] = char_stats.PrimaryStatisticValue(
+                                                    primary_stat,
+                                                    self,
+                                                    DEFAULT_STAT_VALUE)
+            long_name = primary_stat.long_name
+            self.statistics[long_name] = char_stats.PrimaryStatisticValue(
+                                                    primary_stat,
+                                                    self,
+                                                    DEFAULT_STAT_VALUE)
+        for secondary_stat in secondary_stats:
+            name = secondary_stat.name            
+            self.statistics[name] = char_stats.SecondaryStatisticValue(
+                                                    secondary_stat,
+                                                    self)
+        self.inventory = inventory
+            
+class CharacterCreationController(ControllerBase):
+    """Controller defining the behaviour of the character creation screen."""
+    
+    #TODO: Change to actual values
+    MAX_TRAITS = 3
+    MIN_AGE = 16
+    MAX_AGE = 40
+    ORIGINS = {"None": None,}
+    GENDERS = ["Male", "Female",]
+    PICTURES = {"Male": ["None",], "Female": ["None",],}
+    TRAITS = {}
+    def __init__(self, engine, view, model, application):
+        """Construct a new L{CharacterCreationController} instance.
+           @param engine: Rendering engine used to display the associated view.
+           @type engine: L{fife.Engine}
+           @param view: View used to display the character creation screen.
+           @type view: L{ViewBase}
+           @param model: Model of the game state.
+           @type model: L{GameModel}
+           @param application: Application used to glue the various MVC
+               components together.
+           @type application: 
+               L{fife.extensions.basicapplication.ApplicationBase}"""
+        ControllerBase.__init__(self, engine, view, model, application)
+        self.view.start_new_game_callback = self.startNewGame
+        self.view.cancel_new_game_callback = self.cancelNewGame
+        self.view.show()
+        #TODO: Maybe this should not be hardcoded
+        stream = file("character_scripts/primary_stats.xml")        
+        prim_stats = XmlSerializer.deserialize(stream)
+        stream = file("character_scripts/secondary_stats.xml")        
+        sec_stats = XmlSerializer.deserialize(stream)
+        self.char_data = SimpleCharacter("",
+                                              self.GENDERS[0],
+                                              self.ORIGINS.keys()[0],
+                                              20,
+                                              self.PICTURES[self.GENDERS[0]][0],
+                                              [],
+                                              prim_stats,
+                                              sec_stats,
+                                              Inventory())
+        self._stat_points = 200
+  
+       
+    def startNewGame(self):
+        """Create the new character and start a new game.
+           @return: None"""
+        view = GameSceneView(self.engine, self.model)
+        controller = GameSceneController(self.engine, view, self.model,
+                                         self.application)
+        self.application.view = view
+        self.application.switchController(controller)
+        start_map = self.model.settings.parpg.Map
+        self.model.changeMap(start_map)
+    
+    def cancelNewGame(self):
+        """Exit the character creation view and return the to main menu.
+           @return: None"""
+        # KLUDGE Technomage 2010-12-24: This is to prevent a circular import
+        #     but a better fix needs to be thought up.
+        from mainmenucontroller import MainMenuController
+        from mainmenuview import MainMenuView
+        view = MainMenuView(self.engine, self.model)
+        controller = MainMenuController(self.engine, view, self.model,
+                                        self.application)
+        self.application.view = view
+        self.application.switchController(controller)
+    
+    def onStop(self):
+        """Called when the controller is removed from the list.
+           @return: None"""
+        self.view.hide()
+        
+    @property
+    def name(self):
+        """Returns the name of the character.
+        @return: Name of the character"""
+        return self.char_data.name
+    
+    @property
+    def age(self):
+        """Returns the age of the character.
+        @return: Age of the character"""
+        return self.char_data.age
+    
+    @property
+    def gender(self):
+        """Returns the gender of the character.
+        @return: Gender of the character"""
+        return self.char_data.gender
+    
+    @property
+    def origin(self):
+        """Returns the origin of the character.
+        @return: Origin of the character"""
+        return self.char_data.origin
+    
+    @property
+    def picture(self):
+        """Returns the ID of the current picture of the character."""
+        return self.char_data.picture
+    
+    def getStatPoints(self):
+        """Returns the remaining statistic points that can be distributed"""
+        return self._stat_points
+    
+    def increaseStatistic(self, statistic):
+        """Increases the given statistic by one.
+        @param statistic: Name of the statistic to increase
+        @type statistic: string""" 
+        if self.canIncreaseStatistic(statistic):
+            cost = self.getStatisticIncreaseCost(statistic)
+            if  cost <= self._stat_points:
+                self.char_data.statistics[statistic].value += 1
+                self._stat_points -= cost  
+
+    def getStatisticIncreaseCost(self, statistic):
+        """Calculate and return the cost to increase the statistic
+        @param statistic: Name of the statistic to increase
+        @type statistic: string
+        @return cost to increase the statistic"""
+        cur_value = self.char_data.statistics[statistic].value
+        new_value = cur_value + 1
+        offset =  new_value - DEFAULT_STAT_VALUE
+        return getStatCost(offset)
+    
+    def canIncreaseStatistic(self, statistic):
+        """Checks whether the given statistic can be increased or not.
+        @param statistic: Name of the statistic to check
+        @type statistic: string
+        @return: True if the statistic can be increased, False if not."""
+        stat = self.char_data.statistics[statistic].value
+        return stat < stat.statistic_type.maximum
+    
+    def decreaseStatistic(self, statistic):
+        """Decreases the given statistic by one.
+        @param statistic: Name of the statistic to decrease
+        @type statistic: string""" 
+        if self.canDecreaseStatistic(statistic):
+            gain = self.getStatisticDecreaseGain(statistic)
+            self.char_data.statistics[statistic].value -= 1
+            self._stat_points += gain  
+
+    def getStatisticDecreaseGain(self, statistic):
+        """Calculate and return the gain of decreasing the statistic
+        @param statistic: Name of the statistic to decrease
+        @type statistic: string
+        @return cost to decrease the statistic"""
+        cur_value = self.char_data.statistics[statistic].value
+        new_value = cur_value - 1
+        offset =  new_value - DEFAULT_STAT_VALUE
+        return getStatCost(offset)
+    
+    def canDecreaseStatistic(self, statistic):
+        """Checks whether the given statistic can be decreased or not.
+        @param statistic: Name of the statistic to check
+        @type statistic: string
+        @return: True if the statistic can be decreased, False if not."""
+        stat = self.char_data.statistics[statistic].value
+        return stat > stat.statistic_type.minimum
+    
+    def getStatisticValue(self, statistic):
+        """Returns the value of the given statistic.
+        @param statistic: Name of the primary or secondary statistic
+        @type statistic: string
+        @return: Value of the given statistic"""
+        return self.char_data.statistics[statistic]
+    
+    def areAllStatisticsValid(self):
+        """Checks if all statistics are inside the minimum/maximum values
+        @return True if all statistics are valid False if not"""
+        for stat in self.char_data.statistics.items():
+            if not (stat.value > stat.statistic_type.minumum and\
+                stat.value < stat.statistic_type.maximum):
+                return False
+        return True
+    
+    def setName(self, name):
+        """Sets the name of the character to the given value.
+        @param name: New name
+        @type name: string"""
+        self.char_data.name = name
+    
+    def isNameValid(self, name):
+        """Checks whether the name is valid.
+        @param name: Name to check
+        @type name: string
+        @return: True if the name is valid, False if not"""
+        if name:
+            return True
+        return False
+    
+    def changeOrigin(self, origin):
+        """Changes the origin of the character to the given value.
+        @param origin: New origin
+        @type origin: string"""
+        if self.isOriginValid(origin):
+            self.char_data.origin = origin
+            #TODO: Make changes according to origin
+   
+    def isOriginValid(self, origin):
+        """Checks whether the origin is valid.
+        @param origin: Origin to check
+        @type origin: string
+        @return: True if the origin is valid, False if not"""
+        return origin in self.ORIGINS
+    
+    def changeGender(self, gender):
+        """Changes the gender of the character to the given value.
+        @param gender: New gender
+        @param gender: string"""
+        if self.isGenderValid(gender):
+            self.char_data.gender = gender
+    
+    def isGenderValid(self, gender):
+        """Checks whether the gender is valid.
+        @param gender: Gender to check
+        @type gender: string?
+        @return: True if the origin is valid, False if not"""        
+        return gender in self.GENDERS
+    
+    def changeAge(self, age):
+        """Sets the age of the character to the given value.
+        @param age: New age
+        @type age: integer
+        """
+        if self.isAgeValid(age):
+            self.char_data.age = age
+    
+    def isAgeValid(self, age):
+        """Checks whether the age is valid.
+        @param age: Age to check
+        @type age: integer
+        @return: True if the origin is valid, False if not"""
+        return age >= self.MIN_AGE and age <= self.MAX_AGE 
+    
+    def setPicture(self, picture):
+        """Set picture of the character.
+        @param picture: ID of the new picture
+        @type picture: string"""
+        if self.isPictureValid(picture):
+            self.char_data.picture = picture
+    
+    def isPictureValid(self, picture):
+        """Checks whether the picture is valid.
+        @param picture: ID of the picture to check
+        @type picture: string
+        @return: True if the picture is valid, False if not"""
+        return picture in self.PICTURES[self.gender]
+    
+    def addTrait(self, trait):
+        """Adds a trait to the character.
+        @param trait: ID of the trait to add
+        @type trait: string"""
+        if self.canAddAnotherTrait() and self.isTraitValid(trait)\
+                and not self.hasTrait(trait):
+            self.char_data.traits.append(trait)                
+    
+    def canAddAnotherTrait(self):
+        """Checks whether another trait can be added.
+        @return: True if another trait can be added, False if not"""
+        return len(self.char_data.traits) < self.MAX_TRAITS
+    
+    def removeTrait(self, trait):
+        """Remove trait from character.
+        @param trait: ID of the trait to remove
+        @type trait: string"""
+        if self.hasTrait(trait):
+            self.char_data.traits.remove(trait)
+    
+    def hasTrait(self, trait):
+        """Checks whether the character has the trait.
+        @param trait: ID of the trait to check
+        @type trait: string
+        @return: True if the character has the trait, False if not"""
+        return trait in self.char_data.traits
+    
+    def isTraitValid(self, trait):
+        """Checks whether the trait is valid.
+        @param trait: ID of the trait to check
+        @type trait: string
+        @return: True if the trait is valid, False if not"""
+        return trait in self.TRAITS
+    
+    def areCurrentTraitsValid(self):
+        """Checks whether the characters traits are valid.
+        @return: True if the traits are valid, False if not"""
+        if len(self.char_data.traits) > self.MAX_TRAITS:
+            return False 
+        for trait in self.char_data.traits:
+            if not self.isTraitValid(trait):
+                return False
+        return True
+    
+    def isCharacterValid(self):
+        """Checks whether the character as a whole is valid.
+        @return: True if the character is valid, False if not"""
+        #Single checks can be disabled by putting a "#" in front of them
+        if True\
+        and self._stat_points >= 0\
+        and self.areAllStatisticsValid() \
+        and self.areCurrentTraitsValid() \
+        and self.isNameValid(self.name)\
+        and self.isPictureValid(self.picture)\
+        and self.isAgeValid(self.age)\
+        and self.isGenderValid(self.gender)\
+        and self.isOriginValid(self.origin)\
+        :
+            return True
+        return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/charactercreationview.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,80 @@
+#   This file is part of PARPG.
+#
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""Provides the view for displaying the character creation screen."""
+
+import os
+
+from fife.extensions import pychan
+
+from viewbase import ViewBase
+
+class CharacterCreationView(ViewBase):
+    """View used to display the character creation screen.
+       @ivar background: Widget displayed as the background.
+       @type background: L{pychan.Widget}
+       @ivar start_new_game_callback: Callback attached to the startButton.
+       @type start_new_game_callback: callable
+       @ivar cancel_new_game_callback: Callback attached to the cancelButton.
+       @type cancel_new_game_callback: callable
+       @ivar character_screen: Widget used to display the character creation
+           screen.
+       @type character_screen: L{pychan.Widget}"""
+
+    def __init__(self, engine, model, settings):
+        """Construct a new L{CharacterCreationView} instance.
+           @param engine: Rendering engine used to display the view.
+           @type engine: L{fife.Engine}
+           @param model: Model of the game state.
+           @type model: L{GameState}"""
+        ViewBase.__init__(self, engine, model)
+        self.settings = settings
+        gui_path = os.path.join(self.settings.system_path,
+                                self.settings.parpg.GuiPath)
+        self.background = pychan.loadXML(os.path.join(gui_path, 
+                                                    'main_menu_background.xml'))
+        screen_mode = self.engine.getRenderBackend().getCurrentScreenMode()
+        self.background.width = screen_mode.getWidth()
+        self.background.height = screen_mode.getHeight()
+        self.start_new_game_callback = None
+        self.cancel_new_game_callback = None
+        self.character_screen = pychan.loadXML(os.path.join(gui_path,
+                                                       'character_screen.xml'))
+        self.character_screen.adaptLayout()
+        character_screen_events = {}
+        character_screen_events['startButton'] = self.startNewGame
+        character_screen_events['cancelButton'] = self.cancelNewGame
+        self.character_screen.mapEvents(character_screen_events)
+    
+    def show(self):
+        """Display the view.
+           @return: None"""
+        self.background.show()
+        self.character_screen.show()
+    
+    def hide(self):
+        """Hide the view.
+           @return: None"""
+        self.background.hide()
+        self.character_screen.hide()
+    
+    def startNewGame(self):
+        """Callback tied to the startButton.
+           @return: None"""
+        self.start_new_game_callback()
+    
+    def cancelNewGame(self):
+        """Callback tied to the cancelButton.
+           @return: None"""
+        self.cancel_new_game_callback()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/characterstatistics.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,165 @@
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+"""
+Provides classes that define character stats and traits.
+"""
+
+from abc import ABCMeta, abstractmethod
+from weakref import ref as weakref
+
+from .serializers import SerializableRegistry
+
+class AbstractCharacterStatistic(object):
+    __metaclass__ = ABCMeta
+    
+    @abstractmethod
+    def __init__(self, description, minimum, maximum):
+        self.description = description
+        self.minimum = minimum
+        self.maximum = maximum
+
+
+class PrimaryCharacterStatistic(AbstractCharacterStatistic):
+    def __init__(self, long_name, short_name, description, minimum=0,
+                 maximum=100):
+        AbstractCharacterStatistic.__init__(self, description=description,
+                                            minimum=minimum, maximum=maximum)
+        self.long_name = long_name
+        self.short_name = short_name
+
+SerializableRegistry.registerClass(
+    'PrimaryCharacterStatistic',
+    PrimaryCharacterStatistic,
+    init_args=[
+        ('long_name', unicode),
+        ('short_name', unicode),
+        ('description', unicode),
+        ('minimum', int),
+        ('maximum', int),
+    ],
+)
+
+
+class SecondaryCharacterStatistic(AbstractCharacterStatistic):
+    def __init__(self, name, description, unit, mean, sd, stat_modifiers,
+                 minimum=None, maximum=None):
+        AbstractCharacterStatistic.__init__(self, description=description,
+                                            minimum=minimum, maximum=maximum)
+        self.name = name
+        self.unit = unit
+        self.mean = mean
+        self.sd = sd
+        self.stat_modifiers = stat_modifiers
+
+SerializableRegistry.registerClass(
+    'SecondaryCharacterStatistic',
+    SecondaryCharacterStatistic,
+    init_args=[
+        ('name', unicode),
+        ('description', unicode),
+        ('unit', unicode),
+        ('mean', float),
+        ('sd', float),
+        ('stat_modifiers', dict),
+        ('minimum', float),
+        ('maximum', float),
+    ],
+)
+
+
+class AbstractStatisticValue(object):
+    __metaclass__ = ABCMeta
+    
+    @abstractmethod
+    def __init__(self, statistic_type, character):
+        self.statistic_type = statistic_type
+        self.character = weakref(character)
+
+
+class PrimaryStatisticValue(AbstractStatisticValue):
+    def value():
+        def fget(self):
+            return self._value
+        def fset(self, new_value):
+            assert 0 <= new_value <= 100
+            self._value = new_value
+    
+    def __init__(self, statistic_type, character, value):
+        AbstractStatisticValue.__init__(self, statistic_type=statistic_type,
+                                        character=character)
+        self._value = None
+        self.value = value
+
+
+class SecondaryStatisticValue(AbstractStatisticValue):
+    def normalized_value():
+        def fget(self):
+            return self._normalized_value
+        def fset(self, new_value):
+            self._normalized_value = new_value
+            statistic_type = self.statistic_type
+            mean = statistic_type.mean
+            sd = statistic_type.sd
+            self._value = self.calculate_value(mean, sd, new_value)
+        return locals()
+    normalized_value = property(**normalized_value())
+    
+    def value():
+        def fget(self):
+            return self._value
+        def fset(self, new_value):
+            self._value = new_value
+            statistic_type = self.statistic_type
+            mean = statistic_type.mean
+            sd = statistic_type.sd
+            self._normalized_value = self.calculate_value(mean, sd, new_value)
+        return locals()
+    value = property(**value())
+    
+    def __init__(self, statistic_type, character):
+        AbstractStatisticValue.__init__(self, statistic_type=statistic_type,
+                                        character=character)
+        mean = statistic_type.mean
+        sd = statistic_type.sd
+        normalized_value = self.derive_value(normalized=True)
+        self._normalized_value = normalized_value
+        self._value = self.calculate_value(mean, sd, normalized_value)
+    
+    def derive_value(self, normalized=True):
+        """
+        Derive the current value 
+        """
+        statistic_type = self.statistic_type
+        stat_modifiers = statistic_type.stat_modifiers
+        character = self.character()
+        
+        value = sum(
+            character.statistics[name].value * modifier for name, modifier in
+                stat_modifiers.items()
+        )
+        assert 0 <= value <= 100
+        if not normalized:
+            mean = statistic_type.mean
+            sd = statistic_type.sd
+            value = self.calculate_value(mean, sd, value)
+        return value
+    
+    @staticmethod
+    def calculate_value(mean, sd, normalized_value):
+        value = sd * (normalized_value - 50) + mean
+        return value
+    
+    @staticmethod
+    def calculate_normalized_value(mean, sd, value):
+        normalized_value = ((value - mean) / sd) + 50
+        return normalized_value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/common/listeners/command_listener.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module contains the CommandListener class for receiving command events"""
+
+
+class CommandListener(object):
+    """Base class for listeners that receiving command events"""
+
+    def __init__(self, event_listener):
+        self.event_listener = None
+        CommandListener.attach(self, event_listener)
+    
+    def attach(self, event_listener):
+        """Attaches the listener to the event"""
+        event_listener.addListener("Command", self)
+        self.event_listener = event_listener
+        
+    def detach(self):
+        """Detaches the listener from the event"""
+        self.event_listener.removeListener("Command", self)
+        self.event_listener = None
+
+    def onCommand(self, command):
+        """Called when a command is executed"""
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/common/listeners/console_executor.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module contains the ConsoleExecuter class that receives 
+console events"""
+
+class ConsoleExecuter(object):
+    """This class is a base class for listeners receiving console events"""
+
+    def __init__(self, event_listener):
+        self.event_listener = None
+        ConsoleExecuter.attach(self, event_listener)
+    
+    def attach(self, event_listener):
+        """Attaches the listener to the event"""
+        event_listener.addListener("ConsoleCommand", self)
+        self.event_listener = event_listener
+        
+    def detach(self):
+        """Detaches the listener from the event"""
+        self.event_listener.removeListener("ConsoleCommand", self)
+        self.event_listener = None
+
+    def onToolsClick(self):
+        """Called when the tools button has been clicked"""
+        pass
+    
+    def onConsoleCommand(self, command):
+        """Called when a console command is executed"""
+        pass
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/common/listeners/event_listener.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module contains the EventListener that receives events and distributes 
+them to PARPG listeners"""
+
+from fife import fife
+import logging
+
+logger = logging.getLogger('event_listener')
+
+class EventListener(fife.IKeyListener, 
+                   fife.ICommandListener, 
+                   fife.IMouseListener, 
+                   fife.ConsoleExecuter):
+    """Class that receives all events and distributes them to the listeners"""
+    def __init__(self, engine):
+        """Initialize the instance"""
+        self.event_manager = engine.getEventManager()
+
+        fife.IKeyListener.__init__(self)
+        self.event_manager.addKeyListener(self)
+        fife.ICommandListener.__init__(self)
+        self.event_manager.addCommandListener(self)
+        fife.IMouseListener.__init__(self)
+        self.event_manager.addMouseListener(self)
+        fife.ConsoleExecuter.__init__(self)
+        engine.getGuiManager().getConsole().setConsoleExecuter(self)
+        
+        self.listeners = {"Mouse" : [],                          
+                          "Key" : [],
+                          "Command" : [],
+                          "ConsoleCommand" : [],
+                          "Widget" : []}               
+                
+    def addListener(self, listener_type, listener):
+        """Adds a listener"""
+        if listener_type in self.listeners.iterkeys():
+            if not listener in self.listeners[listener_type]:
+                self.listeners[listener_type].append(listener)            
+        else:
+            logger.warning("Listener type "
+                                  "'{0}' not supported".format(listener_type))
+    
+    def removeListener(self, listener_type, listener):
+        """Removes a listener"""
+        if listener_type in self.listeners.iterkeys():
+            self.listeners[listener_type].remove(listener)
+        else:
+            logger.warning("Listener type "
+                                  "'{0}' not supported".format(listener_type))
+            
+    def mousePressed(self, evt):
+        """Called when a mouse button is pressed"""
+        for listeners in self.listeners["Mouse"]:
+            listeners.mousePressed(evt)
+
+    def mouseReleased(self, evt):
+        """Called when a mouse button is released"""
+        for listeners in self.listeners["Mouse"]:
+            listeners.mouseReleased(evt)
+
+    def mouseEntered(self, evt):
+        """Called when a mouse enters a region"""
+        for listeners in self.listeners["Mouse"]:
+            listeners.mouseEntered(evt)
+
+    def mouseExited(self, evt):
+        """Called when a mouse exits a region"""
+        for listeners in self.listeners["Mouse"]:
+            listeners.mouseExited(evt)
+
+    def mouseClicked(self, evt):
+        """Called after a mouse button is pressed and released"""
+        for listeners in self.listeners["Mouse"]:
+            listeners.mouseClicked(evt)
+
+    def mouseWheelMovedUp(self, evt):
+        """Called when the mouse wheel has been moved up"""
+        for listeners in self.listeners["Mouse"]:
+            listeners.mouseWheelMovedUp(evt)
+
+    def mouseWheelMovedDown(self, evt):
+        """Called when the mouse wheel has been moved down"""
+        for listener in self.listeners["Mouse"]:
+            listener.mouseWheelMovedDown(evt)
+
+    def mouseMoved(self, evt):
+        """Called when when the mouse has been moved"""
+        for listener in self.listeners["Mouse"]:
+            listener.mouseMoved(evt)
+
+    def mouseDragged(self, evt):
+        """Called when dragging the mouse"""
+        for listener in self.listeners["Mouse"]:
+            listener.mouseDragged(evt)
+
+    def keyPressed(self, evt):
+        """Called when a key is being pressed"""
+        for listener in self.listeners["Key"]:
+            listener.keyPressed(evt)
+
+    def keyReleased(self, evt):
+        """Called when a key is being released"""
+        for listener in self.listeners["Key"]:
+            listener.keyReleased(evt)
+
+    def onCommand(self, command):
+        """Called when a command is executed"""
+        for listener in self.listeners["Command"]:
+            listener.onCommand(command)
+
+    def onToolsClick(self):
+        """Called when the tools button has been clicked"""
+        for listener in self.listeners["ConsoleCommand"]:
+            listener.onToolsClick()
+
+    def onConsoleCommand(self, command):
+        """Called when a console command is executed"""
+        for listener in self.listeners["ConsoleCommand"]:
+            listener.onConsoleCommand(command)
+
+    def onWidgetAction(self, evt):
+        """Called when a widget action is executed"""
+        for listener in self.listeners["Widget"]:
+            listener.onWidgetAction(evt)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/common/listeners/key_listener.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module contains the KeyListener class for receiving key inputs"""
+
+class KeyListener(object):
+    """Base class for listeners receiving keyboard input"""
+
+    def __init__(self, event_listener):
+        self.event_listener = None
+        KeyListener.attach(self, event_listener)
+    
+    def attach(self, event_listener):
+        """Attaches the listener to the event"""
+        event_listener.addListener("Key", self)
+        self.event_listener = event_listener
+        
+    def detach(self):
+        """Detaches the listener from the event"""
+        self.event_listener.removeListener("Key", self)
+        
+    def keyPressed(self, event):
+        """Called when a key is being pressed"""
+        pass
+    
+    def keyReleased(self, event):
+        """Called when a key is being released"""
+        pass
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/common/listeners/mouse_listener.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module contains the MouseListener class for receiving mouse inputs"""
+
+class MouseListener(object):
+    """Base class for listeners receiving mouse input"""
+    
+    def __init__(self, event_listener):
+        self.event_listener = None
+        MouseListener.attach(self, event_listener)
+    
+    def attach(self, event_listener):
+        """Attaches the listener to the event"""
+        event_listener.addListener("Mouse", self)
+        self.event_listener = event_listener
+        
+    def detach(self):
+        """Detaches the listener from the event"""
+        self.event_listener.removeListener("Mouse", self)
+        self.event_listener = None
+        
+    def mousePressed(self, evt):
+        """Called when a mouse button is pressed"""
+        pass
+
+    def mouseReleased(self, evt):
+        """Called when a mouse button is released"""
+        pass
+
+    def mouseEntered(self, evt):
+        """Called when a mouse enters a region"""
+        pass
+
+    def mouseExited(self, evt):
+        """Called when a mouse exits a region"""
+        pass
+
+    def mouseClicked(self, evt):
+        """Called after a mouse button is pressed and released"""
+        pass
+
+    def mouseWheelMovedUp(self, evt):
+        """Called when the mouse wheel has been moved up"""
+        pass
+
+    def mouseWheelMovedDown(self, evt):
+        """Called when the mouse wheel has been moved down"""
+        pass
+
+    def mouseMoved(self, evt):
+        """Called when when the mouse has been moved"""
+        pass
+
+    def mouseDragged(self, evt):
+        """Called when dragging the mouse"""
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/common/listeners/widget_listener.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""This module contains the WidgetListener class for receiving widget events"""
+
+class WidgetListener(object):
+    """A Base class for listeners receiving widget events"""
+    
+    def __init__(self, event_listener):
+        self.event_listener = None
+        WidgetListener.attach(self, event_listener)
+    
+    def attach(self, event_listener):
+        """Attaches the listener to the event"""
+        event_listener.addListener("Widget", self)
+        self.event_listener = event_listener
+        
+    def detach(self):
+        """Detaches the listener from the event"""
+        self.event_listener.removeListener("Widget", self)
+        self.event_listener = None
+
+    def onWidgetAction(self, evt):
+        """Called when a widget action is executed"""
+        pass
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/common/ordereddict.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,127 @@
+# Copyright (c) 2009 Raymond Hettinger
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+#     The above copyright notice and this permission notice shall be
+#     included in all copies or substantial portions of the Software.
+#
+#     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+#     EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+#     OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+#     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+#     HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+#     WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+#     OTHER DEALINGS IN THE SOFTWARE.
+
+from UserDict import DictMixin
+
+class OrderedDict(dict, DictMixin):
+
+    def __init__(self, *args, **kwds):
+        if len(args) > 1:
+            raise TypeError('expected at most 1 arguments, got %d' % len(args))
+        try:
+            self.__end
+        except AttributeError:
+            self.clear()
+        self.update(*args, **kwds)
+
+    def clear(self):
+        self.__end = end = []
+        end += [None, end, end]         # sentinel node for doubly linked list
+        self.__map = {}                 # key --> [key, prev, next]
+        dict.clear(self)
+
+    def __setitem__(self, key, value):
+        if key not in self:
+            end = self.__end
+            curr = end[1]
+            curr[2] = end[1] = self.__map[key] = [key, curr, end]
+        dict.__setitem__(self, key, value)
+
+    def __delitem__(self, key):
+        dict.__delitem__(self, key)
+        key, prev, next = self.__map.pop(key)
+        prev[2] = next
+        next[1] = prev
+
+    def __iter__(self):
+        end = self.__end
+        curr = end[2]
+        while curr is not end:
+            yield curr[0]
+            curr = curr[2]
+
+    def __reversed__(self):
+        end = self.__end
+        curr = end[1]
+        while curr is not end:
+            yield curr[0]
+            curr = curr[1]
+
+    def popitem(self, last=True):
+        if not self:
+            raise KeyError('dictionary is empty')
+        if last:
+            key = reversed(self).next()
+        else:
+            key = iter(self).next()
+        value = self.pop(key)
+        return key, value
+
+    def __reduce__(self):
+        items = [[k, self[k]] for k in self]
+        tmp = self.__map, self.__end
+        del self.__map, self.__end
+        inst_dict = vars(self).copy()
+        self.__map, self.__end = tmp
+        if inst_dict:
+            return (self.__class__, (items,), inst_dict)
+        return self.__class__, (items,)
+
+    def keys(self):
+        return list(self)
+
+    setdefault = DictMixin.setdefault
+    update = DictMixin.update
+    pop = DictMixin.pop
+    values = DictMixin.values
+    items = DictMixin.items
+    iterkeys = DictMixin.iterkeys
+    itervalues = DictMixin.itervalues
+    iteritems = DictMixin.iteritems
+
+    def __repr__(self):
+        if not self:
+            return '%s()' % (self.__class__.__name__,)
+        return '%s(%r)' % (self.__class__.__name__, self.items())
+
+    def copy(self):
+        return self.__class__(self)
+
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        d = cls()
+        for key in iterable:
+            d[key] = value
+        return d
+
+    def __eq__(self, other):
+        if isinstance(other, OrderedDict):
+            if len(self) != len(other):
+                return False
+            for p, q in  zip(self.items(), other.items()):
+                if p != q:
+                    return False
+            return True
+        return dict.__eq__(self, other)
+
+    def __ne__(self, other):
+        return not self == other
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/common/utils.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,61 @@
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+
+# Miscellaneous game functions
+
+import os, sys, fnmatch
+from textwrap import dedent
+
+def addPaths (*paths):
+    """Adds a list of paths to sys.path. Paths are expected to use forward
+       slashes, for example '../../engine/extensions'. Slashes are converted
+       to the OS-specific equivalent.
+       @type paths: ???
+       @param paths: Paths to files?
+       @return: None"""
+    for p in paths:
+        if not p in sys.path:
+            sys.path.append(os.path.sep.join(p.split('/')))
+
+def parseBool(value):
+    """Parses a string to get a boolean value"""
+    if (value.isdigit()):
+        return bool(int(value))
+    elif (value.isalpha):
+        return value.lower()[0] == "t"
+    return False
+
+def locateFiles(pattern, root=os.curdir):
+    """Locate all files matching supplied filename pattern in and below
+    supplied root directory."""
+    for path, _, files in os.walk(os.path.abspath(root)):
+        for filename in fnmatch.filter(files, pattern):
+            yield os.path.join(path, filename)
+
+def dedent_chomp(string):
+    """Remove common leading whitespace and chomp each non-blank line."""
+    dedented_string = dedent(string).strip()
+    lines = dedented_string.splitlines()
+    formatted_lines = []
+    for index in range(len(lines)):
+        line = lines[index]
+        if index == len(lines) - 1:
+            # Don't do anything to the last line.
+            pass
+        elif not line or line.isspace():
+            line = '\n\n'
+        else:
+            line = ''.join([line, ' '])
+        formatted_lines.append(line)
+    result = ''.join(formatted_lines)
+    return result
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/console.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,204 @@
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+
+import re
+import os
+import sys
+from StringIO import StringIO
+import code
+
+class Console:
+    def __init__(self, app_listener):
+        """
+        @type appListener: ApplicationListener
+        @param appListener: ApplicationListener object providing a link with
+        the Controller, the view and the GameModel"""
+        exit_help   = "Terminate application"
+        grid_help   = "Toggle grid display"
+        run_help    = "Toggle player run/walk"
+        help_help   = "Show this help string"
+        load_help   = "Usage: load directory file"
+        python_help = "Run some python code"
+        quit_help   = "Terminate application"
+        save_help   = "Usage: save directory file"
+        pause_help  = "Pause/Unpause the game"
+
+        self.commands = [
+            {"cmd":"exit"  ,"callback":self.handleQuit  ,"help": exit_help},
+            {"cmd":"grid"  ,"callback":self.handleGrid  ,"help": grid_help},
+            {"cmd":"help"  ,"callback":self.handleHelp  ,"help": help_help},
+            {"cmd":"load"  ,"callback":self.handleLoad  ,"help": load_help},
+            {"cmd":"pause" ,"callback":self.handlePause ,"help": pause_help},
+            {"cmd":"python","callback":self.handlePython,"help": python_help},
+            {"cmd":"run"   ,"callback":self.handleRun   ,"help": run_help},
+            {"cmd":"save"  ,"callback":self.handleSave  ,"help": save_help},
+            {"cmd":"quit"  ,"callback":self.handleQuit  ,"help": quit_help},
+        ]
+        self.app_listener = app_listener
+        self.view = self.app_listener.view 
+        self.model = self.app_listener.model
+        self.game_state = self.app_listener.view.model.game_state
+        self.console_locals = {"__name__":"__paprg_console__",\
+                               "__doc__": None,\
+                               "app_listener":self.app_listener,\
+                               "model":self.app_listener.model,\
+                               "view":self.app_listener.view,\
+                               "engine":self.app_listener.engine}
+
+        self.console = code.InteractiveConsole(self.console_locals)
+
+    def handleQuit(self, command):
+        """Implements the quit console command 
+           @type command: string
+           @param command: The command to run
+           @return: The resultstring"""
+        self.app_listener.quitGame()
+        return "Terminating ..."
+
+    def handlePause(self, command):
+        """Implements the pause console command
+           @type command: string
+           @param command: The command to run
+           @return: The resultstring"""
+        self.model.togglePause()
+        return "Game (un)paused"
+
+    def handleGrid(self, command):
+        """Implements the grid console command 
+           @type command: string
+           @param command: The command to run
+           @return: The resultstring"""
+        self.app_listener.view.active_map.toggleRenderer('GridRenderer')
+        return "Grid toggled"
+
+    def handleRun(self, command):
+        """Toggles run/walk mode of the PC player
+           @type command: string
+           @param command: The command to run.
+           @return: The response"""
+        if self.app_listener.model.pc_run == 1:
+            self.app_listener.model.pc_run = 0
+            return "PC is now walking"
+        else:
+            self.app_listener.model.pc_run = 1
+            return "PC is now running"
+
+    def handleHelp(self, command):
+        """Implements the help console command 
+           @type command: string
+           @param command: The command to run
+           @return: The resultstring"""
+        res = ""
+        for cmd in self.commands:
+            res += "%10s: %s\n" % (cmd["cmd"], cmd["help"])
+        return res
+
+    def handlePython(self,command):
+        user_code = command[7:len(command)]
+
+        codeOut = StringIO()
+
+        #make stdout and stderr write to our file, not the terminal
+        sys.stdout = codeOut
+        sys.stderr = codeOut
+
+        #Workaround it not being possible to enter a blank line in the console
+        if user_code == " ":
+            user_code = ""
+
+        #Process the code
+        self.console.push(user_code)
+        if len(self.console.buffer) == 0:
+            output = codeOut.getvalue()
+        else:
+            output =  "..."
+
+
+        #restore stdout and stderr
+        sys.stdout = sys.__stdout__
+        sys.stderr = sys.__stderr__
+
+        temp_output = output
+        output = ""
+        counter = 0
+
+        #Make the output fit in the console screen
+        for char in temp_output:
+            counter += 1
+            if char == "\n":
+                counter = 0
+            elif counter == 110:
+                output += "\n"
+                counter = 0
+            output += char
+
+        return output
+
+    def handleLoad(self, command):
+        """Implements the load console command 
+           @type command: string
+           @param command: The command to run
+           @return: The resultstring"""
+        result = None
+        load_regex = re.compile('^load')
+        l_matches = load_regex.match(command.lower())
+        if (l_matches is not None):
+            end_load = l_matches.end()
+            try:
+                l_args = command.lower()[end_load + 1:].strip()
+                l_path, l_filename = l_args.split(' ')
+                self.app_listener.model.load_save = True
+                self.app_listener.model.savegame = os.path.join(l_path, l_filename)
+                result = "Load triggered"
+
+            except Exception, l_error:
+                self.app_listener.engine.getGuiManager().getConsole().println('Error: ' + str(l_error))
+                result="Failed to load file"
+
+        return result
+
+    def handleSave(self, command):
+        """Implements the save console command 
+           @type command: string
+           @param command: The command to run
+           @return: The resultstring"""
+        save_regex = re.compile('^save')
+        s_matches = save_regex.match(command.lower())
+        if (s_matches != None):
+            end_save = s_matches.end()
+            try:
+                s_args = command.lower()[end_save+1:].strip()
+                s_path, s_filename = s_args.split(' ')
+                self.app_listener.model.save(s_path, s_filename)
+                result = "Saved to file: " + s_path + s_filename
+
+            except Exception, s_error:
+                self.app_listener.engine.getGuiManager().getConsole(). \
+                    println('Error: ' + str(s_error))
+                result = "Failed to save file"
+        return result 
+
+    def handleConsoleCommand(self, command):
+        """Implements the console logic 
+           @type command: string
+           @param command: The command to run
+           @return: The response string """
+        result = None        
+        for cmd in self.commands:
+            regex = re.compile('^' + cmd["cmd"])
+            if regex.match(command.lower()):
+                result=cmd["callback"](command)
+
+        if result is None:
+            result = self.handlePython("python " + command) 
+        return result
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/controllerbase.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,99 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+import os
+from fife import fife
+
+from parpg.common.listeners.key_listener import KeyListener
+from parpg.common.listeners.mouse_listener import MouseListener
+from parpg.common.listeners.command_listener import CommandListener
+
+class ControllerBase(KeyListener, MouseListener, CommandListener):
+    """Base of Controllers"""
+    def __init__(self, 
+                 engine, 
+                 view, 
+                 model, 
+                 application):
+        '''
+        Constructor
+        @param engine: Instance of the active fife engine
+        @type engine: fife.Engine
+        @param view: Instance of a GameSceneView
+        @param type: parpg.GameSceneView
+        @param model: The model that has the current gamestate
+        @type model: parpg.GameModel
+        @param application: The application that created this controller
+        @type application: parpg.PARPGApplication
+        @param settings: The current settings of the application
+        @type settings: fife.extensions.fife_settings.Setting
+        '''
+        KeyListener.__init__(self, application.event_listener)        
+        MouseListener.__init__(self, application.event_listener)
+        CommandListener.__init__(self, application.event_listener)
+        self.engine = engine
+        self.event_manager = engine.getEventManager()
+        self.view = view
+        self.model = model
+        self.application = application
+        
+    def pause(self, paused):
+        """Stops receiving events"""
+        if paused:
+            KeyListener.detach(self)
+            MouseListener.detach(self)
+        else:
+            KeyListener.attach(self, self.application.event_listener)
+            MouseListener.attach(self, self.application.event_listener)
+    
+    def setMouseCursor(self, image, dummy_image, mc_type="native"): 
+        """Set the mouse cursor to an image.
+           @type image: string
+           @param image: The image you want to set the cursor to
+           @type dummy_image: string
+           @param dummy_image: ???
+           @type type: string
+           @param type: ???
+           @return: None"""
+        cursor = self.engine.getCursor()
+        cursor_type = fife.CURSOR_IMAGE
+        img_pool = self.engine.getImagePool()
+        if(mc_type == "target"):
+            target_cursor_id = img_pool.addResourceFromFile(image)  
+            dummy_cursor_id = img_pool.addResourceFromFile(dummy_image)
+            cursor.set(cursor_type, dummy_cursor_id)
+            cursor.setDrag(cursor_type, target_cursor_id, -16, -16)
+        else:
+            cursor_type = fife.CURSOR_IMAGE
+            zero_cursor_id = img_pool.addResourceFromFile(image)
+            cursor.set(cursor_type, zero_cursor_id)
+            cursor.setDrag(cursor_type, zero_cursor_id)
+
+    def resetMouseCursor(self):
+        """Reset cursor to default image.
+           @return: None"""
+        image = os.path.join(self.model.settings.system_path,
+                             self.model.settings.parpg.GuiPath,
+                             self.model.settings.parpg.CursorPath,
+                             self.model.settings.parpg.CursorDefault)
+        self.setMouseCursor(image, image)
+        
+    def onStop(self):
+        """Called when the controller is removed from the list"""
+        pass 
+                
+    def pump(self):
+        """This method gets called every frame"""
+        pass
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/dialogue.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,192 @@
+#   This file is part of PARPG.
+#
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Provides classes used to contain and organize dialogue data for use within
+in-game dialogues between the player character and NPCs.
+"""
+try:
+    from collections import OrderedDict
+except ImportError:
+    # Python version 2.4-2.6 doesn't have the OrderedDict
+    from parpg.common.ordereddict import OrderedDict
+
+class Dialogue(object):
+    """
+    Represents a complete dialogue and acts as a container for the dialogue
+    data belonging to a particular NPC.
+    """
+    __slots__ = ['npc_name', 'avatar_path', 'default_greeting',
+                 'greetings', 'sections']
+    
+    def __init__(self, npc_name, avatar_path, default_greeting, greetings=None,
+                 sections=None):
+        """
+        Initialize a new L{Dialogue} instance.
+        
+        @param npc_name: name displayed for the NPC in the dialogue.
+        @type npc_name: basestring
+        @param avatar_path: path to the image that should be displayed as the
+            NPC's avatar.
+        @type avatar_path: basestring
+        @param default_greeting: section of dialogue that should be
+            displayed when the dialogue is first initiated and no other start
+            sections are available.
+        @type default_greeting: L{DialogueSection}
+        @param greetings: sections of dialogue defining the conditions
+            under which each should be displayed when the dialogue is first
+            initiated.
+        @type greetings: list of 
+            L{RootDialogueSections<DialogueGreeting>}
+        @param sections: sections of dialogue that make up this
+            L{Dialogue} instance.
+        @type sections: list of L{DialogueSections<DialogueSection>}
+        """
+        self.npc_name = npc_name
+        self.avatar_path = avatar_path
+        self.default_greeting = default_greeting
+        self.greetings = greetings if greetings is not None else []
+        self.sections = OrderedDict()
+        all_sections = [default_greeting]
+        if (greetings is not None):
+            all_sections += greetings
+        if (sections is not None):
+            all_sections += sections
+        if (__debug__):
+            section_ids = [section.id for section in all_sections]
+        for section in all_sections:
+            # Sanity check: All DialogueResponses should have next_section_id
+            # attributes that refer to valid DialogueSections in the Dialogue.
+            if (__debug__):
+                for response in section.responses:
+                    assert response.next_section_id in section_ids + \
+                        ['end', 'back'], ('"{0}" does not refer to a ' 
+                                          'DialogueSection in this Dialogue')\
+                        .format(response.next_section_id)
+            self.sections[section.id] = section
+    
+    def __str__(self):
+        """Return the string representation of a L{Dialogue} instance."""
+        string_representation = 'Dialogue(npc_id={0.npc_name})'.format(self)
+        return string_representation
+
+
+class DialogueNode(object):
+    """
+    Abstract base class that represents a node or related group of attributes
+    within a Dialogue.
+    """
+    def __init__(self, text, actions=None):
+        """
+        Initialize a new L{DialogueNode} instance.
+        
+        @param text: textual content of the L{DialogueNode}.
+        @type text: basestring
+        @param actions: dialogue actions associated with the L{DialogueNode}.
+        @type actions: list of L{DialogueActions<DialogueAction>}
+        """
+        self.text = text
+        self.actions = actions or []
+
+
+class DialogueSection(DialogueNode):
+    """DialogueNode that represents a distinct section of the dialogue."""
+    __slots__ = ['id', 'text', 'responses', 'actions']
+    
+    def __init__(self, id_, text, responses=None, actions=None):
+        """
+        Initialize a new L{DialogueSection} instance.
+        
+        @param id_: named used to uniquely identify the L{DialogueSection}
+            within a L{Dialogue}.
+        @type id_: basestring
+        @param text: text displayed as the NPC's part of the L{Dialogue}.
+        @type text: basestring
+        @param responses: possible responses that the player can choose from.
+        @type responses: list of L{DialogueResponses<DialogueResponse>}
+        @param actions: dialogue actions that should be executed when the
+            L{DialogueSection} is reached.
+        @type actions: list of L{DialogueActions<DialogueAction>}
+        """
+        DialogueNode.__init__(self, text=text, actions=actions)
+        self.id = id_
+        if (responses is not None):
+            self.responses = list(responses)
+
+
+class DialogueGreeting(DialogueSection):
+    """
+    Represents a root section of dialogue in a L{Dialogue} along with the
+    conditional statement used to determine the whether this section should be
+    displayed first upon dialogue initiation.
+    
+    @ivar id: Name used to uniquely identify the L{DialogueSection} to which
+        the L{DialogueRootSectionReference} points.
+    @type id: basestring
+    @ivar condition: Boolean Python expression used to determine if the
+        L{DialogueSection} referenced is a valid starting section.
+    @type condition: basestring
+    """
+    __slots__ = ['id', 'condition', 'text', 'actions', 'responses']
+    
+    def __init__(self, id_, condition, text, responses=None, actions=None):
+        """
+        Initialize a new L{DialogueGreeting} instance.
+        
+        @param id_: named used to uniquely identify the L{DialogueSection}
+            within a L{Dialogue}.
+        @type id_: basestring
+        @param condition: Boolean Python expression used to determine if this
+            root dialogue section should be displayed.
+        @type condition: basestring
+        @param text: text displayed as the NPC's part of the L{Dialogue}.
+        @type text: basestring
+        @param responses: possible responses that the player can choose from.
+        @type responses: list of L{DialogueResponses<DialogueResponse>}
+        @param actions: dialogue actions that should be executed when the
+            L{DialogueSection} is reached.
+        @type actions: list of L{DialogueActions<DialogueAction>}
+        """
+        DialogueSection.__init__(self, id_=id_, text=text, responses=responses,
+                                 actions=actions)
+        self.condition = condition
+
+
+class DialogueResponse(DialogueNode):
+    """
+    L{DialogueNode} that represents one possible player response to a
+    particular L{DialogueSection}.
+    """
+    __slots__ = ['text', 'actions', 'condition', 'next_section_id']
+    
+    def __init__(self, text, next_section_id, actions=None, condition=None):
+        """
+        Initialize a new L{DialogueResponse} instance.
+        
+        @param text: text displayed as the content of the player's response.
+        @type text: basestring
+        @param next_section_id: ID of the L{DialogueSection} that should be
+            jumped to if this response is chosen by the player.
+        @type next_section_id: basestring
+        @param actions: dialogue actions that should be executed if this
+            response is chosen by the player.
+        @type actions: list of L{DialogueActions<DialogueAction>}
+        @param condition: Python expression that when evaluated determines
+            whether the L{DialogueResponse} should be displayed to the player
+            as a valid response.
+        @type condition: basestring
+        """
+        DialogueNode.__init__(self, text=text, actions=actions)
+        self.condition = condition
+        self.next_section_id = next_section_id
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/dialogueactions.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,373 @@
+#   This file is part of PARPG.
+#
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Provides classes used to implement dialogue logic and allow dialogues to have
+external effects on the game state.
+"""
+import logging
+
+logger = logging.getLogger('dialogueaction')
+
+class DialogueAction(object):
+    """
+    Abstract base class for subclasses that represent dialogue actions embedded
+    within a DialogueSection or DialogueResponse.
+    
+    Subclasses must define the keyword class variable and implement both the
+    __init__ and __call__ methods.
+    
+    @cvar keyword: keyword used by the L{DialogueParser} to recognize the
+        L{DialogueAction} in serialized L{Dialogues<Dialogues>}.
+    @type keyword: basestring
+    """
+    logger = logging.getLogger('dialogueaction.DialogueAction')
+    registered_actions = {}
+    
+    @classmethod
+    def registerAction(cls, dialogue_action_type):
+        """
+        Register a L{DialogueAction} subclass for easy reference.
+        
+        @param dialogue_action_type: dialogue action to register.
+        @type dialogue_action_type: L{DialogueAction} subclass
+        """
+        cls.registered_actions[dialogue_action_type.keyword] = \
+            dialogue_action_type
+    
+    def __init__(self, *args, **kwargs):
+        """
+        Initialize a new L{DialogueAction} instance.
+        
+        @param args: positional arguments passed by the L{DialogueParser} after
+            reading a serialized L{Dialogue}.
+        @type args: list of objects
+        @param kwargs: keyword arguments passed by the L{DialogueParser} after
+            reading a serialized L{Dialogue}.
+        @type kwargs: dict of objects
+        """
+        if (not hasattr(type(self), 'keyword')):
+            raise AttributeError('DialogueAction subclasses must define the '
+                                 'keyword class variable.')
+        self.arguments = (args, kwargs)
+    
+    def __call__(self, game_state):
+        """
+        Execute the L{DialogueAction}.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        raise NotImplementedError('subclasses of DialogueAction must '
+                                  'override __call__')
+
+
+class MeetAction(DialogueAction):
+    """
+    L{DialogueAction} that adds an NPC to the list of NPCs known by the player.
+    """
+    keyword = 'meet'
+    
+    def __init__(self, *args, **kwargs):
+        """
+        Initialize a new L{MeetAction} instance.
+        
+        @param args: positional arguments.
+        @type args: list of objects
+        @param npc_id: identifier of the NPC that the player has met.
+        @type npc_id: basestring
+        @param kwargs: keyword arguments (not used).
+        @type kwargs: dict of objects
+        """
+        DialogueAction.__init__(self, *args, **kwargs)
+        self.npc_id = args[0]
+    
+    def __call__(self, game_state):
+        """
+        Add an NPC to the list of NPCs known by the player.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        npc_id = self.npc_id
+        # NOTE Technomage 2010-11-13: This print statement seems overly
+        #     verbose, so I'm logging it as an INFO message instead.
+#        print("You've met {0}!".format(npc_id))
+        self.logger.info("You've met {0}!".format(npc_id))
+        game_state['pc'].meet(npc_id)
+DialogueAction.registerAction(MeetAction)
+
+
+class InventoryAction(DialogueAction):
+    """
+    Abstract base class for L{DialogueActions<DialogueAction>} used to
+    manipulate the NPC's and the player's inventory.
+    """
+    def __init__(self, *args, **kwargs):
+        """
+        Initialize a new L{InventoryAction} instance.
+        
+        @param args: positional arguments.
+        @type args: list of objects
+        @param item_types: item types that should be manipulated.
+        @type item_types: list of basestrings
+        @param kwargs: keyword arguments.
+        @type kwargs: dict of objects
+        """
+        DialogueAction.__init__(self, *args, **kwargs)
+        self.item_types = args
+
+
+class TakeStuffAction(InventoryAction):
+    """
+    L{InventoryAction} used to move items from the NPC's inventory to the
+    player's inventory.
+    """
+    keyword = 'take_stuff'
+    
+    def __call__(self, game_state):
+        """
+        Move items from the NPC's inventory to the player's inventory.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        item_types = self.item_types
+        for item_type in item_types:
+            item = game_state['npc'].inventory.findItem(item_type=item_type)
+            if (item):
+                game_state['npc'].give(item, game_state['pc'])
+                print("{0} gave you the {1}".format(game_state['npc'].name,
+                                                    item_type))
+            else:
+                print("{0} doesn't have the {1}".format(game_state['npc'].name,
+                                                        item_type))
+DialogueAction.registerAction(TakeStuffAction)
+
+
+class GiveStuffAction(InventoryAction):
+    """
+    L{InventoryAction} used to move items from the player's inventory to the
+    NPC's inventory.
+    """
+    keyword = 'give_stuff'
+    
+    def __call__(self, game_state):
+        """
+        Move items from the player's inventory to the NPC's inventory.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        item_types = self.item_types
+        for item_type in item_types:
+            item = game_state['npc'].inventory.findItem(item_type = item_type)
+            if (item):
+                game_state['pc'].give(item, game_state['npc'])
+                print("You give the {0} to {1}".format(item_type,
+                                                       game_state['npc'].name))
+            else:
+                print("You don't have the {0}".format(item_type))
+DialogueAction.registerAction(GiveStuffAction)
+
+
+class QuestAction(DialogueAction):
+    """
+    Abstract base class for quest-related L{DialogueActions<DialogueAction>}.
+    """
+    def __init__(self, *args, **kwargs):
+        """
+        Initialize a new L{QuestAction} instance.
+        
+        @param args: positional arguments.
+        @type args: list of objects
+        @param quest_id: ID of the quest to manipulate.
+        @type quest_id: basestring
+        @param kwargs: keyword arguments (not used).
+        @type kwargs: dict of objects
+        """
+        DialogueAction.__init__(self, *args, **kwargs)
+        self.quest_id = kwargs['quest'] if 'quest' in kwargs else args[0]
+
+
+class StartQuestAction(QuestAction):
+    """L{QuestAction} used to activate a quest."""
+    keyword = 'start_quest'
+    
+    def __call__(self, game_state):
+        """
+        Activate a quest.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        quest_id = self.quest_id
+        print("You've picked up the \"{0}\" quest!".format(quest_id))
+        game_state['quest'].activateQuest(quest_id)
+DialogueAction.registerAction(StartQuestAction)
+
+
+class CompleteQuestAction(QuestAction):
+    """
+    L{QuestAction} used to mark a quest as successfully finished/completed.
+    """
+    keyword = 'complete_quest'
+    
+    def __call__(self, game_state):
+        """
+        Successfully complete a quest.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        quest_id = self.quest_id
+        print("You've finished the \"{0}\" quest".format(quest_id))
+        game_state['quest'].finishQuest(quest_id)
+DialogueAction.registerAction(CompleteQuestAction)
+
+
+class FailQuestAction(QuestAction):
+    """L{QuestAction} used to fail an active quest."""
+    keyword = 'fail_quest'
+    
+    def __call__(self, game_state):
+        """
+        Fail an active quest.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        quest_id = self.quest_id
+        print("You've failed the \"{0}\" quest".format(quest_id))
+        game_state['quest'].failQuest(quest_id)
+DialogueAction.registerAction(FailQuestAction)
+
+
+class RestartQuestAction(QuestAction):
+    """L{QuestAction} used to restart an active quest."""
+    keyword = 'restart_quest'
+    
+    def __call__(self, game_state):
+        """
+        Restart an active quest.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        quest_id = self.quest_id
+        print("You've restarted the \"{0}\" quest".format(quest_id))
+        game_state['quest'].restartQuest(quest_id)
+DialogueAction.registerAction(RestartQuestAction)
+
+
+class QuestVariableAction(QuestAction):
+    """
+    Base class for L{QuestActions<QuestAction>} that modify quest
+    variables.
+    """
+    def __init__(self, *args, **kwargs):
+        """
+        Initialize a new L{QuestVariableAction} instance.
+        
+        @param args: positional arguments (not used).
+        @type args: list of objects
+        @param kwargs: keyword arguments.
+        @type kwargs: dict of objects
+        @keyword quest: ID of the quest whose variable should be modified.
+        @type quest: basestring
+        @keyword variable: name of the quest variable to modify.
+        @type variable: basestring
+        @keyword value: new value that should be used to modify the quest
+            variable.
+        @type value: object
+        """
+        QuestAction.__init__(self, *args, **kwargs)
+        self.variable_name = kwargs['variable']
+        self.value = kwargs['value']
+
+
+class IncreaseQuestVariableAction(QuestVariableAction):
+    """
+    L{QuestVariableAction} used to increase the value of a quest variable by a
+    set amount.
+    """
+    keyword = 'increase_quest_variable'
+    
+    def __call__(self, game_state):
+        """
+        Increase a quest variable by a set amount.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        quest_id = self.quest_id
+        variable_name = self.variable_name
+        value = self.value
+        print('Increased {0} by {1}'.format(variable_name, value))
+        game_state['quest'][quest_id].increaseValue(variable_name, value)
+DialogueAction.registerAction(IncreaseQuestVariableAction)
+
+
+class DecreaseQuestVariableAction(QuestVariableAction):
+    """
+    L{QuestVariableAction} used to decrease the value of a quest variable by a
+    set amount.
+    """
+    keyword = 'decrease_quest_variable'
+    
+    def __call__(self, game_state):
+        """
+        Decrease a quest variable by a set amount.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        quest_id = self.quest_id
+        variable_name = self.variable_name
+        value = self.value
+        print('Decreased {0} by {1}'.format(variable_name, value))
+        game_state['quest'][quest_id].decreaseValue(variable_name, value)
+DialogueAction.registerAction(DecreaseQuestVariableAction)
+
+
+class SetQuestVariableAction(QuestVariableAction):
+    """
+    L{QuestVariableAction} used to set the value of a quest variable.
+    """
+    keyword = 'set_quest_variable'
+    
+    def __call__(self, game_state):
+        """
+        Set the value of a quest variable.
+        
+        @param game_state: variables and functions that make up the current
+            game state.
+        @type game_state: dict of objects
+        """
+        quest_id = self.quest_id
+        variable_name = self.variable_name
+        value = self.value
+        print('Set {0} to {1}'.format(variable_name, value))
+        game_state['quest'][quest_id].setValue(variable_name, value)
+DialogueAction.registerAction(SetQuestVariableAction)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/dialoguecontroller.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,59 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+from controllerbase import ControllerBase
+
+class DialogueController(ControllerBase):
+    """Controller that takes over when a dialogue is started"""
+    def __init__(self, 
+                 engine, 
+                 view, 
+                 model, 
+                 application):
+        """
+        Constructor
+        @param engine: Instance of the active fife engine
+        @type engine: fife.Engine
+        @param view: Instance of a GameSceneView
+        @param type: parpg.GameSceneView
+        @param model: The model that has the current gamestate
+        @type model: parpg.GameModel
+        @param application: The application that created this controller
+        @type application: parpg.PARPGApplication
+        @param settings: The current settings of the application
+        @type settings: fife.extensions.fife_settings.Setting
+        """
+        super(DialogueController, self).__init__(engine,
+                                                  view,
+                                                  model,
+                                                  application)
+        self.dialogue = None
+        self.view = view
+        
+    def startTalk(self, npc):
+        if npc.dialogue is not None:
+            self.model.active_map.centerCameraOnPlayer()            
+            npc.talk(self.model.game_state.player_character)
+            self.dialogue = self.view.hud.showDialogue(npc)
+            self.dialogue.initiateDialogue()
+            self.model.pause(True)
+            self.view.hud.enabled = False
+
+            
+    def pump(self):
+        if self.dialogue and not self.dialogue.active:
+            self.application.popController()
+            self.model.pause(False)
+            self.view.hud.enabled = True
+            
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/dialogueparsers.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,669 @@
+#   This file is part of PARPG.
+#
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Contains classes for parsing and validating L{Dialogues<Dialogue>} and other
+dialogue-related data.
+
+@TODO Technomage 2010-11-13: Exception handling + validation needs work.
+    Currently YAML files are only crudely validated - the code assumes that
+    the file contains valid dialogue data, and if that assumption is
+    violated and causes the code to raise any TypeErrors, AttributeErrors or
+    ValueErrors the code then raises a DialogueFormatError with the
+    original (and mostly unhelpful) error message.
+@TODO Technomage 2010-11-13: Support reading and writing unicode.
+"""
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+from collections import Sequence
+try:
+    from collections import OrderedDict
+except ImportError:
+    # Python version 2.4-2.6 doesn't have the OrderedDict
+    from parpg.common.ordereddict import OrderedDict
+import re
+import textwrap
+
+import yaml
+
+from parpg import COPYRIGHT_HEADER
+from parpg.dialogue import (Dialogue, DialogueSection, DialogueResponse,
+    DialogueGreeting)
+from parpg.dialogueactions import DialogueAction
+
+import logging
+logger = logging.getLogger('dialogueparser')
+
+class DialogueFormatError(Exception):
+    """Exception thrown when the DialogueParser has encountered an error."""
+
+
+class AbstractDialogueParser(object):
+    """
+    Abstract base class defining the interface for parsers responsible for
+    constructing a L{Dialogue} from its serialized representation.
+    """
+    def load(self, stream):
+        """
+        Parse a stream and attempt to construct a new L{Dialogue} instance from
+        its serialized representation.
+        
+        @param stream: open stream containing the serialized representation of
+            a Dialogue.
+        @type stream: BufferType
+        """
+        raise NotImplementedError('AbstractDialogueParser subclasses must '
+                                  'override the load method.')
+    
+    def dump(self, dialogue, stream):
+        """
+        Serialize a L{Dialogue} instance and dump it to an open stream.
+        
+        @param dialogue: dialogue to serialize.
+        @type dialogue: L{Dialogue}
+        @param stream: open stream into which the serialized L{Dialogue} should
+            be dumped.
+        @type stream: BufferType
+        """
+        raise NotImplementedError('AbstractDialogueParser subclasses must '
+                                  'override the dump method.')
+    
+    def validate(self, stream):
+        """
+        Parse a stream and verify that it contains a valid serialization of a
+        L{Dialogue instance}.
+        
+        @param stream: stream containing the serialized representation of a
+            L{Dialogue}
+        @type stream: BufferType
+        """
+        raise NotImplementedError('AbstractDialogueParser subclasses must '
+                                  'override the validate method.')
+
+
+class YamlDialogueParser(AbstractDialogueParser):
+    """
+    L{AbstractDialogueParser} subclass responsible for parsing dialogues
+    serialized in YAML.
+    """
+    logger = logging.getLogger('dialogueparser.OldYamlDialogueParser')
+    
+    def load(self, stream, loader_class=yaml.Loader):
+        """
+        Parse a YAML stream and attempt to construct a new L{Dialogue}
+        instance.
+        
+        @param stream: stream containing the serialized YAML representation of
+            a L{Dialogue}.
+        @type stream: BufferType
+        @param loader_class: PyYAML loader class to use for reading the
+            serialization.
+        @type loader_class: yaml.BaseLoader subclass
+        """
+        loader = loader_class(stream)
+        try:
+            dialogue = \
+                self._constructDialogue(loader, loader.get_single_node())
+        except (AssertionError,) as error:
+            raise DialogueFormatError(str(error))
+        return dialogue
+    
+    def dump(self, dialogue, output_stream, dumper_class=yaml.Dumper):
+        """
+        Serialize a L{Dialogue} instance as YAML and dump it to an open stream.
+        
+        @param dialogue: dialogue to serialize.
+        @type dialogue: L{Dialogue}
+        @param stream: open stream into which the serialized L{Dialogue} should
+            be dumped.
+        @type stream: BufferType
+        @param dumper_class: PyYAML dumper class to use for formatting the
+            serialization.
+        @type dumper_class: yaml.BaseDumper subclass
+        """
+        intermediate_stream = StringIO()
+        # KLUDE Technomage 2010-11-16: The "width" argument seems to be broken,
+        #     as it doesn't take into about current line indentation and fails
+        #     to correctly wrap at word boundaries.
+        dumper = dumper_class(intermediate_stream, default_flow_style=False,
+                              indent=4, width=99999, line_break='\n',
+                              allow_unicode=True, explicit_start=True,
+                              explicit_end=True, tags=False)
+        dialogue_node = self._representDialogue(dumper, dialogue)
+        dumper.open()
+        dumper.serialize(dialogue_node)
+        dumper.close()
+        file_contents = intermediate_stream.getvalue()
+        
+        file_contents = re.sub(r'(\n|\r|\r\n)(\s*)(GOTO: .*)', r'\1\2\3\1\2',
+                               file_contents)
+        lines = file_contents.splitlines()
+        max_line_length = 76 # 79 - 3 chars for escaping newlines
+        for i in range(len(lines)):
+            line = lines[i]
+            match = re.match(
+                r'^(\s*(?:-\s+)?)(SAY|REPLY|CONDITION):\s+"(.*)"$',
+                line
+            )
+            if (match and len(line) > max_line_length):
+                # Wrap long lines for readability.
+                initial_indent = len(match.group(1))
+                subsequent_indent = initial_indent + 4
+                text_wrapper = textwrap.TextWrapper(
+                    max_line_length,
+                    subsequent_indent=' ' * subsequent_indent,
+                    break_long_words=False,
+                    break_on_hyphens=False
+                )
+                new_lines = text_wrapper.wrap(line)
+                new_lines = (
+                    new_lines[:1] + [re.sub(r'^(\s*) (.*)$', r'\1\ \2', l)
+                                     for l in new_lines[1:]]
+                )
+                lines[i] = '\\\n'.join(new_lines)
+        
+        output_stream.write(COPYRIGHT_HEADER)
+        output_stream.write('\n'.join(lines))
+        
+    
+    def _representDialogue(self, dumper, dialogue):
+        dialogue_node = dumper.represent_dict({})
+        dialogue_dict = OrderedDict()
+        dialogue_dict['NPC_NAME'] = dialogue.npc_name
+        dialogue_dict['AVATAR_PATH'] = dialogue.avatar_path
+        dialogue_dict['DEFAULT_GREETING'] = \
+            self._representDialogueSection(dumper,
+                                           dialogue.default_greeting)
+        # NOTE Technomage 2010-11-16: Dialogue stores its sections in an
+        #     OrderedDict, so a round-trip load, dump, and load will preserve
+        #     the order of DialogueSections.
+        if (len(dialogue.greetings) > 0):
+            greetings_list_node = dumper.represent_list([])
+            greetings_list = greetings_list_node.value
+            for greeting in dialogue.greetings:
+                greeting_node = \
+                    self._representRootDialogueSection(dumper, greeting)
+                greetings_list.append(greeting_node)
+            dialogue_dict['GREETINGS'] = greetings_list_node
+        if (len(dialogue.setions) > 0):
+            sections_list_node = dumper.represent_list([])
+            sections_list = sections_list_node.value
+            for section in dialogue.sections.values():
+                section_node = self._representDialogueSection(dumper, section)
+                sections_list.append(section_node)
+            dialogue_dict['SECTIONS'] = sections_list_node
+        
+        for key, value in dialogue_dict.items():
+            if (isinstance(key, yaml.Node)):
+                key_node = key
+            else:
+                key_node = dumper.represent_data(key)
+            if (isinstance(value, yaml.Node)):
+                value_node = value
+            else:
+                value_node = dumper.represent_data(value)
+            dialogue_node.value.append((key_node, value_node))
+        return dialogue_node
+    
+    def _representRootDialogueSection(self, dumper, greeting):
+        greeting_node = dumper.represent_dict({})
+        greeting_dict = OrderedDict()
+        greeting_dict['ID'] = greeting.id
+        greeting_dict['CONDITION'] = dumper.represent_scalar(
+            'tag:yaml.org,2002:str',
+            greeting.condition,
+            style='"'
+        )
+        for key, value in greeting_dict.items():
+            if (isinstance(key, yaml.Node)):
+                key_node = key
+            else:
+                key_node = dumper.represent_data(key)
+            if (isinstance(value, yaml.Node)):
+                value_node = value
+            else:
+                value_node = dumper.represent_data(value)
+            greeting_node.value.append((key_node, value_node))
+        return greeting_node
+    
+    def _representDialogueSection(self, dumper, dialogue_section):
+        section_node = dumper.represent_dict({})
+        section_dict = OrderedDict() # OrderedDict is required to preserve
+                                     # the order of attributes.
+        section_dict['ID'] = dialogue_section.id
+        # KLUDGE Technomage 2010-11-16: Hard-coding the tag like this could be
+        #     a problem when writing unicode.
+        section_dict['SAY'] = dumper.represent_scalar('tag:yaml.org,2002:str',
+                                                      dialogue_section.text,
+                                                      style='"')
+        actions_list_node = dumper.represent_list([])
+        actions_list = actions_list_node.value
+        for action in dialogue_section.actions:
+            action_node = self._representDialogueAction(dumper, action)
+            actions_list.append(action_node)
+        if (actions_list):
+            section_dict['ACTIONS'] = actions_list_node
+        responses_list_node = dumper.represent_list([])
+        responses_list = responses_list_node.value
+        for response in dialogue_section.responses:
+            response_node = self._representDialogueResponse(dumper, response)
+            responses_list.append(response_node)
+        section_dict['RESPONSES'] = responses_list_node
+        
+        for key, value in section_dict.items():
+            if (isinstance(key, yaml.Node)):
+                key_node = key
+            else:
+                key_node = dumper.represent_data(key)
+            if (isinstance(value, yaml.Node)):
+                value_node = value
+            else:
+                value_node = dumper.represent_data(value)
+            section_node.value.append((key_node, value_node))
+        return section_node
+    
+    def _representDialogueResponse(self, dumper, dialogue_response):
+        response_node = dumper.represent_dict({})
+        response_dict = OrderedDict()
+        # KLUDGE Technomage 2010-11-16: Hard-coding the tag like this could be
+        #     a problem when writing unicode.
+        response_dict['REPLY'] = dumper.represent_scalar(
+            'tag:yaml.org,2002:str',
+            dialogue_response.text,
+            style='"')
+        if (dialogue_response.condition is not None):
+            response_dict['CONDITION']  = dumper.represent_scalar(
+                'tag:yaml.org,2002:str',
+                dialogue_response.condition,
+                style='"'
+            )
+        actions_list_node = dumper.represent_list([])
+        actions_list = actions_list_node.value
+        for action in dialogue_response.actions:
+            action_node = self._representDialogueAction(dumper, action)
+            actions_list.append(action_node)
+        if (actions_list):
+            response_dict['ACTIONS'] = actions_list_node
+        response_dict['GOTO'] = dialogue_response.next_section_id
+        
+        for key, value in response_dict.items():
+            if (isinstance(key, yaml.Node)):
+                key_node = key
+            else:
+                key_node = dumper.represent_data(key)
+            if (isinstance(value, yaml.Node)):
+                value_node = value
+            else:
+                value_node = dumper.represent_data(value)
+            response_node.value.append((key_node, value_node))
+        return response_node
+    
+    def _representDialogueAction(self, dumper, dialogue_action):
+        action_node = dumper.represent_dict({})
+        action_dict = OrderedDict()
+        args, kwargs = dialogue_action.arguments
+        if (args and not kwargs):
+            arguments = list(args)
+        elif (kwargs and not args):
+            arguments = kwargs
+        else:
+            arguments = [list(args), kwargs]
+        action_dict[dialogue_action.keyword] = arguments
+        
+        for key, value in action_dict.items():
+            if (isinstance(key, yaml.Node)):
+                key_node = key
+            else:
+                key_node = dumper.represent_data(key)
+            if (isinstance(value, yaml.Node)):
+                value_node = value
+            else:
+                value_node = dumper.represent_data(value)
+            action_node.value.append((key_node, value_node))
+        return action_node
+    
+    def _constructDialogue(self, loader, yaml_node):
+        npc_name = None
+        avatar_path = None
+        default_greeting = None
+        greetings = []
+        sections = []
+        
+        try:
+            for key_node, value_node in yaml_node.value:
+                key = key_node.value
+                if (key == u'NPC_NAME'):
+                    npc_name = loader.construct_object(value_node)
+                elif (key == u'AVATAR_PATH'):
+                    avatar_path = loader.construct_object(value_node)
+                elif (key == u'DEFAULT_GREETING'):
+                    default_greeting = \
+                        self._constructDialogueSection(loader, value_node)
+                elif (key == u'GREETINGS'):
+                    for greeting_node in value_node.value:
+                        greeting = self._constructRootDialogueSection(
+                                loader,
+                                greeting_node
+                        )
+                        greetings.append(
+                            greeting
+                        )
+                elif (key == u'SECTIONS'):
+                    for section_node in value_node.value:
+                        dialogue_section = self._constructDialogueSection(
+                            loader,
+                            section_node
+                        )
+                        sections.append(dialogue_section)
+        except (AttributeError, TypeError, ValueError) as e:
+            raise DialogueFormatError(e)
+        
+        dialogue = Dialogue(npc_name=npc_name, avatar_path=avatar_path,
+                            default_greeting=default_greeting,
+                            greetings=greetings,
+                            sections=sections)
+        return dialogue
+    
+    def _constructRootDialogueSection(self, loader, greeting_node):
+        id = None
+        text = None
+        condition = None
+        responses = []
+        actions = []
+        greeting = None
+        
+        try:
+            for key_node, value_node in greeting_node.value:
+                key = key_node.value
+                if (key == u'ID'):
+                    id = loader.construct_object(value_node)
+                elif (key == u'SAY'):
+                    text = loader.construct_object(value_node)
+                elif (key == u'CONDITION'):
+                    condition = loader.construct_object(value_node)
+                elif (key == u'RESPONSES'):
+                    for response_node in value_node.value:
+                        dialogue_response = self._constructDialogueResponse(
+                            loader,
+                            response_node
+                        )
+                        responses.append(dialogue_response)
+                elif (key == u'ACTIONS'):
+                    for action_node in value_node.value:
+                        action = self._constructDialogueAction(loader,
+                                                             action_node)
+                        actions.append(action)
+        except (AttributeError, TypeError, ValueError) as e:
+            raise DialogueFormatError(e)
+        else:
+            greeting = DialogueSection(id=id, text=text,
+                                           condition=condition,
+                                           responses=responses,
+                                           actions=actions)
+        
+        return greeting
+    
+    def _constructDialogueSection(self, loader, section_node):
+        id_ = None
+        text = None
+        responses = []
+        actions = []
+        dialogue_section = None
+        
+        try:
+            for key_node, value_node in section_node.value:
+                key = key_node.value
+                if (key == u'ID'):
+                    id_ = loader.construct_object(value_node)
+                elif (key == u'SAY'):
+                    text = loader.construct_object(value_node)
+                elif (key == u'RESPONSES'):
+                    for response_node in value_node.value:
+                        dialogue_response = self._constructDialogueResponse(
+                            loader,
+                            response_node
+                        )
+                        responses.append(dialogue_response)
+                elif (key == u'ACTIONS'):
+                    for action_node in value_node.value:
+                        action = self._constructDialogueAction(loader,
+                                                             action_node)
+                        actions.append(action)
+        except (AttributeError, TypeError, ValueError) as e:
+            raise DialogueFormatError(e)
+        else:
+            dialogue_section = DialogueSection(id_=id_, text=text,
+                                               responses=responses,
+                                               actions=actions)
+        
+        return dialogue_section
+    
+    def _constructDialogueResponse(self, loader, response_node):
+        text = None
+        next_section_id = None
+        actions = []
+        condition = None
+        
+        try:
+            for key_node, value_node in response_node.value:
+                key = key_node.value
+                if (key == u'REPLY'):
+                    text = loader.construct_object(value_node)
+                elif (key == u'ACTIONS'):
+                    for action_node in value_node.value:
+                        action = self._constructDialogueAction(loader,
+                                                             action_node)
+                        actions.append(action)
+                elif (key == u'CONDITION'):
+                    condition = loader.construct_object(value_node)
+                elif (key == u'GOTO'):
+                    next_section_id = loader.construct_object(value_node)
+        except (AttributeError, TypeError, ValueError) as e:
+            raise DialogueFormatError(e)
+        
+        dialogue_response = DialogueResponse(text=text,
+                                             next_section_id=next_section_id,
+                                             actions=actions,
+                                             condition=condition)
+        return dialogue_response
+    
+    def _constructDialogueAction(self, loader, action_node):
+        mapping = loader.construct_mapping(action_node, deep=True)
+        keyword, arguments = mapping.items()[0]
+        if (isinstance(arguments, dict)):
+            # Got a dictionary of keyword arguments.
+            args = ()
+            kwargs = arguments
+        elif (not isinstance(arguments, Sequence) or
+              isinstance(arguments, basestring)):
+            # Got a single positional argument.
+            args = (arguments,)
+            kwargs = {}
+        elif (not len(arguments) == 2 or not isinstance(arguments[1], dict)):
+            # Got a list of positional arguments.
+            args = arguments
+            kwargs = {}
+        else:
+            self.logger.error(
+                '{0} is an invalid DialogueAction argument'.format(arguments)
+            )
+            return None
+        
+        action_type = DialogueAction.registered_actions.get(keyword)
+        if (action_type is None):
+            self.logger.error(
+                'no DialogueAction with keyword "{0}"'.format(keyword)
+            )
+            dialogue_action = None
+        else:
+            dialogue_action = action_type(*args, **kwargs)
+        return dialogue_action
+
+
+class OldYamlDialogueParser(YamlDialogueParser):
+    """
+    L{YAMLDialogueParser} that can read and write dialogues in the old
+    Techdemo1 dialogue file format.
+    
+    @warning: This class is deprecated and likely to be removed in a future
+        version.
+    """
+    logger = logging.getLogger('dialogueparser.OldYamlDialogueParser')
+    
+    def __init__(self):
+        self.response_actions = {}
+    
+    def load(self, stream):
+        dialogue = YamlDialogueParser.load(self, stream)
+        # Place all DialogueActions that were in DialogueSections into the
+        # DialogueResponse that led to the action's original section.
+        for section in dialogue.sections.values():
+            for response in section.responses:
+                actions = self.response_actions.get(response.next_section_id)
+                if (actions is not None):
+                    response.actions = actions
+        return dialogue
+    
+    def _constructDialogue(self, loader, yaml_node):
+        npc_name = None
+        avatar_path = None
+        start_section_id = None
+        sections = []
+        
+        try:
+            for key_node, value_node in yaml_node.value:
+                key = key_node.value
+                if (key == u'NPC'):
+                    npc_name = loader.construct_object(value_node)
+                elif (key == u'AVATAR'):
+                    avatar_path = loader.construct_object(value_node)
+                elif (key == u'START'):
+                    start_section_id = loader.construct_object(value_node)
+                elif (key == u'SECTIONS'):
+                    for id_node, section_node in value_node.value:
+                        dialogue_section = self._constructDialogueSection(
+                            loader,
+                            id_node,
+                            section_node
+                        )
+                        sections.append(dialogue_section)
+        except (AttributeError, TypeError, ValueError) as e:
+            raise DialogueFormatError(e)
+        
+        dialogue = Dialogue(npc_name=npc_name, avatar_path=avatar_path,
+                            start_section_id=start_section_id,
+                            sections=sections)
+        return dialogue
+    
+    def _constructDialogueSection(self, loader, id_node, section_node):
+        id = loader.construct_object(id_node)
+        text = None
+        responses = []
+        actions = []
+        dialogue_section = None
+        
+        try:
+            for node in section_node.value:
+                key_node, value_node = node.value[0]
+                key = key_node.value
+                if (key == u'say'):
+                    text = loader.construct_object(value_node)
+                elif (key == u'meet'):
+                    action = self._constructDialogueAction(loader, node)
+                    actions.append(action)
+                elif (key in [u'start_quest', u'complete_quest', u'fail_quest',
+                              u'restart_quest', u'set_value',
+                              u'decrease_value', u'increase_value',
+                              u'give_stuff', u'get_stuff']):
+                    action = self._constructDialogueAction(loader, node)
+                    if (id not in self.response_actions.keys()):
+                        self.response_actions[id] = []
+                    self.response_actions[id].append(action)
+                elif (key == u'responses'):
+                    for response_node in value_node.value:
+                        dialogue_response = self._constructDialogueResponse(
+                            loader,
+                            response_node
+                        )
+                        responses.append(dialogue_response)
+        except (AttributeError, TypeError, ValueError) as e:
+            raise DialogueFormatError(e)
+        else:
+            dialogue_section = DialogueSection(id=id, text=text,
+                                               responses=responses,
+                                               actions=actions)
+        
+        return dialogue_section
+    
+    def _constructDialogueResponse(self, loader, response_node):
+        text = None
+        next_section_id = None
+        actions = []
+        condition = None
+        
+        try:
+            text = loader.construct_object(response_node.value[0])
+            next_section_id = loader.construct_object(response_node.value[1])
+            if (len(response_node.value) == 3):
+                condition = loader.construct_object(response_node.value[2])
+        except (AttributeError, TypeError, ValueError) as e:
+            raise DialogueFormatError(e)
+        
+        dialogue_response = DialogueResponse(text=text,
+                                             next_section_id=next_section_id,
+                                             actions=actions,
+                                             condition=condition)
+        return dialogue_response
+    
+    def _constructDialogueAction(self, loader, action_node):
+        mapping = loader.construct_mapping(action_node, deep=True)
+        keyword, arguments = mapping.items()[0]
+        if (keyword == 'get_stuff'):
+            # Renamed keyword in new syntax.
+            keyword = 'take_stuff'
+        elif (keyword == 'set_value'):
+            keyword = 'set_quest_value'
+        elif (keyword == 'increase_value'):
+            keyword = 'increase_quest_value'
+        elif (keyword == 'decrease_value'):
+            keyword = 'decrease_quest_value'
+        if (isinstance(arguments, dict)):
+            # Got a dictionary of keyword arguments.
+            args = ()
+            kwargs = arguments
+        elif (not isinstance(arguments, Sequence) or
+              isinstance(arguments, basestring)):
+            # Got a single positional argument.
+            args = (arguments,)
+            kwargs = {}
+        elif (not len(arguments) == 2 or not isinstance(arguments[1], dict)):
+            # Got a list of positional arguments.
+            args = arguments
+            kwargs = {}
+        else:
+            self.logger.error(
+                '{0} is an invalid DialogueAction argument'.format(arguments)
+            )
+            return None
+        action_type = DialogueAction.registered_actions.get(keyword)
+        if (action_type is None):
+            self.logger.error(
+                'no DialogueAction with keyword "{0}"'.format(keyword)
+            )
+            dialogue_action = None
+        else:
+            dialogue_action = action_type(*args, **kwargs)
+        return dialogue_action
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/dialogueprocessor.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,378 @@
+#   This file is part of PARPG.
+#
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Provides the core interface to the dialogue subsystem used to process player
+L{Dialogues<Dialogue>} with NPCs.
+"""
+import logging
+
+from parpg.common.utils import dedent_chomp
+
+if (__debug__):
+    from collections import Sequence, MutableMapping
+    from parpg.dialogue import Dialogue
+
+logger = logging.getLogger('dialogueprocessor')
+
+class DialogueProcessor(object):
+    """
+    Primary interface to the dialogue subsystem used to initiate and process a
+    L{Dialogue} with an NPC.
+    
+    To begin a dialogue with an NPC a L{DialogueProcessor} must first be
+    instantiated with the dialogue data to process and a dictionary of Python
+    objects defining the game state for testing of response conditionals. The
+    L{initiateDialogue} must be called to initialized the L{DialogueProcessor},
+    and once it is initialized processing of
+    L{DialogueSections<DialogueSection>} and
+    L{DialogueResponses<DialogueResponse>} can be initiated via the
+    L{continueDialogue} and L{reply} class methods.
+    
+    The state of dialogue processing is stored via the
+    L{dialogue_section_stack} class attribute, which stores a list of
+    L{DialogueSections<DialogueSection>} that have been or are currently being
+    processed. Each time L{reply} is called with a L{DialogueResponse} its
+    next_section_id attribute is used to select a new L{DialogueSection} from
+    the L{dialogue}. The selected L{DialogueSection} is then pushed
+    onto the end of the L{dialogue_section_stack}, ready to be processed via
+    L{continueDialogue}. The exception to this rule occurs when L{reply} is
+    called with a L{DialogueResponse} whose next_section_id attribute is "end"
+    or "back". "end" terminates the dialogue as described below, while "back"
+    removes the last L{DialogueSection} on the L{dialogue_section_stack}
+    effectively going back to the previous section of dialogue.
+    
+    The L{DialogueProcessor} terminates dialogue processing once L{reply} is
+    called with a L{DialogueResponse} whose next_section_id == 'end'.
+    Processing can also be manually terminated by calling the L{endDialogue}
+    class method.
+    
+    @note: See the dialogue_demo.py script for a complete example of how the
+        L{DialogueProcessor} can be used.
+    
+    @ivar dialogue: dialogue data currently being processed.
+    @type dialogue: L{Dialogue}
+    @ivar dialogue_section_stack: sections of dialogue that have been or are
+        currently being processed.
+    @type dialogue_section_stack: list of L{DialogueSections<DialogueSection>}
+    @ivar game_state: objects defining the game state that should be made
+        available for testing L{DialogueResponse} conditionals.
+    @type game_state: dict of Python objects
+    @ivar in_dialogue: whether a dialogue has been initiated.
+    @type in_dialogue: Bool
+    
+    Usage:
+    >>> game_state = {'pc': player_character, 'quest': quest_engine}
+    >>> dialogue_processor = DialogueProcessor(dialogue, game_state)
+    >>> dialogue_processor.initiateDialogue()
+    >>> while dialogue_processor.in_dialogue:
+    ...     valid_responses = dialogue_processor.continueDialogue()
+    ...     response = choose_response(valid_responses)
+    ...     dialogue_processor.reply(response)
+    """
+    _logger = logging.getLogger('dialogueengine.DialogueProcessor')
+    
+    def dialogue():
+        def fget(self):
+            return self._dialogue
+        
+        def fset(self, dialogue):
+            assert isinstance(dialogue, Dialogue), \
+                '{0} does not implement Dialogue interface'.format(dialogue)
+            self._dialogue = dialogue
+        
+        return locals()
+    dialogue = property(**dialogue())
+    
+    def dialogue_section_stack():
+        def fget(self):
+            return self._dialogue_section_stack
+        
+        def fset(self, new_value):
+            assert isinstance(new_value, Sequence) and not \
+                   isinstance(new_value, basestring), \
+                   'dialogue_section_stack must be a Sequence, not {0}'\
+                   .format(new_value)
+            self._dialogue_section_stack = new_value
+        
+        return locals()
+    dialogue_section_stack = property(**dialogue_section_stack())
+    
+    def game_state():
+        def fget(self):
+            return self._game_state
+        
+        def fset(self, new_value):
+            assert isinstance(new_value, MutableMapping),\
+                   'game_state must be a MutableMapping, not {0}'\
+                   .format(new_value)
+            self._game_state = new_value
+        
+        return locals()
+    game_state = property(**game_state())
+    
+    def in_dialogue():
+        def fget(self):
+            return self._in_dialogue
+        
+        def fset(self, value):
+            assert isinstance(value, bool), '{0} is not a bool'.format(value)
+            self._in_dialogue = value
+        
+        return locals()
+    in_dialogue = property(**in_dialogue())
+    
+    def __init__(self, dialogue, game_state):
+        """
+        Initialize a new L{DialogueProcessor} instance.
+        
+        @param dialogue: dialogue data to process.
+        @type dialogue: L{Dialogue}
+        @param game_state: objects defining the game state that should be made
+            available for testing L{DialogueResponse} conditions.
+        @type game_state: dict of objects
+        """
+        self._dialogue_section_stack = []
+        self._dialogue = dialogue
+        self._game_state = game_state
+        self._in_dialogue = False
+    
+    def getDialogueGreeting(self):
+        """
+        Evaluate the L{RootDialogueSections<RootDialogueSection>} conditions
+        and return the valid L{DialogueSection} which should be displayed
+        first.
+        
+        @return: Valid root dialogue section.
+        @rtype: L{DialogueSection}
+        
+        @raise: RuntimeError - evaluation of a DialogueGreeting condition fails
+            by raising an exception (e.g. due to a syntax error).
+        """
+        dialogue = self.dialogue
+        dialogue_greeting = None
+        for greeting in dialogue.greetings:
+            try:
+                condition_met = eval(greeting.condition, self.game_state)
+            except Exception as exception:
+                error_message = dedent_chomp('''
+                    exception raised in DialogueGreeting {id} condition:
+                    {exception}
+                ''').format(id=greeting.id, exception=exception)
+                self._logger.error(error_message)
+            if (condition_met):
+                dialogue_greeting = greeting
+        if (dialogue_greeting is None):
+            dialogue_greeting = dialogue.default_greeting
+        
+        return dialogue_greeting
+    
+    def initiateDialogue(self):
+        """
+        Prepare the L{DialogueProcessor} to process the L{Dialogue} by pushing
+        the starting L{DialogueSection} onto the L{dialogue_section_stack}.
+        
+        @raise RuntimeError: Unable to determine the root L{DialogueSection}
+            defined by the L{Dialogue}.
+        """
+        if (self.in_dialogue):
+            self.endDialogue()
+        dialogue_greeting = self.getDialogueGreeting()
+        self.dialogue_section_stack.append(dialogue_greeting)
+        self.in_dialogue = True
+        self._logger.info('initiated dialogue {0}'.format(self.dialogue))
+    
+    def continueDialogue(self):
+        """
+        Process the L{DialogueSection} at the top of the
+        L{dialogue_section_stack}, run any L{DialogueActions<DialogueActions>}
+        it contains and return a list of valid
+        L{DialogueResponses<DialogueResponses> after evaluating any response
+        conditionals.
+        
+        @returns: valid responses.
+        @rtype: list of L{DialogueResponses<DialogueResponse>}
+        
+        @raise RuntimeError: Any preconditions are not met.
+        
+        @precondition: dialogue has been initiated via L{initiateDialogue}.
+        """
+        if (not self.in_dialogue):
+            error_message = dedent_chomp('''
+                dialogue has not be initiated via initiateDialogue yet
+            ''')
+            raise RuntimeError(error_message)
+        current_dialogue_section = self.getCurrentDialogueSection()
+        self.runDialogueActions(current_dialogue_section)
+        valid_responses = self.getValidResponses(current_dialogue_section)
+        
+        return valid_responses
+    
+    def getCurrentDialogueSection(self):
+        """
+        Return the L{DialogueSection} at the top of the
+        L{dialogue_section_stack}.
+        
+        @returns: section of dialogue currently being processed.
+        @rtype: L{DialogueSection}
+        
+        @raise RuntimeError: Any preconditions are not met.
+        
+        @precondition: dialogue has been initiated via L{initiateDialogue} and
+            L{dialogue_section_stack} contains at least one L{DialogueSection}.
+        """
+        if (not self.in_dialogue):
+            error_message = dedent_chomp('''
+                getCurrentDialogueSection called but the dialogue has not been
+                initiated yet
+            ''')
+            raise RuntimeError(error_message)
+        try:
+            current_dialogue_section = self.dialogue_section_stack[-1]
+        except IndexError:
+            error_message = dedent_chomp('''
+                getCurrentDialogueSection called but no DialogueSections are in
+                the stack
+            ''')
+            raise RuntimeError(error_message)
+        
+        return current_dialogue_section
+    
+    def runDialogueActions(self, dialogue_node):
+        """
+        Execute all L{DialogueActions<DialogueActions>} contained by a
+        L{DialogueSection} or L{DialogueResponse}.
+        
+        @param dialogue_node: section of dialogue or response containing the
+            L{DialogueActions<DialogueAction>} to execute.
+        @type dialogue_node: L{DialogueNode}
+        """
+        self._logger.info('processing commands for {0}'.format(dialogue_node))
+        for command in dialogue_node.actions:
+            try:
+                command(self.game_state)
+            except (Exception,) as error:
+                self._logger.error('failed to execute DialogueAction {0}: {1}'
+                                   .format(command.keyword, error))
+                # TODO Technomage 2010-11-18: Undo previous actions when an
+                #     action fails to execute.
+            else:
+                self._logger.debug('ran {0} with arguments {1}'
+                                   .format(getattr(type(command), '__name__'),
+                                           command.arguments))
+    
+    def getValidResponses(self, dialogue_section):
+        """
+        Evaluate all L{DialogueResponse} conditions for a L{DialogueSection}
+        and return a list of valid responses.
+        
+        @param dialogue_section: section of dialogue containing the
+            L{DialogueResponses<DialogueResponse>} to process.
+        @type dialogue_section: L{DialogueSection}
+        
+        @return: responses whose conditions were met.
+        @rtype: list of L{DialogueResponses<DialogueResponse>}
+        """
+        valid_responses = []
+        for dialogue_response in dialogue_section.responses:
+            condition = dialogue_response.condition
+            try:
+                condition_met = condition is None or \
+                                eval(condition, self.game_state)
+            except (Exception,) as exception:
+                error_message = dedent_chomp('''
+                    evaluation of condition {condition} for {response} failed
+                    with error: {exception}
+                ''').format(condition=dialogue_response.condition,
+                            response=dialogue_response, exception=exception)
+                self._logger.error(error_message)
+            else:
+                self._logger.debug(
+                    'condition "{0}" for {1} evaluated to {2}'
+                    .format(dialogue_response.condition, dialogue_response,
+                            condition_met)
+                )
+                if (condition_met):
+                    valid_responses.append(dialogue_response)
+        
+        return valid_responses
+    
+    def reply(self, dialogue_response):
+        """
+        Reply with a L{DialogueResponse}, execute the
+        L{DialogueActions<DialogueAction>} it contains and push the next
+        L{DialogueSection} onto the L{dialogue_section_stack}.
+        
+        @param dialogue_response: response to reply with.
+        @type dialogue_response: L{DialogueReponse}
+        
+        @raise RuntimeError: Any precondition is not met.
+        
+        @precondition: L{initiateDialogue} must be called before this method
+            is used.
+        """
+        if (not self.in_dialogue):
+            error_message = dedent_chomp('''
+                reply cannot be called until the dialogue has been initiated
+                via initiateDialogue
+            ''')
+            raise RuntimeError(error_message)
+        self._logger.info('replied with {0}'.format(dialogue_response))
+        # FIXME: Technomage 2010-12-11: What happens if runDialogueActions
+        #     raises an error?
+        self.runDialogueActions(dialogue_response)
+        next_section_id = dialogue_response.next_section_id
+        if (next_section_id == 'back'):
+            if (len(self.dialogue_section_stack) == 1):
+                error_message = dedent_chomp('''
+                    attempted to run goto: back action but stack does not
+                    contain a previous DialogueSection
+                ''')
+                raise RuntimeError(error_message)
+            else:
+                try:
+                    self.dialogue_section_stack.pop()
+                except (IndexError,):
+                    error_message = dedent_chomp('''
+                        attempted to run goto: back action but the stack was
+                        empty
+                    ''')
+                    raise RuntimeError(error_message)
+                else:
+                    self._logger.debug(
+                        'ran goto: back action, restored last DialogueSection'
+                    )
+        elif (next_section_id == 'end'):
+            self.endDialogue()
+            self._logger.debug('ran goto: end action, ended dialogue')
+        else:
+            try:
+                next_dialogue_section = \
+                    self.dialogue.sections[next_section_id]
+            except KeyError:
+                error_message = dedent_chomp('''
+                    {0} is not a recognized goto: action or DialogueSection
+                    identifier
+                ''').format(next_section_id)
+                raise RuntimeError(error_message)
+            else:
+                self.dialogue_section_stack.append(next_dialogue_section)
+    
+    def endDialogue(self):
+        """
+        End the current dialogue and clean up any resources in use by the
+        L{DialogueProcessor}.
+        """
+        self.dialogue_section_stack = []
+        self.in_dialogue = False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/font.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,54 @@
+import os
+
+from fife.extensions.pychan.fonts import Font
+from fife.extensions.pychan.internal import get_manager
+
+class PARPGFont(Font):
+    """ Font class for PARPG
+        This class behaves identical to PyChan's Font class except in
+        initialization. Ratherthan take a name and a get object, this class
+        takes a fontdef and settings object as explained below. This class is
+        necessary because the original Font class was too restrictive on how it
+        accepted objects
+
+        @param fontdef: defines the font's name, size, type, and optionally 
+                        row spacing as well as glyph spacing.
+        @type fontdef: dictionary
+        
+        @param settings: settings object used to dynamically determine the
+                         font's source location
+        @type settings: parpg.settings.Settings object
+    """
+    def __init__(self, fontdef, settings):
+        self.font = None
+        self.name = fontdef['name']
+        self.typename = fontdef['typename']
+
+        if self.typename == 'truetype':
+            self.filename = '{0}.ttf'.format(self.name.lower().split('_')[0])
+
+        self.source = os.path.join(settings.system_path,
+                                   settings.fife.FontsPath,
+                                   self.filename)
+        self.row_spacing = fontdef.get('row_spacing', 0)
+        self.glyph_spacing = fontdef.get('glyph_spacing', 0)
+
+        if self.typename == 'truetype':
+            self.size = fontdef['size']
+            self.antialias = fontdef['antialias']
+            self.color = fontdef.get('color', [255, 255, 255])
+            manager = get_manager().hook.engine.getGuiManager()
+            self.font = manager.createFont(self.source, self.size, '')
+
+            if not self.font:
+                raise InitializationError('Could not load font '
+                                          '{0}'.format(self.name))
+        
+            self.font.setAntiAlias(self.antialias)
+            self.font.setColor(*self.color)
+        else:
+            raise InitializationError('Unsupported font type '
+                                      '{0}'.format(self.typename))
+
+        self.font.setRowSpacing(self.row_spacing)
+        self.font.setGlyphSpacing(self.glyph_spacing)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gamemap.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,167 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+from fife import fife
+
+from fife.extensions.loaders import loadMapFile
+
+class GameMap(fife.MapChangeListener):
+    """Map class used to flag changes in the map"""
+    def __init__(self, engine, model):
+        # init mapchange listener
+        fife.MapChangeListener.__init__(self)
+        self.map = None
+        self.engine = engine
+        self.model = model
+
+        # init map attributes
+        self.my_cam_id = None
+        self.cameras = {}
+        self.agent_layer = None
+        self.top_layer = None
+        self.fife_model = engine.getModel()
+        self.transitions = []
+        self.cur_cam2_x = 0
+        self.initial_cam2_x = 0
+        self.cam2_scrolling_right = True
+        self.target_rotation = 0
+        self.outline_renderer = None
+        
+    def reset(self):
+        """Reset the model to default settings.
+           @return: None"""
+        # We have to delete the map in Fife.
+        if self.map:
+            self.model.deleteObjects()
+            self.model.deleteMap(self.map)
+        self.transitions = []
+        self.map = None
+        self.agent_layer = None        
+        self.top_layer = None
+        # We have to clear the cameras in the view as well, or we can't reuse
+        # camera names like 'main'
+        #self.view.clearCameras()
+        self.initial_cam2_x = 0
+        self.cam2_scrolling_right = True
+        #self.cameras = {}
+        self.cur_cam2_x = 0
+        self.target_rotation = 0
+        self.outline_renderer = None
+        
+    def makeActive(self):
+        """Makes this map the active one.
+           @return: None"""
+        self.cameras[self.my_cam_id].setEnabled(True)
+        
+    def load(self, filename):
+        """Load a map given the filename.
+           @type filename: String
+           @param filename: Name of map to load
+           @return: None"""
+        self.reset()
+        self.map = loadMapFile(filename, self.engine)
+        self.agent_layer = self.map.getLayer('ObjectLayer')
+        self.top_layer = self.map.getLayer('TopLayer')      
+            
+        # it's possible there's no transition layer
+        size = len('TransitionLayer')
+        for layer in self.map.getLayers():
+            # could be many layers, but hopefully no more than 3
+            if(layer.getId()[:size] == 'TransitionLayer'):
+                self.transitions.append(self.map.getLayer(layer.getId()))
+
+        """ Initialize the camera.
+        Note that if we have more than one camera in a map file
+        we will have to rework how self.my_cam_id works. To make sure
+        the proper camera is set as the 'main' camera.
+        At this point we also set the viewport to the current resolution."""
+        for cam in self.map.getCameras():
+            width = self.model.settings.fife.ScreenWidth
+            height = self.model.settings.fife.ScreenHeight
+            viewport = fife.Rect(0, 0, width, height)
+            cam.setViewPort(viewport)
+            self.my_cam_id = cam.getId()
+            self.cameras[self.my_cam_id] = cam
+            cam.resetRenderers()
+        
+        self.target_rotation = self.cameras[self.my_cam_id].getRotation()
+
+        self.outline_renderer = fife.InstanceRenderer.\
+                                        getInstance(
+                                                    self.cameras[
+                                                                 self.my_cam_id
+                                                                 ])
+
+        # set the render text
+        rend = fife.FloatingTextRenderer.getInstance(self.cameras[
+                                                                  self.my_cam_id
+                                                                  ])
+        text = self.engine.getGuiManager().\
+                        createFont('fonts/rpgfont.png', 0, \
+                                   self.model.settings.fife.FontGlyphs)
+        rend.changeDefaultFont(text)
+        rend.activateAllLayers(self.map)
+        rend.setEnabled(True)
+        
+        # Activate the grid renderer on all layers
+        rend = self.cameras['map_camera'].getRenderer('GridRenderer')
+        rend.activateAllLayers(self.map)
+         
+        # Activate the grid renderer on all layers
+        rend = fife.CoordinateRenderer.getInstance(self.cameras[
+                                                                  self.my_cam_id
+                                                                  ])
+        rend.setColor(0, 0, 0)
+        rend.addActiveLayer(self.map.getLayer("GroundLayer"))
+
+        # Make World aware that this is now the active map.
+        self.model.active_map = self
+
+    def addPC(self):
+        """Add the player character to the map
+           @return: None"""
+        # Update gamestate.player_character
+        self.model.game_state.player_character.behaviour.onNewMap(self.agent_layer)
+        self.centerCameraOnPlayer()
+
+    def toggleRenderer(self, r_name):
+        """Enable or disable a renderer.
+           @return: None"""
+        renderer = self.cameras[self.my_cam_id].getRenderer(str(r_name))
+        renderer.setEnabled(not renderer.isEnabled())
+
+    def isPaused(self):
+        """Returns wheter the map is currentply paused or not"""
+        # Time multiplier is a float, never do equals on floats
+        return not self.map.getTimeMultiplier() >= 1.0
+    
+    def pause(self, paused):
+        """ Pause/Unpause the game.
+        @return: nothing"""
+        if paused:
+            self.map.setTimeMultiplier(0.0)
+        if not paused and self.isPaused():
+            self.map.setTimeMultiplier(1.0)
+        
+    def togglePause(self):
+        """ Toggle paused state.
+        @return: nothing"""
+        self.pause(not self.isPaused())
+
+    def centerCameraOnPlayer(self):
+        """Center the camera on the player"""
+        camera = self.cameras[self.my_cam_id]
+        player_agent = self.model.game_state.player_character.behaviour.agent
+        camera.setLocation(player_agent.getLocation())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gamemodel.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,775 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+# there should be NO references to FIFE here!
+import sys
+import os.path
+import logging
+from copy import deepcopy
+
+from fife import fife
+from fife.extensions.serializers.xmlobject import XMLObjectLoader 
+
+from gamestate import GameState
+from objects import createObject
+from objects.composed import CarryableItem, CarryableContainer
+from gamemap import GameMap
+from common.utils import locateFiles
+from common.utils import parseBool
+from inventory import Inventory
+from parpg.dialogueparsers import YamlDialogueParser, DialogueFormatError
+
+try:
+    import xml.etree.cElementTree as ElementTree
+except ImportError:
+    import xml.etree.ElementTree as ElementTree
+
+import yaml
+
+logger = logging.getLogger('gamemodel')
+
+class GameModel(object):
+    """GameModel holds the logic for the game.
+       Since some data (object position and so forth) is held in the
+       fife, and would be pointless to replicate, we hold a instance of
+       the fife view here. This also prevents us from just having a
+       function heavy controller."""
+    ALL_AGENTS_KEY = "All"
+    MAX_ID_NUMBER = 1000
+    
+    def __init__(self, engine, settings):
+        """Initialize the instance.
+        @param engine: A fife.Engine object
+        @type emgome: fife.Engine 
+        @param setting: The applications settigns
+        @type setting: parpg.settings.Settings object
+        @return: None"""
+        self.settings = settings
+
+        self.map_change = False
+        self.load_saver = False
+        self.savegame = None
+        quests_directory = os.path.join(self.settings.system_path,
+                                        self.settings.parpg.QuestsPath)
+        self.game_state = GameState(quests_dir=quests_directory)
+        #self.game_state.quest_engine = 
+        #self.game_state.quest_engine.readQuests()
+        self.pc_run = 1
+        self.target_position = None
+        self.target_map_name = None
+        self.object_db = {}
+        self.active_map = None
+        self.map_files = {}
+        self.agents = {}
+        self.agents[self.ALL_AGENTS_KEY] = {}
+        self.engine = engine
+        self.fife_model = engine.getModel()
+
+        # set values from settings
+        maps_file = os.path.join(self.settings.system_path, 
+                                 self.settings.parpg.MapsPath,
+                                 self.settings.parpg.MapsFile)
+        self.game_state.maps_file = maps_file
+        all_agents_file = os.path.join(self.settings.system_path,
+                                       self.settings.parpg.MapsPath,
+                                       self.settings.parpg.AllAgentsFile)
+        self.all_agents_file = all_agents_file
+        objects_dir = os.path.join(self.settings.system_path,
+                                    self.settings.parpg.ObjectsPath)
+        self.objects_directory = objects_dir
+        object_db_file = os.path.join(self.objects_directory,
+                                      self.settings.parpg.ObjectDatabaseFile)
+        self.object_db_file = object_db_file
+        dialogues_dir = os.path.join(self.settings.system_path, 
+                                     self.settings.parpg.DialoguesPath)
+        self.dialogues_directory = dialogues_dir
+        self.dialogues = {}
+        self.agent_import_files = {}
+        self.obj_loader = XMLObjectLoader(
+                                          self.engine.getImagePool(), 
+                                          self.engine.getAnimationPool(), 
+                                          self.engine.getModel(),
+                                          self.engine.getVFS() 
+                                          )
+
+    def checkAttributes(self, attributes):
+        """Checks for attributes that where not given in the map file
+        and fills them with values from the object database
+        @param attributes: attributes to check
+        @type attributes: Dictionary
+        @return: The modified attributes""" 
+        if attributes.has_key("object_type"):
+            class_name = attributes.pop("object_type")
+        else:
+            class_name = attributes["type"]
+        if not attributes.has_key("type"):
+            attributes["type"] = class_name
+        if self.object_db.has_key(class_name):
+            db_attributes = deepcopy(self.object_db[class_name])
+            for key in db_attributes.keys():
+                if attributes.has_key(key):
+                    attributes[key] = attributes[key] or db_attributes[key]
+                else:
+                    attributes[key] = db_attributes[key]
+        return attributes
+    
+    def isIDUsed(self, ID):
+        if self.game_state.hasObject(ID):
+            return True
+        for namespace in self.agents:
+            if ID in self.agents[namespace]:
+                return True
+        return False
+    
+    def createUniqueID(self, ID):
+        if self.isIDUsed(ID):
+            id_number = 1
+            while self.isIDUsed(ID + "_" + str(id_number)):
+                id_number += 1
+                if id_number > self.MAX_ID_NUMBER:
+                    raise ValueError(
+                        "Number exceeds MAX_ID_NUMBER:" + str(self.MAX_ID_NUMBER))
+            
+            ID = ID + "_" + str(id_number)
+        return ID
+
+    def createContainerItems(self, container_objs):
+        """Create the items of a container from a dictionary
+        @param container_objs: Dictionary containing the items
+        @type container_objs: dict"""
+        items = []
+        for container_obj in container_objs:
+            items.append(self.createContainerObject(container_obj))
+        
+        return items
+
+    def createContainerObject(self, attributes):
+        """Create an object that can be stored in 
+        an container and return it
+        @param attributes: Dictionary of all object attributes
+        @type attributes: Dictionary
+        @return: The created object """
+        # create the extra data
+        extra = {}
+        extra['controller'] = self
+        attributes = self.checkAttributes(attributes)
+        
+        info = {}
+        info.update(attributes)
+        info.update(extra)
+        ID = info.pop("id") if info.has_key("id") else info.pop("ID")
+        if not info.has_key("item_type"):
+            info["item_type"] = info["type"]
+        ID = self.createUniqueID(ID)
+        if info.has_key("attributes"):
+            attributes = info["attributes"]
+            if "Container" in attributes:
+                info["actions"]["Open"] = ""
+                if info.has_key("Items"):
+                    inventory_objs = info["Items"]
+                    info["items"] = self.createContainerItems(inventory_objs)
+                
+                new_item = CarryableContainer(ID = ID, **info) 
+            else:
+                new_item = CarryableItem(ID = ID, **info) 
+        else:
+            new_item = CarryableItem(ID = ID, **info) 
+        self.game_state.addObject(None, new_item)
+        return new_item
+      
+    def createInventoryObject(self, container, attributes):
+        """Create an inventory object and place it into a container
+           @type container: base.Container
+           @param container: Container where the item is on
+           @type attributes: Dictionary
+           @param attributes: Dictionary of all object attributes
+           @return: None"""
+        index = attributes.pop("index") if attributes.has_key("index") else None
+        slot = attributes.pop("slot") if attributes.has_key("slot") else None
+        obj = self.createContainerObject(attributes)        
+        #obj = createObject(attributes, extra)
+        if slot:
+            container.moveItemToSlot(obj, slot)
+        else:
+            container.placeItem(obj, index)
+    
+    def deleteObject(self, object_id):
+        """Removes an object from the game
+        @param object_id: ID of the object
+        @type object_id: str """
+        del self.agents["All"][object_id]
+        self.game_state.deleteObject(object_id)
+        
+    def save(self, path, filename):
+        """Writes the saver to a file.
+           @type filename: string
+           @param filename: the name of the file to write to
+           @return: None"""
+        fname = '/'.join([path, filename])
+        try:
+            save_file = open(fname, 'w')
+        except(IOError):
+            sys.stderr.write("Error: Can't create save game: " + fname + "\n")
+            return
+        save_state = {}
+        save_state["Agents"] = {}
+        for map_name in self.agents:
+            if map_name == self.ALL_AGENTS_KEY:
+                continue
+            agents_dict = {}
+            for agent in self.agents[map_name]:
+                agent_obj = self.game_state.getObjectById(agent, map_name)
+                agent_inst = self.game_state.maps[map_name].\
+                                    agent_layer.getInstance(agent)
+                agent_dict = self.agents[map_name][agent]
+                agent_dict.update(agent_obj.getStateForSaving())
+                agent_dict["Rotation"] = agent_inst.getRotation()
+                agents_dict[agent] = agent_dict
+            save_state["Agents"][map_name] = agents_dict
+        agents_dict = {}
+        for agent in self.agents["All"]:
+            map_name = self.agents["All"][agent]["Map"]
+            agent_dict = self.agents["All"][agent]
+            agent_obj = None
+            if agent == "PlayerCharacter":
+                agent_obj = self.game_state.player_character
+            else:
+                agent_obj = self.game_state.getObjectById(agent, map_name)
+            if agent_obj:
+                agent_inst = self.game_state.maps[map_name].\
+                                    agent_layer.getInstance(agent)
+                agent_dict.update(agent_obj.getStateForSaving())
+                agent_dict["Rotation"] = agent_inst.getRotation()
+                agent_dict["MapName"] = map_name
+            agents_dict[agent] = agent_dict
+        save_state["Agents"]["All"] = agents_dict
+        save_state["GameState"] = self.game_state.getStateForSaving()
+        yaml.dump(save_state, save_file)
+        
+        save_file.close()       
+
+    def load(self, path, filename):
+        """Loads a saver from a file.
+           @type filename: string
+           @param filename: the name of the file (including path) to load from
+           @return: None"""
+        fname = '/'.join([path, filename])
+
+        try:
+            load_file = open(fname, 'r')
+        except(IOError):
+            sys.stderr.write("Error: Can't find save game file\n")
+            return        
+        self.deleteMaps()
+        self.clearAgents()
+        
+        save_state = yaml.load(load_file)
+        self.game_state.restoreFromState(save_state["GameState"])
+        maps = save_state["Agents"]
+        for map_name in maps:
+            for agent_name in maps[map_name]:
+                agent = {agent_name:maps[map_name][agent_name]}
+                self.addAgent(map_name, agent)
+                
+        # Load the current map
+        if self.game_state.current_map_name:
+            self.loadMap(self.game_state.current_map_name)         
+        load_file.close()
+        
+
+        # Recreate all the behaviours. These can't be saved because FIFE
+        # objects cannot be pickled
+        
+        self.placeAgents()
+        self.placePC()
+      
+        # In most maps we'll create the PlayerCharacter Instance internally. 
+        # In these cases we need a target position
+         
+    def teleport(self, agent, position):
+        """Called when a an agent is moved instantly to a new position. 
+        The setting of position may wan to be created as its own method down the road.
+        @type position: String Tuple
+        @param position: X,Y coordinates passed from engine.changeMap
+        @return: fife.Location"""
+        logging.debug(position)
+        coord = fife.DoublePoint3D(float(position[0]), float(position[1]), 0)
+        location = fife.Location(self.active_map.agent_layer)
+        location.setMapCoordinates(coord)
+        agent.teleport(location)         
+               
+    def getObjectAtCoords(self, coords):
+        """Get the object which is at the given coords
+        @type coords: fife.Screenpoint
+        @param coords: Coordinates where to check for an object
+        @rtype: fife.Object
+        @return: An object or None"""
+        instances = self.active_map.cameras[
+                                            self.active_map.my_cam_id].\
+            getMatchingInstances(coords, self.active_map.agent_layer)
+        # no object returns an empty tuple
+        if(instances != ()):
+            front_y = 0
+            
+
+            for obj in instances:
+                # check to see if this in our list at all
+                if(self.objectActive(obj.getId())):
+                    # check if the object is on the foreground
+                    obj_map_coords = \
+                                      obj.getLocation().getMapCoordinates()
+                    obj_screen_coords = self.active_map.\
+                        cameras[self.active_map.my_cam_id]\
+                        .toScreenCoordinates(obj_map_coords)
+
+                    if obj_screen_coords.y > front_y:
+                        #Object on the foreground
+                        front_y = obj_screen_coords.y
+                        return obj
+                    else:
+                        return None
+        else:
+            return None
+
+    def getCoords(self, click):
+        """Get the map location x, y coordinates from the screen coordinates
+           @type click: fife.ScreenPoint
+           @param click: Screen coordinates
+           @rtype: fife.Location
+           @return: The map coordinates"""
+        coord = self.active_map.cameras[self.active_map.my_cam_id].\
+                    toMapCoordinates(click, False)
+        coord.z = 0
+        location = fife.Location(self.active_map.agent_layer)
+        location.setMapCoordinates(coord)
+        return location
+
+    def pause(self, paused):
+        """ Pause/Unpause the game
+        @return: nothing"""
+        if self.active_map:
+            self.active_map.pause(paused)
+    
+    def togglePause(self):
+        """ Toggle paused state.
+        @return: nothing"""
+        self.active_map.togglePause()
+        
+    def isPaused(self):
+        """Returns wheter the game is paused or not"""
+        return self.active_map.isPaused()
+    
+    def readMapFiles(self):
+        """Read all a available map-files and store them"""
+        maps_data = file(self.game_state.maps_file)
+        self.map_files = yaml.load(maps_data)["Maps"]
+    
+    def addAgent(self, namespace, agent):
+        """Adds an agent to the agents dictionary
+        @param namespace: the namespace where the agent is to be added to
+        @type namespace: str
+        @param agent: The agent to be added
+        @type agent: dict """
+        from fife.extensions.serializers.xml_loader_tools import loadImportFile
+        if not self.agents.has_key(namespace):
+            self.agents[namespace] = {}
+            
+        agent_values = agent.values()[0]
+        unique_agent_id = self.createUniqueID(agent.keys()[0])
+        del agent[agent.keys()[0]]
+        agent[unique_agent_id] = agent_values
+        self.agents[namespace].update(agent)
+        object_model = ""
+        if agent_values.has_key("ObjectModel"): 
+            object_model =  agent_values["ObjectModel"]
+        elif agent_values["ObjectType"] == "MapItem":
+            object_data = self.object_db[agent_values["ItemType"]]
+            object_model = object_data["gfx"] if object_data.has_key("gfx") \
+                        else "generic_item"
+        else:
+            object_model = self.object_db[agent_values["ObjectType"]]["gfx"]
+        import_file = self.agent_import_files[object_model]
+        loadImportFile(self.obj_loader, import_file, self.engine)
+        
+    def readAgentsOfMap(self, map_name):
+        """Read the agents of the map
+        @param map_name: Name of the map
+        @type map_name: str """
+        #Get the agents of the map        
+        map_agents_file = self.map_files[map_name].\
+                            replace(".xml", "_agents.yaml")   
+        agents_data = file(map_agents_file)
+        agents = yaml.load_all(agents_data)
+        for agent in agents:
+            if not agent == None:
+                self.addAgent(map_name, agent)  
+    
+    def readAllAgents(self):
+        """Read the agents of the all_agents_file and store them"""
+        agents_data = file(self.all_agents_file)
+        agents = yaml.load_all(agents_data)
+        for agent in agents:
+            if not agent == None:
+                self.addAgent(self.ALL_AGENTS_KEY, agent)  
+                
+    def getAgentsOfMap(self, map_name):
+        """Returns the agents that are on the given map
+        @param map_name: Name of the map
+        @type map_name: str
+        @return: A dictionary with the agents of the map"""
+        if not self.agents.has_key(map_name):
+            return {}
+        ret_dict = self.agents[map_name].copy()
+        for agent_name, agent_value in self.agents[self.ALL_AGENTS_KEY]\
+                                                .iteritems():
+            if agent_value["Map"] == map_name:
+                ret_dict[agent_name] = agent_value
+        return ret_dict
+                
+    def getAgentsOfActiveMap(self):
+        """Returns the agents that are on active map
+        @return: A dictionary with the agents of the map """
+        return self.getAgentsOfMap(self.active_map.map.getId())
+
+    def clearAgents(self):
+        """Resets the agents dictionary"""
+        self.agents = {}
+        self.agents[self.ALL_AGENTS_KEY] = {}
+    
+    def loadMap(self, map_name):
+        """Load a new map.
+           @type map_name: string
+           @param map_name: Name of the map to load
+           @return: None"""
+        if not map_name in self.game_state.maps:  
+            map_file = self.map_files[map_name]
+            new_map = GameMap(self.engine, self)
+            self.game_state.maps[map_name] = new_map
+            new_map.load(map_file)    
+
+    def createAgent(self, agent, inst_id):
+        object_type = agent["ObjectType"]
+        object_id = agent["ObjectModel"] \
+                                if agent.has_key("ObjectModel") \
+                                else None
+        if object_id == None:
+            if object_type == "MapItem":
+                object_data = self.object_db[agent["ItemType"]]
+                object_id = object_data["gfx"] if object_data.has_key("gfx") \
+                            else "generic_item"
+            else:
+                object_id = self.object_db[object_type]["gfx"]
+        map_obj = self.fife_model.getObject(str(object_id), "PARPG")
+        if not map_obj:
+            logging.warning("Object with inst_id={0}, ns=PARPG, "
+                                  "could not be found. "
+                                  "Omitting...".format(str(obj_id)))
+
+        x_pos = agent["Position"][0]
+        y_pos = agent["Position"][1]
+        z_pos = agent["Position"][2] if len(agent["Position"]) == 3 \
+                                        else -0.1 if object_type == "MapItem" \
+                                        else 0.0  
+        stack_pos = agent["Stackposition"] if \
+                        agent.has_key("StackPosition") \
+                        else None
+        inst = self.active_map.agent_layer.\
+                        createInstance(map_obj,
+                                       fife.ExactModelCoordinate(x_pos, 
+                                                                 y_pos, 
+                                                                 z_pos),
+                                       inst_id)
+        inst.setId(inst_id)
+
+        rotation = agent["Rotation"]
+        inst.setRotation(rotation)
+
+        fife.InstanceVisual.create(inst)
+        if (stack_pos):
+            inst.get2dGfxVisual().setStackPosition(int(stack_pos))
+
+        if (map_obj.getAction('default')):
+            target = fife.Location(self.active_map.agent_layer)
+            inst.act('default', target, True)
+            
+        inst_dict = {}
+        inst_dict["id"] = inst_id
+        inst_dict["type"] = object_type
+        inst_dict["xpos"] = x_pos
+        inst_dict["ypos"] = y_pos
+        inst_dict["gfx"] = object_id
+        inst_dict["is_open"] = parseBool(agent["Open"]) \
+                                if agent.has_key("Open") \
+                                else False
+        inst_dict["locked"] = parseBool(agent["Locked"]) \
+                                if agent.has_key("Locked") \
+                                else False
+        inst_dict["name"] = agent["ViewName"]
+        inst_dict["real_name"] = agent["RealName"] \
+                                    if agent.has_key("RealName") \
+                                    else agent["ViewName"]
+        inst_dict["text"] = agent["Text"] \
+                                    if agent.has_key("Text") \
+                                    else None
+        if self.dialogues.has_key(inst_id):
+            inst_dict["dialogue"] = self.dialogues[inst_id]
+        inst_dict["target_map_name"] = agent["TargetMap"] \
+                                        if agent.\
+                                            has_key("TargetMap") \
+                                        else None
+        inst_dict["target_x"] = agent["TargetPosition"][0] \
+                                    if agent.\
+                                        has_key("TargetPosition") \
+                                    else None
+        inst_dict["target_y"] = agent["TargetPosition"][1] \
+                                    if agent.\
+                                        has_key("TargetPosition") \
+                                    else None
+        if agent.has_key("Inventory"):
+            inventory = Inventory()
+            inventory_objs = agent["Inventory"]
+            for inventory_obj in inventory_objs:
+                self.createInventoryObject(inventory,
+                                           inventory_obj 
+                                           )
+            inst_dict["inventory"] = inventory
+
+        if agent.has_key("Items"):
+            container_objs = agent["Items"]
+            items = self.createContainerItems(container_objs)
+            inst_dict["items"] = items
+            
+        if agent.has_key("ItemType"):
+            if not agent.has_key("item"):
+                item_data = {}
+                item_data["type"] = agent["ItemType"]
+                item_data["ID"] = inst_id 
+                item_data = self.createContainerObject(item_data)
+            else:
+                item_data = agent["item"]
+            inst_dict["item"] = item_data
+            inst_dict["item_type"] = agent["ItemType"]
+
+        self.createMapObject(self.active_map.agent_layer, inst_dict)
+    
+    def placeAgents(self):
+        """Places the current maps agents """
+        if not self.active_map:
+            return
+        agents = self.getAgentsOfMap(self.game_state.current_map_name)
+        for agent in agents:
+            if agent == "PlayerCharacter":
+                continue
+            if self.active_map.agent_layer.getInstances(agent):
+                continue
+            self.createAgent(agents[agent], agent)
+
+    def placePC(self):
+        """Places the PlayerCharacter on the map"""
+        agent = self.agents[self.ALL_AGENTS_KEY]["PlayerCharacter"]
+        inst_id = "PlayerCharacter"
+        self.createAgent(agent, inst_id)
+        
+        # create the PlayerCharacter agent
+        self.active_map.addPC()
+        self.game_state.player_character.start()
+        if agent.has_key("PeopleKnown"):
+            self.game_state.player_character.people_i_know = agent["PeopleKnown"]
+                      
+    def changeMap(self, map_name, target_position = None):
+        """Registers for a map change on the next pump().
+           @type map_name: String
+           @param map_name: Id of the map to teleport to
+           @type map_file: String
+           @param map_file: Filename of the map to teleport to
+           @type target_position: Tuple
+           @param target_position: Position of PlayerCharacter on target map.
+           @return None"""
+        # set the parameters for the map change if moving to a new map
+        if map_name != self.game_state.current_map_name:
+            self.target_map_name = map_name
+            self.target_position = target_position
+            # issue the map change
+            self.map_change = True
+
+    def deleteMaps(self):
+        """Clear all currently loaded maps from FIFE as well as clear our
+            local map cache
+            @return: nothing"""
+        self.engine.getModel().deleteMaps()
+        self.engine.getModel().deleteObjects()
+        self.game_state.clearObjects()
+        self.game_state.maps = {}
+        
+    def setActiveMap(self, map_name):
+        """Sets the active map that is to be rendered.
+           @type map_name: String
+           @param map_name: The name of the map to load
+           @return: None"""
+        # Turn off the camera on the old map before we turn on the camera
+        # on the new map.
+        self.active_map.cameras[self.active_map.my_cam_id].setEnabled(False)
+        # Make the new map active.
+        self.active_map = self.game_state.maps[map_name]
+        self.active_map.makeActive()
+        self.game_state.current_map_name = map_name
+
+    def createMapObject (self, layer, attributes):
+        """Create an object and add it to the current map.
+           @type layer: fife.Layer
+           @param layer: FIFE layer object exists in
+           @type attributes: Dictionary
+           @param attributes: Dictionary of all object attributes
+           @type instance: fife.Instance
+           @param instance: FIFE instance corresponding to the object
+           @return: None"""
+        # create the extra data
+        extra = {}
+        if layer is not None:
+            extra['agent_layer'] = layer
+        attributes = self.checkAttributes(attributes)
+        
+        obj = createObject(attributes, extra)
+        
+        if obj.trueAttr("PC"):
+            self.addPC(layer, obj)
+        else:
+            self.addObject(layer, obj) 
+
+    def addPC(self, layer, player_char):
+        """Add the PlayerCharacter to the map
+           @type layer: fife.Layer
+           @param layer: FIFE layer object exists in
+           @type player_char: PlayerCharacter
+           @param player_char: PlayerCharacter object
+           @type instance: fife.Instance
+           @param instance: FIFE instance of PlayerCharacter
+           @return: None"""
+        # For now we copy the PlayerCharacter, 
+        # in the future we will need to copy
+        # PlayerCharacter specifics between the different PlayerCharacter's
+        self.game_state.player_character = player_char
+        self.game_state.player_character.setup()        
+        self.game_state.player_character.behaviour.speed = self.settings.parpg.PCSpeed
+
+
+    def addObject(self, layer, obj):
+        """Adds an object to the map.
+           @type layer: fife.Layer
+           @param layer: FIFE layer object exists in
+           @type obj: GameObject
+           @param obj: corresponding object class
+           @type instance: fife.Instance
+           @param instance: FIFE instance of object
+           @return: None"""
+        ref = self.game_state.getObjectById(obj.ID, \
+                                            self.game_state.current_map_name) 
+        if ref is None:
+            # no, add it to the game state
+            self.game_state.addObject(self.game_state.current_map_name, obj)
+        else:
+            # yes, use the current game state data
+            obj.X = ref.X
+            obj.Y = ref.Y
+            obj.gfx = ref.gfx  
+             
+        if obj.trueAttr("NPC"):
+            # create the agent
+            obj.setup()
+            obj.behaviour.speed = self.settings.parpg.PCSpeed - 1
+            # create the PlayerCharacter agent
+            obj.start()
+        if obj.trueAttr("AnimatedContainer"):
+            # create the agent
+            obj.setup()
+
+    def objectActive(self, ident):
+        """Given the objects ID, pass back the object if it is active,
+           False if it doesn't exist or not displayed
+           @type ident: string
+           @param ident: ID of object
+           @rtype: boolean
+           @return: Status of result (True/False)"""
+        for game_object in \
+           self.game_state.getObjectsFromMap(self.game_state.current_map_name):
+            if (game_object.ID == ident):
+                # we found a match
+                return game_object
+        # no match
+        return False    
+
+    def movePlayer(self, position):
+        """Code called when the player should move to another location
+           @type position: fife.ScreenPoint
+           @param position: Screen position to move to
+           @return: None"""
+        if(self.pc_run == 1):
+            self.game_state.player_character.run(position)
+        else:
+            self.game_state.player_character.walk(position)
+        
+    def teleportAgent(self, agent, position):
+        """Code called when an agent should teleport to another location
+           @type position: fife.ScreenPoint
+           @param position: Screen position to teleport to
+           @return: None"""
+        agent.teleport(position)
+        self.agents[agent.ID]["Position"] = position
+
+    def readObjectDB(self):
+        """Reads the Object Information Database from a file. """
+        database_file = file(self.object_db_file, "r")
+        database = yaml.load_all(database_file)
+        for object_info in database:
+            self.object_db.update(object_info)
+
+    def getAgentImportFiles(self):
+        """Searches the agents directory for import files """
+        files = locateFiles("*.xml", self.objects_directory)
+        for xml_file in files:
+            xml_file = os.path.relpath(xml_file).replace("\\", "/")
+            try:
+                root = ElementTree.parse(xml_file).getroot()
+                if root.tag == "object":
+                    self.agent_import_files[root.attrib["id"]] = xml_file
+            except SyntaxError as error:
+                assert(isinstance(error, SyntaxError))
+                logging.critical("Error parsing file {0}: "
+                                       "{1}".format(xml_file, error.msg))
+                sys.exit(1)
+    
+    def getDialogues(self):
+        """Searches the dialogue directory for dialogues """
+        files = locateFiles("*.yaml", self.dialogues_directory)
+        dialogue_parser = YamlDialogueParser()
+        for dialogue_filepath in files:
+            dialogue_filepath = os.path.relpath(dialogue_filepath) \
+                                .replace("\\", "/")
+            # Note Technomage 2010-11-13: the new DialogueEngine uses its own
+            #     parser now, YamlDialogueParser.
+#            dialogues = yaml.load_all(file(dialogue_file, "r"))
+            with file(dialogue_filepath, 'r') as dialogue_file:
+                try:
+                    dialogue = dialogue_parser.load(dialogue_file)
+                except (DialogueFormatError,) as error:
+                    logging.error('unable to load dialogue file {0}: {1}'
+                                  .format(dialogue_filepath, error))
+                else:
+                    self.dialogues[dialogue.npc_name] = dialogue
+            # Note Technomage 2010-11-13: the below code is used to load
+            #     multiple dialogues from a single file. Is this functionality
+            #     used/necessary?
+#            for dialogue in dialogues:
+#                self.dialogues[dialogue["NPC"]] = dialogue
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gamescenecontroller.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,461 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""This file contains the GameSceneController that handles input when the game
+   is exploring a scene"""
+
+
+from datetime import datetime
+import random
+import glob
+import os
+
+from fife import fife
+from fife import extensions
+
+from controllerbase import ControllerBase
+from parpg.gui.hud import Hud
+from parpg.gui import drag_drop_data as data_drag
+from objects.action import ChangeMapAction, ExamineAction, OpenBoxAction, \
+                           UnlockBoxAction, LockBoxAction, TalkAction, \
+                           PickUpAction, DropItemAction
+
+#For debugging/code analysis
+if False:
+    from gamesceneview import GameSceneView
+    from gamemodel import GameModel
+    from parpg import PARPGApplication
+
+import logging
+
+logger = logging.getLogger('gamescenecontroller')
+
+class GameSceneController(ControllerBase):
+    '''
+    This controller handles inputs when the game is in "scene" state.
+    "Scene" state is when the player can move around and interact
+    with objects. Like, talking to a npc or examining the contents of a box. 
+    '''
+
+
+    def __init__(self, engine, view, model, application):
+        '''
+        Constructor
+        @param engine: Instance of the active fife engine
+        @type engine: fife.Engine
+        @param view: Instance of a GameSceneView
+        @param type: parpg.GameSceneView
+        @param model: The model that has the current gamestate
+        @type model: parpg.GameModel
+        @param application: The application that created this controller
+        @type application: parpg.PARPGApplication
+        @param settings: The current settings of the application
+        @type settings: fife.extensions.fife_settings.Setting
+        '''
+        ControllerBase.__init__(self,
+                                engine,
+                                view,
+                                model,
+                                application)
+        #this can be helpful for IDEs code analysis
+        if False:
+            assert(isinstance(self.engine, fife.Engine))
+            assert(isinstance(self.view, GameSceneView))
+            assert(isinstance(self.view, GameModel))
+            assert(isinstance(self.application, PARPGApplication))
+            assert(isinstance(self.event_manager, fife.EventManager))
+        
+        # Last saved mouse coords        
+        self.action_number = 1
+
+        self.has_mouse_focus = True
+        self.last_mousecoords = None
+        self.mouse_callback = None
+        self.original_cursor_id = self.engine.getCursor().getId()        
+        self.scroll_direction = [0, 0]
+        self.scroll_timer = extensions.fife_timer.Timer(100,
+                                          lambda: self.view.moveCamera \
+                                                   (self.scroll_direction))    
+        
+        #this is temporary until we can set the native cursor
+        self.resetMouseCursor()
+        self.paused = False
+
+        if model.settings.fife.EnableSound:
+            if not self.view.sounds.music_init:
+                music_file = random.choice(glob.glob(os.path.join(
+                                                                  "music", 
+                                                                  "*.ogg")))
+                self.view.sounds.playMusic(music_file) 
+        self.initHud()
+                
+
+    def initHud(self):
+        """Initialize the hud member
+        @return: None"""
+        hud_callbacks = {
+            'saveGame': self.saveGame,
+            'loadGame': self.loadGame,
+            'quitGame': self.quitGame,
+        }
+        self.view.hud = Hud(self, 
+                            self.model.settings, 
+                            hud_callbacks)
+
+    def keyPressed(self, evt):
+        """Whenever a key is pressed, fife calls this routine.
+           @type evt: fife.event
+           @param evt: The event that fife caught
+           @return: None"""
+        key = evt.getKey()
+        key_val = key.getValue()
+
+        if(key_val == key.Q):
+            # we need to quit the game
+            self.view.hud.quitGame()
+        if(key_val == key.T):
+            self.model.active_map.toggleRenderer('GridRenderer')
+        if(key_val == key.F1):
+            # display the help screen and pause the game
+            self.view.hud.displayHelp()
+        if(key_val == key.F5):
+            self.model.active_map.toggleRenderer('CoordinateRenderer')
+        if(key_val == key.F7):
+            # F7 saves a screenshot to screenshots directory
+
+            settings = self.model.settings
+            screenshot_directory = os.path.join(settings.user_path,
+                                           settings.parpg.ScreenshotsPath)
+            # try to create the screenshots directory
+            try:
+                os.mkdir(screenshot_directory)
+            #TODO: distinguish between already existing permissions error
+            except OSError:
+                logger.warning("screenshot directory wasn't created.")
+
+            screenshot_file = os.path.join(screenshot_directory,
+                                           'screen-{0}.png'.format(
+                                           datetime.now().strftime(
+                                           '%Y-%m-%d-%H-%M-%S')))
+            self.engine.getRenderBackend().captureScreen(screenshot_file)
+            logger.info("PARPG: Saved: {0}".format(screenshot_file))
+        if(key_val == key.F10):
+            # F10 shows/hides the console
+            self.engine.getGuiManager().getConsole().toggleShowHide()
+        if(key_val == key.C):
+            # C opens and closes the character screen.
+            self.view.hud.toggleCharacterScreen()
+        if(key_val == key.I):
+            # I opens and closes the inventory
+            self.view.hud.toggleInventory()
+        if(key_val == key.A):
+            # A adds a test action to the action box
+            # The test actions will follow this format: Action 1,
+            # Action 2, etc.
+            self.view.hud.addAction("Action " + str(self.action_number))
+            self.action_number += 1
+        if(key_val == key.ESCAPE):
+            # Escape brings up the main menu
+            self.view.hud.displayMenu()
+            # Hide the quit menu
+            self.view.hud.quit_window.hide()
+        if(key_val == key.M):
+            self.view.sounds.toggleMusic()
+        if(key_val == key.PAUSE):
+            # Pause pause/unpause the game 
+            self.model.togglePause()
+            self.pause(False)
+        if(key_val == key.SPACE):
+            self.model.active_map.centerCameraOnPlayer() 
+    
+    def mouseReleased(self, evt):
+        """If a mouse button is released, fife calls this routine.
+           We want to wait until the button is released, because otherwise
+           pychan captures the release if a menu is opened.
+           @type evt: fife.event
+           @param evt: The event that fife caught
+           @return: None"""
+        self.view.hud.hideContextMenu()
+        scr_point = fife.ScreenPoint(evt.getX(), evt.getY())
+        if(evt.getButton() == fife.MouseEvent.LEFT):
+            if(data_drag.dragging):
+                coord = self.model.getCoords(scr_point)\
+                                    .getExactLayerCoordinates()
+                commands = ({"Command": "ResetMouseCursor"}, 
+                            {"Command": "StopDragging"})
+                self.model.game_state.player_character.approach([coord.x, 
+                                                                 coord.y],
+                                    DropItemAction(self, 
+                                                   data_drag.dragged_item, 
+                                                   commands))
+            else:
+                self.model.movePlayer(self.model.getCoords(scr_point))
+        elif(evt.getButton() == fife.MouseEvent.RIGHT):
+            # is there an object here?
+            tmp_active_map = self.model.active_map
+            instances = tmp_active_map.cameras[tmp_active_map.my_cam_id].\
+                            getMatchingInstances(scr_point,
+                                                 tmp_active_map.agent_layer)
+            info = None
+            for inst in instances:
+                # check to see if this is an active item
+                if(self.model.objectActive(inst.getId())):
+                    # yes, get the model
+                    info = self.getItemActions(inst.getId())
+                    break
+
+            # take the menu items returned by the engine or show a
+            # default menu if no items
+            data = info or \
+                [["Walk", "Walk here", self.view.onWalk, 
+                  self.model.getCoords(scr_point)]]
+            # show the menu
+            self.view.hud.showContextMenu(data, (scr_point.x, scr_point.y))
+    
+        
+    def updateMouse(self):
+        """Updates the mouse values"""
+        if self.paused:
+            return
+        cursor = self.engine.getCursor()
+        #this can be helpful for IDEs code analysis
+        if False:
+            assert(isinstance(cursor, fife.Cursor))
+        self.last_mousecoords = fife.ScreenPoint(cursor.getX(), 
+                                                 cursor.getY())        
+        self.view.highlightFrontObject(self.last_mousecoords)       
+        
+        #set the trigger area in pixles
+        pixle_edge = 20
+        
+        mouse_x = self.last_mousecoords.x
+        screen_width = self.model.engine.getSettings().getScreenWidth()
+        mouse_y = self.last_mousecoords.y
+        screen_height = self.model.engine.getSettings().getScreenHeight()
+        
+        image = None
+        settings = self.model.settings
+        
+        
+        #edge logic
+        self.scroll_direction = [0, 0]
+        if self.has_mouse_focus:
+            direction = self.scroll_direction
+            #up
+            if mouse_y <= pixle_edge: 
+                direction[0] += 1
+                direction[1] -= 1
+                image = os.path.join(settings.system_path,
+                                     settings.parpg.GuiPath,
+                                     settings.parpg.CursorPath,
+                                     settings.parpg.CursorUp)
+                
+            #right
+            if mouse_x >= screen_width - pixle_edge:
+                direction[0] += 1
+                direction[1] += 1
+                image = os.path.join(settings.system_path,
+                                     settings.parpg.GuiPath,
+                                     settings.parpg.CursorPath,
+                                     settings.parpg.CursorRight)
+                
+            #down
+            if mouse_y >= screen_height - pixle_edge:
+                direction[0] -= 1
+                direction[1] += 1
+                image = os.path.join(settings.system_path,
+                                     settings.parpg.GuiPath,
+                                     settings.parpg.CursorPath,
+                                     settings.parpg.CursorDown)
+                
+            #left
+            if mouse_x <= pixle_edge:
+                direction[0] -= 1
+                direction[1] -= 1
+                image = os.path.join(settings.system_path,
+                                     settings.parpg.GuiPath,
+                                     settings.parpg.CursorPath,
+                                     settings.parpg.CursorLeft)
+            
+            if image is not None and not data_drag.dragging:
+                self.setMouseCursor(image, image)
+       
+
+    def handleCommands(self):
+        """Check if a command is to be executed
+        """
+        if self.model.map_change:
+            self.pause(True)
+            if self.model.active_map:
+                player_char = self.model.game_state.player_character
+                self.model.game_state.player_character = None
+                pc_agent = self.model.agents[self.model.ALL_AGENTS_KEY]\
+                                                ["PlayerCharacter"]
+                pc_agent.update(player_char.getStateForSaving())
+                pc_agent["Map"] = self.model.target_map_name 
+                pc_agent["Position"] = self.model.target_position
+                pc_agent["Inventory"] = \
+                        player_char.inventory.serializeInventory()
+                player_agent = self.model.active_map.\
+                                    agent_layer.getInstance("PlayerCharacter")
+                self.model.active_map.agent_layer.deleteInstance(player_agent)
+            self.model.loadMap(self.model.target_map_name)
+            self.model.setActiveMap(self.model.target_map_name)          
+            self.model.readAgentsOfMap(self.model.target_map_name)
+            self.model.placeAgents()
+            self.model.placePC()
+            self.model.map_change = False
+            # The PlayerCharacter has an inventory, and also some 
+            # filling of the ready slots in the HUD. 
+            # At this point we sync the contents of the ready slots 
+            # with the contents of the inventory.
+            self.view.hud.inventory = None
+            self.view.hud.initializeInventory()         
+            self.pause(False)
+
+    def nullFunc(self, userdata):
+        """Sample callback for the context menus."""
+        logger.info(userdata)
+
+    def initTalk(self, npc_info):
+        """ Starts the PlayerCharacter talking to an NPC. """
+        # TODO: work more on this when we get NPCData and HeroData straightened
+        # out
+        npc = self.model.game_state.getObjectById(npc_info.ID,
+                                            self.model.game_state.\
+                                                current_map_name)
+        self.model.game_state.player_character.approach([npc.getLocation().\
+                                     getLayerCoordinates().x,
+                                     npc.getLocation().\
+                                     getLayerCoordinates().y],
+                                    TalkAction(self, npc))
+
+    def getItemActions(self, obj_id):
+        """Given the objects ID, return the text strings and callbacks.
+           @type obj_id: string
+           @param obj_id: ID of object
+           @rtype: list
+           @return: List of text and callbacks"""
+        actions = []
+        # note: ALWAYS check NPC's first!
+        obj = self.model.game_state.\
+                        getObjectById(obj_id,
+                                      self.model.game_state.current_map_name)
+        
+        if obj is not None:
+            if obj.trueAttr("NPC"):
+                # keep it simple for now, None to be replaced by callbacks
+                actions.append(["Talk", "Talk", self.initTalk, obj])
+                actions.append(["Attack", "Attack", self.nullFunc, obj])
+            else:
+                actions.append(["Examine", "Examine",
+                                self.model.game_state.\
+                                player_character.approach, 
+                                [obj.X, obj.Y],
+                                ExamineAction(self, 
+                                              obj_id, obj.name, 
+                                              obj.text)])
+                # is it a Door?
+                if obj.trueAttr("door"):
+                    actions.append(["Change Map", "Change Map",
+                       self.model.game_state.player_character.approach, 
+                       [obj.X, obj.Y],
+                       ChangeMapAction(self, obj.target_map_name,
+                                       obj.target_pos)])
+                # is it a container?
+                if obj.trueAttr("container"):
+                    actions.append(["Open", "Open", 
+                                    self.model.game_state.\
+                                        player_character.approach,
+                                    [obj.X, obj.Y],
+                                    OpenBoxAction(self, obj)])
+                    actions.append(["Unlock", "Unlock", 
+                                    self.model.game_state.\
+                                        player_character.approach,
+                                    [obj.X, obj.Y],
+                                    UnlockBoxAction(self, obj)])
+                    actions.append(["Lock", "Lock", 
+                                    self.model.game_state.\
+                                        player_character.approach,
+                                    [obj.X, obj.Y],
+                                    LockBoxAction(self, obj)])
+                # can you pick it up?
+                if obj.trueAttr("carryable"):
+                    actions.append(["Pick Up", "Pick Up", 
+                                    self.model.game_state.\
+                                        player_character.approach,
+                                    [obj.X, obj.Y],
+                                    PickUpAction(self, obj)])
+
+        return actions
+    
+    def saveGame(self, *args, **kwargs):
+        """Saves the game state, delegates call to engine.Engine
+           @return: None"""
+        self.model.pause(False)
+        self.pause(False)
+        self.view.hud.enabled = True
+        self.model.save(*args, **kwargs)
+
+    def loadGame(self, *args, **kwargs):
+        """Loads the game state, delegates call to engine.Engine
+           @return: None"""
+        # Remove all currently loaded maps so we can start fresh
+        self.model.pause(False)
+        self.pause(False)
+        self.view.hud.enabled = True
+        self.model.deleteMaps()
+        self.view.hud.inventory = None
+
+        self.model.load(*args, **kwargs)
+        self.view.hud.initializeInventory()          
+
+    def quitGame(self):
+        """Quits the game
+           @return: None"""
+        self.application.listener.quitGame()
+    
+    def pause(self, paused):
+        """Pauses the controller"""
+        super(GameSceneController, self).pause(paused)
+        self.paused = paused
+        if paused:
+            self.scroll_timer.stop()
+            self.resetMouseCursor()
+    
+    def onCommand(self, command):
+        if(command.getCommandType() == fife.CMD_MOUSE_FOCUS_GAINED):
+            self.has_mouse_focus = True
+        elif(command.getCommandType() == fife.CMD_MOUSE_FOCUS_LOST):
+            self.has_mouse_focus = False
+      
+    def pump(self):
+        """Routine called during each frame. Our main loop is in ./run.py"""
+        # uncomment to instrument
+        # t0 = time.time()
+        if self.paused: 
+            return
+        self.updateMouse()
+        if self.model.active_map:
+            self.view.highlightFrontObject(self.last_mousecoords)
+            self.view.refreshTopLayerTransparencies()
+            if self.scroll_direction != [0, 0]:
+                self.scroll_timer.start()
+            else: 
+                self.scroll_timer.stop()
+                if not data_drag.dragging:
+                    self.resetMouseCursor()
+                
+        self.handleCommands()
+        # print "%05f" % (time.time()-t0,)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gamesceneview.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,159 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+from sounds import SoundEngine
+from viewbase import ViewBase
+from fife import fife
+
+class GameSceneView(ViewBase):
+    """GameSceneView is responsible for drawing the scene"""
+    def __init__(self, engine, model):
+        """Constructor for GameSceneView
+           @param engine: A fife.Engine instance
+           @type engine: fife.Engine
+           @param model: a script.GameModel instance
+           @type model: script.GameModel 
+           """
+        super(GameSceneView, self).__init__(engine, model)
+
+        # init the sound
+        self.sounds = SoundEngine(engine)
+
+        self.hud = None        
+
+        # The current highlighted object
+        self.highlight_obj = None
+     
+        # faded objects in top layer
+        self.faded_objects = set()
+
+    def displayObjectText(self, obj_id, text, time=1000):
+        """Display on screen the text of the object over the object.
+           @type obj_id: id of fife.instance
+           @param obj: id of object to draw over
+           @type text: String
+           @param text: text to display over object
+           @return: None"""
+        try:
+            if obj_id:
+                obj = self.model.active_map.agent_layer.getInstance(obj_id)
+            else:
+                obj = None
+        except RuntimeError as error:
+            if error.args[0].split(',')[0].strip() == "_[NotFound]_":
+                obj = None
+            else:
+                raise
+        if obj:
+            obj.say(str(text), time)
+
+    def onWalk(self, click):
+        """Callback sample for the context menu."""
+        self.hud.hideContainer()
+        self.model.game_state.player_character.run(click)
+
+    def refreshTopLayerTransparencies(self):
+        """Fade or unfade TopLayer instances if the PlayerCharacter 
+        is under them."""
+        if not self.model.active_map:
+            return
+
+        # get the PlayerCharacter's screen coordinates
+        camera = self.model.active_map.cameras[self.model.active_map.my_cam_id]
+        point = self.model.game_state.player_character.\
+                                        behaviour.agent.getLocation()
+        scr_coords = camera.toScreenCoordinates(point.getMapCoordinates())
+
+        # find all instances on TopLayer that fall on those coordinates
+        instances = camera.getMatchingInstances(scr_coords,
+                        self.model.active_map.top_layer)
+        instance_ids = [ instance.getId() for instance in instances ]
+        faded_objects = self.faded_objects
+
+        # fade instances
+        for instance_id in instance_ids:
+            if instance_id not in faded_objects:
+                faded_objects.add(instance_id)
+                self.model.active_map.top_layer.getInstance(instance_id).\
+                        get2dGfxVisual().setTransparency(128)
+
+        # unfade previously faded instances
+        for instance_id in faded_objects.copy():
+            if instance_id not in instance_ids:
+                faded_objects.remove(instance_id)
+                self.model.active_map.top_layer.getInstance(instance_id).\
+                        get2dGfxVisual().setTransparency(0)
+
+
+    #def removeHighlight(self):
+        
+    
+    def highlightFrontObject(self, mouse_coords):
+        """Highlights the object that is at the 
+        current mouse coordinates"""        
+        if not self.model.active_map:
+            return
+        if mouse_coords:
+            front_obj = self.model.getObjectAtCoords(mouse_coords)
+            if front_obj != None:
+                if self.highlight_obj == None \
+                                    or front_obj.getId() != \
+                                    self.highlight_obj:
+                    if self.model.game_state.hasObject(front_obj.getId()):
+                        self.displayObjectText(self.highlight_obj, "")
+                    self.model.active_map.outline_renderer.removeAllOutlines()
+                    self.highlight_obj = front_obj.getId()
+                    self.model.active_map.outline_renderer.addOutlined(
+                                                    front_obj, 
+                                                    0,
+                                                    137, 255, 2)
+                    # get the text
+                    item = self.model.objectActive(self.highlight_obj)
+                    if item is not None:
+                        self.displayObjectText(self.highlight_obj, 
+                                                    item.name)
+            else:
+                self.model.active_map.outline_renderer.removeAllOutlines()
+                self.highlight_obj = None  
+           
+
+    def moveCamera(self, direction):
+        """Move the camera in the given direction.
+        @type direction: list of two integers
+        @param direction: the two integers can be 1, -1, or 0
+        @return: None """  
+        
+        if 'cameras' in dir(self.model.active_map):
+            cam = self.model.active_map.cameras[self.model.active_map.my_cam_id]
+            location = cam.getLocation()
+            position = location.getMapCoordinates()
+            
+            #how many pixls to move by each call
+            move_by = 1
+            #create a new DoublePoint3D and add it to position DoublePoint3D
+            new_x, new_y = move_by * direction[0], move_by * direction[1]
+
+            position_offset = fife.DoublePoint3D(int(new_x), int(new_y))
+            position += position_offset
+            
+            #give location the new position
+            location.setMapCoordinates(position)
+
+            #detach the camera from any objects
+            cam.detach()
+            #move the camera to the new location
+            cam.setLocation(location)
+            
+            
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gamestate.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,124 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+from parpg.quest_engine import QuestEngine
+
+class GameState(object):
+    """This class holds the current state of the game."""
+    def __init__(self, quests_dir = None):
+        self.player_character = None
+        self.quest_engine = QuestEngine(quests_dir)
+        self.quest_engine.readQuests()
+        self.objects = {}
+        self.object_ids = {}
+        self.current_map_name = None
+        self.maps = {}
+        
+        
+    def addObject(self, map_id, game_object):
+        """Adds an object to the objects and object_ids
+        dictionaries.
+        @param map_id: ID of the map the object is on. 
+        If the object is in a container this has to be None
+        @param object: object to be added
+        @type object: GameObject
+        @type map_id: str or None
+        """
+        object_id = game_object.ID
+        if not self.object_ids.has_key(object_id):
+            if map_id:
+                self.objects[map_id][object_id] = game_object
+            self.object_ids[object_id] = map_id
+    
+    def deleteObject(self, object_id):
+        """Removes an object from the dictionaries
+        @param object_id: ID of the object
+        @type object_id: str
+        """
+        if self.hasObject(object_id):
+            map_id = self.getMapOfObject(object_id)
+            if map_id:
+                inst = self.maps[map_id].agent_layer.getInstance(object_id)
+                self.maps[map_id].agent_layer.deleteInstance(inst)
+                del self.objects[map_id][object_id]
+            del self.object_ids[object_id]
+            
+            
+    def getObjectsFromMap(self, map_id):
+        """Gets all objects that are currently on the given map.
+           @type map: String
+           @param map: The map name.
+           @returns: The list of objects on this map. Or an empty list"""
+        if map_id in self.objects:
+            return [i for i in self.objects[map_id].values() \
+                                        if map_id in self.objects]
+        
+        return {}
+    
+    def hasObject(self, object_id):
+        """Check if an object with the given id is present 
+        @param object_id: ID of the object
+        @type object_id: str
+        @return: True if there is an object False if not
+        """
+        return self.object_ids.has_key(object_id)
+    
+    def getMapOfObject(self, object_id):
+        """Returns the map the object is on.
+        @param object_id: ID of the object
+        @type object_id: str
+        @return: Name of the map the object is on. 
+        If there is no such object or the object is in a container None is returned
+        """
+        if self.object_ids.has_key(object_id):
+            return self.object_ids[object_id]
+        return None
+    
+    def getObjectById(self, obj_id, map_id = None):
+        """Gets an object by its object id and map id
+           @type obj_id: String
+           @param obj_id: The id of the object.
+           @type map_id: String
+           @param map_id: It id of the map containing the object.
+           @returns: The object or None."""
+        if not map_id:
+            map_id = self.getMapOfObject(obj_id)
+        if not map_id in self.objects:
+            self.objects[map_id] = {}
+        if obj_id in self.objects[map_id]:
+            return self.objects[map_id][obj_id]
+    
+    def clearObjects(self):
+        """Delete all objects from the state
+        """
+        self.objects = {}
+        self.object_ids = {}
+        
+    def getStateForSaving(self):
+        """Prepares state for saving
+        @type state: dictionary
+        @param state: State of the object  
+        """
+        ret_dict = {}
+        ret_dict["CurrentMap"] = self.current_map_name
+        ret_dict["Quests"] = self.quest_engine.getStateForSaving()
+        return ret_dict
+
+    def restoreFromState(self, state):
+        """Restores the state"""
+        self.current_map_name = state["CurrentMap"]
+        self.quest_engine.readQuests()
+        self.quest_engine.restoreFromState(state["Quests"])
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/__init__.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,10 @@
+from fife.extensions import pychan
+from .inventorygui import EquipmentSlot, InventoryGrid
+from .spinners import Spinner, IntSpinner
+from .tabwidget import TabWidget
+
+pychan.registerWidget(EquipmentSlot)
+pychan.registerWidget(InventoryGrid)
+pychan.registerWidget(Spinner)
+pychan.registerWidget(IntSpinner)
+pychan.registerWidget(TabWidget)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/actionsbox.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,73 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Widget for displaying actions"""
+
+from fife.extensions import pychan
+from fife.extensions.pychan import ScrollArea
+from fife.extensions.pychan import VBox
+
+class ActionsBox(ScrollArea):
+    def __init__(self, **kwargs):
+        ScrollArea.__init__(self, **kwargs)
+        self.ContentBox = VBox(name = "ActionsContentBox", is_focusable=False)
+        self.addChild(self.ContentBox)
+    
+    def refresh(self):
+        """Refresh the actions box so that it displays the contents of
+        self.actions_text
+        @return: None"""
+        self.adaptLayout()
+        self.vertical_scroll_amount = self.getVerticalMaxScroll()
+
+    def addAction(self, action):
+        """Add an action to the actions box.
+        @type action: (unicode) string
+        @param action: The text that you want to display in the actions box
+        @return: None"""      
+      
+        if not type(action) is unicode:
+            action = unicode(action)
+        action_label = pychan.widgets.Label(text = action, wrap_text = True)
+        action_label.max_width = self.ContentBox.width
+        self.ContentBox.addChild(action_label)
+        self.refresh()
+    
+    def addDialog(self, name, text):
+        """Add a dialog text to the actions box. Prints first the name and then, indented to the right, the text.
+        @type name: (unicode) string
+        @param action: The name of the character that spoke
+        @type text:: (unicode) string
+        @param text: The text that was said
+        @return: None"""        
+        if not type(name) is unicode:
+            name = unicode(name)
+        if not type(text) is unicode:
+            text = unicode(text)
+        
+        
+        name_label = pychan.widgets.Label(text = name, wrap_text = True)
+        self.ContentBox.addChild(name_label)
+        text_box = pychan.widgets.HBox()
+        spacer = pychan.widgets.Label()
+        spacer.min_width = int(self.ContentBox.width * 0.05)
+        spacer.max_width = int(self.ContentBox.width * 0.05)
+        text_box.addChild(spacer)
+        text_label = pychan.widgets.Label(text = text, wrap_text = True)
+        text_label.max_width = int(self.ContentBox.width * 0.95)
+        text_box.addChild(text_label)
+        self.ContentBox.addChild(text_box)
+        self.refresh()
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/charactercreationview.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,28 @@
+from fife.extensions import pychan
+from fife.extensions.pychan.widgets import Label, HBox
+
+from parpg.gui.spinner import IntSpinner
+
+class CharacterCreationView(object):
+    def __init__(self, xml_script_path='gui/character_creation.xml'):
+        self.gui = pychan.loadXML(xml_script_path)
+    
+    def createStatisticList(self, statistics):
+        statistics_list = self.gui.findChild(name='statisticsList')
+        # Start with an empty list.
+        statistics_list.removeAllChildren()
+        for statistic in statistics:
+            name = statistic.long_name
+            hbox = HBox()
+            hbox.opaque = 0
+            label = Label(text=name)
+            spinner = IntSpinner(lower_limit=0, upper_limit=100)
+            hbox.addChildren(label, spinner)
+            statistics_list.addChildren(hbox)
+    
+    def createTraitsList(self, traits):
+        pass
+    
+    def updateMessageArea(self, message):
+        message_area = self.gui.findChild(name='messageArea')
+        message_area.text = unicode(message)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/containergui.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,139 @@
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+from fife.extensions.pychan.tools import callbackWithArguments as cbwa
+
+from parpg.gui.containergui_base import ContainerGUIBase
+from parpg.gui import drag_drop_data as data_drag
+from parpg.objects.base import Container
+
+class ContainerGUI(ContainerGUIBase):
+    def __init__(self, controller, title, container):
+        """A class to create a window showing the contents of a container.
+           @param controller: The current Controller
+           @type controller: Class derived from ControllerBase
+           @param title: The title of the window
+           @type title: string
+           @param container: A container to represent
+           @type container: Container
+           @return: None"""
+        super(ContainerGUI, self).__init__(controller, "gui/container_base.xml")
+        self.gui.findChild(name="topWindow").title = title
+        
+        self.empty_images = dict()
+        self.container = container
+        self.events_to_map = {}        
+        self.buttons = ("Slot1", "Slot2", "Slot3",
+                        "Slot4", "Slot5", "Slot6",
+                        "Slot7", "Slot8", "Slot9")         
+    
+    def updateImages(self):
+        for index, button in enumerate(self.buttons):
+            widget = self.gui.findChild(name=button)
+            widget.item = self.container.getItemAt(index)
+            self.updateImage(widget) 
+               
+    def updateImage(self, button):
+        if (button.item == None):
+            image = self.empty_images[button.name]
+        else:
+            image = button.item.getInventoryThumbnail()
+        button.up_image = image
+        button.down_image = image
+        button.hover_image = image
+
+    def dragObject(self, obj):
+        """Drag the selected object.
+           @type obj: string
+           @param obj: The name of the object within
+                       the dictionary 'self.buttons'
+           @return: None"""
+        # get the widget from the gui with the name obj
+        drag_widget = self.gui.findChild(name = obj)
+        drag_item = drag_widget.item
+        # only drag if the widget is not empty
+        if (drag_item != None):
+            # get the item that the widget is 'storing'
+            data_drag.dragged_item = drag_widget.item
+            # get the up and down images of the widget
+            up_image = drag_widget.up_image
+            down_image = drag_widget.down_image
+            # set the mouse cursor to be the widget's image
+            self.controller.setMouseCursor(up_image.source, down_image.source)
+            data_drag.dragged_image = up_image.source
+            data_drag.dragging = True
+            data_drag.dragged_widget = drag_widget
+            data_drag.source_container = self.container
+
+            self.container.takeItem(drag_widget.item)
+            
+            # after dragging the 'item', set the widgets' images
+            # so that it has it's default 'empty' images
+            drag_widget.item = None
+            self.updateImage(drag_widget)
+            
+    def dropObject(self, obj):
+        """Drops the object being dropped
+           @type obj: string
+           @param obj: The name of the object within
+                       the dictionary 'self.buttons' 
+           @return: None"""
+        try:
+            drop_widget = self.gui.findChild(name = obj)
+            drop_index = drop_widget.index
+            replace_item = drop_widget.item
+    
+            if data_drag.dragging:
+                container = self.container
+                drag_item = data_drag.dragged_item
+                #this will get the replacement item and the data for drag_drop if
+                ## there is an item all ready occupying the slot
+                if replace_item != None:
+                    self.dragObject(obj)
+                container.placeItem(drag_item, drop_index)
+                
+            drop_widget.item = drag_item
+            self.updateImage(drop_widget)
+            #if there was no item the stop dragging and reset cursor
+            if replace_item == None:
+                data_drag.dragging = False
+                #reset the mouse cursor to the normal cursor
+                self.controller.resetMouseCursor()
+        except (Container.SlotBusy, Container.TooBig, Container.ItemSelf):
+            #Do we want to notify the player why the item can't be dropped?
+            pass
+        
+    def showContainer(self):
+        """Show the container
+           @return: None"""
+        # Prepare slots 1 through 9
+        empty_image = "gui/inv_images/inv_backpack.png"
+        slot_count = 9
+        for counter in range(1, slot_count+1):
+            slot_name = "Slot%i" % counter
+            self.empty_images[slot_name] = empty_image
+            widget = self.gui.findChild(name=slot_name)
+            widget.item = self.container.items.get(counter-1)
+            widget.index = counter-1
+            self.updateImage(widget)
+            self.events_to_map[slot_name] = cbwa(self.dragDrop, slot_name)
+            self.events_to_map[slot_name + "/mouseReleased"] = \
+                                            self.showContextMenu
+
+        self.gui.mapEvents(self.events_to_map)
+        self.gui.show()
+        
+    def hideContainer(self):
+        """Hide the container
+           @return: None"""
+        if self.gui.isVisible():
+            self.gui.hide()      
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/containergui_base.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,122 @@
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+
+from fife import fife
+from fife.extensions import pychan
+
+from parpg.gui import drag_drop_data as data_drag
+from parpg.objects.action import ACTIONS
+from copy import deepcopy
+
+class ContainerGUIBase(object):
+    """
+    Base class for windows that show the content of a container
+    """
+
+
+    def __init__(self, controller, gui_file):
+        self.controller = controller
+        self.gui = pychan.loadXML(gui_file)
+
+    def dragDrop(self, obj):
+        """Decide whether to drag or drop the image.
+           @type obj: string
+           @param obj: The name of the object within 
+                       the dictionary 'self.buttons'
+           @return: None"""
+        if(data_drag.dragging == True):
+            self.dropObject(obj)
+        elif(data_drag.dragging == False):
+            self.dragObject(obj)
+                
+    def dragObject(self, obj):
+        """Drag the selected object.
+           @type obj: string
+           @param obj: The name of the object within
+                       the dictionary 'self.buttons'
+           @return: None"""           
+        pass
+       
+    def dropObject(self, obj):
+        """Drops the object being dropped
+           @type obj: string
+           @param obj: The name of the object within
+                       the dictionary 'self.buttons' 
+           @return: None"""
+        pass    
+    
+
+    def createMenuItems(self, item, actions):
+        """Creates context menu items for all classes based on ContainerGUI"""
+        menu_actions = []
+        for action_name in actions:
+            display_name = action_name
+            if action_name in ACTIONS:
+                param_dict = {}
+                param_dict["controller"] = self.controller
+                param_dict["commands"] = {}
+                if action_name == "Look":
+                    param_dict["examine_name"] = item.name
+                    param_dict["examine_desc"] = actions[action_name].\
+                                                                pop("text")
+                if action_name == "Read":
+                    param_dict["text_name"] = item.name
+                    param_dict["text"] = ""
+                if action_name == "Use":
+                    param_dict["item"] = item
+                    display_name = actions[action_name].pop("text")
+                if action_name == "Open":
+                    param_dict["container"] = item
+                if action_name == "BrewBeer":
+                    param_dict["pot"] = item
+                    display_name = "Brew beer"
+                if actions[action_name]:
+                    param_dict.update(actions[action_name])
+                menu_actions.append([action_name, 
+                                     display_name, 
+                                     self.executeMenuItem, 
+                                     ACTIONS[action_name]\
+                                                (**param_dict)])        
+        return menu_actions
+
+    def showContextMenu(self, event, widget):
+        """Decide whether to drag or drop the image.
+           @type obj: string
+           @param obj: The name of the object within 
+                       the dictionary 'self.buttons'
+           @return: None"""
+        if event.getButton() == event.RIGHT:
+            item = widget.item
+            if item and item.trueAttr("usable"):
+                actions = deepcopy(item.actions)
+                if not actions:
+                    return
+                x_pos, y_pos = widget.getAbsolutePos()
+                x_pos += event.getX()
+                y_pos += event.getY()
+                menu_actions = self.createMenuItems(item, actions)
+                self.controller.view.hud.hideContextMenu()
+                self.controller.view.hud.showContextMenu(menu_actions,
+                                                 (x_pos, 
+                                                  y_pos)
+                                                  )
+
+    def executeMenuItem(self, action):
+        """Executes the items action
+        @param action: The action to run
+        @type action: Class derived from parpg.objects.action.Action
+        """
+        action.execute()
+    
+    def updateImages(self):
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/dialogs.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,32 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from fife.extensions import pychan
+
+class RestartDialog(object):
+    def __init__(self, settings):
+        self.settings = settings
+        self.window = pychan.loadXML(os.path.join(self.settings.system_path,
+                                                  self.settings.parpg.GuiPath,
+                                                  'restart_dialog.xml'))
+        self.window.mapEvents({'closeButton': self.hide})
+
+    def hide(self):
+        self.window.hide()
+
+    def show(self):
+        self.window.show()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/dialoguegui.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,165 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+import logging
+
+from fife import fife
+from fife.extensions import pychan
+from fife.extensions.pychan import widgets
+
+from parpg.dialogueprocessor import DialogueProcessor
+
+logger = logging.getLogger('dialoguegui')
+
+class DialogueGUI(object):
+    """Window that handles the dialogues."""
+    _logger = logging.getLogger('dialoguegui.DialogueGUI')
+    
+    def __init__(self, controller, npc, quest_engine, player_character):
+        self.active = False
+        self.controller = controller
+        self.dialogue_gui = pychan.loadXML("gui/dialogue.xml")
+        self.npc = npc
+        # TODO Technomage 2010-11-10: the QuestEngine should probably be
+        #     a singleton-like object, which would avoid all of this instance
+        #     handling.
+        self.quest_engine = quest_engine
+        self.player_character = player_character
+    
+    def initiateDialogue(self):
+        """Callback for starting a quest"""
+        self.active = True
+        stats_label = self.dialogue_gui.findChild(name='stats_label')
+        stats_label.text = u'Name: John Doe\nAn unnamed one'
+        events = {
+            'end_button': self.handleEnd
+        }
+        self.dialogue_gui.mapEvents(events)
+        self.dialogue_gui.show()
+        self.setNpcName(self.npc.name)
+        self.setAvatarImage(self.npc.dialogue.avatar_path)
+        
+        game_state = {'npc': self.npc, 'pc': self.player_character,
+                      'quest': self.quest_engine}
+        try:
+            self.dialogue_processor = DialogueProcessor(self.npc.dialogue,
+                                                        game_state)
+            self.dialogue_processor.initiateDialogue()
+        except (TypeError) as error:
+            self._logger.error(str(error))
+        else:
+            self.continueDialogue()
+    
+    def setDialogueText(self, text):
+        """Set the displayed dialogue text.
+           @param text: text to display."""
+        text = unicode(text)
+        speech = self.dialogue_gui.findChild(name='speech')
+        # to append text to npc speech box, uncomment the following line
+        #speech.text = speech.text + "\n-----\n" + unicode(say)
+        speech.text = text
+        self._logger.debug('set dialogue text to "{0}"'.format(text))
+    
+    def continueDialogue(self):
+        """Display the dialogue text and responses for the current
+           L{DialogueSection}."""
+        dialogue_processor = self.dialogue_processor
+        dialogue_text = dialogue_processor.getCurrentDialogueSection().text
+        self.setDialogueText(dialogue_text)
+        self.responses = dialogue_processor.continueDialogue()
+        self.setResponses(self.responses)
+    
+    def handleEntered(self, *args):
+        """Callback for when user hovers over response label."""
+        pass
+    
+    def handleExited(self, *args):
+        """Callback for when user hovers out of response label."""
+        pass
+    
+    def handleClicked(self, *args):
+        """Handle a response being clicked."""
+        response_n = int(args[0].name.replace('response', ''))
+        response = self.responses[response_n]
+        dialogue_processor = self.dialogue_processor
+        dialogue_processor.reply(response)
+        if (not dialogue_processor.in_dialogue):
+            self.handleEnd()
+        else:
+            self.continueDialogue()
+    
+    def handleEnd(self):
+        """Handle the end of the conversation being reached, either from the
+           GUI or from within the conversation itself."""
+        self.dialogue_gui.hide()
+        self.responses = []
+        self.npc.behaviour.state = 1
+        self.npc.behaviour.idle()
+        self.active = False
+    
+    def setNpcName(self, name):
+        """Set the NPC name to display on the dialogue GUI.
+           @param name: name of the NPC to set
+           @type name: basestring"""
+        name = unicode(name)
+        stats_label = self.dialogue_gui.findChild(name='stats_label')
+        try:
+            (first_name, desc) = name.split(" ", 1)
+            stats_label.text = u'Name: ' + first_name + "\n" + desc
+        except ValueError:
+            stats_label.text = u'Name: ' + name
+        
+        self.dialogue_gui.title = name
+        self._logger.debug('set NPC name to "{0}"'.format(name))
+    
+    def setAvatarImage(self, image_path):
+        """Set the NPC avatar image to display on the dialogue GUI
+           @param image_path: filepath to the avatar image
+           @type image_path: basestring"""
+        avatar_image = self.dialogue_gui.findChild(name='npc_avatar')
+        avatar_image.image = image_path
+    
+    def setResponses(self, dialogue_responses):
+        """Creates the list of clickable response labels and sets their
+           respective on-click callbacks.
+           @param responses: list of L{DialogueResponses} from the
+               L{DialogueProcessor}
+           @type responses: list of L{DialogueResponses}"""
+        choices_list = self.dialogue_gui.findChild(name='choices_list')
+        choices_list.removeAllChildren()
+        for index, response in enumerate(dialogue_responses):
+            button = widgets.Label(
+                name="response{0}".format(index),
+                text=unicode(response.text),
+                hexpand="1",
+                min_size=(100,16),
+                max_size=(490,48),
+                position_technique='center:center'
+            )
+            button.margins = (5, 5)
+            button.background_color = fife.Color(0, 0, 0)
+            button.color = fife.Color(0, 255, 0)
+            button.border_size = 0
+            button.wrap_text = 1
+            button.capture(lambda button=button: self.handleEntered(button),
+                           event_name='mouseEntered')
+            button.capture(lambda button=button: self.handleExited(button),
+                           event_name='mouseExited')
+            button.capture(lambda button=button: self.handleClicked(button),
+                           event_name='mouseClicked')
+            choices_list.addChild(button)
+            self.dialogue_gui.adaptLayout(True)
+            self._logger.debug(
+                'added {0} to response choice list'.format(response)
+            )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/drag_drop_data.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,25 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+This contains the data that tells the GUI whether something is being dragged, dropped etc.
+It is in one place to allow communication between multiple windows
+"""
+dragging = False
+dragged_image = None
+dragged_type = None
+dragged_item = None
+dragged_widget = None
+source_container = None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/filebrowser.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,159 @@
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+
+from fife.extensions import pychan
+from fife.extensions.pychan import widgets
+
+import sys
+import os
+import logging
+
+logger = logging.getLogger('filebrowser')
+
+def u2s(string):
+    # TODO: cryptic function name
+    return string.encode(sys.getfilesystemencoding())
+
+class FileBrowser(object):
+    """FileBrowser displays directory and file listings from the vfs.
+       The file_selected parameter is a callback invoked when a file selection
+       has been made; its signature must be file_selected(path,filename). If
+       select_dir is set, file_selected's filename parameter should be optional.
+       The save_file option provides a box for supplying a new filename that
+       doesn't exist yet. The select_dir option allows directories to be
+       selected as well as files."""
+    def __init__(self, engine, settings, file_selected, gui_xml_path,
+                 close_callback=None, save_file=False, select_dir=False, 
+                 extensions=('.dat',)):
+        self.engine = engine
+        self.settings = settings
+        print self.settings.parpg.SavesPath
+        self.file_selected = file_selected
+
+        self._widget = None
+        self.save_file = save_file
+        self.select_dir = select_dir
+        self.close_callback = close_callback
+        self.gui_xml_path = gui_xml_path 
+        
+        self.extensions = extensions
+        self.path = os.path.join(self.settings.user_path,
+                                 self.settings.parpg.SavesPath)
+        self.dir_list = []
+        self.file_list = []
+        
+    def close(self):
+        """Closes the browser"""        
+        self._widget.hide()
+        if self.close_callback:
+            self.close_callback()
+    
+    def showBrowser(self):
+        """Shows the file dialog browser"""
+        if self._widget:
+            self._widget.show()
+            return
+        self._widget = pychan.loadXML(self.gui_xml_path)
+        self._widget.mapEvents({
+            'dirList'       : self._setPath,
+            'selectButton'  : self._selectFile,
+            'closeButton'   : self.close
+        })
+        self._setPath()
+        if self.save_file:
+            self._file_entry = widgets.TextField(name='saveField', text=u'')    
+            self._widget.findChild(name="fileColumn").\
+                addChild(self._file_entry)
+        self._widget.show()
+
+    def _setPath(self):
+        """Path change callback."""
+        selection = self._widget.collectData('dirList')
+        if not (selection < 0):
+            new_dir = u2s(self.dir_list[selection])
+            lst = self.path.split('/')
+            if new_dir == '..' and lst[-1] != '..' and lst[-1] != '.':
+                lst.pop()
+            else:
+                lst.append(new_dir)
+            self.path = '/'.join(lst)
+            
+        def decodeList(list):
+            fs_encoding = sys.getfilesystemencoding()
+            if fs_encoding is None: fs_encoding = "ascii"
+        
+            new_list = []
+            for i in list:
+                try:
+                    new_list.append(unicode(i, fs_encoding))
+                except:
+                    new_list.append(unicode(i, fs_encoding, 'replace'))
+                    logger.debug("WARNING: Could not decode item:\n"
+                                        "{0}".format(i))
+            return new_list
+            
+        
+
+        self.dir_list = []
+        self.file_list = []
+        
+        dir_list = ('..',) + filter(lambda d: not d.startswith('.'), \
+                                    self.engine.getVFS().\
+                                    listDirectories(self.path))
+        file_list = filter(lambda f: f.split('.')[-1] in self.extensions, \
+                           self.engine.getVFS().listFiles(self.path))
+                
+        self.dir_list = decodeList(dir_list)
+        self.file_list = decodeList(file_list)
+        self._widget.distributeInitialData({
+            'dirList'  : self.dir_list,
+            'fileList' : self.file_list
+        })
+
+    def _selectFile(self):
+        """ File selection callback. """
+        self._widget.hide()
+        selection = self._widget.collectData('fileList')
+
+        if self.save_file:
+            data = self._widget.collectData('saveField')
+            if data:
+                if (data.endswith(".dat")):
+                    self.file_selected(self.path, \
+                                u2s(self._widget.collectData('saveField')))
+                else:
+                    self.file_selected(self.path, 
+                                       u2s(self._widget.collectData('saveField')) + '.dat')
+                return
+            
+
+        if selection >= 0 and selection < len(self.file_list):
+            self.file_selected(self.path, u2s(self.file_list[selection]))
+            return
+        
+        if self.select_dir:
+            self.file_selected(self.path)
+            return
+
+        logger.error('no selection')
+
+    def _warningMessage(self):
+        """Shows the warning message dialog when a file with a
+           faulty extension was selected."""
+        window = widgets.Window(title="Warning")
+        text = "Please save the file as a .dat"
+        label = widgets.Label(text=text)
+        ok_button = widgets.Button(name="ok_button", text="Ok")
+        window.addChildren([label, ok_button])
+        window.mapEvents({'ok_button':window.hide})
+        window.show()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/hud.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,505 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import logging
+
+from fife.extensions import pychan
+from fife.extensions.pychan.tools import callbackWithArguments as cbwa
+
+from parpg.gui.filebrowser import FileBrowser
+from parpg.gui.menus import ContextMenu, SettingsMenu
+from parpg.gui import inventorygui
+from parpg.gui.popups import ExaminePopup
+from parpg.gui.containergui import ContainerGUI
+from parpg.gui.dialoguegui import DialogueGUI
+from parpg.gui import drag_drop_data as data_drag
+from actionsbox import ActionsBox
+
+logger = logging.getLogger('hud')
+class Hud(object):
+    """Main Hud class"""
+    def __init__(self, controller, settings, callbacks):
+        """Initialise the instance.
+           @type controller: Class derived from ControllerBase
+           @param controller: The current controller
+           @type settings: settings.Setting
+           @param settings: The settings
+           @type inv_model: dict
+           @type callbacks: dict
+           @param callbacks: a dict of callbacks
+               saveGame: called when the user clicks on Save
+               loadGame: called when the user clicks on Load
+               quitGame: called when the user clicks on Quit
+           @return: None"""
+
+        # TODO: perhaps this should not be hard-coded here
+        pychan.registerWidget(ActionsBox)
+        self.hud = pychan.loadXML("gui/hud.xml")
+        self.controller = controller
+        self.engine = controller.engine
+        self.model = controller.model
+        self.settings = settings
+        self.inventory = None
+        self.character_screen = None
+
+        self.save_game_callback = callbacks['saveGame']
+        self.load_game_callback = callbacks['loadGame']
+        self.quit_callback      = callbacks['quitGame']
+
+        self.box_container = None
+        self.examine_box = None
+        self.context_menu = None
+        self.help_dialog = None
+        self.events_to_map = None
+        self.main_menu = None
+        self.menu_events = None
+        self.quit_window = None
+        self.bottom_panel = self.hud.findChild(name="mainHudWindow")
+        
+        self.actions_box = self.hud.findChild(name="actionsBox")
+        self.menu_displayed = False
+        self.inventory_storage = None
+        self.initializeHud()
+        self.initializeMainMenu()
+        self.initializeContextMenu()
+        self.initializeHelpMenu()
+        self.initializeEvents()
+        self.initializeQuitDialog()
+        self.initializeSettingsMenu()
+    
+    def _getEnabled(self):
+        """"Returns whether the gui widget is enabled or not"""
+        return self.hud.real_widget.isEnabled()
+    
+    def _setEnabled(self, enabled):
+        """"Sets whether the gui widget is enabled or not"""
+        self.hud.real_widget.setEnabled(enabled)
+        childs = self.hud.getNamedChildren()
+        for child_list in childs.itervalues():
+            for child in child_list:
+                child.real_widget.setEnabled(enabled)
+    
+    enabled = property(_getEnabled, _setEnabled)
+        
+    def initializeHud(self):
+        """Initialize and show the main HUD
+           @return: None"""
+        self.events_to_map = {"menuButton":self.displayMenu, }
+        self.hud.mapEvents(self.events_to_map) 
+        # set HUD size according to screen size
+        screen_width = self.engine.getSettings().getScreenWidth()
+        self.hud.findChild(name="mainHudWindow").size = (screen_width, 65)
+        self.hud.findChild(name="inventoryButton").position = \
+                                                    (screen_width-59, 7)
+        # add ready slots
+        ready1 = self.hud.findChild(name='hudReady1')
+        ready2 = self.hud.findChild(name='hudReady2')
+        ready3 = self.hud.findChild(name='hudReady3')
+        ready4 = self.hud.findChild(name='hudReady4')
+
+        if (screen_width <=800) :
+            gap = 0
+        else :
+            gap = 40
+        ready1.position = (160+gap, 7)
+        ready2.position = (220+gap, 7)
+        ready3.position = (screen_width-180-gap, 7)
+        ready4.position = (screen_width-120-gap, 7)
+        self.actions_box.position = (280+gap, 5)
+        actions_width = screen_width - 470 - 2*gap
+
+        self.actions_box.ContentBox.min_width = actions_width
+        self.actions_box.ContentBox.max_width = actions_width
+        
+        # and finally add an actions box
+        self.actions_box.min_size = (actions_width, 55)
+        self.actions_box.max_size = (actions_width, 55)
+        # now it should be OK to display it all
+        self.showHUD()
+        
+    def addAction(self, action):
+        """Add an action to the actions box.
+           @type action: (unicode) string
+           @param action: The text that you want to display in the actions box
+           @return: None"""    
+        self.actions_box.addAction(action)
+
+    def showHUD(self):
+        """Show the HUD.
+           @return: None"""
+        self.hud.show()
+        self.enabled = True
+
+    def hideHUD(self):
+        """Hide the HUD.
+           @return: None"""
+        self.hud.hide()
+        self.enabled = False
+
+    def initializeInventory(self):
+        """Initialize the inventory"""
+        if not self.inventory:
+            self.inventory = inventorygui.InventoryGUI(self.controller,
+                                                       None,
+                                                       None)
+#        inv_callbacks = {
+#            'refreshReadyImages': self.refreshReadyImages,
+#            'toggleInventoryButton': self.toggleInventoryButton,
+#        }
+#        self.inventory_storage = \
+#            self.model.game_state.player_character.inventory
+#        if self.inventory == None:
+#            self.inventory = inventorygui.InventoryGUI(self.controller,
+#                                                       self.inventory_storage,
+#                                                       inv_callbacks)
+#        else:
+#            self.inventory.inventory_storage = self.inventory_storage
+#        self.refreshReadyImages()
+    
+    def initializeCharacterScreen(self):
+        """Initialize the character screen."""
+        # TODO Technomage 2010-12-24: 
+        if not self.character_screen:
+            self.character_screen = pychan.loadXML('gui/character_screen.xml')
+        
+    
+    def initializeContextMenu(self):
+        """Initialize the Context Menu
+           @return: None"""
+        self.context_menu = ContextMenu (self.engine, [], (0, 0))
+
+    def showContextMenu(self, data, pos):
+        """Display the Context Menu with model at pos
+           @type model: list
+           @param model: model to pass to context menu
+           @type pos: tuple
+           @param pos: tuple of x and y coordinates
+           @return: None"""
+        self.context_menu = ContextMenu(self.engine, data, pos)
+        self.context_menu.show()
+
+    def hideContextMenu(self):
+        """Hides the context menu
+           @return: None"""
+        self.context_menu.hide()
+
+    def initializeMainMenu(self):
+        """Initalize the main menu.
+           @return: None"""
+        self.main_menu = pychan.loadXML("gui/hud_pause_menu.xml")
+        #TODO: find more suitalbe place for onOptilonsPress implementation
+        self.menu_events = {"resumeButton": self.hideMenu, 
+                            "settingsButton": self.displaySettings,
+                            "helpButton": self.displayHelp}
+        self.main_menu.mapEvents(self.menu_events)
+
+    def displayMenu(self):
+        """Displays the main in-game menu.
+           @return: None"""
+        self.stopActions()
+        if (self.menu_displayed == False):
+            self.main_menu.show()
+            self.menu_displayed = True
+            self.model.pause(True)
+            self.controller.pause(True)
+            self.enabled = False
+        elif (self.menu_displayed == True):
+            self.hideMenu()
+
+    def hideMenu(self):
+        """Hides the main in-game menu.
+           @return: None"""
+        self.main_menu.hide()
+        self.menu_displayed = False
+        self.model.pause(False)
+        self.controller.pause(False)
+        self.enabled = True
+
+    def initializeSettingsMenu(self):
+        self.settings_menu = SettingsMenu(self.engine, self.settings)
+
+    def displaySettings(self):
+        self.settings_menu.show()
+
+    def initializeHelpMenu(self):
+        """Initialize the help menu
+           @return: None"""
+        self.help_dialog = pychan.loadXML("gui/help.xml")
+        help_events = {"closeButton":self.help_dialog.hide}
+        self.help_dialog.mapEvents(help_events)
+        main_help_text = u"Welcome to Post-Apocalyptic RPG or PARPG![br][br]"\
+        "This game is still in development, so please expect for there to be "\
+        "bugs and[br]feel free to tell us about them at "\
+        "http://www.forums.parpg.net.[br]This game uses a "\
+        "\"Point 'N' Click\" interface, which means that to move around,[br]"\
+        "just click where you would like to go and your character will move "\
+        "there.[br]PARPG also utilizes a context menu. To access this, just "\
+        "right click anywhere[br]on the screen and a menu will come up. This "\
+        "menu will change depending on[br]what you have clicked on, hence "\
+        "it's name \"context menu\".[br][br]"
+        
+        k_text = u" Keybindings" 
+        k_text += "[br] A : Add a test action to the actions display"
+        k_text += "[br] I : Toggle the inventory screen"
+        k_text += "[br] F7 : Take a screenshot"
+        k_text += "[br]      (Saves to screenshots directory)"
+        k_text += "[br] F10 : Toggle console"
+        k_text += "[br] PAUSE : (Un)Pause the game"
+        k_text += "[br] Q : Quit the game"
+        self.help_dialog.distributeInitialData({
+                "MainHelpText":main_help_text,
+                "KeybindText":k_text
+                })
+
+    def displayHelp(self):
+        """Display the help screen.
+           @return: None"""
+        self.help_dialog.show()
+
+    def saveGame(self):
+        """ Called when the user wants to save the game.
+            @return: None"""
+        self.stopActions()
+        xml_path = os.path.join(self.settings.system_path,
+                                    self.settings.parpg.GuiPath,
+                                    'savebrowser.xml')
+        save_browser = FileBrowser(self.engine,
+                                   self.settings,
+                                   self.save_game_callback,
+                                   xml_path,
+                                   self.loadsave_close,
+                                   save_file=True,
+                                   extensions=('.dat'))
+        save_browser.showBrowser()
+        self.controller.pause(True)
+        self.model.pause(True)
+        self.enabled = False
+
+    def stopActions(self):
+        """This method stops/resets actions that are currently performed 
+        like dragging an item.
+        This is done to be able to savely perform other actions that might 
+        interfere with current running ones."""
+        #Reset dragging - move item back to its old container
+        if data_drag.dragging:
+            data_drag.source_container.placeItem(data_drag.dragged_item)
+            data_drag.dragging = False
+            data_drag.dragged_item = None
+        if self.inventory:
+            self.inventory.closeInventory()
+        
+    def newGame(self):
+        """Called when user request to start a new game.
+           @return: None"""
+        self.stopActions()
+        logger.info('new game')
+    
+    def loadsave_close(self):
+        """Called when the load/save filebrowser was closed without a file selected"""
+        if not self.menu_displayed:
+            self.enabled = True
+            self.model.pause(False)
+            self.controller.pause(False)
+            
+    def loadGame(self):
+        """ Called when the user wants to load a game.
+            @return: None"""
+        self.stopActions()
+        xml_path = os.path.join(self.settings.system_path,
+                                    self.settings.parpg.GuiPath,
+                                    'loadbrowser.xml')
+        load_browser = FileBrowser(self.engine,
+                                   self.settings,
+                                   self.load_game_callback,
+                                   xml_path,
+                                   close_callback = self.loadsave_close,
+                                   save_file=False,
+                                   extensions=('.dat'))
+        load_browser.showBrowser()
+        self.model.pause(True)
+        self.controller.pause(True)
+        self.enabled = False
+    
+    def initializeQuitDialog(self):
+        """Creates the quit confirmation dialog
+           @return: None"""
+        self.quit_window = pychan.widgets.Window(title=unicode("Quit?"), \
+                                                 min_size=(200,0))
+
+        hbox = pychan.widgets.HBox()
+        are_you_sure = "Are you sure you want to quit?"
+        label = pychan.widgets.Label(text=unicode(are_you_sure))
+        yes_button = pychan.widgets.Button(name="yes_button", 
+                                           text=unicode("Yes"),
+                                           min_size=(90,20),
+                                           max_size=(90,20))
+        no_button = pychan.widgets.Button(name="no_button",
+                                          text=unicode("No"),
+                                          min_size=(90,20),
+                                          max_size=(90,20))
+
+        self.quit_window.addChild(label)
+        hbox.addChild(yes_button)
+        hbox.addChild(no_button)
+        self.quit_window.addChild(hbox)
+
+        events_to_map = { "yes_button": self.quit_callback,
+                          "no_button":  self.quit_window.hide }
+        
+        self.quit_window.mapEvents(events_to_map)
+
+
+    def quitGame(self):
+        """Called when user requests to quit game.
+           @return: None"""
+        self.stopActions()
+        self.quit_window.show()
+
+    def toggleInventoryButton(self):
+        """Manually toggles the inventory button.
+           @return: None"""
+        button = self.hud.findChild(name="inventoryButton")
+        if button.toggled == 0:
+            button.toggled = 1
+        else:
+            button.toggled = 0
+
+    def toggleInventory(self, toggle_image=True):
+        """Displays the inventory screen
+           @return: None"""
+        if self.inventory == None:
+            self.initializeInventory()
+        self.inventory.toggleInventory(toggle_image)
+    
+    def toggleCharacterScreen(self):
+        if not self.character_screen:
+            self.initializeCharacterScreen()
+        if not self.character_screen.isVisible():
+            self.character_screen.show()
+        else:
+            self.character_screen.hide()
+    
+    def refreshReadyImages(self):
+        """Make the Ready slot images on the HUD be the same as those 
+           on the inventory
+           @return: None"""
+        for ready in range(1, 5):
+            button = self.hud.findChild(name=("hudReady%d" % ready))
+            if self.inventory_storage == None :
+                origin = None
+            else:
+                origin = self.inventory_storage.getItemsInSlot('ready', ready-1)
+            if origin == None:
+                self.setImages(button, 
+                               self.inventory.slot_empty_images['ready'])
+            else:
+                self.setImages(button, origin.getInventoryThumbnail())
+
+    def setImages(self, widget, image):
+        """Set the up, down, and hover images of an Imagebutton.
+           @type widget: pychan.widget
+           @param widget: widget to set
+           @type image: string
+           @param image: image to use
+           @return: None"""
+        widget.up_image = image
+        widget.down_image = image
+        widget.hover_image = image
+
+    def initializeEvents(self):
+        """Intialize Hud events
+           @return: None"""
+        events_to_map = {}
+
+        # when we click the toggle button don't change the image
+        events_to_map["inventoryButton"] = cbwa(self.toggleInventory, False)
+        events_to_map["saveButton"] = self.saveGame
+        events_to_map["loadButton"] = self.loadGame
+
+        hud_ready_buttons = ["hudReady1", "hudReady2", \
+                             "hudReady3", "hudReady4"]
+
+        for item in hud_ready_buttons:
+            events_to_map[item] = cbwa(self.readyAction, item)
+
+        self.hud.mapEvents(events_to_map)
+
+        menu_events = {}
+        menu_events["newButton"] = self.newGame
+        menu_events["quitButton"] = self.quitGame
+        menu_events["saveButton"] = self.saveGame
+        menu_events["loadButton"] = self.loadGame
+        self.main_menu.mapEvents(menu_events)
+
+    def readyAction(self, ready_button):
+        """ Called when the user selects a ready button from the HUD """
+        text = "Used the item from %s" % ready_button        
+        self.addAction(text)
+        
+    def createBoxGUI(self, title, container):
+        """Creates a window to display the contents of a box
+           @type title: string
+           @param title: The title for the window
+           @param items: The box to display
+           @return: A new ContainerGui"""
+        events = {'takeAllButton':self.hideContainer,
+                  'closeButton':self.hideContainer}
+        #hide previous container if any, to avoid orphaned dialogs
+        self.hideContainer()
+
+        self.box_container = ContainerGUI(self.controller,
+                                              unicode(title), container)
+        self.box_container.gui.mapEvents(events)
+        self.box_container.showContainer()
+        return self.box_container
+
+    def hideContainer(self):
+        """Hide the container box
+           @return: None"""
+        if self.box_container:
+            self.box_container.hideContainer()
+            #TODO: Move the close() call into OpenBoxAction(). This can be done 
+            # after createBoxGUI becomes a blocking call or it's otherwise
+            # possible to wait till the box GUI is closed.
+            if self.box_container.container.trueAttr("openable"):
+                self.box_container.container.close()
+            self.box_container = None
+
+    def createExamineBox(self, title, desc):
+        """Create an examine box. It displays some textual description of an
+           object
+           @type title: string
+           @param title: The title of the examine box
+           @type desc: string
+           @param desc: The main body of the examine box
+           @return: None"""
+        if self.examine_box is not None:
+            self.examine_box.closePopUp()
+        self.examine_box = ExaminePopup(self.engine, title, desc)
+        self.examine_box.showPopUp()
+
+    def showDialogue(self, npc):
+        """Show the NPC dialogue window
+           @type npc: actors.NonPlayerCharacter
+           @param npc: the npc that we are having a dialogue with
+           @return: The dialogue"""
+        self.stopActions()
+        dialogue = DialogueGUI(
+                    self.controller,
+                    npc,
+                    self.model.game_state.quest_engine,
+                    self.model.game_state.player_character)
+        return dialogue
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/inventorygui.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,365 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+from fife.extensions.pychan.tools import callbackWithArguments as cbwa
+from fife.extensions import pychan
+from fife.extensions.pychan.attrs import UnicodeAttr
+
+from parpg.gui import drag_drop_data as data_drag
+from parpg.objects.base import Container
+from parpg.gui.containergui_base import ContainerGUIBase
+from parpg.objects.action import ACTIONS
+
+import logging
+
+logger = logging.getLogger('action')
+
+class EquipmentSlot(pychan.VBox):
+    ATTRIBUTES = pychan.VBox.ATTRIBUTES + [UnicodeAttr('label_text')]
+    
+    def _setLabelText(self, text):
+        label = self.findChild()
+        label.text = unicode(text)
+        label.resizeToContent()
+        self.margins = (
+            int((self.width - label.width) / 2.0),
+            int((self.height - label.height) / 2.0)
+        )
+    
+    def _getLabelText(self):
+        label = self.findChild()
+        return label.text
+    
+    label_text = property(fget=_getLabelText, fset=_setLabelText)
+    
+    def __init__(self, label_text=u'equipment', min_size=(50, 50),
+                 max_size=(50, 50), margins=None,
+                 background_image="gui/inv_images/inv_background.png",
+                 **kwargs):
+        pychan.VBox.__init__(self, min_size=min_size, max_size=max_size,
+                             **kwargs)
+        self.background_image = background_image
+        label = pychan.Label(text=unicode(label_text))
+        self.addChild(label)
+        self.label_text = label_text
+        self.adaptLayout()
+        if self.parent is not None:
+            self.beforeShow()
+
+
+class InventoryGrid(pychan.VBox):
+    ATTRIBUTES = pychan.VBox.ATTRIBUTES + [pychan.attrs.PointAttr('grid_size')]
+    
+    def _setNColumns(self, n_columns):
+        n_rows = self.grid_size[1]
+        self.grid_size = (n_columns, n_rows)
+    
+    def _getNColumns(self):
+        n_columns = self.grid_size[0]
+        return n_columns
+    n_columns = property(fget=_getNColumns, fset=_setNColumns)
+    
+    def _setNRows(self, n_rows):
+        n_columns = self.grid_size[0]
+        self.grid_size = (n_columns, n_rows)
+    
+    def _getNRows(self):
+        n_rows = self.grid_size[1]
+        return n_rows
+    n_rows = property(fget=_getNRows, fset=_getNColumns)
+    
+    def _setGridSize(self, grid_size):
+        n_columns, n_rows = grid_size
+        self.removeAllChildren()
+        for row_n in range(n_rows):
+            row_size = (n_columns * 50, 50)
+            row = pychan.HBox(min_size=row_size, max_size=row_size,
+                              padding=self.padding)
+            row.border_size = 1
+            row.opaque = 0
+            for column_n in range(n_columns):
+                slot = pychan.Icon(min_size=(50, 50), max_size=(50, 50))
+                slot.border_size = 1
+                row.addChild(slot)
+            self.addChild(row)
+        self.min_size = ((n_columns * 50) + 2, (n_rows * 50) + 2)
+        self.max_size = self.min_size
+    
+    def _getGridSize(self):
+        n_rows = len(self.children)
+        n_columns = len(self.children[0].children)
+        return (n_rows, n_columns)
+    grid_size = property(fget=_getGridSize, fset=_setGridSize)
+    
+    def __init__(self, grid_size=(2, 2), padding=0, **kwargs):
+        pychan.VBox.__init__(self, padding=padding, **kwargs)
+        self.opaque = 0
+        self.grid_size = grid_size
+        self.border_size = 1
+
+
+class InventoryGUI(ContainerGUIBase):
+    def __init__(self, controller, inventory, callbacks):
+        super(InventoryGUI, self).__init__(controller, "gui/inventory.xml")
+        self.engine = controller.engine
+        self.inventory_shown = False
+        render_backend = self.engine.getRenderBackend()
+        screen_mode = render_backend.getCurrentScreenMode()
+        screen_width, screen_height = (screen_mode.getWidth(),
+                                       screen_mode.getHeight())
+        widget_width, widget_height = self.gui.size
+        self.gui.position = ((screen_width - widget_width) / 2,
+                             (screen_height - widget_height) / 2)
+    
+    def toggleInventory(self, toggleImage=True):
+        """Pause the game and enter the inventory screen, or close the
+           inventory screen and resume the game.
+           @type toggleImage: bool
+           @param toggleImage:
+               Call toggleInventoryCallback if True. Toggling via a
+               keypress requires that we toggle the Hud inventory image
+               explicitly. Clicking on the Hud inventory button toggles the
+               image implicitly, so we don't change it.
+           @return: None"""
+        if not self.inventory_shown:
+            self.showInventory()
+            self.inventory_shown = True
+        else:
+            self.closeInventory()
+            self.inventory_shown = False
+    
+    def showInventory(self):
+        self.gui.show()
+    
+    def closeInventory(self):
+        self.gui.hide()
+
+
+class _InventoryGUI(ContainerGUIBase):
+    """Inventory GUI class"""
+    def __init__(self, controller, inventory, callbacks):
+        """Initialise the instance.
+           @param controller: Current Controller
+           @type controller: Class derived from ControllerBase
+           @type inventory: Inventory
+           @param inventory: An inventory object to be displayed and manipulated
+           @type callbacks: dict
+           @param callbacks: a dict of callbacks
+               refreshReadyImages:
+                   Function that will make the ready slots on the HUD
+                   reflect those within the inventory
+               toggleInventoryButton:
+                   Function that will toggle the state of the inventory button
+           @return: None"""
+        super(InventoryGUI, self).__init__(controller, "gui/inventory.xml")
+        self.engine = controller.engine
+        self.readyCallback = callbacks['refreshReadyImages']
+        self.toggleInventoryButtonCallback = callbacks['toggleInventoryButton']
+        self.original_cursor_id = self.engine.getCursor().getId()
+
+        self.inventory_shown = False
+        events_to_map = {}
+        self.inventory_storage = inventory
+        
+        # Buttons of inventory arranged by slots
+
+        self.slot_buttons = {'head': ('Head',), 'chest': ('Body',),
+                             'left_arm': ('LeftHand',),
+                             'right_arm': ('RightHand',),
+                             'hips' : ('Belt',), 'left_leg': ('LeftFoot',),
+                             'right_leg': ('RightFoot',),
+                             'left_hand': ('LeftHeld',),
+                             'right_hand': ('RightHeld',),
+                             'backpack': ('A1', 'A2', 'A3', 'A4', 'A5',
+                                          'B1', 'B2', 'B3', 'B4', 'B5',
+                                          'C1', 'C2', 'C3', 'C4', 'C5',
+                                          'D1', 'D2', 'D3', 'D4', 'D5'),
+                             'ready': ('Ready1', 'Ready2', 'Ready3', 'Ready4')
+        }
+        # the images that should be used for the buttons when they are "empty"
+        self.slot_empty_images = {'head':'gui/inv_images/inv_head.png',
+                                  'chest':'gui/inv_images/inv_torso.png',
+                                  'left_arm':'gui/inv_images/inv_lhand.png',
+                                  'right_arm':'gui/inv_images/inv_rhand.png',
+                                  'hips':'gui/inv_images/inv_belt.png',
+                                  'left_leg':'gui/inv_images/inv_lfoot.png',
+                                  'right_leg':'gui/inv_images/inv_rfoot.png',
+                                  'left_hand':'gui/inv_images/inv_litem.png',
+                                  'right_hand':'gui/inv_images/inv_ritem.png',
+                                  'backpack':'gui/inv_images/inv_backpack.png',
+                                  'ready':'gui/inv_images/inv_belt_pouches.png',
+                                  }
+        self.updateInventoryButtons()
+
+        for slot in self.slot_buttons:
+            for _, button in enumerate(self.slot_buttons[slot]):
+                events_to_map[button] = cbwa(self.dragDrop, button)
+                events_to_map[button + "/mouseReleased"] = \
+                                                self.showContextMenu
+        events_to_map['close_button'] = self.closeInventoryAndToggle
+        self.gui.mapEvents(events_to_map)
+        # TODO: Why the commented out code?
+        # self.resetMouseCursor()
+
+    def updateImages(self):
+        self.updateInventoryButtons()
+    
+    def updateInventoryButtons (self):
+        for slot in self.slot_buttons:
+            for index, button in enumerate(self.slot_buttons[slot]):
+                widget = self.gui.findChild(name=button)
+                widget.slot = slot
+                widget.index = index
+                widget.item = self.inventory_storage.getItemsInSlot(widget.slot,
+                                                                   widget.index)
+                self.updateImage(widget)
+                
+    def updateImage(self, button):
+        if (button.item == None):
+            image = self.slot_empty_images[button.slot]
+        else:
+            image = button.item.getInventoryThumbnail()
+        button.up_image = image
+        button.down_image = image
+        button.hover_image = image
+
+    def closeInventory(self):
+        """Close the inventory.
+           @return: None"""
+        self.gui.hide()
+
+    def closeInventoryAndToggle(self):
+        """Close the inventory screen.
+           @return: None"""
+        self.closeInventory()
+        self.toggleInventoryButtonCallback()
+        self.inventory_shown = False
+
+    def toggleInventory(self, toggleImage=True):
+        """Pause the game and enter the inventory screen, or close the
+           inventory screen and resume the game.
+           @type toggleImage: bool
+           @param toggleImage:
+               Call toggleInventoryCallback if True. Toggling via a
+               keypress requires that we toggle the Hud inventory image
+               explicitly. Clicking on the Hud inventory button toggles the
+               image implicitly, so we don't change it.
+           @return: None"""
+        if not self.inventory_shown:
+            self.showInventory()
+            self.inventory_shown = True
+        else:
+            self.closeInventory()
+            self.inventory_shown = False
+
+        if toggleImage:
+            self.toggleInventoryButtonCallback()
+
+    def showInventory(self):
+        """Show the inventory.
+           @return: None"""
+        self.updateInventoryButtons()
+        self.gui.show()                
+                
+    def dragObject(self, obj):
+        """Drag the selected object.
+           @type obj: string
+           @param obj: The name of the object within
+                       the dictionary 'self.buttons'
+           @return: None"""
+        # get the widget from the inventory with the name obj
+        drag_widget = self.gui.findChild(name = obj)
+        drag_item = drag_widget.item
+        # only drag if the widget is not empty
+        if (drag_item != None):
+            # get the item that the widget is 'storing'
+            data_drag.dragged_item = drag_widget.item
+            # get the up and down images of the widget
+            up_image = drag_widget.up_image
+            down_image = drag_widget.down_image
+            # set the mouse cursor to be the widget's image
+            self.controller.setMouseCursor(up_image.source,down_image.source)
+            data_drag.dragged_image = up_image.source
+            data_drag.dragging = True
+            data_drag.dragged_widget = drag_widget
+            data_drag.source_container = self.inventory_storage
+            
+            self.inventory_storage.takeItem(drag_widget.item)
+            # after dragging the 'item', set the widgets' images
+            # so that it has it's default 'empty' images
+            drag_widget.item = None
+            self.updateImage(drag_widget)
+            
+            
+    def dropObject(self, obj):
+        """Drops the object being dropped
+           @type obj: string
+           @param obj: The name of the object within
+                       the dictionary 'self.buttons' 
+           @return: None"""
+        drop_widget = self.gui.findChild(name = obj)
+        drop_slot, drop_index = drop_widget.slot, drop_widget.index
+        replace_item = None
+        try :
+            if data_drag.dragging:
+                inventory = self.inventory_storage
+                drag_item = data_drag.dragged_item
+                #this will get the replacement item and data for drag_drop if
+                ## there is an item All ready occupying the slot
+                if not inventory.isSlotEmpty(drop_slot, drop_index):
+                    #get the item and then remove it from the inventory
+                    replace_item = inventory.getItemsInSlot \
+                                                (drop_slot, drop_index)
+                    self.dragObject(obj)
+                self.inventory_storage.moveItemToSlot(drag_item,
+                                                      drop_slot,
+                                                      drop_index)
+                    
+            if drop_widget.slot == 'ready':
+                self.readyCallback()
+            
+            if replace_item == None:
+                self.controller.resetMouseCursor()
+                data_drag.dragging = False
+        except Container.TooBig :
+            logger.warning("%s too big to fit "
+                                 "into %s" % (data_drag.dragged_item,
+                                              drop_widget.slot))
+        except (Container.SlotBusy, Container.ItemSelf):
+            pass
+        self.updateInventoryButtons()
+              
+    def createMenuItems(self, item, actions):
+        """Creates context menu items for the InventoryGUI"""
+        menu_actions = super(InventoryGUI, self).createMenuItems(item, actions)
+        param_dict = {}
+        param_dict["controller"] = self.controller
+        param_dict["commands"] = {}
+        param_dict["item"] = item
+        param_dict["container_gui"] = self
+        menu_actions.append(["Drop",
+                             "Drop", 
+                             self.executeMenuItem, 
+                             ACTIONS["DropFromInventory"](**param_dict)])        
+        return menu_actions
+    
+    def getImage(self, name):
+        """Return a current image from the inventory
+           @type name: string
+           @param name: name of image to get
+           @return: None"""
+        return self.gui.findChild(name = name)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/menus.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,156 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import logging
+
+from fife.extensions import pychan
+from dialogs import RestartDialog
+
+logger = logging.getLogger('menus')
+
+class ContextMenu(object):
+    def __init__(self, engine, menu_items, pos):
+        """@type engine: engine.Engine
+           @param engine: An instance of the class Engine from engine.py 
+           @type menu_items: list
+           @param menu_items: A list of items containing the name and 
+                              text for the menu item and callback
+                              i.e. [["menu", "Some text",  Callback]
+           @type pos: (int, int)
+           @param pos: Screen position to use 
+           @return: None"""
+        self.vbox = pychan.widgets.VBox(position=pos)
+        events_to_map = {}
+        for item in menu_items:
+            p = pychan.widgets.Button(name=item[0], text=unicode(item[1]))
+            self.vbox.addChild(p)
+            events_to_map [item[0]] = self.actionDecorator(*item[2:])
+        self.vbox.mapEvents(events_to_map)
+    
+    def show(self):
+        """Shows the context menu"""
+        self.vbox.show()
+    def hide(self):
+        """Hides the context menu"""
+        self.vbox.hide()
+        
+    def actionDecorator (self,func, *args, **kwargs):
+        """This function is supposed to add some generic that should be
+        executed before and/or after an action is fired through the
+        context menu.
+        
+        @type func: Any callable
+        @param func: The original action function
+        @param args: Unpacked list of positional arguments
+        @param kwargs: Unpacked list of keyword arguments
+        @return: A wrapped version of func"""
+        def decoratedFunc ():
+            """ This is the actual wrapped version of func, that is returned.
+            It takes no external arguments, so it can safely be passed around
+            as a callback."""
+            # some stuff that we do before the actual action is executed
+            self.hide()
+            # run the action function, and record the return value
+            ret_val = func (*args,**kwargs)
+            # we can eventually add some post-action code here, if needed (e.g. logging)
+            pass        
+            # return the value, as if the original function was called
+            return ret_val
+        return decoratedFunc
+        
+class SettingsMenu(object):
+    def __init__(self, engine, settings):
+        self.engine = engine
+        self.settings = settings
+
+        width = self.settings.fife.ScreenWidth
+        height = self.settings.fife.ScreenHeight
+
+        # available options
+        screen_modes = self.engine.getDeviceCaps().getSupportedScreenModes()
+        resolutions = list(set([(mode.getWidth(), mode.getHeight())
+                                for mode in screen_modes]))
+        self.resolutions = ["{0}x{1}".format(item[0], item[1])
+                            for item in sorted(resolutions)[1:]]
+
+        self.render_backends = ['OpenGL', 'SDL']
+        self.lighting_models = range(3)
+
+        # selected options
+        self.resolution = "{0}x{1}".format(width, height)
+        self.render_backend = self.settings.fife.RenderBackend
+        self.lighting_model = self.settings.fife.Lighting
+        self.fullscreen = self.settings.fife.FullScreen
+        self.sound = self.settings.fife.EnableSound
+
+        self.window = pychan.loadXML(os.path.join(self.settings.system_path,
+                                                 self.settings.parpg.GuiPath,
+                                                 'settings_menu.xml'))
+        self.restart_dialog = RestartDialog(self.settings)
+        self.window.mapEvents({'okButton': self.save,
+                               'cancelButton': self.hide,
+                               'defaultButton': self.reset})
+        self.initializeWidgets()
+        self.fillWidgets()
+
+    def initializeWidgets(self):
+        initial_data = {'screen_resolution': self.resolutions,
+                        'render_backend': self.render_backends,
+                        'lighting_model': self.lighting_models}
+
+        self.window.distributeInitialData(initial_data)
+
+
+    def fillWidgets(self):
+        resolution = self.resolutions.index(self.resolution)
+        backend = self.render_backends.index(self.render_backend)
+        lighting = self.lighting_models.index(self.lighting_model)
+
+        self.window.distributeData({'screen_resolution': resolution,
+                                    'render_backend': backend,
+                                    'lighting_model': lighting,
+                                    'enable_fullscreen': self.fullscreen,
+                                    'enable_sound': self.sound})
+
+    def show(self):
+        self.window.show()
+
+    def hide(self):
+        self.window.hide()
+
+    def reset(self):
+        self.settings.read(self.settings.system_path)
+        self.fillWidgets()
+
+    def save(self):
+        (resolution, backend, lighting,
+         fullscreen, sound) = self.window.collectData('screen_resolution',
+                                                      'render_backend',
+                                                      'lighting_model', 
+                                                      'enable_fullscreen',
+                                                      'enable_sound')
+
+        width, height = self.resolutions[resolution].split('x')
+        self.settings.fife.ScreenWidth = width
+        self.settings.fife.ScreenHeight = height
+        self.settings.fife.RenderBackend = self.render_backends[backend]
+        self.settings.fife.Lighting = self.lighting_models[lighting]
+        self.settings.fife.FullScreen = fullscreen
+        self.settings.fife.EnableSound = sound
+        self.settings.write()
+       
+        self.restart_dialog.show()
+        self.hide()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/popups.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,60 @@
+#/usr/bin/python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+from fife.extensions import pychan
+
+class ExaminePopup():
+    """Create a popup for when you click examine on an object"""
+    def __init__(self, engine, object_title, desc):
+        """Initialize the popup  
+           @type engine: fife.Engine
+           @param engine: an instance of the fife engine
+           @type object_title: string
+           @param object_title: The title for the window, probably should just
+                                be the name of the object
+           @type desc: string
+           @param desc: The description of the object
+           @return: None"""
+        self.engine = engine
+
+        self.examine_window = pychan.widgets.\
+                                Window(title=unicode(object_title),
+                                       position_technique="center:center",
+                                       min_size=(175,175))
+
+        self.scroll = pychan.widgets.ScrollArea(name='scroll', size=(150,150))
+        self.description = pychan.widgets.Label(name='descText',
+                                                text=unicode(desc),
+                                                wrap_text=True)
+        self.description.max_width = 170
+        self.scroll.addChild(self.description)
+        self.examine_window.addChild(self.scroll)
+        
+        self.close_button = pychan.widgets.Button(name='closeButton',
+                                                  text=unicode('Close'))
+        self.examine_window.addChild(self.close_button)
+
+        self.examine_window.mapEvents({'closeButton':self.examine_window.hide})
+
+    def closePopUp(self):
+        # TODO: missing function information
+        if self.examine_window.isVisible():
+            self.examine_window.hide()
+    
+    def showPopUp(self):
+        # TODO: missing function information
+        self.examine_window.show()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/spinners.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,234 @@
+from fife.extensions.pychan.widgets import (ImageButton, TextField, HBox,
+    Spacer)
+from fife.extensions.pychan.attrs import Attr, IntAttr, BoolAttr
+
+class ListAttr(Attr):
+    def parse(self, value):
+        list_ = value.split(',')
+        return list_
+
+
+class Spinner(HBox):
+    ATTRIBUTES = HBox.ATTRIBUTES + [
+        ListAttr('items'),
+        IntAttr('default_item_n'),
+        BoolAttr('circular'),
+    ]
+    
+    def default_item_n():
+        def fget(self):
+            return self._default_item_n
+        
+        def fset(self, index):
+            if len(self.items) -1 >= index:
+                self._default_item_n = index
+                self.text_field.text = self.items[index]
+            else:
+                error_message = \
+                    'default_item_n exceeds number of items in spinner'
+                raise ValueError(error_message)
+        
+        return locals()
+    default_item_n = property(**default_item_n())
+    
+    def items():
+        def fget(self):
+            return self._items
+        
+        def fset(self, items):
+            self._items = map(unicode, items)
+            if self.default_item_n > len(items) - 1:
+                self.default_item_n = 0
+            self.text_field.text = self.items[self.default_item_n] if \
+                                   len(self.items) > 0 else u''
+        
+        return locals()
+    items = property(**items())
+    
+    def background_color():
+        def fget(self):
+            return self.text_field.background_color
+        
+        def fset(self, background_color):
+            self.text_field.background_color = background_color
+        
+        return locals()
+    background_color = property(**background_color())
+    
+    def font():
+        def fget(self):
+            return self.text_field.font
+        
+        def fset(self, font):
+            self.text_field.font = font
+        
+        return locals()
+    font = property(**font())
+    
+    def background_color():
+        def fget(self):
+            return self.text_field.background_color
+        
+        def fset(self, background_color):
+            self.text_field.background_color = background_color
+        
+        return locals()
+    background_color = property(**background_color())
+    
+    def min_size():
+        def fget(self):
+            return self._min_size
+        
+        def fset(self, min_size):
+            self._min_size = min_size
+            self.decrease_button.capture(self.previousItem)
+            increase_button_width, increase_button_height = \
+                self.increase_button.size
+            decrease_button_width, decrease_button_height = \
+                self.decrease_button.size
+            text_field_width = min_size[0] - (2 * self.padding) - \
+                               (increase_button_width + decrease_button_width)
+            self.text_field.min_width = text_field_width
+            self.text_field.max_width = text_field_width
+            self.text_field.min_height = min_size[1]
+        
+        return locals()
+    min_size = property(**min_size())
+    
+    
+    def max_size():
+        def fget(self):
+            return self._max_size
+        
+        def fset(self, max_size):
+            self._max_size = max_size
+            self.decrease_button.capture(self.previousItem)
+            increase_button_width, increase_button_height = \
+                self.increase_button.size
+            decrease_button_width, decrease_button_height = \
+                self.decrease_button.size
+            text_field_width = max_size[0] - (2 * self.padding) - \
+                               (increase_button_width + decrease_button_width)
+            self.text_field.max_width = text_field_width
+            self.text_field.max_height = max_size[1]
+        
+        return locals()
+    max_size = property(**max_size())
+    
+    def __init__(self, items=None, default_item_n=0, circular=True,
+                 min_size=(50, 14), max_size=(50, 14), font=None, background_color=None, **kwargs):
+        self._current_index = 0
+        self._items = map(unicode, items) if items is not None else []
+        self._default_item_n = default_item_n
+        self._min_size = min_size
+        self.circular = circular
+        padding = 1
+        self.text_field = TextField(background_color=background_color)
+        self.decrease_button = ImageButton(
+            up_image='gui/buttons/left_arrow_up.png',
+            down_image='gui/buttons/left_arrow_down.png',
+            hover_image='gui/buttons/left_arrow_hover.png',
+        )
+        # FIXME Technomage 2011-03-05: This is a hack to prevent the button
+        #     from expanding width-wise and skewing the TextField orientation.
+        #     Max size shouldn't be hard-coded like this though...
+        self.decrease_button.max_size = (12, 12)
+        self.decrease_button.capture(self.previousItem)
+        self.increase_button = ImageButton(
+            up_image='gui/buttons/right_arrow_up.png',
+            down_image='gui/buttons/right_arrow_down.png',
+            hover_image='gui/buttons/right_arrow_hover.png',
+        )
+        self.increase_button.capture(self.nextItem)
+        increase_button_width, increase_button_height = \
+            self.increase_button.size
+        decrease_button_width, decrease_button_height = \
+            self.decrease_button.size
+        self.text_field = TextField(font=font)
+        text_field_width = min_size[0] - (2 * padding) - \
+                           (increase_button_width + decrease_button_width)
+        self.text_field.min_width = text_field_width
+        self.text_field.max_width = text_field_width
+        self.text_field.min_height = min_size[1]
+        self.text_field.text = self.items[default_item_n] if \
+                               len(self.items) > 0 else u''
+        HBox.__init__(self, **kwargs)
+        self.opaque = 0
+        self.padding = padding
+        self.margins = (0, 0)
+        self.addChildren(self.decrease_button, self.text_field,
+                         self.increase_button)
+    
+    def nextItem(self, event, widget):
+        if self.circular:
+            if self._current_index < len(self.items) - 1:
+                self._current_index += 1
+            else:
+                self._current_index = 0
+            self.text_field.text = self.items[self._current_index]
+        elif self._current_index < len(self.items) - 1:
+            self._current_index += 1
+            self.text_field.text = self.items[self._current_index]
+    
+    def previousItem(self, event, widget):
+        if self.circular:
+            if self._current_index > 0:
+                self._current_index -= 1
+            else:
+                self._current_index = len(self.items) - 1
+            self.text_field.text = self.items[self._current_index]
+        elif self._current_index > 0:
+            self._current_index -= 1
+            self.text_field.text = self.items[self._current_index]
+
+
+class IntSpinner(Spinner):
+    ATTRIBUTES = Spinner.ATTRIBUTES + [
+        IntAttr('lower_limit'),
+        IntAttr('upper_limit'),
+        IntAttr('step_size'),
+    ]
+    
+    def lower_limit():
+        def fget(self):
+            return self._lower_limit
+        
+        def fset(self, lower_limit):
+            self._lower_limit = lower_limit
+            integers = range(lower_limit, self.upper_limit + 1, self.step_size)
+            self.items = integers
+        
+        return locals()
+    lower_limit = property(**lower_limit())
+    
+    def upper_limit():
+        def fget(self):
+            return self._upper_limit
+        
+        def fset(self, upper_limit):
+            self._upper_limit = upper_limit
+            integers = range(self.lower_limit, upper_limit + 1, self.step_size)
+            self.items = integers
+        
+        return locals()
+    upper_limit = property(**upper_limit())
+    
+    def step_size():
+        def fget(self):
+            return self._step_size
+        
+        def fset(self, step_size):
+            self._step_size = step_size
+            integers = range(self.lower_limit, self.upper_limit + 1, step_size)
+            self.items = integers
+        
+        return locals()
+    step_size = property(**step_size())
+    
+    def __init__(self, lower_limit=0, upper_limit=100, step_size=1, **kwargs):
+        self._lower_limit = lower_limit
+        self._upper_limit = upper_limit
+        self._step_size = step_size
+        integers = range(lower_limit, upper_limit + 1, step_size)
+        Spinner.__init__(self, items=integers, **kwargs)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/gui/tabwidget.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,105 @@
+from fife.extensions.pychan.widgets import VBox, HBox, ScrollArea, Button
+from fife.extensions.pychan.tools import callbackWithArguments
+
+class TabWidget(VBox):
+    def min_size():
+        def fget(self):
+            return self._min_size
+        
+        def fset(self, min_size):
+            self._min_size = min_size
+            self.view.min_size = min_size
+            self.adaptLayout()
+        
+        return locals()
+    min_size = property(**min_size())
+    
+    def max_size():
+        def fget(self):
+            return self._max_size
+        
+        def fset(self, max_size):
+            self._max_size = max_size
+            self.view.max_size = max_size
+            self.adaptLayout()
+        
+        return locals()
+    max_size = property(**max_size())
+    
+    def opaque():
+        def fget(self):
+            return self._getOpaque()
+        
+        def fset(self, opaque):
+            self._setOpaque(opaque)
+            self.view.opaque = opaque
+            base_color = self.view.base_color
+            base_red = base_color.r
+            base_green = base_color.g
+            base_blue = base_color.b
+            background_color = self.view.background_color
+            background_red = background_color.r
+            background_green = background_color.g
+            background_blue = background_color.b
+            alpha = 255 if opaque else 0
+            self.view.base_color = (base_red, base_green, base_blue, alpha)
+            self.view.background_color = (background_red, background_green,
+                                          background_blue, alpha)
+        
+        return locals()
+    opaque = property(**opaque())
+    
+    def border_size():
+        def fget(self):
+            frame = self.findChild(name='frame')
+            return frame.border_size
+        
+        def fset(self, border_size):
+            frame = self.findChild(name='frame')
+            frame.border_size = border_size
+        
+        return locals()
+    border_color = property(**border_size())
+    
+    def __init__(self, min_size=(0, 0), max_size=(9999, 9999), border_size=1,
+                 **kwargs):
+        self._min_size = min_size
+        self._max_size = max_size
+        self.views = {}
+        tab_bar = HBox(name='tabBar')
+        tab_bar.min_size = (0, 20)
+        tab_bar.max_size = (9999, 20)
+        self.view = ScrollArea(name='view')
+        self.view.min_size = self._min_size
+        self.view.max_size = self._max_size
+        self.view.border_size = border_size
+        frame = VBox(name='frame')
+        frame.border_size = border_size
+        frame.opaque = 0
+        frame.addChild(self.view)
+        VBox.__init__(self, **kwargs)
+        self.padding = 0
+        VBox.addChild(self, tab_bar)
+        VBox.addChild(self, frame)
+        self.adaptLayout()
+    
+    def addTab(self, text):
+        text = unicode(text)
+        tab = Button(text=text)
+        tab_bar = self.findChild(name='tabBar')
+        tab_bar.addChild(tab)
+        tab.capture(callbackWithArguments(self.showView, text))
+        self.adaptLayout()
+    
+    def addChild(self, child):
+        name = child.name or unicode(str(child))
+        self.addTab(name)
+        self.views[name] = child
+        if len(self.views) == 1:
+            # Show the first view by default.
+            self.showView(name)
+    
+    def showView(self, name):
+        view = self.views[name]
+        self.view.content = view
+        self.adaptLayout()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/inventory.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,177 @@
+# This file is part of PARPG.
+# 
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+# 
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+from parpg.objects.base import Container
+from parpg.objects.composed import SingleItemContainer as Slot
+
+import copy
+
+# TODO: many missing function definitions in this code
+
+class Inventory(Container):
+    """The class to represent inventory 'model': allow operations with
+       inventory contents, perform weight/bulk calculations, etc"""
+    def __init__(self, **kwargs):
+        """Initialise instance"""
+        Container.__init__(self,  **kwargs)
+        self.items = {"head": Slot(), "neck": Slot(),
+                      "shoulders": Slot(), "chest": Slot(),
+                      "abdomen": Slot(), "left_arm": Slot(),
+                      "right_arm": Slot(),"groin": Slot(),
+                      "hips": Slot(), "left_leg": Slot(),
+                      "right_leg": Slot(), "left_hand": Slot(),
+                      "right_hand": Slot(), "ready": Container(),
+                      "backpack": Container()}
+        for key, item in self.items.iteritems():
+            item.name = key
+            kwargs = {}
+            kwargs["container"] = item
+            item.setScript("onPlaceItem", self.onChildPlaceItem, kwargs = kwargs)
+        self.item_lookup = {}
+        
+    def onChildPlaceItem(self, container):
+        for item in container.items.itervalues():
+            self.item_lookup[item.ID] = container.name  
+
+    def placeItem(self, item, index=None):
+        self.items["backpack"].placeItem(item, index)
+        #self.item_lookup[item.ID] = "backpack"
+        
+    def takeItem(self, item):
+        if not item.ID in self.item_lookup:
+            raise ValueError ('I do not contain this item: %s' % item)
+        self.items[self.item_lookup[item.ID]].takeItem(item)
+        del self.item_lookup[item.ID]
+
+    def removeItem(self, item):
+        if not item.ID in self.item_lookup:
+            raise ValueError ('I do not contain this item: %s' % item)
+        self.items[self.item_lookup[item.ID]].removeItem(item)
+        del self.item_lookup[item.ID]
+
+    def replaceItem(self, old_item, new_item):
+        """Replaces the old item with the new one
+        @param old_item: Old item which is removed
+        @type old_item: Carryable
+        @param new_item: New item which is added
+        @type new_item: Carryable
+        """
+        if not old_item.ID in self.item_lookup:
+            raise ValueError ('I do not contain this item: %s' % old_item)
+        self.items[self.item_lookup[old_item.ID]]\
+            .replaceItem(old_item, new_item)
+
+    def getWeight(self):
+        """Total weight of all items in container + container's own weight"""
+        return sum((item.weight for item in self.items.values()))
+
+    def setWeightDummy(self, weight):
+        pass
+
+    weight = property(getWeight, setWeightDummy, "Total weight of container")
+
+
+    def count(self, item_type = ""):
+        return sum(item.count(item_type) for item in self.items.values())
+    
+    def takeOff(self, item):
+        return self.moveItemToSlot(item, "backpack")
+
+    def moveItemToSlot(self,item,slot,index=None):
+        if not slot in self.items:
+            raise(ValueError("%s: No such slot" % slot))
+
+        if item.ID in self.item_lookup:
+            self.items[self.item_lookup[item.ID]].takeItem(item)
+        try:
+            self.items[slot].placeItem(item, index)
+        except Container.SlotBusy:
+            if index == None :
+                offending_item = self.items[slot].items[0]
+            else :
+                offending_item = self.items[slot].items[index]
+            self.items[slot].takeItem(offending_item)
+            self.items[slot].placeItem(item, index)
+            self.placeItem(offending_item)
+        self.item_lookup[item.ID] = slot
+     
+    def getItemsInSlot(self, slot, index=None):
+        if not slot in self.items:
+            raise(ValueError("%s: No such slot" % slot))
+        if index != None:
+            return self.items[slot].items.get(index)
+        else:
+            return copy.copy(self.items[slot].items)
+
+    def isSlotEmpty(self, slot, index=None):
+        if not slot in self.items:
+            raise(ValueError("%s: No such slot" % slot))
+        if index == None:
+            return self.items[slot].count() == 0
+        else:
+            return not index in self.items[slot].items
+                 
+
+    def has(self, item_ID):
+        return item_ID in self.item_lookup
+
+    def findItemByID(self, ID):
+        if ID not in self.item_lookup:
+            return None
+        return self.items[self.item_lookup[ID]].findItemByID(ID)
+
+    def findItem(self, **kwargs):
+        """Find an item in inventory by various attributes. All parameters 
+           are optional.
+           @type name: String
+           @param name: Object name. If the name is non-unique, 
+                        first matching object is returned
+           @type kind: String
+           @param kind: One of the possible object kinds like "openable" or 
+                        "weapon" (see base.py)
+           @return: The item matching criteria or None if none was found"""
+        for slot in self.items:
+            item_found = self.items[slot].findItem(**kwargs)
+            if item_found != None:
+                return item_found
+        return None
+
+    def __repr__(self):
+        return "[Inventory contents: " + \
+                            reduce((lambda a,b: str(a) 
+                                    + ', ' + str(b)), 
+                                    self.items.values()) + " ]"
+
+    def serializeInventory(self):
+        """Returns the inventory items as a list"""
+        inventory = []
+        inventory.extend(self.items["backpack"].serializeItems())
+        for key, slot in self.items.iteritems():
+            if key == "ready" or key == "backpack":
+                continue
+            elif len(slot.items) > 0:
+                item = slot.items[0]
+                item_dict = item.getStateForSaving()
+                item_dict["slot"] = key                                
+                item_dict["type"] = type(item).__name__ 
+                inventory.append(item_dict)
+        return inventory
+            
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        state = {}
+        state["Inventory"] = self.serializeInventory()
+        return state
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/main.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,96 @@
+#!/usr/bin/env python2 
+#   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 3 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, see <http://www.gnu.org/licenses/>.
+
+#TODO: Modularize this script
+import sys
+import logging
+
+from optparse import OptionParser
+
+usage = ('usage: %prog [options] settings_path [system_path user_path]\n\n'
+         'The settings_path argument is mandatory and is the directory in \n'
+         'which your system.cfg file is located. Optionally, you may \n'
+         'specify where data files are located (system_path), and where \n'
+         'the user settings and data files should be saved to (user_path)\n\n'
+         'Example: python %prog .')
+
+parser = OptionParser(description='PARPG Launcher Script', usage=usage)
+parser.add_option('-f', '--logfile',
+                  help='Name of log file to save to')
+parser.add_option('-l', '--loglevel', default='critical',
+                  help='desired output level for log file')
+parser.add_option('-m', '--module',
+                  help='location of the parpg module')
+opts, args = parser.parse_args()
+
+if not args:
+    parser.print_help()
+    sys.exit(1)
+            
+
+# initialize settings
+if opts.module:
+    print('added ' + opts.module)
+    sys.path.insert(0, opts.module)
+
+from parpg.settings import Settings
+
+settings = Settings(*args)
+
+levels = {'debug': logging.DEBUG,
+          'info': logging.INFO,
+          'warning': logging.WARNING,
+          'error': logging.ERROR,
+          'critical': logging.CRITICAL}
+
+#TODO: setup formating
+logging.basicConfig(filename=opts.logfile, level=levels[opts.loglevel])
+logger = logging.getLogger('parpg')
+
+try:
+    sys.path.insert(0, settings.parpg.FifePath) 
+except AttributeError:
+    logger.warning('[parpg] section has no FifePath option')
+
+try:
+    from fife import fife
+except ImportError:
+    logger.critical("Could not import fife module. Please install fife or add "
+                     "'FifePath' to the [parpg] section of your settings file")
+    sys.exit(1)
+
+from parpg.application import PARPGApplication
+from parpg.common import utils
+
+# enable psyco if available and in settings file
+try:
+    import psyco
+    psyco_available = True
+except ImportError:
+    logger.warning('Psyco Acceleration unavailable')
+    psyco_available = False
+
+if settings.fife.UsePsyco:
+    if psyco_available:
+        psyco.full()
+        logger.info('Psyco Acceleration enabled')
+    else:
+        logger.warning('Please install psyco before attempting to use it'
+                        'Psyco Acceleration disabled')
+else:
+    logger.info('Psycho Acceleration disabled')
+
+# run the game
+app = PARPGApplication(settings)
+app.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/mainmenucontroller.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,95 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/
+
+from controllerbase import ControllerBase
+from charactercreationview import CharacterCreationView
+from charactercreationcontroller import CharacterCreationController
+from gamescenecontroller import GameSceneController
+from gamesceneview import GameSceneView
+
+#For debugging/code analysis
+if False:
+    from parpg.mainmenuview import MainMenuView
+    from fife import fife
+    from gamemodel import GameModel
+    from parpg import PARPGApplication
+
+class MainMenuController(ControllerBase):
+    """Controller for handling the main menu state"""
+
+    def __init__(self, engine, view, model, application):
+        """Constructor"""
+        super(MainMenuController, self).__init__(engine, view, model, 
+                                                 application)
+    
+        #this can be helpful for IDEs code analysis
+        if False:
+            assert(isinstance(self.engine, fife.Engine))
+            assert(isinstance(self.view, MainMenuView))
+            assert(isinstance(self.model, GameModel))
+            assert(isinstance(self.application, PARPGApplication))
+            assert(isinstance(self.event_manager, fife.EventManager))
+        
+        self.view.quit_callback = self.quitGame
+        self.view.new_game_callback = self.newGame
+        self.view.initalizeMainMenu(self.newGame, self.loadGame, self.quitGame)
+        self.view.showMenu()
+        self.resetMouseCursor()
+    
+    def newGame(self):
+        """Start a new game and switch to the character creation controller."""
+        view = CharacterCreationView(self.engine, self.model,
+                                     self.model.settings)
+        controller = CharacterCreationController(self.engine, view, self.model,
+                                                 self.application)
+        self.application.view = view
+        self.application.switchController(controller)
+    
+#    def newGame(self):
+#        """Starts a new game"""
+#        view = GameSceneView(self.engine,
+#                             self.model)
+#        controller = GameSceneController(self.engine,
+#                                         view,
+#                                         self.model,
+#                                         self.application)        
+#        self.application.view = view
+#        self.application.switchController(controller)
+#        start_map = self.model.settings.get("PARPG", "Map")
+#        self.model.changeMap(start_map)
+
+    def loadGame(self, *args, **kwargs):
+        """Loads the game state
+           @return: None"""
+
+        view = GameSceneView(self.engine,
+                             self.model)
+        controller = GameSceneController(self.engine,
+                                         view,
+                                         self.model,
+                                         self.application)        
+        self.application.view = view
+        self.application.switchController(controller)
+        controller.loadGame(*args, **kwargs)
+        
+    def onStop(self):
+        """Called when the controller is removed from the list"""
+        self.view.hideMenu()
+                                         
+    
+    def quitGame(self):
+        """Quits the game
+           @return: None"""
+        self.application.listener.quitGame()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/mainmenuview.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,151 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/
+
+import os
+
+from fife.extensions import pychan
+
+from viewbase import ViewBase
+from parpg.gui.filebrowser import FileBrowser
+from parpg.gui.menus import SettingsMenu
+
+class MainMenuView(ViewBase):
+    """View that is used to display the main menu"""
+
+    def __init__(self, engine, model):
+        """Constructor for MainMenuView
+           @param engine: A fife.Engine instance
+           @type engine: fife.Engine
+           @param model: a script.GameModel instance
+           @type model: script.GameModel 
+           """        
+        ViewBase.__init__(self, engine, model)
+        self.quit_window = None
+        self.new_game_callback = None
+        self.load_game_callback = None
+        self.quit_callback = None
+        self.main_menu = None
+        self.character_screen = None
+        self.gui_path = os.path.join(self.model.settings.system_path,
+                                     self.model.settings.parpg.GuiPath)
+        
+    def showMenu(self):
+        """"Shows the main menu"""
+        self.main_menu_background.show()
+        self.main_menu.show()
+        
+    def hideMenu(self):
+        """"Hides the main menu"""
+        self.main_menu.hide()
+        self.main_menu_background.hide()
+
+    def initalizeMainMenu(self, new_game, load_game, quit_game):
+        """Initialized the main menu and sets the callbacks"""
+        # Set a simple background to display the main screen.
+        self.main_menu_background = pychan.loadXML(os.path.join(self.gui_path,
+                                                   'main_menu_background.xml'))
+        
+        # Initialize the main menu screen.
+        screen_mode = self.engine.getRenderBackend().getCurrentScreenMode()
+        self.main_menu_background.width = screen_mode.getWidth()
+        self.main_menu_background.height = screen_mode.getHeight()
+        self.main_menu = pychan.loadXML(os.path.join(self.gui_path,
+                                                     'main_menu.xml'))
+
+        # Setup images for variables widgets 
+        self.main_menu.background_image = os.path.join(self.gui_path,
+                                                       'notebook',
+                                                       'notebook_background.png')
+        quit_button = self.main_menu.findChild(name='quitButton')
+        quit_button.up_image = os.path.join(self.gui_path, 'notebook', 'tabs',
+                                            'tab2_bg_dark_bottom.png')
+        quit_button.hover_image = os.path.join(self.gui_path, 'notebook',
+                                               'tabs',
+                                               'tab2_bg_normal_bottom.png')
+        quit_button.down_image = os.path.join(self.gui_path, 'notebook',
+                                              'tabs',
+                                              'tab2_bg_normal_bottom.png')
+
+        self.main_menu.adaptLayout()
+        self.new_game_callback = new_game
+        self.load_game_callback = load_game
+        self.quit_callback = quit_game
+        menu_events = {}
+        menu_events["newButton"] = self.newGame
+        menu_events["loadButton"] = self.loadGame
+        menu_events["settingsButton"] = self.displaySettings
+        menu_events["quitButton"] = self.quitGame
+        self.main_menu.mapEvents(menu_events)
+        
+        self.initializeQuitDialog()
+        self.initializeSettingsMenu()
+    
+    def newGame(self):
+        """Called when user request to start a new game.
+           @return: None"""
+        self.new_game_callback()
+    
+    def loadGame(self):
+        """ Called when the user wants to load a game.
+            @return: None"""
+        load_browser = FileBrowser(self.engine,
+                                   self.model.settings,
+                                   self.load_game_callback,
+                                   gui_xml_path=os.path.join(self.gui_path,
+                                                             'loadbrowser.xml'),
+                                   save_file=False,
+                                   extensions=('.dat'))
+        load_browser.showBrowser()
+    
+    def initializeQuitDialog(self):
+        """Creates the quit confirmation dialog
+           @return: None"""
+        
+        self.quit_window = pychan.widgets.Window(title=unicode("Quit?"), \
+                                                 min_size=(200,0))
+
+        hbox = pychan.widgets.HBox()
+        are_you_sure = "Are you sure you want to quit?"
+        label = pychan.widgets.Label(text=unicode(are_you_sure))
+        yes_button = pychan.widgets.Button(name="yes_button", 
+                                           text=unicode("Yes"),
+                                           min_size=(90,20),
+                                           max_size=(90,20))
+        no_button = pychan.widgets.Button(name="no_button",
+                                          text=unicode("No"),
+                                          min_size=(90,20),
+                                          max_size=(90,20))
+
+        self.quit_window.addChild(label)
+        hbox.addChild(yes_button)
+        hbox.addChild(no_button)
+        self.quit_window.addChild(hbox)
+
+        events_to_map = { "yes_button": self.quit_callback,
+                          "no_button":  self.quit_window.hide }
+        
+        self.quit_window.mapEvents(events_to_map)
+
+
+    def quitGame(self):
+        """Called when user requests to quit game.
+           @return: None"""
+        self.quit_window.show()
+
+    def initializeSettingsMenu(self):
+        self.settings_menu = SettingsMenu(self.engine, self.model.settings)
+
+    def displaySettings(self):
+        self.settings_menu.show()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/__init__.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,54 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import containers
+import doors
+import actors
+import items
+import sys
+
+OBJECT_MODULES = [containers, actors, doors, items]
+
+def getAllObjects ():
+    """Returns a dictionary with the names of the concrete game object classes
+       mapped to the classes themselves"""
+    result = {}
+    for module in OBJECT_MODULES:
+        for class_name in module.__all__:
+            result[class_name] = getattr (module, class_name)
+    return result
+
+def createObject(info, extra = None):
+    """Called when we need to get an actual object.
+       @type info: dict
+       @param info: stores information about the object we want to create
+       @type extra: dict
+       @param extra: stores additionally required attributes
+       @return: the object"""
+    # First, we try to get the type and ID, which every game_obj needs.
+    extra = extra or {}
+    try:
+        obj_type = info.pop('type')
+        ID = info.pop('id')
+    except KeyError:
+        sys.stderr.write("Error: Game object missing type or id.")
+        sys.exit(False)
+
+    # add the extra info
+    for key, val in extra.items():
+        info[key] = val
+
+    # this is for testing purposes
+    return getAllObjects()[obj_type](ID, **info)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/action.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,548 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+#exceptions
+
+import logging
+
+logger = logging.getLogger('action')
+
+from parpg.gui import drag_drop_data as data_drag
+
+class NoSuchQuestException(Exception):
+    """NoQuestException is used when there is no active quest with the id"""
+    pass
+
+#classes
+
+class Action(object):
+    """Base Action class, to define the structure"""
+
+
+    def __init__(self, controller, commands = None):
+        """Basic action constructor
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        """
+        self.commands = commands or ()
+        self.controller = controller
+        self.model = controller.model
+    
+    def execute(self):
+        """To be overwritten"""        
+        #Check if there are special commands and execute them
+        for command_data in self.commands:
+            command = command_data["Command"]
+            if command == "SetQuestVariable":
+                quest_id = command_data["ID"]
+                variable = command_data["Variable"]
+                value = command_data["Value"]
+                quest_engine = self.model.game_state.quest_engine 
+                if quest_engine.hasQuest(quest_id):
+                    quest_engine[quest_id].setValue(variable, value)
+                else:
+                    raise NoSuchQuestException
+            elif command == "ResetMouseCursor":
+                self.controller.resetMouseCursor()
+            elif command == "StopDragging":
+                data_drag.dragging = False
+                
+class ChangeMapAction(Action):
+    """A change map scheduled"""
+    def __init__(self, controller, target_map_name, target_pos, commands=None):
+        """Initiates a change of the position of the character
+        possibly flagging a new map to be loaded.
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @type view: class derived from parpg.ViewBase
+        @param view: The view
+        @type target_map_name: String
+        @param target_map_name: Target map id 
+        @type target_pos: Tuple
+        @param target_pos: (X, Y) coordinates on the target map.
+        @return: None"""
+        super(ChangeMapAction, self).__init__(controller, commands)
+        self.view = controller.view
+        self.target_pos = target_pos
+        self.target_map_name = target_map_name
+
+    def execute(self):
+        """Executes the map change."""
+        self.model.changeMap(self.target_map_name,
+                              self.target_pos)
+        super(ChangeMapAction, self).execute()
+
+class OpenAction(Action):
+    """Open a container"""
+    def __init__(self, controller, container, commands=None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @type view: class derived from parpg.ViewBase
+        @param view: The view
+        @param container: A reference to the container
+        """
+        super(OpenAction, self).__init__(controller, commands)
+        self.view = controller.view
+        self.container = container
+    def execute(self):
+        """Open the box."""
+        self.view.hud.createBoxGUI(self.container.name, \
+                                              self.container)
+        super(OpenAction, self).execute()
+       
+       
+class OpenBoxAction(OpenAction):
+    """Open a box. Needs to be more generic, but will do for now."""
+    def __init__(self, controller, container, commands = None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @type view: class derived from parpg.ViewBase
+        @param view: The view
+        @param container: A reference to the container
+        """
+        super(OpenBoxAction, self).__init__(controller, commands)
+        self.view = controller.view
+        self.container = container
+        
+    def execute(self):
+        """Open the box."""
+        try:
+            self.container.open()
+            super(OpenBoxAction, self).execute()
+
+        except ValueError:
+            self.view.hud.createExamineBox(self.container.name, \
+                                                  "The container is locked")
+        
+class UnlockBoxAction(Action):
+    """Unlocks a box. Needs to be more generic, but will do for now."""
+    def __init__(self, controller, container, commands = None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @param container: A reference to the container
+        """
+        super(UnlockBoxAction, self).__init__(controller, commands)
+        self.container = container
+    
+    def execute(self):
+        """Open the box."""
+        self.container.unlock()
+        super(UnlockBoxAction, self).execute()
+        
+class LockBoxAction(Action):
+    """Locks a box. Needs to be more generic, but will do for now."""
+    def __init__(self, controller, container, commands = None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @param container: A reference to the container
+        """
+        super(LockBoxAction, self).__init__(controller, commands)
+        self.container = container
+        
+    def execute(self):
+        """Lock the box."""
+        self.container.lock()
+        super(LockBoxAction, self).execute()
+
+
+class ExamineAction(Action):
+    """Examine an object."""
+    def __init__(self, controller, examine_id, examine_name, examine_desc=None, commands=None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param examine_id: An object id
+        @type examine_id: integer
+        @param examine_name: An object name
+        @type examine_name: string
+        @param examine_desc: A description of the object that will be displayed.
+        @type examine_desc: string
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @type view: class derived from parpg.ViewBase
+        @param view: The view
+        
+        """
+        super(ExamineAction, self).__init__(controller, commands)
+        self.view = controller.view
+        self.examine_id = examine_id
+        self.examine_name = examine_name
+        if examine_desc is not None:
+            self.examine_desc = examine_desc
+        else:
+            self.examine_desc = "No Description"
+        
+    def execute(self):
+        """Display the text."""
+        action_text = self.examine_desc
+        self.view.hud.addAction(unicode(action_text))
+        logger.debug(action_text)
+        #this code will cut the line up into smaller lines that will be displayed
+        place = 25
+        while place <= len(action_text):
+            if action_text[place] == ' ':
+                action_text = action_text[:place] +'\n'+action_text[place:]
+                place += 26 #plus 1 character to offset the new line
+            else: place += 1
+        self.view.displayObjectText(self.examine_id, unicode(action_text), time=3000)
+
+class ExamineItemAction(Action):
+    """Examine an item."""
+    def __init__(self, controller, examine_name, examine_desc, commands = None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @type view: class derived from parpg.ViewBase
+        @param view: The view
+        @type examine_name: String
+        @param examine_name: Name of the object to be examined.
+        @type examine_name: String
+        @param examine_name: Description of the object to be examined.
+        """
+        super(ExamineItemAction, self).__init__(controller, commands)
+        self.view = controller.view
+        self.examine_name = examine_name
+        self.examine_desc = examine_desc
+        
+    def execute(self):
+        """Display the text."""
+        action_text = unicode(self.examine_desc)
+        self.view.hud.addAction(action_text)
+        logger.debug(action_text)
+
+class ReadAction(Action):
+    """Read a text."""
+    def __init__(self, controller, text_name, text, commands = None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @param view: The view
+        @type view: class derived from parpg.ViewBase
+        @param text_name: Name of the object containing the text
+        @type text_name: String
+        @param text: Text to be displayied
+        @type text: String
+        """
+        super(ReadAction, self).__init__(controller, commands)
+        self.view = controller.view
+        self.text_name = text_name
+        self.text = text
+        
+    def execute(self):
+        """Examine the box."""
+        action_text = unicode('\n'.join(["You read " + self.text_name + ".", 
+                                         self.text]))
+        self.view.hud.addAction(action_text)
+        logger.debug(action_text)
+        super(ReadAction, self).execute()
+
+class TalkAction(Action):
+    """An action to represent starting a dialogue"""
+    def __init__(self, controller, npc, commands = None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        @type view: class derived from parpg.ViewBase
+        @param view: The view
+        @type npc: NonPlayerCharacter
+        @param npc: NPC to interact with.
+        """
+        super(TalkAction, self).__init__(controller, commands)
+        self.view = controller.view
+        self.npc = npc
+        
+    def execute(self):
+        """Talk with the NPC when close enough, otherwise move closer.
+           @return: None"""
+        from parpg.dialoguecontroller import DialogueController
+        
+        player_char = self.model.game_state.player_character
+        npc_coordinates = self.npc.getLocation().getLayerCoordinates()
+        pc_coordinates = player_char.behaviour.agent.\
+                            getLocation().getLayerCoordinates()
+        
+        distance_squared = (npc_coordinates.x - pc_coordinates.x) *\
+                           (npc_coordinates.x - pc_coordinates.x) +\
+                           (npc_coordinates.y - pc_coordinates.y) *\
+                           (npc_coordinates.y - pc_coordinates.y)
+        
+        # If we are too far away, we approach the NPC again
+        if distance_squared > 2:
+            player_char.approach([self.npc.getLocation().
+                         getLayerCoordinates().x,
+                         self.npc.getLocation().
+                         getLayerCoordinates().y], 
+                        TalkAction(self.controller,
+                                   self.npc, self.commands))        
+        else:
+            player_char.behaviour.agent.act('stand', self.npc.getLocation())
+    
+            if self.npc.dialogue is not None:
+                dialogue_controller = DialogueController(self.controller.engine,
+                                                         self.view,
+                                                         self.model,
+                                                         self.controller.application)
+                self.controller.application.pushController(dialogue_controller)
+                dialogue_controller.startTalk(self.npc)
+            else:
+                self.npc.behaviour.agent.say("Leave me alone!", 1000)
+                
+            self.model.game_state.player_character.behaviour.idle()
+            self.model.game_state.player_character.nextAction = None
+            super(TalkAction, self).execute()
+
+class UseAction(Action):
+    """Action for carryable items. It executes special commands that can be only
+    used on carryable utens"""
+
+
+    def __init__(self, controller, item, commands = None):
+        """
+        @param controller: A reference to the GameSceneController.
+        @type controller: parpg.GameSceneController
+        @param item: Item on which the action is called
+        @type item: CarryableItem
+        @param commands: Special commands that are executed
+        @type commands: Dictionary 
+        """
+        super(UseAction, self).__init__(controller, commands)
+        self.view = controller.view
+        self.item = item
+    
+    def execute(self):
+        #Check if there are special commands and execute them
+        for command_data in self.commands:
+            command = command_data["Command"]
+            if command == "ReplaceItem":
+                object_id = command_data["ID"]
+                object_type = command_data["ObjectType"]
+                container = self.item.in_container
+                inst_dict = {}
+                inst_dict["ID"] = object_id
+                inst_dict["object_type"] = object_type
+                new_item = self.model.createContainerObject(inst_dict)
+                container.replaceItem(self.item, new_item)
+                self.view.hud.inventory.updateInventoryButtons()
+        super(UseAction, self).execute()
+
+class PickUpAction(Action):
+    """Action for picking up items from a map"""
+
+    def __init__(self, controller, map_item, commands = None):
+        super(PickUpAction, self).__init__(controller, commands)
+        self.map_item = map_item
+        self.view = controller.view
+        
+    def execute(self):
+        real_item = self.map_item.item
+        self.model.deleteObject(self.map_item.ID)
+        self.model.game_state.player_character.\
+                                inventory.placeItem(real_item)
+        self.view.hud.inventory.updateInventoryButtons()
+        super(PickUpAction, self).execute()
+
+class DropItemAction(Action):
+    """Action for dropping an items on a map"""
+    def __init__(self, controller, item, commands = None):
+        super(DropItemAction, self).__init__(controller, commands)
+        self.item = item
+        
+    def execute(self):
+        map_name = self.model.game_state.current_map_name
+        map_item_values = {}
+        map_item_values["ViewName"] = self.item.name
+        map_item_values["ObjectType"] = "MapItem"
+        map_item_values["ItemType"] = self.item.item_type
+        map_item_values["Map"] = map_name
+        coords = self.model.game_state.player_character.\
+                                        getLocation().getExactLayerCoordinates()
+        map_item_values["Position"] = (coords.x, coords.y)
+        map_item_values["Rotation"] = 0
+        map_item_values["item"] = self.item
+        agent = {}
+        agent[self.item.ID] = map_item_values
+        self.model.addAgent("All", agent)
+        self.model.placeAgents()
+        super(DropItemAction, self).execute()
+        
+class DropItemFromContainerAction(DropItemAction):
+    """Action for dropping an items from the Inventory to a map"""
+
+    def __init__(self, controller, item, container_gui, commands = None):
+        super(DropItemFromContainerAction, self).__init__(controller, item, commands)
+        self.container_gui = container_gui
+
+    def execute(self):
+        super(DropItemFromContainerAction, self).execute()
+        self.item.in_container.takeItem(self.item)
+        self.container_gui.updateImages()
+        
+class BrewBeerAction(Action):
+    """Action for brewing beer in a pot"""
+    def __init__(self, controller, pot, commands = None):
+        super(BrewBeerAction, self).__init__(controller, commands)
+        self.pot = pot
+        self.view = controller.view
+        
+    def execute(self):
+        """Brew the beer"""
+        has_water = False
+        has_yeast = False
+        has_fruit = False
+        has_wood = False
+        has_bottle = False
+        player_character = self.model.game_state.player_character
+        for item in self.pot.items.itervalues():
+            if item.item_type == "Questionable water":
+                if has_water:
+                    self.view.hud.addAction(unicode(\
+                        "Please put only 1 water in the pot"))
+                    return
+                has_water = True
+                water_type = 1 
+                water = item
+            elif item.item_type == "Pure water":
+                if has_water:
+                    self.view.hud.addAction(unicode(\
+                        "Please put only 1 water in the pot"))
+                    return
+                has_water = True
+                water_type = 2
+                water = item
+            elif item.item_type == "Grain":
+                if has_fruit:
+                    self.view.hud.addAction(unicode(\
+                        "Please put only 1 fruit in the pot"))
+                    return
+                has_fruit = True
+                fruit_type = 3
+                fruit = item
+            elif item.item_type == "Wild potato":
+                if has_fruit:
+                    self.view.hud.addAction(unicode(\
+                        "Please put only 1 fruit in the pot"))
+                    return
+                has_fruit = True
+                fruit_type = 2
+                fruit = item
+            elif item.item_type == "Rotten yam":
+                if has_fruit:
+                    self.view.hud.addAction(unicode(\
+                        "Please put only 1 fruit in the pot"))
+                    return
+                has_fruit = True
+                fruit_type = 1
+                fruit = item
+            elif item.item_type == "Yeast":
+                if has_yeast:
+                    self.view.hud.addAction(unicode(\
+                        "Please put only 1 yeast in the pot"))
+                    return
+                has_yeast = True
+                yeast = item 
+            else:
+                self.view.hud.addAction(unicode("Item " + item.name + \
+                                                " is not needed for brewing beer"))
+                self.view.hud.addAction(unicode(\
+                    "Please put only ingredients for the beer in the pot.\
+                    Things like bottles and wood have to be in your inventory"))
+                return
+        wood = player_character.hasItem("Wood")
+        if wood:
+            has_wood = True        
+        bottle = player_character.hasItem("Empty beer bottle")
+        if bottle:
+            has_bottle = True        
+        if has_water and has_fruit and has_wood and has_bottle:
+            self.pot.removeItem(water)
+            self.pot.removeItem(fruit)
+            if has_yeast:
+                self.pot.removeItem(yeast)
+            player_character.inventory.removeItem(wood)
+            inst_dict = {}
+            inst_dict["ID"] = "Beer"
+            inst_dict["object_type"] = "Beer"
+            new_item = self.model.createContainerObject(inst_dict)
+            player_character.inventory.placeItem(new_item)
+            self.view.hud.inventory.updateInventoryButtons()
+            beer_quality = 0
+            if water_type == 1:
+                if fruit_type == 1:
+                    beer_quality = -1
+                elif fruit_type == 2:
+                    beer_quality = 2
+                elif fruit_type == 3:
+                    beer_quality = 3
+            if water_type == 2:
+                if fruit_type == 1:
+                    beer_quality = 1
+                elif fruit_type == 2:
+                    beer_quality = 3
+                elif fruit_type == 3:
+                    beer_quality = 4
+            if beer_quality > 0 and has_yeast:
+                beer_quality += 1
+            self.model.game_state.quest_engine.quests["beer"].\
+                    setValue("beer_quality", beer_quality)
+        else:
+            self.view.hud.addAction(unicode(
+            """For brewing beer you need at least:
+            In the pot:
+                Fruit (like grain, potato, yam)
+                Water
+                Optionally:
+                    Good quality yeast.
+                    Wild yeast will be used if none present.
+            In the inventory:
+                Wood
+                Empty bottle"""))
+        super(BrewBeerAction, self).execute()
+
+ACTIONS = {"ChangeMap":ChangeMapAction, 
+           "Open":OpenAction,
+           "OpenBox":OpenBoxAction, 
+           "Unlock":UnlockBoxAction,
+           "Lock":LockBoxAction,
+           "ExamineItem":ExamineItemAction,
+           "Examine":ExamineAction,
+           "Look":ExamineItemAction,
+           "Read":ReadAction,
+           "Talk":TalkAction,
+           "Use":UseAction,
+           "PickUp":PickUpAction,
+           "DropFromInventory":DropItemFromContainerAction,
+           "BrewBeer":BrewBeerAction}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/actors.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,414 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+from random import randrange
+
+
+from fife import fife
+
+from base import GameObject, Living, Scriptable, CharStats
+from composed import CarryableItem
+from parpg.inventory import Inventory
+
+"""All actors go here. Concrete classes only."""
+
+__all__ = ["PlayerCharacter", "NonPlayerCharacter", ]
+
+_AGENT_STATE_NONE, _AGENT_STATE_IDLE, _AGENT_STATE_APPROACH, _AGENT_STATE_RUN, _AGENT_STATE_WANDER, _AGENT_STATE_TALK = xrange(6)
+
+class ActorBehaviour (fife.InstanceActionListener):
+    """Fife agent listener"""
+    def __init__(self, layer):
+        fife.InstanceActionListener.__init__(self)
+        self.layer = layer
+        self.agent = None
+        self.state = None
+        self.speed = 0
+        self.idle_counter = 1
+    
+    def attachToLayer(self, agent_ID):
+        """Attaches to a certain layer
+           @type agent_ID: String
+           @param agent_ID: ID of the layer to attach to.
+           @return: None"""
+        self.agent = self.layer.getInstance(agent_ID)
+        self.agent.addActionListener(self)
+        self.state = _AGENT_STATE_NONE
+        
+    def getX(self):
+        """Get the NPC's x position on the map.
+           @rtype: integer"
+           @return: the x coordinate of the NPC's location"""
+        return self.agent.getLocation().getLayerCoordinates().x
+
+    def getY(self):
+        """Get the NPC's y position on the map.
+           @rtype: integer
+           @return: the y coordinate of the NPC's location"""
+        return self.agent.getLocation().getLayerCoordinates().y
+        
+    def onNewMap(self, layer):
+        """Sets the agent onto the new layer."""
+        if self.agent is not None:
+            self.agent.removeActionListener(self)
+            
+        self.agent = layer.getInstance(self.parent.ID)
+        self.agent.addActionListener(self)
+        self.state = _AGENT_STATE_NONE
+        self.idle_counter = 1
+    
+    def idle(self):
+        """@return: None"""
+        self.state = _AGENT_STATE_IDLE
+        self.agent.act('stand', self.agent.getFacingLocation())
+
+    def onInstanceActionFinished(self, instance, action):
+        pass
+
+class PCBehaviour (ActorBehaviour):
+    def __init__(self, parent=None, layer=None):
+        super(PCBehaviour, self).__init__(layer)        
+        self.parent = parent
+        self.idle_counter = 1
+        self.speed = 0
+        self.nextAction = None
+        self.agent = None
+        
+    def onInstanceActionFinished(self, instance, action):
+        """@type instance: ???
+           @param instance: ???
+           @type action: ???
+           @param action: ???
+           @return: None"""
+        # First we reset the next behavior 
+        act = self.nextAction
+        self.nextAction = None 
+        self.idle()
+        
+        if act:
+            act.execute()
+            
+        if(action.getId() != 'stand'):
+            self.idle_counter = 1
+        else:
+            self.idle_counter += 1            
+
+
+class NPCBehaviour(ActorBehaviour):
+    def __init__(self, Parent=None, Layer=None):
+        super(NPCBehaviour, self).__init__(Layer)
+        
+        self.parent = Parent
+        self.state = _AGENT_STATE_NONE
+        self.pc = None
+        self.target_loc = None
+        self.nextAction = None
+        
+        # hard code these for now
+        self.distRange = (2, 4)
+        # these are parameters to lower the rate of wandering
+        # wander rate is the number of "IDLEs" before a wander step
+        # this could be set for individual NPCs at load time
+        # or thrown out altogether.
+        self.wanderCounter = 0
+        self.wanderRate = 9
+        
+    def getTargetLocation(self):
+        """@rtype: fife.Location
+           @return: NPC's position"""
+        x = self.getX()
+        y = self.getY()
+        if self.state == _AGENT_STATE_WANDER:
+            """ Random Target Location """
+            l = [0, 0]
+            for i in range(len(l)):
+                sign = randrange(0, 2)
+                dist = randrange(self.distRange[0], self.distRange[1])
+                if sign == 0:
+                    dist *= -1
+                l[i] = dist
+            x += l[0]
+            y += l[1]
+            # Random walk is
+            # rl = randint(-1, 1);ud = randint(-1, 1);x += rl;y += ud
+        l = fife.Location(self.agent.getLocation())
+        l.setLayerCoordinates(fife.ModelCoordinate(x, y))
+        return l
+
+    def onInstanceActionFinished(self, instance, action):
+        """What the NPC does when it has finished an action.
+           Called by the engine and required for InstanceActionListeners.
+           @type instance: fife.Instance
+           @param instance: self.agent (the NPC listener is listening for this
+                                        instance)
+           @type action: ???
+           @param action: ???
+           @return: None"""
+        if self.state == _AGENT_STATE_WANDER:
+            self.target_loc = self.getTargetLocation()
+        self.idle()
+        
+    
+    def idle(self):
+        """Controls the NPC when it is idling. Different actions
+           based on the NPC's state.
+           @return: None"""
+        if self.state == _AGENT_STATE_NONE:
+            self.state = _AGENT_STATE_IDLE
+            self.agent.act('stand', self.agent.getFacingLocation())
+        elif self.state == _AGENT_STATE_IDLE:
+            if self.wanderCounter > self.wanderRate:
+                self.wanderCounter = 0
+                self.state = _AGENT_STATE_WANDER
+            else:
+                self.wanderCounter += 1
+                self.state = _AGENT_STATE_NONE
+            
+            self.target_loc = self.getTargetLocation()
+            self.agent.act('stand', self.agent.getFacingLocation())
+        elif self.state == _AGENT_STATE_WANDER:
+            self.parent.wander(self.target_loc)
+            self.state = _AGENT_STATE_NONE
+        elif self.state == _AGENT_STATE_TALK:
+            self.agent.act('stand', self.pc.getLocation())
+            
+class CharacterBase(GameObject, CharStats, Living):
+    """Base class for Characters"""
+    def __init__(self, ID, agent_layer=None, inventory=None, text="",
+                 primary_stats=None, secondary_stats=None, **kwargs):
+        GameObject.__init__(self, ID, text=text, **kwargs)
+        CharStats.__init__(self, **kwargs)
+        Living.__init__(self, **kwargs)
+        self.statistics = {}
+        if primary_stats is not None:
+            for primary_stat in primary_stats:
+                name = primary_stat.name
+                self.statistics[name] = primary_stat
+        if secondary_stats is not None:
+            for secondary_stat in primary_stats:
+                long_name = secondary_stat.long_name
+                self.statistics[long_name] = secondary_stat
+                short_name = secondary_stat.short_name
+                self.statistics[short_name] = secondary_stat
+                secondary_stat.attach(self)
+        self.behaviour = None
+        if inventory == None:
+            self.inventory = Inventory()
+        else:
+            self.inventory = inventory
+        self.state = _AGENT_STATE_NONE
+        self.layer_id = agent_layer.getId()
+        self.createBehaviour(agent_layer)
+    
+    def createBehaviour(self, layer):
+        """Creates the behaviour for this actor.
+           @return: None"""
+        pass
+    
+    def setup(self):
+        """@return: None"""
+        self.behaviour.attachToLayer(self.ID)
+
+    def start(self):
+        """@return: None"""
+        self.behaviour.idle()
+
+    def teleport(self, location):
+        """Teleports a Character instantly to the given location.
+           @type location: fife.Location
+           @param location: Target coordinates for Character.
+           @return: None"""
+        self.state = _AGENT_STATE_IDLE
+        self.behaviour.nextAction = None 
+        self.behaviour.agent.setLocation(location)
+
+    def give (self, item, actor):
+        """Gives the specified item to the different actor. Raises an exception if the item was invalid or not found
+           @type item: Carryable
+           @param item: The item object to give
+           @param actor: Person to give item to"""
+        if item == None: 
+            raise ValueError("I don't have %s" % item.name)
+        self.inventory.takeItem(item)
+        actor.inventory.placeItem(item)           
+        
+    def hasItem(self, item_type):
+        """Returns wether an item is present in the players inventory or not
+        @param item_type: ID of the item
+        @type item_type: str
+        @return: True when the item is present, False when not"""
+        return self.inventory.findItem(item_type=item_type)
+
+    def itemCount(self, item_type=""):
+        """Returns number of all items or items specified by item_type 
+        the player has.
+        @param item_type: ID of the item, can be empty
+        @type item_type: str
+        @return: Number of items"""
+        return self.inventory.count(item_type)
+
+    def getLocation(self):
+        """Get the NPC's position as a fife.Location object. Basically a
+           wrapper.
+           @rtype: fife.Location
+           @return: the location of the NPC"""
+        return self.behaviour.agent.getLocation()
+    
+    def run(self, location):
+        """Makes the PC run to a certain location
+           @type location: fife.ScreenPoint
+           @param location: Screen position to run to.
+           @return: None"""
+        self.state = _AGENT_STATE_RUN
+        self.behaviour.nextAction = None
+        self.behaviour.agent.move('run', location, self.behaviour.speed + 1)
+
+    def walk(self, location):
+        """Makes the PC walk to a certain location.
+           @type location: fife.ScreenPoint
+           @param location: Screen position to walk to.
+           @return: None"""
+        self.state = _AGENT_STATE_RUN
+        self.behaviour.nextAction = None 
+        self.behaviour.agent.move('walk', location, self.behaviour.speed - 1)
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        ret_dict = GameObject.getStateForSaving(self)
+        ret_dict["Inventory"] = self.inventory.serializeInventory()
+        return ret_dict
+
+    def _getCoords(self):
+        """Get-er property function"""
+        return (self.getLocation().getMapCoordinates().x,
+                self.getLocation().getMapCoordinates().y)
+    
+    def _setCoords(self, coords):
+        """Set-er property function"""
+        map_coords = self.getLocation().getMapCoordinates()
+        map_coords.X, map_coords.Y = float(coords[0]), float (coords[1])
+        self.teleport(map_coords)
+    
+    coords = property (_getCoords, _setCoords,
+        doc="Property allowing you to get and set the object's \
+                coordinates via tuples")
+           
+class PlayerCharacter (CharacterBase):
+    """PC class"""
+    def __init__ (self, ID, agent_layer=None, inventory=None,
+                  text="Its you. Who would've thought that?", **kwargs):
+        if inventory == None:
+            inventory = Inventory()
+            inventory.placeItem(CarryableItem(ID=456, name="Dagger123"))
+            inventory.placeItem(CarryableItem(ID=555, name="Beer"))
+            inventory.placeItem(CarryableItem(ID=616,
+                                    name="Pamphlet",
+                                    image="/gui/inv_images/inv_pamphlet.png"))
+        CharacterBase.__init__(self, ID, agent_layer, inventory, text, **kwargs)
+        self.people_i_know = set()
+        self.attributes.append("PC")
+  
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        ret_dict = super(PlayerCharacter, self).getStateForSaving()
+        ret_dict["PeopleKnown"] = self.people_i_know
+        return ret_dict
+    
+    def meet(self, npc):
+        """Record that the PC has met a certain NPC
+           @type npc: str
+           @param npc: The NPC's name or id"""
+        if npc in self.people_i_know:
+            # we could raise an error here, but should probably be a warn
+            # raise RuntimeError("I already know %s" % npc)
+            return
+        self.people_i_know.add(npc)
+
+    def met(self, npc):
+        """Indicate whether the PC has met this npc before
+           @type npc: str
+           @param npc: The NPC's name or id
+           @return: None"""
+        return npc in self.people_i_know
+
+    def createBehaviour(self, layer):
+        """Creates the behaviour for this actor.
+           @return: None"""
+        self.behaviour = PCBehaviour(self, layer)
+ 
+    def approach(self, location, action=None):
+        """Approaches a location and then perform an action (if set).
+           @type loc: fife.Location
+           @param loc: the location to approach
+           @type action: Action
+           @param action: The action to schedule for execution after the approach.
+           @return: None"""
+        self.state = _AGENT_STATE_APPROACH
+        self.behaviour.nextAction = action
+        boxLocation = tuple([int(float(i)) for i in location])
+        l = fife.Location(self.behaviour.agent.getLocation())
+        l.setLayerCoordinates(fife.ModelCoordinate(*boxLocation))
+        self.behaviour.agent.move('run', l, self.behaviour.speed + 1)
+    
+class NonPlayerCharacter(CharacterBase, Scriptable):
+    """NPC class"""
+    def __init__(self, ID, agent_layer=None, name='NPC', \
+                 text='A nonplayer character', inventory=None,
+                 real_name='NPC', dialogue=None, **kwargs):
+        # init game object
+        CharacterBase.__init__(self, ID, agent_layer=agent_layer,
+                               inventory=inventory, name=name,
+                               real_name=real_name, text=text, **kwargs)
+        Scriptable.__init__(self, **kwargs)
+
+        self.attributes.append("NPC")
+        self.dialogue = dialogue
+
+    def prepareStateForSaving(self, state):
+        """Prepares state for saving
+        @type state: dictionary
+        @param state: State of the object  
+        """
+        CharacterBase.prepareStateForSaving(self, state)
+        del state["behaviour"]
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        ret_dict = CharacterBase.getStateForSaving(self)
+        ret_dict["Lives"] = self.lives
+        ret_dict["State"] = self.behaviour.state
+        return ret_dict
+
+    def createBehaviour(self, layer):
+        """Creates the behaviour for this actor.
+           @return None """
+        self.behaviour = NPCBehaviour(self, layer)
+
+    def wander(self, location):
+        """Nice slow movement for random walking.
+           @type location: fife.Location
+           @param location: Where the NPC will walk to.
+           @return: None"""
+        self.behaviour.agent.move('walk', location, self.behaviour.speed - 1)
+
+    def talk(self, pc):
+        """Makes the NPC ready to talk to the PC
+           @return: None"""
+        self.behaviour.state = _AGENT_STATE_TALK
+        self.behaviour.pc = pc.behaviour.agent
+        self.behaviour.idle()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/base.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,508 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Containes classes defining the base properties of all interactable in-game 
+   objects (such as Carryable, Openable, etc. These are generally independent 
+   classes, which can be combined in almost any way and order. 
+
+   Some rules that should be followed when CREATING base property classes:
+   
+   1. If you want to support some custom initialization arguments, 
+      always define them as keyword ones. Only GameObject would use 
+      positional arguments.
+   2. In __init__() **ALWAYS** call the parent's __init__(**kwargs), preferably 
+      *at the end* of your __init__() (makes it easier to follow)
+   3. There should always be an attributes.append(x) call on __init__ 
+      (where X is the name of the class)
+
+   EXAMPLE:
+
+   class Openable(object):
+       def __init__ (self, is_open = True, **kwargs):
+           self.attribbutes.append("openable")
+           self.is_open = is_open
+           super(Openable,self).__init__ (**kwargs)
+        
+
+   Some rules are to be followed when USING the base classes to make composed 
+   ones:
+
+   1. The first parent should always be the base GameObject class
+   2. Base classes other than GameObject can be inherited in any order
+   3. The __init__ functoin of the composed class should always invoke the
+      parent's __init__() *before* it starts customizing any variables.
+
+   EXAMPLE:
+
+   class TinCan (GameObject, Container, Scriptable, Destructable, Carryable):
+       def __init__ (self, *args, **kwargs):
+           super(TinCan,self).__init__ (*args, **kwargs)
+           self.name = 'Tin Can'"""
+         
+class BaseObject(object):
+    """A base class that supports dynamic attributes functionality"""
+    def __init__ (self):
+        if not self.__dict__.has_key("attributes"):
+            self.attributes = []
+    
+    def trueAttr(self, attr):
+        """Method that checks if the instance has an attribute"""
+        return attr in self.attributes
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        state = {}
+        state["attributes"] = self.attributes
+        return state
+
+class DynamicObject (BaseObject):
+    """Class with basic attributes"""
+    def __init__ (self, name="Dynamic object", real_name=None, image=None, **kwargs):
+        """Initialise minimalistic set of data
+           @type name: String
+           @param name: Object display name
+           @type image: String or None
+           @param name: Filename of image to use in inventory"""
+        BaseObject.__init__(self)
+        self.name = name
+        self.real_name = real_name or name
+        self.image = image
+
+    def prepareStateForSaving(self, state):
+        """Prepares state for saving
+        @type state: dictionary
+        @param state: State of the object  
+        """
+        pass
+    
+    def restoreState(self, state):
+        """Restores a state from a saved state
+        @type state: dictionary
+        @param state: Saved state  
+        """
+        self.__dict__.update(state)
+
+    def __getstate__(self):
+        odict = self.__dict__.copy()
+        self.prepareStateForSaving(odict)
+        return odict
+    
+    def __setstate__(self, state):
+        self.restoreState(state)
+    
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        state = BaseObject.getStateForSaving(self)
+        state["Name"] = self.name
+        state["RealName"] = self.real_name
+        state["Image"] = self.image
+        return state
+
+class GameObject (DynamicObject):
+    """A base class to be inherited by all game objects. This must be the
+       first class (left to right) inherited by any game object."""
+    def __init__ (self, ID, gfx = None, xpos = 0.0, ypos = 0.0, map_id = None, 
+                  blocking=True, name="Generic object", real_name="Generic object", text="Item description",
+                  desc="Detailed description", **kwargs):
+        """Set the basic values that are shared by all game objects.
+           @type ID: String
+           @param ID: Unique object identifier. Must be present.
+           @type gfx: Dictionary
+           @param gfx: Dictionary with graphics for the different contexts       
+           @type coords 2-item tuple
+           @param coords: Initial coordinates of the object.
+           @type map_id: String
+           @param map_id: Identifier of the map where the object is located
+           @type blocking: Boolean
+           @param blocking: Whether the object blocks character movement
+           @type name: String
+           @param name: The display name of this object (e.g. 'Dirty crate')
+           @type text: String
+           @param text: A longer description of the item
+           @type desc: String
+           @param desc: A long description of the item that is displayed when it is examined
+           """
+        DynamicObject.__init__(self, name, real_name, **kwargs)
+        self.ID = ID
+        self.gfx = gfx or {}
+        self.X = xpos
+        self.Y = ypos
+        self.map_id = map_id
+        self.blocking = True
+        self.text = text
+        self.desc = desc
+        
+    def _getCoords(self):
+        """Get-er property function"""
+        return (self.X, self.Y)
+    
+    def _setCoords(self, coords):
+        """Set-er property function"""
+        self.X, self.Y = float(coords[0]), float (coords[1])
+        
+    coords = property (_getCoords, _setCoords, 
+        doc = "Property allowing you to get and set the object's \
+                coordinates via tuples")
+    
+    def __repr__(self):
+        """A debugging string representation of the object"""
+        return "<%s:%s>" % (self.name, self.ID)
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        state = super(GameObject, self).getStateForSaving()
+        state["ObjectModel"] = self.gfx
+        state["Text"] = self.text
+        state["Desc"] = self.desc
+        state["Position"] = list(self.coords)
+        return state
+
+
+class Scriptable (BaseObject):
+    """Allows objects to have predefined parpg executed on certain events"""
+    def __init__ (self, parpg = None, **kwargs):
+        """Init operation for scriptable objects
+           @type parpg: Dictionary
+           @param parpg: Dictionary where the event strings are keys. The 
+           values are 3-item tuples (function, positional_args, keyword_args)"""
+        BaseObject.__init__(self)
+        self.attributes.append("scriptable")
+        self.parpg = parpg or {}
+        
+    def runScript (self, event):
+        """Runs the script for the given event"""
+        if event in self.parpg and self.parpg[event]:
+            func, args, kwargs = self.parpg[event]
+            func (*args, **kwargs)
+            
+    def setScript (self, event, func, args = None , kwargs = None):
+        """Sets a script to be executed for the given event."""
+        args = args or {}
+        kwargs = kwargs or {}
+        self.parpg[event] = (func, args, kwargs)
+
+class Openable(DynamicObject, Scriptable):
+    """Adds open() and .close() capabilities to game objects
+       The current state is tracked by the .is_open variable"""
+    def __init__(self, is_open = True, **kwargs):
+        """Init operation for openable objects
+           @type is_open: Boolean
+           @param is_open: Keyword boolean argument sets the initial state."""
+        DynamicObject.__init__(self, **kwargs)
+        Scriptable.__init__(self, **kwargs)
+        self.attributes.append("openable")
+        self.is_open = is_open
+    
+    def open(self):
+        """Opens the object, and runs an 'onOpen' script, if present"""
+        self.is_open = True
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onOpen')
+        except AttributeError :
+            pass
+            
+    def close(self):
+        """Opens the object, and runs an 'onClose' script, if present"""
+        self.is_open = False
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onClose')
+        except AttributeError :
+            pass
+        
+class Lockable (Openable):
+    """Allows objects to be locked"""
+    def __init__ (self, locked = False, is_open = True, **kwargs):
+        """Init operation for lockable objects
+           @type locked: Boolean
+           @param locked: Keyword boolen argument sets the initial locked state.
+           @type is_open: Boolean
+           @param is_open: Keyword boolean argument sets the initial open state.
+                           It is ignored if locked is True -- locked objects
+                           are always closed."""
+        self.attributes.append("lockable")
+        self.locked = locked
+        if locked :
+            is_open = False
+        Openable.__init__( self, is_open, **kwargs )
+        
+    def unlock (self):
+        """Handles unlocking functionality"""
+        self.locked = False      
+        
+    def lock (self):
+        """Handles  locking functionality"""
+        self.close()
+        self.locked = True
+        
+    def open (self, *args, **kwargs):
+        """Adds a check to see if the object is unlocked before running the
+           .open() function of the parent class"""
+        if self.locked:
+            raise ValueError ("Open failed: object locked")
+        super (Lockable, self).open(*args, **kwargs)
+        
+class Carryable (DynamicObject):
+    """Allows objects to be stored in containers"""
+    def __init__ (self, weight=0.0, bulk=0.0, **kwargs):
+        DynamicObject.__init__(self, **kwargs)
+        self.attributes.append("carryable")
+        self.in_container = None
+        self.on_map = None
+        self.agent = None
+        self.weight = weight
+        self.bulk = bulk
+
+    def getInventoryThumbnail(self):
+        """Returns the inventory thumbnail of the object"""
+        # TODO: Implement properly after the objects database is in place
+        if self.image == None:
+            return "gui/inv_images/inv_litem.png"
+        else:
+            return self.image
+    
+class Container (DynamicObject, Scriptable):
+    """Gives objects the capability to hold other objects"""
+    class TooBig(Exception):
+        """Exception to be raised when the object is too big
+        to fit into container"""
+        pass
+    
+    class SlotBusy(Exception):
+        """Exception to be raised when the requested slot is occupied"""
+        pass
+    
+    class ItemSelf(Exception):
+        """Exception to be raised when trying to add the container as an item"""
+        pass
+  
+    def __init__ (self, capacity = 0, items = None, **kwargs):
+        DynamicObject.__init__(self, **kwargs)
+        Scriptable.__init__(self, **kwargs)
+        self.attributes.append("container")
+        self.items = {}
+        self.capacity = capacity
+        if items:
+            for item in items:
+                self.placeItem(item)
+        
+    def placeItem (self, item, index=None):
+        """Adds the provided carryable item to the inventory. 
+           Runs an 'onStoreItem' script, if present""" 
+        if item is self:
+            raise self.ItemSelf("Paradox: Can't contain myself")    
+        if not item.trueAttr ('carryable'):
+            raise TypeError ('%s is not carryable!' % item)
+        if self.capacity and self.getContentsBulk()+item.bulk > self.capacity:
+            raise self.TooBig ('%s is too big to fit into %s' % (item, self))
+        item.in_container = self
+        if index == None:
+            self._placeAtVacant(item)
+        else:
+            if index in self.items :
+                raise self.SlotBusy('Slot %d is busy in %s' % (index, 
+                                                               self.name))
+            self.items[index] = item
+
+        # Run any parpg associated with storing an item in the container
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onPlaceItem')
+        except AttributeError :
+            pass
+
+    def _placeAtVacant(self, item):
+        """Places an item at a vacant slot"""
+        vacant = None
+        for i in range(len(self.items)):
+            if i not in self.items :
+                vacant = i
+        if vacant == None :
+            vacant = len(self.items)
+        self.items[vacant] = item
+    
+    def takeItem (self, item):
+        """Takes the listed item out of the inventory. 
+           Runs an 'onTakeItem' script"""        
+        if not item in self.items.values():
+            raise ValueError ('I do not contain this item: %s' % item)
+        del self.items[self.items.keys()[self.items.values().index(item)]]
+
+        # Run any parpg associated with popping an item out of the container
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onTakeItem')
+        except AttributeError :
+            pass
+    
+    def replaceItem(self, old_item, new_item):
+        """Replaces the old item with the new one
+        @param old_item: Old item which is removed
+        @type old_item: Carryable
+        @param new_item: New item which is added
+        @type new_item: Carryable
+        """
+        old_index = self.indexOf(old_item.ID)
+        self.removeItem(old_item)
+        self.placeItem(new_item, old_index)
+        
+    def removeItem(self, item):
+        """Removes an item from the container, basically the same as 'takeItem'
+        but does run a different script. This should be used when an item is
+        destroyed rather than moved out.
+        Runs 'onRemoveItem' script
+        """
+        if not item in self.items.values():
+            raise ValueError ('I do not contain this item: %s' % item)
+        del self.items[self.items.keys()[self.items.values().index(item)]]
+
+        # Run any parpg associated with popping an item out of the container
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onRemoveItem')
+        except AttributeError :
+            pass
+
+    def count (self, item_type = ""):
+        """Returns the number of items"""
+        if item_type:
+            ret_count = 0
+            for index in self.items :
+                if self.items[index].item_type == item_type:
+                    ret_count += 1
+            return ret_count
+        return len(self.items)   
+    
+    def getContentsBulk(self):
+        """Bulk of the container contents"""
+        return sum((item.bulk for item in self.items.values()))
+
+    def getItemAt(self, index):
+        return self.items[index]
+    
+    def indexOf(self, ID):
+        """Returns the index of the item with the passed ID"""
+        for index in self.items :
+            if self.items[index].ID == ID:
+                return index
+        return None
+
+    def findItemByID(self, ID):
+        """Returns the item with the passed ID"""
+        for i in self.items :
+            if self.items[i].ID == ID:
+                return self.items[i]
+        return None
+
+    def findItemByItemType(self, item_type):
+        """Returns the item with the passed item_type"""
+        for index in self.items :
+            if self.items[index].item_type == item_type:
+                return self.items[index]
+        return None
+
+    def findItem(self, **kwargs):
+        """Find an item in container by attributes. All params are optional.
+           @type name: String
+           @param name: If the name is non-unique, return first matching object
+           @type kind: String
+           @param kind: One of the possible object types
+           @return: The item matching criteria or None if none was found"""
+        for index in self.items :
+            if "name" in kwargs and self.items[index].name != kwargs["name"]:
+                continue
+            if "ID" in kwargs and self.items[index].ID != kwargs["ID"]:
+                continue
+            if "kind" in kwargs and not self.items[index].trueAttr(kwargs["kind"]):
+                continue
+            if "item_type" in kwargs and self.items[index].item_type != kwargs["item_type"]:
+                continue
+            return self.items[index]
+        return None    
+    
+    def serializeItems(self):
+        """Returns the items as a list"""
+        items = []
+        for index, item in self.items.iteritems():
+            item_dict = item.getStateForSaving()
+            item_dict["index"] = index
+            item_dict["type"] = item.item_type
+            items.append(item_dict)
+        return items
+    
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        ret_state = DynamicObject.getStateForSaving(self)
+        ret_state["Items"] = self.serializeItems()
+        return ret_state
+        
+class Living (BaseObject):
+    """Objects that 'live'"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("living")
+        self.lives = True
+
+    def die(self):
+        """Kills the object"""
+        self.lives = False   
+
+class CharStats (BaseObject):
+    """Provides the object with character statistics"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("charstats")
+        
+class Wearable (BaseObject):
+    """Objects than can be weared"""
+    def __init__ (self, slots, **kwargs):
+        """Allows the object to be worn somewhere on the body (e.g. pants)"""
+        BaseObject.__init__(self)
+        self.attributes.append("wearable")
+        if isinstance(slots, tuple) :
+            self.slots = slots
+        else :
+            self.slots = (slots,)
+    
+class Usable (BaseObject):
+    """Allows the object to be used in some way (e.g. a Zippo lighter 
+       to make a fire)"""
+    def __init__ (self, actions = None, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("usable")
+        self.actions = actions or {}   
+        
+class Weapon (BaseObject):
+    """Allows the object to be used as a weapon"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("weapon")
+        
+class Destructable (BaseObject):
+    """Allows the object to be destroyed"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("destructable")
+        
+class Trapable (BaseObject):
+    """Provides trap slots to the object"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("trapable")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/composed.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,145 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Composite game object classes are kept here"""
+
+from base import GameObject, Container, Lockable, \
+                Scriptable, Trapable, Destructable, Carryable, \
+                Usable
+
+class ImmovableContainer(GameObject, Container, Lockable, Scriptable, 
+                         Trapable, Destructable):
+    """Composite class that can be used for crates, chests, etc."""
+    def __init__ (self, **kwargs):
+        GameObject   .__init__(self, **kwargs)
+        Container    .__init__(self, **kwargs)
+        Lockable     .__init__(self, **kwargs)
+        Scriptable   .__init__(self, **kwargs)
+        Trapable    .__init__(self, **kwargs)
+        Destructable .__init__(self, **kwargs)
+        self.blocking = True
+
+class SingleItemContainer (Container) :
+    """Container that can only store a single item.
+       This class can represent single-item inventory slots"""
+    def __init__ (self, **kwargs):
+        Container.__init__(self, **kwargs)
+
+    def placeItem(self,item, index=None):
+        if len(self.items) > 0 :
+            raise self.SlotBusy ('%s is already busy' % self)
+        Container.placeItem(self, item)
+    
+class CarryableItem (GameObject, Carryable, Usable):
+    """Composite class that will be used for all carryable items"""
+    def __init__(self, item_type, **kwargs):
+        GameObject.__init__(self, **kwargs)
+        Carryable.__init__(self, **kwargs)
+        Usable.__init__(self, **kwargs)
+        self.item_type = item_type
+
+    def prepareStateForSaving(self, state):
+        """Prepares state for saving
+        @type state: dictionary
+        @param state: State of the object  
+        """
+        super(CarryableItem, self).prepareStateForSaving(state)
+        if state.has_key("in_container"):
+            del state["in_container"]
+        if state.has_key("on_map"):
+            del state["on_map"]
+        if state.has_key("agent"):
+            del state["agent"]
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        @type state: dictionary
+        @param state: State of the object  
+        """
+        ret_dict = self.__dict__.copy()
+        self.prepareStateForSaving(ret_dict)
+        return ret_dict
+
+class CarryableContainer(Container, CarryableItem):
+    """Composite class that will be used for backpack, pouches, etc."""
+    def __init__ (self, item_type, **kwargs):
+        Container.__init__(self, **kwargs)
+        CarryableItem.__init__(self, item_type, **kwargs)
+        self.own_bulk = 0
+        self.own_weight = 0
+
+    def getWeight(self):
+        """Resulting weight of a container"""
+        return sum((item.weight for item in self.items.values()), 
+                   self.own_weight)
+
+    def setWeight(self, weight):
+        """Set container's own weight. 
+        For compatibility with inherited methods"""
+        self.own_weight = weight
+
+    weight = property(getWeight, setWeight, "Total weight of container")
+
+    def getBulk(self):
+        """Resulting bulk of container"""
+        return self.getContentsBulk()+self.own_bulk
+
+    def setBulk(self, bulk):
+        """Set container's own bulk. For compatibility with inherited methods"""
+        self.own_bulk = bulk
+
+    bulk = property(getBulk, setBulk, "Total bulk of container")
+    
+    def __repr__(self):
+        return "[%s" % self.name + str(reduce((lambda a, b: a + ', ' + \
+                                    str(self.items[b])), self.items, "")) + " ]"
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        @type state: dictionary
+        @param state: State of the object  
+        """
+        state = Container.getStateForSaving(self)
+        if not state.has_key("attributes"):
+            state["attributes"] = []
+        state["attributes"].append("Container")
+        state.update(CarryableItem.getStateForSaving(self))
+        return state
+
+class CarryableSingleItemContainer (SingleItemContainer, CarryableContainer) :
+    """Container that can only store a single item.
+       This class can represent single-item inventory slots"""
+    def __init__ (self, item_type, **kwargs):
+        SingleItemContainer.__init__(self, **kwargs)
+        CarryableContainer.__init__(self, item_type, **kwargs)
+        
+class Door(GameObject, Lockable, Scriptable, Trapable):
+    """Composite class that can be used to create doors on a map."""
+    def __init__ (self, target_map_name = 'my-map',
+                  target_x = 0.0, target_y = 0.0, **kwargs):
+        GameObject.__init__(self, **kwargs)
+        Lockable.__init__(self, **kwargs)
+        Scriptable.__init__(self, **kwargs)
+        Trapable.__init__(self, **kwargs)
+        self.attributes.append("door")
+        self.target_map_name = target_map_name
+        self.target_pos = (target_x, target_y)
+        self.blocking = True
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        ret_dict = super(Door, self).getStateForSaving()
+        return ret_dict
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/containers.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,110 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Containes classes defining concrete container game objects like crates,
+   barrels, chests, etc."""
+
+__all__ = ["WoodenCrate", "Footlocker"]
+
+_AGENT_STATE_NONE, _AGENT_STATE_OPENED, _AGENT_STATE_CLOSED, \
+_AGENT_STATE_OPENING, _AGENT_STATE_CLOSING = xrange(5)
+
+from composed import ImmovableContainer
+from fife import fife
+
+class WoodenCrate (ImmovableContainer):
+    def __init__(self, object_id, name = 'Wooden Crate',
+                 text = 'A battered crate', gfx = 'crate', **kwargs):
+        ImmovableContainer.__init__(self, ID = object_id, name = name, 
+                                    gfx = gfx, text = text, **kwargs)
+        
+class ContainerBehaviour(fife.InstanceActionListener):
+    def __init__(self, parent = None, agent_layer = None):
+        fife.InstanceActionListener.__init__(self)
+        self.parent = parent
+        self.layer = agent_layer
+        self.state = _AGENT_STATE_CLOSED
+        self.agent = None
+
+    def attachToLayer(self, agent_id):
+        """ Attaches to a certain layer
+            @type agent_id: String
+            @param agent_id: ID of the layer to attach to.
+            @return: None"""
+        self.agent = self.layer.getInstance(agent_id)
+        self.agent.addActionListener(self)
+        self.state = _AGENT_STATE_CLOSED
+        self.agent.act('closed', self.agent.getLocation())
+        
+    def onInstanceActionFinished(self, instance, action):
+        """What the Actor does when it has finished an action.
+           Called by the engine and required for InstanceActionListeners.
+           @type instance: fife.Instance
+           @param instance: self.agent (the Actor listener is listening for this
+            instance)
+           @type action: ???
+           @param action: ???
+           @return: None"""
+        if self.state == _AGENT_STATE_OPENING:
+            self.agent.act('opened', self.agent.getFacingLocation(), True)
+            self.state = _AGENT_STATE_OPENED
+        if self.state == _AGENT_STATE_CLOSING:
+            self.agent.act('closed', self.agent.getFacingLocation(), True)
+            self.state = _AGENT_STATE_CLOSED
+        
+    def open (self):
+        if self.state != _AGENT_STATE_OPENED and self.state != \
+                                                _AGENT_STATE_OPENING:
+            self.agent.act('open', self.agent.getLocation())
+            self.state = _AGENT_STATE_OPENING
+
+    def close(self):
+        if self.state != _AGENT_STATE_CLOSED and self.state != \
+                                                _AGENT_STATE_CLOSING:
+            self.agent.act('close', self.agent.getLocation())
+            self.state = _AGENT_STATE_CLOSING  
+    
+class Footlocker(ImmovableContainer):
+    def __init__ (self, object_id, agent_layer=None, name = 'Footlocker',
+                  text = 'A Footlocker', gfx = 'lock_box_metal01', **kwargs):
+        ImmovableContainer.__init__(self, ID = object_id, name = name, 
+                                    gfx = gfx, text = text, **kwargs)
+        self.behaviour = None
+
+        self.attributes.append("AnimatedContainer")
+        self.createBehaviour(agent_layer)        
+        
+    def prepareStateForSaving(self, state):
+        """Prepares state for saving
+        @type state: dictionary
+        @param state: State of the object  
+        """
+        ImmovableContainer.prepareStateForSaving(self, state)
+        del state["behaviour"]
+    
+    def createBehaviour(self, layer):
+        self.behaviour = ContainerBehaviour(self, layer)
+
+    def setup(self):
+        """@return: None"""
+        self.behaviour.attachToLayer(self.ID)
+
+    def open (self):
+        super (Footlocker, self).open()
+        self.behaviour.open()
+
+    def close(self):
+        super (Footlocker, self).close()
+        self.behaviour.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/doors.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,29 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Containes classes defining concrete door game objects."""
+
+__all__ = ["ShantyDoor", ]
+
+from composed import Door
+
+class ShantyDoor(Door):
+    def __init__ (self, ID, name = 'Shanty Door', text = 'A door',
+                   gfx = 'shanty-door', target_map_name = 'my-map',
+                   target_x = 0.0, target_y = 0.0, **kwargs):
+        Door.__init__(self, ID = ID, name = name, text = text, gfx = gfx,
+                      target_map_name = target_map_name,
+                      target_x = target_x,
+                      target_y = target_y, **kwargs)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/items.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,26 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+__all__ = ["MapItem", ]
+
+from composed import CarryableItem
+
+class MapItem(CarryableItem):
+    """Item that is lying on a map"""
+    def __init__(self, ID, item_type, item, name = 'Item', text = 'An item',
+                   gfx = 'item', **kwargs):
+        CarryableItem.__init__(self, ID = ID, item_type = item_type, name = name, 
+                               text = text, gfx = gfx, **kwargs)
+        self.item = item
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/quest_engine.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,259 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import yaml
+from parpg.common.utils import locateFiles
+import os.path
+
+class Quest(object):
+    """Class that holds the information for a quest"""
+    def __init__(self, quest_id, quest_giver_id, quest_name, description,
+                 variables):
+        self.quest_id = quest_id
+        self.quest_giver_id = quest_giver_id
+        self.quest_name = quest_name
+        self.description = description
+        self.quest_variables = variables
+    
+    def setValue(self, variable_name, value):
+        """Set the value of a quest variable
+           @param variable_name: the name of the variable to set
+           @param value: the value you want to assign to the variable
+           @return: True on success
+           @return: False when it failes"""
+
+        if self.quest_variables.has_key(variable_name):
+            self.quest_variables[variable_name]["value"] = value
+            return True
+        else:
+            return False
+
+    def getValue(self, variable_name):
+        """Get the value of a quest_variable
+           @param variable_name: the name of the variable to set
+           @return: the value of the quest_variable"""
+        if self.quest_variables.has_key(variable_name):
+            return self.quest_variables[variable_name]["value"]
+        else:
+            return False
+
+    def getGoalValue(self, variable_name):
+        """Get the goal value of a quest_variable
+           @param variable_name: the name of the variable to set
+           @return: the goal value of the quest variable"""
+        if self.quest_variables.has_key(variable_name):
+            return self.quest_variables[variable_name]["goal_value"]
+        else:
+            return False
+
+    def increaseValue(self, variable_name, value):
+        """Increase a variable by a specified value
+           @param variable_name: the name of the variable to set
+           @param value: the value you want to increase the variable with
+           @return: True on success
+           @return: False when it fails"""
+        if self.quest_variables.has_key(variable_name):
+            self.quest_variables[variable_name]["value"] += value
+            return True
+        else:
+            return False
+
+    def decreaseValue(self, variable_name, value):
+        """Decrease a variable by a specified value
+           @param variable_name: the name of the variable to set
+           @param value: the value you want to decrease the variable with
+           @return: True on success
+           @return: False when it failes"""
+        if self.quest_variables.has_key(variable_name):
+            self.quest_variables[variable_name]["value"] -= value
+            return True
+        else:
+            return False
+
+    def isGoalValue(self, variable_name):
+        """Check if the variable has reached it's goal value
+           @param variable_name: the name of the variable to check
+           @return: True when the variable has reached the goal value
+           @return: False when it has not reached the goal value"""
+        if self.quest_variables.has_key(variable_name):
+            return self.quest_variables[variable_name]["value"] == \
+                    self.quest_variables[variable_name]["goal_value"]
+        else:
+            return False
+
+    def isEqualOrBiggerThanGoalValue(self, variable_name):
+        """Check if the variable is equil or bigger then it's goal value
+           @param variable_name: the name of the variable to set
+           @return: True when it has reached or exceeded the goal value
+           @return: False when it has not reached or exceeded the goal value """
+        if variable_name in self.quest_variables:
+            return self.quest_variables[variable_name]["value"] >= \
+                             self.quest_variables[variable_name]["goal_value"]
+        else:
+            return False
+    
+    def restartQuest(self):
+        """Restarts the quest. This sets all values to the reset values,
+        if there is a reset value present """
+        for variable in self.quest_variables.itervalues():
+            if variable.has_key("reset_value"):
+                variable["value"] = variable["reset_value"]
+
+class QuestEngine(dict):
+    def __init__(self, quest_dir):
+        """Create a quest engine object"""
+        dict.__init__(self)
+        self.empty_quest = Quest(None, None, None, None, {})
+        self.quests = {}
+        self.active_quests = []
+        self.finished_quests = []
+        self.failed_quests = []
+        self.quest_dir = quest_dir
+
+    def __str__(self):
+        return self.quests.__str__()
+
+    def __getitem__(self, key):
+        try:
+            return self.quests.__getitem__(key)
+        except KeyError:
+            return self.empty_quest
+
+    def items(self):
+        return self.quests.items()
+
+    def values(self):
+        return self.quests.values()
+
+    def keys(self):
+        return self.quests.keys()
+    
+    def readQuests(self):
+        """Reads in the quests in the quest directory"""
+        files = locateFiles("*.yaml", self.quest_dir)
+        self.quests = {}
+        self.active_quests = []
+        self.finished_quests = []
+        self.failed_quests = []
+        for quest_file in files:
+            quest_file = os.path.relpath(quest_file).replace("\\", "/")
+            tree = yaml.load(open(quest_file))
+            quest_properties = tree["QUEST_PROPERTIES"]
+            variable_defines = tree["DEFINES"]
+    
+            self.quests[quest_properties["quest_id"]] = \
+                                  Quest(quest_properties["quest_id"],
+                                        quest_properties["quest_giver_id"],
+                                        quest_properties["quest_name"],
+                                        quest_properties["description"],
+                                        variable_defines)
+
+    def activateQuest(self, quest_id):
+        """Add a quest to the quest log
+           @param quest: the quest id of the quest to add to the quest log
+           @return: True if succesfully added
+           @return: False if failed to add"""
+
+        if quest_id in self.quests \
+            and not (quest_id in self.active_quests \
+                        or quest_id in self.finished_quests):
+            self.active_quests.append(quest_id)
+            return True
+        return False
+
+    def finishQuest(self, quest_id):
+        """Move a quest to the finished quests log
+           @param quest_id: The id of the quest you want to move
+           @return: True on success
+           @return: False when it failes"""
+        if quest_id in self.active_quests:
+            self.finished_quests.append(quest_id)
+            self.active_quests.remove(quest_id)
+            return True
+        return False
+    
+    def restartQuest(self, quest_id):
+        """Restart a quest
+           @param quest_id: ID of the quest you want to restart
+           @return: True on success
+           @return: False when it failes"""
+        if quest_id in self.active_quests:
+            self.quests[quest_id].restartQuest()
+    
+    def failQuest(self, quest_id):
+        """Set a quest to failed
+           @param quest_id: ID of the quest you want to fail
+           @return: True on success
+           @return: False when it failes"""
+        if quest_id in self.active_quests:
+            self.failed_quests.append(quest_id)
+            self.active_quests.remove(quest_id)
+            return True
+        return False
+            
+    def hasQuest(self, quest_id):
+        """Check whether a quest is present in the quest_list.
+        It doesn't matter which state the quest is, or even if its
+        started.
+        @param quest_id: ID of the quest you want to check
+        @return: True on when the quest is in the quest log
+        @return: False when it's not in the quest log"""
+        return quest_id in self.quests
+
+    def hasActiveQuest(self, quest_id):
+        """Check whether a quest is in the quest log
+        @param quest_id: ID of the quest you want to check
+        @return: True on when the quest is in the quest log
+        @return: False when it's not in the quest log"""
+        return quest_id in self.active_quests
+
+    def hasFinishedQuest(self, quest_id):
+        """Check whether a quest is in the finished quests log
+        @param quest_id: ID of the quest you want to check
+        @return: True on when the quest is in the finished quests log
+        @return: False when it's not in the finished quests log"""
+        return quest_id in self.finished_quests
+    
+    def hasFailedQuest(self, quest_id):
+        """Check whether a quest is in the failed quests log
+        @param quest_id: ID of the quest you want to check
+        @return: True on when the quest is in the failed quests log
+        @return: False when it's not in the failed quests log"""
+        return quest_id in self.failed_quests
+    
+    def getStateForSaving(self):
+        """Prepares state for saving
+        @type state: dictionary
+        @param state: State of the object"""
+        ret_dict = {}
+        variables_dict = ret_dict["Variables"] = {}
+        for quest in self.quests.itervalues():
+            quest_dict = variables_dict[quest.quest_id] = {}
+            for variable, data in quest.quest_variables.iteritems():
+                quest_dict[variable] = data["value"]
+        ret_dict["ActiveQuests"] = self.active_quests
+        ret_dict["FinishedQuests"] = self.finished_quests
+        ret_dict["FailedQuests"] = self.failed_quests
+        return ret_dict
+
+    def restoreFromState(self, state):
+        """Restores the state"""
+        variables_dict = state["Variables"]
+        for quest_id, variables in variables_dict.iteritems():
+            for variable, value in variables.iteritems():
+                self.quests[quest_id].setValue(variable, value)
+        self.active_quests = state["ActiveQuests"]
+        self.finished_quests = state["FinishedQuests"]
+        self.failed_quests = state["FailedQuests"]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/serializers.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,156 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Provides classes used to serialize and deserialize Python classes.
+"""
+
+from abc import ABCMeta, abstractmethod
+try:
+    from xml.etree import cElementTree as ElementTree
+except ImportError:
+    from xml.etree import ElementTree
+try:
+    from collections import OrderedDict
+except ImportError:
+    from .common.ordereddict import OrderedDict
+
+from .common.utils import dedent_chomp
+
+import logging
+
+logger = logging.getLogger('serializers')
+
+class Serializable(object):
+    def __init__(self, class_, init_args=None, attributes=None):
+        self.class_ = class_
+        if init_args is not None:
+            self.init_args = OrderedDict(init_args)
+        else:
+            self.init_args = OrderedDict()
+        if attributes is not None:
+            self.attributes = OrderedDict(attributes)
+        else:
+            self.attributes = OrderedDict()
+
+
+class SerializableRegistry(object):
+    """
+    Class holding the data used to serialize and deserialize a particular
+    Python object.
+    """
+    registered_classes = {}
+    
+    @classmethod
+    def registerClass(cls, name, class_, init_args=None, attributes=None):
+        serializable = Serializable(class_, init_args, attributes)
+        cls.registered_classes[name] = serializable
+
+
+class AbstractSerializer(object):
+    __metaclass__ = ABCMeta
+    
+    @abstractmethod
+    def serialize(self, object_, stream):
+        pass
+    
+    @abstractmethod
+    def deserialize(self, stream):
+        pass
+
+
+class XmlSerializer(AbstractSerializer):
+    def serialize(self, statistic, stream):
+        pass
+    
+    @classmethod
+    def deserialize(cls, stream):
+        element_tree = ElementTree.parse(stream)
+        root_element = element_tree.getroot()
+        object_ = cls.construct_object(root_element)
+        return object_
+    
+    @classmethod
+    def construct_object(cls, element):
+        element_name = element.tag
+        if element_name in SerializableRegistry.registered_classes.keys():
+            object_ = cls.construct_registered_class(element)
+        elif len(element) > 0:
+            # Element contains subelements, so we'll treat it as an
+            # OrderedDict.
+            if element_name == 'list':
+                object_ = cls.construct_list(element)
+            else:
+                object_ = cls.construct_ordered_dict(element)
+        else:
+            object_ = cls.construct_primitive(element)
+        return object_
+    
+    @classmethod
+    def construct_registered_class(cls, element):
+        element_name = element.tag
+        serializable = SerializableRegistry.registered_classes[element_name]
+        class_ = serializable.class_
+        init_args = OrderedDict()
+        for subelement in element:
+            arg = cls.construct_object(subelement)
+            subelement_name = subelement.tag
+            init_args[subelement_name] = arg
+        try:
+            object_ = class_(**init_args)
+        except (TypeError, ValueError) as exception:
+            logger.error(init_args)
+            error_message = \
+                'unable to deserialize tag {0}: {1}'.format(element_name,
+                                                            exception)
+            raise ValueError(error_message)
+        return object_
+    
+    @classmethod
+    def construct_ordered_dict(cls, element):
+        object_ = OrderedDict()
+        for subelement in element:
+            child = cls.construct_object(subelement)
+            name = subelement.tag
+            object_[name] = child
+        return object_
+    
+    @classmethod
+    def construct_list(cls, element):
+        object_ = []
+        for subelement in element:
+            child = cls.construct_object(subelement)
+            object_.append(child)
+        return object_
+    
+    @classmethod
+    def construct_primitive(cls, element):
+        text = element.text
+        # Interpret the element's text as unicode by default.
+        element_type = element.attrib.get('type', 'unicode')
+        if element_type == 'unicode':
+            formatted_text = dedent_chomp(text)
+            object_ = unicode(formatted_text)
+        elif element_type == 'str':
+            formatted_text = dedent_chomp(text)
+            object_ = str(formatted_text)
+        elif element_type == 'int':
+            object_ = int(text)
+        elif element_type == 'float':
+            object_ = float(text)
+        else:
+            error_message = '{0!r} is not a recognized primitive type'
+            error_message.format(element_type)
+            raise ValueError(error_message)
+        return object_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/settings.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,477 @@
+#!/usr/bin/env python2
+
+#  Copyright (C) 2011  Edwin Marshall <emarshall85@gmail.com>
+
+#   This file is part of PARPG.
+#
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+""" Provides a class used for reading and writing various configurable options
+    throughout the game
+
+    This class produces an INI formated settings file as opposed to an XML
+    formatted one. The reason that python's built-in ConfigurationParser isn't 
+    sufficient is because comments aren't preserved when writing a settings
+    file, the order in which the options are written isn't preserved, and the 
+    interface used with this class is arguably more convenient that
+    ConfigParser's.
+
+    Default Settings may be generated by envoking this module from the
+    command line:
+        python -m settings.py [system] [data_directory]
+
+    where [system] is one of local, windows, or linux (mac coming soon),
+    and data_directory is the base path for the data files to be loaded.
+
+    Both [system] and [data_directory] are option. If omitted, both
+    default to whichever what is reasonable based on the system settings.py
+    is run on
+"""
+
+import os
+import sys
+import platform
+
+#TODO: add logging to replace print statements
+class Section(object):
+    """ An object that represents a section in a settings file.
+
+        Options can be added to a section by simply assigning a value to an 
+        attribute:
+            section.foo = baz
+        would produce:
+            [section]
+            foo = baz
+        in the settings file. Options that do not exist on assignment
+        are created dynamcially.
+
+        Values are automatically converted to the appropriate python type. 
+        Options that begin and end with brackets([, ]) are converted to lists,
+        and options that are double-quoted (") are converted to strings. 
+        Section also recognizes booleans regardless of case, in addition to the
+        literals 'yes' and 'no' of any case. Except in the case of 
+        double-quoted strings, extra white-space is trimmed, so you need not 
+        worry. For example:
+            foo = bar
+        is equivalent to :
+            foo    =         baz
+    """
+    def __init__(self, name):
+        """ Initialize a new section.
+
+            @param name: name of the section. In the INI file, sections are surrounded
+                         by brackets ([name])
+            @type name: string
+        """
+        self.name = name
+
+    def __setattr__(self, option, value):
+        """ Assign a value to an option, converting types when appropriate.
+
+            @param option: name of the option to assign a value to.
+            @type option: string @param value: value to be assigned to the option.
+            @type value: int, float, string, boolean, or list
+        """
+        value = str(value)
+        if value.startswith('[') and value.endswith(']'):
+            value = [item.strip() for item in value[1:-1].split(',')]
+        elif value.lower() == 'true' or value.lower() == 'yes':
+            value = True
+        elif value.lower() == 'false' or value.lower() == 'no':
+            value = False
+        elif value.isdigit():
+            value = int(value)
+        else:
+            try:
+                value = float(value)
+            except ValueError:
+                # leave as string
+                pass
+
+        self.__dict__[option] = value
+
+    def __getattribute__(self, option):
+        """ Returns the option's value"""
+        # Remove leading and trailing quotes from strings that have them
+        return_value = object.__getattribute__(self, option)
+        try:
+            for key, value in return_value.iteritems():
+                if (hasattr(value, 'split') and 
+                    value.startswith("\"") and value.endswith("\"")):
+                    return_value[key] = value[1:-1]
+        except AttributeError:
+            pass
+
+        return return_value
+
+    @property
+    def options(self):
+        """ Returns a dictionary of existing options """
+        options = self.__dict__
+        # get rid of properties that aren't actually options
+        if options.has_key('name'):
+            options.pop('name')
+
+        return options
+
+class Settings(object):
+    """ An object that represents a settings file, its sectons,
+        and the options defined within those sections.
+    """
+    def __init__(self, settings_path='', system_path='', user_path='', suffix='.cfg'):
+        """ initializes a new settings object. If no paths are given, they are
+            guessed based on whatever platform the script was run on.
+
+            Examples:
+                paths = ['/etc/parpg', '/home/user_name/.config/parpg']
+                settings = Settings(*paths)
+                
+                paths = {'system': '/etc/parpg', 
+                         'user': '/home/user_name/.config/parpg'}
+                settings = Settings(**paths)
+
+                settings = Settings('.')
+
+                settigns = Settings()
+
+            @param system_path: Path to the system settings file.
+            @type system_path: string (must be a valid path)
+
+            @param user_path: Path to the user settings file. Options that
+                              are missing from this file are propogated 
+                              from the system settings file and saved on
+                              request
+            @type user_path: string (must be a valid path)
+            
+            @param suffix: Suffix of the settings file that will be generated.
+            @type suffix: string
+        """
+        if not suffix.startswith('.'):
+            suffix = '.' + suffix
+
+        self.suffix = suffix
+        self.settings_file = ''
+
+
+        self.paths = {}
+        if not system_path and not user_path and not settings_path:
+            # use platform-specific values as paths
+            (self.paths['system'], self.paths['user'], 
+             self.paths['settings']) = self.platform_paths()
+        else:
+            # convert supplied paths to absolute paths
+            abs_paths = [os.path.expanduser(path)
+                         for path in [system_path, user_path, settings_path]]
+            (self.paths['system'], self.paths['user'],
+             self.paths['settings']) = abs_paths
+
+        self.read()
+
+
+    def __getattr__(self, name):
+        """ Returns a Section object to be used for assignment, creating one
+            if it doesn't exist.
+
+            @param name: name of section to be retrieved
+            @type name: string
+        """
+        if name in ['get', 'set']:
+            raise AttributeError("{0} is deprecated. Please consult Settings' "
+                                  "documentation for information on how to "
+                                  "create/modify sections and their respective "
+                                  "options".format(name))
+        else:
+            if not self.__dict__.has_key(name):
+                setattr(self, name, Section(name))
+
+        return getattr(self, name)
+
+    def platform_paths(self, system=None):
+        if system is None:
+            system = platform.system().lower()
+        
+        if system == 'linux':
+            return (os.path.join(os.sep, 'usr', 'share', 'parpg'),
+                    os.path.join(os.environ['XDG_CONFIG_HOME'], 'parpg'),
+                    os.path.join(os.sep, 'etc', 'parpg'))
+        elif system == 'windows':
+            return (os.path.join(os.environ['PROGRAMFILES'], 'PARPG'),
+                    os.path.join(os.environ['USERDATA'], 'PARPG'),
+                    os.path.join(os.environ['PROGRAMFILES'], 'PARPG'))
+        else:
+            # TODO: determine values for Mac
+            return None
+
+    def read(self, filenames=None):
+        """ Reads a settings file and populates the settings object 
+            with its sections and options. Calling this method without
+            any arguments simply re-reads the previously defined filename
+            and paths
+
+            @param filenames: name of files to be parsed. 
+            @type path: string or list
+        """
+        
+        if filenames is None:
+            filenames = [os.path.join(self.paths['settings'], 
+                                      'system{0}'.format(self.suffix)),
+                         os.path.join(self.paths['user'],
+                                      'user{0}'.format(self.suffix))]
+        elif hasattr(filenames, 'split'):
+            filenames = [filenames]
+
+        for filename in filenames:
+            section = None
+
+            try:
+                self.settings_file = open(filename, 'r').readlines()
+            except IOError as (errno, strerror):
+                if errno == 2:
+                    if os.path.basename(filename).startswith('system'):
+                        print ('{0} could not be found. Please supply a '
+                               'different path or generate a system settings '
+                               'file with:\n'
+                               'python2 -m parpg.settings').format(filename)
+                        sys.exit(1)
+                else:
+                    print 'Error No. {0}: {1} {2}'.format(errno, filename, strerror)
+                    sys.exit(1)
+
+            for line in self.settings_file:
+                if line.startswith('#') or line.strip() == '':
+                    continue
+                elif line.startswith('[') and line.endswith(']\n'):
+                    getattr(self, line[1:-2])
+                    section = line[1:-2]
+                else:
+                    option, value = [item.strip() 
+                                     for item in line.split('=', 1)]
+                    setattr(getattr(self, section), option, value)
+
+    def write(self, filename=None):
+        """ Writes a settings file based on the settings object's 
+            sections and options
+
+            @param filename: Name of file to save to. By default, this is
+                             the user settings file.
+            @type path: string
+        """
+        if filename is None:
+            filename = os.path.join(self.paths['user'], 
+                                    'user{0}'.format(self.suffix))
+
+        for section in self.sections:
+            if '[{0}]\n'.format(section) not in self.settings_file:
+                self.settings_file.append('\n[{0}]\n'.format(section))
+                for option, value in getattr(self, section).options.iteritems():
+                    template = '{0} = {1}\n'.format(option, value)
+                    self.settings_file.append(template)
+            else:
+                start_of_section = (self.settings_file
+                                        .index('[{0}]\n'.format(section)) + 1)
+
+                for option, value in getattr(self, 
+                                             section).options.iteritems():
+                    if hasattr(value, 'sort'):
+                        value = '[{0}]'.format(', '.join(value))
+
+                    new_option = False
+                    template = '{0} = {1}\n'.format(option, value)
+                    for index, line in enumerate(self.settings_file[:]):
+                        if option in line:
+                            new_option = False
+                            if str(value) not in line:
+                                self.settings_file[index] = template
+
+                            break
+                        else:
+                            new_option = True
+                    if new_option:
+                        while self.settings_file[start_of_section].startswith('#'):
+                            start_of_section += 1
+
+                        self.settings_file.insert(start_of_section, template)
+
+        with open(filename, 'w') as out_stream:
+            for line in self.settings_file:
+                out_stream.write(line)
+
+    @property
+    def sections(self):
+        """ Returns a list of existing sections"""
+        sections = self.__dict__.keys()
+        sections.pop(sections.index('settings_file'))
+        sections.pop(sections.index('paths'))
+        sections.pop(sections.index('suffix'))
+        
+        return sections
+
+    @property
+    def system_path(self):
+        return self.paths['system']
+
+    @property
+    def user_path(self):
+        return self.paths['user']
+
+    @property
+    def settings_path(self):
+        return self.paths['settings']
+
+DEFAULT_SETTINGS = """\
+[fife]
+#------------------------------------------------------------------------------
+# Options marked with ? are untested/unknown
+
+# Game window's title (string) DO NOT EDIT!
+WindowTitle = PARPG Techdemo 2
+
+# Icon to use for the game window's border (filename) DO NOT EDIT!
+WindowIcon = window_icon.png
+
+# Video driver to use. (?)
+VideoDriver = ""
+
+# Backend to use for graphics (OpenGL|SDL)
+RenderBackend = OpenGL 
+
+# Run the game in fullscreen mode or not. (True|False)
+FullScreen = False
+
+# Screen Resolution's width. Not used if FullScreen is set to False (800|1024|etc)
+ScreenWidth = 1024
+
+# Screen Resolution's height. Not used if FullScreen is set to False (600|768|etc)
+ScreenHeight = 768
+
+# Screen DPI? (?)
+BitsPerPixel = 0
+
+# ? (?)
+SDLRemoveFakeAlpha = 1
+
+# Subdirectory to load icons from (path)
+IconsPath = icons
+
+# ? ([R, G, B])
+ColorKey = [250, 0, 250]
+
+# ? (True|False)
+ColorKeyEnabled = False
+
+# Turn on sound effects and music (True|False)
+EnableSound = True
+
+# Initial volume of sound effects and music (0.0-100.0?)
+InitialVolume = 5.0
+
+# Characters to use to render fonts. DO NOT EDIT!
+FontGlyphs = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`'*#=[]\""
+
+# Subdirectory to load fronts from (path)
+FontsPath = fonts
+
+# Font to load when game starts
+Font = oldtypewriter.ttf
+
+# Size of in-game fonts
+DefaultFontSize = 12
+
+# ? (?)
+LogModules = [controller]
+
+# ? (?)
+PychanDebug = False
+
+# use Psyco Acceperation (True|False)
+UsePsyco = False
+
+# ? (?)
+ProfilingOn = False
+
+# Lighting Model to use (0-2)
+Lighting = 0
+
+[parpg]
+#------------------------------------------------------------------------------
+
+# System subdirectory to load maps from (path)
+MapsPath = maps
+
+# YAML file that contains the available maps (filename)
+MapsFile = maps.yaml
+
+# Map to load when game starts (filename)
+Map = Mall
+
+# ? (filename)
+AllAgentsFile = all_agents.yaml
+
+# System subdirectory to load objects from (path)
+ObjectsPath = objects
+
+# YAML file that contains the database of availabel objects (filename)
+ObjectDatabaseFile = object_database.yaml
+
+# System subdirectory to load dialogues from (path)
+DialoguesPath = dialogue
+
+# System subdirectory to load quests from (path)
+QuestsPath = quests
+
+# User subdirectory to save screenshots to
+ScreenshotsPath = screenshots
+
+# User subdirectory to save games to
+SavesPath = saves
+
+# System subdirectory where gui files are loaded from (path)
+GuiPath = gui
+
+# System subdirectory where cursors are loaded from (path)
+CursorPath = cursors
+
+# File to use for default cursor (filename)
+CursorDefault = cursor_plain.png
+
+# File to use for up cursor (filename)
+CursorUp = cursor_up.png
+
+# File to use for right cursor (filename)
+CursorRight = cursor_right.png
+
+# File to use for down cursor (filename)
+CursorDown = cursor_down.png
+
+# File to use for left cursor (filename)
+CursorLeft = cursor_left.png
+
+# Player walk speed (digit)
+PCSpeed = 3\
+"""
+
+if __name__ == '__main__':
+    from optparse import OptionParser
+
+    usage = "usage: %prog [options] system[, system, ...]"
+    parser = OptionParser(usage=usage)
+
+    parser.add_option('-f', '--filename', default='system.cfg',
+                      help='Filename of output configuration file')
+
+    opts, args = parser.parse_args()
+    
+    with open(opts.filename, 'w') as f:
+        for line in DEFAULT_SETTINGS:
+            f.write(line)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/sounds.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,67 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+# sounds.py holds the object code to play sounds and sound effects
+
+class SoundEngine:
+    def __init__(self, fife_engine):
+        """Initialise the SoundEngine instance
+           @type fife_engine: fine.Engine
+           @param fife_engine: Instance of the Fife engine
+           @return: None"""
+        self.engine = fife_engine
+        self.sound_engine = self.engine.getSoundManager()
+        self.sound_engine.init()
+        # set up the sound
+        self.music = self.sound_engine.createEmitter()
+        self.music_on = False
+        self.music_init = False
+    
+    def playMusic(self, sfile=None):
+        """Play music, with the given file if passed
+           @type sfile: string
+           @param sfile: Filename to play
+           @return: None"""
+        if(sfile is not None):
+            # setup the new sound
+            sound = self.engine.getSoundClipPool().addResourceFromFile(sfile)
+            self.music.setSoundClip(sound)
+            self.music.setLooping(True)
+            self.music_init = True
+        self.music.play()
+        self.music_on = True
+
+    def pauseMusic(self):
+        """Stops current playback
+           @return: None"""
+        if(self.music_init == True):
+            self.music.pause()
+            self.music_on = False
+
+    def toggleMusic(self):
+        """Toggle status of music, either on or off
+           @return: None"""
+        if((self.music_on == False)and(self.music_init == True)):
+            self.playMusic()
+        else:
+            self.pauseMusic()
+
+    def setVolume(self, volume):
+        """Set the volume of the music
+           @type volume: integer
+           @param volume: The volume wanted, 0 to 100
+           @return: None"""
+        self.sound_engine.setVolume(0.01 * volume)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/viewbase.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,26 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+class ViewBase(object):
+    """Base class for views"""
+    def __init__(self, engine, model):
+        """Constructor for engine
+           @param engine: A fife.Engine instance
+           @type engine: fife.Engine
+           @param model: a script.GameModel instance
+           @type model: script.GameModel 
+           """
+        self.engine = engine
+        self.model = model
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system.cfg.in	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,122 @@
+[fife]
+# Options marked with ? are untested/unknown
+
+# Game window's title (string) DO NOT EDIT!
+WindowTitle = PARPG Techdemo 2
+
+# Icon to use for the game window's border (filename) DO NOT EDIT!
+WindowIcon = window_icon.png
+
+# Video driver to use. (?)
+VideoDriver = ""
+
+# Backend to use for graphics (OpenGL|SDL)
+RenderBackend = OpenGL 
+
+# Run the game in fullscreen mode or not. (True|False)
+FullScreen = False
+
+# Screen Resolution's width. Not used if FullScreen is set to False (800|1024|etc)
+ScreenWidth = 800
+
+# Screen Resolution's height. Not used if FullScreen is set to False (600|768|etc)
+ScreenHeight = 600
+
+# Screen DPI? (?)
+BitsPerPixel = 0
+
+# ? (?)
+SDLRemoveFakeAlpha = 1
+
+# Subdirectory to load icons from (path)
+IconsPath = @DATA_DIR@/icons
+
+# ? ([R, G, B])
+ColorKey = [250, 0, 250]
+
+# ? (True|False)
+ColorKeyEnabled = False
+
+# Turn on sound effects and music (True|False)
+EnableSound = True
+
+# Initial volume of sound effects and music (0.0-100.0?)
+InitialVolume = 5.0
+
+# Characters to use to render fonts. DO NOT EDIT!
+FontGlyphs = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`'*#=[]""
+
+# Subdirectory to load fronts from (path)
+FontsPath = @DATA_DIR@/fonts
+
+# Font to load when game starts
+#TODO: make it so that the font name is supplied instead of the filename
+Font = oldtypewriter.ttf
+
+# Size of in-game fonts
+DefaultFontSize = 12
+
+# ? (?)
+LogModules = [controller]
+
+# ? (?)
+PychanDebug = False
+
+# use Psyco Acceperation (True|False)
+UsePsyco = False
+
+# ? (?)
+ProfilingOn = False
+
+# Lighting Model to use (0-2)
+Lighting = 0
+
+[parpg]
+
+# System subdirectory to load maps from (path)
+MapsPath = @DATA_DIR@/maps
+
+# YAML file that contains the available maps (filename)
+MapsFile = maps.yaml
+
+# Map to load when game starts (filename)
+Map = Mall
+
+# ? (filename)
+AllAgentsFile = all_agents.yaml
+
+# System subdirectory to load objects from (path)
+ObjectsPath = @DATA_DIR@/objects
+
+# YAML file that contains the database of availabel objects (filename)
+ObjectDatabaseFile = object_database.yaml
+
+# System subdirectory to load dialogues from (path)
+DialoguesPath = @DATA_DIR@/dialogues
+
+# System subdirectory to load quests from (path)
+QuestsPath = @DATA_DIR@/quests
+
+# System subdirectory where gui files are loaded from (path)
+GuiPath = @DATA_DIR@/gui
+
+# System subdirectory where cursors are loaded from (path)
+CursorPath = @DATA_DIR@/cursors
+
+# File to use for default cursor (filename)
+CursorDefault = cursor_plain.png
+
+# File to use for up cursor (filename)
+CursorUp = cursor_up.png
+
+# File to use for right cursor (filename)
+CursorRight = cursor_right.png
+
+# File to use for down cursor (filename)
+CursorDown = cursor_down.png
+
+# File to use for left cursor (filename)
+CursorLeft = cursor_left.png
+
+# Player walk speed (digit)
+PCSpeed = 3
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_carryable_container.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+
+# This file is part of PARPG.
+#
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+#
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import GameObject, Carryable
+from parpg.objects.composed import CarryableContainer
+
+class  TestCarryableContainer(unittest.TestCase):
+
+    class CarryableObject (GameObject, Carryable):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Carryable.__init__(self, **kwargs)
+
+
+    def setUp(self):
+        self.item = self.CarryableObject(9)
+        self.item.weight = 9
+        self.item2 = self.CarryableObject(10)
+        self.item2.weight = 10
+
+    def tearDown(self):
+        self.item = None
+        self.item2 = None
+
+
+    def testWeight(self):
+        """ Test CarryableContainer weight calculation"""
+        container = CarryableContainer(ID=8)
+        self.assertEquals(container.weight, 0)
+        container.weight = 8
+        self.assertEquals(container.weight, 8)
+        container.placeItem(self.item)
+        print(container.items)
+        self.assertEquals(container.weight, 8+9)
+        container.placeItem(self.item2)
+        print(container.items)
+        self.assertEquals(container.weight, 8+9+10)
+        container.takeItem(self.item)
+        self.assertEquals(container.weight, 8+10)
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_console.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import unittest
+from parpg.console import Console
+
+class test_console(unittest.TestCase):
+    def setUp(self):
+        self.con=Console(None)
+        self.invalString="Invalid command, enter help for more information"
+        pass
+    
+    def tearDown(self):
+        pass 
+
+    def testConsoleCommandHelp(self):
+        """ Test the help console command """
+        
+        self.assertNotEqual(self.con.handleHelp("help"),self.invalString)
+        self.assertNotEqual(self.con.handleConsoleCommand("help"),
+                            self.invalString)
+
+    def testConsoleCommandPython(self):
+        """ Test the python console command """ 
+        self.assertEqual(self.con.handlePython("python 1+1"),"2")
+        self.assertEqual(self.con.handleConsoleCommand("python 1+1"),"2")
+       
+    def testInvalid(self):
+        """Test an invalid console command """
+
+        self.assertEqual(self.con.handleConsoleCommand("invalid"),
+                         self.invalString)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_container.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+
+# This file is part of PARPG.
+# 
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+# 
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import GameObject, Container, Scriptable, Carryable
+
+class  TestContainer(unittest.TestCase):
+    class ScriptableContainer (GameObject, Container, Scriptable):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Container.__init__(self, **kwargs)
+            Scriptable.__init__(self, **kwargs)
+
+    class NonScriptableContainer (GameObject, Container):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Container.__init__(self, **kwargs)
+
+    class CarryableObject (GameObject, Carryable):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Carryable.__init__(self, **kwargs)
+
+
+    def setUp(self):
+        self.ranOnPlaceItem = False
+        self.ranOnTakeItem = False
+        self.item = self.CarryableObject(6)
+        self.item2 = self.CarryableObject(7)
+
+    def tearDown(self):
+        self.item = None
+        self.item2 = None
+
+
+    def onPlaceItem(self):
+        self.ranOnPlaceItem = True
+
+    def onTakeItem(self):
+        self.ranOnTakeItem = True
+    
+
+    def testPlaceTake(self):
+        """ Test Container mixin Place/Take functions """
+        container = self.NonScriptableContainer(5)
+        self.assertEqual(container.items,{})
+        self.assertEqual(self.item.in_container,None)
+        self.assertEqual(container.count(), 0)
+        container.placeItem(self.item)
+        self.assertEqual(container.items,{0:self.item})
+        self.assertEqual(self.item.in_container, container)
+        self.assertEqual(container.count(), 1)
+        self.assertRaises(Container.SlotBusy, container.placeItem, self.item2, 0)
+        self.assertRaises(ValueError, container.takeItem, self.item2)
+        container.takeItem(self.item)
+        self.assertEqual(container.items, {})
+        self.assertEqual(container.count(), 0)
+
+    def testBulk(self):
+        container = self.NonScriptableContainer(5, capacity=15)
+        self.item.bulk=10
+        self.item2.bulk=7
+        container.placeItem(self.item)
+        self.assertRaises(Container.TooBig, container.placeItem, self.item2)
+
+    def testScripting(self):
+        container = self.ScriptableContainer(5,parpg={'onPlaceItem':(self.onPlaceItem,(),{}),'onTakeItem':(self.onTakeItem,(),{})})
+        self.assertFalse(self.ranOnPlaceItem)
+        self.assertFalse(self.ranOnTakeItem)
+        container.placeItem(self.item)
+        self.assertTrue(self.ranOnPlaceItem)
+        self.assertFalse(self.ranOnTakeItem)
+        self.ranOnPlaceItem = False
+        container.takeItem(self.item)
+        self.assertFalse(self.ranOnPlaceItem)
+        self.assertTrue(self.ranOnTakeItem)
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_crate.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.containers import WoodenCrate
+
+class TestWoodenCrate(unittest.TestCase):
+    def setUp(self):
+        self.crate = WoodenCrate(ID='crate01')
+        self.crate2 = WoodenCrate(ID='crate02', locked=True)
+
+    def testCreation(self):
+        """ Test the WoodenCrate creation"""
+        self.assertEqual(self.crate.ID, 'crate01')
+        self.assertEqual(self.crate.name, 'Wooden Crate')
+        self.assertEqual(self.crate.text, 'A battered crate')
+        self.assertEqual(self.crate.gfx, 'crate')
+        self.assertEqual(self.crate.coords, (0.0, 0.0))
+        self.assertEqual(self.crate.map_id, None)
+        self.assertEqual(self.crate.blocking, True)
+        self.assertEqual(self.crate.is_open, True)
+        self.assertEqual(self.crate.locked, False)
+        self.assertEqual(self.crate.parpg, {})
+
+        self.assertEqual(self.crate2.ID, 'crate02')
+        self.assertEqual(self.crate2.locked, True)
+
+    # can't test containing functionality...there are no containable objects
+
+    def testLockable(self):
+        """ Test the WoodenCrate lockability"""
+        self.crate.lock()
+        self.assertEqual(self.crate.locked, True)
+        self.crate.unlock()
+        self.assertEqual(self.crate.locked, False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_dialogueprocessor.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,412 @@
+#!/usr/bin/env python
+#
+#   This file is part of PARPG.
+#
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+try:
+    # Python 2.6
+    import unittest2 as unittest
+except:
+    # Python 2.7
+    import unittest
+
+from parpg.common.utils import dedent_chomp
+from parpg.dialogueprocessor import DialogueProcessor
+# NOTE Technomage 2010-12-08: Using the dialogue data structures might be a
+#    violation of unit test isolation, but ultimately they are just simple
+#    data structures that don't require much testing of their own so I feel
+#    that it isn't a mistake to use them.
+from parpg.dialogue import (Dialogue, DialogueSection, DialogueResponse,
+    DialogueGreeting)
+
+class MockDialogueAction(object):
+    keyword = 'mock_action'
+    
+    def __init__(self, *args, **kwargs):
+        self.arguments = (args, kwargs)
+        self.was_called = False
+        self.call_arguments = []
+    
+    def __call__(self, game_state):
+        self.was_called = True
+        self.call_arguments = ((game_state,), {})
+
+
+class TestDialogueProcessor(unittest.TestCase):
+    """Base class for tests of the L{DialogueProcessor} class."""
+    def assertStateEqual(self, object_, **state):
+        """
+        Assert that an object's attributes match an expected state.
+        
+        @param 
+        """
+        object_dict = {}
+        for key in state.keys():
+            if (hasattr(object_, key)):
+                actual_value = getattr(object_, key)
+                object_dict[key] = actual_value
+        self.assertDictContainsSubset(state, object_dict)
+    
+    def setUp(self):
+        self.npc_id = 'mr_npc'
+        self.dialogue = Dialogue(
+            npc_name='Mr. NPC',
+            avatar_path='/some/path',
+            default_greeting=DialogueSection(
+                id_='greeting',
+                text='This is the root dialogue section.',
+                actions=[
+                    MockDialogueAction('foo'),
+                ],
+                responses=[
+                    DialogueResponse(
+                        text='A response.',
+                        next_section_id='another_section',
+                    ),
+                    DialogueResponse(
+                        text='A conditional response evaluated to True.',
+                        condition='True',
+                        actions=[
+                            MockDialogueAction('foo'),
+                        ],
+                        next_section_id='another_section',
+                    ),
+                    DialogueResponse(
+                        text='A conditional response evaluated to False.',
+                        condition='False',
+                        next_section_id='another_section',
+                    ),
+                    DialogueResponse(
+                        text='A response that ends the dialogue.',
+                        next_section_id='end',
+                    ),
+                ],
+            ),
+            greetings=[
+                DialogueGreeting(
+                    id_='alternative_greeting',
+                    condition='use_alternative_root is True',
+                    text='This is an alternate root section.',
+                    responses=[
+                        DialogueResponse(
+                            text='End dialogue.',
+                            next_section_id='end',
+                        ),
+                    ],
+                ),
+            ],
+            sections=[
+                DialogueSection(
+                    id_='another_section',
+                    text='This is another dialogue section.',
+                    responses=[
+                        DialogueResponse(
+                            text='End dialogue.',
+                            next_section_id='end',
+                        ),
+                    ],
+                ),
+            ]
+        )
+        self.game_state = {'use_alternative_root': False}
+
+
+class TestInitiateDialogue(TestDialogueProcessor):
+    """Tests of the L{DialogueProcessor.initiateDialogue} method."""
+    def setUp(self):
+        TestDialogueProcessor.setUp(self)
+        self.dialogue = Dialogue(
+            npc_name='Mr. NPC',
+            avatar_path='/some/path',
+            default_greeting=DialogueSection(
+                id_='greeting',
+                text='This is the one (and only) dialogue section.',
+                responses=[
+                    DialogueResponse(
+                        text=dedent_chomp('''
+                            A response that moves the dialogue to
+                            another_section.
+                        '''),
+                        next_section_id='another_section'
+                    ),
+                    DialogueResponse(
+                        text='A response that ends the dialogue.',
+                        next_section_id='end',
+                    ),
+                ],
+            ),
+            sections=[
+                DialogueSection(
+                    id_='another_section',
+                    text='This is another section.',
+                    responses=[
+                        DialogueResponse(
+                            text='A response that ends the dialogue',
+                            next_section_id='end',
+                        )
+                    ],
+                ),
+            ]
+        )
+        self.dialogue_processor = DialogueProcessor(self.dialogue, {})
+    
+    def testSetsState(self):
+        """initiateDialogue correctly sets DialogueProcessor state"""
+        dialogue_processor = self.dialogue_processor
+        dialogue_processor.initiateDialogue()
+        
+        # Default root dialogue section should have been pushed onto the stack.
+        default_greeting = self.dialogue.default_greeting
+        self.assertStateEqual(dialogue_processor, in_dialogue=True,
+                              dialogue=self.dialogue,
+                              dialogue_section_stack=[default_greeting])
+    
+    def testEndsExistingDialogue(self):
+        """initiateDialogue ends a previously initiated dialogue"""
+        dialogue_processor = self.dialogue_processor
+        dialogue_processor.initiateDialogue()
+        valid_responses = dialogue_processor.continueDialogue()
+        dialogue_processor.reply(valid_responses[0])
+        
+        # Sanity check.
+        assert dialogue_processor.in_dialogue
+        dialogue_processor.initiateDialogue()
+        default_greeting = self.dialogue.default_greeting
+        self.assertStateEqual(dialogue_processor, in_dialogue=True,
+                              dialogue=self.dialogue,
+                              dialogue_section_stack=[default_greeting])
+
+class TestEndDialogue(TestDialogueProcessor):
+    """Tests of the L{DialogueProcessor.endDialogue} method."""
+    def setUp(self):
+        TestDialogueProcessor.setUp(self)
+        self.dialogue_processor = DialogueProcessor(self.dialogue,
+                                                    self.game_state)
+    
+    def testResetsState(self):
+        """endDialogue correctly resets DialogueProcessor state"""
+        dialogue_processor = self.dialogue_processor
+        # Case: No dialogue initiated.
+        assert not dialogue_processor.in_dialogue, \
+            'assumption that dialogue_processor has not initiated a dialogue '\
+            'violated'
+        self.assertStateEqual(dialogue_processor, in_dialogue=False,
+                              dialogue=self.dialogue,
+                              dialogue_section_stack=[])
+        # Case: Dialogue previously initiated.
+        dialogue_processor.initiateDialogue()
+        assert dialogue_processor.in_dialogue, \
+            'assumption that dialogue_processor initiated a dialogue violated'
+        dialogue_processor.endDialogue()
+        self.assertStateEqual(dialogue_processor, in_dialogue=False,
+                              dialogue=self.dialogue,
+                              dialogue_section_stack=[])
+
+
+class TestContinueDialogue(TestDialogueProcessor):
+    """Tests of the L{DialogueProcessor.continueDialogue} method."""
+    def setUp(self):
+        TestDialogueProcessor.setUp(self)
+        self.dialogue_processor = DialogueProcessor(self.dialogue,
+                                                    self.game_state)
+        self.dialogue_processor.initiateDialogue()
+        self.dialogue_action = \
+            self.dialogue.default_greeting.actions[0]
+    
+    def testRunsDialogueActions(self):
+        """continueDialogue executes all DialogueActions"""
+        dialogue_processor = self.dialogue_processor
+        dialogue_processor.continueDialogue()
+        self.assertTrue(self.dialogue_action.was_called)
+        expected_tuple = ((self.game_state,), {})
+        self.assertTupleEqual(expected_tuple,
+                              self.dialogue_action.call_arguments)
+    
+    def testReturnsValidResponses(self):
+        """continueDialogue returns list of valid DialogueResponses"""
+        dialogue_processor = self.dialogue_processor
+        valid_responses = \
+            dialogue_processor.dialogue_section_stack[0].responses
+        valid_responses.pop(2)
+        # Sanity check, all "valid" responses should have a condition that
+        # evaluates to True.
+        for response in valid_responses:
+            if (response.condition is not None):
+                result = eval(response.condition, self.game_state, {})
+                self.assertTrue(result)
+        responses = dialogue_processor.continueDialogue()
+        self.assertItemsEqual(responses, valid_responses)
+
+
+class TestGetRootDialogueSection(TestDialogueProcessor):
+    """Tests of the L{DialogueProcessor.getDialogueGreeting} method."""
+    def setUp(self):
+        TestDialogueProcessor.setUp(self)
+        self.dialogue_processor = DialogueProcessor(
+            self.dialogue,
+            {'use_alternative_root': True}
+        )
+        self.dialogue_processor.initiateDialogue()
+    
+    def testReturnsCorrectDialogueSection(self):
+        """getDialogueGreeting returns first section with true condition"""
+        dialogue_processor = self.dialogue_processor
+        dialogue = self.dialogue
+        root_dialogue_section = dialogue_processor.getDialogueGreeting()
+        expected_dialogue_section = dialogue.greetings[0]
+        self.assertEqual(root_dialogue_section, expected_dialogue_section)
+
+
+class TestGetCurrentDialogueSection(TestDialogueProcessor):
+    """Tests of the L{DialogueProcessor.getCurrentDialogueSection} method."""
+    def setUp(self):
+        TestDialogueProcessor.setUp(self)
+        self.dialogue_processor = DialogueProcessor(self.dialogue,
+                                                    self.game_state)
+        self.dialogue_processor.initiateDialogue()
+    
+    def testReturnsCorrectDialogueSection(self):
+        """getCurrentDialogueSection returns section at top of stack"""
+        dialogue_processor = self.dialogue_processor
+        expected_dialogue_section = self.dialogue.default_greeting
+        actual_dialogue_section = \
+            dialogue_processor.getCurrentDialogueSection()
+        self.assertEqual(expected_dialogue_section, actual_dialogue_section)
+
+
+class TestRunDialogueActions(TestDialogueProcessor):
+    """Tests of the L{DialogueProcessor.runDialogueActions} method."""
+    def setUp(self):
+        TestDialogueProcessor.setUp(self)
+        self.dialogue_processor = DialogueProcessor(self.dialogue,
+                                                    self.game_state)
+        self.dialogue_processor.initiateDialogue()
+        self.dialogue_section = DialogueSection(
+            id_='some_section',
+            text='Test dialogue section.',
+            actions=[
+                MockDialogueAction('foo'),
+            ],
+        )
+        self.dialogue_response = DialogueResponse(
+            text='A response.',
+            actions=[
+                MockDialogueAction('foo'),
+            ],
+            next_section_id='end',
+        )
+    
+    def testExecutesDialogueActions(self):
+        """runDialogueActions correctly executes DialogueActions"""
+        dialogue_processor = self.dialogue_processor
+        # Case: DialogueSection
+        dialogue_processor.runDialogueActions(self.dialogue_section)
+        dialogue_section_action = self.dialogue_section.actions[0]
+        self.assertTrue(dialogue_section_action.was_called)
+        expected_call_args = ((self.game_state,), {})
+        self.assertTupleEqual(expected_call_args,
+                              dialogue_section_action.call_arguments)
+        # Case: DialogueResponse
+        dialogue_processor.runDialogueActions(self.dialogue_response)
+        dialogue_response_action = self.dialogue_response.actions[0]
+        self.assertTrue(dialogue_response_action.was_called)
+        self.assertTupleEqual(expected_call_args,
+                              dialogue_response_action.call_arguments)
+
+
+class TestGetValidResponses(TestDialogueProcessor):
+    """Tests of the L{DialogueProcessor.getValidResponses} method."""
+    def setUp(self):
+        TestDialogueProcessor.setUp(self)
+        self.dialogue_processor = DialogueProcessor(self.dialogue,
+                                                    self.game_state)
+        self.dialogue_processor.initiateDialogue()
+    
+    def testReturnsValidResponses(self):
+        """getValidResponses returns list of valid DialogueResponses"""
+        dialogue_processor = self.dialogue_processor
+        valid_responses = \
+            dialogue_processor.dialogue_section_stack[0].responses
+        valid_responses.pop(2)
+        # Sanity check, all "valid" responses should have a condition that
+        # evaluates to True.
+        for response in valid_responses:
+            if (response.condition is not None):
+                result = eval(response.condition, {}, {})
+                self.assertTrue(result)
+        responses = dialogue_processor.continueDialogue()
+        self.assertItemsEqual(responses, valid_responses)
+
+
+class TestReply(TestDialogueProcessor):
+    """Tests of the L{DialogueProcessor.reply} method."""
+    def setUp(self):
+        TestDialogueProcessor.setUp(self)
+        self.dialogue_processor = DialogueProcessor(self.dialogue,
+                                                    self.game_state)
+        self.response = self.dialogue.default_greeting.responses[1]
+        self.ending_response = \
+            self.dialogue.default_greeting.responses[3]
+    
+    def testRaisesExceptionWhenNotInitiated(self):
+        """reply raises exception when called before initiateDialogue"""
+        dialogue_processor = self.dialogue_processor
+        # Sanity check: A dialogue must not have been initiated beforehand.
+        self.assertFalse(dialogue_processor.in_dialogue)
+        with self.assertRaisesRegexp(RuntimeError, r'initiateDialogue'):
+            dialogue_processor.reply(self.response)
+    
+    def testExecutesDialogueActions(self):
+        """reply correctly executes DialogueActions in a DialogueResponse"""
+        dialogue_processor = self.dialogue_processor
+        dialogue_processor.initiateDialogue()
+        dialogue_processor.reply(self.response)
+        dialogue_action = self.response.actions[0]
+        self.assertTrue(dialogue_action.was_called)
+        expected_call_args = ((self.game_state,), {})
+        self.assertTupleEqual(expected_call_args,
+                              dialogue_action.call_arguments)
+    
+    def testJumpsToCorrectSection(self):
+        """reply pushes section specified by response onto stack"""
+        dialogue_processor = self.dialogue_processor
+        dialogue_processor.initiateDialogue()
+        # Sanity check: Test response's next_section_id attribute must be refer
+        # to a valid DialogueSection in the test Dialogue.
+        self.assertIn(self.response.next_section_id,
+                      self.dialogue.sections.keys())
+        dialogue_processor.reply(self.response)
+        greeting = self.dialogue.default_greeting
+        next_section = self.dialogue.sections[self.response.next_section_id]
+        self.assertStateEqual(
+            dialogue_processor,
+            in_dialogue=True,
+            dialogue=self.dialogue,
+            dialogue_section_stack=[greeting, next_section],
+        )
+    
+    def testCorrectlyEndsDialogue(self):
+        """reply ends dialogue when DialogueResponse specifies 'end'"""
+        dialogue_processor = self.dialogue_processor
+        dialogue_processor.initiateDialogue()
+        # Sanity check: Test response must have a next_section_id of 'end'.
+        self.assertEqual(self.ending_response.next_section_id, 'end')
+        dialogue_processor.reply(self.ending_response)
+        self.assertStateEqual(dialogue_processor, in_dialogue=False,
+                              dialogue=self.dialogue,
+                              dialogue_section_stack=[])
+
+
+if __name__ == "__main__":
+    unittest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_game_object.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import GameObject
+
+class TestGameObject(unittest.TestCase):
+    def setUp(self):
+        self.game_object=GameObject (1, {'map':'img/test.png'},
+                           1, 1, None, True, 'Test object', 'Description')
+    
+
+    def tearDown(self):
+        self.game_object = None
+
+    def testCoords(self):
+        """ Test GameObject coordinates manipulation"""
+
+        self.assertEqual(self.game_object.coords, (1, 1))
+        self.assertEqual(self.game_object.X, 1)
+        self.assertEqual(self.game_object.Y, 1)
+        self.game_object.coords = (2,2)
+        self.assertEqual(self.game_object.X, 2.0)
+        self.assertEqual(self.game_object.Y, 2.0)
+
+    def testTrueAttr(self):
+        """ Test GameObject trueAttr functionality"""
+        
+        self.game_object.is_test=True
+        self.game_object.is_test2=False
+        self.assertEqual(self.game_object.trueAttr('test'),True)
+        self.assertEqual(self.game_object.trueAttr('test2'),False)
+        self.assertEqual(self.game_object.trueAttr('test3'),False)
+
+    def testRepr(self):
+        """ Test GameObject textual representation"""
+
+        self.assertEqual(repr(self.game_object), "<Test object:1>")
+
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_inventory.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+
+# This file is part of PARPG.
+# 
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+# 
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.inventory import Inventory
+from parpg.objects.base import GameObject, Carryable
+
+class  TestInventory(unittest.TestCase):
+    class CarryableObject (GameObject, Carryable):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Carryable.__init__(self, **kwargs)
+
+    def setUp(self):
+        self.item = self.CarryableObject(12, name="TestItem1")
+        self.item.weight = 12
+        self.item2 = self.CarryableObject(13)
+        self.item2.weight = 13
+        self.inventory = Inventory()
+
+    def testPlaceTakeMove(self):
+        """Test Inventory Place/Take/Move functions"""
+        self.assertTrue(self.inventory.isSlotEmpty("backpack"))
+        self.inventory.placeItem(self.item)
+        self.assertTrue(self.item in self.inventory.getItemsInSlot("backpack").values())
+        self.assertEqual(self.inventory.weight, 12)
+        self.assertEqual(self.inventory.count(), 1)
+        self.assertFalse(self.inventory.isSlotEmpty("backpack"))
+
+        self.inventory.moveItemToSlot(self.item, "groin")
+        self.assertFalse(self.item in self.inventory.getItemsInSlot("backpack").values())
+        self.assertTrue(self.item in self.inventory.getItemsInSlot("groin").values())
+        self.assertEqual(self.inventory.count(), 1)
+        
+        self.assertRaises(ValueError, self.inventory.moveItemToSlot, self.item2, "somewhere")
+        
+        self.inventory.moveItemToSlot(self.item2, "chest")
+        self.assertEqual(self.inventory.count(),2)
+        self.assertEqual(self.inventory.weight, 12+13)
+        self.assertTrue(self.item2 in self.inventory.getItemsInSlot("chest").values())
+
+        self.inventory.takeItem(self.item)
+        self.assertEqual(self.inventory.count(),1)
+        self.assertEqual(self.inventory.weight, 13)
+
+    def testReplace(self):
+        """Test Inventory items replace each other in single-item slots"""
+        self.inventory.placeItem(self.item)
+        self.inventory.moveItemToSlot(self.item,"neck")
+        self.assertFalse(self.inventory.isSlotEmpty("neck"))
+        self.assertTrue(self.item in self.inventory.getItemsInSlot("neck").values())
+
+        self.inventory.moveItemToSlot(self.item2, "neck")
+        self.assertFalse(self.inventory.isSlotEmpty("neck"))
+        self.assertTrue(self.item2 in self.inventory.getItemsInSlot("neck").values())
+        self.assertFalse(self.item in self.inventory.getItemsInSlot("neck").values())
+
+    def testFind(self):
+        self.inventory.placeItem(self.item)
+        self.assertEqual(self.inventory.findItemByID(12), self.item)
+        self.assertEqual(self.inventory.findItemByID(13), None)
+        self.assertEqual(self.inventory.findItem(name="TestItem1"), self.item)
+        self.assertEqual(self.inventory.findItem(name="RandomName1"), None)
+        self.assertEqual(self.inventory.findItem(kind="carryable"), self.item)
+        self.assertEqual(self.inventory.findItem(kind="weapon"), None)
+
+        
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_living.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+# This file is part of PARPG.
+# 
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+# 
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import Living
+
+class  TestLiving(unittest.TestCase):
+
+    def testDie(self):
+        """Test Living mixin die ability"""
+        creature = Living();
+        self.assertTrue(creature.lives)
+        creature.die()
+        self.assertFalse(creature.lives)
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_lockable.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+
+# This file is part of PARPG.
+# 
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+# 
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import Lockable, GameObject
+
+
+class  TestLockable(unittest.TestCase):
+
+    class LockableObject (GameObject, Lockable):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Lockable.__init__(self, **kwargs)
+
+    def testConstructor(self):
+        """ Test Lockable mixin constructor """
+        lockable = self.LockableObject(4)
+        self.assertEqual(lockable.locked,False)
+        self.assertEqual(lockable.is_open,True)
+        lockable = self.LockableObject(4,locked=False,is_open=False)
+        self.assertEqual(lockable.locked, False)
+        self.assertEqual(lockable.is_open, False)
+        lockable = self.LockableObject(4,locked=True)
+        self.assertEqual(lockable.locked, True)
+        self.assertEqual(lockable.is_open, False)
+
+    def testLockUnlock(self):
+        """ Test Lockable mixin locking and unlocking """
+        lockable = self.LockableObject(4)
+        lockable.open()
+        self.assertEqual(lockable.is_open, True)
+        lockable.lock()
+        self.assertEqual(lockable.locked, True)
+        self.assertEqual(lockable.is_open,False)
+        self.assertRaises(ValueError, lockable.open)
+        lockable.unlock()
+        self.assertEqual(lockable.locked, False)
+        self.assertEqual(lockable.is_open,False)    
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_npc.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.actors import NonPlayerCharacter
+
+class MockLayer(object):
+    """Mock Layer Object"""
+    def getId(self):
+        return 1
+
+class TestNonPlayerCharacter(unittest.TestCase):
+    def setUp(self):
+        self.npc=NonPlayerCharacter(1, MockLayer(), 'Ronnie Dobbs')
+    
+    def tearDown(self):
+        self.npc = None
+
+    def testTrueAttr(self):
+        """Test NPC trueAttr functionality"""
+        self.assertEqual(self.npc.trueAttr('living'), True)
+        self.assertEqual(self.npc.trueAttr('charstats'), True)
+
+    def testRepr(self):
+        """Test NPC textual representation"""
+        self.assertEqual(repr(self.npc), "<Ronnie Dobbs:1>")
+
+    def testName(self):
+        """Test NPC name"""
+        self.assertEqual(self.npc.name, "Ronnie Dobbs")
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_objects_base.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import GameObject, Lockable, Container, Living, \
+                            Scriptable, CharStats, Wearable, Usable, Weapon, \
+                            Destructable, Trapable, Carryable
+
+class TestObjectsBase(unittest.TestCase):
+
+    def testWildcard(self):
+        class Wildcard (GameObject, Lockable, Container, Living, Scriptable,
+                        CharStats, Wearable, Usable, Weapon, Destructable,
+                        Trapable, Carryable, ):
+            def __init__ (self, ID, *args, **kwargs):
+                self.name = 'All-purpose carry-all'
+                self.text = 'What is this? I dont know'
+                GameObject.  __init__( self, ID, **kwargs )
+                Lockable.    __init__( self, **kwargs )
+                Container.   __init__( self, **kwargs )
+                Living.      __init__( self, **kwargs )
+                Scriptable.  __init__( self, **kwargs )
+                CharStats.   __init__( self, **kwargs )
+                Wearable.    __init__( self, "left_arm", **kwargs )
+                Usable.      __init__( self, **kwargs )
+                Weapon.      __init__( self, **kwargs )
+                Destructable.__init__( self, **kwargs )
+                Trapable.   __init__( self, **kwargs )
+                Carryable.   __init__( self, **kwargs )
+        wc = Wildcard (2)
+
+        # TODO: need to fill the rest of these tests out
+
+        
+        attrs = dict(
+            openable = {"is_open":True},
+            lockable = {"locked":False},
+            carryable = {"weight":0.0, "bulk":0.0},
+            container = {"items":{}},
+            living = {"lives":True},
+            scriptable = {}
+        )
+
+        for attr in attrs:
+            assert(wc.trueAttr(attr))
+            for value in attrs[attr]:
+                self.assertEqual(getattr(wc, value), attrs[value])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_openable.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import Scriptable, Openable, GameObject
+
+
+class TestOpenable(unittest.TestCase):
+
+    class OpenableScriptable (GameObject, Openable, Scriptable):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Openable.__init__(self, **kwargs)
+            Scriptable.__init__(self, **kwargs)
+
+    class OpenableNonScriptable (GameObject, Openable):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Openable.__init__(self, **kwargs)
+    
+    def onOpen(self):
+        self.ran_on_open=True
+        
+    def onClose(self):
+        self.ran_on_close=True
+
+    def setUp(self):   
+        self.ran_on_open=False
+        self.ran_on_close=False
+    
+    def testOpenClose(self):
+        """ Test Openable mixin open-close functionality"""
+
+        self.openable = self.OpenableNonScriptable(3)
+        self.assertEqual(self.openable.is_open,True)
+
+        self.openable.close()
+        self.assertEqual(self.openable.is_open,False)
+        
+        # Duplicate close() should not lead to any ill effects
+        self.openable.close()
+        self.assertEqual(self.openable.is_open,False)
+
+        self.openable.open()
+        self.assertEqual(self.openable.is_open,True)
+
+        # Duplicate open() should not lead to any ill effects
+        self.openable.open()
+        self.assertEqual(self.openable.is_open,True)
+
+    def testScripting(self):
+        """ Test Openable mixin with scripting"""
+
+        self.openable = self.OpenableScriptable(3, parpg={'onOpen':(self.onOpen,(),{}),'onClose':(self.onClose,(),{})})
+        self.assertEqual(self.ran_on_close,False)
+        self.assertEqual(self.ran_on_open,False)
+        self.assertEqual(self.openable.is_open,True)
+        self.openable.close()
+        self.assertEqual(self.ran_on_close,True)
+        self.assertEqual(self.ran_on_open,False)
+        self.assertEqual(self.openable.is_open,False)
+        self.ran_on_close=False
+        self.openable.open()
+        self.assertEqual(self.ran_on_close,False)
+        self.assertEqual(self.ran_on_open,True)
+        self.assertEqual(self.openable.is_open,True)
+
+    def testInitiallyClosed(self):
+        """ Test Openable mixin instantiation in closed state"""
+        
+        self.openable = self.OpenableNonScriptable(3, is_open=False)
+        self.assertEqual(self.openable.is_open,False)
+        self.openable.open()
+        self.assertEqual(self.openable.is_open,True)
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_scriptable.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+
+# This file is part of PARPG.
+# 
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+# 
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import GameObject, Scriptable
+
+class  TestScriptable(unittest.TestCase):
+    def setUp(self):
+        self.script_ran1=False
+        self.script_ran2=False
+
+    def tearDown(self):
+        self.scriptable = None
+
+    def script1(self,param1,param2):
+        self.script_ran1=True
+        self.assertEqual(param1, 'param1')
+        self.assertEqual(param2, 'param2')
+
+    def script2(self,param3,param4):
+        self.script_ran2=True
+        self.assertEqual(param3, 'param3')
+        self.assertEqual(param4, 'param4')
+
+    def testScripting(self):
+        """Test Scriptable mixin scripting abilities"""
+        scriptable = Scriptable()
+        scriptable.runScript('script1')
+        self.assertFalse(self.script_ran1)
+        self.assertFalse(self.script_ran2)
+        scriptable = Scriptable({'script1':(self.script1,('param1',),{'param2':'param2'})})
+        scriptable.runScript('script1')
+        self.assertTrue(self.script_ran1)
+        self.assertFalse(self.script_ran2)
+        self.script_ran1=False
+        scriptable.setScript('script2', self.script2, ('param3',), {'param4':'param4'})
+        scriptable.runScript('script2')
+        self.assertTrue(self.script_ran2)
+        self.assertFalse(self.script_ran1)
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_single_item_container.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+# This file is part of PARPG.
+# 
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+# 
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import GameObject, Carryable
+from parpg.objects.composed import SingleItemContainer
+
+class  TestSingleItemContainer(unittest.TestCase):
+
+    class CarryableObject (GameObject, Carryable):
+        def __init__ (self, ID, **kwargs):
+            GameObject.__init__(self, ID, **kwargs)
+            Carryable.__init__(self, **kwargs)
+
+
+    def setUp(self):
+        self.item = self.CarryableObject(6)
+        self.item2 = self.CarryableObject(7)
+
+    def tearDown(self):
+        self.item = None
+        self.item2 = None
+
+
+    def testPlaceTake(self):
+        """ Test SingleItemContainer Place/Take functions """
+        container = SingleItemContainer()
+        container.placeItem(self.item)
+        self.assertRaises(container.SlotBusy, container.placeItem, self.item2)
+        container.takeItem(self.item)
+        self.assertEqual(container.items, {})
+        container.placeItem(self.item2)
+
+if __name__ == '__main__':
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_wearable.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+# This file is part of PARPG.
+# 
+# PARPG 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 3 of the License, or
+# (at your option) any later version.
+# 
+# PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from parpg.objects.base import Wearable
+
+class  TestWearable(unittest.TestCase):
+
+    def testInit(self):
+        """Test Wearable mixin's various forms of instantiation"""
+        apparel = Wearable("head");
+        self.assertEqual(apparel.slots,("head",))
+        apparel = Wearable(("head", "shoulders"))
+        self.assertEqual(apparel.slots,("head","shoulders"))
+
+if __name__ == '__main__':
+    unittest.main()