--- /dev/null
+ 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>.
--- /dev/null
+#!/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