Add Level 1 and 2
authorJason Self <j@jxself.org>
Sun, 10 Jan 2021 19:41:36 +0000 (11:41 -0800)
committerJason Self <j@jxself.org>
Sun, 10 Jan 2021 19:41:36 +0000 (11:41 -0800)
COPYING [new file with mode: 0644]
README [new file with mode: 0644]
c4 [new file with mode: 0755]
twep.txt [new file with mode: 0644]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..f288702
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://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 scripts 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.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 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 <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..aff1dbb
--- /dev/null
+++ b/README
@@ -0,0 +1,33 @@
+These are additional levels for the game Tile World. To use them first 
+catch your rabbit: Somehow get Tile World installed. Your GNU/Linux 
+distribution has probably already packaged it for easy installation.
+
+The level files in the .txt file need to be converted into the .dat 
+file that Tile World expects. Do something like:
+
+c4 -T twep.txt -D twep.dat
+
+Place the .dat file in /usr/share/games/tworld/data.
+
+Edit or create the file /usr/share/games/tworld/sets/test-lynx.dac 
+with this inside:
+
+file=twep.dat
+ruleset=lynx
+
+Now run Tile World see TWEP listed.
+
+Enjoy!
+
+You can redistribute and/or modify these levels, and this file, 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.
+
+The levels are distributed in the hope that they 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 these levels. If not, see <http://www.gnu.org/licenses/>.
\ No newline at end of file
diff --git a/c4 b/c4
new file mode 100755 (executable)
index 0000000..d6ffd5c
--- /dev/null
+++ b/c4
@@ -0,0 +1,3627 @@
+#!/usr/bin/perl -w
+
+#
+# c4: Chip's Challenge Combined Converter
+#
+# Use "perldoc c4" to read the documentation.
+#
+# Copyright (C) 2003-2006 Brian Raiter. This program is licensed under
+# an MIT-style license. Please see the documentation for details.
+#
+
+use strict;
+
+#
+# First, some global functions used across packages.
+#
+
+package main;
+
+# All the names of all the tiles.
+#
+my @tilenames;
+my %tilenames;
+foreach my $names
+       ([ "empty", "floor" ],
+       [ "wall" ],
+       [ "ic chip", "computer chip" ],
+       [ "water" ],
+       [ "fire" ],
+       [ "hidden wall", "invisible wall permanent", "inv wall permanent" ],
+       [ "wall north", "partition north", "blocked north" ],
+       [ "wall west", "partition west", "blocked west" ],
+       [ "wall south", "partition south", "blocked south" ],
+       [ "wall east", "partition east", "blocked east" ],
+       [ "block", "moveable block", "movable block" ],
+       [ "dirt" ],
+       [ "ice" ],
+       [ "force floor south", "force south", "slide south",
+                                             "slide floor south" ],
+       [ "block north", "cloning block north" ],
+       [ "block west", "cloning block west" ],
+       [ "block south", "cloning block south" ],
+       [ "block east", "cloning block east" ],
+       [ "force floor north", "force north", "slide north",
+                                             "slide floor north" ],
+       [ "force floor east", "force east", "slide east", "slide floor east" ],
+       [ "force floor west", "force west", "slide west", "slide floor west" ],
+       [ "exit" ],
+       [ "blue door", "door blue" ],
+       [ "red door", "door red" ],
+       [ "green door", "door green" ],
+       [ "yellow door", "door yellow" ],
+       [ "ice wall southeast", "ice wall se", "ice se",
+                               "ice corner southeast", "ice corner se" ],
+       [ "ice wall southwest", "ice wall sw", "ice sw",
+                               "ice corner southwest", "ice corner sw" ],
+       [ "ice wall northwest", "ice wall nw", "ice nw",
+                               "ice corner northwest", "ice corner nw" ],
+       [ "ice wall northeast", "ice wall ne", "ice ne",
+                               "ice corner northeast", "ice corner ne" ],
+       [ "blue block floor", "blue block fake", "blue wall fake" ],
+       [ "blue block wall", "blue block real", "blue wall real" ],
+       [ "(combination)" ],
+       [ "thief", "spy" ],
+       [ "socket" ],
+       [ "green button", "button green", "toggle button", "button toggle" ],
+       [ "red button", "button red", "clone button", "button clone" ],
+       [ "toggle closed", "toggle wall closed", "closed toggle wall",
+                          "toggle door closed", "closed toggle door" ],
+       [ "toggle open", "toggle wall open", "open toggle wall",
+                        "toggle door open", "open toggle door" ],
+       [ "brown button", "button brown", "trap button", "button trap" ],
+       [ "blue button", "button blue", "tank button", "button tank" ],
+       [ "teleport" ],
+       [ "bomb" ],
+       [ "trap", "beartrap", "bear trap" ],
+       [ "invisible wall", "invisible wall temporary", "inv wall temporary" ],
+       [ "gravel" ],
+       [ "popup wall", "pass once" ],
+       [ "hint button" ],
+       [ "wall southeast", "partition southeast", "blocked southeast",
+                           "wall se", "partition se", "blocked se" ],
+       [ "clone machine", "cloner", "cloning machine" ],
+       [ "force floor any", "force any", "slide any", "slide floor any",
+                            "force floor random", "force random",
+                            "slide random", "slide floor random",
+                            "random slide floor" ],
+       [ "(chip drowned)" ],
+       [ "(chip burned)" ],
+       [ "(chip bombed)" ],
+       [ "(unused 1)" ],
+       [ "(unused 2)" ],
+       [ "(unused 3)" ],
+       [ "(exiting)" ],
+       [ "(exit 1)" ],
+       [ "(exit 2)" ],
+       [ "(chip swimming north)", "(chip swimming n)" ],
+       [ "(chip swimming west)", "(chip swimming w)" ],
+       [ "(chip swimming south)", "(chip swimming s)" ],
+       [ "(chip swimming east)", "(chip swimming e)" ],
+       [ "bug north", "bee north" ],
+       [ "bug west", "bee west" ],
+       [ "bug south", "bee south" ],
+       [ "bug east", "bee east" ],
+       [ "fireball north", "flame north" ],
+       [ "fireball west", "flame west" ],
+       [ "fireball south", "flame south" ],
+       [ "fireball east", "flame east" ],
+       [ "ball north" ],
+       [ "ball west" ],
+       [ "ball south" ],
+       [ "ball east" ],
+       [ "tank north" ],
+       [ "tank west" ],
+       [ "tank south" ],
+       [ "tank east" ],
+       [ "glider north", "ghost north" ],
+       [ "glider west", "ghost west" ],
+       [ "glider south", "ghost south" ],
+       [ "glider east", "ghost east" ],
+       [ "teeth north", "frog north" ],
+       [ "teeth west", "frog west" ],
+       [ "teeth south", "frog south" ],
+       [ "teeth east", "frog east" ],
+       [ "walker north", "dumbbell north" ],
+       [ "walker west", "dumbbell west" ],
+       [ "walker south", "dumbbell south" ],
+       [ "walker east", "dumbbell east" ],
+       [ "blob north" ],
+       [ "blob west" ],
+       [ "blob south" ],
+       [ "blob east" ],
+       [ "paramecium north", "centipede north" ],
+       [ "paramecium west", "centipede west" ],
+       [ "paramecium south", "centipede south" ],
+       [ "paramecium east", "centipede east" ],
+       [ "blue key", "key blue" ],
+       [ "red key", "key red" ],
+       [ "green key", "key green" ],
+       [ "yellow key", "key yellow" ],
+       [ "water boots", "boots water", "water shield", "flippers" ],
+       [ "fire boots", "boots fire", "fire shield" ],
+       [ "ice boots", "boots ice", "spike shoes", "spiked shoes",
+                      "ice skates", "skates" ],
+       [ "force boots", "boots force", "slide boots", "boots slide",
+                        "magnet", "suction boots" ],
+       [ "chip north" ],
+       [ "chip west" ],
+       [ "chip south" ],
+       [ "chip east" ])
+{
+    push @tilenames, $names->[0];
+    @tilenames{@$names} = ($#tilenames) x @$names;
+}
+
+# The original 150 passwords.
+#
+my @origpasswords = @{
+    [qw(BDHP   JXMJ    ECBQ    YMCJ    TQKB    WNLP    FXQO    NHAG
+       KCRE    VUWS    CNPE    WVHI    OCKS    BTDY    COZQ    SKKK
+       AJMG    HMJL    MRHR    KGFP    UGRW    WZIN    HUVE    UNIZ
+       PQGV    YVYJ    IGGZ    UJDD    QGOL    BQZP    RYMS    PEFS
+       BQSN    NQFI    VDTM    NXIS    VQNK    BIFA    ICXY    YWFH
+       GKWD    LMFU    UJDP    TXHL    OVPZ    HDQJ    LXPP    JYSF
+       PPXI    QBDH    IGGJ    PPHT    CGNX    ZMGC    SJES    FCJE
+       UBXU    YBLT    BLDM    ZYVI    RMOW    TIGW    GOHX    IJPQ
+       UPUN    ZIKZ    GGJA    RTDI    NLLY    GCCG    LAJM    EKFT
+       QCCR    MKNH    MJDV    NMRH    FHIC    GRMO    JINU    EVUG
+       SCWF    LLIO    OVPJ    UVEO    LEBX    FLHH    YJYS    WZYV
+       VCZO    OLLM    JPQG    DTMI    REKF    EWCS    BIFQ    WVHY
+       IOCS    TKWD    XUVU    QJXR    RPIR    VDDU    PTAC    KWNL
+       YNEG    NXYB    ECRE    LIOC    KZQR    XBAO    KRQJ    NJLA
+       PTAS    JWNL    EGRW    HXMF    FPZT    OSCW    PHTY    FLXP
+       BPYS    SJUM    YKZE    TASX    MYRT    QRLD    JMWZ    FTLA
+       HEAN    XHIZ    FIRD    ZYFA    TIGG    XPPH    LYWO    LUZL
+       HPPX    LUJT    VLHH    SJUK    MCJE    UCRY    OKOR    GVXQ
+       YBLI    JHEN    COZA    RGSK    DIGW    GNLP)]
+};
+
+# Return true if the given tile is one of the creatures, one of the
+# blocks, or Chip.
+#
+sub iscreature($) { $_[0] >= 0x40 && $_[0] < 0x64 }
+sub isblock($)    { $_[0] == 0x0A || ($_[0] >= 0x0E && $_[0] < 0x12) }
+sub ischip($)     { $_[0] >= 0x6C && $_[0] < 0x70 }
+
+my $filename = undef;
+my $filepos = undef;
+my $filelevel = undef;
+sub err(@)
+{
+    if (defined $filename) {
+       if (defined $filelevel) {
+           print STDERR "$filename: level $filelevel: ";
+       } elsif (defined $filepos) {
+           print STDERR "$filename, byte $filepos: ";
+       } elsif ($.) {
+           print STDERR "$filename:$.: ";
+       } else {
+           print STDERR "$filename: ";
+       }
+    } else {
+       if (defined $filelevel) {
+           print STDERR "$filename: level $filelevel: ";
+       } elsif (defined $filepos) {
+           print STDERR "byte $filepos: ";
+       } elsif ($.) {
+           print STDERR "line $.: ";
+       }
+    }
+    print STDERR @_, "\n";
+    return;
+}
+
+# Given a pack template, return the size of the packed data in bytes.
+# The template is assumed to only contain the types a, C, v, and V.
+#
+sub packlen($)
+{
+    my $template = shift;
+    my $size = 0;
+    while (length $template) {
+       my $char = substr $template, 0, 1, "";
+       my $n = $char eq "V" ? 4 : $char eq "v" ? 2 : 1;
+       $n *= $1 if $template =~ s/\A(\d+)//;
+       $size += $n;
+    }
+    return $size;
+}
+
+# Read a sequence of bytes from a binary file, according to a pack
+# template. The unpacked values are returned.
+#
+sub fileread($$;\$@)
+{
+    my $input = shift;
+    my $template = shift;
+    my $levelsize = shift;
+    my ($buf, $len);
+    $len = ::packlen $template;
+    return ::err "invalid template given to fileread" unless $len > 0;
+    my $ret = sysread $input, $buf, $len;
+    return ::err $! unless defined $ret;
+    return ::err "unexpected EOF" unless $ret;
+    $filepos ||= 0;
+    $filepos += $ret;
+    if (ref $levelsize) {
+       return ::err "invalid metadata in data file",
+                    " (expecting $len bytes; found only $$levelsize)"
+           unless $len <= $$levelsize;
+       $$levelsize -= $len;
+    }
+    my (@fields) = (unpack $template, $buf);
+    foreach my $field (@fields) {
+       last unless @_;
+       my $min = shift;
+       my $max = shift;
+       return ::err "invalid data in data file"
+           if defined $min && $field < $min or defined $max && $field > $max;
+    }
+    return wantarray ? @fields : $fields[-1];
+}
+
+# Translate escape sequences in the given string.
+#
+sub unescape($)
+{
+    local $_ = shift;
+    s/\\([0-7][0-7][0-7])/chr oct$1/eg;
+    s/\\([\\\"])/$1/g;
+    return $_;
+}
+
+sub escape($)
+{
+    local $_ = shift;
+    s/([\\\"])/\\$1/g;
+    s/([^\020-\176])/sprintf"\\%03o",ord$1/eg;
+    return $_;
+}
+
+# Take a standard creature list from a dat file and augment it as
+# necessary for a Lynx-based file format. This involves adding entries
+# for Chip, blocks, immobile creatures, and creatures on clone
+# machines.
+#
+sub makelynxcrlist($$)
+{
+    my $map = shift;
+    my $datcreatures = shift;
+    my @crlist;
+    my @listed;
+
+    if (defined $datcreatures) {
+       foreach my $n (0 .. $#$datcreatures) {
+           $listed[$datcreatures->[$n][0]][$datcreatures->[$n][1]] = $n;
+       }
+    }
+
+    my $chip = undef;
+    foreach my $y (0 .. 31) {
+       foreach my $x (0 .. 31) {
+           my $obj = $map->[$y][$x][0];
+           next unless ::iscreature $obj || ::isblock $obj || ::ischip $obj;
+           my ($seq, $ff, $mobile) = (0, 0, 1);
+           if (::ischip $obj) {
+               return "multiple Chips present" if defined $chip;
+               $chip = @crlist;
+           } elsif (::isblock $obj) {
+               $mobile = -1 if $map->[$y][$x][1] == $tilenames{"cloner"};
+           } else {
+               if ($map->[$y][$x][1] == $tilenames{"cloner"}) {
+                   $mobile = -1;
+               } else {
+                   $mobile = defined $listed[$y][$x] ? 1 : 0;
+               }
+               $seq = $listed[$y][$x] + 1 if defined $listed[$y][$x];
+           }
+           push @crlist, [ $seq, $y, $x, $mobile ];
+       }
+    }
+    return "Chip absent" unless defined $chip;
+    return "over 128 creatures" if @crlist > 128;
+    ($crlist[$chip], $crlist[0]) = ($crlist[0], $crlist[$chip]);
+
+    my @sortlist;
+    foreach my $n (0 .. $#crlist) { push @sortlist, $n if $crlist[$n][0] }
+    @sortlist = sort { $crlist[$a][0] <=> $crlist[$b][0] } @sortlist;
+
+    my @lynxcreatures;
+    foreach my $n (0 .. $#crlist) {
+       my $creature = $crlist[$n];
+       $creature = $crlist[shift @sortlist] if $creature->[0];
+       push @lynxcreatures, [ $creature->[1],
+                              $creature->[2],
+                              $creature->[3] ];
+    }
+
+    return \@lynxcreatures;
+}
+
+# Translate a creature list from a lynx-based file format to one
+# appropriate for a dat-based file format.
+#
+sub makedatcrlist($$)
+{
+    my $map = shift;
+    my $lynxcreatures = shift;
+    my @crlist;
+
+    return undef unless defined $lynxcreatures;
+
+    foreach my $creature (@$lynxcreatures) {
+       next if $creature->[2] != 1;
+       next if ::ischip $map->[$creature->[0]][$creature->[1]][0];
+       next if ::isblock $map->[$creature->[0]][$creature->[1]][0];
+       push @crlist, [ $creature->[0], $creature->[1] ];
+    }
+
+    return \@crlist;
+}
+
+#
+# The textual source file format
+#
+
+package txtfile;
+
+# The list of default tile symbols.
+#
+my %tilesymbols = %{{
+    " "  => $tilenames{"empty"},
+    "#"  => $tilenames{"wall"},
+    "\$" => $tilenames{"ic chip"},
+    ","  => $tilenames{"water"},
+    "&"  => $tilenames{"fire"},
+    "~"  => $tilenames{"wall north"},
+    "|"  => $tilenames{"wall west"},
+    "_"  => $tilenames{"wall south"},
+    " |" => $tilenames{"wall east"},
+    "[]" => $tilenames{"block"},
+    "["  => $tilenames{"block"},
+    ";"  => $tilenames{"dirt"},
+    "="  => $tilenames{"ice"},
+    "v"  => $tilenames{"force south"},
+    "^"  => $tilenames{"force north"},
+    ">"  => $tilenames{"force east"},
+    "<"  => $tilenames{"force west"},
+    "E"  => $tilenames{"exit"},
+    "H"  => $tilenames{"socket"},
+    "6"  => $tilenames{"bomb"},
+    ":"  => $tilenames{"gravel"},
+    "?"  => $tilenames{"hint button"},
+    "_|" => $tilenames{"wall southeast"},
+    "<>" => $tilenames{"force any"},
+    "@"  => $tilenames{"chip south"},
+    "^]" => [ $tilenames{"cloning block north"}, $tilenames{"clone machine"} ],
+    "<]" => [ $tilenames{"cloning block west"},  $tilenames{"clone machine"} ],
+    "v]" => [ $tilenames{"cloning block south"}, $tilenames{"clone machine"} ],
+    ">]" => [ $tilenames{"cloning block east"},  $tilenames{"clone machine"} ]
+}};
+
+#
+#
+#
+
+# Error message display.
+#
+sub err(@) { warn "line $.: ", @_, "\n"; return; }
+
+# The list of incomplete tile names recognized. Each incomplete name
+# has a list of characters that complete them.
+#
+my %partialnames = %{{
+    "key"      => { "blue key"         => "b", "red key"         => "r",
+                    "green key"        => "g", "yellow key"      => "y" },
+    "door"     => { "blue door"        => "b", "red door"        => "r",
+                    "green door"       => "g", "yellow door"     => "y" },
+    "bug"      => { "bug north"        => "n", "bug west"        => "w",
+                    "bug south"        => "s", "bug east"        => "e" },
+    "bee"      => { "bee north"        => "n", "bee west"        => "w",
+                    "bee south"        => "s", "bee east"        => "e" },
+    "fireball" => { "fireball north"   => "n", "fireball west"   => "w",
+                    "fireball south"   => "s", "fireball east"   => "e" },
+    "flame"    => { "flame north"      => "n", "flame west"      => "w",
+                    "flame south"      => "s", "flame east"      => "e" },
+    "ball"     => { "ball north"       => "n", "ball west"       => "w",
+                    "ball south"       => "s", "ball east"       => "e" },
+    "tank"     => { "tank north"       => "n", "tank west"       => "w",
+                    "tank south"       => "s", "tank east"       => "e" },
+    "glider"   => { "glider north"     => "n", "glider west"     => "w",
+                    "glider south"     => "s", "glider east"     => "e" },
+    "ghost"    => { "ghost north"      => "n", "ghost west"      => "w",
+                    "ghost south"      => "s", "ghost east"      => "e" },
+    "teeth"    => { "teeth north"      => "n", "teeth west"      => "w",
+                    "teeth south"      => "s", "teeth east"      => "e" },
+    "frog"     => { "frog north"       => "n", "frog west"       => "w",
+                    "frog south"       => "s", "frog east"       => "e" },
+    "walker"   => { "walker north"     => "n", "walker west"     => "w",
+                    "walker south"     => "s", "walker east"     => "e" },
+    "dumbbell" => { "dumbbell north"   => "n", "dumbbell west"   => "w",
+                    "dumbbell south"   => "s", "dumbbell east"   => "e" },
+    "blob"     => { "blob north"       => "n", "blob west"       => "w",
+                    "blob south"       => "s", "blob east"       => "e" },
+    "paramecium"=> { "paramecium north"        => "n", "paramecium west" => "w",
+                    "paramecium south" => "s", "paramecium east" => "e" },
+    "centipede"        => { "centipede north"  => "n", "centipede west"  => "w",
+                    "centipede south"  => "s", "centipede east"  => "e" },
+    "chip"     => { "chip north"       => "n", "chip west"       => "w",
+                    "chip south"       => "s", "chip east"       => "e" },
+    "(swimming chip)"
+               => { "(swimming chip north)" => "n",
+                    "(swimming chip west)"  => "w",
+                    "(swimming chip south)" => "s",
+                    "(swimming chip east)"  => "e" }
+}};
+
+# The list of tile definitions that are defined throughout the set. A
+# number of definitions are made by default at startup.
+#
+my %globaltiles = %tilesymbols;
+
+# The list of tile definitions for a given level.
+#
+my %localtiles;
+
+# Add a list of tile definitions to a hash.
+#
+sub addtiledefs(\%@)
+{
+    my $tiledefs = shift;
+    while (my $def = shift) { $tiledefs->{$def->[0]} = $def->[1] }
+}
+
+# Given a string, return the tile with that name. If the name is not
+# recognized, undef is returned and a error message is displayed.
+#
+sub lookuptilename($)
+{
+    my $name = shift;
+    my $value = undef;
+
+    return $tilenames{$name} if exists $tilenames{$name};
+
+    if ($name =~ /^0x([0-9A-Fa-f][0-9A-Fa-f])$/) {
+       $value = hex $1;
+       return $value if $value >= 0 && $value <= 255;
+    }
+
+    my $n = length $name;
+    foreach my $key (keys %tilenames) {
+       if ($name eq substr $key, 0, $n) {
+           return ::err "ambiguous object id \"$name\""
+               if defined $value && $value != $tilenames{$key};
+           $value = $tilenames{$key};
+       }
+    }
+    return ::err "unknown object id \"$name\"" unless defined $value;
+    return $value;
+}
+
+# Given two characters, return the tile or pair of tiles which the
+# characters represent. The characters can stand for a pair of tiles
+# directly, or each character can independently represent one tile. In
+# either case, a pair of tiles is returned as an array ref. A single
+# tile is returned directly. If one or both characters are
+# unrecognized, undef is returned and an error message is displayed.
+#
+sub lookuptile($);
+sub lookuptile($)
+{
+    my $symbol = shift;
+    $symbol =~ s/\A(.) \Z/$1/;
+
+    return $localtiles{$symbol} if exists $localtiles{$symbol};
+    return $globaltiles{$symbol} if exists $globaltiles{$symbol};
+
+    if (length($symbol) == 2) {
+       my $top = lookuptile substr $symbol, 0, 1;
+       if (defined $top && ref $top && $top->[1] < 0) {
+           return $top;
+       } elsif (defined $top && !ref $top) {
+           my $bot = lookuptile substr $symbol, 1, 1;
+           if (defined $bot && !ref $bot) {
+               return [ $top, $bot ];
+           }
+       }
+    }
+
+    return ::err "unrecognized map tile \"$symbol\"";
+}
+
+# Return the number of chips present on the map.
+#
+sub getchipcount($)
+{
+    my $map = shift;
+    my $count = 0;
+
+    foreach my $y (0 .. 31) {
+       foreach my $x (0 .. 31) {
+           ++$count if $map->[$y][$x][0] == 0x02;
+           ++$count if $map->[$y][$x][1] == 0x02;
+       }
+    }
+    return $count;
+}
+
+# Given a completed map, return the default list of traps connections
+# as an array ref. (The default list follows the original Lynx rules
+# of connecting buttons to the first subsequent trap in reading
+# order.)
+#
+sub buildtraplist($)
+{
+    my $map = shift;
+    my $firsttrap = undef;
+    my @traps;
+    my @buttons;
+
+    foreach my $y (0 .. 31) {
+       foreach my $x (0 .. 31) {
+           if ($map->[$y][$x][0] == 0x27 || $map->[$y][$x][1] == 0x27) {
+               push @buttons, [ $y, $x ];
+           } elsif ($map->[$y][$x][0] == 0x2B || $map->[$y][$x][1] == 0x2B) {
+               push @traps, map { { from => $_, to => [ $y, $x ] } } @buttons;
+               undef @buttons;
+               $firsttrap = [ $y, $x ] unless defined $firsttrap;
+           }
+       }
+    }
+    push @traps, map { { from => $_, to => $firsttrap } } @buttons
+       if @buttons && defined $firsttrap;
+    return \@traps;
+}
+
+# Given a completed map, return the default list of clone machine
+# connections as an array ref. (This function looks a lot like the
+# prior one.)
+#
+sub buildclonerlist($)
+{
+    my $map = shift;
+    my $firstcm = undef;
+    my @cms;
+    my @buttons;
+
+    foreach my $y (0 .. 31) {
+       foreach my $x (0 .. 31) {
+           if ($map->[$y][$x][0] == 0x24 || $map->[$y][$x][1] == 0x24) {
+               push @buttons, [ $y, $x ];
+           } elsif ($map->[$y][$x][0] == 0x31 || $map->[$y][$x][1] == 0x31) {
+               push @cms, map { { from => $_, to => [ $y, $x ] } } @buttons;
+               undef @buttons;
+               $firstcm = [ $y, $x ] unless defined $firstcm;
+           }
+       }
+    }
+    push @cms, map { { from => $_, to => $firstcm } } @buttons
+       if @buttons && defined $firstcm;
+    return \@cms;
+}
+
+# Given a completed map, return the default ordering of creatures as
+# an array ref. (The default ordering is to first list the creatures
+# in reading order, including Chip. Then, the first creature on the
+# list swaps positions with Chip, who is then removed from the list.)
+#
+sub buildcreaturelist($$)
+{
+    my $map = shift;
+    my $ruleset = shift;
+    my $chippos = undef;
+    my @crlist;
+
+    foreach my $y (0 .. 31) {
+       foreach my $x (0 .. 31) {
+           my $tile = $map->[$y][$x][0];
+           if (::iscreature $tile) {
+               push @crlist, [ $y, $x ];
+           } elsif (::isblock $tile) {
+               push @crlist, [ $y, $x, 0 ];
+           } elsif (::ischip $tile) {
+               $chippos = @crlist;
+               push @crlist, [ $y, $x, 0 ];
+           }
+       }
+    }
+    if ($ruleset eq "lynx") {
+       ($crlist[0], $crlist[$chippos]) = ($crlist[$chippos], $crlist[0])
+           if $chippos;
+       foreach my $item (@crlist) { $#$item = 1 }
+    } else {
+       if (defined $chippos && $chippos > 1) {
+           my $cr = shift @crlist;
+           $crlist[$chippos - 1] = $cr;
+       }
+       for (my $n = $#crlist ; $n >= 0 ; --$n) {
+           splice @crlist, $n, 1 if $#{$crlist[$n]} > 1;
+       }
+    }
+
+    return \@crlist;
+}
+
+# Compare two arrays of lines of text. Wherever the same pair of
+# characters appears in same place in both arrays, the occurrence in
+# the first array is replaced with spaces.
+#
+sub subtracttext(\@\@)
+{
+    my $array = shift;
+    my $remove = shift;
+
+    for (my $n = 0 ; $n < @$array && $n < @$remove ; ++$n) {
+       my $m = 0;
+       while ($m < length $array->[$n] && $m < length $remove->[$n]) {
+           my $a = substr $array->[$n], $m, 2;
+           my $b = substr $remove->[$n], $m, 2;
+           $a .= " " if length $a == 1;
+           $b .= " " if length $b == 1;
+           substr($array->[$n], $m, 2) = "  " if $a eq $b;
+           $m += 2;
+       }
+    }
+}
+
+# Interpret a textual description of a section of the map. The
+# interpreted map data is added to the map array passed as the first
+# argument. The second and third arguments set the origin of the map
+# section. The remaining arguments are the lines from the text file
+# describing the map section. The return value is 1 if the
+# interpretation is successful. If any part of the map sections cannot
+# be understood, undef is returned and an error message is displayed.
+#
+sub parsemap($$$@)
+{
+    my $map = shift;
+    my $y0 = shift;
+    my $x0 = shift;
+    return ::err "map extends below the 32nd row" if $y0 + @_ > 32;
+    for (my $y = $y0 ; @_ ; ++$y) {
+       my $row = shift;
+       return ::err "map extends beyond the 32nd column"
+           if $x0 + length($row) / 2 > 32;
+       for (my $x = $x0 ; length $row ; ++$x) {
+           my $cell = lookuptile substr $row, 0, 2;
+           return ::err "unrecognized tile at ($x $y)" unless defined $cell;
+           return unless defined $cell;
+           if (ref $cell) {
+               if ($cell->[1] < 0) {
+                   $map->[$y][$x] = [ $cell, 0x00 ];
+               } else {
+                   $map->[$y][$x] = $cell;
+               }
+           } else {
+               $map->[$y][$x] = [ $cell, 0x00 ];
+           }
+           substr($row, 0, 2) = "";
+       }
+    }
+    return 1;
+}
+
+# Interpret a textual overlay section. The first argument is the
+# level's hash ref. The second and third arguments set the origin of
+# the overlay section. The remaining arguments are the lines from the
+# text file describing the overlay. The return value is 1 if the
+# interpretation is successful. If any part of the overlay section
+# cannot be understood, undef is returned and an error message is
+# displayed.
+#
+sub parsecon($$$@)
+{
+    my %symbols;
+    my $data = shift;
+    my $y0 = shift;
+    my $x0 = shift;
+    return ::err "overlay extends below the 32nd row" if $y0 + @_ > 32;
+    for (my $y = $y0 ; @_ ; ++$y) {
+       my $row = shift;
+       return ::err "overlay extends beyond the 32nd column"
+           if $x0 + length($row) / 2 > 32;
+       for (my $x = $x0 ; length $row ; ++$x) {
+           $_ = substr $row, 0, 1, "";
+           push @{$symbols{$_}}, [ $y, $x ] unless $_ eq " " || $_ eq "";
+           $_ = substr $row, 0, 1, "";
+           push @{$symbols{$_}}, [ $y, $x ] unless $_ eq " " || $_ eq "";
+       }
+    }
+
+    foreach my $symbol (sort keys %symbols) {
+       my $list = $symbols{$symbol};
+       if (@$list == 1) {
+           my ($y, $x) = ($list->[0][0], $list->[0][1]);
+           my $cell = $data->{map}[$y][$x];
+           return ::err "no creature under \"$symbol\" at ($x $y)"
+               unless defined $cell &&
+                       (::iscreature $cell->[0] || ::iscreature $cell->[1]);
+           push @{$data->{creatures}}, [ $y, $x ];
+       } else {
+           my $linktype = undef;
+           my $to = undef;
+           my (@from, $type);
+           foreach my $pos (@$list) {
+               my ($y, $x) = ($pos->[0], $pos->[1]);
+               my $cell = $data->{map}[$y][$x];
+               my $obj = $cell->[1] || $cell->[0];
+               if ($obj == $tilenames{"red button"}) {
+                   $type = "cloners";
+                   push @from, [ $y, $x ];
+               } elsif ($obj == $tilenames{"brown button"}) {
+                   $type = "traps";
+                   push @from, [ $y, $x ];
+               } elsif ($obj == $tilenames{"clone machine"}) {
+                   $type = "cloners";
+                   return ::err "clone machine under \"$symbol\" at ($x $y) ",
+                                "wired to non-button at ($to->[1] $to->[0])"
+                       if defined $to;
+                   $to = [ $y, $x ];
+               } elsif ($obj == $tilenames{"beartrap"}) {
+                   $type = "traps";
+                   return ::err "beartrap under \"$symbol\" at ($x $y) ",
+                                "wired to non-button at ($to->[1] $to->[0])"
+                       if defined $to;
+                   $to = [ $y, $x ];
+               } else {
+                   return ::err "no button/trap/clone machine ",
+                                "under \"$symbol\" at ($x $y)";
+               }
+               $linktype ||= $type;
+               return ::err "inconsistent connection ",
+                            "under \"$symbol\" at ($x $y)"
+                   unless $linktype eq $type;
+           }
+           push @{$data->{$linktype}},
+                map { { from => $_, to => $to } } @from;
+       }
+    }
+    return 1;
+}
+
+# Interpret a tile definition. Given a line of text supplying the tile
+# definition, the function returns an array ref. Each element in the
+# array is a pair: the first element gives the character(s), and the
+# second element supplies the tile(s). If the definition is ambiguous
+# or invalid, undef is returned and an error message is displayed.
+#
+sub parsetiledef($)
+{
+    my $def = shift;
+    $def =~ s/^(\S\S?)\t//
+       or return ::err "syntax error in tile defintion \"$def\"";
+    my $symbol = $1;
+    $def = lc $def;
+    $def =~ s/^\s+//;
+    $def =~ s/\s+$//;
+
+    if ($def =~ /^([^\+]*[^\+\s])\s*\+\s*([^\+\s][^\+]*)$/) {
+       my ($def1, $def2) = ($1, $2);
+       my ($tile1, $tile2);
+       $tile1 = lookuptilename $def1;
+       return unless defined $tile1;
+       if (lc $def2 eq "pos") {
+           return ::err "ordered tile definition \"$symbol\" ",
+                        "must be a single character"
+               unless length($symbol) == 1;
+           $tile2 = -1;
+       } else {
+           $tile2 = lookuptilename $def2;
+           return unless defined $tile2;
+       }
+       return [ [ $symbol, [ $tile1, $tile2 ] ] ];
+    }
+
+    my @defs;
+    if (exists $partialnames{$def}) {
+       return ::err "incomplete tile definition \"$symbol\" ",
+                    "must be a single character"
+           unless length($symbol) == 1;
+       foreach my $comp (keys %{$partialnames{$def}}) {
+           push @defs, [ $symbol . $partialnames{$def}{$comp},
+                         $tilenames{$comp} ];
+       }
+       return \@defs;
+    }
+
+    my $tile = lookuptilename $def;
+    return [ [ $symbol, $tile ] ] if defined $tile;
+    return;
+}
+
+# Given a handle to a text file, read the introductory lines that
+# precede the first level definition, if any, and return a hash ref
+# for storing the level set. If an error occurs, undef is returned and
+# an error message is displayed.
+#
+sub parseheader($)
+{
+    my $input = shift;
+    my $data = { ruleset => "lynx" };
+    my $slurpingdefs = undef;
+    local $_;
+
+    while (<$input>) {
+       chomp;
+       if (defined $slurpingdefs) {
+           if (/^\s*[Ee][Nn][Dd]\s*$/) {
+               undef $slurpingdefs;
+           } else {
+               my $def = parsetiledef $_;
+               return unless $def;
+               addtiledefs %globaltiles, @$def;
+           }
+           next;
+       } elsif (/^\s*[Tt][Ii][Ll][Ee][Ss]\s*$/) {
+           $slurpingdefs = 1;
+           next;
+       }
+
+       last if /^%%%$/;
+       next if /^\s*$/ || /^%/;
+
+       /^\s*(\S+)\s+(\S(?:.*\S)?)\s*$/ or return ::err "syntax error";
+       my ($name, $value) = ($1, $2);
+       $name = lc $name;
+       if ($name eq "ruleset") {
+           $value = lc $value;
+           return ::err "invalid ruleset \"$value\""
+               unless $value =~ /^(lynx|ms)$/;
+           $data->{ruleset} = $value;
+       } elsif ($name eq "maxlevel") {
+           return ::err "invalid maximum level \"$value\""
+               unless $value =~ /\A\d+\Z/ && $value < 65536;
+           $data->{maxlevel} = $value;
+       } else {
+           return ::err "invalid statement \"$name\"";
+       }
+    }
+
+    return ::err "unclosed definition section" if $slurpingdefs;
+    return $data;
+}
+
+# Given a handle to a text file, positioned at the start of a level
+# description, parse the lines describing the level and return a hash
+# ref containing the level data. If the end of the file is encountered
+# before a level description is found, false is returned. If any
+# errors are encountered, undef is returned and an error message is
+# displayed.
+#
+sub parselevel($$$)
+{
+    my $input = shift;
+    my $ruleset = shift;
+    my $number = shift;
+    my %data = (number => $number, leveltime => 0);
+    my $seenanything = undef;
+    my $slurpingdefs = undef;
+    my $slurpingmap = undef;
+    my @maptext;
+    local $_;
+
+    $data{passwd} = $origpasswords[$number - 1]
+       if $number >= 1 && $number <= 150;
+
+    for my $y (0 .. 31) {
+       for my $x (0 .. 31) { $data{map}[$y][$x] = [ 0, 0 ] }
+    }
+    undef %localtiles;
+
+    while (<$input>) {
+       chomp;
+       if (defined $slurpingdefs) {
+           if (/^\s*[Ee][Nn][Dd]\s*$/) {
+               undef $slurpingdefs;
+           } else {
+               my $def = parsetiledef $_;
+               return unless $def;
+               addtiledefs %localtiles, @$def;
+           }
+           next;
+       } elsif (defined $slurpingmap) {
+           if (/^\s*([AEae])[Nn][Dd]\s*$/) {
+               my $overlay = lc($1) eq "a";
+               if ($slurpingmap->[2] >= 0) {
+                   my @overlaytext = splice @maptext, $slurpingmap->[2];
+                   return ::err "overlay section is taller than map section"
+                       if @overlaytext > @maptext;
+                   subtracttext @overlaytext, @maptext;
+                   return unless parsecon \%data,
+                                          $slurpingmap->[0],
+                                          $slurpingmap->[1],
+                                          @overlaytext;
+               } else {
+                   $slurpingmap->[2] = @maptext;
+                   return unless parsemap $data{map},
+                                          $slurpingmap->[0],
+                                          $slurpingmap->[1],
+                                          @maptext;
+               }
+               unless ($overlay) {
+                   undef $slurpingmap;
+                   undef @maptext;
+               }
+           } else {
+               1 while s{^([^\t]*)\t}{$1 . (" " x (8 - length($1) % 8))}e;
+               push @maptext, $_;
+           }
+           next;
+       } elsif (/^\s*[Tt][Ii][Ll][Ee][Ss]\s*$/) {
+           $slurpingdefs = 1;
+           next;
+       } elsif (/^\s*[Mm][Aa][Pp]\s*(?:(\d+)\s+(\d+)\s*)?$/) {
+           $slurpingmap = [ $2 || 0, $1 || 0, -1 ];
+           next;
+       } elsif (/^\s*[Mm][Aa][Pp]/) {
+           return ::err "invalid syntax following \"map\"";
+       } elsif (/^\s*[Tt][Rr][Aa][Pp][Ss]\s*$/) {
+           $data{traps} ||= [ ];
+           next;
+       } elsif (/^\s*[Cc][Ll][Oo][Nn][Ee][Rr][Ss]\s*$/) {
+           $data{cloners} ||= [ ];
+           next;
+       } elsif (/^\s*[Cc][Rr][Ee][Aa][Tt][Uu][Rr][Ee][Ss]\s*$/) {
+           $data{creatures} ||= [ ];
+           next;
+       }
+
+       last if /^%%%$/;
+       next if /^\s*$/ || /^%/;
+
+       $seenanything = 1;
+       /^\s*(\S+)\s+(\S(?:.*\S)?)\s*$/ or return ::err "syntax error";
+       my ($name, $value) = ($1, $2);
+       $name = lc $name;
+       if ($name eq "level") {
+           return ::err "invalid level number \"$value\""
+               unless $value =~ /\A\d+\Z/ && $value < 65536;
+           $data{number} = $value;
+       } elsif ($name eq "time") {
+           return ::err "invalid level time \"$value\""
+               unless $value =~ /\A\d+\Z/ && $value < 65536;
+           $data{leveltime} = $value;
+       } elsif ($name eq "chips") {
+           return ::err "invalid chip count \"$value\""
+               unless $value =~ /\A\d+\Z/ && $value < 65536;
+           $data{chips} = $value;
+       } elsif ($name eq "title" || $name eq "name") {
+           $value = ::unescape $value if $value =~ s/\A\"(.*)\"\Z/$1/;
+           $data{title} .= " " if defined $data{title};
+           $data{title} .= $value;
+       } elsif ($name eq "password" || $name eq "passwd") {
+           return ::err "invalid password \"$value\""
+               unless $value =~ /\A[A-Z][A-Z][A-Z][A-Z]\Z/;
+           $data{passwd} = $value;
+       } elsif ($name eq "hint") {
+           $value = ::unescape $value if $value =~ s/\A\"(.*)\"\Z/$1/;
+           $data{hint} .= " " if defined $data{hint};
+           $data{hint} .= $value;
+       } elsif ($name eq "traps") {
+           $data{traps} ||= [ ];
+           while ($value =~ s/\A\s* (\d+)\s+(\d+) \s*[-=]?>\s*
+                                    (\d+)\s+(\d+) (?:\s*[,;])?//x) {
+               push @{$data{traps}}, { from => [ $2, $1 ],
+                                       to => [ $4, $3 ] };
+           }
+           return ::err "syntax error in trap list at \"$value\""
+               if $value && $value !~ /\A[,;]\Z/;
+       } elsif ($name eq "cloners") {
+           $data{cloners} ||= [ ];
+           while ($value =~ s/\A\s* (\d+)\s+(\d+) \s*[-=]?>\s*
+                                    (\d+)\s+(\d+) (?:\s*[,;])?//x) {
+               push @{$data{cloners}}, { from => [ $2, $1 ],
+                                         to => [ $4, $3 ] };
+           }
+           return ::err "syntax error in clone machine list at \"$value\""
+               if $value && $value !~ /\A[,;]\Z/;
+       } elsif ($name eq "creatures") {
+           $data{creatures} ||= [ ];
+           while ($value =~ s/\A\s* (\d+)\s+(\d+) (?:\s*[,;])?//x) {
+               push @{$data{creatures}}, [ $2, $1 ];
+           }
+           return ::err "syntax error in creature list at \"$value\""
+               if $value && $value !~ /\A[,;]\Z/;
+       } elsif ($name eq "border") {
+           my $cell = lookuptile $value;
+           return unless defined $cell;
+           $cell = [ $cell, 0x00 ] unless ref $cell;
+           foreach my $y (0 .. 31) { $data{map}[$y][0]  = [ @$cell ] }
+           foreach my $y (0 .. 31) { $data{map}[$y][31] = [ @$cell ] }
+           foreach my $x (1 .. 30) { $data{map}[0][$x]  = [ @$cell ] }
+           foreach my $x (1 .. 30) { $data{map}[31][$x] = [ @$cell ] }
+       } elsif ($name eq "field") {
+           return ::err "invalid field spec \"$value\""
+               unless $value =~ /^(\d+)\s+(\d+(?:\s+\d+)*)$/;
+           my ($num, $data) = ($1, $2);
+           return ::err "multiple specs for field $num"
+               if exists $data{fields}{$num};
+           $data{fields}{$num} = join "", map { chr } split " ", $data;
+       } else {
+           return ::err "invalid command \"$name\"";
+       }
+    }
+    return "" unless $seenanything;
+
+    return ::err "unclosed defs section" if $slurpingdefs;
+    return ::err "unclosed map section" if $slurpingmap;
+
+    return ::err "missing level title" unless exists $data{title};
+    return ::err "missing password" unless exists $data{passwd};
+    return ::err "missing level map" unless exists $data{map};
+
+    $data{chips} = getchipcount $data{map} unless exists $data{chips};
+    $data{traps} ||= buildtraplist $data{map};
+    $data{cloners} ||= buildclonerlist $data{map};
+    $data{creatures} ||= buildcreaturelist $data{map}, $ruleset;
+    $data{lynxcreatures} = ::makelynxcrlist $data{map}, $data{creatures};
+    $data{fields} ||= { };
+
+    return ::err "title too long (", length($data{title}), "); ",
+                "254 is the maximum length allowed"
+       if length($data{title}) > 254;
+    return ::err "hint too long (", length($data{hint}), "); ",
+                "254 is the maximum length allowed"
+       if exists $data{hint} && length($data{hint}) > 254;
+    return ::err "too many (", scalar(@{$data{traps}}), ") ",
+                "trap connections; 25 is the maximum allowed"
+       if @{$data{traps}} > 25;
+    return ::err "too many (", scalar(@{$data{cloners}}), ") ",
+                "clone machine connections; 31 is the maximum allowed"
+       if @{$data{cloners}} > 31;
+    return ::err "too many (", scalar(@{$data{creatures}}), ") ",
+                "creatures; 127 is the maximum allowed"
+       if @{$data{creatures}} > 127;
+
+    return \%data;
+}
+
+# This function takes a handle to a text file and returns a hash ref
+# containing the described level set. If the file could not be
+# completely translated, undef is returned and one or more error
+# messages will be displayed.
+#
+sub read($)
+{
+    my $input = shift;
+    my $data;
+
+    $data = parseheader $input;
+    return unless $data;
+
+    my $lastnumber = 0;
+    for (;;) {
+       my $level = parselevel $input, $data->{ruleset}, $lastnumber + 1;
+       return unless defined $level;
+       last unless $level;
+       $lastnumber = $level->{number};
+       push @{$data->{levels}}, $level;
+       last if eof $input;
+    }
+
+    $#{$data->{levels}} = $data->{maxlevel} - 1
+       if exists $data->{maxlevel} && $data->{maxlevel} < @{$data->{levels}};
+
+    return $data;
+}
+
+#
+#
+#
+
+my %globalsymbols;
+my %localsymbols;
+
+$globalsymbols{"0"}[1] = " ";
+$globalsymbols{"0"}[2] = "  ";
+$globalsymbols{"0:0"}[1] = " ";
+$globalsymbols{"0:0"}[2] = "  ";
+foreach my $symbol (keys %tilesymbols) {
+    my $key;
+    if (ref $tilesymbols{$symbol}) {
+       $key = "$tilesymbols{$symbol}[0]:$tilesymbols{$symbol}[1]";
+    } else {
+       $key = $tilesymbols{$symbol};
+    }
+    $globalsymbols{$key}[length $symbol] ||= $symbol;
+}
+
+my @symbollist;
+my $newsym = -1;
+
+sub printwrap($$$)
+{
+    my $output = shift;
+    my $prefix = shift;
+    my @segments = split /(\S\s\S)/, ::escape shift;
+
+    push @segments, "" if @segments % 2 == 0;
+    for (my $n = 1 ; $n < $#segments; ++$n) {
+       $segments[$n - 1] .= substr($segments[$n], 0, 1);
+       $segments[$n] = substr($segments[$n], 2, 1) . $segments[$n + 1];
+       splice @segments, $n + 1, 1;
+    }
+
+    my $width = 75 - length $prefix;
+    my $line = shift @segments;
+    while (@segments) {
+       if (!$line || length($line) + length($segments[0]) < $width) {
+           $line .= " " . shift @segments;
+       } else {
+           $line = "\"$line\"" if $line =~ /\\/;
+           print $output "$prefix $line\n";
+           $line = shift @segments;
+       }
+    }
+    $line = "\"$line\"" if $line =~ /\\/ || $line =~ /^\s/ || $line =~ /\s$/;
+    print $output "$prefix $line\n";
+
+    return 1;
+}
+
+sub printlist($$@)
+{
+    my $output = shift;
+    my $prefix = shift;
+
+    while (@_) {
+       my $item = shift;
+       local $_ = "$prefix $item";
+       my $x = length $_;
+       print $output $_ or return;
+       while (@_) {
+           $x += 3 + length $_[0];
+           last if $x > 76;
+           $item = shift;
+           print $output " ; $item" or return;
+       }
+       print $output "\n" or return;
+    }
+    return 1;
+}
+
+sub tilesymbol($;$)
+{
+    my $tile = shift;
+    my $max = shift || 2;
+
+    return $globalsymbols{$tile}[$max] if defined $globalsymbols{$tile}[$max];
+    return $globalsymbols{$tile}[1] if defined $globalsymbols{$tile}[1];
+    return $localsymbols{$tile}[$max] if defined $localsymbols{$tile}[$max];
+    return $localsymbols{$tile}[1] if defined $localsymbols{$tile}[1];
+    return undef;
+}
+
+sub getnewsym() { shift @symbollist }
+
+sub resetnewsyms()
+{
+    @symbollist = split //,
+      "ABCDFGIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345789@*+.,'`-!";
+}
+
+sub cellsymbol($;$)
+{
+    my $top = shift;
+    my $bot = shift || 0;
+    my $tile;
+    my $symbol;
+
+    return "  " if $top == 0 && $bot == 0;
+
+    $tile = $bot ? "$top:$bot" : $top;
+    $symbol = tilesymbol $tile;
+    if (defined $symbol) {
+       $symbol = "$symbol " if length($symbol) == 1;
+       return $symbol;
+    }
+
+    if ($bot) {
+       if ($top == 0) {
+           $symbol = tilesymbol $bot, 1;
+           return " $symbol" if defined $symbol;
+       } else {
+           my $st = tilesymbol $top, 1;
+           if (defined $st) {
+               my $sb = tilesymbol $bot, 1;
+               return "$st$sb" if defined $sb;
+           }
+       }
+    }
+
+    $symbol = getnewsym;
+    unless (defined $symbol) {
+       ::err "too many unique tile combinations required";
+       $symbol = "\\";
+    }
+    $localsymbols{$tile}[length $symbol] = $symbol;
+
+    $symbol = "$symbol " if length($symbol) == 1;
+    return $symbol;
+}
+
+sub trimmap(\@)
+{
+    my $map = shift;
+    my @xs = (0) x 32;
+    my @ys = (0) x 32;
+
+    my $count = 0;
+    foreach my $y (0 .. 31) {
+       foreach my $x (0 .. 31) {
+           next if $map->[$y][$x][0] == 0 && $map->[$y][$x][1] == 0;
+           ++$xs[$x];
+           ++$ys[$y];
+           ++$count;
+       }
+    }
+    return (0, 0, 0, 0, 0) unless $count;
+
+    my $border = 0;
+    if ($map->[0][0][0] != 0 && $map->[0][0][1] == 0) {
+       my $tile = $map->[0][0][0];
+       foreach my $n (1 .. 31) {
+           goto noborder unless $map->[$n][0][0] == $tile
+                             && $map->[$n][31][0] == $tile
+                             && $map->[0][$n][0] == $tile
+                             && $map->[31][$n][0] == $tile
+                             && $map->[$n][0][1] == 0
+                             && $map->[$n][31][1] == 0
+                             && $map->[0][$n][1] == 0
+                             && $map->[31][$n][1] == 0;
+       }
+       $border = $tile;
+       $xs[0] = $xs[31] = $ys[0] = $ys[31] = 0;
+      noborder:
+    }
+
+    my ($left, $right, $top, $bottom) = (-1, 32, -1, 32);
+    1 until $xs[++$left];
+    1 until $xs[--$right];
+    1 until $ys[++$top];
+    1 until $ys[--$bottom];
+
+    return 0, 31, 0, 31, 0 if $border && $left == 1 && $right == 30
+                                     && $top == 1 && $bottom == 30;
+
+    return ($left, $right, $top, $bottom, $border);
+}
+
+sub writeheader($\%)
+{
+    my $output = shift;
+    my $data = shift;
+
+       print $output "ruleset $data->{ruleset}\n"
+    and print $output "\n%%%\n";
+}
+
+sub writelevelheader($\%)
+{
+    my $output = shift;
+    my $level = shift;
+
+    printwrap $output, "title  ", $level->{title} or return;
+    print $output "passwd  $level->{passwd}\n" or return;
+    print $output "chips   $level->{chips}\n" or return
+       if exists $level->{chips} && $level->{chips};
+    print $output "time    $level->{leveltime}\n" or return
+       if exists $level->{leveltime} && $level->{leveltime};
+    printwrap $output, "hint   ", $level->{hint} or return
+       if exists $level->{hint};
+    print $output "\n";
+}
+
+sub writelevelmap($\@)
+{
+    my $output = shift;
+    my $map = shift;
+    my (@tiletext, @maptext);
+
+    undef %localsymbols;
+    resetnewsyms;
+
+    my ($left, $right, $top, $bottom, $border) = trimmap @$map;
+
+    $border = cellsymbol $border if $border;
+    foreach my $y ($top .. $bottom) {
+       my $mapline = "";
+       foreach my $x ($left .. $right) {
+           $mapline .= cellsymbol $map->[$y][$x][0], $map->[$y][$x][1];
+       }
+       $mapline =~ s/\s+$//;
+       push @maptext, "$mapline\n";
+    }
+
+    foreach my $tiles (keys %localsymbols) {
+       foreach my $tile (@{$localsymbols{$tiles}}) {
+           next unless defined $tile;
+           my $line = "$tile\t";
+           if ($tiles =~ /^(\d+):(\d+)$/) {
+               my ($top, $bot) = ($1, $2);
+               $line .= "$tilenames[$top] + $tilenames[$bot]\n";
+           } else {
+               $line .= "$tilenames[$tiles]\n";
+           }
+           push @tiletext, $line;
+       }
+    }
+    @tiletext = sort @tiletext;
+
+    print $output "tiles\n", @tiletext, "end\n\n" or return if @tiletext;
+    print $output "border $border\n\n" or return if $border;
+
+    print $output ($left || $top ? "map $left $top\n" : "map\n"),
+                 @maptext,
+                 "end\n\n"
+       or return;
+}
+
+sub writelevelcloners($\%)
+{
+    my $output = shift;
+    my $level = shift;
+    my $n;
+
+    my $default = txtfile::buildclonerlist $level->{map};
+    if (!defined $level->{cloners}) {
+       return print $output "cloners\n\n" if @$default;
+       return 1;
+    }
+    $n = 0;
+    if (@$default == @{$level->{cloners}}) {
+       for ($n = 0 ; $n < @$default ; ++$n) {
+           last if $default->[$n]{from}[0] != $level->{cloners}[$n]{from}[0]
+                || $default->[$n]{from}[1] != $level->{cloners}[$n]{from}[1]
+                || $default->[$n]{to}[0] != $level->{cloners}[$n]{to}[0]
+                || $default->[$n]{to}[1] != $level->{cloners}[$n]{to}[1];
+       }
+    }
+    return 1 if $n == @$default;
+
+    printlist $output, "cloners",
+             map { "$_->{from}[1] $_->{from}[0] -> $_->{to}[1] $_->{to}[0]" }
+                 @{$level->{cloners}}
+       or return;
+    print $output "\n";
+}
+
+sub writeleveltraps($\%)
+{
+    my $output = shift;
+    my $level = shift;
+    my $n;
+
+    my $default = txtfile::buildtraplist $level->{map};
+    if (!defined $level->{traps}) {
+       return print $output "traps\n\n" if @$default;
+       return 1;
+    }
+    $n = 0;
+    if (@$default == @{$level->{traps}}) {
+       for ($n = 0 ; $n < @$default ; ++$n) {
+           last if $default->[$n]{from}[0] != $level->{traps}[$n]{from}[0]
+                || $default->[$n]{from}[1] != $level->{traps}[$n]{from}[1]
+                || $default->[$n]{to}[0] != $level->{traps}[$n]{to}[0]
+                || $default->[$n]{to}[1] != $level->{traps}[$n]{to}[1];
+       }
+    }
+    return 1 if $n == @$default;
+
+    printlist $output, "traps",
+             map { "$_->{from}[1] $_->{from}[0] -> $_->{to}[1] $_->{to}[0]" }
+                 @{$level->{traps}}
+       or return;
+    print $output "\n";
+}
+
+sub writelevelcrlist($\%$)
+{
+    my $output = shift;
+    my $level = shift;
+    my $ruleset = shift;
+    my $n;
+
+    my $default = txtfile::buildcreaturelist $level->{map}, $ruleset;
+    if (!defined $level->{creatures}) {
+       return print $output "creatures\n\n" if @$default;
+       return 1;
+    }
+
+    $n = 0;
+    if (@$default == @{$level->{creatures}}) {
+       for ($n = 0 ; $n < @$default ; ++$n) {
+           last if $default->[$n][0] != $level->{creatures}[$n][0]
+                || $default->[$n][1] != $level->{creatures}[$n][1];
+       }
+    }
+    return 1 if $n == @$default;
+
+    printlist $output, "creatures",
+             map { "$_->[1] $_->[0]" } @{$level->{creatures}}
+       or return;
+    print $output "\n";
+}
+
+sub writelevel($\%$)
+{
+    my $output = shift;
+    my $level = shift;
+    my $ruleset = shift;
+
+    writelevelheader $output, %$level or return;
+    writelevelmap $output, @{$level->{map}} or return;
+    writeleveltraps $output, %$level or return;
+    writelevelcloners $output, %$level or return;
+    writelevelcrlist $output, %$level, $ruleset or return;
+
+    print $output "%%%\n";
+}
+
+sub write($$)
+{
+    my $output = shift;
+    my $data = shift;
+
+    $globalsymbols{$tilenames{"block north"}} =
+                       [ @{$globalsymbols{$tilenames{"block"}}} ]
+       if $data->{ruleset} eq "lynx";
+
+    writeheader $output, %$data or return;
+
+    my $lastnumber = 0;
+    foreach my $level (@{$data->{levels}}) {
+       $filelevel = $level->{number};
+       ++$lastnumber;
+       print $output "\n" or return;
+       print $output "level $level->{number}\n" or return
+           unless $level->{number} == $lastnumber;
+       writelevel $output, %$level, $data->{ruleset} or return;
+       $lastnumber = $level->{number};
+    }
+
+    return 1;
+}
+
+#
+#
+#
+
+package datfile;
+
+# Given a string of run-length encoded data, return the original
+# uncompressed string.
+#
+sub rleuncompress($)
+{
+    local $_ = shift;
+    1 while s/\xFF(.)(.)/$2 x ord$1/se;
+    return $_;
+}
+
+sub parseheader($)
+{
+    my $input = shift;
+    my %data;
+
+    my ($sig, $maxlevel) = ::fileread $input, "Vv" or return;
+    if ($sig == 0x0002AAAC) {
+       $data{ruleset} = "ms";
+    } elsif ($sig == 0x0102AAAC) {
+       $data{ruleset} = "lynx";
+    } else {
+       return ::err "not a valid data file";
+    }
+    return ::err "file contains no maps" if $maxlevel <= 0;
+    $data{maxlevel} = $maxlevel;
+
+    return \%data;
+}
+
+sub parselevelmap($$)
+{
+    my $layer1 = shift;
+    my $layer2 = shift;
+    my @map;
+    if (length($layer1) > 1024) {
+       ::err "warning: excess data in top layer of map";
+       substr($layer1, 1024) = "";
+    }
+    if (length($layer2) > 1024) {
+       ::err "warning: excess data in bottom layer of map";
+       substr($layer2, 1024) = "";
+    }
+    return ::err "invalid map in data file"
+       unless length($layer1) == 1024 && length($layer2) == 1024;
+    foreach my $y (0 .. 31) {
+       foreach my $x (0 .. 31) {
+           $map[$y][$x][0] = ord substr $layer1, 0, 1, "";
+           $map[$y][$x][1] = ord substr $layer2, 0, 1, "";
+       }
+    }
+    return \@map;
+}
+
+sub parselevel($)
+{
+    my $input = shift;
+    my %level;
+    my ($fieldnum, $fieldsize, $data);
+
+    my $levelsize = "";
+    return ::err $! unless defined sysread $input, $levelsize, 2;
+    return "" unless length($levelsize) == 2;
+    $levelsize = unpack "v", $levelsize;
+    return ::err "invalid metadata in file (only $levelsize bytes in level)"
+       unless $levelsize > 8;
+
+    @level{qw(number leveltime chips)} = ::fileread $input, "vvv", $levelsize
+       or return;
+
+    ($fieldnum, $fieldsize) = ::fileread $input, "vv", $levelsize,
+                                        1, 1, 0, 1024
+       or return;
+    my $layer1 = ::fileread $input, "a$fieldsize", $levelsize or return;
+    $fieldsize = ::fileread $input, "v", $levelsize, 0, 1024 or return;
+    my $layer2 = ::fileread $input, "a$fieldsize", $levelsize or return;
+    ::fileread $input, "v", $levelsize or return;
+    $level{map} = parselevelmap rleuncompress $layer1, rleuncompress $layer2
+       or return;
+
+    while ($levelsize > 0) {
+       ($fieldnum, $fieldsize) = ::fileread $input, "CC", $levelsize, 1, 10
+           or last;
+       $data = ::fileread $input, "a$fieldsize", $levelsize or return;
+       if ($fieldnum == 1) {
+           return ::err "invalid field" unless $fieldsize > 1;
+           $level{leveltime} = unpack "v", $data;
+           return ::err "invalid data in field 1"
+               unless $level{leveltime} >= 0 && $level{leveltime} <= 65535;
+       } elsif ($fieldnum == 2) {
+           return ::err "invalid field" unless $fieldsize > 1;
+           $level{chips} = unpack "v", $data;
+           return ::err "invalid data in field 2"
+               unless $level{chips} >= 0 && $level{chips} <= 65535;
+       } elsif ($fieldnum == 3) {
+           ($level{title} = $data) =~ s/\0\Z//;
+       } elsif ($fieldnum == 4) {
+           $fieldsize /= 2;
+           my @values = unpack "v$fieldsize", $data;
+           for (my $i = 0 ; $i < $fieldsize / 5 ; ++$i) {
+               $level{traps}[$i]{from}[1] = shift @values;
+               $level{traps}[$i]{from}[0] = shift @values;
+               $level{traps}[$i]{to}[1] = shift @values;
+               $level{traps}[$i]{to}[0] = shift @values;
+               shift @values;
+           }
+       } elsif ($fieldnum == 5) {
+           $fieldsize /= 2;
+           my @values = unpack "v$fieldsize", $data;
+           for (my $i = 0 ; $i < $fieldsize / 4 ; ++$i) {
+               $level{cloners}[$i]{from}[1] = shift @values;
+               $level{cloners}[$i]{from}[0] = shift @values;
+               $level{cloners}[$i]{to}[1] = shift @values;
+               $level{cloners}[$i]{to}[0] = shift @values;
+           }
+       } elsif ($fieldnum == 6) {
+           ($level{passwd} = $data) =~ s/\0\Z//;
+           $level{passwd} ^= "\x99" x length $level{passwd};
+       } elsif ($fieldnum == 7) {
+           ($level{hint} = $data) =~ s/\0\Z//;
+       } elsif ($fieldnum == 8) {
+           ::err "field 8 not yet supported; ignoring";
+       } elsif ($fieldnum == 9) {
+           ::err "ignoring useless field 9 entry";
+       } elsif ($fieldnum == 10) {
+           my @values = unpack "C$fieldsize", $data;
+           for (my $i = 0 ; $i < $fieldsize / 2 ; ++$i) {
+               $level{creatures}[$i][1] = shift @values;
+               $level{creatures}[$i][0] = shift @values;
+           }
+       }
+    }
+    return ::err "$levelsize bytes left over at end" if $levelsize;
+
+    $level{lynxcreatures} = ::makelynxcrlist $level{map}, $level{creatures};
+
+    return \%level;
+}
+
+sub read($)
+{
+    my $input = shift;
+    my $data;
+
+    $data = parseheader $input;
+    return unless $data;
+
+    for (;;) {
+       my $level = parselevel $input;
+       return unless defined $level;
+       last unless $level;
+       push @{$data->{levels}}, $level;
+    }
+
+    ::err "warning: number of levels incorrect in header ($data->{maxlevel}, ",
+               "should be ", scalar(@{$data->{levels}}), ")"
+       unless $data->{maxlevel} == @{$data->{levels}};
+
+    return $data;
+}
+
+#
+#
+#
+
+# Given a string of packed data, return a string containing the same
+# data run-length encoded.
+#
+sub rlecompress($)
+{
+    my $in = shift;
+    my $out = "";
+
+    while (length $in) {
+       my $byte = substr $in, 0, 1;
+       my $n = 1;
+       ++$n while $n < length $in && $byte eq substr $in, $n, 1;
+       substr($in, 0, $n) = "";
+       while ($n >= 255) { $out .= "\xFF\xFF$byte"; $n -= 255; }
+       if ($n > 3) {
+           $out .= "\xFF" . chr($n) . $byte;
+       } elsif ($n) {
+           $out .= $byte x $n;
+       }
+    }
+    return $out;
+}
+
+# Given a level set definition, return the pack arguments for creating
+# the .dat file's header data.
+#
+sub mkdatfileheader(\%)
+{
+    my $data = shift;
+    my @fields;
+
+    if ($data->{ruleset} eq "ms") {
+       push @fields, 0x0002AAAC;
+    } else {
+       push @fields, 0x0102AAAC;
+    }
+    push @fields, scalar @{$data->{levels}};
+    return ("Vv", @fields);
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's header data in the .dat file.
+#
+sub mkdatfilelevelheader(\%)
+{
+    my $data = shift;
+    my @fields;
+
+    push @fields, $data->{number};
+    push @fields, $data->{leveltime};
+    push @fields, $data->{chips};
+    return ("vvv", @fields);
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's map data in the .dat file.
+# 
+sub mkdatfilelevelmap(\%)
+{
+    my $data = shift;
+    my $map = $data->{map};
+    my ($layer1, $layer2);
+    my @fields;
+
+    for my $y (0 .. 31) {
+       for my $x (0 .. 31) {
+           if (defined $map->[$y][$x]) {
+               if (defined $map->[$y][$x][0]) {
+                   $layer1 .= chr $map->[$y][$x][0];
+               } else {
+                   $layer1 .= "\0";
+               }
+               if (defined $map->[$y][$x][1]) {
+                   $layer2 .= chr $map->[$y][$x][1];
+               } else {
+                   $layer2 .= "\0";
+               }
+           } else {
+               $layer1 .= "\0";
+               $layer2 .= "\0";
+           }
+       }
+    }
+
+    $layer1 = rlecompress $layer1;
+    $layer2 = rlecompress $layer2;
+
+    push @fields, 1;
+    push @fields, length $layer1;
+    push @fields, $layer1;
+    push @fields, length $layer2;
+    push @fields, $layer2;
+
+    return ("vva$fields[1]va$fields[3]", @fields);
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's title field in the .dat file.
+#
+sub mkdatfileleveltitle(\%)
+{
+    my $data = shift;
+    my $n = length($data->{title}) + 1;
+    return ("CCa$n", 3, $n, $data->{title});
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's hint field in the .dat file.
+#
+sub mkdatfilelevelhint(\%)
+{
+    my $data = shift;
+    return ("") unless exists $data->{hint};
+    my $n = length($data->{hint}) + 1;
+    return ("CCa$n", 7, $n, $data->{hint});
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's password field in the .dat file.
+#
+sub mkdatfilelevelpasswd(\%)
+{
+    my $data = shift;
+    my $n = length($data->{passwd}) + 1;
+    return ("CCa$n", 6, $n, $data->{passwd} ^ "\x99\x99\x99\x99");
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's bear trap list field in the .dat file.
+#
+sub mkdatfileleveltraps(\%)
+{
+    my $data = shift;
+
+    return ("") unless exists $data->{traps};
+    my $list = $data->{traps};
+    my $n = @$list;
+    return ("") unless $n;
+    my @fields;
+
+    push @fields, 4;
+    push @fields, $n * 10;
+    foreach my $i (0 .. $#$list) {
+       push @fields, $list->[$i]{from}[1], $list->[$i]{from}[0];
+       push @fields, $list->[$i]{to}[1], $list->[$i]{to}[0];
+       push @fields, 0;
+    }
+    return (("CCv" . ($n * 5)), @fields);
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's clone machine list field in the .dat file.
+#
+sub mkdatfilelevelcloners(\%)
+{
+    my $data = shift;
+
+    return ("") unless exists $data->{cloners};
+    my $list = $data->{cloners};
+    my $n = @$list;
+    return ("") unless $n;
+    my @fields;
+
+    push @fields, 5;
+    push @fields, $n * 8;
+    foreach my $i (0 .. $#$list) {
+       push @fields, $list->[$i]{from}[1], $list->[$i]{from}[0];
+       push @fields, $list->[$i]{to}[1], $list->[$i]{to}[0];
+    }
+    return (("CCv" . ($n * 4)), @fields);
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's creature list field in the .dat file.
+#
+sub mkdatfilelevelcrlist(\%)
+{
+    my $data = shift;
+
+    return ("") unless exists $data->{creatures};
+    my $list = $data->{creatures};
+    return ("") unless $list && @$list;
+    my $n = @$list;
+    my @fields;
+
+    push @fields, 10;
+    push @fields, $n * 2;
+    foreach my $i (0 .. $#$list) {
+       push @fields, $list->[$i][1], $list->[$i][0];
+    }
+    return (("CCC" . ($n * 2)), @fields);
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level's miscellaneous fields, if any, in the .dat file.
+#
+sub mkdatfilelevelmisc(\%)
+{
+    my $data = shift;
+    my ($template, @fields) = ("");
+
+    return ("") unless exists $data->{fields};
+    foreach my $num (keys %{$data->{fields}}) {
+       my $n = length($data->{fields}{$num});
+       $template .= "CCa$n";
+       push @fields, $num, $n, $data->{fields}{$num};
+    }
+    return ($template, @fields);
+}
+
+# Given a level definition, return the pack arguments for creating the
+# level in the .dat file.
+#
+sub mkdatfilelevel(\%)
+{
+    my $data = shift;
+    my ($template, @fields);
+    my @p;
+
+    @p = mkdatfilelevelheader %$data;  $template .= shift @p; push @fields, @p;
+    @p = mkdatfilelevelmap %$data;     $template .= shift @p; push @fields, @p;
+
+    my $data2pos = @fields;            $template .= "v";      push @fields, 0;
+    my $tmplt2pos = length $template;
+
+    @p = mkdatfileleveltitle %$data;   $template .= shift @p; push @fields, @p;
+    @p = mkdatfilelevelhint %$data;    $template .= shift @p; push @fields, @p;
+    @p = mkdatfilelevelpasswd %$data;  $template .= shift @p; push @fields, @p;
+    @p = mkdatfileleveltraps %$data;   $template .= shift @p; push @fields, @p;
+    @p = mkdatfilelevelcloners %$data; $template .= shift @p; push @fields, @p;
+    @p = mkdatfilelevelcrlist %$data;  $template .= shift @p; push @fields, @p;
+    @p = mkdatfilelevelmisc %$data;    $template .= shift @p; push @fields, @p;
+
+    $fields[$data2pos] = ::packlen substr $template, $tmplt2pos;
+
+    unshift @fields, ::packlen $template;
+    $template = "v$template";
+
+    return ($template, @fields);
+}
+
+# Given a level set definition, return the pack arguments for creating
+# the .dat file.
+#
+sub mkdatfile(\%)
+{
+    my $data = shift;
+    my ($template, @fields);
+    my @p;
+
+    @p = mkdatfileheader %$data;
+    $template = shift @p;
+    @fields = @p;
+
+    foreach my $level (@{$data->{levels}}) {
+       $filelevel = $level->{number};
+       @p = mkdatfilelevel %$level;
+       $template .= shift @p;
+       push @fields, @p;
+    }
+
+    return ($template, @fields);
+}
+
+# This function takes a handle to a binary file and a hash ref
+# defining a level set, and writes the level set to the binary file as
+# a .dat file. The return value is false if the file's contents could
+# not be completely created; otherwise a true value is returned.
+#
+sub write($$)
+{
+    my $file = shift;
+    my $data = shift;
+
+    my @args = mkdatfile %$data;
+    my $template = shift @args;
+    print $file pack $template, @args;
+}
+
+#
+#
+#
+
+package lynxfmt;
+
+my @objectkey = ($tilenames{"empty"},
+                $tilenames{"wall"},
+                $tilenames{"ice"},
+                $tilenames{"dirt"},
+                $tilenames{"blue block floor"},
+                $tilenames{"force north"},
+                $tilenames{"force east"},
+                $tilenames{"force south"},
+                $tilenames{"force west"},
+                $tilenames{"force any"},
+                $tilenames{"ice corner se"},
+                $tilenames{"ice corner sw"},
+                $tilenames{"ice corner nw"},
+                $tilenames{"ice corner ne"},
+                $tilenames{"teleport"},
+                $tilenames{"ice boots"},
+                $tilenames{"fire boots"},
+                $tilenames{"force boots"},
+                $tilenames{"water boots"},
+                $tilenames{"fire"},
+                $tilenames{"water"},
+                $tilenames{"thief"},
+                $tilenames{"popup wall"},
+                $tilenames{"toggle open"},
+                $tilenames{"toggle closed"},
+                $tilenames{"green button"},
+                $tilenames{"red door"},
+                $tilenames{"blue door"},
+                $tilenames{"yellow door"},
+                $tilenames{"green door"},
+                $tilenames{"red key"},
+                $tilenames{"blue key"},
+                $tilenames{"yellow key"},
+                $tilenames{"green key"},
+                $tilenames{"blue button"},
+                $tilenames{"computer chip"},   # counted
+                $tilenames{"socket"},
+                $tilenames{"exit"},
+                $tilenames{"invisible wall temporary"},
+                $tilenames{"invisible wall permanent"},
+                $tilenames{"gravel"},
+                $tilenames{"wall east"},
+                $tilenames{"wall south"},
+                $tilenames{"wall southeast"},
+                $tilenames{"bomb"},
+                $tilenames{"bear trap"},
+                $tilenames{"brown button"},
+                $tilenames{"clone machine"},
+                $tilenames{"red button"},
+                $tilenames{"computer chip"},   # uncounted
+                $tilenames{"blue block wall"},
+                $tilenames{"hint button"});
+
+my @creaturekey = (0, 0, 0, 0,
+                  $tilenames{"chip north"},      $tilenames{"chip east"},
+                  $tilenames{"chip south"},      $tilenames{"chip west"},
+                  $tilenames{"bug north"},       $tilenames{"bug east"},
+                  $tilenames{"bug south"},       $tilenames{"bug west"},
+                  $tilenames{"centipede north"}, $tilenames{"centipede east"},
+                  $tilenames{"centipede south"}, $tilenames{"centipede west"},
+                  $tilenames{"fireball north"},  $tilenames{"fireball east"},
+                  $tilenames{"fireball south"},  $tilenames{"fireball west"},
+                  $tilenames{"glider north"},    $tilenames{"glider east"},
+                  $tilenames{"glider south"},    $tilenames{"glider west"},
+                  $tilenames{"ball north"},      $tilenames{"ball east"},
+                  $tilenames{"ball south"},      $tilenames{"ball west"},
+                  $tilenames{"block north"},     $tilenames{"block east"},
+                  $tilenames{"block south"},     $tilenames{"block west"},
+                  $tilenames{"tank north"},      $tilenames{"tank east"},
+                  $tilenames{"tank south"},      $tilenames{"tank west"},
+                  $tilenames{"walker north"},    $tilenames{"walker east"},
+                  $tilenames{"walker south"},    $tilenames{"walker west"},
+                  $tilenames{"blob north"},      $tilenames{"blob east"},
+                  $tilenames{"blob south"},      $tilenames{"blob west"},
+                  $tilenames{"teeth north"},     $tilenames{"teeth east"},
+                  $tilenames{"teeth south"},     $tilenames{"teeth west"});
+
+my @textkey = 
+    ("\n"," ","0","1","2","3","4","5","6","7","8","9","A","B","C","D",
+      "E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T",
+      "U","V","W","X","Y","Z","!",'"',"'","(",")",",","-",".",":",";",
+      "?", (("%") x 207));
+
+my @levelfilenames = @{
+    [qw(lesson_1.pak   lesson_2.pak    lesson_3.pak    lesson_4.pak
+       lesson_5.pak    lesson_6.pak    lesson_7.pak    lesson_8.pak
+       nuts_and.pak    brushfir.pak    trinity.pak     hunt.pak
+       southpol.pak    telebloc.pak    elementa.pak    cellbloc.pak
+       nice_day.pak    castle_m.pak    digger.pak      tossed_s.pak
+       iceberg.pak     forced_e.pak    blobnet.pak     oorto_ge.pak
+       blink.pak       chchchip.pak    go_with_.pak    ping_pon.pak
+       arcticfl.pak    mishmesh.pak    knot.pak        scavenge.pak
+       on_the_r.pak    cypher.pak      lemmings.pak    ladder.pak
+       seeing_s.pak    sampler.pak     glut.pak        floorgas.pak
+       i.pak           beware_o.pak    lock_blo.pak    refracti.pak
+       monster_.pak    three_do.pak    pier_sev.pak    mugger_s.pak
+       problems.pak    digdirt.pak     i_slide.pak     the_last.pak
+       traffic_.pak    grail.pak       potpourr.pak    deepfree.pak
+       mulligan.pak    loop_aro.pak    hidden_d.pak    scoundre.pak
+       rink.pak        slo_mo.pak      block_fa.pak    spooks.pak
+       amsterda.pak    victim.pak      chipmine.pak    eeny_min.pak
+       bounce_c.pak    nightmar.pak    corridor.pak    reverse_.pak
+       morton.pak      playtime.pak    steam.pak       four_ple.pak
+       invincib.pak    force_sq.pak    drawn_an.pak    vanishin.pak
+       writers_.pak    socialis.pak    up_the_b.pak    wars.pak
+       telenet.pak     suicide.pak     citybloc.pak    spirals.pak
+       block.pak       playhous.pak    jumping_.pak    vortex.pak
+       roadsign.pak    now_you_.pak    four_squ.pak    paranoia.pak
+       metastab.pak    shrinkin.pak    catacomb.pak    colony.pak
+       apartmen.pak    icehouse.pak    memory.pak      jailer.pak
+       short_ci.pak    kablam.pak      balls_o_.pak    block_ou.pak
+       torturec.pak    chiller.pak     time_lap.pak    fortune_.pak
+       open_que.pak    deceptio.pak    oversea_.pak    block_ii.pak
+       the_mars.pak    miss_dir.pak    slide_st.pak    alphabet.pak
+       perfect_.pak    t_fair.pak      the_pris.pak    firetrap.pak
+       mixed_nu.pak    block_n_.pak    skelzie.pak     all_full.pak
+       lobster_.pak    ice_cube.pak    totally_.pak    mix_up.pak
+       blobdanc.pak    pain.pak        trust_me.pak    doublema.pak
+       goldkey.pak     partial_.pak    yorkhous.pak    icedeath.pak
+       undergro.pak    pentagra.pak    stripes.pak     fireflie.pak
+       level145.pak    cake_wal.pak    force_fi.pak    mind_blo.pak
+       special.pak     level150.pak)]
+};
+
+my (%objectkey, %creaturekey, %textkey);
+for (0 .. $#objectkey) { $objectkey{$objectkey[$_]} = $_ }
+for (0 .. $#creaturekey) { $creaturekey{$creaturekey[$_]} = $_ }
+$creaturekey{$tilenames{"block"}} = $creaturekey{$tilenames{"block north"}};
+for (0 .. $#textkey) { $textkey{$textkey[$_]} = chr $_ }
+
+#
+#
+#
+
+sub longestmatch($$$)
+{
+    my $dictionary = shift;
+    my $data = shift;
+    my $pos = shift;
+
+    my ($longest, $longestlen) = ("", 0);
+    foreach my $entry (@$dictionary) {
+       my $len = length $entry->{text};
+       if ($len > $longestlen && $entry->{text} eq substr $data, $pos, $len) {
+           ($longest, $longestlen) = ($entry, $len);
+       }
+    }
+    return $longest;
+}
+
+sub builddict($)
+{
+    my $data = shift;
+    my $dictionary = [ ];
+
+    my $pos = 0;
+    while ($pos < length $data) {
+       my $entry = { refcount => 0 };
+       my ($match, $len);
+       $match = longestmatch $dictionary, $data, $pos;
+       if ($match) {
+           $entry->{left} = $match;
+           $len = length $match->{text};
+       } else {
+           $len = 1;
+       }
+       $entry->{text} = substr $data, $pos, $len;
+       $pos += $len;
+       last if $pos >= length $data;
+       $match = longestmatch $dictionary, $data, $pos;
+       if ($match) {
+           $entry->{right} = $match;
+           $len = length $match->{text};
+       } else {
+           $len = 1;
+       }
+       $entry->{text} .= substr $data, $pos, $len;
+       $pos += $len;
+       push @$dictionary, $entry;
+    }
+
+    return $dictionary;
+}
+
+sub refcountadd($$);
+sub refcountadd($$)
+{
+    my $entry = shift;
+    $entry->{refcount} += shift;
+    refcountadd $entry->{left}, $entry->{refcount} if exists $entry->{left};
+    refcountadd $entry->{right}, $entry->{refcount} if exists $entry->{right};
+}
+
+sub countuses($$)
+{
+    my $dictionary = shift;
+    my $data = shift;
+
+    my $pos = 0;
+    while ($pos < length $data) {
+       my $entry = longestmatch $dictionary, $data, $pos;
+       if ($entry) {
+           ++$entry->{refcount};
+           $pos += length $entry->{text};
+       } else {
+           ++$pos;
+       }
+    }
+    foreach my $entry (@$dictionary) { refcountadd $entry, 0 }
+}
+
+sub assignkeys($$)
+{
+    my $dictionary = shift;
+    my $data = shift;
+    my @used;
+
+    while ($data =~ /(.)/gs) { $used[ord $1] = 1 }
+    my $n = 0;
+    foreach my $entry (@$dictionary) {
+       ++$n while $used[$n];
+       die "too many dictionary entries; not enough keys" if $n >= 256;
+       $entry->{key} = chr $n;
+       $used[$n] = 1;
+    }
+}
+
+sub composedict($)
+{
+    my $dictionary = shift;
+    my ($out, $len) = ("", 0);
+
+    foreach my $entry (@$dictionary) {
+       $out .= $entry->{key};
+       if (exists $entry->{left}) {
+           $out .= $entry->{left}{key};
+       } else {
+           $out .= substr $entry->{text}, 0, 1;
+       }
+       if (exists $entry->{right}) {
+           $out .= $entry->{right}{key};
+       } else {
+           $out .= substr $entry->{text}, -1;
+       }
+       ++$len;
+    }
+
+    return ($out, $len);
+}
+
+sub composedata($$)
+{
+    my $dictionary = shift;
+    my $data = shift;
+    my ($out, $len) = ("", 0);
+
+    my $pos = 0;
+    while ($pos < length $data) {
+       my $entry = longestmatch $dictionary, $data, $pos;
+       if ($entry) {
+           $out .= $entry->{key};
+           $pos += length $entry->{text};
+       } else {
+           $out .= substr $data, $pos, 1;
+           ++$pos;
+       }
+       ++$len;
+    }
+
+    return ($out, $len);
+}
+
+sub compress($)
+{
+    my $data = shift;
+    my $dictionary = builddict $data;
+    countuses $dictionary, $data;
+    $dictionary = [ grep { $_->{refcount} > 3 } @$dictionary ];
+    assignkeys $dictionary, $data;
+    my ($cdict, $dictlen) = composedict $dictionary;
+    my ($cdata, $datalen) = composedata $dictionary, $data;
+    return pack("vv", $dictlen, $datalen) . $cdict . $cdata;
+}
+
+sub expand($)
+{
+    my $data = shift;
+
+    my $tablesize = unpack "v", substr $data, 0, 2, "";
+    my $datasize = unpack "v", substr $data, 0, 2, "";
+
+    my @data = map { ord } split //, $data;
+    my @table;
+
+    for (my $n = 0 ; $n < $tablesize ; ++$n) {
+       return ::err "@{[$tablesize - $n]} entries missing"
+           unless @data;
+       my $key = shift @data;
+       my $val1 = shift @data;
+       my $val2 = shift @data;
+       if (defined $table[$val1]) {
+           $val1 = $table[$val1];
+       } else {
+           $val1 = chr $val1;
+       }
+       if (defined $table[$val2]) {
+           $val2 = $table[$val2];
+       } else {
+           $val2 = chr $val2;
+       }
+       $table[$key] = "$val1$val2";
+    }
+
+    $data = "";
+    foreach my $byte (@data) {
+       if (defined $table[$byte]) {
+           $data .= $table[$byte];
+       } else {
+           $data .= chr $byte;
+       }
+    }
+
+    return $data;
+}
+
+sub parsemap($$)
+{
+    my $level = shift;
+    my @data = map { ord } split //, shift;
+
+    return ::err "@{[1024 - @data]} bytes missing from map data"
+       unless @data == 1024;
+    $level->{chips} = 0;
+    foreach my $y (0 .. 31) {
+       foreach my $x (0 .. 31) {
+           my $obj = shift @data;
+           ::err "undefined object $obj at ($x $y)"
+               unless defined $objectkey[$obj];
+           $level->{map}[$y][$x][0] = $objectkey[$obj];
+           $level->{map}[$y][$x][1] = 0;
+           ++$level->{chips} if $obj == 0x23;
+       }
+    }
+
+    return 1;
+}
+
+sub parsecrlist($$)
+{
+    my $level = shift;
+    my $data = shift;
+
+    my @t = map { ord } split //, substr $data, 0, 128, "";
+    my @x = map { ord } split //, substr $data, 0, 128, "";
+    my @y = map { ord } split //, substr $data, 0, 128, "";
+
+    foreach my $n (0 .. 127) {
+       next unless $t[$n];
+       my $x = $x[$n] >> 3;
+       my $y = $y[$n] >> 3;
+       my $t = $creaturekey[$t[$n] & 0x7F];
+       push @{$level->{creatures}}, [ $y, $x ]; # unless $t[$n] & 0x80;
+       $level->{map}[$y][$x][1] = $level->{map}[$y][$x][0];
+       $level->{map}[$y][$x][0] = $t;
+    }
+
+    return 1;
+}
+
+sub parselevel($)
+{
+    my $data = shift;
+    my $level = { };
+
+    $data = expand $data or return ::err "invalid data";
+
+    local $_;
+    $_ = substr $data, 0, 1024, "";
+    parsemap $level, $_
+       or return ::err "invalid map";
+    $_ = substr $data, 0, 384, "";
+    parsecrlist $level, $_
+       or return ::err "invalid creature list";
+    $level->{creatures} = ::makedatcrlist $level->{map},
+                                         $level->{lynxcreatures};
+    $level->{traps} = txtfile::buildtraplist $level->{map};
+    $level->{cloners} = txtfile::buildclonerlist $level->{map};
+
+    $level->{leveltime} = unpack "v", substr $data, 0, 2, "";
+
+    $data = join "", map { $textkey[ord] } split //, $data;
+    $data =~ s/\A([^\n]+)\n//;
+    $level->{title} = $1;
+    $data =~ s/\n+\Z//;
+    if (length $data) {
+       $data =~ tr/\n/ /s;
+       $level->{hint} = $data;
+    }
+
+    return $level;
+}
+
+sub readmsdos($)
+{
+    my $dirname = shift;
+    my $data = { ruleset => "lynx" };
+
+    foreach my $n (0 .. $#levelfilenames) {
+       $filename = "$dirname/$levelfilenames[$n]";
+       $filelevel = $n + 1;
+       next unless -e $filename;
+       open FILE, "< $filename" or return ::err $!;
+       binmode FILE;
+       my $level = parselevel join "", <FILE>;
+       close FILE;
+       return unless defined $level;
+       $level->{number} = $n + 1;
+       $level->{passwd} = $origpasswords[$n];
+       push @{$data->{levels}}, $level;
+    }
+
+    return $data;
+}
+
+sub readrom($)
+{
+    my $input = shift;
+    my $data = { ruleset => "lynx" };
+
+    my $buf = ::fileread $input, "a20" or return;
+    return ::err "invalid ROM file"
+       unless $buf eq "LYNX\000\002\000\000\001\000chipchal.l";
+
+    my @levels;
+    sysseek $input, 0x02F0, 0 or return ::err $!;
+    for (my $n = 0 ; $n < 150 ; ++$n) {
+       my @rec = ::fileread $input, "C4vv" or return;
+       $levels[$n][0] = (($rec[0] << 9) | $rec[1]
+                                        | (($rec[2] & 0x01) << 8)) + 0x40;
+       $levels[$n][1] = $rec[5];
+    }
+
+    for (my $n = 0 ; $n < 150 ; ++$n) {
+       $filelevel = $n + 1;
+       $buf = sysseek $input, $levels[$n][0], 0 or return ::err $!;
+       $buf = ::fileread $input, "a$levels[$n][1]" or return;
+       next if $levels[$n][1] == 5 && $buf eq "\000\000\001\000\377";
+       my $level = parselevel $buf;
+       return unless defined $level;
+       $level->{number} = $n + 1;
+       $level->{passwd} = $origpasswords[$n];
+       push @{$data->{levels}}, $level;
+    }
+
+    return $data;
+}
+
+#
+#
+#
+
+sub translatetext($;$)
+{
+    my $in = shift;
+    my $multiline = shift || 0;
+
+    my $out = "";
+    my ($x, $y) = (0, 0);
+    my $brk = [ undef ];
+
+    foreach my $char (split //, $in) {
+       if ($char eq "\n") {
+           ++$y;
+           $x = -1;
+           $brk = [ undef ];
+       } elsif ($x >= 19) {
+           if (!$multiline || $y >= 6) {
+               ::err "truncated text";
+               substr($out, 17 - $x) = "" if $y >= 6 && $x >= 19;
+               last;
+           }
+           if ($brk->[0]) {
+               $x -= $brk->[0];
+               substr($out, $brk->[1], 1) = "\0";
+           } else {
+               $x = -1;
+               $out .= "\0";
+           }
+           ++$y;
+           $brk = [ undef ];
+       } elsif ($char eq " ") {
+           $brk = [ $x, length $out ];
+       }
+       $out .= $textkey{uc $char};
+       ++$x;
+    }
+
+    return $out;
+}
+
+sub mklevelmap($)
+{
+    my $level = shift;
+    my $out = "";
+    my $chips = 0;
+
+    for (my $y = 0 ; $y < 32 ; ++$y) {
+       for (my $x = 0 ; $x < 32 ; ++$x) {
+           my $obj;
+           my $top = $level->{map}[$y][$x][0];
+           my $bot = $level->{map}[$y][$x][1];
+           if (::iscreature $top || ::ischip $top || ::isblock $top) {
+               $obj = $bot;
+               if (::iscreature $obj || ::isblock $obj || ::ischip $obj) {
+                   ::err "ignoring buried creature";
+                   $obj = 0;
+               }
+           } else {
+               ::err "ignoring buried object" if $bot;
+               $obj = $top;
+           }
+           if ($obj == $tilenames{"computer chip"}) {
+               $obj = $chips < $level->{chips} ? 0x23 : 0x31;
+               ++$chips;
+           } else {
+               $obj = $objectkey{$obj};
+               unless (defined $obj) {
+                   ::err "ignoring non-Lynx object";
+                   $obj = 0;
+               }
+           }
+           $out .= chr $obj;
+       }
+    }
+
+    ::err "chips needed was reduced" if $chips < $level->{chips};
+
+    return $out;
+}
+
+sub mklevelcrlist($)
+{
+    my $level = shift;
+    my @listed;
+    my @crlist;
+
+    return ::err "invalid creature list: $level->{lynxcreatures}"
+       unless ref $level->{lynxcreatures};
+
+    my ($types, $xs, $ys) = ("", "", "");
+    foreach my $creature (@{$level->{lynxcreatures}}) {
+       my $y = $creature->[0];
+       my $x = $creature->[1];
+       my $type = $level->{map}[$y][$x][0];
+       $type = $creaturekey{$type};
+       unless (defined $type) {
+           ::err "ignoring non-Lynx creature in creature list";
+           next;
+       }
+       $type |= 0x80 if $creature->[2] < 0;
+       $y <<= 3;
+       $x <<= 3;
+       ++$y, ++$x if $creature->[2] == 0;
+       $types .= chr $type;
+       $xs .= chr $x;
+       $ys .= chr $y;
+    }
+
+    return pack "a128 a128 a128", $types, $xs, $ys;
+}
+
+sub mkleveldata($)
+{
+    my $level = shift;
+    my $out = "";
+    my $part;
+
+    $part = mklevelmap $level;
+    return unless defined $part;
+    $out .= $part;
+
+    $part = mklevelcrlist $level;
+    return unless defined $part;
+    $out .= $part;
+
+    $out .= pack "v", $level->{leveltime};
+
+    $part = translatetext $level->{title};
+    return unless defined $part;
+    $out .= "$part\0";
+
+    if (exists $level->{hint}) {
+       $part = translatetext $level->{hint}, 1;
+       return unless defined $part;
+       $out .= "$part\0";
+    }
+
+    $out .= "\0";
+
+    return compress $out;
+}
+
+sub writemsdos($$)
+{
+    my $dirname = shift;
+    my $data = shift;
+
+    ::err "warning: storing an MS-ruleset level set in a Lynx-only file format"
+       unless $data->{ruleset} eq "lynx";
+
+    foreach my $level (@{$data->{levels}}) {
+       $filename = $dirname;
+       $filelevel = undef;
+       if ($level->{number} >= @levelfilenames) {
+           ::err "ignoring level $level->{number}, number too high";
+           next;
+       } elsif ($level->{number} < 1) {
+           ::err "ignoring level $level->{number}, number invalid";
+           next;
+       }
+       $filename = "$dirname/$levelfilenames[$level->{number} - 1]";
+       $filelevel = $level->{number};
+       ::err "ignoring password"
+           if $level->{passwd} ne $origpasswords[$level->{number} - 1];
+       open FILE, "> $filename" or return ::err $!;
+       binmode FILE;
+       my $out = mkleveldata $level or return;
+       print FILE $out or return ::err $!;
+       close FILE or return ::err $!;
+    }
+
+    return 1;
+}
+
+sub writerom($$)
+{
+    my $file = shift;
+    my $data = shift;
+
+    ::err "warning: storing an MS-ruleset level set in a Lynx-only file format"
+       unless $data->{ruleset} eq "lynx";
+
+    my $buf = ::fileread $file, "a22" or return;
+    return ::err "invalid ROM file"
+       unless $buf eq "LYNX\000\002\000\000\001\000chipchal.lyx";
+
+    sysseek $file, 0x02F0, 0 or return ::err $!;
+    my @ptr = ::fileread $file, "C4" or return ::err $!;
+    my $startpos = (($ptr[0] << 9) | $ptr[1] | (($ptr[2] & 0x01) << 8));
+
+    my @levellist;
+    my $dropped;
+    foreach my $level (@{$data->{levels}}) {
+       my $n = $level->{number};
+       $filelevel = $n;
+       if ($n < 1) {
+           ::err "ignoring invalid-numbered level $n";
+       } elsif ($n > 149) {
+           ++$dropped;
+       } elsif (defined $levellist[$n]) {
+           ::err "ignoring duplicate level $n";
+       } else {
+           ::err "ignoring password"
+               if $level->{passwd} ne $origpasswords[$n - 1];
+           $levellist[$n] = mkleveldata $level;
+           return unless defined $levellist[$n];
+       }
+    }
+    ::err "ignored $dropped level(s) above level 149" if $dropped;
+
+    my $levels = "";
+    my $index = "";
+    my $ptr = $startpos;
+    for (my $n = 1 ; $n <= 149 ; ++$n) {
+       my $size;
+       if ($levellist[$n]) {
+           $levels .= $levellist[$n];
+           $size = length $levellist[$n];
+       } else {
+           $levels .= "\000\000\001\000\377";
+           $size = 5;
+       }
+       $index .= pack "C4vv", ($ptr >> 9), ($ptr & 0xFF),
+                              (($ptr >> 8) & 0x01), 0, 0, $size;
+       $ptr += $size;
+    }
+    $levels .= "\000\000\001\000\377";
+    $index .= pack "C4vv", ($ptr >> 9), ($ptr & 0xFF),
+                          (($ptr >> 8) & 0x01), 0, 0, 5;
+
+    return ::err "too much data; cannot fit inside the ROM file"
+       if length $levels > 0x11D00;
+
+    sysseek $file, 0x02F0, 0 or return ::err $!;
+    syswrite $file, $index or return ::err $!;
+    sysseek $file, $startpos + 0x40, 0 or return ::err $!;
+    syswrite $file, $levels or return ::err $!;
+
+    return 1;
+}
+
+#
+#
+#
+
+package cudfile;
+
+# The terse names used by the universal-dump file format.
+#
+my @shortnames = ("empty",             # 0x00
+                 "wall",               # 0x01
+                 "ic_chip",            # 0x02
+                 "water",              # 0x03
+                 "fire",               # 0x04
+                 "inv_wall_per",       # 0x05
+                 "wall_N",             # 0x06
+                 "wall_W",             # 0x07
+                 "wall_S",             # 0x08
+                 "wall_E",             # 0x09
+                 "block",              # 0x0A
+                 "dirt",               # 0x0B
+                 "ice",                # 0x0C
+                 "force_S",            # 0x0D
+                 "block_N",            # 0x0E
+                 "block_W",            # 0x0F
+                 "block_S",            # 0x10
+                 "block_E",            # 0x11
+                 "force_N",            # 0x12
+                 "force_E",            # 0x13
+                 "force_W",            # 0x14
+                 "exit",               # 0x15
+                 "blue_door",          # 0x16
+                 "red_door",           # 0x17
+                 "green_door",         # 0x18
+                 "yellow_door",        # 0x19
+                 "ice_turn_SE",        # 0x1A
+                 "ice_turn_SW",        # 0x1B
+                 "ice_turn_NW",        # 0x1C
+                 "ice_turn_NE",        # 0x1D
+                 "blue_floor",         # 0x1E
+                 "blue_wall",          # 0x1F
+                 "overlay",            # 0x20
+                 "thief",              # 0x21
+                 "socket",             # 0x22
+                 "green_button",       # 0x23
+                 "red_button",         # 0x24
+                 "toggle_close",       # 0x25
+                 "toggle_open",        # 0x26
+                 "brown_button",       # 0x27
+                 "blue_button",        # 0x28
+                 "teleport",           # 0x29
+                 "bomb",               # 0x2A
+                 "trap",               # 0x2B
+                 "inv_wall_tmp",       # 0x2C
+                 "gravel",             # 0x2D
+                 "popup_wall",         # 0x2E
+                 "hint_button",        # 0x2F
+                 "wall_SE",            # 0x30
+                 "cloner",             # 0x31
+                 "force_any",          # 0x32
+                 "chip_drowned",       # 0x33
+                 "chip_burned",        # 0x34
+                 "chip_bombed",        # 0x35
+                 "unused_1",           # 0x36
+                 "unused_2",           # 0x37
+                 "unused_3",           # 0x38
+                 "chip_exiting",       # 0x39
+                 "exit_1",             # 0x3A
+                 "exit_2",             # 0x3B
+                 "chip_swim_N",        # 0x3C
+                 "chip_swim_W",        # 0x3D
+                 "chip_swim_S",        # 0x3E
+                 "chip_swim_E",        # 0x3F
+                 "bug_N",              # 0x40
+                 "bug_W",              # 0x41
+                 "bug_S",              # 0x42
+                 "bug_E",              # 0x43
+                 "fireball_N",         # 0x44
+                 "fireball_W",         # 0x45
+                 "fireball_S",         # 0x46
+                 "fireball_E",         # 0x47
+                 "ball_N",             # 0x48
+                 "ball_W",             # 0x49
+                 "ball_S",             # 0x4A
+                 "ball_E",             # 0x4B
+                 "tank_N",             # 0x4C
+                 "tank_W",             # 0x4D
+                 "tank_S",             # 0x4E
+                 "tank_E",             # 0x4F
+                 "glider_N",           # 0x50
+                 "glider_W",           # 0x51
+                 "glider_S",           # 0x52
+                 "glider_E",           # 0x53
+                 "teeth_N",            # 0x54
+                 "teeth_W",            # 0x55
+                 "teeth_S",            # 0x56
+                 "teeth_E",            # 0x57
+                 "walker_N",           # 0x58
+                 "walker_W",           # 0x59
+                 "walker_S",           # 0x5A
+                 "walker_E",           # 0x5B
+                 "blob_N",             # 0x5C
+                 "blob_W",             # 0x5D
+                 "blob_S",             # 0x5E
+                 "blob_E",             # 0x5F
+                 "centipede_N",        # 0x60
+                 "centipede_W",        # 0x61
+                 "centipede_S",        # 0x62
+                 "centipede_E",        # 0x63
+                 "blue_key",           # 0x64
+                 "red_key",            # 0x65
+                 "green_key",          # 0x66
+                 "yellow_key",         # 0x67
+                 "water_boots",        # 0x68
+                 "fire_boots",         # 0x69
+                 "ice_boots",          # 0x6A
+                 "force_boots",        # 0x6B
+                 "chip_N",             # 0x6C
+                 "chip_W",             # 0x6D
+                 "chip_S",             # 0x6E
+                 "chip_E"              # 0x6F
+);
+for (0x70 .. 0xFF) { $shortnames[$_] = sprintf "tile_%02X", $_ }
+
+sub write($$)
+{
+    my $output = shift;
+    my $data = shift;
+    my $list;
+
+    print $output "BEGIN CUD 1 ruleset $data->{ruleset}\n\n" or return;
+
+    foreach my $level (@{$data->{levels}}) {
+       printf $output "%03d chips %d\n", $level->{number}, $level->{chips}
+           or return;
+       printf $output "%03d time %d\n", $level->{number}, $level->{leveltime}
+           or return;
+       printf $output "%03d passwd %s\n", $level->{number}, $level->{passwd}
+           or return;
+       printf $output "%03d title:%s\n", $level->{number},
+                                         ::escape $level->{title}
+           or return;
+       printf $output "%03d hint", $level->{number} or return;
+       print $output ":", ::escape $level->{hint} or return
+           if exists $level->{hint};
+       print $output "\n" or return;
+
+       my @notes;
+       $list = $level->{traps};
+       foreach my $i (0 .. $#$list) {
+           $notes[$list->[$i]{from}[0]][$list->[$i]{from}[1]]{tfr} = $i + 1;
+           $notes[$list->[$i]{to}[0]][$list->[$i]{to}[1]]{tto} = $i + 1;
+       }
+       $list = $level->{cloners};
+       foreach my $i (0 .. $#$list) {
+           $notes[$list->[$i]{from}[0]][$list->[$i]{from}[1]]{cfr} = $i + 1;
+           $notes[$list->[$i]{to}[0]][$list->[$i]{to}[1]]{cto} = $i + 1;
+       }
+       $list = $level->{creatures};
+       foreach my $i (0 .. $#$list) {
+           $notes[$list->[$i][0]][$list->[$i][1]]{crl} = $i + 1;
+       }
+
+       foreach my $y (0 .. 31) {
+           foreach my $x (0 .. 31) {
+               next if $level->{map}[$y][$x][0] == 0
+                    && $level->{map}[$y][$x][1] == 0
+                    && !defined $notes[$y][$x];
+               printf $output "%03d (%02d %02d) ", $level->{number}, $x, $y
+                   or return;
+               printf $output "%-12.12s %-12.12s ",
+                              $shortnames[$level->{map}[$y][$x][0]],
+                              $shortnames[$level->{map}[$y][$x][1]]
+                   or return;
+               printf $output " Tfr=%-2.2s", $notes[$y][$x]{tfr} or return
+                   if exists $notes[$y][$x]{tfr};
+               printf $output " Tto=%-2.2s", $notes[$y][$x]{tto} or return
+                   if exists $notes[$y][$x]{tto};
+               printf $output " Cfr=%-2.2s", $notes[$y][$x]{cfr} or return
+                   if exists $notes[$y][$x]{cfr};
+               printf $output " Cto=%-2.2s", $notes[$y][$x]{cto} or return
+                   if exists $notes[$y][$x]{cto};
+               printf $output " CL=%-3.3s", $notes[$y][$x]{crl} or return
+                   if exists $notes[$y][$x]{crl};
+               printf $output "\n" or return;
+           }
+       }
+       printf $output "\n" or return;
+    }
+
+    print $output "END\n" or return;
+
+    return 1;
+}
+
+#
+#
+#
+
+package main;
+
+use constant yowzitch => <<EOT;
+Usage: c4 [-INTYPE] INFILE [-OUTTYPE] OUTFILE
+
+The type switches can be omitted if the file's type can be inferred
+directly. Available types:
+
+    -D     Microsoft data file (*.dat)
+    -T     textual source file (*.txt)
+    -R     Lynx ROM file (*.lnx, *.lyx)
+    -P     MS-DOS fileset (directory of *.pak files)
+    -U     Chip's universal dump file (*.cud) [write-only]
+EOT
+use constant vourzhon => "1.0\n";
+
+my ($infile, $outfile);
+my ($intype, $outtype);
+
+sub deducetype($)
+{
+    local $_ = shift;
+    if (-d $_) {
+       return "P";
+    } elsif (/\.dat$/) {
+       return "D";
+    } elsif (/\.txt$/ || /^-$/) {
+       return "T";
+    } elsif (/\.lnx$/ || /\.lyx$/) {
+       return "R";
+    } elsif (/\.cud$/) {
+       return "U";
+    }
+    return;
+}
+
+sub findfiletype($)
+{
+    open FILE, shift or return;
+    local $_;
+    sysread FILE, $_, 16 or return;
+    close FILE;
+    return "D" if /\A\xAC\xAA\x02/;
+    return "R" if /\ALYNX\0/;
+    return "T" if /\A\s*(rul|til|max|%%%)/;
+    return;
+}
+
+die yowzitch unless @ARGV;
+print yowzitch and exit if $ARGV[0] =~ /^--?(h(elp)?|\?)$/;
+print vourzhon and exit if $ARGV[0] =~ /^--?[Vv](ersion)?$/;
+
+$infile = shift;
+if ($infile =~ /^-([A-Za-z])$/) {
+    $intype = uc $1;
+    $infile = shift;
+}
+die yowzitch unless @ARGV;
+$outfile = shift;
+if ($outfile =~ /^-([A-Za-z])$/) {
+    $outtype = uc $1;
+    $outfile = shift;
+}
+die yowzitch unless defined $infile && defined $outfile && @ARGV == 0;
+
+$intype ||= deducetype $infile;
+$outtype ||= deducetype $outfile;
+die "$outfile: file type unspecified\n" unless $outtype;
+$intype = findfiletype $infile if !defined $intype && -f $infile;
+die "$infile: file type unspecified\n" unless $intype;
+
+my $data;
+
+$filename = $infile;
+if ($intype eq "D") {
+    open FILE, "< $infile" or die "$infile: $!\n";
+    binmode FILE;
+    $data = datfile::read \*FILE or exit 1;
+    close FILE;
+} elsif ($intype eq "T") {
+    open FILE, "< $infile" or die "$infile: $!\n";
+    $data = txtfile::read \*FILE or exit 1;
+    close FILE;
+} elsif ($intype eq "P") {
+    $data = lynxfmt::readmsdos $infile or exit 1;
+} elsif ($intype eq "R") {
+    open FILE, "< $infile" or die "$infile: $!\n";
+    binmode FILE;
+    $data = lynxfmt::readrom \*FILE or exit 1;
+    close FILE;
+} elsif ($intype eq "U") {
+    die "File type -U is a write-only file format.\n";
+} else {
+    die "Unknown file type option -$intype.\n";
+}
+
+undef $filename;
+undef $filelevel;
+undef $filepos;
+
+$filename = $outfile;
+
+if ($outtype eq "D") {
+    open FILE, "> $outfile" or die "$outfile: $!\n";
+    binmode FILE;
+    datfile::write \*FILE, $data or die "$outfile: $!\n";
+    close FILE or die "$outfile: $!\n";
+} elsif ($outtype eq "T") {
+    open FILE, "> $outfile" or die "$outfile: $!\n";
+    txtfile::write \*FILE, $data or die "$outfile: $!\n";
+    close FILE or die "$outfile: $!\n";
+} elsif ($outtype eq "P") {
+    lynxfmt::writemsdos $outfile, $data or exit 1;
+} elsif ($outtype eq "R") {
+    open FILE, "+< $outfile" or die "$outfile: $!\n";
+    binmode FILE;
+    lynxfmt::writerom \*FILE, $data or die "$outfile: $!\n";
+    close FILE or die "$outfile: $!\n";
+} elsif ($outtype eq "U") {
+    open FILE, "> $outfile" or die "$outfile: $!\n";
+    cudfile::write \*FILE, $data or die "$outfile: $!\n";
+    close FILE or die "$outfile: $!\n";
+} else {
+    die "Unknown file type option -$outtype.\n";
+}
+
+#
+# The documentation
+#
+
+=head1 NAME
+
+c4 - Chip's Challenge combined converter
+
+=head1 SYNOPSIS
+
+    c4 [-INTYPE] INFILENAME [-OUTTYPE] OUTFILENAME
+
+c4 allows one to translate between the several different types of
+files used to represent level sets for the game Chip's Challenge.
+
+c4 expects there to be two files named on the command-line. c4 reads
+the levels stored in the first file, and then writes the levels out to
+the second file. The format to use with each file usually can be
+inferred by c4 by examining the filenames. If not, then it may be
+necessary to use switches before one or both filenames to indicate
+their type.
+
+There are four different types of files that c4 understands.
+
+    -D     MS data file (*.dat).
+
+This is the file type used by Chip's Challenge for Microsoft Windows
+3.x. It is the file type used by most other programs, such as ChipEdit
+and Tile World.
+
+    -R     Lynx ROM file (*.lnx, *.lyx)
+
+This "file type" is actually just a ROM image of the original Chip's
+Challenge for the Atari Lynx handheld. It is used by Lynx emulators
+such as Handy.
+
+    -P     MS-DOS fileset (directory of *.pak files)
+
+This is the format used by the MS-DOS port of Chip's Challenge. In
+this case, the filename given on the command line actually names a
+directory, containing *.pak files.
+
+    -T     textual source file (*.txt)
+
+This file type is native to c4. It is a plain text file, which allows
+levels to be defined pictorially using a simple text editor. A
+complete description of the syntax of these files is provided below.
+
+=head1 EXAMPLES
+
+    c4 mylevels.txt mylevels.dat
+
+Create a .dat file from a textual source file.
+
+    c4 -P levels -D doslevels.dat
+
+"levels" is a directory of MS-DOS *.pak files. c4 translates the
+directory contents into a single .dat file. Note that the switches in
+this example are optional, as c4 would be able to infer the desired
+formats.
+
+    c4 mylevels.dat chipsch.lnx
+
+Embed the levels from the .dat file into a Lynx ROM file. Note that c4
+does NOT create chipsch.lnx. You must provide the ROM image file,
+which c4 then alters to contain your levels. (Obviously, you should
+not use this command on your master copy of the ROM file.)
+
+    c4 chipsch.lnx -T out
+
+Output the levels in the .dat file as a text file. Here the -T switch
+is needed to indicate that a text file is the desired output format.
+
+When producing a text file, c4 will attempt to produce legible source,
+but the results will often not be as good as what a human being would
+produce. (In particular, c4 cannot draw overlays.)
+
+=head1 NOTES
+
+Be aware that there can be various problems when translating a set of
+levels using the MS ruleset to one of the Lynx-only file formats.
+There are numerous objects and configurations in the MS ruleset which
+cannot be represented in the Lynx ruleset. Usually c4 will display a
+warning when some aspect of the data could not be transferred intact
+because of this.
+
+The remainder of this documentation describes the syntax of the
+textual source file format.
+
+=head1 LAYOUT OF THE INPUT FILE
+
+The source file is broken up into subsections. Each subsection defines
+a separate level in the set.
+
+The subsections are separated from each other by a line containing
+three percent signs:
+
+    %%%
+
+A line of three percent signs also comes before the first level and
+after the last level, at the end of the source file.
+
+Any other line that begins with a percent sign is treated as a
+comment, and its contents are ignored.
+
+Beyond these things, the source file consists of statements.
+Statements generally appear as a single line of text. Some statements,
+however, require multiple lines. These multi-line statements are
+terminated with the word B<end> appearing alone on a line.
+
+=head1 INPUT FILE HEADER STATEMENTS
+
+There are a couple of statements that can appear at the very top of
+the source file, before the first level subsection.
+
+    ruleset [ lynx | ms ]
+
+The B<ruleset> statement is the most important of these. It defines
+the ruleset for the level set. If the B<ruleset> statment is absent,
+it defaults to B<lynx>.
+
+    maxlevel NNN
+
+The B<maxlevel> statement specifies the number of the last level in
+the .dat file. By default, this value is provided automatically and
+does not need to be specified.
+
+In addition to the above, a set of tile definitions can appear in the
+header area. See below for a full description of the B<tiles>
+multi-line statement. Any tile definitions provided here remain in
+force throughout the file.
+
+=head1 INPUT FILE LEVEL STATEMENTS
+
+Within each level's subsection, the following two statments will
+usually appear at the top.
+
+    title STRING
+    password PASS
+
+The B<title> statement supplies the level's title, or name. The title
+string can be surrounded by double quotes, or unadorned. The
+B<password> statement supplies the level's password. This password
+must consist of exactly four uppercase alphabetic characters.
+
+If the level's number is 150 or less, the B<password> statement may be
+omitted. In that case the level's password will default to match that
+level in the original Lynx set. (N.B.: The Lynx ROM file format does
+not provide a mechanism for setting passwords, so in that case the
+default password will be used regardless.)
+
+The following statements may also appear in a level subsection.
+
+    chips NNN
+
+The B<chips> statement defines how many chips are required on this
+level to open the chip socket. The default value is zero.
+
+    time NNN
+
+The B<time> statement defines how many seconds are on the level's
+clock. The default value is zero (i.e., no time limit).
+
+    hint STRING
+
+The B<hint> statement defines the level's hint text. As with the
+B<title> statement, the string can either be unadorned or delimited
+with double quotes. If a section contains multiple B<hint> statements,
+the texts are appended together, e.g.:
+
+    hint This is a relatively long hint, and so it
+    hint is helpful to be able to break it up across
+    hint several lines.
+
+Note that the same can be done with B<title> statements.
+
+    tiles
+    DEF1
+    DEF2
+    ...
+    end
+
+The B<tiles> multi-line statement introduces one or more tile
+definitions. The definitions appear one per line, until a line
+containing B<end> is found. Note that the tile definitions given here
+only apply to the current level. A complete description of tile
+definitions is given below.
+
+    map [ X Y ]    map [ X Y ]
+    LINE1         LINE1
+    LINE2         LINE2
+    ...                   ...
+    and                   end
+    OVER1
+    OVER2
+    ...
+    end
+
+The B<map> statement defines the actual contents of (part of) the
+level's map. The line containing the B<map> statement can optionally
+include a pair of coordinates; these coordinates indicate where the
+the section will be located on the level's map. If coordinates are
+omitted, the defined section will be located at (0 0) -- i.e., the
+upper-left corner of the level. The lines inside the B<map> statement
+pictorially define the contents of the map section, until a line
+containing B<and> or B<end> is encountered. When the map is terminated
+by B<and>, then the lines defining the map section are immediately
+followed by lines defining an overlay. The overlay uses the same
+origin as the map section (though it is permissible for the overlay to
+be smaller than the map section it is paired with). A complete
+description of the map and overlay sections is given below.
+
+    border TL
+
+The B<border> statement specifies a tile. The edges of the map are
+then changed to contain this tile. Typically this is used to enclose
+the level in walls.
+
+The following statements are also available, though they are usually
+not needed. They provide means for explicitly defining level data, for
+the occasional situation where the usual methods are more cumbersome.
+
+    creatures X1 Y1 ; X2 Y2 ...
+
+The B<creatures> statements permits explicit naming of the coordinates
+in the creature list. Pairs of coordinates are separated from each
+other by semicolons; any number of coordinate pairs can be specified.
+There can be multiple B<creatures> statements in a level's subsection.
+
+    traps P1 Q1 -> R1 S1 ; P2 Q2 -> R2 S2 ...
+
+The B<traps> statement permits explicit naming of the coordinates for
+elements in the bear trap list. Coordinates are given in one or more
+groups of four, separated by semicolons. Each group consists of the x-
+and y-coordinates of the brown button, an arrow (->), and then the x-
+and y-coordinates of the bear trap. Any number of B<traps> statements
+can appear in a level's subsection.
+
+    cloners P1 Q1 -> R1 S1 ; P2 Q2 -> R2 S2 ...
+
+The B<cloners> statement permits explicit naming of elements in the
+clone machine list. It uses the same syntax as the B<traps> statment,
+with the red button's coordinates preceding the coordinates of the
+clone machine.
+
+    level NNN
+
+The B<level> statement defines the level's number. By default it is
+one more than the number of the prior level.
+
+    field NN B01 B02 ...
+
+The B<field> statement allows fields to be directly specified and
+embedded in the .dat file. The first argument specifies the field
+number; the remaining arguments provide the byte values for the actual
+field data. These statements are only meaningful in conjunction with
+producing a .dat file.
+
+=head1 DEFINING TILES
+
+A tile definition consists of two parts. The first part is either one
+or two characters. The characters can be letters, numbers, punctuation
+-- anything except spaces. The second part is the name of a tile or a
+pair of tiles. The characters then become that tile's representation.
+
+Here is an example of some tile definitions:
+
+    tiles
+    #       wall
+    *       teleport
+    rb      red button
+    @       chip south
+    end
+
+(Note that a single tab character comes after the characters and
+before the tile names.) Once these definitions have been provided, the
+newly-defined characters can then be used in a map.
+
+The above definitions all use singular tiles. To define a pair of
+tiles, combine the two names with a plus sign, like so:
+
+    tiles
+    X       block + bomb
+    G       glider north + clone machine
+    end
+
+Notice that the top tile is named first, then the bottom tile.
+
+The B<tiles> statement is the only statement that can appear in the
+header, as well as in a level's subsection. Tile definitions in the
+header are global, and can be used in every subsection. Tile
+definitions inside a subsection are local, and apply only to that
+level.
+
+A number of tile definitions are pre-set ahead of time, supplying
+standard representations for some of the most common tiles. (If these
+representations are not desired, the characters can always be
+redefined.) Here are some of the built-in definitions:
+
+    #       wall                  $       computer chip
+    ,       water                 H       socket
+    =       ice                   E       exit
+    &       fire                  []      block
+    6       bomb                  ?       hint button
+
+See below for the complete list of tile names and built-in
+definitions.
+
+A few groups tiles allow one to specify multiple definitions in a
+single line. For example:
+
+    tiles
+    G       glider
+    end
+
+This one definition is equivalent to the following:
+
+    tiles
+    Gn      glider north
+    Gs      glider south
+    Ge      glider east
+    Gw      glider west
+    end
+
+(Note that "G" by itself is still undefined.) All creatures, including
+Chip, can be defined using this abbreviated form.
+
+Doors and keys are the other groups that have this feature; the
+following definition:
+
+    tiles
+    D       door
+    end
+
+is equivalent to:
+
+    tiles
+    Dr      red door
+    Db      blue door
+    Dy      yellow door
+    Dg      green door
+    end
+
+=head1 MAP SECTIONS
+
+Once all the needed tiles have defined representations, using the map
+statement is a simple matter. Here is an example:
+
+    map
+    # # # # # #
+    # &     & # # #
+        []    H E #
+    # &     $ # # #
+    # # # # # #
+    end
+
+This is a map of a small room. A block stands in the way of the
+entrance. Three of the four corners contain fire; the fourth contains
+a chip. On the east wall is an exit guarded by a chip socket.
+
+Note that each cell in the map is two characters wide. (Thus, for
+example, the octothorpes describe a solid wall around the room.)
+
+Here is a larger example, which presents the map from LESSON 2:
+
+    tiles
+    B       bug north
+    C       chip south
+    end
+
+    map 7 7
+        # # # # # # #
+        #     $     #
+        #           #
+        #     #     # # # # # # # # # #
+    # # #     # B     , ,           $ #
+    # E H     # # B   , ,   [][]C ?   #
+    # # #     # B     , ,           $ #
+        #     #     # # # # # # # # # #
+        #           #
+        #     $     #
+        # # # # # # #
+    end
+
+There are a couple of different ways to fill a cell with two tiles.
+The first way is to simply use tile definitions which contains two
+tiles:
+
+    tiles
+    X       block + bomb
+    G       glider east + clone machine
+    end
+
+    map 12 14
+            # #
+          6 E #
+            # # X
+    G
+    end
+
+The second way is to squeeze two representations into a single cell.
+Obviously, this can only be done with both representations are a
+single character.
+
+    tiles
+    [       block
+    G       glider east
+    +       clone machine
+    end
+
+    map 12 14
+            # #
+          6 E #
+            # # [6
+    G+
+    end
+
+In both cases, the top tile always comes before the bottom tile. Note
+that you can "bury" a tile by placing it to the right of a space:
+
+    map
+    # # # # # #
+       6 6 6E #
+    # # # # # #
+    end
+
+Any number of map statements can appear in a level's subsection. The
+map statements will be combined together to make the complete map.
+
+=head1 OVERLAY SECTIONS
+
+Every map statement can optionally include an overlay section. This
+overlay permits button connections and monster ordering to be defined.
+
+The overlay is applied to the same position as the map section it
+accompanies. The overlay can duplicate parts of the map section it
+covers, and any such duplication will be ignored. The only characters
+in the overlay that are significant are the ones that differ from the
+map section it covers. These characters are treated as labels. Labels
+are always a single character; two non-space characters in a cell
+always indicates two separate labels. Any non-space characters can be
+used as labels, as long as they don't match up with the map.
+
+An overlay section defines a button connection by using the same label
+in two (or more) cells. One of the labelled cells will contain either
+a bear trap or a clone machine, and the other will contain the
+appropriate button. If there are more than two cells with the same
+label, all but one should contain a button.
+
+Characters that only appear once in an overlay, on the other hand,
+indicate creatures. The characters then indicate the ordering of the
+creatures in the creature list with respect to each other. The
+ordering of characters is the usual ASCII sequence (e.g., numbers
+first, then capital letters, then lowercase letters).
+
+For example, here is a map with an overlay that demonstrates all three
+of these uses:
+
+    tiles
+    G       glider east
+    +       clone machine
+    r       red button
+    *       beartrap
+    b       brown button
+    end
+
+    map
+    G                           v #
+    G+      *   r   * G+    b &   # r
+    G+    *     r     #           # r
+    # >   b b G             < #   #
+    and
+    2                           v #
+    A       c   C   d C     d &   # A
+    B     a     C     #           # B
+    # >   a c 1             < #   #
+    end
+
+In this example, capitals are used for the clone machine connections,
+lowercase for the bear trap connections, and numbers are used for the
+creature ordering.
+
+(Note that the gliders atop clone machines are not numbered. While it
+is not an error to include clone machine creatures in the ordering,
+they are ignored under the MS ruleset.)
+
+It is not necessary to reproduce any of the map section's text in the
+overlay section. Blanks can be used instead. The ignoring of matching
+text is simply a feature designed to assist the user in keeping the
+overlay's contents properly aligned.
+
+The B<traps>, B<cloners>, and B<creatures> statements can be used in
+lieu of, or in conjunction with, data from overlay sections. In the
+case of the creature list, items are added to the list in the order
+that they are encountered in the source text.
+
+If a level contains no overlay information and none of the above three
+statements, then this information will be filled in automatically. The
+data will be determined by following the original Lynx-based rules --
+viz., buttons are connected to the next beartrap/clone machine in
+reading order, wrapping around to the top if necessary. (Likewise, the
+creature ordering is just the order of the creatures in their initial
+placement, modified by swapping the first creature with Chip.) Thus,
+if you actually want to force an empty bear trap list, clone machine
+list, or creature list, you must include an empty B<traps>,
+B<cloners>, and/or B<creatures> statement.
+
+=head1 TILE NAMES
+
+Here is the complete list of tiles as they are named in definitions.
+Two or more names appearing on the same line indicates that they are
+two different names for the same tile. Note that the tile names are
+not case-sensitive; capitalization is ignored.
+
+    empty
+    wall
+    water
+    fire
+    dirt
+    ice
+    gravel
+    computer chip          ic chip
+    socket
+    exit
+    ice corner southeast   ice se
+    ice corner southwest   ice sw
+    ice corner northwest   ice nw
+    ice corner northeast   ice ne
+    force floor north      force north
+    force floor south      force south
+    force floor east      force east
+    force floor west      force west
+    force floor random    force random              force any
+    hidden wall permanent  invisible wall permanent
+    hidden wall temporary  invisible wall temporary
+    wall north             partition north
+    wall south            partition south
+    wall east             partition east
+    wall west             partition west
+    wall southeast         partition southeast      wall se
+    closed toggle wall    closed toggle door        toggle closed
+    open toggle wall      open toggle door          toggle open
+    blue door             door blue
+    red door              door red
+    green door            door green
+    yellow door                   door yellow
+    blue key              key blue
+    red key               key red
+    green key             key green
+    yellow key            key yellow
+    blue button                   button blue               tank button
+    red button            button red                clone button
+    green button          button green              toggle button
+    brown button          button brown              trap button
+    blue block floor      blue wall fake
+    blue block wall       blue wall real
+    thief
+    teleport
+    bomb
+    beartrap              trap
+    popup wall
+    hint button
+    clone machine         cloner
+    water boots                   water shield              flippers
+    fire boots            fire shield
+    ice boots             spiked shoes              skates
+    force boots                   magnet                    suction boots
+    block                 moveable block
+    cloning block north           block north
+    cloning block south           block south
+    cloning block east    block east
+    cloning block west    block west
+    chip north
+    chip south
+    chip east
+    chip west
+    ball north
+    tank north
+    bug north             bee north
+    paramecium north      centipede north
+    fireball north        flame north
+    glider north          ghost north
+    blob north
+    walker north          dumbbell north
+    teeth north                   frog north
+
+(The last nine lines, listing the creatures, only show the
+north-facing versions. The remaining 27 names, for the south-, east-,
+and west-facing versions, follow the obvious patttern.)
+
+Note that tile names may be abbreviated to any unique prefix. In
+particular, this permits one to write names like "glider north" as
+simply "glider n".
+
+There are also tile names for the "extra" MS tiles. These tiles are
+listed in parentheses, as an indicator that they were not originally
+intended to be used in maps.
+
+    (combination)
+    (chip drowned)
+    (chip burned)
+    (chip bombed)
+    (unused 1)
+    (unused 2)
+    (unused 3)
+    (exiting)
+    (exit 1)
+    (exit 2)
+    (chip swimming north)  (chip swimming n)
+    (chip swimming west)   (chip swimming w)
+    (chip swimming south)  (chip swimming s)
+    (chip swimming east)   (chip swimming e)
+
+Finally, note that one can also explicitly refer to tiles by their
+hexadecimal byte value under the MS rules by using the "0x" prefix.
+Thus, the names "0x2A" and "bomb" are equivalent.
+
+=head1 PREDEFINED TILE DEFINITIONS
+
+The following is the complete list of built-in tile definitions:
+
+    #       wall                  E       exit
+    $       ic chip               H       socket
+    ,       water                 =       ice
+    &       fire                  6       bomb
+    ;       dirt                  :       gravel
+    ~       wall north            ^       force floor north
+    _       wall south            v       force floor south
+    |       wall west             <       force floor west
+     |      wall east             >       force floor east
+    _|      wall southeast        <>      force floor random
+    ?       hint button           @       chip south
+    []      block                 [       block
+    ^]      cloning block north + clone machine
+    <]      cloning block west + clone machine 
+    v]      cloning block south + clone machine 
+    >]      cloning block east + clone machine
+
+=head1 LICENSE
+
+c4, Copyright (C) 2003-2006 Brian Raiter <breadbox@muppetlabs.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and documentation (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.
+
+=cut
diff --git a/twep.txt b/twep.txt
new file mode 100644 (file)
index 0000000..0823fcc
--- /dev/null
+++ b/twep.txt
@@ -0,0 +1,109 @@
+% SPDX-License-Identifier: GPL-3.0-or-later
+%
+% You can redistribute and/or modify these levels 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.
+%
+% The levels are distributed in the hope that they 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 these levels. If not, see <http://www.gnu.org/licenses/>.
+
+ruleset lynx
+
+%%%
+
+title   Level 1
+passwd  ITBX
+chips   5
+time    500
+hint    Welcome to TWEP: Tile World Expansion Pack
+
+map 3 3
+# # # # # # # # # #
+#   ?             #
+# $               #
+#       @         #
+#                 #
+#               $ #
+#                 #
+# $               #
+#                 #
+#                 #
+#                 #
+#               $ #
+#                 #
+#                 #
+#                 #
+#                 #
+# $               #
+# H #             #
+# E #             #
+# # # # # # # # # #
+end
+
+%%%
+
+title   Level 2
+passwd  KMHB
+chips   34
+
+tiles
+A      ice boots
+B      yellow door
+C      red door
+D      thief
+F      green door
+G      water boots
+I      blue key
+J      red key
+K      yellow key
+L      blue door
+M      green key
+N      ball east
+O      chip north
+P      ball west
+end
+
+map
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+#                             A B                             #
+#   # # # # # # # # # # # # # # # C # # # # # # # # # # # # D #
+#                     $ #     $     # $                       #
+# = = # # # # # # # # # #   # # # # # # # # # # # # # # # , , #
+# = = =               $ #     $     # $                 , , , #
+# = = = # # # # # # # # # # # # #   # # # # # # # # # # , , , #
+# = = = =             $ #     $     # $               , , , , #
+# = = = = # # # # # # # #   # # # # # # # # # # # # # , , , , #
+#                     $ #     $     # $                       #
+#           # # # # # # # # # # #   # # # # # # # #           #
+#                     $ #     $     # $                       #
+#             # # # # # #   # # # # # # # # # # #             #
+#                     $ #     $     # $                       #
+#               # # # # #   # # #   # # # # #                 #
+#               F $ #         # $       # G F                 #
+#     []        # I #       $ #         # J #                 #
+#               # # # # # # # # #   # # # # #                 #
+#     []              $ #         $ # $                       #
+#             # # # # # # # #   # # # # # # # #               #
+#     [][]            $ # $         # $                       #
+#           # # # # # # # # #   # # # # # # # # #             #
+#     [][]            $ #         $ # $                       #
+# , , , , # # # # # # # # # #   # # # # # # # # # # # = = = = #
+# , , , ,             $ # $       K # $               = = = = #
+# , , , # # # # # # # # # # # # # L # # # # # # # # # # = = = #
+# , , ,             H E # M         # $                 = = = #
+# , , # # # # # # # # # #           # # # # # # # # # # # = = #
+# N                                                           #
+#   # # # # # # # # # # # # # # # # # # # # # # # # # # # #   #
+# O                                                         P #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+end
+
+creatures 30 30 ; 1 28
+
+%%%