diff --git a/GPL3.txt b/GPL3.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/GPL3.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ 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.
+
+
+ Copyright (C)
+
+ 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 .
+
+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:
+
+ Copyright (C)
+ 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
+.
+
+ 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
+.
diff --git a/INSTALL.txt b/INSTALL.txt
new file mode 100644
index 0000000000..2ec5974e4a
--- /dev/null
+++ b/INSTALL.txt
@@ -0,0 +1,232 @@
+OpenMW - the completely unofficial reimplementation of Morrowind
+================================================================
+
+Written by Nicolay Korslund
+Email: korslund@gmail.com
+WWW: http://openmw.snaptoad.com
+License: See GPL3.txt
+Current version: 0.2 (second release, very pre-alpha)
+Date: 2008 jun. 17
+
+
+
+QUICK NOTE: You must own and install Morrowind before you can use
+OpenMW. Let me repeat that: OpenMW will NOT run if you do not have
+Morrowind installed on your system!
+
+
+
+
+New in version 0.2
+==================
+
+The second release should now work with the GDC compiler (the D
+frontend for GCC) and DSSS. Since GDC is the only option for compiling
+on other Unixes, on 64 bit and on Mac, this will probably make porting
+to these platforms much easier. DSSS (a specialized D build tool) will
+hopefully make porting to Windows a lot easier.
+
+To compile, you no longer use 'make'. Instead you use 'dsss build',
+which will run 'make' for you. (More details below.)
+
+See the changelog at the end.
+
+
+
+
+Installation from source
+========================
+
+These instructions are for the brave souls who seek to compile OpenMW
+on their own. If you are using the binary version, you can skip to the
+next section.
+
+Supported platforms:
+--------------------
+
+The only operating system that has been tested and is known to work is
+32bit Ubuntu Linux 8.04. Windows and other platforms have not been
+tested as of yet.
+
+Dependencies:
+-------------
+
+Dependencies needed to build OpenMW:
+
+OGRE 1.4.5 (3d engine)
+Audiere 1.9.4 (sound engine)
+OIS-1.2.0 (input system)
+gcc and g++ (C++ compiler)
+GNU make (build tool for C++ files)
+DMD 1.030 (D compiler)
+ or GDC 4.1.3 (alternative D compiler)
+Monster 0.8 (scripting language and tools)
+DSSS 0.75 (D build tool)
+curl (for DSSS)
+
+The above versions are the ones I have tested recently, but other
+versions might work. OGRE and Audiere will require their own set of
+dependencies. I recommend using an automated package tool to install
+as many of these as possible. On ubuntu, try typing:
+
+sudo apt-get install libogre-dev libaudiere-dev build-essential g++ curl gdc
+
+This takes care of OGRE, Audiere, the C and D compilers, make and
+curl. The rest have to be installed manually. There is a libois-dev
+package in Ubuntu (OIS version 0.99), but this has NOT been tested.
+
+You can find the other libraries and tools here:
+
+OIS: http://sourceforge.net/projects/wgois/
+DMD: http://digitalmars.com/d/1.0/dmd-linux.html
+DSSS: http://svn.dsource.org/projects/dsss/downloads/
+Monster: http://monster.snaptoad.com/download.html
+
+If you are using a DSSS binary and not compiling from source (I
+recommend the binary), make sure to get one that matches your D
+compiler. Ie. get the GDC version if you installed GDC, and the DMD
+version for DMD.
+
+If you want to install Ogre and Audiere manually as well, try:
+
+OGRE: http://ogre3d.org
+Audiere: http://audiere.sourceforge.net/
+
+Once everything is set up correctly, you might need to alter a couple
+of lines in the Makefile. The Makefile is only used to compile the C++
+parts of OpenMW, that interfaces with Ogre, OIS and Audiere. On the
+line:
+
+OGCC=g++ `pkg-config --cflags OGRE`
+
+Insert the path to your OIS include directory (or leave it blank if
+you installed OIS in /usr). For example, you might add:
+
+-I/home/me/my/path/to/OIS/include
+
+You might also need to add other include paths or options for Ogre if
+the pkg-config command doesn't work for some reason. Mine outputs
+
+$ pkg-config --cflags OGRE
+-DOGRE_GUI_GLX -DOGRE_CONFIG_LITTLE_ENDIAN -I/usr/include/OGRE
+
+After you are reasonably satisfied, you can try running make manually
+to see if the C++ parts compile. When you are ready to compile the D
+parts and link it all together, type:
+
+dsss build
+
+If something goes terribly wrong during the build (it probably will),
+_and_ you figure out how to solve it, I would appreciate if you told
+me about it so I could update these instructions.
+
+
+
+
+Running the binary
+==================
+
+The binary downloads have been compiled on a 32-bit Ubuntu 8.04 box
+with the libraries mentioned below. They might not work on other linux
+systems due to differing library setups. If this is the case, then
+your only option is to compile from source.
+
+Dependencies:
+-------------
+
+The binary depends on the following libraries:
+
+OGRE 1.4
+Audiere 1.9.4
+OIS-1.2.0
+
+and Morrowind of course. If you followed the compilation instructions
+above, you will have these already. If not, you can find them here:
+
+OGRE: http://ogre3d.org
+Audiere: http://audiere.sourceforge.net/
+OIS: http://sourceforge.net/projects/wgois/
+
+Alternatively (on Ubuntu 8.04) you can at least get Ogre and Audiere
+with the command:
+
+sudo apt-get install libogre14 libaudiere-1.9.4
+
+Configuration:
+--------------
+
+Before you can run OpenMW, you have to help it find the Morrowind data
+files. The 'morro' program needs the files Morrowind.esm and
+Morrowind.bsa, and the directories Sound/ and Music/ from your
+Morrowind Data Files directory. By default it expects to find these in
+the data/ directory. (Can be changed in morro.ini.)
+
+I recommend creating a symbolic link to your original Morrowind
+install. For example, if you have Morrowind installed in:
+
+c:\Program Files\Bethesda Softworks\Morrowind\
+
+and your windows c: drive is mounted on /media/hda1, then run the
+following command:
+
+ln -s "/media/hda1/Program Files/Bethesda Softworks/Morrowind/Data Files/" data
+
+Also, if you have OGRE installed in a non-standard directory (ie. NOT
+to /usr/lib/OGRE), you have to change the PluginFolder in plugins.cfg.
+
+Finally, you can change screen resolution and fullscreen mode in
+ogre.cfg. (I don't recommend fullscreen mode yet, since it may mess up
+your screen and input settings if the program crashes.)
+
+Running OpenMW:
+---------------
+
+If Azura is with you and all the stars and planets are aligned in your
+favor, you should now be able to run OpenMW using the program called
+'morro'.
+
+Write morro -h to see a list of options.
+
+Running without parameters should bring you into the cave called Sud,
+or the last cell loaded. You are in free float mode. Move around with
+WASD (or arrow keys), move up and down with left shift and ctrl, exit
+with 'q' or escape.
+
+To load another cell, specify the cell name on the command line. Use
+the 'esmtool' program to get a list of cells (see below.) Note that
+you must use quotation marks "" if the cell name contains spaces or
+other weird characters. Exterior cells are disabled at the moment.
+
+Enjoy! ;-)
+
+
+
+
+Other included tools:
+=====================
+
+esmtool - Used to inspect ES files (ESM, ESP, ESS). Run without
+ arguments to get a list of options.
+
+bsatool - Tool for viewing and extracting files from BSA archives.
+ (Can also be used to test the NIF parser on a BSA.)
+
+niftool - Decodes one or more NIF files and prints the details.
+
+
+
+
+Changelog:
+==========
+
+0.3 (work in progress)
+- updated Makefile and sources for increased portability (thanks to
+ Dmitry Marakasov for FreeBSD tips and testing!)
+
+0.2 (2008 jun. 17) - latest release
+- compiles with gdc
+- switched to DSSS for building D code
+- includes the program esmtool
+
+0.1 (2008 jun. 03)
+- first release
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000..dc81b8a7df
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,33 @@
+# Designed for GNU Make
+
+# Compiler settings
+CXX?= g++
+CXXFLAGS?=
+
+# Replace this path with your OIS include path
+OIS_PATH=../../software/include
+
+# Compiler settings for Ogre + OIS. Change as needed.
+OGCC=$(CXX) $(CXXFLAGS) `pkg-config --cflags OGRE` -I$(OIS_PATH)
+
+# Compiler settings for Audiere
+AGCC=$(CXX) $(CXXFLAGS) `audiere-config --cxxflags`
+
+# Ogre C++ files, on the form ogre/cpp_X.cpp. Only the first file is
+# passed to the compiler, the rest are dependencies.
+ogre_cpp=ogre framelistener interface overlay bsaarchive
+
+# Audiere C++ files, on the form sound/cpp_X.cpp.
+audiere_cpp=audiere
+
+## The rest of this file is automatic ##
+ogre_cpp_files=$(ogre_cpp:%=ogre/cpp_%.cpp)
+audiere_cpp_files=$(audiere_cpp:%=sound/cpp_%.cpp)
+
+all: cpp_ogre.o cpp_audiere.o
+
+cpp_ogre.o: $(ogre_cpp_files)
+ $(OGCC) -c $<
+
+cpp_audiere.o: $(audiere_cpp_files)
+ $(AGCC) -c $<
diff --git a/bored.d b/bored.d
new file mode 100644
index 0000000000..46f1c466d5
--- /dev/null
+++ b/bored.d
@@ -0,0 +1,364 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (bored.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module bored;
+
+import std.stdio;
+import std.cstream;
+import std.stream;
+import std.random;
+import std.file;
+
+double rnd()
+{
+ return cast(double)std.random.rand()/uint.max;
+}
+
+int rand(int a, int b)
+{
+ return cast(int)((1+b-a)*rnd()+a);
+}
+
+void main()
+{
+ int gold = 0;
+ int loot = 0;
+ int level = 1;
+ int life = 20;
+ bool play = true;
+
+ writefln("\nWelcome to Norrowind!");
+
+ while(play)
+ {
+ writefln();
+ if(life < 4) writefln("You are badly hurt.");
+ else if(life < 10) writefln("You are injured.");
+ else if(life < 15) writefln("You are slightly wounded.");
+ if(gold) writefln("You have %d gold.", gold);
+ if(loot) writefln("You have loot.");
+ if(level>1) writefln("You are level ", level);
+ writefln("
+ 1) Kill monster
+ 2) Read a book
+ 3) Read an NPC
+ 4) Sell some loot
+ 5) Walk around and get drunk on skooma
+ 6) Watch TEH k33wL P1XAL SHADID W4T3R!!!!1
+ 7) Exit
+");
+ uint input;
+ dout.writef("Your choice: ");
+ do
+ {
+ input = cast(uint)(din.getc() - '0');
+ }
+ while(input >= 8);
+
+ if(rnd() < 0.01)
+ {
+ writefln("Program has performed an illegal instruction, the programmer will be shot.");
+ break;
+ }
+
+ writefln();
+
+ if(((input == 5) || (input == 3) || (input == 1)) && (loot > 100))
+ {
+ writefln("You are encumbered and cannot move. Try selling some of your junk.");
+ continue;
+ }
+
+ char[] str;
+ int lootinc, goldinc;
+ int oldlife = life;
+
+ switch(input)
+ {
+ case 1: // Hunting
+ if(rnd() < 0.02)
+ {
+ writefln("You killed a Bodak and a Greater Mumm.. oops, wrong game, never mind.");
+ break;
+ }
+ if(rnd() < 0.02)
+ {
+ writefln(
+"You were killed by an white striped aquatic flying dire gigant dragon
+bear in a Balmora mansion. This is largely your own fault for using all
+those plugins.");
+ play=false;
+ break;
+ }
+ switch(rand(0,15))
+ {
+ case 0: str = "Fjol the Outlaw"; goldinc = rand(0,70); lootinc = rand(10,120); break;
+ case 1: str = "a Betty Netch"; lootinc = rand(0,7); break;
+ case 2: str = "a Vampire"; goldinc = rand(0,10); lootinc = rand(20,40); break;
+ case 3: str = "a Dremora"; lootinc = rand(50,200); break;
+ case 4: str = "some NPC"; goldinc = rand(0,80); lootinc = rand(3,35); break;
+ case 5: str = "an Ordinator"; lootinc = rand(30,45); break;
+ case 6: str = "a Skeleton"; lootinc = 1; break;
+ case 7: str = "Fargoth"; goldinc = 10; lootinc = 4; break;
+ case 8: str = "a Cliff Racer"; lootinc = 2; break;
+ case 9: str = "Vivec"; lootinc = rand(0,20); goldinc = rand(0,60); life-=rand(1,2); break;
+ case 10: str = "a soultrapped Vivec"; goldinc = rand(0,60); lootinc = rand(100,300);
+ life-=rand(1,3); break;
+ case 11: str = "an Ascended Sleeper"; lootinc = rand(5,12); goldinc = rand(0,10); break;
+ case 12: str = "the entire town of Gnaar Mok"; goldinc = rand(40,50); lootinc = rand(70,140);
+ life-=rand(0,2); break;
+ case 13: str = "a Bethesda programmer for being so late with Oblivion"; break;
+ case 14: str = "a Werewolf. Which is kinda strange since you don't have Bloodmoon"; lootinc = rand(4,50); break;
+ case 15: str = "an important quest character. Way to go"; goldinc = rand(0,40); lootinc = rand(0,70); break;
+ }
+ if(rnd() < 0.65)
+ life -= rand(1,8);
+ if(life > 0)
+ {
+ writefln("You killed ", str, ".");
+ if(life < oldlife) writefln("You were hurt in the fight.");
+ else writefln("You survived the fight unscathed.");
+ if(goldinc) writefln("You got ", goldinc, " bucks.");
+ if(lootinc) writefln("You found some loot.");
+ gold += goldinc;
+ loot += lootinc;
+ if(rnd() < 0.2)
+ {
+ writefln("You have gained a level!");
+ life += rand(3,10);
+ level++;
+ }
+ }
+ else
+ {
+ writefln("You met ", str, " and were killed.");
+ play = false;
+ }
+ break;
+
+ case 2:// Book
+ switch(rand(0,5))
+ {
+ case 0:
+ writefln("You read The History of The Emipire and fell asleep.");
+ break;
+ case 1:
+ writefln("You read The Pilgrim's Path and became a fanatical religious nut.");
+ break;
+ case 2:
+ writefln("You read the scroll 'Divine Intervention' and suddenly found yourself
+outside, wearing only your night gown and slippers.");
+ break;
+ case 3:
+ writefln("You read Divine Metaphysics. Again");
+ if(rnd()<0.09)
+ {
+ writefln("You discovered where the dwarwes went! And, more importantly, where
+they stashed all their loot.");
+ loot += 1000;
+ }
+ break;
+ case 4:
+ writefln("You learned a new skill.");
+ if(rnd() < 0.4) level++;
+ break;
+ case 5:
+ writefln("You dropped a book on you toe.");
+ life--;
+ if(life == 0)
+ {
+ writefln("You are dead.");
+ play = false;
+ }
+ break;
+ }
+ break;
+ case 3://NPC
+ if(rnd()<0.05)
+ {
+ writefln("Nobody wants to speak with you.");
+ break;
+ }
+ writefln("You met an NPC");
+ switch(rand(0,9))
+ {
+ case 0: writefln("He had nothing interesting to say."); break;
+ case 1: writefln("She was really boring."); break;
+ case 2: writefln("You got a quest!"); break;
+ case 3: writefln("You completed a quest and got some dough."); gold += rand(1,10); break;
+ case 4: writefln("The nice NPC gave you a healing potion."); life+=rand(2,4); break;
+ case 5: writefln("You robbed 'em blind and got some loot."); loot+=(10,20); break;
+ case 6: writefln("The guard took some of your money, saying you were
+late on your child support payments."); gold = gold/3; break;
+ case 7: writefln("You spent some money on bribes"); gold -= gold/4; break;
+ case 8: writefln("You had to travel all the way accross the island to talk to this person."); gold -= gold/4; break;
+ case 9: writefln("The Breton mistook you for his mother, and gave you tons of gold."); gold += 100; break;
+ }
+ break;
+
+ case 4://Sell
+ if(loot == 0)
+ writefln("You have nothing to sell (except that moon sugar and the home made poetry that nobody wants)");
+ else if(rnd()<0.93)
+ {
+ goldinc = cast(int)(loot*rnd()*2);
+ if(goldinc > loot) writefln("The merchant likes you, you got ", goldinc, " gold for stuff worth only ", loot, ".");
+ if(goldinc <= loot) writefln("The merchant didn't like you, your ", loot, " worth of stuff
+only got you ", goldinc, " gold.");
+ }
+ else
+ {
+ writefln("You met a talking mudcrab and an unfunny scamp! You got lots of\ncash for your loot.");
+ goldinc = 5*loot;
+ }
+ gold += goldinc;
+ loot = 0;
+ break;
+ case 5://Skooma
+ switch(rand(0,7))
+ {
+ case 0:
+ str = "gigant, flesh eating mushrooms"; break;
+ case 1:
+ str = "a firm, slender and agile female argonian"; break;
+ case 2:
+ str = "dead people and some stupid guy in a golden mask"; break;
+ case 3:
+ str = "the whole world only being part of a computer game"; break;
+ case 4:
+ str = "nothing in particular"; break;
+ case 5:
+ str = "an old, half naked guy giving you orders, insisting you\ncall him 'spymaster'";
+ break;
+ case 6:
+ str = "being a geek who sits in front of a screen all day long"; break;
+ case 7:
+ str = "the clouds, man, the crazy clouds!"; break;
+ }
+ writefln("You fall asleep in a ditch and dream about ", str, ".");
+ break;
+ case 6: //Water effects
+ switch(rand(0,5))
+ {
+ case 0: writefln("Them waves sure are pretty!"); break;
+ case 1:
+ writefln("A slaughter fish jumps up and bites you in the nose.");
+ life--;
+ if(life == 0)
+ {
+ writefln("You are dead.");
+ play = false;
+ }
+ break;
+ case 2: writefln("Those graphics might have looked impressive six years ago...");
+ break;
+ case 3: writefln("You were eaten by a Mudcrab. You are dead."); play=false; break;
+ case 4: writefln("You suddenly realize that the person who made this program has way too much time on his hands.");break;
+ case 5: writefln("You found a note with cheat codes on them."); level+=2; life+=rand(5,15); break;
+ }
+ break;
+
+ // Exit
+ case 7: play=false; break;
+ }
+ }
+ writefln("\nScore:");
+ writefln("Gold: %d : %d points", gold, gold);
+ writefln("Level: %d : %d points", level, (level-1)*40);
+ if(loot) writefln("Loot: you have to sell the loot to get any points for it.");
+ Entry n;
+
+ n.score = gold + (level-1) * 40;
+
+ writefln("Total score: ", n.score);
+
+ Entry[] high = getScores();
+
+
+
+
+ int index = 10;
+
+ foreach(int i, Entry e; high)
+ if(n.score > e.score)
+ {
+ index = i;
+ break;
+ }
+
+ writefln();
+
+ if(index != 10)
+ {
+ writef("Congratulations! You've made it to the Hall of Fame.\nEnter your name: ");
+ din.readLine();
+ n.name = din.readLine();
+
+ for(int i = 9; i>index; i--)
+ high[i] = high[i-1];
+
+ high[index] = n;
+
+ setScores(high);
+ }
+
+ writefln("Hall of Fame:");
+ foreach(int i, Entry e; high)
+ if(e.score) writefln("%-2d: %-10d %s", i+1, e.score, e.name);
+
+ writefln("\n(Apologies to Bethesda Softworks)");
+}
+
+struct Entry
+{
+ char[] name;
+ int score;
+}
+
+void setScores(Entry[] l)
+{
+ auto File f = new File("bored.highscores", FileMode.OutNew);
+ foreach(Entry e; l)
+ {
+ f.write(e.name);
+ f.write(e.score);
+ }
+}
+
+Entry[] getScores()
+{
+ Entry[] l;
+ l.length = 10;
+
+ if(exists("bored.highscores"))
+ {
+ auto File f = new File("bored.highscores");
+ foreach(ref Entry e; l)
+ {
+ f.read(e.name);
+ f.read(e.score);
+ }
+ }
+
+ return l;
+}
diff --git a/bsa/bsafile.d b/bsa/bsafile.d
new file mode 100644
index 0000000000..ddb293c170
--- /dev/null
+++ b/bsa/bsafile.d
@@ -0,0 +1,358 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (bsafile.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+module bsa.bsafile;
+
+//debug=checkHash;
+
+debug(checkHash) import std.stdio;
+
+// This file does not have any unit tests, since you really need the
+// data to test it. Use the program named 'bsatool', it uses the NIF
+// reader library and scans through a bsa archive, providing a good
+// test of both libraries.
+
+//import std.stream;
+import std.string;
+import std.mmfile;
+
+import core.memory;
+
+import monster.util.aa;
+
+class BSAFileException : Exception
+{
+ this(char[] msg) {super("BSAFileException: " ~ msg);}
+}
+
+/**
+ * This class is used to read "Bethesda Archive Files", or BSAs.
+ *
+ * The BSA archives are typically held open for the entire lifetime of
+ * the application, and are accessed more or less randomly. For that
+ * reason the BSAFile class uses memory mapped files. However, to be
+ * reasonably memory efficient, only the last requested file is
+ * guaranteed to be mapped at any given time, therefore make sure you
+ * don't use any persistant slices.
+ *
+ */
+
+class BSAFile
+{
+ private:
+ // Size of the blocks to map with the memory mapped file. If set to
+ // 0, we map the entire file, but then we use a LOT of system
+ // memory. There is really no penalty in setting it too small, since
+ // multiple blocks may be mapped simultaneously. However, it MUST be
+ // a multiple of the system page size. TODO: On my system it is 4K,
+ // later on I will have to call getpagesize and the windows
+ // equivalent to find this. For now I just assume 8K is ok.
+ static int pageSize = 8*1024;
+
+ // Represents one file entry in the archive
+ struct FileStruct
+ {
+ // File size and offset in file. We store the offset from the
+ // beginning of the file, not the offset into the data buffer
+ // (which is what is stored in the archive.)
+ uint fileSize, offset;
+ char[] name;
+
+ void getName(char[] buf, uint start)
+ {
+ if(start >= buf.length)
+ throw new BSAFileException("Name offset outside buffer");
+
+ uint end = start;
+ // We have to search for the end of the name string, marked by a zero.
+ for(; end> n); // "rotate right" operation
+ off += 8;
+ }
+ hash2 = sum;
+ }
+ }
+ }
+
+ MmFile mmf; // Handle to memory mapped file
+ char[] filename; // File name
+ FileStruct files[]; // The file table is stored here
+ bool isLoaded; // Set to true if a file has been loaded
+
+ // An AA for fast file name lookup. The CITextHash is a
+ // case-insensitive text hasher, meaning that all lookups are case
+ // insensitive.
+ HashTable!(char[], int, ESMRegionAlloc, CITextHash) lookup;
+
+ void fail(char[] msg)
+ {
+ throw new BSAFileException(msg ~ "\nFile: " ~ filename);
+ }
+
+ void read()
+ {
+ /*
+ * The layout of a BSA archive is as follows:
+ *
+ * - 12 bytes header, contains 3 ints:
+ * id number - equal to 0x100
+ * dirsize - size of the directory block (see below)
+ * numfiles - number of files
+ *
+ * ---------- start of directory block -----------
+ *
+ * - 8 bytes*numfiles, each record contains
+ * fileSize
+ * offset into data buffer (see below)
+ *
+ * - 4 bytes*numfiles, each record is an offset into the following name buffer
+ *
+ * - name buffer, indexed by the previous table, each string is
+ * null-terminated. Size is (dirsize - 12*numfiles).
+ *
+ * ---------- end of directory block -------------
+ *
+ * - 8*filenum - hast table block, we currently ignore this
+ *
+ * Data buffer:
+ *
+ * - The rest of the archive is file data, indexed by the
+ * offsets in the directory block. The offsets start at 0 at
+ * the beginning of this buffer.
+ *
+ */
+ assert(!isLoaded);
+
+ uint fsize = mmf.length;
+
+ if( fsize < 12 )
+ fail("File too small to be a valid BSA archive");
+
+ // Recast the file header as a list of uints
+ uint[] array = cast(uint[]) mmf[0..12];
+
+ if(array[0] != 0x100)
+ fail("Unrecognized BSA header");
+
+ // Total number of bytes used in size/offset-table + filename
+ // sections.
+ uint dirsize = array[1];
+ debug writefln("Directory size: ", dirsize);
+
+ // Number of files
+ uint filenum = array[2];
+ debug writefln("Number of files: ", filenum);
+
+ // Each file must take up at least 21 bytes of data in the
+ // bsa. So if files*21 overflows the file size then we are
+ // guaranteed that the archive is corrupt.
+ if( (filenum*21 > fsize -12) ||
+ (dirsize+8*filenum > fsize -12) )
+ fail("Directory information larger than entire archive");
+
+ // Map the entire directory (skip the hashes if we don't need them)
+ debug(checkHash)
+ void[] mm = mmf[12..(12+dirsize+8*filenum)];
+ else
+ void[] mm = mmf[12..(12+dirsize)];
+
+ // Allocate the file list from esmRegion
+ files = esmRegion.allocateT!(FileStruct)(filenum);
+
+ // Calculate the offset of the data buffer. All file offsets are
+ // relative to this.
+ uint fileDataOffset = 12 + dirsize + 8*filenum;
+
+ // Get a slice of the size/offset table
+ array = cast(uint[])mm[0..(8*filenum)];
+ int index = 0; // Used for indexing array[]
+
+ // Read the size/offset table
+ foreach(ref FileStruct fs; files)
+ {
+ fs.fileSize = array[index++];
+ fs.offset = array[index++] + fileDataOffset;
+ if(fs.offset+fs.fileSize > fsize) fail("Archive contains files outside itself");
+ }
+
+ // Get a slice of the name offset table
+ array = cast(uint[])mm[(8*filenum)..(12*filenum)];
+
+ // Get a slice of the name field
+ char[] nameBuf = cast(char[])mm[(12*filenum)..dirsize];
+ // And copy it!
+ nameBuf = esmRegion.copy(nameBuf);
+
+ // Tell the lookup table how big it should be
+ lookup.rehash(filenum);
+
+ // Loop through the name offsets and pick out the names
+ foreach(int idx, ref FileStruct fs; files)
+ {
+ fs.getName(nameBuf, array[idx]);
+ lookup[fs.name] = idx;
+ debug(2) writefln("%d: %s, %d bytes at %x", idx,
+ fs.name, fs.fileSize, fs.offset);
+ }
+
+ // Code to check if file hashes are correct - this was mostly
+ // used to check our hash algorithm.
+ debug(checkHash)
+ {
+ // Slice the Hash table
+ array = cast(uint[])mm[dirsize..(dirsize+8*filenum)];
+ index = 0;
+ foreach(ref FileStruct fs; files)
+ {
+ uint h1, h2;
+ fs.hashName(h1,h2);
+ uint h11 = array[index++];
+ uint h22 = array[index++];
+ if(h1 != h11) writefln("1 : %d vs. %d", h1, h11);
+ if(h2 != h22) writefln("2 : %d vs. %d", h2, h22);
+ }
+ }
+
+ isLoaded = true;
+ }
+
+ public:
+
+ /* -----------------------------------
+ * BSA management methods
+ * -----------------------------------
+ */
+
+ this() {}
+ this(char[] file) {open(file);}
+
+ // We should clean up after us
+ ~this() {clear();}
+
+ // Open a new file. Clears any existing data
+ void open(char[] file)
+ {
+ clear();
+ filename = file;
+
+ // Open a memory mapped file
+ mmf = new MmFile(
+ file, // File name
+ MmFile.Mode.Read, // Read only
+ 0, // We need the entire file
+ null, // Don't care where it is mapped
+ pageSize); // DON'T map the entire file, uses
+ // too much memory.
+ // Load header and file directory
+ read();
+ }
+
+ // Clear all data and close file
+ void clear()
+ {
+ delete mmf;
+
+ lookup.reset;
+ files.length = 0;
+ isLoaded = false;
+ }
+
+ /* -----------------------------------
+ * Archive file routines
+ * -----------------------------------
+ */
+
+ void[] findSlice(int index)
+ {
+ if(!isLoaded)
+ fail("No archive is open");
+
+ if(index < 0 || index >= files.length)
+ fail("Index out of bounds");
+
+ //writefln("\noffset %d, filesize %d", files[index].offset, files[index].fileSize);
+ // Get a slice of the buffer that comprises this file
+ with(files[index])
+ return mmf[offset..offset+fileSize];
+ }
+
+ int getIndex(char[] name)
+ {
+ int i;
+
+ // Look it up in the AA
+ if( lookup.inList( name, i ) )
+ return i;
+ else
+ return -1;
+ }
+
+ // Return a slice. This routine converts the name to lower case,
+ // since all BSA file names are stored that way, but references to
+ // them in ESM/ESPs and NIFs are not.
+ void[] findSlice(char[] name)
+ {
+ int i = getIndex(name);
+ if(i == -1) return null;
+ return findSlice(i);
+ }
+
+ // Used by the 'bsatest' program to loop through the entire
+ // archive.
+ FileStruct[] getFiles() { return files; }
+
+ // Number of files in the archive
+ uint numFiles() { return files.length; }
+
+ // Gets the name of the archive file.
+ char[] getName() { return filename; }
+}
diff --git a/bsa/bsatool.d b/bsa/bsatool.d
new file mode 100644
index 0000000000..b8282548a6
--- /dev/null
+++ b/bsa/bsatool.d
@@ -0,0 +1,129 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (bsatool.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module bsa.bsatool;
+
+import std.stdio;
+
+import bsa.bsafile;
+
+import nif.nif;
+import nif.niffile;
+
+import core.memory;
+
+void scanAllNifs(BSAFile f)
+{
+ uint totSize;
+ BSAFile.FileStruct[] files = f.getFiles();
+
+ foreach(int ind, BSAFile.FileStruct file; files)
+ {
+ if(file.name[$-4..$] == ".nif" ||
+ file.name[$-3..$] == ".kf" )
+ {
+ void[] str = f.findSlice(ind);
+ totSize += str.length;
+
+ try nifMesh.open(str, file.name);
+ catch(NIFFileException e)
+ writefln("Got exception: %s", e);
+ }
+ }
+ writefln("Total NIF/KF size in this archive: ", totSize);
+}
+
+void listFiles(BSAFile f)
+{
+ BSAFile.FileStruct[] files = f.getFiles();
+
+ foreach(BSAFile.FileStruct file; files)
+ writefln(file.name, " (%d bytes)", file.fileSize);
+}
+
+int main(char[][] args)
+{
+ //*
+ int help(char[] msg)
+ {
+ writefln("%s", msg);
+ writefln("Format: bsatool archive.bsa [-x filename] [-l] [-n]");
+ writefln(" -x filename extract file");
+ writefln(" -l list all files");
+ writefln(" -n scan through all nifs");
+ return 1;
+ }
+
+ if(args.length < 3) return help("Insufficient arguments");
+
+ initializeMemoryRegions();
+
+ char[] filename, ext;
+ bool list, nifs, extract;
+ foreach(char[] a; args[1..$])
+ if(a == "-l") list = true;
+ else if(a == "-n") nifs = true;
+ else if(a == "-x") extract = true;
+ else if(extract && ext == "") ext = a;
+ else if(filename == "") filename = a;
+ else return help("Unknown option " ~ a);
+
+ if(filename == "") return help("No file specified");
+
+ auto BSAFile f = new BSAFile(filename);
+
+ if(list) listFiles(f);
+ if(nifs) scanAllNifs(f);
+ if(extract)
+ {
+ if(ext == "") return help("No file to extract");
+
+ void[] s = cast(ubyte[]) f.findSlice(ext);
+
+ if(s.ptr == null)
+ {
+ writefln("File '%s' not found in '%s'", ext, filename);
+ return 1;
+ }
+ ext = getWinBaseName(ext);
+
+ File o = new File(ext, FileMode.OutNew);
+ o.writeExact(s.ptr, s.length);
+ o.close();
+ writefln("File extracted to '%s'", ext);
+ }
+
+ //din.readLine();
+ return 0;
+}
+
+// Picks out part of a string after the last '\'.
+char[] getWinBaseName(char[] input)
+{
+ uint i;
+ for(i = input.length; i > 0; i--)
+ if(input[i-1] == '\\') break;
+ return input[i..$];
+}
+
+//import std.cstream;
diff --git a/cells.txt b/cells.txt
new file mode 100644
index 0000000000..4cff4a9852
--- /dev/null
+++ b/cells.txt
@@ -0,0 +1,1134 @@
+"Abaelun Mine"
+"Abaesen-Pulu Egg Mine"
+"Abanabi"
+"Abandoned Shipwreck, Cabin"
+"Abandoned Shipwreck, Lower Level"
+"Abandoned Shipwreck, Upper Level"
+"Abebaal Egg Mine"
+"Abernanit"
+"Abinabi"
+"Adanumuran"
+"Addadshashanammu, Shrine"
+"Addamasartus"
+"Ahallaraddon Egg Mine"
+"Ahanibi-Malmus Egg Mine"
+"Aharasaplit Camp, Zallay's Yurt"
+"Aharnabi"
+"Aharunartus"
+"Ahemmusa Camp, Addammus's Yurt"
+"Ahemmusa Camp, Ashkhan's Yurt"
+"Ahemmusa Camp, Assamma-Idan's Yurt"
+"Ahemmusa Camp, Dutadalk's Yurt"
+"Ahemmusa Camp, Kausi's Yurt"
+"Ahemmusa Camp, Lanabi's Yurt"
+"Ahemmusa Camp, Mamaea's Yurt"
+"Ahemmusa Camp, Nummu's Yurt"
+"Ahemmusa Camp, Wise Woman's Yurt"
+"Ahinipalit"
+"Aidanat Camp, Abassel's Yurt"
+"Ainab"
+"Ainab, Shrine"
+"Ainat"
+"Akimaes Grotto"
+"Akimaes-Ilanipu Egg Mine"
+"Akulakhan's Chamber"
+"Alas Ancestral Tomb"
+"Ald Daedroth, Antechamber"
+"Ald Daedroth, Inner Shrine"
+"Ald Daedroth, Left Wing"
+"Ald Daedroth, Outer Shrine"
+"Ald Daedroth, Right Wing"
+"Ald Redaynia, Tower"
+"Ald Sotha, Lower Level"
+"Ald Sotha, Shrine"
+"Ald Sotha, Upper Level"
+"Ald Velothi, Fevila Bethrano's Shack"
+"Ald Velothi, Ganus Lloryn's Shack"
+"Ald Velothi, Ienas Arvel's Shack"
+"Ald Velothi, Lledsea Relas's Shack"
+"Ald Velothi, Llerar Vinden's Shack"
+"Ald Velothi, Outpost"
+"Ald Velothi, Sathas Rivul's Shack"
+"Ald Velothi, Vireveri Darethran's Shack"
+"Ald-ruhn, Ald Skar Inn"
+"Ald-ruhn, Arobar Guard Quarters"
+"Ald-ruhn, Arobar Manor Bedrooms"
+"Ald-ruhn, Arobar Manor Entrance"
+"Ald-ruhn, Arobar Private Quarters"
+"Ald-ruhn, Aryni Orethi's House"
+"Ald-ruhn, Bevene Releth: Clothier"
+"Ald-ruhn, Bivale Teneran: Clothier"
+"Ald-ruhn, Boldrisa Andrano's House"
+"Ald-ruhn, Braynas Hlervu's House"
+"Ald-ruhn, Cienne Sintieve: Alchemist"
+"Ald-ruhn, Codus Callonus: Bookseller"
+"Ald-ruhn, Council Club"
+"Ald-ruhn, Council Hostel"
+"Ald-ruhn, Dandera Selaro: Smith"
+"Ald-ruhn, Daynes Redothril: Pawnbroker"
+"Ald-ruhn, Dravasa Andrethi's House"
+"Ald-ruhn, Drinar Varyon's House"
+"Ald-ruhn, Galsa Gindu's House"
+"Ald-ruhn, Galthragoth's House"
+"Ald-ruhn, Gildan's House"
+"Ald-ruhn, Gindrala Hleran's House"
+"Ald-ruhn, Goras Andrelo's House"
+"Ald-ruhn, Guard Tower 1"
+"Ald-ruhn, Guard Tower 2"
+"Ald-ruhn, Guild of Fighters"
+"Ald-ruhn, Guild of Mages"
+"Ald-ruhn, Guls Llervu's House"
+"Ald-ruhn, Hanarai Assutlanipal's House"
+"Ald-ruhn, Ienas Sarandas's House"
+"Ald-ruhn, Ilmiril's House"
+"Ald-ruhn, Llether Vari: Enchanter"
+"Ald-ruhn, Llethri Guard Quarters"
+"Ald-ruhn, Llethri Manor Bedrooms"
+"Ald-ruhn, Llethri Manor Entrance"
+"Ald-ruhn, Llethri Private Quarters"
+"Ald-ruhn, Malpenix Blonia: Trader"
+"Ald-ruhn, Manor District"
+"Ald-ruhn, Morag Tong Guildhall"
+"Ald-ruhn, Morvayn Quarters"
+"Ald-ruhn, Pellecia Aurrus's House"
+"Ald-ruhn, Practice Room"
+"Ald-ruhn, Ramoran Manor"
+"Ald-ruhn, Ramoran Private Quarters"
+"Ald-ruhn, Redoran Council Entrance"
+"Ald-ruhn, Redoran Council Hall"
+"Ald-ruhn, Sarethi Guard Quarters"
+"Ald-ruhn, Sarethi Manor"
+"Ald-ruhn, Tauryon's House"
+"Ald-ruhn, Temple"
+"Ald-ruhn, The Rat In The Pot"
+"Ald-ruhn, Tiras Sadus: General Merchandise"
+"Ald-ruhn, Tuveso Beleth: Smith"
+"Ald-ruhn, Venim Manor Bedrooms"
+"Ald-ruhn, Venim Manor Entrance"
+"Ald-ruhn, Venim Manor Guard Quarters"
+"Ald-ruhn, Venim Manor Private Quarters"
+"Ald-ruhn, Venim Manor Right Wing"
+"Aleft"
+"Alen Ancestral Tomb"
+"Almurbalarammi, Shrine"
+"Alof's Farmhouse"
+"An Abandoned Shack"
+"Ancient Shipwreck, Cabin"
+"Ancient Shipwreck, Lower Level"
+"Ancient Shipwreck, Upper Level"
+"Andalen Ancestral Tomb"
+"Andalor Ancestral Tomb"
+"Andas Ancestral Tomb"
+"Andasreth, Lower Level"
+"Andasreth, Propylon Chamber"
+"Andasreth, Upper Level"
+"Andavel Ancestral Tomb"
+"Andrano Ancestral Tomb"
+"Andrethi Ancestral Tomb"
+"Andules Ancestral Tomb"
+"Ansi"
+"Aralen Ancestral Tomb"
+"Aran Ancestral Tomb"
+"Arano Ancestral Tomb"
+"Arano Plantation"
+"Arenim Ancestral Tomb"
+"Arethan Ancestral Tomb"
+"Arkngthand, Cells of Hollow Hand"
+"Arkngthand, Deep Ore Passage"
+"Arkngthand, Hall of Centrifuge"
+"Arkngthand, Heaven's Gallery"
+"Arkngthand, Land's Blood Gallery"
+"Arkngthand, Weepingbell Hall"
+"Arkngthunch-Sturdumz"
+"Arrow, Cabin"
+"Arrow, Lower Level"
+"Arrow, Upper Level"
+"Arvel Manor"
+"Arvel Manor, Slave Shack"
+"Arvel Manor, Slavemaster's Shack"
+"Arvel Manor, Storage Shack"
+"Aryon Ancestral Tomb"
+"Arys Ancestral Tomb"
+"Asha-Ahhe Egg Mine"
+"Ashalmawia, Shrine"
+"Ashalmawia, Shrine, Sunken Vaults"
+"Ashalmimilkala, Shrine"
+"Ashamanu Camp, Kausha's Yurt"
+"Ashanammu"
+"Ashimanu Egg Mine"
+"Ashimanu Egg Mine, Queen's Lair"
+"Ashinabi"
+"Ashinabi, Smuggler Den"
+"Ashir-Dan"
+"Ashirbadon"
+"Ashmelech"
+"Ashunartes, Shrine"
+"Ashurnibibi, Shrine"
+"Assalkushalit, Shrine"
+"Assarnatamat, Shrine"
+"Assarnud"
+"Assemanu"
+"Assemanu, Shrine"
+"Assernerairan, Shrine"
+"Assu"
+"Assumanu"
+"Assurdirapal, Inner Shrine"
+"Assurdirapal, Shrine"
+"Assurnabitashpi"
+"Assurnabitashpi, Shrine"
+"Bal Fell, East Wing"
+"Bal Fell, Inner Shrine"
+"Bal Fell, Outer Shrine"
+"Bal Fell, West Wing"
+"Bal Ur, Shrine"
+"Bal Ur, Underground"
+"Balmora, Astius Hanotepelus' House"
+"Balmora, Balyn Omarel's House"
+"Balmora, Caius Cosades' House"
+"Balmora, Clagius Clanler: Outfitter"
+"Balmora, Council Club"
+"Balmora, Dorisa Darvel: Bookseller"
+"Balmora, Dralasa Nithryon: Pawnbroker"
+"Balmora, Dralcea Arethi's House"
+"Balmora, Dralosa Athren's House"
+"Balmora, Drarayne Thelas' House"
+"Balmora, Drarayne Thelas' Storage"
+"Balmora, Dura gra-Bol's House"
+"Balmora, Eastern Guard Tower"
+"Balmora, Eight Plates"
+"Balmora, Fast Eddie's House"
+"Balmora, Guild of Fighters"
+"Balmora, Guild of Mages"
+"Balmora, Hecerinde's House"
+"Balmora, Hlaalo Manor"
+"Balmora, Hlaalu Council Manor"
+"Balmora, Itan's House"
+"Balmora, Karlirah's House"
+"Balmora, Lucky Lockup"
+"Balmora, Meldor: Armorer"
+"Balmora, Milie Hastien: Fine Clothier"
+"Balmora, Morag Tong Guild"
+"Balmora, Nalcarya of White Haven: Fine Alchemist"
+"Balmora, Nerano Manor"
+"Balmora, Nine Toes' House"
+"Balmora, Ra'Virr: Trader"
+"Balmora, Rarayn Radarys' House"
+"Balmora, Rithleen's House"
+"Balmora, South Wall Cornerclub"
+"Balmora, Temple"
+"Balmora, The Razor Hole"
+"Balmora, Tsiya's House"
+"Balmora, Tyermaillin's House"
+"Balmora, Tyravel Manor"
+"Balmora, Vorar Helas' House"
+"Balmora, Vori's House"
+"Balmora, Western Guard Tower North"
+"Balmora, Western Guard Tower South"
+"Balur's Farmhouse"
+"Band Egg Mine"
+"Baram Ancestral Tomb"
+"Bensamsi"
+"Bensiberib Camp, Odaishah's Yurt"
+"Beran Ancestral Tomb"
+"Berandas, Keep, Bottom Level"
+"Berandas, Keep, Top Level"
+"Berandas, Propylon Chamber"
+"Berandas, Underground"
+"Beshara"
+"Big Head's Shack"
+"Bthanchend"
+"Bthuand"
+"Bthungthumz"
+"Buckmoth Legion Fort, Interior"
+"Buckmoth Legion Fort, Prison"
+"Buckmoth Legion Fort, Towers"
+"Caldera Mine"
+"Caldera, Bashuk gra-Bat's House"
+"Caldera, Canodia Felannus' House"
+"Caldera, Dro'Shavir's House"
+"Caldera, Elmussa Damori's House"
+"Caldera, Falanaamo: Clothier"
+"Caldera, Ghorak Manor"
+"Caldera, Governor's Hall"
+"Caldera, Guild of Mages"
+"Caldera, Hodlismod: Armorer"
+"Caldera, Irgola: Pawnbroker"
+"Caldera, Keel-Raniur's House"
+"Caldera, Mining Bunkhouse"
+"Caldera, Mining Company Office"
+"Caldera, Mining Guard Tower"
+"Caldera, Nedhelas' House"
+"Caldera, North Guard Towers"
+"Caldera, Odairan Ashummi-Ammus' House"
+"Caldera, Shenk's Shovel"
+"Caldera, Slave Shack One"
+"Caldera, Slave Shack Two"
+"Caldera, South Guard Towers"
+"Caldera, Surane Leoriane's House"
+"Caldera, Valvius Mevureius' House"
+"Caldera, Verick Gemain: Trader"
+"Cavern of the Incarnate"
+"Character Stuff Wonderland"
+"Chun-Ook, Cabin"
+"Chun-Ook, Lower Level"
+"Chun-Ook, Upper Level"
+"Clutter Warehouse - Everything Must Go!"
+"Corprusarium"
+"Corprusarium Bowels"
+"Dagon Fel, Andre Maul's Tower"
+"Dagon Fel, Anja Swift-Sailer's House"
+"Dagon Fel, End of the World Renter Rooms"
+"Dagon Fel, Fanar Fork-Beard's House"
+"Dagon Fel, Fjorrod's House"
+"Dagon Fel, Greidil Half-Troll's House"
+"Dagon Fel, Heifnir: Trader"
+"Dagon Fel, Hurg's House"
+"Dagon Fel, Itar the Gentle's House"
+"Dagon Fel, Mette's House"
+"Dagon Fel, Onmi Hard-mouth's House"
+"Dagon Fel, Sorkvild's Tower"
+"Dagon Fel, The End of the World"
+"Dagon Fel, Vacant Tower"
+"Dagon Fel, Watch Tower"
+"Dagoth Ur, Facility Cavern"
+"Dagoth Ur, Inner Facility"
+"Dagoth Ur, Inner Tower"
+"Dagoth Ur, Lower Facility"
+"Dagoth Ur, Outer Facility"
+"Dareleth Ancestral Tomb"
+"Derelict Shipwreck, Cabin"
+"Derelict Shipwreck, Lower Level"
+"Derelict Shipwreck, Upper Level"
+"Deserted Shipwreck, Cabin"
+"Deserted Shipwreck, Lower Level"
+"Deserted Shipwreck, Upper Level"
+"Desolate Shipwreck, Cabin"
+"Desolate Shipwreck, Lower Level"
+"Desolate Shipwreck, Upper Level"
+"Dirara's Farmhouse"
+"Dissapla Mine"
+"Dralas Ancestral Tomb"
+"Drath Ancestral Tomb"
+"Dreloth Ancestral Tomb"
+"Dren Plantation, Doves' Shack"
+"Dren Plantation, Dren's Villa"
+"Dren Plantation, Guardhouse"
+"Dren Plantation, Helvi's Shack"
+"Dren Plantation, Hlevala's Shack"
+"Dren Plantation, Rethan's Shack"
+"Dren Plantation, Shipping House"
+"Dren Plantation, Storage Shack"
+"Dren Plantation, Tower Shack"
+"Dren Plantation, Verethi and Dralor"
+"Drethan Ancestral Tomb"
+"Drinith Ancestral Tomb"
+"Drulene Falen's Hut"
+"Druscashti, Lower Level"
+"Druscashti, Upper Level"
+"Dubdilla"
+"Dubdilla, Uncharted Caverns"
+"Dubdilla, Uncharted Caverns, Lower"
+"Dulo Ancestral Tomb"
+"Dun-Ahhe"
+"Dunirai Caverns"
+"Dushariran, Shrine"
+"Ebernanit, Shrine"
+"Ebonheart, Argonian Mission"
+"Ebonheart, East Empire Company Hall"
+"Ebonheart, East Empire North Warehouse"
+"Ebonheart, East Empire South Warehouse"
+"Ebonheart, Grand Council Chambers"
+"Ebonheart, Grand Council Chambers Tower"
+"Ebonheart, Hawkmoth Legion Garrison"
+"Ebonheart, Hawkmoth Towers"
+"Ebonheart, Imperial Chapels"
+"Ebonheart, Imperial Commission"
+"Ebonheart, Imperial Guard Garrison"
+"Ebonheart, Six Fishes"
+"Ebonheart, Skyrim Mission"
+"Ebonheart, Underground Caves"
+"Elanius Camp, Yapal's Yurt"
+"Elf-Skerring, Cabin"
+"Elf-Skerring, Lower Level"
+"Elf-Skerring, Upper Level"
+"Elith-Pal Mine"
+"Eluba-Addon Egg Mine"
+"Eluba-Addon Grotto"
+"Endusal, Kagrenac's study"
+"Erabenimsun Camp, Addut-Lamanu's Yurt"
+"Erabenimsun Camp, Ainab's Yurt"
+"Erabenimsun Camp, Ashkhan's Yurt"
+"Erabenimsun Camp, Ashu-Ahhe's Yurt"
+"Erabenimsun Camp, Assemmus' Yurt"
+"Erabenimsun Camp, Han-Ammu's Yurt"
+"Erabenimsun Camp, Massarapal's Yurt"
+"Erabenimsun Camp, Ranabi's Yurt"
+"Erabenimsun Camp, Salattanat's Yurt"
+"Erabenimsun Camp, Wise Woman's Yurt"
+"Eretammus-Sennammu Egg Mine"
+"Esutanamus, Shrine"
+"Fadathram Ancestral Tomb"
+"Fair Helas, Cabin"
+"Fair Helas, Lower Level"
+"Fair Helas, Upper Level"
+"Falas Ancestral Tomb"
+"Falasmaryon, Lower Level"
+"Falasmaryon, Missun Akin's Hut"
+"Falasmaryon, Propylon Chamber"
+"Falasmaryon, Sewers"
+"Falasmaryon, Upper Level"
+"Falensarano, Lower Level"
+"Falensarano, Propylon Chamber"
+"Falensarano, Upper Level"
+"Falvillo's Endeavor, Cabin"
+"Falvillo's Endeavor, Lower Level"
+"Falvilo's Endeavor, Upper Level"
+"Favel Ancestral Tomb"
+"Forgotten Shipwreck"
+"Forgotten Vaults of Anudnabia, Forge of Hilbongard"
+"Galom Daeus, Entry"
+"Galom Daeus, Observatory"
+"Ghostgate, Temple"
+"Ghostgate, Tower of Dawn"
+"Ghostgate, Tower of Dawn Lower Level"
+"Ghostgate, Tower of Dusk"
+"Ghostgate, Tower of Dusk Lower Level"
+"Gimothran Ancestral Tomb"
+"Ginith Ancestral Tomb"
+"Gnaar Mok, Anglalos's Shack"
+"Gnaar Mok, Arenim Manor"
+"Gnaar Mok, Caryarel's Shack"
+"Gnaar Mok, Druegh-jigger's Rest"
+"Gnaar Mok, Jerian Dolbanitte's Shack"
+"Gnaar Mok, Mush-Mere's Shack"
+"Gnaar Mok, Nadene Rotheran's Shack"
+"Gnaar Mok, Rostlogi's Shack"
+"Gnisis, Abelmawia Hut"
+"Gnisis, Almu Cave Dwelling"
+"Gnisis, Arvs-Drelen"
+"Gnisis, Barracks"
+"Gnisis, Bethamez"
+"Gnisis, Dinadad Hut"
+"Gnisis, Eggmine"
+"Gnisis, Fort Darius"
+"Gnisis, Lasamsi Cave Dwelling"
+"Gnisis, Lower Eggmine"
+"Gnisis, Madach Tradehouse"
+"Gnisis, Man-llu Hut"
+"Gnisis, Mantiti Cave Dwelling"
+"Gnisis, Shand Hut"
+"Gnisis, Shishara Hut"
+"Gnisis, Tansumiran Cave Dwelling"
+"Gnisis, Temple"
+"Gnisis, Underground Stream"
+"Gnisis, Vabdas Hut"
+"Gnisis, Yahaz Hut"
+"Gro-Bagrat Plantation"
+"Grytewake, Cabin"
+"Grytewake, Lower Level"
+"Grytewake, Upper Level"
+"Habinbaes"
+"Hairat-Vassamsi Egg Mine"
+"Hairat-Vassamsi Egg Mine, Queen's Lair"
+"Halit Mine"
+"Hanud"
+"Hanud, Tower"
+"Hassour"
+"Hassour, Shrine"
+"Hawia Egg Mine"
+"Helan Ancestral Tomb"
+"Helas Ancestral Tomb"
+"Heleran Ancestral Tomb"
+"Heran Ancestral Tomb"
+"Hinnabi"
+"Hla Oad, Fadila Balvel's House"
+"Hla Oad, Fatleg's Drop Off"
+"Hla Oad, Murudius Flaeus's House"
+"Hla Oad, Okur's House"
+"Hla Oad, Relien Rirne's House"
+"Hla Oad, Shurkul gro-Sharga's House"
+"Hlaalu Ancestral Tomb"
+"Hleran Ancestral Tomb"
+"Hlervi Ancestral Tomb"
+"Hlervu Ancestral Tomb"
+"Hlormaren, Dome"
+"Hlormaren, Keep, Bottom Level"
+"Hlormaren, Keep, Top Level"
+"Hlormaren, Propylon Chamber"
+"Hlormaren, Sewers"
+"Hlormaren, Underground"
+"Holamayan Monastery"
+"Ibar-Dad"
+"Ibishammus, Shrine"
+"Ienith Ancestral Tomb"
+"Ihinipalit, Shrine"
+"Ilanipu Grotto"
+"Ilunibi, Blackened Heart"
+"Ilunibi, Carcass of the Saint"
+"Ilunibi, Marowak's Spine"
+"Ilunibi, Soul's Rattle"
+"Ilunibi, Tainted Marrow"
+"Imperial Prison Ship"
+"Inanius Egg Mine"
+"Indalen Ancestral Tomb"
+"Indaren Ancestral Tomb"
+"Indarys Manor"
+"Indarys Manor, Berendas' House"
+"Indarys Manor, Manor Services"
+"Indarys Manor, Raram's House"
+"Indoranyon"
+"Indoranyon, Propylon Chamber"
+"Kaushtababi Camp, Adibael's Yurt"
+"Kaushtarari, Shrine"
+"ken's test hole"
+"Khuul, Aldi's Shack"
+"Khuul, Brurid's Shack"
+"Khuul, Endris Dilmyn's Shack"
+"Khuul, Helga's Shack"
+"Khuul, Miron Garer's Shack"
+"Khuul, Nelmyne Andules's Shack"
+"Khuul, Rivame Samandas's Shack"
+"Khuul, Svadstar's Shack"
+"Khuul, Thongar's Tradehouse"
+"Koal Cave"
+"Kogoruhn, Bleeding Heart"
+"Kogoruhn, Charma's Breath"
+"Kogoruhn, Dome of Pollock's Eve"
+"Kogoruhn, Dome of Urso"
+"Kogoruhn, Hall of Maki"
+"Kogoruhn, Hall of Phisto"
+"Kogoruhn, Hall of the Watchful Touch"
+"Kogoruhn, Nabith Waterway"
+"Kogoruhn, Temple of Fey"
+"Kogoruhn, Vault of Aerode"
+"Kora-Dur"
+"Kudanat"
+"Kumarahaz"
+"Kunirai"
+"Kushtashpi, Shrine"
+"Llando Ancestral Tomb"
+"Lleran Ancestral Tomb"
+"Llervu Ancestral Tomb"
+"Llirala's Shack"
+"Llovyn's Farmhouse"
+"Lonely Shipwreck, Cabin"
+"Lonely Shipwreck, Lower Level"
+"Lonely Shipwreck, Upper Level"
+"Lonesome Shipwreck, Cabin"
+"Lonesome Shipwreck, Lower Level"
+"Lonesome Shipwreck, Upper Level"
+"Lost Shipwreck, Cabin"
+"Lost Shipwreck, Lower Level"
+"Lost Shipwreck, Upper Level"
+"Maar Gan, Andus Tradehouse"
+"Maar Gan, Assi Serimilk's Hut"
+"Maar Gan, Assirari Zama-Rasour's Hut"
+"Maar Gan, Garry's Hut"
+"Maar Gan, Guard Tower 1"
+"Maar Gan, Guard Tower 2"
+"Maar Gan, Guard Tower 3"
+"Maar Gan, Huleen's Hut"
+"Maar Gan, Kind Erushara's Hut"
+"Maar Gan, Mabrelle Geles's Hut"
+"Maar Gan, Outpost"
+"Maar Gan, Shilipuran Zama-Rasour's Hut"
+"Maar Gan, Shrine"
+"Maar Gan, Tashpi Ashibael's Hut"
+"Maar Gan, Ulisamsi Shaddarnuran's Hut"
+"Maar Gan, Yeherradad's Hut"
+"Maba-Ilu"
+"Mababi"
+"Madas Grotto"
+"Madas-Zebba Egg Mine"
+"Maelkashishi, Shrine"
+"Maelkashishi, Shrine, Forgotten Galleries"
+"Maelu Egg Mine"
+"Maesa-Shammus Egg Mine"
+"Magas Volar"
+"Mallapi"
+"Malmus Grotto"
+"Mamaea, Sanctum of Awakening"
+"Mamaea, Sanctum of Black Hope"
+"Mamaea, Shrine of Pitted Dreams"
+"Mamshar-Disamus Camp, Maesat's Yurt"
+"Manat's Farmhouse"
+"Mannammu"
+"Maran-Adon"
+"Marandus, Dome"
+"Marandus, Lower Level"
+"Marandus, Propylon Chamber"
+"Marandus, Upper Level"
+"Maren Ancestral Tomb"
+"Mark's Vampire Test Cell"
+"Marvani Ancestral Tomb"
+"Massahanud Camp, Sargon's Yurt"
+"Massama Cave"
+"Masseranit"
+"Mat"
+"Matus-Akin Egg Mine"
+"Matus-Akin Egg Mine, Queen's Lair"
+"Mausur Caverns"
+"Mawia"
+"Mila-Nipal, Manat's Yurt"
+"Milk"
+"Minabi"
+"Minabi, Bandit Lair"
+"Ministry of Truth, Hall of Processing"
+"Ministry of Truth, Holding Cells"
+"Ministry of Truth, Prison Keep"
+"Missamsi"
+"Missir-Dadalit Egg Mine"
+"Molag Mar, Armigers Stronghold"
+"Molag Mar, Canalworks"
+"Molag Mar, Redoran Stronghold"
+"Molag Mar, Saetring the Nord: Smith"
+"Molag Mar, St. Veloth's Hostel"
+"Molag Mar, Temple"
+"Molag Mar, The Pilgrim's Rest"
+"Molag Mar, Underworks"
+"Molag Mar, Vasesius Viciulus: Trader"
+"Molag Mar, Waistworks"
+"Moonmoth Legion Fort, Interior"
+"Moonmoth Legion Fort, Prison Towers"
+"Morvayn Manor"
+"Mount Kand, Cavern"
+"Mudan Grotto"
+"Mudan, Central Vault"
+"Mudan, Lost Dwemer Checkpoint"
+"Mudan, Right Tower"
+"Mudan-Mul Egg Mine"
+"Mul Grotto"
+"Mzahnch"
+"Mzahnch Lower Level"
+"Mzanchend"
+"Mzuleft"
+"Nallit"
+"Nammu"
+"Nchardahrk"
+"Nchardumz"
+"Nchardumz Lower Level"
+"Nchuleft"
+"Nchuleftingth, Lower Levels"
+"Nchuleftingth, Test of Pattern"
+"Nchuleftingth, Upper Levels"
+"Nchurdamz, Interior"
+"Neglected Shipwreck, Cabin"
+"Neglected Shipwreck, Lower Level"
+"Neglected Shipwreck, Upper Level"
+"Nelas Ancestral Tomb"
+"Nerano Ancestral Tomb"
+"Nilera's Farmhouse"
+"Nimawia Grotto"
+"Nissintu"
+"Norvayn Ancestral Tomb"
+"Nund"
+"Obscure Shipwreck, Cabin"
+"Obscure Shipwreck, Lower Level"
+"Obscure Shipwreck, Upper Level"
+"Odaishah"
+"Odibaal"
+"Odirnamat"
+"Odirniran"
+"Odirniran, Tower"
+"Odrosal, Dwemer Training Academy"
+"Odrosal, Tower"
+"Omalen Ancestral Tomb"
+"Omani Manor"
+"Omaren Ancestral Tomb"
+"Onnissiralis, Shrine"
+"Orethi Ancestral Tomb"
+"Othrelas Ancestral Tomb"
+"Palansour"
+"Panabanit-Nimawia Egg Mine"
+"Panat"
+"Panud Egg Mine"
+"Pelagiad, Adanja's House"
+"Pelagiad, Ahnassi's House"
+"Pelagiad, Dralas Gilu's House"
+"Pelagiad, Erval's House"
+"Pelagiad, Farusea Salas' House"
+"Pelagiad, Fort Pelagiad"
+"Pelagiad, Guard Tower"
+"Pelagiad, Halfway Tavern"
+"Pelagiad, Junal-lei's House"
+"Pelagiad, Madres Navur's House"
+"Pelagiad, Mebestien Ence: Trader"
+"Pelagiad, Murberius Harmevus' House"
+"Pelagiad, North Wall"
+"Pelagiad, South Wall"
+"Pelagiad, Uulernil : Armorer"
+"Piernette's Farmhouse"
+"Pinsun"
+"Piran"
+"Prelude Shipwreck, Cabin"
+"Prelude Shipwreck, Lower Level"
+"Prelude Shipwreck, Upper Level"
+"Pudai Egg Mine"
+"Pudai Egg Mine, Queen's Lair"
+"Pulk"
+"Punabi"
+"Punammu"
+"Punsabanit"
+"Ramimilk, Shrine"
+"Randas Ancestral Tomb"
+"Ravel Ancestral Tomb"
+"Raviro Ancestral Tomb"
+"Rayna Drolan's Shack"
+"Redas Ancestral Tomb"
+"Redoran interior"
+"Redoran interior2"
+"Releth Ancestral Tomb"
+"Reloth Ancestral Tomb"
+"Remote Shipwreck, Cabin"
+"Remote Shipwreck, Lower Level"
+"Remote Shipwreck, Upper Level"
+"Rethan Manor"
+"Rethan Manor, Berendas' House"
+"Rethan Manor, Drelas' House"
+"Rethan Manor, Gols' House"
+"Rethan Manor, Tures' House"
+"Rethandus Ancestral Tomb"
+"Rissun"
+"Rothan Ancestral Tomb"
+"Rotheran, Arena"
+"Rotheran, Communal Hut"
+"Rotheran, Propylon Chamber"
+"Sadrith Mora, Anis Seloth: Alchemist"
+"Sadrith Mora, Balen Vendu: Monk"
+"Sadrith Mora, Dirty Muriel's Cornerclub"
+"Sadrith Mora, Fara's Hole in the Wall"
+"Sadrith Mora, Gateway Inn"
+"Sadrith Mora, Gateway Inn: North Wing"
+"Sadrith Mora, Gateway Inn: South Wing"
+"Sadrith Mora, Gateway Inn: West Wing"
+"Sadrith Mora, Hleras Gidren's House"
+"Sadrith Mora, Llaalam Madalas: Mage"
+"Sadrith Mora, Madran Ulvel's House"
+"Sadrith Mora, Meluria Seleth's House"
+"Sadrith Mora, Milara Vedran's House"
+"Sadrith Mora, Morag Tong Guild"
+"Sadrith Mora, Nevrila Areloth's House"
+"Sadrith Mora, Nirasa Aren's House"
+"Sadrith Mora, Pierlette Rostorard: Apothecary"
+"Sadrith Mora, Rolis Garvon's House"
+"Sadrith Mora, Tel Naga General Quarters"
+"Sadrith Mora, Tel Naga Great Hall"
+"Sadrith Mora, Tel Naga Upper Hall"
+"Sadrith Mora, Tel Naga Upper Tower"
+"Sadrith Mora, Telvanni Council House"
+"Sadrith Mora, Telvanni Council House, Chambers"
+"Sadrith Mora, Telvanni Council House, Entry"
+"Sadrith Mora, Telvanni Council House, Hermitage"
+"Sadrith Mora, Thervul Serethi: Healer"
+"Sadrith Mora, Trendrus Dral's House"
+"Sadrith Mora, Urtisa Romayn's House"
+"Sadrith Mora, Urtiso Faryon: Sorcerer"
+"Sadrith Mora, Vaden Belas' House"
+"Sadrith Mora, Volmyni Dral's House"
+"Sadrith Mora, Wolverine Hall"
+"Sadrith Mora, Wolverine Hall: Fighter's Guild"
+"Sadrith Mora, Wolverine Hall: Imperial Shrine"
+"Sadrith Mora, Wolverine Hall: Mage's Guild"
+"Sadryon Ancestral Tomb"
+"Salit Camp, Zalit's Yurt"
+"Salit Camp, Zelay's Yurt"
+"Salmantu"
+"Salmantu, Shrine"
+"Salothan Ancestral Tomb"
+"Salothran Ancestral Tomb"
+"Salvel Ancestral Tomb"
+"Samarys Ancestral Tomb"
+"Sanabi"
+"Sandas Ancestral Tomb"
+"Sandus Ancestral Tomb"
+"Sanit"
+"Sanit, Shrine"
+"Sanni"
+"Sarano Ancestral Tomb"
+"Saren Ancestral Tomb"
+"Sarethi Ancestral Tomb"
+"Sargon"
+"Sarimisun-Assa Egg Mine"
+"Sarimisun-Assa Egg Mine, Queen's Lair"
+"Sarys Ancestral Tomb"
+"Saturan"
+"Savel Ancestral Tomb"
+"Senim Ancestral Tomb"
+"Sennananit"
+"Seran Ancestral Tomb"
+"Serano Ancestral Tomb"
+"Sethan Ancestral Tomb"
+"Setus Egg Mine"
+"Seyda Neen, Arrille's Tradehouse"
+"Seyda Neen, Census and Excise Office"
+"Seyda Neen, Census and Excise Warehouse"
+"Seyda Neen, Draren Thiralas' House"
+"Seyda Neen, Eldafire's House"
+"Seyda Neen, Erene Llenim's Shack"
+"Seyda Neen, Fargoth's House"
+"Seyda Neen, Fine-Mouth's Shack"
+"Seyda Neen, Foryn Gilnith's Shack"
+"Seyda Neen, Indrele Rathryon's Shack"
+"Seyda Neen, Lighthouse"
+"Seyda Neen, Terurise Girvayne's House"
+"Seyda Neen, Vodunius Nuccius' House"
+"Sha-Adnius"
+"Shal"
+"Shallit"
+"Shara"
+"Sharapli"
+"Shashmanu Camp, Anit's Yurt"
+"Shashpilamat"
+"Shashpilamat, Shrine"
+"Shashurari Camp, Zennammu's Yurt"
+"Shishara"
+"Shishi"
+"Shrine of Azura"
+"Shulk Egg Mine"
+"Shulk Egg Mine, Mining Camp"
+"Shulk Egg Mine, Queen's Lair"
+"Shunned Shipwreck, Cabin"
+"Shunned Shipwreck, Lower Level"
+"Shunned Shipwreck, Upper Level"
+"Shurdan-Raplay Egg Mine"
+"Shurinbaal"
+"Shushan"
+"Shushishi"
+"Sinamusa Egg Mine"
+"Sinarralit Egg Mine"
+"Sinsibadon"
+"Sjorvar Horse-Mouth's House"
+"Small Farmhouse"
+"Sobitbael Camp, Mal's Yurt"
+"Sterdecan's Farmhouse"
+"Strange Shipwreck, Cabin"
+"Strange Shipwreck, Lower Level"
+"Strange Shipwreck, Upper Level"
+"Subdun"
+"Subdun, Shrine"
+"Sud"
+"Sudanit Mine"
+"Sulipund"
+"Sur Egg Mine"
+"Suran, Desele's House of Earthly Delights"
+"Suran, Garothmuk gro-Muzgub: Smith"
+"Suran, Goldyn Belaram: Pawnbroker"
+"Suran, Guard Tower"
+"Suran, Ibarnadad Assirnarari: Apothecary"
+"Suran, Oran Manor"
+"Suran, Ralds Oril: Trader"
+"Suran, Ranosa Gilvayn: Outfitter"
+"Suran, Suran Slave Market"
+"Suran, Suran Temple"
+"Suran, Suran Tradehouse"
+"Suran, Verara Rendo: Clothier"
+"Surirulk"
+"Tel Aruhn, Aryne Telnim: Smith"
+"Tel Aruhn, Bildren Areleth: Apothecary"
+"Tel Aruhn, Ferele Athram: Trader"
+"Tel Aruhn, Maren Uvaren: Enchanter"
+"Tel Aruhn, Plot and Plaster"
+"Tel Aruhn, Tower Entry"
+"Tel Aruhn, Tower Living Quarters"
+"Tel Aruhn, Underground"
+"Tel Aruhn, Upper Tower"
+"Tel Branora, Ervyna Hlervu's Shack"
+"Tel Branora, Evos Goran's House"
+"Tel Branora, Fadase Selvayn: Trader"
+"Tel Branora, Fedar Davus's House"
+"Tel Branora, Galen Berer: Armorer"
+"Tel Branora, Giron Manas's Shack"
+"Tel Branora, Lower Tower"
+"Tel Branora, Manos Vavyn's House"
+"Tel Branora, Seryne Relas's House"
+"Tel Branora, Sethan's Tradehouse"
+"Tel Branora, Tower Dungeon"
+"Tel Branora, Tower Guardpost"
+"Tel Branora, Upper Tower"
+"Tel Branora, Upper Tower: Therana's Chamber"
+"Tel Mora, Berwen: Trader"
+"Tel Mora, Elegnan: Clothier"
+"Tel Mora, Jolda: Apothecary"
+"Tel Mora, Kirsty's House"
+"Tel Mora, Liette's House"
+"Tel Mora, Lower Tower"
+"Tel Mora, Nona's House"
+"Tel Mora, Radras: Smith"
+"Tel Mora, The Covenant"
+"Tel Mora, Tower Services"
+"Tel Mora, Upper Tower"
+"Tel Uvirith, Arelas' House"
+"Tel Uvirith, Menas' House"
+"Tel Uvirith, Omavel's House"
+"Tel Uvirith, Seleth's House"
+"Tel Uvirith, Tower Dungeon"
+"Tel Uvirith, Tower Lower"
+"Tel Uvirith, Tower Upper"
+"Tel Vos, Aryon's Chambers"
+"Tel Vos, Barracks and Armory"
+"Tel Vos, Central Tower"
+"Tel Vos, Dungeon"
+"Tel Vos, Jail"
+"Tel Vos, Northeastern Tower"
+"Tel Vos, Services Tower"
+"Tel Vos, Southern Tower"
+"Telasero, Lower Level"
+"Telasero, Propylon Chamber"
+"Telasero, Upper Level"
+"Telvayn Ancestral Tomb"
+"Thalas Ancestral Tomb"
+"Tharys Ancestral Tomb"
+"Thelas Ancestral Tomb"
+"Thiralas Ancestral Tomb"
+"Tin-Ahhe"
+"ToddTest"
+"Tower of Tel Fyr, Hall of Fyr"
+"Tower of Tel Fyr, Onyx Hall"
+"Tukushapal"
+"Tukushapal, Sepulcher"
+"Tureynulal, Bladder of Clovis"
+"Tureynulal, Eye of Duggan"
+"Tureynulal, Eye of Thom Wye"
+"Tureynulal, Kagrenac's Library"
+"Tusenend, Shrine"
+"Ularradallaku, Shrine"
+"Ules Manor"
+"Ules Manor, Slave Shack"
+"Ules Manor, Slavemaster's Shack"
+"Ulummusa"
+"Uncharted Shipwreck, Lower Level"
+"Unchartered Shipwreck, Cabin"
+"Unchartered Shipwreck, Upper Level"
+"Unexplored Shipwreck, Cabin"
+"Unexplored Shipwreck, Lower Level"
+"Unexplored Shipwreck, Upper Level"
+"Unknown Shipwreck, Cabin"
+"Unknown Shipwreck, Lower Level"
+"Unknown Shipwreck, Upper Level"
+"Unmarked Shipwreck, Lower Level"
+"Unmarked Shipwreck, Upper Level"
+"Urshilaku Camp, Ahasour's Yurt"
+"Urshilaku Camp, Ashkhan's Yurt"
+"Urshilaku Camp, Kurapli's Yurt"
+"Urshilaku Camp, Maeli's Yurt"
+"Urshilaku Camp, Sakiran's Yurt"
+"Urshilaku Camp, Shara's Yurt"
+"Urshilaku Camp, Shimsun's Yurt"
+"Urshilaku Camp, Wise Woman's Yurt"
+"Urshilaku Camp, Zabamund's Yurt"
+"Urshilaku Camp, Zanummu's Yurt"
+"Urshilaku, Astral Burial"
+"Urshilaku, Fragile Burial"
+"Urshilaku, Juno Burial"
+"Urshilaku, Kakuna Burial"
+"Urshilaku, Karma Burial"
+"Urshilaku, Kefka Burial"
+"Urshilaku, Laterus Burial"
+"Uveran Ancestral Tomb"
+"Valenvaryon, Durz's Hut"
+"Valenvaryon, Gashna's Hut"
+"Valenvaryon, Lambug's Hut"
+"Valenvaryon, Lurog's Hut"
+"Valenvaryon, Propylon Chamber"
+"Valenvaryon, Umug's Hut"
+"Vandus Ancestral Tomb"
+"Vansunalit Egg Mine"
+"Vas, Entry Level"
+"Vas, Tower"
+"Vassamsi Grotto"
+"Vassir-Didanat Cave"
+"Velas Ancestral Tomb"
+"Veloth Ancestral Tomb"
+"Vemynal, Hall of Torque"
+"Vemynal, Outer Fortress"
+"Venim Ancestral Tomb"
+"Verelnim Ancestral Tomb"
+"Vivec, Agrippina Herennia: Clothier"
+"Vivec, Alusaron: Smith"
+"Vivec, Andilu Drothan: Alchemist"
+"Vivec, Arena Canalworks"
+"Vivec, Arena Fighters Quarters"
+"Vivec, Arena Fighters Training"
+"Vivec, Arena Hidden Area"
+"Vivec, Arena Holding Cells"
+"Vivec, Arena Pit"
+"Vivec, Arena Storage"
+"Vivec, Arena Underworks"
+"Vivec, Arena Waistworks"
+"Vivec, Aurane Frernis: Apothecary"
+"Vivec, Black Shalk Cornerclub"
+"Vivec, Canon Offices"
+"Vivec, Canon Quarters"
+"Vivec, Curio Manor"
+"Vivec, Dralor Manor"
+"Vivec, Elven Nations Cornerclub"
+"Vivec, Foreign Quarter Canalworks"
+"Vivec, Foreign Quarter Lower Waistworks"
+"Vivec, Foreign Quarter Plaza"
+"Vivec, Foreign Quarter Tomb"
+"Vivec, Foreign Quarter Underworks"
+"Vivec, Foreign Quarter Upper Waistworks"
+"Vivec, Guild of Fighters"
+"Vivec, Guild of Mages"
+"Vivec, Hall of Justice"
+"Vivec, Hall of Justice Secret Library"
+"Vivec, Hall of Wisdom"
+"Vivec, Hall Underworks"
+"Vivec, High Fane"
+"Vivec, Hlaalu Alchemist"
+"Vivec, Hlaalu Ancestral Vaults"
+"Vivec, Hlaalu Canalworks"
+"Vivec, Hlaalu Edryno Arethi's House"
+"Vivec, Hlaalu General Goods"
+"Vivec, Hlaalu Pawnbroker"
+"Vivec, Hlaalu Plaza"
+"Vivec, Hlaalu Prison Cells"
+"Vivec, Hlaalu Records"
+"Vivec, Hlaalu Temple"
+"Vivec, Hlaalu Treasury"
+"Vivec, Hlaalu Underworks"
+"Vivec, Hlaalu Vaults"
+"Vivec, Hlaalu Waistworks"
+"Vivec, Hlaalu Weaponsmith"
+"Vivec, Hlaren Residence"
+"Vivec, J'Rasha: Healer"
+"Vivec, Jeanne: Trader"
+"Vivec, Jobasha's Rare Books"
+"Vivec, Justice Offices"
+"Vivec, Library of Vivec"
+"Vivec, Lucretinaus Olcinius: Trader"
+"Vivec, Mevel Fererus: Trader"
+"Vivec, Milo's Quarters"
+"Vivec, Miun-Gei: Enchanter"
+"Vivec, No Name Club"
+"Vivec, Office of the Watch"
+"Vivec, Ordinator Barracks"
+"Vivec, Palace of Vivec"
+"Vivec, Puzzle Canal, Center"
+"Vivec, Puzzle Canal, Level 1"
+"Vivec, Puzzle Canal, Level 2"
+"Vivec, Puzzle Canal, Level 3"
+"Vivec, Puzzle Canal, Level 4"
+"Vivec, Puzzle Canal, Level 5"
+"Vivec, Ralen Tilvur: Smith"
+"Vivec, Redoran Ancestral Vaults"
+"Vivec, Redoran Canalworks"
+"Vivec, Redoran Plaza"
+"Vivec, Redoran Prison Cells"
+"Vivec, Redoran Records"
+"Vivec, Redoran Scout & Drillmaster"
+"Vivec, Redoran Smith"
+"Vivec, Redoran Temple Shrine"
+"Vivec, Redoran Trader"
+"Vivec, Redoran Treasury"
+"Vivec, Redoran Underworks"
+"Vivec, Redoran Vaults"
+"Vivec, Redoran Waistworks"
+"Vivec, Saren Manor"
+"Vivec, Simine Fralinie: Bookseller"
+"Vivec, St. Delyn Canal North-One"
+"Vivec, St. Delyn Canal North-Three"
+"Vivec, St. Delyn Canal North-Two"
+"Vivec, St. Delyn Canal South-One"
+"Vivec, St. Delyn Canal South-Three"
+"Vivec, St. Delyn Canal South-Two"
+"Vivec, St. Delyn Canalworks"
+"Vivec, St. Delyn Glassworker's Hall"
+"Vivec, St. Delyn Plaza"
+"Vivec, St. Delyn Potter's Hall"
+"Vivec, St. Delyn Storage"
+"Vivec, St. Delyn Underworks"
+"Vivec, St. Delyn Waist North-One"
+"Vivec, St. Delyn Waist North-Two"
+"Vivec, St. Delyn Waist South-One"
+"Vivec, St. Delyn Waist South-Two"
+"Vivec, St. Delyn Waistworks"
+"Vivec, St. Olms Brewers and Fishmongers Hall"
+"Vivec, St. Olms Canal North-One"
+"Vivec, St. Olms Canal North-Three"
+"Vivec, St. Olms Canal North-Two"
+"Vivec, St. Olms Canal South-One"
+"Vivec, St. Olms Canal South-Three"
+"Vivec, St. Olms Canal South-Two"
+"Vivec, St. Olms Canalworks"
+"Vivec, St. Olms Farmers and Laborers Hall"
+"Vivec, St. Olms Haunted Manor"
+"Vivec, St. Olms Plaza"
+"Vivec, St. Olms Storage"
+"Vivec, St. Olms Tailors and Dyers Hall"
+"Vivec, St. Olms Tanners and Miners Hall"
+"Vivec, St. Olms Temple"
+"Vivec, St. Olms Underworks"
+"Vivec, St. Olms Upper North-One"
+"Vivec, St. Olms Upper North-Two"
+"Vivec, St. Olms Upper South-One"
+"Vivec, St. Olms Waist North-One"
+"Vivec, St. Olms Waist North-Two"
+"Vivec, St. Olms Waist South-One"
+"Vivec, St. Olms Waist South-Two"
+"Vivec, St. Olms Waistworks"
+"Vivec, St. Olms Yngling Manor"
+"Vivec, St. Olms Yngling Manor Basement"
+"Vivec, Telvanni Alchemist"
+"Vivec, Telvanni Apothecary"
+"Vivec, Telvanni Canalworks"
+"Vivec, Telvanni Enchanter"
+"Vivec, Telvanni Mage"
+"Vivec, Telvanni Monster Lab"
+"Vivec, Telvanni Plaza"
+"Vivec, Telvanni Prison Cells"
+"Vivec, Telvanni Sorcerer"
+"Vivec, Telvanni Temple"
+"Vivec, Telvanni Tower"
+"Vivec, Telvanni Underworks"
+"Vivec, Telvanni Upper Storage"
+"Vivec, Telvanni Vault"
+"Vivec, Telvanni Waistworks"
+"Vivec, Temporary Telvanni Housing"
+"Vivec, Tervur Braven: Trader"
+"Vivec, The Abbey of St. Delyn the Wise"
+"Vivec, The Flowers of Gold"
+"Vivec, The Lizard's Head"
+"Vos, Dreynos Elvul's Farmhouse"
+"Vos, Fanisea Irano's Farmhouse"
+"Vos, Ienasa Radas's Farmhouse"
+"Vos, Maela Kaushad's Farmhouse"
+"Vos, Mandyn Ralas's Farmhouse"
+"Vos, Menus Felas's Farmhouse"
+"Vos, Runethyne Andas's Farmhouse"
+"Vos, Thilse Aralas's Farmhouse"
+"Vos, Trilam Drolnor's Farmhouse"
+"Vos, Ulvil Llothas's Farmhouse"
+"Vos, Varo Tradehouse"
+"Vos, Varo Tradehouse Entrance"
+"Vos, Varo Tradehouse Storage"
+"Vos, Vos Chapel"
+"Yakanalit"
+"Yakaridan Camp, Kitbael's Yurt"
+"Yakin"
+"Yakin, Shrine"
+"Yanemus Mine"
+"Yansirramus, Shrine"
+"Yasammidan, Shrine"
+"Yasamsi"
+"Yassu Mine"
+"Yesamsi"
+"Zainab Camp, Ababael Timsar-Dadisun's Yurt"
+"Zainab Camp, Ashibaal's Yurt"
+"Zainab Camp, Ashkhan's Yurt"
+"Zainab Camp, Ashur-Dan's Yurt"
+"Zainab Camp, Kuda's Yurt"
+"Zainab Camp, Kummu's Yurt"
+"Zainab Camp, Minassour's Yurt"
+"Zainab Camp, Patababi's Yurt"
+"Zainab Camp, Tussi's Yurt"
+"Zainab Camp, Wise Woman's Yurt"
+"Zainsipilu"
+"Zaintirari"
+"Zaintiraris, Shrine"
+"Zalkin Grotto"
+"Zalkin-Sul Egg Mine"
+"Zanabi"
+"Zebabi"
+"Zenarbael"
+"Zergonipal, Shrine"
diff --git a/core/config.d b/core/config.d
new file mode 100644
index 0000000000..1182b93b7c
--- /dev/null
+++ b/core/config.d
@@ -0,0 +1,276 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (config.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module core.config;
+
+import std.string;
+import std.file;
+import std.stdio;
+
+import monster.util.string;
+
+import core.inifile;
+
+import sound.audio;
+
+import input.keys;
+import input.ois;
+
+//import sdl.Keysym;
+
+//import input.events : updateMouseSensitivity;
+
+ConfigManager config;
+
+/*
+ * Structure that handles all user adjustable configuration options,
+ * including things like file paths, plugins, graphics resolution,
+ * game settings, window positions, etc. It is also responsible for
+ * reading and writing configuration files, for importing settings
+ * from Morrowind.ini and for configuring OGRE. It doesn't currently
+ * DO all of this, but it is supposed to in the future.
+ */
+struct ConfigManager
+{
+ IniWriter iniWriter;
+
+ float musicVolume;
+ float sfxVolume;
+ float mainVolume;
+ bool useMusic;
+
+ // Mouse sensitivity
+ float mouseSensX;
+ float mouseSensY;
+ bool flipMouseY;
+
+ // Number of current screen shot. Saved upon exit, so that shots
+ // from separate sessions don't overwrite each other.
+ int screenShotNum;
+
+ // Directories
+ char[] esmDir;
+ char[] bsaDir;
+ char[] sndDir;
+ char[] musDir; // Explore music
+ char[] musDir2; // Battle music
+
+ // Cell to load at startup
+ char[] defaultCell;
+
+ // Check that a given volume is within sane limits (0.0-1.0)
+ private static float saneVol(float vol)
+ {
+ if(!(vol >= 0)) vol = 0;
+ else if(!(vol <= 1)) vol = 1;
+ return vol;
+ }
+
+ // These set the volume to a new value and updates all sounds to
+ // take notice.
+ void setMusicVolume(float vol)
+ {
+ musicVolume = saneVol(vol);
+
+ jukebox.updateVolume();
+ battleMusic.updateVolume();
+ }
+
+ void setSfxVolume(float vol)
+ {
+ sfxVolume = saneVol(vol);
+
+ // TODO: Call some sound manager here to adjust all active sounds
+ }
+
+ void setMainVolume(float vol)
+ {
+ mainVolume = saneVol(vol);
+
+ // Update the sound managers
+ setMusicVolume(musicVolume);
+ setSfxVolume(sfxVolume);
+ }
+
+ // These calculate the "effective" volumes.
+ float calcMusicVolume()
+ {
+ return musicVolume * mainVolume;
+ }
+
+ float calcSfxVolume()
+ {
+ return sfxVolume * mainVolume;
+ }
+
+ // Initialize the config manager. Send a 'true' parameter to reset
+ // all keybindings to the default. A lot of this stuff will be moved
+ // to script code at some point. In general, all input mechanics and
+ // distribution of key events should happen in native code, while
+ // all setup and control should be handled in script code.
+ void initialize(bool reset = false)
+ {
+ // Initialize the key binding manager
+ keyBindings.initKeys();
+
+ readIni();
+
+ if(reset) with(keyBindings)
+ {
+ // Remove all existing key bindings
+ clear();
+
+ // Bind some default keys
+ bind(Keys.MoveLeft, KC.A, KC.LEFT);
+ bind(Keys.MoveRight, KC.D, KC.RIGHT);
+ bind(Keys.MoveForward, KC.W, KC.UP);
+ bind(Keys.MoveBackward, KC.S, KC.DOWN);
+ bind(Keys.MoveUp, KC.LSHIFT);
+ bind(Keys.MoveDown, KC.LCONTROL);
+
+ bind(Keys.MainVolUp, KC.ADD);
+ bind(Keys.MainVolDown, KC.SUBTRACT);
+ bind(Keys.MusVolDown, KC.N1);
+ bind(Keys.MusVolUp, KC.N2);
+ bind(Keys.SfxVolDown, KC.N3);
+ bind(Keys.SfxVolUp, KC.N4);
+ bind(Keys.ToggleBattleMusic, KC.SPACE);
+
+ bind(Keys.Pause, KC.PAUSE, KC.P);
+ bind(Keys.ScreenShot, KC.SYSRQ);
+ bind(Keys.Exit, KC.Q, KC.ESCAPE);
+ }
+
+ // I think DMD is on the brink of collapsing here. This has been
+ // moved elsewhere, because DMD couldn't handle one more import in
+ // this file.
+ //updateMouseSensitivity();
+ }
+
+ // Read config from morro.ini, if it exists.
+ void readIni()
+ {
+ // Read configuration file, if it exists.
+ IniReader ini;
+
+ // TODO: Right now we have to specify each option twice, once for
+ // reading and once for writing. Fix it? Nah. Don't do anything,
+ // this entire configuration scheme is likely to change anyway.
+
+ ini.readFile("morro.ini");
+
+ screenShotNum = ini.getInt("General", "Screenshots", 0);
+ mainVolume = saneVol(ini.getFloat("Sound", "Main Volume", 0.7));
+ musicVolume = saneVol(ini.getFloat("Sound", "Music Volume", 0.5));
+ sfxVolume = saneVol(ini.getFloat("Sound", "SFX Volume", 0.5));
+ useMusic = ini.getBool("Sound", "Enable Music", true);
+
+ mouseSensX = ini.getFloat("Controls", "Mouse Sensitivity X", 0.2);
+ mouseSensY = ini.getFloat("Controls", "Mouse Sensitivity Y", 0.2);
+ flipMouseY = ini.getBool("Controls", "Flip Mouse Y Axis", false);
+
+ defaultCell = ini.getString("General", "Default Cell", "");
+
+ // Read key bindings
+ for(int i; i
+ WWW: http://openmw.snaptoad.com/
+
+ This file (filefinder.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module core.filefinder;
+
+import std.file;
+import std.string;
+
+import monster.util.string;
+import monster.util.aa;
+
+import core.memory;
+
+import std.stdio;
+
+class FileFinderException : Exception
+{
+ this(char[] msg, char[] ext, char[] dir)
+ {
+ if(ext.length) super(format("FileFinder for %s files in %s: %s", ext, dir, msg));
+ else super(format("FileFinder for %s: %s", dir, msg));
+ }
+}
+
+// Do we traverse directories recursively? Default is yes.
+enum Recurse { Yes, No }
+
+// The file finder is used to list all files in a directory so we can
+// look up files without searching the filesystem each time. It is
+// case insensitive on all platforms, and transparently converts to
+// the right directory separator character (\ or /). We might extend
+// it later with code from other projects.
+class FileFinder
+{
+ private:
+ char[][] files; // Use GC for this, it's not too big and we don't
+ // have to manage roots pointing to the filenames.
+ HashTable!(char[], int, ESMRegionAlloc, FilenameHasher) lookup;
+
+ char[] dir; // Base directory to search
+ char[] ext; // Extensions to pick out
+
+ void fail(char[] err)
+ {
+ throw new FileFinderException(err, ext, dir);
+ }
+
+ // Removes the part of a path that is stored in 'dir'
+ char[] removeDir(char[] path)
+ {
+ //TODO: Should this be case insensitive?
+ assert(path[0..dir.length] == dir);
+
+ return path[dir.length..$];
+ }
+
+ void insert(char[] filename)
+ {
+ // Only keep the part of the filename not given in 'dir'.
+ char[] name = removeDir(filename);
+
+ if(!name.iEnds(ext)) return;
+
+ // We start counting from 1
+ uint newVal = files.length+1;
+
+ // Insert it, or get the old value if it already exists
+ uint oldVal = lookup[name, newVal];
+ if(oldVal != newVal)
+ fail("Already have " ~ name ~ "\nPreviously inserted as " ~ files[oldVal-1]);
+
+ // Store it
+ files ~= filename;
+ }
+
+ public:
+
+ static char[] addSlash(char[] dir)
+ {
+ // Add a trailing slash
+ version(Windows) if(!dir.ends("\\")) dir ~= '\\';
+ version(Posix) if(!dir.ends("/")) dir ~= '/';
+ return dir;
+ }
+
+ int length() { return lookup.length; }
+
+ this(char[] dir, char[] ext = null, Recurse r = Recurse.Yes)
+ in
+ {
+ if(!dir.length) fail("'dir' can not be empty");
+ }
+ out
+ {
+ assert(files.length == lookup.length);
+ }
+ body
+ {
+ // Add a trailing slash
+ dir = addSlash(dir);
+
+ this.dir = dir;
+
+ if(ext.length && ext[0] != '.') ext = "." ~ ext;
+ this.ext = ext;
+
+ bool callback(DirEntry* de)
+ {
+ if (de.isdir)
+ {
+ if(r == Recurse.Yes)
+ listdir(de.name, &callback);
+ }
+ else
+ insert(de.name);
+ return true;
+ }
+
+ try listdir(dir, &callback);
+ catch(FileException e)
+ fail(e.toString);
+ }
+
+ char[] opIndex(int i) { return files[i-1]; }
+
+ int opIndex(char[] file)
+ {
+ int i;
+
+ // Get value if it exists
+ if(lookup.inList(file, i))
+ return i;
+ return 0;
+ }
+
+ int opApply(int delegate(ref char[]) del)
+ {
+ int res = 0;
+
+ foreach(char[] s; files)
+ {
+ char[] tmp = removeDir(s);
+ res = del(tmp);
+ if(res) break;
+ }
+ return res;
+ }
+
+ char[] toString()
+ {
+ char[] result;
+ foreach(char[] s; this)
+ result ~= s ~ "\n";
+ return result;
+ }
+}
+
+// Hash functions that does not differentiate between linux and
+// windows file names. This means that it is case insensitive, and
+// treats '\' and '/' as the same character. Only needed in linux, in
+// windows just use CITextHasher.
+version(Posix)
+struct FilenameHasher
+{
+ static const char conv = 'a'-'A';
+
+ static int isEqual(char[] aa, char[] bb)
+ {
+ if(aa.length != bb.length) return 0;
+
+ foreach(int i, char a; aa)
+ {
+ char b = bb[i];
+
+ if(a == b)
+ continue;
+
+ // Convert both to lowercase and "/ case"
+ if(a <= 'Z' && a >= 'A') a += conv;
+ else if(a == '\\') a = '/';
+ if(b <= 'Z' && b >= 'A') b += conv;
+ else if(b == '\\') b = '/';
+
+ if(a != b) return 0;
+ }
+
+ // No differences were found
+ return 1;
+ }
+
+ static uint hash(char[] s)
+ {
+ uint hash;
+ foreach (char c; s)
+ {
+ if(c <= 'Z' && c >= 'A') c += conv;
+ else if(c == '\\') c = '/';
+ hash = (hash * 37) + c;
+ }
+ return hash;
+ }
+}
+
+version(Windows) alias CITextHasher FilenameHasher;
diff --git a/core/inifile.d b/core/inifile.d
new file mode 100644
index 0000000000..2727fa5d56
--- /dev/null
+++ b/core/inifile.d
@@ -0,0 +1,228 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (inifile.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module core.inifile;
+
+import std.stream;
+import std.string;
+import std.file;
+
+import monster.util.string;
+import monster.util.aa;
+
+// Writes an ini file.
+struct IniWriter
+{
+ File ini;
+ bool firstSection = true;
+
+ void openFile(char[] file)
+ {
+ if(ini is null) ini = new File();
+ if(exists(file))
+ rename(file, file~".old");
+ ini.open(file, FileMode.OutNew);
+ }
+
+ void close()
+ {
+ ini.close();
+ }
+
+ void section(char[] name)
+ {
+ if(firstSection)
+ firstSection = false;
+ else
+ ini.writefln();
+
+ ini.writefln("[%s]", name);
+ }
+
+ void comment(char[] str)
+ {
+ ini.writefln("; %s", str);
+ }
+
+ void writeType(T)(char[] name, T t)
+ {
+ ini.writefln("%s=%s", name, t);
+ }
+
+ alias writeType!(int) writeInt;
+ alias writeType!(float) writeFloat;
+ alias writeType!(char[]) writeString;
+
+ void writeBool(char[] name, bool b)
+ {
+ ini.writefln("%s=%s", name, b?"yes":"no");
+ }
+}
+
+// Keytype that holds section name and variable name
+struct SecVar
+{
+ private:
+ char[] section;
+ char[] variable;
+
+ public:
+
+ bool opEquals(ref SecVar v)
+ {
+ return section == v.section && variable == v.variable;
+ }
+
+ uint toHash()
+ {
+ const auto ID = typeid(char[]);
+ return ID.getHash(§ion) + ID.getHash(&variable);
+ }
+
+ static SecVar opCall(char[] sec, char[] var)
+ {
+ SecVar sv;
+ sv.section = sec;
+ sv.variable = var;
+ return sv;
+ }
+
+ char[] toString()
+ {
+ return "[" ~ section ~ "]:" ~ variable;
+ }
+}
+
+struct IniReader
+{
+ private:
+ HashTable!(SecVar, char[]) vars;
+ char[] section;
+
+ // Start a new section
+ void setSection(char[] sec)
+ {
+ section = sec.dup;
+ }
+
+ // Insert a new value from file
+ void set(char[] variable, char[] value)
+ {
+ vars[SecVar(section, variable.dup)] = value.dup;
+ }
+
+ public:
+
+ void reset()
+ {
+ vars.reset();
+ section = null;
+ }
+
+ int getInt(char[] sec, char[] var, int def)
+ {
+ char[] value;
+ if(vars.inList(SecVar(sec,var), value))
+ return atoi(value);
+ else
+ return def;
+ }
+
+ float getFloat(char[] sec, char[] var, float def)
+ {
+ char[] value;
+ if(vars.inList(SecVar(sec,var), value))
+ return atof(value);
+ else
+ return def;
+ }
+
+ char[] getString(char[] sec, char[] var, char[] def)
+ {
+ char[] value;
+ if(vars.inList(SecVar(sec,var), value))
+ return value;
+ else
+ return def;
+ }
+
+ // Return true if the string matches some case of 'yes', and false
+ // otherwise.
+ bool getBool(char[] sec, char[] var, bool def)
+ {
+ char[] value;
+ if(vars.inList(SecVar(sec,var), value))
+ return icmp(value, "yes") == 0;
+ else
+ return def;
+ }
+
+ void readFile(char[] fn)
+ {
+ // Reset this struct
+ reset();
+
+ // If the file doesn't exist, simply exit. This will work fine,
+ // and default values will be used instead.
+ if(!exists(fn)) return;
+
+ // Read buffer. Finite in size but perfectly safe - the readLine
+ // routine allocates more mem if it needs it.
+ char[300] buffer;
+
+ scope File ini = new File(fn);
+ while(!ini.eof)
+ {
+ char[] line = ini.readLine(buffer);
+
+ // Remove leading and trailing whitespace
+ line = strip(line);
+
+ // Ignore comments and blank lines
+ if(line.length == 0 || line.begins(";")) continue;
+
+ // New section?
+ if(line.begins("["))
+ {
+ if(!line.ends("]"))
+ {
+ //writefln("Malformed section: %s", line);
+ continue;
+ }
+
+ setSection(line[1..$-1]);
+ continue;
+ }
+
+ // Split line into a key and a value
+ int index = line.find('=');
+ if(index != -1)
+ {
+ char[] value = line[index+1..$];
+ line = line[0..index];
+ set(line, value);
+ }
+ //else writefln("Malformed value: '%s'", line);
+ }
+ }
+}
diff --git a/core/memory.d b/core/memory.d
new file mode 100644
index 0000000000..3bb1f414d1
--- /dev/null
+++ b/core/memory.d
@@ -0,0 +1,60 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (memory.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module core.memory;
+
+import util.regions;
+
+// Global memory managers used by various loading routines
+RegionManager
+ esmRegion, // Memory used by ESMs, BSAs and plugins. Cleared only
+ // when all files are reloaded. Lifetime is current
+ // plugin setting session - if we change the set of
+ // plugins, this region is reset and everything is
+ // reloaded.
+
+ gameRegion, // Memory used in current game. Cleared whenever a new
+ // game is loaded. Lifetime is the current game
+ // session, loading a saved game will reset the
+ // region.
+
+ nifRegion; // Used by the NIF loader. Cleared when a NIF file has
+ // been inserted into OGRE and the file data is no
+ // longer needed. In other words this is cleared
+ // immediately after loading each NIF file.
+
+// AA allocator that uses esmRegion
+struct ESMRegionAlloc
+{
+ static const bool autoinit = false;
+ static void* alloc(uint size) { return esmRegion.allocate(size).ptr; }
+ static void free(void* p) { }
+}
+
+void initializeMemoryRegions()
+{
+ // Default block sizes are probably ok
+ esmRegion = new RegionManager("ESM");
+ gameRegion = new RegionManager("GAME");
+ nifRegion = new RegionManager("NIF");
+}
diff --git a/core/resource.d b/core/resource.d
new file mode 100644
index 0000000000..5b622c3f63
--- /dev/null
+++ b/core/resource.d
@@ -0,0 +1,538 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (resource.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module core.resource;
+
+import std.stdio;
+import std.string;
+import std.stream;
+import std.file;
+import std.path;
+
+import monster.util.aa;
+import monster.util.string;
+import util.random;
+
+import bsa.bsafile;
+
+import core.memory;
+import core.config;
+
+import ogre.bindings;
+import ogre.meshloader;
+
+import sound.audio;
+import sound.sfx;
+
+import nif.nif;
+
+import core.filefinder;
+//import core.config;
+
+// Random number generator
+DRand rnd;
+
+// These are handles for various resources. They may refer to a file
+// in the file system, an entry in a BSA archive, or point to an
+// already loaded resource. Resource handles that are not implemented
+// yet are typedefed as ints for the moment.
+typedef int IconIndex;
+
+alias SoundResource* SoundIndex;
+alias TextureResource* TextureIndex;
+alias MeshResource* MeshIndex;
+
+ResourceManager resources;
+
+// Called from ogre/cpp_bsaarchive.cpp. We will probably move these
+// later.
+extern(C)
+{
+ // Does the file exist in the archives?
+ int d_bsaExists(char *filename)
+ {
+ char[] name = toString(filename);
+
+ auto res = resources.lookupTexture(name);
+ if(res.bsaFile != -1)
+ return 1;
+
+ return 0;
+ }
+
+ // Open a file. Return the pointer and size.
+ void d_bsaOpenFile(char *filename,
+ void **retPtr, uint *retSize)
+ {
+ char[] name = toString(filename);
+ void[] result;
+
+ //writefln("calling d_bsaOpenFile(%s, %s)", bsaFile, name);
+
+ auto tex = resources.lookupTexture(name);
+
+ if(tex.bsaFile == -1) result = null;
+ else result = resources.archives[tex.bsaFile].findSlice(tex.bsaIndex);
+ *retPtr = result.ptr;
+ *retSize = result.length;
+ }
+}
+
+struct ResourceManager
+{
+ private:
+ // Holds lists of resources in the file system.
+ FileFinder
+ meshes,
+ icons,
+ textures,
+ sounds,
+ bookart,
+ bsa, esm, esp;
+
+ // Archives
+ BSAFile archives[];
+
+ // List of resources that have already been looked up (case
+ // insensitive)
+ HashTable!(char[], MeshIndex, ESMRegionAlloc, CITextHash) meshLookup;
+ HashTable!(char[], TextureIndex, ESMRegionAlloc, CITextHash) textureLookup;
+ HashTable!(char[], SoundIndex, ESMRegionAlloc, CITextHash) soundLookup;
+
+ public:
+
+ // Hack. Set to true in esmtool to disable all the lookup*
+ // functions.
+ bool dummy = false;
+
+ void initResources()
+ {
+ rnd = new DRand;
+
+ bsa = new FileFinder(config.bsaDir, "bsa", Recurse.No);
+ archives.length = bsa.length;
+ foreach(int i, ref BSAFile f; archives)
+ f = new BSAFile(bsa[i+1]);
+
+ sounds = new FileFinder(config.sndDir);
+
+ // Simple playlists, similar to the Morrowind one. Later I imagine
+ // adding an interactive MP3 player with a bit more finesse, but
+ // this will do for now.
+ char[][] music;
+
+ char[][] getDir(char[] dir)
+ {
+ dir = FileFinder.addSlash(dir);
+ char[][] res = listdir(dir);
+ foreach(ref char[] fn; res)
+ fn = dir ~ fn;
+ return res;
+ }
+
+ jukebox.setPlaylist(getDir(config.musDir));
+ battleMusic.setPlaylist(getDir(config.musDir2));
+
+ meshLookup.reset();
+ textureLookup.reset();
+ soundLookup.reset();
+
+ meshBuffer[0..7] = "meshes\\";
+ texBuffer[0..9] = "textures\\";
+ }
+
+ // These three functions are so similar that I should probably split
+ // out big parts of them into one common function.
+ SoundIndex lookupSound(char[] id)
+ {
+ if(dummy) return null;
+
+ assert(id != "", "loadSound called with empty id");
+
+ SoundIndex si;
+
+ if( soundLookup.inList(id, si) ) return si;
+
+ si = esmRegion.newT!(SoundResource);
+
+ // Check if the file exists
+ int index = sounds[id];
+
+ // If so, get the real file name
+ if(index) si.file = sounds[index];
+ // Otherwise, make this an empty resource
+ else
+ {
+ writefln("Lookup failed to find sound %s", id);
+ si.file = null;
+ }
+
+ si.res.loaded = false;
+
+ // Copy name and insert. We MUST copy here, since indices during
+ // load are put in a temporary buffer, and thus overwritten.
+ si.name = esmRegion.copyz(id);
+ soundLookup[si.name] = si;
+
+ return si;
+ }
+
+ // Quick but effective hack
+ char[80] meshBuffer;
+
+ MeshIndex lookupMesh(char[] id)
+ {
+ if(dummy) return null;
+
+ MeshIndex mi;
+
+ // If it is already looked up, return the result
+ if( meshLookup.inList(id, mi) ) return mi;
+
+ mi = esmRegion.newT!(MeshResource);
+
+ // If not, find it. For now we only check the BSA.
+ char[] search;
+ if(id.length < 70)
+ {
+ // Go to great lengths to avoid the concat :) The speed gain
+ // is negligible (~ 1%), but the GC memory usage is HALVED!
+ // This may mean significantly fewer GC collects during a long
+ // run of the program.
+ meshBuffer[7..7+id.length] = id;
+ search = meshBuffer[0..7+id.length];
+ }
+ else
+ search = "meshes\\" ~ id;
+
+ //writefln("lookupMesh(%s): searching for %s", id, search);
+
+ mi.bsaIndex = -1;
+ mi.bsaFile = -1;
+ foreach(int ind, BSAFile bs; archives)
+ {
+ mi.bsaIndex = bs.getIndex(search);
+ if(mi.bsaIndex != -1) // Found something
+ {
+ mi.bsaFile = ind;
+ break;
+ }
+
+ }
+
+ if(mi.bsaIndex == -1)
+ {
+ writefln("Lookup failed to find mesh %s", search);
+ assert(mi.bsaFile == -1);
+ }
+
+ // Resource is not loaded
+ mi.node = null;
+
+ // Make a copy of the id
+ mi.name = esmRegion.copyz(id);
+ meshLookup[mi.name] = mi;
+
+ return mi;
+ }
+
+ char[80] texBuffer;
+
+ TextureIndex lookupTexture(char[] id)
+ {
+ if(dummy) return null;
+
+ TextureIndex ti;
+
+ // Checked if we have looked it up before
+ if( textureLookup.inList(id, ti) ) return ti;
+
+ // Create a new resource locator
+ ti = esmRegion.newT!(TextureResource);
+
+ ti.name = esmRegion.copyz(id);
+ ti.newName = ti.name;
+ ti.type = ti.name[$-3..$];
+
+ char tmp[];
+ if(id.length < 70)
+ {
+ // See comment in lookupMesh
+ texBuffer[9..9+id.length] = id;
+ tmp = texBuffer[0..9+id.length];
+ }
+ else
+ tmp = "textures\\" ~ id;
+
+ void searchBSAs(char[] search)
+ {
+ // Look it up in the BSA
+ ti.bsaIndex = -1;
+ ti.bsaFile = -1;
+ foreach(int ind, BSAFile bs; archives)
+ {
+ ti.bsaIndex = bs.getIndex(search);
+ if(ti.bsaIndex != -1) // Found something
+ {
+ ti.bsaFile = ind;
+ break;
+ }
+ }
+ }
+
+ searchBSAs(tmp);
+
+ // If we can't find it, try the same filename but with .dds as the
+ // extension. Bethesda did at some point convert all their
+ // textures to dds to improve loading times. However, they did not
+ // update their esm-files or require them to use the correct
+ // extention (if they had, it would have broken a lot of user
+ // mods). So we must support files that are referenced as eg .tga
+ // but stored as .dds.
+ if(ti.bsaIndex == -1 && ti.type != "dds")
+ {
+ tmp[$-3..$] = "dds";
+ searchBSAs(tmp);
+ if(ti.bsaIndex != -1)
+ {
+ // Store the real name in newName.
+ ti.newName = esmRegion.copyz(ti.name);
+
+ // Get a slice of the extension and overwrite it.
+ ti.type = ti.newName[$-3..$];
+ ti.type[] = "dds";
+ }
+ }
+
+ // Check that extensions match, to be on the safe side
+ assert(ti.type == ti.newName[$-3..$]);
+
+ if(ti.bsaIndex == -1)
+ {
+ writefln("Lookup failed to find texture %s", tmp);
+ assert(ti.bsaFile == -1);
+ }
+
+ ti.ml = null;
+
+ textureLookup[ti.name] = ti;
+
+ return ti;
+ }
+
+ IconIndex lookupIcon(char[] id) { return -3; }
+
+ // Inserts a given mesh into ogre. Currently only reads the BSA
+ // file. Is only called from within MeshResource itself, and should
+ // never be called when the mesh is already loaded.
+ private void loadMesh(MeshIndex mi)
+ in
+ {
+ assert(!mi.isLoaded);
+ assert(!mi.isEmpty);
+ }
+ body
+ {
+ // Get a slice of the mesh
+ void[] s = archives[mi.bsaFile].findSlice(mi.bsaIndex);
+
+ // Load the NIF into memory. No need to call close(), the file is
+ // automatically closed when the data is loaded.
+ nifMesh.open(s, mi.name);
+
+ // Load and insert nif
+ // TODO: Might add BSA name to the handle name, for clarity
+ mi.node = meshLoader.loadMesh(mi.name);
+
+ // TODO: We could clear the BSA memory mapping here to free some
+ // mem
+ }
+
+ // Inserts a texture into ogre. Currently it only supports BSA
+ // textures, will later support textures in the file system. OGRE is
+ // able to load these itself, so in that case we can IIRC do
+ // nothing. The tex pointer points to an already existing Texture*
+ // resource in C++.
+ void loadTexture(TextureIndex ti, void *tex)
+ in
+ {
+ assert(ti.isLoaded);
+ assert(!ti.isEmpty);
+ }
+ body
+ {
+ assert(0, "This function is no longer in use");
+
+ void[] s = archives[ti.bsaFile].findSlice(ti.bsaIndex);
+
+ // Insert it into ogre. Note that we are actually being called
+ // from a manual loader at this point, this just dumps the texture
+ // data into an existing OGRE resource.
+ cpp_loadMemImage(ti.name.ptr, ti.type.ptr, s.ptr, s.length, tex);
+ }
+}
+
+struct SoundResource
+{
+ private:
+ char[] name;
+ char[] file;
+ SoundFile res;
+
+ public:
+ char[] getName() { return name; }
+
+ SoundInstance getInstance()
+ in
+ {
+ assert(!isEmpty());
+ }
+ body
+ {
+ if(!isLoaded())
+ res.load(file);
+
+ return res.getInstance();
+ }
+
+ bool isEmpty() { return file == ""; }
+ bool isLoaded() { return res.loaded; }
+}
+
+struct MeshResource
+{
+ private:
+ char[] name;
+ int bsaFile;
+ int bsaIndex;
+
+ // Points to the 'template' SceneNode of this mesh. Is null if this
+ // mesh hasn't been inserted yet.
+ NodePtr node;
+
+ public:
+
+ NodePtr getNode()
+ in
+ {
+ assert(!isEmpty());
+ }
+ body
+ {
+ if(node == null) resources.loadMesh(this);
+ return node;
+ }
+
+ char[] getName() { return name; }
+
+ // Returns true if this resource does not exist (ie. file not found)
+ // TODO: This must be modified later for non-BSA files.
+ bool isEmpty()
+ {
+ return bsaIndex == -1;
+ }
+
+ // Returns true if resource is loaded
+ bool isLoaded()
+ {
+ return node != null;
+ }
+}
+
+struct TextureResource
+{
+ private:
+ char[] name;
+ char[] newName; // Converted name, ie. with extension converted to
+ // .dds if necessary
+ int bsaFile; // If set to -1, the file is in the file system
+ int bsaIndex;
+ char[] type; // Texture format, eg "tga" or "dds";
+ ManualLoader ml;
+
+ public:
+
+ // Insert the texture resource into OGRE. THIS IS NO LONGER NEEDED.
+ void load()
+ in
+ {
+ assert(!isEmpty());
+ }
+ body
+ {
+ writefln("still calling TextureResource.load");
+ if(ml == null) ml = cpp_createTexture(name.ptr, this);
+ }
+
+ char[] getName() { return name; }
+ char[] getNewName() { return newName; }
+
+ // Returns true if this resource does not exist (ie. file not found)
+ bool isEmpty()
+ {
+ return bsaIndex == -1;
+ }
+
+ // Returns true if resource is loaded
+ bool isLoaded()
+ {
+ return ml != null;
+ }
+}
+
+// OLD STUFF
+
+/+
+
+ void initResourceManager()
+ {
+ // Find all resource files
+ char[] morroDir = config.morrowindDirectory();
+ bsa = new FileFinder(morroDir, "bsa");
+
+ // Jump to the data files directory
+ morroDir = config.dataFilesDirectory();
+
+ esm = new FileFinder(morroDir, "esm", Recurse.No);
+ esp = new FileFinder(morroDir, "esp", Recurse.No);
+
+ meshes = new FileFinder(morroDir ~ "Meshes");
+ icons = new FileFinder(morroDir ~ "Icons");
+ textures = new FileFinder(morroDir ~ "Textures");
+ sounds = new FileFinder(morroDir ~ "Sound");
+ bookart = new FileFinder(morroDir ~ "BookArt");
+
+ char[][] bsas = config.bsaArchives();
+ archives.length = bsas.length;
+ writef("Loading BSA archives...");
+
+ writefln(" Done\n");
+ }
+
+ void killBSAs()
+ {
+ foreach(BSAFile f; archives)
+ delete f;
+ }
+}
++/
diff --git a/dsss.conf b/dsss.conf
new file mode 100644
index 0000000000..210e5cbe72
--- /dev/null
+++ b/dsss.conf
@@ -0,0 +1,43 @@
+###########################
+# Main program #
+###########################
+[morro.d]
+# Add libraries and the two C++ object files
+buildflags = -llOgreMain -llaudiere -llOIS cpp_audiere.o cpp_ogre.o
+
+# Make sure the C++ object files are built first
+version(Windows) {
+ prebuild = warn Not designed for Windows yet.
+}
+version(Posix) {
+ prebuild = make ;
+}
+# Make sure we recompile the nif files without the debug output
+prebuild += dsss clean niftool
+
+
+###########################
+# Bsa inspection tool #
+###########################
+[bsa/bsatool.d]
+target=bsatool
+
+###########################
+# Esm inspection tool #
+###########################
+[esm/esmtool.d]
+target=esmtool
+# Because of interdepencies between the ESM code and the resource
+# manager, we have to include all the C++ stuff.
+buildflags = -llOgreMain -llaudiere -llOIS cpp_audiere.o cpp_ogre.o
+
+###########################
+# Nif inspection tool #
+###########################
+[nif/niftool.d]
+target=niftool
+buildflags = -debug=warnstd -debug=check -debug=statecheck -debug=strict -debug=verbose
+# Clean the nif object files to make sure they are recompiled in debug mode
+prebuild = dsss clean niftool
+
+[bored.d]
diff --git a/esm/defs.d b/esm/defs.d
new file mode 100644
index 0000000000..7d96aeb2af
--- /dev/null
+++ b/esm/defs.d
@@ -0,0 +1,137 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (defs.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.defs;
+
+public import std.string;
+public import monster.util.string;
+
+/*
+ * Types and definitions related to parsing esm and esp files
+ */
+
+alias char[4] NAME;
+alias char[32] NAME32;
+alias char[256] NAME256;
+
+union Color
+{
+ align(1) struct
+ {
+ ubyte red, green, blue, alpha;
+ }
+
+ ubyte[4] array;
+ uint value;
+
+ char[] toString() { return format("RGBA:%s", array); }
+}
+static assert(Color.sizeof==4);
+
+// State of a record struct
+enum LoadState
+ {
+ Unloaded, // This record is not loaded, it has just been
+ // referenced.
+ Loaded, // This record has been loaded by the current file
+ Previous // The record has been loaded by a previous file
+
+ // Finalized - might be the case for some record types, but I
+ // don't know if this actual state value would be used for
+ // anything.
+ }
+
+enum VarType { Unknown, None, Short, Int, Long, Float, String, Ignored }
+
+enum SpellSchool : int
+ {
+ Alteration = 0,
+ Conjuration = 1,
+ Destruction = 2,
+ Illusion = 3,
+ Mysticism = 4,
+ Restoration = 5,
+ Length
+ }
+
+enum Attribute : int
+ {
+ Strength = 0,
+ Intelligence = 1,
+ Willpower = 2,
+ Agility = 3,
+ Speed = 4,
+ Endurance = 5,
+ Personality = 6,
+ Luck = 7,
+ Length
+ }
+
+enum SkillEnum : int
+ {
+ Block = 0,
+ Armorer = 1,
+ MediumArmor = 2,
+ HeavyArmor = 3,
+ BluntWeapon = 4,
+ LongBlade = 5,
+ Axe = 6,
+ Spear = 7,
+ Athletics = 8,
+ Enchant = 9,
+ Destruction = 10,
+ Alteration = 11,
+ Illusion = 12,
+ Conjuration = 13,
+ Mysticism = 14,
+ Restoration = 15,
+ Alchemy = 16,
+ Unarmored = 17,
+ Security = 18,
+ Sneak = 19,
+ Acrobatics = 20,
+ LightArmor = 21,
+ ShortBlade = 22,
+ Marksman = 23,
+ Mercantile = 24,
+ Speechcraft = 25,
+ HandToHand = 26,
+ Length
+ }
+
+// Shared between SPEL (Spells), ALCH (Potions) and ENCH (Item
+// enchantments) records
+align(1) struct ENAMstruct
+{
+ // Magical effect
+ short effectID; // ID of magic effect
+
+ // Which skills/attributes are affected (for restore/drain spells etc.)
+ byte skill, attribute; // -1 if N/A
+
+ // Other spell parameters
+ int range; // 0 - self, 1 - touch, 2 - target
+ int area, duration, magnMin, magnMax;
+
+ static assert(ENAMstruct.sizeof==24);
+}
diff --git a/esm/esmmain.d b/esm/esmmain.d
new file mode 100644
index 0000000000..f333557a14
--- /dev/null
+++ b/esm/esmmain.d
@@ -0,0 +1,117 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (esmmain.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.esmmain;
+
+public import esm.records;
+
+/* This file is the main module for loading from ESM, ESP and ESS
+ files. It stores all the data in the appropriate data structures
+ for later referal. TODO: Put this in a class or whatever? Nah, we
+ definately only need one structure like this at any one
+ time. However, we have to deal with unloading and reloading it,
+ even though that should be the exceptional case (change of plugins,
+ etc), not the rule (loading a savegame should not alter the base
+ data set, I think, but it's to early do decide.)*/
+
+// Load a set of esm and esp files. For now, we just traverse in the
+// order given. Later, we should sort these into 'masters' and
+// 'plugins', because esms are always supposed to be loaded
+// first. TODO: I'm not sure if I should load all these in one
+// function. Do we need to be able to respond to errors in each file?
+// Nah, if anything fails, give a general error message, remove the
+// file from the list and try again. We have to be able to get a list
+// of which files depend upon which, though... this can be done before
+// this function is called.
+void loadTESFiles(char[][] files)
+{
+ // Set up all the lists to hold our data
+ initializeLists();
+
+ foreach(char[] filename; files)
+ {
+ esFile.open(filename, esmRegion);
+ while(esFile.hasMoreRecs())
+ {
+ uint flags;
+
+ // Read record header
+ char[] recName = esFile.getRecName();
+ esFile.getRecHeader(flags);
+
+ if(flags & RecordFlags.Unknown)
+ esFile.fail(format("UNKNOWN record flags: %xh", flags));
+
+ loadRecord(recName);
+ }
+
+ // We have to loop through the lists and check for broken
+ // references at this point, if all forward references were
+ // loaded. There might be other end-of-file things to do
+ // also.
+ endFiles();
+ }
+
+ esFile.close();
+
+ // Put all inventory items into one list
+ items.addList(appas, ItemType.Apparatus);
+ items.addList(lockpicks, ItemType.Pick);
+ items.addList(probes, ItemType.Probe);
+ items.addList(repairs, ItemType.Repair);
+ items.addList(lights, ItemType.Light);
+ items.addList(ingreds, ItemType.Ingredient);
+ items.addList(potions, ItemType.Potion);
+ items.addList(armors, ItemType.Armor);
+ items.addList(weapons, ItemType.Weapon);
+ items.addList(books, ItemType.Book);
+ items.addList(clothes, ItemType.Clothing);
+ items.addList(miscItems, ItemType.Misc);
+ items.addList(itemLists, ItemType.ItemList); // Leveled item lists
+
+ // Same with all actors
+ actors.addList(creatures, ItemType.Creature);
+ actors.addList(creatureLists, ItemType.CreatureList);
+ actors.addList(npcs, ItemType.NPC);
+
+ // Finally, add everything that might be looked up in a cell into
+ // one list
+ cellRefs.addList(items);
+ cellRefs.addList(actors);
+ cellRefs.addList(doors, ItemType.Door);
+ cellRefs.addList(activators, ItemType.Activator);
+ cellRefs.addList(statics, ItemType.Static);
+ cellRefs.addList(containers, ItemType.Container);
+
+ // Check that all references are resolved
+ items.endMerge();
+ actors.endMerge();
+ cellRefs.endMerge();
+
+ // Put all NPC dialogues into the hyperlink list
+ foreach(char[] id, ref Dialogue dl; dialogues.names)
+ hyperlinks.add(id, &dl);
+
+ // Finally, sort the hyperlink lists
+ hyperlinks.sort();
+}
diff --git a/esm/esmtool.d b/esm/esmtool.d
new file mode 100644
index 0000000000..1ceb6d4f92
--- /dev/null
+++ b/esm/esmtool.d
@@ -0,0 +1,357 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (esmtool.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.esmtool;
+
+import std.stdio;
+
+import core.memory;
+import esm.esmmain;
+
+import std.utf;
+import std.gc;
+import gcstats;
+
+import monster.util.string;
+
+// Not used, but we have to link it in along with the C++ stuff.
+import input.events;
+
+void poolSize()
+{
+ GCStats gc;
+ getStats(gc);
+ writefln("Pool size: ", comma(gc.poolsize));
+ writefln("Used size: ", comma(gc.usedsize));
+}
+
+//alias int[Dialogue*] TopicList;
+
+void main(char[][] args)
+{
+ char[][] files;
+ bool raw;
+
+ bool scptList; // List scripts
+ bool scptShow; // Show a script
+ char[] scptName; // Script to show
+
+ bool ciList; // List interior cells
+ bool ceList; // List exterior cells that have names
+
+ bool weList; // List weapons
+
+ bool numbers; // List how many there are of each record type
+
+ foreach(char[] a; args[1..$])
+ if(a == "-r") raw = true;
+
+ else if(a == "-sl") scptList = true;
+ else if(a == "-s") scptShow = true;
+ else if(scptShow && scptName == "") scptName = a;
+
+ else if(a == "-cil") ciList = true;
+ else if(a == "-cel") ceList = true;
+
+ else if(a == "-wl") weList = true;
+
+ else if(a == "-n") numbers = true;
+
+ else if(a.begins("-")) writefln("Ignoring unknown option %s", a);
+ else files ~= a;
+
+ int help(char[] msg)
+ {
+ writefln("%s", msg);
+ writefln("Syntax: %s [options] esm-file [esm-file ... ]", args[0]);
+ writefln(" Options:");
+ writefln(" -r Display all records in raw format");
+ writefln(" -n List the number of each record");
+ writefln(" -sl List scripts");
+ writefln(" -s name Show given script");
+ writefln(" -cil List interior cells");
+ writefln(" -cel List exterior cells with names");
+ writefln(" -wl List weapons");
+ return 1;
+ }
+ if(files.length == 0) return help("No input files given");
+
+ if(scptShow && scptName == "") return help("No script name given");
+
+ initializeMemoryRegions();
+
+ if(raw)
+ {
+ foreach(int fileNum, char[] filename; files)
+ {
+ try
+ {
+ esFile.open(filename, esmRegion);
+ printRaw();
+ }
+ catch(Exception e)
+ {
+ try {writefln(e);}
+ catch {}
+ writefln("Error on file %s", filename);
+ }
+ catch
+ {
+ writefln("Error: Unkown failure on file %s", filename);
+ }
+ }
+ return;
+ }
+
+ // Disable resource lookups.
+ resources.dummy = true;
+
+ try loadTESFiles(files);
+ catch(Exception e)
+ {
+ try {writefln(e);}
+ catch {writefln("(Invalid UTF in error message)");}
+ }
+ catch { writefln("Error: Unkown failure"); }
+
+ // List weapons
+ if(weList) foreach(n, m; weapons.names)
+ {
+ alias Weapon.Type WT;
+ switch(m.data.type)
+ {
+ case WT.ShortBladeOneHand: writef("Short Sword"); break;
+ case WT.LongBladeOneHand: writef("Long Sword, One-Handed"); break;
+ case WT.LongBladeTwoHand: writef("Long Sword, Two-Handed"); break;
+ case WT.BluntOneHand: writef("Blunt, One-Handed"); break;
+ case WT.BluntTwoClose: writef("Blunt, Two-Handed"); break;
+ case WT.BluntTwoWide: writef("Blunt, Two-Handed Wide"); break;
+ case WT.SpearTwoWide: writef("Spear, Two-Handed"); break;
+ case WT.AxeOneHand: writef("Axe, One-Handed"); break;
+ case WT.AxeTwoHand: writef("Axe, Two-Handed"); break;
+ case WT.MarksmanBow: writef("Bow"); break;
+ case WT.MarksmanCrossbow: writef("Crossbow"); break;
+ case WT.MarksmanThrown: writef("Thrown weapon"); break;
+ case WT.Arrow: writef("Arrow"); break;
+ case WT.Bolt: writef("Bolt"); break;
+ }
+ writefln(" id '%s': name '%s'", n, m.name);
+
+ if(m.data.flags & Weapon.Flags.Magical)
+ writefln("Magical");
+ if(m.data.flags & Weapon.Flags.Silver)
+ writefln("Silver");
+
+ writefln("Weight: ", m.data.weight);
+ writefln("Value: ", m.data.value);
+ writefln("Health: ", m.data.health);
+ writefln("Speed: ", m.data.speed);
+ writefln("Reach: ", m.data.reach);
+ writefln("Enchantment points: ", m.data.enchant);
+ writefln("Combat: ", m.data.chop, m.data.slash, m.data.thrust);
+
+ if(m.enchant) writefln("Has enchantment '%s'", m.enchant.id);
+ if(m.script) writefln("Has script '%s'", m.script.id);
+
+ writefln();
+ }
+
+ if(numbers)
+ {
+ writefln("Activators: ", activators.length);
+ writefln("Doors: ", doors.length);
+ writefln("Globals: ", globals.length);
+ writefln("Sounds: ", sounds.length);
+ writefln("Game Settings: ", gameSettings.length);
+ writefln("Factions: ", factions.length);
+ writefln("Statics: ", statics.length);
+ writefln("Spells: ", spells.length);
+ writefln("Potions: ", potions.length);
+ writefln("Apparatus: ", appas.length);
+ writefln("Armors: ", armors.length);
+ writefln("Body parts: ", bodyParts.length);
+ writefln("Enchantments: ", enchants.length);
+ writefln("Books: ", books.length);
+ writefln("Birth signs: ", birthSigns.length);
+ writefln("Land texture files: ", landTextures.length);
+ writefln("Weapons: ", weapons.length);
+ writefln("Lockpicks: ", lockpicks.length);
+ writefln("Probes: ", probes.length);
+ writefln("Repairs: ", repairs.length);
+ writefln("Cells: ", cells.length);
+ writefln(" Interior: ", cells.numInt);
+ writefln(" Exterior: ", cells.numExt);
+ writefln("Regions: ", regions.length);
+ writefln("Lights: ", lights.length);
+ writefln("Skills: ", skills.length);
+ writefln("Sound generators: ", soundGens.length);
+ writefln("Races: ", races.length);
+ writefln("Misc items: ", miscItems.length);
+ writefln("Cloths: ", clothes.length);
+ writefln("Ingredients: ", ingreds.length);
+ writefln("Classes: ", classes.length);
+ writefln("Containers: ", containers.length);
+ writefln("Creatures: ", creatures.length);
+ writefln("Leveled item lists: ", itemLists.length);
+ writefln("Leveled creature lists: ", creatureLists.length);
+ writefln("NPCs: ", npcs.length);
+ writefln("Scripts: ", scripts.length);
+ writefln("Dialogues: ", dialogues.length);
+ writefln("Hyperlinks: ", hyperlinks.list.length);
+ writefln("Start scripts: ", startScripts.length);
+ writefln("\nTotal items: ", items.length);
+ writefln("Total actors: ", actors.length);
+ writefln("Total cell placable items: ", cellRefs.length);
+ }
+ if(scptList) foreach(a, b; scripts.names) writefln(a);
+ if(ciList) foreach(a, b; cells.in_cells) writefln(a);
+ if(ceList)
+ foreach(uint i, c; .cells.ex_cells)
+ {
+ int x, y;
+ CellList.decompound(i, x, y);
+ if(c.name.length)
+ writefln("%s,%s: %s", x, y, c.name);
+ }
+
+ if(scptShow)
+ {
+ Script *p = scripts.names.lookup(scptName);
+ if(p)
+ writefln("Script '%s', text is:\n-------\n%s\n-------", p.id, p.scriptText);
+ else writefln("Script '%s' not found", scptName);
+ writefln();
+ }
+
+ writefln(esmRegion);
+
+ poolSize();
+}
+
+// Quick function that simply iterates through an ES file and prints
+// out all the records and subrecords. Some of this code is really old
+// (about 2004-2005)
+void printRaw()
+{
+ with(esFile)
+ {
+ // Variable length integer (this only works for unsigned ints!)
+ ulong getHVUint()
+ {
+ ulong l;
+
+ getSubHeader();
+ if( (getSubSize != 4) &&
+ (getSubSize != 2) &&
+ (getSubSize != 8) )
+ fail(format("Unknown integer size: ", getSubSize));
+
+ readExact(&l, getSubSize);
+ return l;
+ }
+
+ writefln("Filename: ", getFilename);
+ writef("Filetype: ");
+ switch(getFileType())
+ {
+ case FileType.Plugin: writefln("Plugin"); break;
+ case FileType.Master: writefln("Master"); break;
+ case FileType.Savegame: writefln("Savegame"); break;
+ case FileType.Unknown: writefln("Unknown"); break;
+ }
+ writef("Version: ");
+ if(isVer12()) writefln("1.2");
+ else if(isVer13()) writefln("1.3");
+ else writefln("Unknown");
+
+ writefln("Records: ", getRecords);
+ writefln("Master files:");
+ for(int i; i
+ WWW: http://openmw.snaptoad.com/
+
+ This file (filereader.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.filereader;
+
+private:
+import std.stdio;
+import std.stream;
+import std.string;
+
+import util.regions;
+
+import core.resource;
+
+import esm.listkeeper;
+import esm.defs;
+
+public:
+
+/*
+ * Exception class for TES3File
+ */
+
+class TES3FileException: Exception
+{
+ this(char[] msg) {super("Error reading TES3 file: " ~ msg);}
+ this() {this("Unknown error");}
+}
+
+// Some flags are in use that we don't know. But we don't really know
+// any of them.
+enum RecordFlags : uint
+ {
+ Flag6 = 0x20, // Eg. adventurers_v2.0.esp (only once per file?)
+ Persistent = 0x400,
+ Flag13 = 0x1000, // Eg. Astarsis_BR.esm (several times per file?)
+ Blocked = 0x2000,
+
+ Unknown = 0xffffffff - 0x3420
+ }
+
+enum FileType
+ {
+ Unknown,
+ Esp, Plugin = Esp,
+ Esm, Master = Esm,
+ Ess, Savegame = Ess
+ }
+
+// Special files
+enum SpecialFile
+ {
+ Other,
+ Morrowind,
+ Tribunal,
+ Bloodmoon
+ }
+
+enum Version { Unknown, v12, v13 }
+
+// This struct should contain enough data to put a TES3File object
+// back into a specific file position and state. We use it to save the
+// "position" of objects in a file (eg. a cell), so we can return
+// there later and continue where we stopped (eg. when we want to load
+// that specific cell.)
+struct TES3FileContext
+{
+ char[] filename;
+ uint leftFile, leftRec, leftSub;
+ NAME recName, subName;
+ FileType type;
+ Version ver;
+
+ ulong filepos;
+}
+
+/**
+ * Instance used to read TES3 files. Since we will only be reading one
+ * file at a time, we might as well make one global instance.
+ */
+TES3File esFile;
+
+/**
+ * This struct reads an Elder Scrolls 3 file (esp, esm or ess)
+ *
+ * Makes heavy use of private variables to represent current
+ * state.
+ *
+ * Relevant exceptions are
+ * TES3FileException - error interpreting file
+ * StreamFileException - file IO error
+ */
+struct TES3File
+{
+ private:
+ BufferedFile file;// Input file
+
+ // These are only used by getRecHeader and getSubHeader for
+ // asserting the file's integrity.
+ uint leftFile; // Number of unread bytes in file
+ uint leftRec; // Number of unread bytes in record
+
+ // This is used by sub-record readers for integrity checking.
+ uint leftSub; // Number of bytes in subrecord
+
+ // Name of current record and current sub-record.
+ NAME recName, subName;
+
+ char[] filename; // Filename
+ FileType type; // File type
+ Version ver; // File format version
+ char[] author; // File author (max 32 bytes (with null?))
+ char[] desc; // Description (max 256 bytes (ditto?))
+ uint records; // Number of records in the file (doesn't seem to be right?)
+ SpecialFile spf; // Is this a file we have to treat in a special way?
+
+ struct _mast
+ {
+ char[] name; // File name of an esm master for this file
+ ulong size; // The master file's size in bytes (used for
+ // version control)
+ }
+
+ // List of esm masters for this file. For savegames this list also
+ // contains all plugins.
+ _mast masters[];
+
+ // TES3.HEDR, file header struct
+ align(1) struct HEDRstruct
+ {
+ union
+ {
+ float ver; // File format version, 1.2 and 1.3 supported.
+ uint verHex; // 1.2 = 0x3f99999a, 1.3 = 0x3fa66666
+ }
+ int type; // 0=esp, 1=esm, 32=ess
+ NAME32 author; // Author's name
+ NAME256 desc; // File description blurb
+ uint records; // Number of records in file (?)
+ }
+
+ static assert(HEDRstruct.sizeof == 300);
+
+ // Which memory region to use for allocations.
+ RegionManager region;
+
+ public:
+
+ // Get file information
+ char[] getFilename() { return filename; }
+ ulong getFileSize() { return file.size; }
+ ulong getPosition() { return file.position; }
+ SpecialFile getSpecial() { return spf; }
+
+ char[] retSubName() { return subName; }
+
+ bool isVer12() { return ver == Version.v12;}
+ bool isVer13() { return ver == Version.v13;}
+ FileType getFileType() { return type; }
+ _mast[] getMasters() { return masters; }
+ uint getRecords() { return records; }
+ char[] getAuthor() { return author; }
+ RegionManager getRegion() { return region; }
+
+ // Store the current file state (position, file name, version, debug
+ // info). The info should be enough to get us back on track for
+ // reading from a file, without having to reread the header or any
+ // previous records.
+ void getContext(ref TES3FileContext c)
+ {
+ c.filename = filename;
+ c.leftFile = leftFile;
+ c.leftRec = leftRec;
+ c.leftSub = leftSub;
+ c.recName[] = recName;
+ c.subName[] = subName;
+ c.type = type;
+ c.ver = ver;
+ c.filepos = file.position;
+ }
+
+ // Opens the file if it is not already opened. A region manager has
+ // to be specified.
+ void restoreContext(TES3FileContext c, RegionManager r)
+ {
+ if(filename != c.filename)
+ openFile(c.filename, r);
+ file.seekSet(c.filepos);
+
+ // File is now open, copy state information
+ filename = c.filename;
+ leftFile = c.leftFile;
+ leftRec = c.leftRec;
+ leftSub = c.leftSub;
+ recName[] = c.recName;
+ subName[] = c.subName;
+ type = c.type;
+ ver = c.ver;
+ }
+
+ // Open a new file and assign a region
+ private void openFile(char[] filename, RegionManager r)
+ {
+ close();
+ debug writefln("Opening file");
+ if(file is null) file = new BufferedFile(new File());
+ file.open(filename);
+
+ region = r;
+ }
+
+ void open(char[] filename, RegionManager r)
+ {
+ uint flags;
+
+ debug writefln("openFile(%s, %s)", filename, r);
+ openFile(filename, r);
+
+ if(iEnds(filename, "Morrowind.esm")) spf = SpecialFile.Morrowind;
+ else if(iEnds(filename, "Tribunal.esm")) spf = SpecialFile.Tribunal;
+ else if(iEnds(filename, "Bloodmoon.esm")) spf = SpecialFile.Bloodmoon;
+ else spf = SpecialFile.Other;
+
+ debug writefln("Reading header");
+
+ this.filename = filename;
+
+ // Make a copy so the file name is not overwritten (not really needed?)
+ // this.filename = region.copy(filename);
+
+ // Oops, this was bad. The file name ends up in various
+ // TES3FileContext structs, allocated outside the GC's reach. A
+ // .dup'ed copy might be released and overwritten.
+ // this.filename = filename.dup;
+
+ leftFile = file.size;
+
+ // First things first
+ if(getRecName() != "TES3")
+ fail("Not a valid Morrowind file");
+
+ // Record header
+ getRecHeader(flags);
+ if(flags)
+ writefln("WARNING: Header flags are non-zero");
+
+ // Read and analyse the header data
+ HEDRstruct hedr;
+ readHNExact(&hedr, hedr.sizeof, "HEDR");
+
+ // The float hedr.ver signifies the file format version. It can
+ // take on these two values:
+ // 0x3f99999a = 1.2
+ // 0x3fa66666 = 1.3
+ if( hedr.verHex == 0x3f99999a )
+ ver = Version.v12;
+ else if( hedr.verHex == 0x3fa66666 )
+ ver = Version.v13;
+ else
+ {
+ ver = Version.Unknown;
+ writefln("WARNING: Unknown version: ", hedr.ver);
+ writefln(" Hex: %X h", *(cast(uint*)&hedr.ver));
+ }
+
+ switch(hedr.type)
+ {
+ case 0: type = FileType.Esp; break;
+ case 1: type = FileType.Esm; break;
+ case 32: type = FileType.Ess; break;
+ default:
+ type = FileType.Unknown;
+ writefln("WARNING: Unknown file type: ", hedr.type);
+ }
+
+ author = region.copy(stripz(hedr.author));
+ desc = region.copy(stripz(hedr.desc));
+ records = hedr.records;
+
+ masters = null;
+ // Reads a MAST and a DATA fields
+ while(isNextSub("MAST"))
+ {
+ _mast ma;
+
+ // MAST entry - master file name
+ ma.name = getHString();
+
+ // DATA entry - master file size
+ ma.size = getHNUlong("DATA");
+
+ // Add to the master list!
+ masters ~= ma;
+ }
+
+ if(type == FileType.Savegame)
+ {
+ // What are these again? I don't remember.
+ getSubNameIs("GMDT");
+ skipHSubSize(124);
+ getSubNameIs("SCRD");
+ skipHSubSize(20);
+
+ // Screenshot, used for save games.
+ getSubNameIs("SCRS");
+ skipHSubSize(65536);
+ }
+ }
+
+ // Close the file. We do not clear any object data at this point.
+ void close()
+ {
+ debug writefln("close()");
+ if(file !is null)
+ file.close();
+ leftFile = leftRec = leftSub = 0;
+ debug writefln("Clearing strings");
+
+ recName[] = '\0';
+ subName[] = '\0';
+
+ // This tells restoreContext() that we have to reopen the file
+ filename = null;
+
+ debug writefln("exit close()");
+ }
+
+ /*
+ * Error reporting
+ */
+
+ void fail(char[] msg)
+ {
+ throw new TES3FileException
+ (msg ~ "\nFile: " ~ filename ~ "\nRecord name: " ~ recName
+ ~ "\nSubrecord name: " ~ subName);
+ }
+
+ /************************************************************************
+ *
+ * Highest level readers, reads a name and looks it up in the given
+ * list.
+ *
+ ************************************************************************/
+
+ // This should be more than big enough for references.
+ private char lookupBuffer[200];
+
+ char[] tmpHString()
+ {
+ getSubHeader();
+
+ // Use this to test the difference in memory consumption.
+ //return getString(region.getString(leftSub));
+ return getString(lookupBuffer[0..leftSub]);
+ }
+
+ // These are used for file lookups
+ MeshIndex getMesh()
+ { getSubNameIs("MODL"); return resources.lookupMesh(tmpHString()); }
+ SoundIndex getSound()
+ { getSubNameIs("FNAM"); return resources.lookupSound(tmpHString()); }
+ IconIndex getIcon(char[] s = "ITEX")
+ { getSubNameIs(s); return resources.lookupIcon(tmpHString()); }
+ TextureIndex getTexture()
+ { getSubNameIs("DATA"); return resources.lookupTexture(tmpHString()); }
+
+ // The getO* functions read optional records. If they are not
+ // present, return null.
+
+ MeshIndex getOMesh()
+ { return isNextSub("MODL") ? resources.lookupMesh(tmpHString()) : MeshIndex.init; }
+ /*
+ SoundIndex getOSound()
+ { return isNextSub("FNAM") ? resources.lookupSound(tmpHString()) : SoundIndex.init; }
+ */
+ IconIndex getOIcon()
+ { return isNextSub("ITEX") ? resources.lookupIcon(tmpHString()) : IconIndex.init; }
+ TextureIndex getOTexture(char[] s="TNAM")
+ { return isNextSub(s) ? resources.lookupTexture(tmpHString()) : TextureIndex.init; }
+
+ // Reference with name s
+ template getHNPtr(Type)
+ {
+ Type* getHNPtr(char[] s, ListKeeper list)
+ { getSubNameIs(s); return cast(Type*) list.lookup(tmpHString()); }
+ }
+
+ // Reference, only get header
+ template getHPtr(Type)
+ {
+ Type* getHPtr(ListKeeper list)
+ { return cast(Type*) list.lookup(tmpHString()); }
+ }
+
+ // Optional reference with name s
+ template getHNOPtr(Type)
+ {
+ Type* getHNOPtr(char[] s, ListKeeper list)
+ { return isNextSub(s) ? cast(Type*)list.lookup(tmpHString()) : null; }
+ }
+
+ /************************************************************************
+ *
+ * Somewhat high level reading methods. Knows about headers and
+ * leftFile/leftRec/leftSub.
+ *
+ ************************************************************************/
+
+ // "Automatic" versions. Sets and returns recName and subName and
+ // updates leftFile/leftRec.
+ char[] getRecName()
+ {
+ if(!hasMoreRecs())
+ fail("No more records, getRecName() failed");
+ getName(recName);
+ leftFile-= 4;
+ return recName;
+ }
+
+ // This is specially optimized for LoadINFO
+ bool isEmptyOrGetName()
+ {
+ if(leftRec)
+ {
+ file.readBlock(subName.ptr, 4);
+ leftRec -= 4;
+ return false;
+ }
+ return true;
+ }
+
+ // I've tried to optimize this slightly, since it gets called a LOT.
+ void getSubName()
+ {
+ if(leftRec <= 0)
+ fail("No more sub-records, getSubName() failed");
+
+ // Don't bother with error checking, we will catch an EOF upon
+ // reading the subrecord data anyway.
+ file.readBlock(subName.ptr, 4);
+
+ leftRec -= 4;
+ }
+
+ // We often expect a certain subrecord type, this makes it easy to
+ // check.
+ void getSubNameIs(char[] s)
+ {
+ getSubName();
+ if( subName != s )
+ fail("Expected subrecord "~s~" but got "~subName);
+ }
+
+ // Checks if the next sub-record is called s. If it is, run
+ // getSubName, if not, return false.
+ bool isNextSub(char[] s)
+ {
+ if(!leftRec) return false;
+
+ getName(subName);
+ if(subName != s)
+ {
+ file.seekCur(-4);
+ return false;
+ }
+ leftRec -= 4;
+
+ //getSubName();
+ return true;
+ }
+
+ // Same as isNextSub, only it works on records instead of
+ // sub-records. It also loads the record header.
+ bool isNextHRec(char[] s)
+ {
+ if(!leftFile) return false;
+ getName(recName);
+ if(recName != s)
+ {
+ file.seekCur(-4);
+ return false;
+ }
+ leftFile -= 4;
+
+ uint flags;
+ getRecHeader(flags);
+
+ return true;
+ }
+
+ bool hasMoreSubs() { return leftRec > 0; }
+ bool hasMoreRecs() { return leftFile > 0; }
+
+ // Remaining size of current record
+ uint getRecLeft() { return leftRec; }
+ // Size of current sub record
+ uint getSubSize() { return leftSub; }
+
+ // Skip the rest of this record
+ void skipRecord()
+ {
+ file.seekCur(leftRec);
+ leftRec = 0;
+ }
+
+ // Skip current sub record and return size
+ uint skipHSub()
+ {
+ getSubHeader();
+ file.seekCur(leftSub);
+ return leftSub;
+ }
+
+ // Skip sub record and check it's size
+ void skipHSubSize(uint size)
+ {
+ getSubHeader();
+ if(leftSub != size)
+ fail(format("Size mismatch: got %d, wanted %d", leftSub, size));
+ file.seekCur(leftSub);
+ }
+
+ // These read an entire sub-record, including the header. They also
+ // adjust and check leftSub and leftRecord variables through calling
+ // getSubHeader().
+ void readHExact(void * p, uint size)
+ {
+ getSubHeader();
+ if(leftSub != size)
+ fail(format("Size mismatch: got %d, wanted %d", leftSub, size));
+ readExact(p, leftSub);
+ }
+
+ template TgetHType(T)
+ { T TgetHType() { T t; readHExact(&t, t.sizeof); return t;} }
+
+ // To make these easier to use (and to further distinguish them from
+ // the above "raw" versions), these return their value instead of
+ // using an ref argument.
+ alias TgetHType!(uint) getHUint;
+ alias TgetHType!(int) getHInt;
+ alias TgetHType!(float) getHFloat;
+ alias TgetHType!(ulong) getHUlong;
+ alias TgetHType!(byte) getHByte;
+
+ // Reads a string sub-record, including header
+ char[] getHString()
+ {
+ getSubHeader();
+
+ // Hack to make MultiMark.esp load
+ if(leftSub == 0)
+ {
+ // Skip the following zero byte
+ leftRec--;
+ assert(file.getc() == 0);
+ // Report this by setting a flag or something?
+ return null;
+ }
+
+ return getString(region.getString(leftSub));
+ }
+
+ // Other quick aliases (this is starting to get messy)
+ // Get string sub record string with name s
+ char[] getHNString(char[] s)
+ { getSubNameIs(s); return getHString(); }
+
+ // Get optional sub record string with name s
+ char[] getHNOString(char[] s)
+ { return isNextSub(s) ? getHString() : null; }
+
+ template TgetHNType(T)
+ { T TgetHNType(char[] s) { T t; readHNExact(&t, t.sizeof, s); return t;} }
+
+ template TgetHNOType(T)
+ {
+ T TgetHNOType(char[] s, T def)
+ {
+ if(isNextSub(s))
+ {
+ T t;
+ readHExact(&t, t.sizeof);
+ return t;
+ }
+ else return def;
+ }
+ }
+
+ alias TgetHNType!(uint) getHNUint;
+ alias TgetHNType!(int) getHNInt;
+ alias TgetHNType!(float) getHNFloat;
+ alias TgetHNType!(ulong) getHNUlong;
+ alias TgetHNType!(byte) getHNByte;
+ alias TgetHNType!(short) getHNShort;
+ alias TgetHNType!(byte) getHNByte;
+
+ alias TgetHNOType!(float) getHNOFloat;
+ alias TgetHNOType!(int) getHNOInt;
+ alias TgetHNOType!(byte) getHNOByte;
+
+ void readHNExact(void* p, uint size, char[] s)
+ { getSubNameIs(s); readHExact(p,size); }
+
+ // Record header
+ // This updates the leftFile variable BEYOND the data that follows
+ // the header, ie beyond the entire record. You are supposed to use
+ // the leftRec variable when reading record data.
+ void getRecHeader(out uint flags)
+ {
+ // General error checking
+ if(leftFile < 12)
+ fail("End of file while reading record header");
+ if(leftRec)
+ fail(format("Previous record contains %d unread bytes", leftRec));
+
+ getUint(leftRec);
+ getUint(flags);// This header entry is always zero
+ assert(flags == 0);
+ getUint(flags);
+ leftFile -= 12;
+
+ // Check that sizes add up
+ if(leftFile < leftRec)
+ fail(format(leftFile, " bytes left in file, but next record contains ",
+ leftRec," bytes"));
+
+ // Adjust number of bytes left in file
+ leftFile -= leftRec;
+ }
+
+ // Sub-record head
+ // This updates leftRec beyond the current sub-record as
+ // well. leftSub contains size of current sub-record.
+ void getSubHeader()
+ {
+ if(leftRec < 4)
+ fail("End of record while reading sub-record header");
+
+ if(file.readBlock(&leftSub, 4) != 4)
+ fail("getSubHeader could not read header length");
+
+ leftRec -= 4;
+
+ // Adjust number of record bytes left
+ leftRec -= leftSub;
+
+ // Check that sizes add up
+ if(leftRec < 0)
+ fail(format(leftRec+leftSub,
+ " bytes left in record, but next sub-record contains ",
+ leftSub," bytes"));
+ }
+
+ void getSubHeaderIs(uint size)
+ {
+ getSubHeader();
+ if(leftSub != size)
+ fail(format("Expected header size to be ", size, ", not ", leftSub));
+ }
+
+ /*************************************************************************
+ *
+ * Low level reading methods
+ *
+ *************************************************************************/
+
+ /// Raw data of any size
+ void readExact(void *buf, uint size)
+ {
+ assert(size != 0);
+ file.readExact(buf,size);
+ }
+
+ // One byte
+ void getByte(out byte b) { file.read(b); }
+ void getUByte(out ubyte b) { file.read(b); }
+ // Two bytes
+ void getUShort(out ushort s) { file.read(s); }
+ // Four bytes
+ void getUint(out uint u) { file.read(u); }
+ void getInt(out int i) { file.read(i); }
+ void getFloat(out float f) { file.read(f); }
+ // Eight bytes
+ void getUlong(out ulong l) { file.read(l); }
+
+ // Get a record or subrecord name, four bytes
+ void getName(NAME name)
+ {
+ file.readBlock(name.ptr, 4);
+ /*
+ if(file.readBlock(name.ptr, 4) != 4)
+ fail("getName() could not find more data");
+ */
+ }
+
+ // Fill buffer of predefined length. If actual string is shorter
+ // (ie. null terminated), the buffer length is set
+ // accordingly. Chopped string is returned.
+ char[] getString(char[] str)
+ {
+ if(str.length != file.readBlock(str.ptr,str.length))
+ fail("getString() could not find enough data in stream");
+
+ str = stripz(str);
+ return str;
+ }
+
+ // Use this to allocate and read strings of predefined length
+ char[] getString(int l)
+ {
+ char[] str = region.getString(l);
+ return getString(str);
+ }
+}
diff --git a/esm/imports.d b/esm/imports.d
new file mode 100644
index 0000000000..b6cfd1d22e
--- /dev/null
+++ b/esm/imports.d
@@ -0,0 +1,44 @@
+module esm.imports;
+
+/* This is a file that imports common modules used by the load*.d
+ record loaders. It is really a cut down version of the start of
+ records.d.
+
+ This file MUST NOT import records.d - directly or indirectly -
+ because that will trigger a nice three page long list of template
+ forwarding errors from the compiler.
+
+ What happens is that when DMD/GDC compiles one of the load* files,
+ it is forced to read records.d first (since it is an imported
+ module) - but then it sees a template that referes to a struct in
+ the current load* file, before that struct is defined. Curriously
+ enough, DMD has no problems when you specify all the source files
+ on the command line simultaneously. This trick doesn't work with
+ GDC though, and DSSS doesn't use it either.
+
+ This file was created to work around this compiler bug.
+*/
+
+public
+{
+import esm.defs;
+import esm.filereader;
+import esm.listkeeper;
+
+import core.resource;
+import core.memory;
+
+import util.regions;
+import monster.util.aa;
+
+import std.stdio;
+import std.string;
+
+alias RegionBuffer!(ENAMstruct) EffectList;
+
+// Records that are cross referenced often
+import esm.loadscpt;
+import esm.loadsoun;
+import esm.loadspel;
+import esm.loadench;
+}
diff --git a/esm/listkeeper.d b/esm/listkeeper.d
new file mode 100644
index 0000000000..1040d98cc9
--- /dev/null
+++ b/esm/listkeeper.d
@@ -0,0 +1,367 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (listkeeper.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.listkeeper;
+
+import monster.util.aa;
+
+import core.memory;
+
+import esm.filereader;
+import esm.defs;
+
+import std.stdio;
+
+// Item types, used in the lookup table for inventory items, creature
+// lists and leveled lists. We also use it for all types of references
+// that can exist in cells.
+enum ItemType
+ {
+ // Items
+ None = 0, Potion, Apparatus, Armor, Weapon, Book, Clothing,
+ Light, Ingredient, Pick, Probe, Repair, Misc, ItemList,
+
+ // Used for creature lists
+ Creature, CreatureList, NPC,
+
+ // Other cell references
+ Door, Activator, Static, Container//, SoundGen
+ }
+
+abstract class ListKeeper
+{
+ int listIndex;
+
+ new(uint size)
+ {
+ return esmRegion.allocate(size).ptr;
+ }
+
+ delete(void *p) { assert(0); }
+
+ this()
+ {
+ // Store our index for later use
+ listIndex = recordLists.length;
+
+ // Add the class to the global list
+ recordLists ~= this;
+ }
+
+ // Load a record from a master or plugin file
+ void load();
+
+ // Looks up a reference. If it does not exist it is assumed to be a
+ // forward reference within a file, and is inserted.
+ void* lookup(char[] s);
+
+ // Tell the loader that current file has ended, so it can do things
+ // like check that all referenced objects have been loaded.
+ void endFile();
+
+ // Number of inserted elements
+ uint length();
+
+ void addToList(ref ItemBaseList l, ItemType t) { assert(0); }
+}
+
+ListKeeper recordLists[];
+
+// Keep the list of Type structures for records where the first
+// subrecord is an id string called NAME. This id is used for
+// lookup. Although almost all lookups match in case, there are a few
+// sounds that don't, so we treat these id lookups as generally case
+// insensitive. This hasn't posed any problems so far.
+class ListID(Type) : ListKeeper
+{
+ HashTable!(char[], Type, ESMRegionAlloc, CITextHash) names;
+
+ this(uint size)
+ {
+ names = names.init;
+ if(size) names.rehash(size);
+ }
+
+ // Reads the id for this header. Override if the id is not simply
+ // getHNString("NAME")
+ char[] getID()
+ {
+ return esFile.getHNString("NAME");
+ }
+
+ // Load a record from a master of plugin file
+ void load()
+ {
+ assert(esFile.getFileType == FileType.Esm ||
+ esFile.getFileType == FileType.Esp);
+
+ // Get the identifier of this record
+ char[] id = getID();
+
+ // Get pointer to a new or existing object.
+ Type *p;
+ if(names.insertEdit(id, p))
+ // A new item was inserted
+ {
+ p.state = LoadState.Unloaded;
+ p.id = id;
+ p.load();
+ p.state = LoadState.Loaded;
+ }
+ else
+ // Item already existed, either from a previous file or as a
+ // forward reference from this file. Load on top of it. The
+ // LoadState tells the struct whether it contains loaded data.
+ {
+ if(p.state == LoadState.Loaded)
+ // Make a special case for this, perhaps, or just ignore it.
+ writefln("WARNING: Duplicate record in file %s: '%s'",
+ esFile.getFilename(), id);
+ //esFile.fail("Duplicate record in file: '" ~ id ~ "'");
+
+ assert(icmp(p.id, id) == 0);
+ p.load();
+ p.state = LoadState.Loaded;
+ }
+ }
+
+ // Looks up a reference. If it does not exist it is assumed to be a
+ // forward reference within a file, and is inserted.
+ void* lookup(char[] id)
+ {
+ if(!id.length) return null; // Empty reference
+
+ Type *p = names.lookup(id);
+ // Is the value in the list?
+ if(!p)
+ // No, assume it is a forward reference.
+ {
+ // Since the lookup name is stored in an internal buffer in
+ // esFile, we have to copy it.
+ id = esmRegion.copy(id);
+
+ // To avoid copying the string on every lookup, we have to
+ // insert in a separate step. But a double lookup isn't
+ // really THAT expensive. Besides, my tests show that this
+ // is used in less than 10% of the cases.
+ names.insertEdit(id, p);
+ p.id = id;
+ p.state = LoadState.Unloaded;
+ }
+ return cast(void*)p;
+ }
+
+ // Check that all referenced objects are actually loaded.
+ void endFile()
+ in
+ {
+ // We can skip this in release builds
+ names.validate();
+ }
+ body
+ {
+ foreach(char[] id, ref Type t; names)
+ // Current file is now counted as done
+ if(t.state == LoadState.Loaded) t.state = LoadState.Previous;
+ else if(t.state == LoadState.Unloaded)
+ //writefln("WARNING: Unloaded reference " ~ id);
+ esFile.fail("Unloaded reference " ~ id);
+ }
+
+ // Number of inserted elements
+ uint length() {return names.length;}
+
+ // Add the names in this list to an ItemList
+ void addToList(ref ItemBaseList l, ItemType t)
+ {
+ foreach(char[] id, ref Type s; names)
+ l.insert(id, &s, t);
+ }
+}
+
+/*
+class Dummy : ListKeeper
+{
+ this() {}
+ void load() {}
+ void* lookup(char[] s) { return null; }
+ void endFile() {}
+ uint length() { return 0; }
+}
+*/
+
+// A pointer to an item
+struct ItemBase
+{
+ ItemType type;
+ void *p;
+}
+
+struct ItemBaseList
+{
+ HashTable!(char[],ItemBase,ESMRegionAlloc) list;
+
+ void addList(ItemBaseList l)
+ {
+ foreach(char[] id, ItemBase b; l.list)
+ insert(id, b.p, b.type);
+ }
+
+ void addList(ListKeeper source, ItemType type)
+ {
+ source.addToList(*this, type);
+ }
+
+ void insert(char[] id, void* p, ItemType type)
+ {
+ ItemBase *b;
+ if(!list.insertEdit(id, b))
+ {
+ //writefln("Replacing item ", id);
+ if(b.type != ItemType.None)
+ esFile.fail("Replaced valid item: " ~ id);
+ }
+ //else writefln("Inserting new item ", id);
+
+ b.type = type;
+ b.p = p;
+ }
+
+ // Called at the end to check that all referenced items have been resolved
+ void endMerge()
+ {
+ foreach(char[] id, ref ItemBase t; list)
+ // Current file is now counted as done
+ if(t.type == ItemType.None)
+ // TODO: Don't use esFile.fail for this
+ esFile.fail("ItemBaseList: Unresolved forward reference: " ~ id);
+ }
+
+ // Look up an item, return a pointer to the ItemBase representing
+ // it. If it does not exist, it is inserted.
+ ItemBase *lookup(char[] id)
+ {
+ if(!id.length) return null; // Empty reference
+ ItemBase *b = list.lookup(id);
+ // Is the value in the list?
+ if(!b)
+ // No, assume it is a forward reference.
+ {
+ // Since the lookup name is stored in an internal buffer in
+ // esFile, we have to copy it.
+ id = esmRegion.copy(id);
+
+ // To avoid copying the string on every lookup, we have to
+ // insert in a separate step. But a double lookup isn't
+ // really THAT expensive.
+ list.insertEdit(id, b);
+
+ b.p = null;
+ b.type = ItemType.None;
+ }
+ return b;
+ }
+}
+
+// Um... I think this code is double up because I had to split it into
+// separate files to avoid template stuff. I don't think we need to do
+// that anymore.
+
+// An item. Contains a reference to an ItemBase, which again is a
+// reference to an item. The ItemBase might change after we have
+// looked it up (for forward references), so we have to use a pointer.
+struct Item
+{
+ ItemBase *i;
+
+ void* getPtr(ItemType type)
+ {
+ if(i != null && i.type == type) return i.p;
+ return null;
+ }
+
+ T* getType(T, ItemType Type)()
+ {
+ return cast(T*)getPtr(Type);
+ }
+
+ /* Avoid the templates for now
+ alias getType!(Potion, ItemType.Potion) getPotion;
+ alias getType!(Apparatus, ItemType.Apparatus) getApparatus;
+ alias getType!(Armor, ItemType.Armor) getArmor;
+ alias getType!(Weapon, ItemType.Weapon) getWeapon;
+ alias getType!(Book, ItemType.Book) getBook;
+ alias getType!(Clothing, ItemType.Clothing) getClothing;
+ alias getType!(Light, ItemType.Light) getLight;
+ alias getType!(Ingredient, ItemType.Ingredient) getIngredient;
+ alias getType!(Tool, ItemType.Pick) getPick;
+ alias getType!(Tool, ItemType.Probe) getProbe;
+ alias getType!(Tool, ItemType.Repair) getRepair;
+ alias getType!(Misc, ItemType.Misc) getMisc;
+ alias getType!(LeveledItems, ItemType.ItemList) getItemList;
+ alias getType!(Creature, ItemType.Creature) getCreature;
+ alias getType!(LeveledCreatures, ItemType.CreatureList) getCreatureList;
+ alias getType!(NPC, ItemType.NPC) getNPC;
+ alias getType!(Door, ItemType.Door) getDoor;
+ alias getType!(Activator, ItemType.Activator) getActivator;
+ alias getType!(Static, ItemType.Static) getStatic;
+ alias getType!(Container, ItemType.Container) getContainer;
+ */
+}
+
+struct ItemList
+{
+ private:
+ ItemBaseList list;
+
+ public:
+ void addList(ItemList l)
+ { list.addList(l.list); }
+
+ void addList(ListKeeper source, ItemType type)
+ { list.addList(source, type); }
+
+ Item lookup(char[] id)
+ {
+ Item i;
+ i.i = list.lookup(id);
+ return i;
+ }
+
+ void endMerge()
+ { list.endMerge(); }
+
+ void endFile()
+ in { list.list.validate(); }
+ body {}
+
+ void rehash(uint size)
+ { list.list.rehash(size); }
+
+ uint length() { return list.list.length(); }
+}
+
+// Aggregate lists, made by concatinating several other lists.
+ItemList items; // All inventory items, including leveled item lists
+ItemList actors; // All actors, ie. NPCs, creatures and leveled lists
+ItemList cellRefs; // All things that are referenced from cells
diff --git a/esm/loadacti.d b/esm/loadacti.d
new file mode 100644
index 0000000000..85ca7b6de7
--- /dev/null
+++ b/esm/loadacti.d
@@ -0,0 +1,53 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadacti.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadacti;
+
+import esm.imports;
+
+/*
+ * Activator
+ *
+ * Activators are static in-world objects that pop up a caption when
+ * you stand close and look at them, for example signs. Some
+ * activators have scrips, such as beds that let you sleep when you
+ * activate them.
+ */
+
+struct Activator
+{
+ char[] id;
+ LoadState state;
+
+ char[] name;
+ Script *script;
+ MeshIndex model;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNString("FNAM");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ }}
+}
+ListID!(Activator) activators;
diff --git a/esm/loadalch.d b/esm/loadalch.d
new file mode 100644
index 0000000000..3e6006fc1e
--- /dev/null
+++ b/esm/loadalch.d
@@ -0,0 +1,74 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadalch.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadalch;
+import esm.imports;
+
+/*
+ * Alchemy item (potions)
+ */
+
+struct Potion
+{
+ char[] id;
+ LoadState state;
+
+ align(1) struct ALDTstruct
+ {
+ float weight;
+ int value;
+ int autoCalc;
+ static assert(ALDTstruct.sizeof == 12);
+ }
+
+ ALDTstruct data;
+
+ char[] name;
+
+ MeshIndex model;
+ IconIndex icon;
+
+ Script *script;
+
+ EffectList effects;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ icon = getIcon("TEXT");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ name = getHNOString("FNAM");
+
+ readHNExact(&data, data.sizeof, "ALDT");
+
+ effects = getRegion().getBuffer!(ENAMstruct)(0,1);
+
+ while(isNextSub("ENAM"))
+ {
+ effects.length = effects.length + 1;
+ readHExact(&effects.array[$-1], effects.array[$-1].sizeof);
+ }
+
+ }}
+}
+ListID!(Potion) potions;
diff --git a/esm/loadappa.d b/esm/loadappa.d
new file mode 100644
index 0000000000..3d8a5ac1de
--- /dev/null
+++ b/esm/loadappa.d
@@ -0,0 +1,67 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadappa.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadappa;
+import esm.imports;
+
+/*
+ * Alchemist apparatus
+ */
+
+struct Apparatus
+{
+ char[] id, name;
+ LoadState state;
+
+ enum AppaType : int
+ {
+ MortarPestle = 0,
+ Albemic = 1,
+ Calcinator = 2,
+ Retort = 3
+ }
+
+ align(1) struct AADTstruct
+ {
+ AppaType type;
+ float quality;
+ float weight;
+ int value;
+
+ static assert(AADTstruct.sizeof == 16);
+ }
+ AADTstruct data;
+ MeshIndex model;
+ IconIndex icon;
+ Script *script;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNString("FNAM");
+ readHNExact(&data, data.sizeof, "AADT");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ icon = getIcon();
+ }}
+}
+ListID!(Apparatus) appas;
diff --git a/esm/loadarmo.d b/esm/loadarmo.d
new file mode 100644
index 0000000000..6e0bfee221
--- /dev/null
+++ b/esm/loadarmo.d
@@ -0,0 +1,143 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadarmo.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadarmo;
+import esm.imports;
+
+import esm.loadbody, esm.loadench;
+
+/*
+ * Armor
+ */
+
+// Reference to body parts
+struct PartReference
+{
+ enum Index
+ {
+ Head = 0,
+ Hair = 1,
+ Neck = 2,
+ Cuirass = 3,
+ Groin = 4,
+ Skirt = 5,
+ RHand = 6,
+ LHand = 7,
+ RWrist = 8,
+ LWrist = 9,
+ Shield = 10,
+ RForearm = 11,
+ LForearm = 12,
+ RUpperarm = 13,
+ LUpperarm = 14,
+ RFoot = 15,
+ LFoot = 16,
+ RAnkle = 17,
+ LAnkle = 18,
+ RKnee = 19,
+ LKnee = 20,
+ RLeg = 21,
+ LLeg = 22,
+ RPauldron = 23,
+ LPauldron = 24,
+ Weapon = 25,
+ Tail = 26
+ }
+ Index part;
+
+ BodyPart* male, female;
+}
+
+struct PartReferenceList
+{
+ RegionBuffer!(PartReference) parts;
+
+ void load()
+ {with(esFile){
+ parts = getRegion().getBuffer!(PartReference)(0,1);
+ while(isNextSub("INDX"))
+ {
+ parts.length = parts.length + 1;
+ with(parts.array[$-1])
+ {
+ part = cast(PartReference.Index) getHByte;
+ male = getHNOPtr!(BodyPart)("BNAM", bodyParts);
+ female = getHNOPtr!(BodyPart)("CNAM", bodyParts);
+ }
+ }
+ }}
+}
+
+struct Armor
+{
+ enum Type : int
+ {
+ Helmet = 0,
+ Cuirass = 1,
+ LPauldron = 2,
+ RPauldron = 3,
+ Greaves = 4,
+ Boots = 5,
+ LGauntlet = 6,
+ RGauntlet = 7,
+ Shield = 8,
+ LBracer = 9,
+ RBracer = 10
+ }
+
+ align(1) struct AODTstruct
+ {
+ Type type;
+ float weight;
+ int value, health, enchant, armor;
+
+ static assert(AODTstruct.sizeof==24);
+ }
+
+ AODTstruct data;
+
+ char[] name, id;
+ LoadState state;
+
+ PartReferenceList parts;
+
+ MeshIndex model;
+ IconIndex icon;
+
+ Script *script;
+ Enchantment *enchant;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNString("FNAM");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ readHNExact(&data, data.sizeof, "AODT");
+ icon = getOIcon();
+
+ parts.load();
+
+ enchant = getHNOPtr!(Enchantment)("ENAM", enchants);
+ }}
+}
+ListID!(Armor) armors;
diff --git a/esm/loadbody.d b/esm/loadbody.d
new file mode 100644
index 0000000000..24f2b3019d
--- /dev/null
+++ b/esm/loadbody.d
@@ -0,0 +1,90 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadbody.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadbody;
+import esm.imports;
+
+/*
+ * Body part mesh. NPCs have several mesh files, one for each element
+ * in the enum below. One BODY record is given for each part.
+ */
+
+struct BodyPart
+{
+ enum MeshType : byte
+ {
+ Head = 0,
+ Hair = 1,
+ Neck = 2,
+ Chest = 3,
+ Groin = 4,
+ Hand = 5,
+ Wrist = 6,
+ Forearm = 7,
+ Upperarm = 8,
+ Foot = 9,
+ Ankle = 10,
+ Knee = 11,
+ Upperleg = 12,
+ Clavicle = 13,
+ Tail = 14
+ }
+
+ enum Type : byte
+ {
+ Skin = 0,
+ Clothing = 1,
+ Armor = 2
+ }
+
+ enum Flags : byte
+ {
+ Female = 1,
+ Playable = 2
+ }
+
+ align(1) struct BYDTstruct
+ {
+ MeshType part;
+ byte vampire;
+ Flags flags;
+ Type type;
+ static assert(BYDTstruct.sizeof==4);
+ }
+
+ BYDTstruct data;
+
+ char[] name, id;
+ LoadState state;
+ MeshIndex model;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNString("FNAM");
+ readHNExact(&data, data.sizeof, "BYDT");
+ }}
+
+}
+
+ListID!(BodyPart) bodyParts;
diff --git a/esm/loadbook.d b/esm/loadbook.d
new file mode 100644
index 0000000000..822ed3e760
--- /dev/null
+++ b/esm/loadbook.d
@@ -0,0 +1,62 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadbook.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadbook;
+import esm.imports;
+
+/*
+ * Books
+ */
+
+struct Book
+{
+ align(1) struct BKDTstruct
+ {
+ float weight;
+ int value, isScroll, skillID, enchant;
+
+ static assert(BKDTstruct.sizeof == 20);
+ }
+
+ BKDTstruct data;
+
+ MeshIndex model;
+ IconIndex icon;
+ Script *script;
+ Enchantment *enchant;
+ char[] name, text, id;
+
+ LoadState state;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNString("FNAM");
+ readHNExact(&data, data.sizeof, "BKDT");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ icon = getIcon();
+ text = getHNOString("TEXT");
+ enchant = getHNOPtr!(Enchantment)("ENAM", enchants);
+ }}
+}
+ListID!(Book) books;
diff --git a/esm/loadbsgn.d b/esm/loadbsgn.d
new file mode 100644
index 0000000000..a8c865d5b7
--- /dev/null
+++ b/esm/loadbsgn.d
@@ -0,0 +1,53 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadbsgn.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadbsgn;
+import esm.imports;
+
+/*
+ * Birth signs
+ */
+
+struct BirthSign
+{
+ LoadState state;
+
+ char[] id, name, description;
+
+ TextureIndex texture;
+
+ // List of powers and abilities that come with this birth sign.
+ SpellList powers;
+
+ void load()
+ {with(esFile){
+ name = getHNString("FNAM");
+
+ texture = getOTexture();
+
+ description = getHNOString("DESC");
+
+ powers.load();
+ }}
+}
+ListID!(BirthSign) birthSigns;
diff --git a/esm/loadcell.d b/esm/loadcell.d
new file mode 100644
index 0000000000..0dd503c830
--- /dev/null
+++ b/esm/loadcell.d
@@ -0,0 +1,371 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadcell.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadcell;
+
+import esm.imports;
+import esm.loadregn;
+
+/* Cells can be seen as a collection of several records, and holds
+ * data about objects, creatures, statics and landscape (for exterior
+ * cells). This data can be huge, and has to be loaded when we need
+ * it, not when parsing the file.
+ */
+
+align(1) struct AMBIStruct
+{
+ Color ambient, sunlight, fog;
+ float fogDensity;
+
+ static assert(AMBIStruct.sizeof == 16);
+}
+
+enum CellFlags : uint
+ {
+ Interior = 0x01,
+ HasWater = 0x02,
+ NoSleep = 0x04,
+ QuasiExt = 0x80 // Behave like exterior (tribunal, requires
+ // version 1.3? TODO: Check this (and for other
+ // tribunal-only stuff))
+ }
+
+struct InteriorCell
+{
+ char[] id;
+ CellFlags flags;
+
+ LoadState state;
+
+ // Stores file position so we can load the cell later
+ TES3FileContext context;
+
+ // This struct also holds a file context for use later
+ PathGrid paths;
+
+ void load()
+ {with(esFile){
+ // Save the file position and skip to the next record (after the CELL)
+ getContext(context);
+ skipRecord();
+
+ // Check for path grid data
+ paths.state = LoadState.Unloaded;
+ if(isNextHRec("PGRD")) paths.load();
+ }}
+}
+
+struct ExteriorCell
+{
+ // This isn't an id, so we call it 'name' instead. May be empty, if
+ // so use region name to display
+ char[] name;
+
+ CellFlags flags;
+ int gridX, gridY;
+
+ LoadState state;
+
+ Region* region;
+
+ // We currently don't load all cell data (these can be huge!),
+ // anyway it makes sense to only load these when accessed. Use this
+ // to reopen the file later.
+ TES3FileContext context;
+
+ // Landscape and path grid data
+ Land land; // There can be TWO landscapes! Or maybe I have
+ // misunderstood something. Need to check what it means.
+
+ PathGrid paths;
+
+ void load()
+ {with(esFile){
+ this.region = getHNOPtr!(Region)("RGNN", regions);
+
+ // Save the file position and skip to the next record (after the CELL)
+ getContext(context);
+ skipRecord();
+
+ // Exterior cells might have landscape data
+ land.state = LoadState.Unloaded;
+ if(isNextHRec("LAND")) land.load();
+
+ // Check for path grid data
+ paths.state = LoadState.Unloaded;
+ if(isNextHRec("PGRD")) paths.load();
+
+ // Land can also be here instead. In fact, it can be both
+ // places. I have to figure out what it means.
+ if(isNextHRec("LAND")) land.load();
+ }}
+}
+
+struct ExtCellHash
+{
+ // This is a pretty good hash, gives no collisions at all for
+ // Morrowind.esm and a table size of 2048, and very few collisions
+ // overall. Not that it matters of course.
+ static uint hash(uint val)
+ {
+ uint res = cast(ushort)val;
+ res += *(cast(ushort*)&val+1)*41;
+ return res;
+ }
+
+ static bool isEqual(uint a, uint b) { return a==b; }
+}
+
+class CellList : ListKeeper
+{
+ // Again, these are here to avoid DMD template bugs
+ alias _aaNode!(char[], InteriorCell) _unused1;
+ alias _aaNode!(uint, ExteriorCell) _unused2;
+
+ HashTable!(char[], InteriorCell, ESMRegionAlloc) in_cells;
+ HashTable!(uint, ExteriorCell, ESMRegionAlloc, ExtCellHash) ex_cells;
+
+ this()
+ {
+ in_cells = in_cells.init;
+ in_cells.rehash(1600);
+
+ ex_cells = ex_cells.init;
+ ex_cells.rehash(1800);
+ }
+
+ align(1) struct DATAstruct
+ {
+ CellFlags flags;
+ int gridX, gridY;
+ static assert(DATAstruct.sizeof==12);
+ }
+
+ DATAstruct data;
+
+ // Look up an interior cell, throws an error if not found (might
+ // change later)
+ InteriorCell *getInt(char[] s)
+ {
+ return in_cells.getPtr(s);
+ }
+
+ // Exterior cell, same as above
+ ExteriorCell *getExt(int x, int y)
+ {
+ return ex_cells.getPtr(compound(x,y));
+ }
+
+ void *lookup(char[] s)
+ { assert(0); }
+
+ void endFile()
+ out
+ {
+ in_cells.validate();
+ ex_cells.validate();
+ }
+ body
+ {
+ foreach(id, ref c; in_cells)
+ {
+ if(c.state == LoadState.Loaded) c.state = LoadState.Previous;
+ // We never forward reference cells!
+ assert(c.state != LoadState.Unloaded);
+ }
+
+ foreach(id, ref c; ex_cells)
+ {
+ if(c.state == LoadState.Loaded) c.state = LoadState.Previous;
+ // We never forward reference cells!
+ assert(c.state != LoadState.Unloaded);
+ }
+ }
+
+ uint length() { return numInt() + numExt(); }
+
+ uint numInt() { return in_cells.length; }
+ uint numExt() { return ex_cells.length; }
+
+ // Turn an exterior cell grid position into a unique number
+ static uint compound(int gridX, int gridY)
+ {
+ return cast(ushort)gridX + ((cast(ushort)gridY)<<16);
+ }
+
+ static void decompound(uint val, out int gridX, out int gridY)
+ {
+ gridX = cast(short)(val&0xffff);
+ gridY = cast(int)(val&0xffff0000) >> 16;
+ }
+
+ void load()
+ {with(esFile){
+ char[] id = getHNString("NAME");
+
+ // Just ignore this, don't know what it does.
+ if(isNextSub("DELE")) getHInt();
+
+ readHNExact(&data, data.sizeof, "DATA");
+
+ if(data.flags & CellFlags.Interior)
+ {
+ InteriorCell *p;
+ if(in_cells.insertEdit(id, p))
+ // New item was inserted
+ {
+ p.state = LoadState.Unloaded;
+ p.id = id;
+ p.flags = data.flags;
+ p.load();
+ p.state = LoadState.Loaded;
+ }
+ else
+ // Overloading an existing cell
+ {
+ if(p.state != LoadState.Previous)
+ fail("Duplicate internal cell " ~ id);
+
+ assert(id == p.id);
+ p.load();
+ p.state = LoadState.Loaded;
+ }
+ }
+ else // Exterior cell
+ {
+ uint key = compound(data.gridX, data.gridY);
+
+ ExteriorCell *p;
+ if(ex_cells.insertEdit(key, p))
+ // New cell
+ {
+ p.state = LoadState.Unloaded;
+ p.name = id;
+ p.flags = data.flags;
+ p.gridX = data.gridX;
+ p.gridY = data.gridY;
+ p.load();
+ p.state = LoadState.Loaded;
+ }
+ else
+ {
+ if(p.state != LoadState.Previous)
+ fail(format("Duplicate exterior cell %d %d",
+ data.gridX, data.gridY));
+ assert(p.gridX == data.gridX);
+ assert(p.gridY == data.gridY);
+ p.load();
+ p.state = LoadState.Loaded;
+ }
+ }
+ }}
+}
+
+/*
+ * Landscape data. The landscape is stored as a hight map, and that is
+ * pretty much all I know at the moment.
+ */
+
+struct Land
+{
+ LoadState state;
+
+ uint flags; // ?? - only first four bits seem to be used?
+
+ TES3FileContext context;
+
+ void load()
+ {with(esFile){
+ getHNUlong("INTV"); // Grid location. See next line.
+ //writefln("x=%d, y=%d", *(cast(int*)&grid), *(cast(int*)&grid+1));
+
+ flags = getHNInt("DATA");
+
+ // Save file position
+ getContext(context);
+
+ // TODO: We don't decode these yet. Se ESLand.h from
+ // Morrowindremake for a lot of hints.
+ if(isNextSub("VNML")) skipHSubSize(12675);
+ if(isNextSub("VHGT")) skipHSubSize(4232);
+ if(isNextSub("WNAM")) skipHSubSize(81);
+ if(isNextSub("VCLR")) skipHSubSize(12675);
+ if(isNextSub("VTEX")) skipHSubSize(512);
+
+ if(state == LoadState.Loaded)
+ writefln("Duplicate landscape data in " ~ getFilename());
+ state = LoadState.Loaded;
+ }}
+}
+
+/*
+ * Path grid.
+ */
+struct PathGrid
+{
+ struct DATAstruct
+ {
+ int x, y; // Grid location, matches cell for exterior cells
+ short s1; // ?? Usually but not always a power of 2. Doesn't seem
+ // to have any relation to the size of PGRC.
+ short s2; // Number of path points? Size of PGRP block is always 16 * s2;
+ }
+
+ DATAstruct data;
+
+ TES3FileContext context;
+
+ LoadState state;
+
+ void load()
+ {with(esFile){
+ assert(state == LoadState.Unloaded);
+
+ readHNExact(&data, data.sizeof, "DATA");
+ getHNString("NAME"); // Cell name, we don't really need it.
+
+ // Remember this file position
+ getContext(context);
+
+ // Check that the sizes match up. Size = 16 * s2 (path points?)
+ if(isNextSub("PGRP"))
+ {
+ int size = skipHSub();
+ if(size != 16*data.s2)
+ fail("Path grid table size");
+ }
+
+ // Size varies. Path grid chances? Connections? Multiples of 4
+ // suggest either int or two shorts, or perhaps a float. Study
+ // it later.
+ if(isNextSub("PGRC"))
+ {
+ int size = skipHSub();
+ if(size % 4 != 0)
+ fail("PGRC size not a multiple of 4");
+ }
+
+ state = LoadState.Loaded;
+ }}
+}
+CellList cells;
diff --git a/esm/loadclas.d b/esm/loadclas.d
new file mode 100644
index 0000000000..2009c7860d
--- /dev/null
+++ b/esm/loadclas.d
@@ -0,0 +1,90 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadclas.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+module esm.loadclas;
+
+import esm.imports;
+
+import esm.loadskil;
+
+/*
+ * Character class definitions
+ */
+
+// These flags tells us which items should be auto-calculated for this
+// class
+struct Class
+{
+ enum AutoCalc : uint
+ {
+ Weapon = 0x00001,
+ Armor = 0x00002,
+ Clothing = 0x00004,
+ Books = 0x00008,
+ Ingredient = 0x00010,
+ Lockpick = 0x00020,
+ Probe = 0x00040,
+ Lights = 0x00080,
+ Apparatus = 0x00100,
+ Repair = 0x00200,
+ Misc = 0x00400,
+ Spells = 0x00800,
+ MagicItems = 0x01000,
+ Potions = 0x02000,
+ Training = 0x04000,
+ Spellmaking = 0x08000,
+ Enchanting = 0x10000,
+ RepairItem = 0x20000
+ }
+
+ align(1) struct CLDTstruct
+ {
+ Attribute attribute[2]; // Attributes that get class bonus
+
+ Specialization specialization; // 0 = Combat, 1 = Magic, 2 = Stealth
+
+ SkillEnum[2][5] skills; // Minor and major skills.
+
+ uint isPlayable; // 0x0001 - Playable class
+
+ // I have no idea how to autocalculate these items...
+ AutoCalc calc;
+
+ static assert(CLDTstruct.sizeof == 60);
+ }
+
+ LoadState state;
+ char[] id, name, description;
+ CLDTstruct data;
+
+ void load()
+ {
+ name = esFile.getHNString("FNAM");
+ esFile.readHNExact(&data, data.sizeof, "CLDT");
+
+ if(data.isPlayable > 1)
+ esFile.fail("Unknown bool value");
+
+ description = esFile.getHNOString("DESC");
+ }
+}
+ListID!(Class) classes;
diff --git a/esm/loadclot.d b/esm/loadclot.d
new file mode 100644
index 0000000000..e70ffdecb7
--- /dev/null
+++ b/esm/loadclot.d
@@ -0,0 +1,83 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadclot.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadclot;
+import esm.imports;
+import esm.loadarmo;
+
+/*
+ * Clothing
+ */
+
+struct Clothing
+{
+ enum Type : int
+ {
+ Pants = 0,
+ Shoes = 1,
+ Shirt = 2,
+ Belt = 3,
+ Robe = 4,
+ RGlove = 5,
+ LGlove = 6,
+ Skirt = 7,
+ Ring = 8,
+ Amulet = 9
+ }
+
+ struct CTDTstruct
+ {
+ Type type;
+ float weight;
+ short value;
+ short enchantPoints;
+
+ static assert(CTDTstruct.sizeof == 12);
+ }
+
+ CTDTstruct data;
+
+ LoadState state;
+ char[] id, name;
+ PartReferenceList parts;
+
+ MeshIndex model;
+ IconIndex icon;
+ Enchantment *enchant;
+ Script *script;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNOString("FNAM");
+ readHNExact(&data, data.sizeof, "CTDT");
+
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ icon = getOIcon();
+
+ parts.load();
+
+ enchant = getHNOPtr!(Enchantment)("ENAM", enchants);
+ }}
+}
+ListID!(Clothing) clothes;
diff --git a/esm/loadcont.d b/esm/loadcont.d
new file mode 100644
index 0000000000..a088b08077
--- /dev/null
+++ b/esm/loadcont.d
@@ -0,0 +1,106 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadcont.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadcont;
+import esm.imports;
+
+/*
+ * Container definition
+ */
+
+struct ContItem
+{
+ Item item;
+ int count;
+}
+
+struct InventoryList
+{
+ RegionBuffer!(ContItem) list;
+
+ private static char[32] buffer;
+
+ void load()
+ {
+ list = esFile.getRegion().getBuffer!(ContItem)(0,10);
+
+ while(esFile.isNextSub("NPCO"))
+ {
+ esFile.getSubHeaderIs(36);
+ list.length = list.length + 1;
+
+ with(list.array[$-1])
+ {
+ esFile.getInt(count);
+
+ // String is automatically chopped
+ item = items.lookup(esFile.getString(buffer));
+ }
+ }
+ }
+}
+
+struct Container
+{
+ enum Flags
+ {
+ Organic = 1, // Objects cannot be placed in this container
+ Respawn = 2, // Respawns after 4 months
+ Unknown = 8
+ }
+
+ char[] id, name;
+ LoadState state;
+
+ MeshIndex model;
+ Script *script;
+
+ float weight; // Not sure, might be max total weight allowed?
+ Flags flags;
+ InventoryList inventory;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNString("FNAM");
+ weight = getHNFloat("CNDT");
+ flags = cast(Flags)getHNUint("FLAG");
+
+ if(flags & 0xf4) fail("Unknown flags");
+ if(!(flags & 0x8)) fail("Flag 8 not set");
+
+ /*
+ if(getFileType == FileType.Savegame)
+ {
+ int tmp = getHNInt("INDX");
+ if(tmp) writefln("WARNING, INDX != 0: ", tmp);
+ }
+ */
+
+ script = getHNOPtr!(Script)("SCRI", scripts);
+
+ inventory.load();
+ }}
+}
+
+ListID!(Container) containers;
diff --git a/esm/loadcrea.d b/esm/loadcrea.d
new file mode 100644
index 0000000000..fcfe4b76f4
--- /dev/null
+++ b/esm/loadcrea.d
@@ -0,0 +1,127 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadcrea.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadcrea;
+import esm.imports;
+
+import esm.loadcont;
+
+/*
+ * Creature definition
+ *
+ * Container structs are in loadcont.d
+ */
+
+struct Creature
+{
+ // Default is 0x48?
+ enum Flags
+ {
+ Biped = 0x001,
+ Respawn = 0x002,
+ Weapon = 0x004, // Has weapon and shield
+ None = 0x008, // ??
+ Swims = 0x010,
+ Flies = 0x020, // Don't know what happens if several
+ Walks = 0x040, // of these are set
+ Essential = 0x080,
+ Skeleton = 0x400, // Does not have normal blood
+ Metal = 0x800 // Has 'golden' blood
+ }
+
+ enum Type : int
+ {
+ Creature = 0,
+ Deadra = 1,
+ Undead = 2,
+ Humanoid = 3
+ }
+
+ align(1) struct NPDTstruct
+ {
+ Type type;
+ // For creatures we obviously have to use ints, not shorts and
+ // bytes like we use for NPCs.... this file format just makes so
+ // much sense! (Still, _much_ easier to decode than the NIFs.)
+ int level;
+ int strength, intelligence, willpower, agility, speed, endurance,
+ personality, luck, health, mana, fatigue; // Stats
+ int soul; // The creatures soul value (used with soul gems.)
+ int combat, magic, stealth; // Don't know yet.
+ int attack[6]; // AttackMin1, AttackMax1, ditto2, ditto3
+ int gold;
+
+ static assert(NPDTstruct.sizeof == 96);
+ }
+
+ NPDTstruct data;
+
+ Flags flags;
+
+ LoadState state;
+ char[] id, name;
+
+ MeshIndex model;
+
+ // Base creature. Any missing data must be taken from here, I quess.
+ Creature *original;
+ Script *script;
+
+ float scale;
+
+ InventoryList inventory;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ original = getHNOPtr!(Creature)("CNAM", creatures);
+ name = getHNOString("FNAM");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+
+ readHNExact(&data, data.sizeof, "NPDT");
+
+ flags = cast(Flags)getHNInt("FLAG");
+ scale = getHNOFloat("XSCL", 1.0);
+
+ inventory.load();
+
+ // AIDT - data (12 bytes, unknown)
+ // AI_W - wander (14 bytes, i don't understand it)
+ // short distance
+ // byte duration
+ // byte timeOfDay
+ // byte idle[10]
+ //
+ // Rest is optional:
+ // AI_T - travel?
+ // AI_F - follow?
+ // AI_E - escort?
+ // AI_A - activate?
+
+ //*
+ skipRecord();
+ //*/
+ }}
+
+}
+ListID!(Creature) creatures;
diff --git a/esm/loadcrec.d b/esm/loadcrec.d
new file mode 100644
index 0000000000..2ce223b1fa
--- /dev/null
+++ b/esm/loadcrec.d
@@ -0,0 +1,59 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadcrec.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadcrec;
+import esm.imports;
+
+/*
+ * Creature and container change information? Used in save games only.
+ */
+
+struct LoadCREC
+{
+ void load(TES3File f)
+ {
+ writefln("Creature-changer %s", f.getHNString("NAME"));
+ writefln(" Index: ", f.getHNInt("INDX"));
+
+ while(f.hasMoreSubs)
+ {
+ f.getSubName();
+ f.skipHSub();
+ }
+ }
+}
+
+struct LoadCNTC
+{
+ void load(TES3File f)
+ {
+ writefln("Itemlist-changer %s", f.getHNString("NAME"));
+ writefln(" Index: ", f.getHNInt("INDX"));
+
+ while(f.hasMoreSubs)
+ {
+ f.getSubName();
+ f.skipHSub();
+ }
+ }
+}
diff --git a/esm/loaddial.d b/esm/loaddial.d
new file mode 100644
index 0000000000..3c7e7cddb3
--- /dev/null
+++ b/esm/loaddial.d
@@ -0,0 +1,291 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loaddial.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loaddial;
+import esm.imports;
+import esm.loadinfo;
+
+/*
+ * Dialogue topic and journal entries. The acutal data is contained in
+ * the following INFO records.
+ */
+
+// Keep a list of possible hyper links. This list is used when parsing
+// text and finding references to topic names. Eg. if a character says
+// "Would you like to join the mages guild?", we must be able to pick
+// out the phrase "join the mages guild?" and make a hyperlink of
+// it. Each link is indexed by their first word. The structure
+// contains the rest of the phrase, so the phrase above would be
+// indexed by "join" and contain the string "the mages guild?", for
+// quick comparison with the text are currently parsing. It also
+// contains a pointer to the corresponding dialogue struct. The lists
+// are sorted by descending string length, in order to match the
+// longest possible term first.
+
+struct Hyperlink
+{
+ Dialogue *ptr;
+ char[] rest;
+
+ // Returns a < b if a.length > b.length.
+ int opCmp(Hyperlink *h) {return h.rest.length - rest.length;}
+}
+
+alias RegionBuffer!(Hyperlink) HyperlinkArray;
+
+// This is much nicer now that we use our own AA.
+struct HyperlinkList
+{
+ // Make a case insensitive hash table of Hyperlink arrays.
+ HashTable!(char[], HyperlinkArray, ESMRegionAlloc, CITextHash) list;
+
+ void add(char[] topic, Dialogue* ptr)
+ {
+ // Only add dialogues
+ if(ptr.type != Dialogue.Type.Topic) return;
+
+ Hyperlink l;
+ l.ptr = ptr;
+ l.rest = topic;
+
+ // Use the first word as the index
+ topic = nextWord(l.rest);
+
+ // Insert a new array, or get an already existing one
+ HyperlinkArray ha = list.get(topic,
+ // Create a new array
+ delegate void (ref HyperlinkArray a)
+ { a = esmRegion.getBuffer!(Hyperlink)(0,1); }
+ );
+
+ // Finally, add it to the list
+ ha ~= l;
+ }
+
+ Hyperlink[] getList(char[] word)
+ {
+ HyperlinkArray p;
+ if(list.inList(word, p)) return p.array();
+ return null;
+ }
+
+ void rehash(uint size)
+ {
+ list.rehash(size);
+ }
+
+ // We wouldn't need this if we only dealt with one file, since the
+ // topics are already sorted in Morrowind.esm. However, other files
+ // might add items out of order later, so we have to sort it. To
+ // understand why this is needed, consider the following example:
+ //
+ // Morrowind.esm contains the topic 'join us'. When ever the text
+ // ".. join us blahblah ..." is encountered, this match is
+ // found. However, if a plugin adds the topic 'join us today', we
+ // have to place this _before_ 'join us' in the list, or else it
+ // will never be matched.
+ void sort()
+ {
+ foreach(char[] s, HyperlinkArray l; list)
+ {
+ l.array().sort;
+ /*
+ writefln("%s: ", s, l.length);
+ foreach(Hyperlink h; l.array())
+ writefln(" %s (%s)", h.rest, h.ptr.id);
+ */
+ }
+ }
+}
+
+// List of dialogue hyperlinks
+HyperlinkList hyperlinks;
+
+struct Dialogue
+{
+ enum Type
+ {
+ Topic = 0,
+ Voice = 1,
+ Greeting = 2,
+ Persuasion = 3,
+ Journal = 4,
+ Deleted = -1
+ }
+
+ //Type type;
+ DialogueType type;
+
+ DialInfoList infoList;
+
+ char[] id; // This is the 'dialogue topic' that the user actually
+ // sees.
+ LoadState state;
+
+ void load()
+ {with(esFile){
+ getSubNameIs("DATA");
+
+ getSubHeader();
+ int si = getSubSize();
+ if(si == 1)
+ {
+ byte b;
+ getByte(b);
+ DialogueType t = cast(DialogueType)b;
+
+ // Meet the new type, same as the old type
+ if(t != this.type && state == LoadState.Previous)
+ fail("Type changed in dialogue " ~ id);
+
+ this.type = t;
+ }
+ else if(si == 4)
+ {
+ // These are just markers, their values are not used.
+ int i;
+ getInt(i);
+ //writefln("In file %s:", getFilename());
+ //writefln(" WARNING: DIAL.DATA was size 4 and contains: ", i);
+ i = getHNInt("DELE");
+ //writefln(" DELE contains ", i);
+ this.type = Type.Deleted;
+ }
+ else fail("Unknown sub record size " ~ toString(si));
+
+ infoList.state = state;
+ while(isNextHRec("INFO"))
+ infoList.load(this.type);
+ //skipRecord();
+ }}
+}
+
+// TODO/FIXME: HACK suddenly needed with version 0.167 :(
+// Haven't re-tested it with non-ancient compiler
+typedef Dialogue.Type DialogueType;
+
+/+
+ // I don't remember when I commented out this code or what state
+ // it is in. Probably highly experimental.
+ // --------------
+
+ // Loop through the info blocks in this dialogue, and update the
+ // master as necessary.
+
+ // TODO: Note that the dialogue system in Morrowind isn't very
+ // robust. If several mods insert dialogues at exactly the same
+ // place, the mods loaded last might overwrite the previous mods,
+ // completely removing the previous entry even if the two entries
+ // do not have the same id. This is because a change also
+ // overwrites the previous and the next entry, in order to update
+ // their "previous" and "next" fields. Furthermore, we might put
+ // ourselves in a situation where the forward and backward chains
+ // do not match, or in a situation where we update a deleted
+ // info. For now I do nothing about it, but I will have to develop
+ // a "conflict manager" later on. It SHOULD be possible to merge
+ // these info lists automatically in most cases, but it
+ // complicates the code.
+
+ // Whoa, seems we have a case study already with just tribunal and
+ // bloodmoon loaded! See comments below.
+
+ foreach(char[] id, ref DialInfoLoad m; mod.infoList)
+ {
+ // Remove the response if it is marked as deleted.
+ if(m.deleted)
+ {
+ if((id in master.infoList) == null)
+ writefln("Cannot delete info %s, does not exist", id);
+ else master.infoList.remove(id);
+ }
+ else
+ // Just plain copy it in.
+ master.infoList[id] = m;
+ }
+ }
+
+ // Here we have to fix inconsistencies. A good case example is the
+ // dialogue "Apelles Matius" in trib/blood. Trib creates a
+ // dialogue of a few items, bloodmoon adds another. But since the
+ // two are independent, the list in bloodmoon does not change the
+ // one in trib but rather creates a new one. In other words, we
+ // will have to deal with the possibility of several "independent"
+ // lists within each topic. We can do this by looking for several
+ // start points (ie. infos with prev="") and just latch them onto
+ // each other. I'm not sure it gives the correct result,
+ // though. For example, which list comes first would be rather
+ // arbitrarily decided by the order we traverse the infoList AA. I
+ // will just have to assume that they truly are "independent".
+
+ // There still seems to be a problem though. Bloodmoon overwrites
+ // some stuff added by Tribunal, see "Boots of the Apostle" for an
+ // example. Looks like the editor handles it just fine... We need
+ // to make sure that all the items in our AA are put into the
+ // list, and in the right place too. We obviously cannot fully
+ // trust the 'next' and 'prev' fields, but they are the only
+ // guidance we have. Deal with it later!
+
+ // At this point we assume "master" to contain the final dialogue
+ // list, so at this point we can set it in stone.
+ infoList.length = master.infoList.length;
+
+ // Find the first entry
+ DialInfoLoad* starts[]; // starting points for linked lists
+ DialInfoLoad *current;
+ foreach(char[] id, ref DialInfoLoad l; master.infoList)
+ if(l.prev == "") starts ~= &l;
+
+ foreach(int num, ref DialInfo m; infoList)
+ {
+ if(current == null)
+ {
+ if(starts.length == 0)
+ {
+ writefln("Error: No starting points!");
+ infoList.length = num;
+ break;
+ }
+ // Pick the next starting point
+ current = starts[0];
+ starts = starts[1..$];
+ }
+ m.copy(*current, this);
+
+ if((*current).next == "")
+ current = null;
+ else
+ {
+ current = (*current).next in master.infoList;
+ if(current == null)
+ {
+ writefln("Error in dialouge info lookup!");
+ break;
+ }
+ }
+ }
+ if(infoList.length != master.infoList.length)
+ writefln("Dialogue list lengths do not match, %d != %d",
+ infoList.length, master.infoList.length);
+ }
+}
++/
diff --git a/esm/loaddoor.d b/esm/loaddoor.d
new file mode 100644
index 0000000000..22bc806a10
--- /dev/null
+++ b/esm/loaddoor.d
@@ -0,0 +1,49 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loaddoor.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loaddoor;
+import esm.imports;
+
+/*
+ * Doors
+ */
+
+struct Door
+{
+ LoadState state;
+ char[] id, name;
+
+ MeshIndex model;
+ Script *script;
+ Sound* openSound, closeSound;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNOString("FNAM");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ openSound = getHNOPtr!(Sound)("SNAM", sounds);
+ closeSound = getHNOPtr!(Sound)("ANAM", sounds);
+ }}
+}
+ListID!(Door) doors; // Break on through
diff --git a/esm/loadench.d b/esm/loadench.d
new file mode 100644
index 0000000000..3a882d3b73
--- /dev/null
+++ b/esm/loadench.d
@@ -0,0 +1,70 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadench.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadench;
+import esm.imports;
+
+/*
+ * Enchantments
+ */
+
+struct Enchantment
+{
+ enum Type : int
+ {
+ CastOnce = 0,
+ WhenStrikes = 1,
+ WhenUsed = 2,
+ ConstantEffect = 3
+ }
+
+ align(1) struct ENDTstruct
+ {
+ Type type;
+ int cost;
+ int charge;
+ int autocalc; // Guessing this is 1 if we are supposed to auto
+ // calculate
+
+ static assert(ENDTstruct.sizeof == 16);
+ }
+ ENDTstruct data;
+
+ EffectList effects;
+
+ char[] id;
+ LoadState state;
+
+ void load()
+ {with(esFile){
+ readHNExact(&data, data.sizeof, "ENDT");
+
+ effects = getRegion().getBuffer!(ENAMstruct)(0,1);
+ while(isNextSub("ENAM"))
+ {
+ effects.length = effects.length + 1;
+ readHExact(&effects.array[$-1], effects.array[$-1].sizeof);
+ }
+ }}
+}
+ListID!(Enchantment) enchants;
diff --git a/esm/loadfact.d b/esm/loadfact.d
new file mode 100644
index 0000000000..d7a234b1e0
--- /dev/null
+++ b/esm/loadfact.d
@@ -0,0 +1,106 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadfact.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadfact;
+
+import esm.imports;
+
+/*
+ * Faction definitions
+ */
+
+// Requirements for each rank
+align(1) struct RankData
+{
+ int attribute1, attribute2; // Attribute level
+
+ int skill1, skill2; // Skill level (faction skills given in
+ // skillID below.) You need one skill at
+ // level 'skill1' and two skills at level
+ // 'skill2' to advance to this rank.
+
+ int factReaction; // Reaction from faction members
+}
+
+struct Faction
+{
+ char[] id, name;
+ LoadState state;
+
+ align(1) struct FADTstruct
+ {
+ // Which attributes we like
+ int attribute1, attribute2;
+
+ RankData rankData[10];
+
+ int skillID[6]; // IDs of skills this faction require
+ int unknown; // Always -1?
+ uint isHidden; // 1 - hidden from player
+
+ static assert(RankData.sizeof == 20);
+ static assert(FADTstruct.sizeof == 240);
+ }
+
+ FADTstruct data;
+
+ RankData rankData[10];
+
+ struct Reaction
+ {
+ Faction* faction;
+ int reaction;
+ }
+
+ RegionBuffer!(Reaction) reactions;
+
+ char[] ranks[10]; // Name of faction ranks (may be empty for NPC
+ // factions)
+ void load()
+ {with(esFile){
+ name = getHNString("FNAM");
+
+ // Read rank names. These are optional.
+ ranks[] = null;
+ int i = 0;
+ while(isNextSub("RNAM") && i<10) ranks[i++] = getHString();
+ if(isNextSub("RNAM")) fail("Too many rank names");
+
+ // Main data struct
+ readHNExact(&data, data.sizeof, "FADT");
+
+ if(data.isHidden > 1) fail("Unknown flag!");
+
+ // Read faction response values
+ reactions = getRegion().getBuffer!(Reaction)();
+ while(hasMoreSubs())
+ {
+ Reaction r;
+ r.faction = getHNPtr!(Faction)("ANAM", factions);
+ r.reaction = getHNInt("INTV");
+ reactions ~= r;
+ }
+ }}
+}
+
+ListID!(Faction) factions;
diff --git a/esm/loadglob.d b/esm/loadglob.d
new file mode 100644
index 0000000000..32ee3c770e
--- /dev/null
+++ b/esm/loadglob.d
@@ -0,0 +1,61 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadglob.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadglob;
+import esm.imports;
+
+/*
+ * Global script variables
+ */
+
+struct Global
+{
+ LoadState state;
+ char[] id;
+
+ float value;
+
+ VarType type;
+
+ void load()
+ {
+ VarType t;
+ char[] tmp = esFile.getHNString("FNAM");
+ switch(tmp)
+ {
+ case "s": t = VarType.Short; break;
+ case "l": t = VarType.Int; break;
+ case "f": t = VarType.Float; break;
+ default:
+ esFile.fail("Illegal global variable type " ~ tmp);
+ }
+ if(state == LoadState.Previous && t != type)
+ esFile.fail(format("Variable changed type from %d to %d",
+ cast(int)type, cast(int)t));
+
+ type = t;
+
+ value = esFile.getHNFloat("FLTV");
+ }
+}
+ListID!(Global) globals;
diff --git a/esm/loadgmst.d b/esm/loadgmst.d
new file mode 100644
index 0000000000..213cf6613a
--- /dev/null
+++ b/esm/loadgmst.d
@@ -0,0 +1,305 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadgmst.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadgmst;
+import esm.imports;
+
+/*
+ * Game setting
+ */
+
+struct GameSetting
+{
+ LoadState state;
+ char[] id;
+
+ union
+ {
+ char[] str;
+ int i;
+ float f;
+ }
+ VarType type;
+
+ // These functions check if this game setting is one of the "dirty"
+ // GMST records found in many mods. These are due to a serious bug
+ // in the official TES3 editor. It only occurs in the newer editor
+ // versions that came with Tribunal and Bloodmoon, and only if a
+ // modder tries to make a mod without loading the corresponding
+ // expansion master file. For example, if you have Tribunal
+ // installed and try to make a mod without loading Tribunal.esm, the
+ // editor will insert these GMST records.
+
+ // The values of these "dirty" records differ in general from their
+ // values as defined in Tribunal.esm and Bloodmoon.esm, and are
+ // always set to the same "default" values. Most of these values are
+ // nonsensical, ie. changing the "Seller Max" string to "Max Sale",
+ // or change the stats of werewolves to useless values like 1. Some
+ // of them break certain spell effects.
+
+ // It is most likely that these values are just leftover values from
+ // an early stage of development that are inserted as default values
+ // by the editor code. They are supposed to be overridden when the
+ // correct esm file is loaded. When it isn't loaded, you get stuck
+ // with the initial value, and this gets written to every mod for
+ // some reason.
+
+ // Bethesda themselves have fallen for this bug. If you install both
+ // Tribunal and Bloodmoon, the updated Tribunal.esm will contain the
+ // dirty GMST settings from Bloodmoon, and Bloodmoon.esm will
+ // contain some of the dirty settings from Tribunal. In other words,
+ // this bug affects the game EVEN IF YOU DO NOT USE ANY MODS!
+
+ // The guys at Bethesda are well aware of this bug (and many
+ // others), as the mod community and fan base complained about them
+ // for a long time. But it was never fixed.
+
+ // There are several programs available to help modders remove these
+ // records from their files, but not all modders use them, and they
+ // really shouldn't have to. In this file we choose instead to
+ // reject all the corrupt values at load time.
+
+ // Checks if the current game setting is one of the "dirty" ones as
+ // described above. TODO: I have not checked this against other
+ // sources yet, do that later. Currently recognizes 22 values for
+ // tribunal and 50 for bloodmoon.
+
+ // Checks for dirty tribunal values. These will be ignored if found
+ // in any file except when they are found in "Tribunal.esm".
+ bool isDirtyTribunal()
+ {
+ bool result = false;
+ void cI(int ii) { if(ii == i) result = true; }
+ void cF(float ff) { if(ff == f) result = true; }
+ void cS(char[] ss) { if(ss == str) result = true; }
+
+ // Here, id contains the game setting name, and we check the
+ // setting for certain values. If it matches, this is a "dirty"
+ // entry. The correct entry (as defined in Tribunal and Bloodmoon
+ // esms) are given in the comments. Many of the values are
+ // correct, and are marked as 'same'. We still ignore them though,
+ // as they are still in the wrong file and might override custom
+ // values from other mods.
+
+ switch(id)
+ {
+ // Strings
+ case "sProfitValue": cS("Profit Value"); break; // 'Profit:'
+ case "sEditNote": cS("Edit Note"); break; // same
+ case "sDeleteNote": cS("Delete Note?"); break; // same
+ case "sMaxSale": cS("Max Sale"); break; // 'Seller Max'
+ case "sMagicFabricantID": cS("Fabricant"); break; // 'Fabricant_summon'
+ case "sTeleportDisabled": cS("Teleportation magic does not work here.");
+ break; // same
+ case "sLevitateDisabled": cS("Levitation magic does not work here.");
+ break; // same
+ case "sCompanionShare": cS("Companion Share"); break; // 'Share'
+ case "sCompanionWarningButtonOne": cS("Let the mercenary quit.");
+ break; // same
+ case "sCompanionWarningButtonTwo": cS("Return to Companion Share display."); break; // same
+ case "sCompanionWarningMessage": cS("Your mercenary is poorer now than when he contracted with you. Your mercenary will quit if you do not give him gold or goods to bring his Profit Value to a positive value."); break;
+ // 'Your mercenary is poorer now than when he contracted with
+ // you. Your mercenary will quit if you do not give him gold
+ // or goods to bring his Profit to a positive value.'
+ // [The difference here is "Profit Value" -> "Profit"}
+
+ // Strings that matches the id
+ case "sEffectSummonFabricant": // 'Summon Fabricant'
+ cS(id);
+ break;
+
+ default:
+ }
+ return result;
+ }
+
+ // Bloodmoon variant
+ bool isDirtyBloodmoon()
+ {
+ bool result = false;
+ void cI(int ii) { if(ii == i) result = true; }
+ void cF(float ff) { if(ff == f) result = true; }
+ void cS(char[] ss) { if(ss == str) result = true; }
+
+ switch(id)
+ {
+ // Strings
+ case "sWerewolfPopup": cS("Werewolf"); break; // same
+ case "sWerewolfRestMessage": cS("You cannot rest in werewolf form.");
+ break; // same
+ case "sWerewolfRefusal": cS("You cannot do this as a werewolf.");
+ break; // same
+ case "sWerewolfAlarmMessage": cS("You have been detected changing from a werewolf state."); break;
+ // 'You have been detected as a known werewolf.'
+
+ // Strings that matches the id
+ case "sMagicCreature01ID": // 'BM_wolf_grey_summon'
+ case "sMagicCreature02ID": // 'BM_bear_black_summon'
+ case "sMagicCreature03ID": // 'BM_wolf_bone_summon'
+ case "sMagicCreature04ID": // same
+ case "sMagicCreature05ID": // same
+ case "sEffectSummonCreature01": // 'Calf Wolf'
+ case "sEffectSummonCreature02": // 'Calf Bear'
+ case "sEffectSummonCreature03": // 'Summon Bonewolf'
+ case "sEffectSummonCreature04": // same
+ case "sEffectSummonCreature05": // same
+ cS(id);
+ break;
+
+ // Integers
+ case "iWereWolfBounty": cI(10000); break; // 1000
+ case "iWereWolfFightMod": cI(100); break; // same
+ case "iWereWolfFleeMod": cI(100); break; // same
+ case "iWereWolfLevelToAttack": cI(20); break; // same
+
+ // Floats
+ case "fFleeDistance": cF(3000); break; // same
+ case "fCombatDistanceWerewolfMod": cF(0.3); break; // same
+ case "fWereWolfFatigue": cF(400); break; // same
+ case "fWereWolfEnchant": cF(1); break; // 0
+ case "fWereWolfArmorer": cF(1); break; // 0
+ case "fWereWolfBlock": cF(1); break; // 0
+ case "fWereWolfSneak": cF(1); break; // 95
+ case "fWereWolfDestruction": cF(1); break; // 0
+ case "fWereWolfEndurance": cF(150); break; // same
+ case "fWereWolfConjuration": cF(1); break; // 0
+ case "fWereWolfRestoration": cF(1); break; // 0
+ case "fWereWolfAthletics": cF(150); break; // 50
+ case "fWereWolfLuck": cF(1); break; // 25
+ case "fWereWolfSilverWeaponDamageMult": cF(1.5); break; // 2
+ case "fWereWolfMediumArmor": cF(1); break; // 0
+ case "fWereWolfShortBlade": cF(1); break; // 0
+ case "fWereWolfAcrobatics": cF(150); break; // 80
+ case "fWereWolfSpeechcraft": cF(1); break; // 0
+ case "fWereWolfAlteration": cF(1); break; // 0
+ case "fWereWolfIllusion": cF(1); break; // 0
+ case "fWereWolfLongBlade": cF(1); break; // 0
+ case "fWereWolfMarksman": cF(1); break; // 0
+ case "fWereWolfHandtoHand": cF(100); break; // same
+ case "fWereWolfIntellegence": cF(1); break; // 0
+ case "fWereWolfAlchemy": cF(1); break; // 0
+ case "fWereWolfUnarmored": cF(100); break; // same
+ case "fWereWolfAxe": cF(1); break; // 0
+ case "fWereWolfRunMult": cF(1.5); break; // 1.3
+ case "fWereWolfMagicka": cF(100); break; // same
+ case "fWereWolfAgility": cF(150); break; // same
+ case "fWereWolfBluntWeapon": cF(1); break; // 0
+ case "fWereWolfSecurity": cF(1); break; // 0
+ case "fWereWolfPersonality": cF(1); break; // 0
+ case "fWereWolfMerchantile": cF(1); break; // 0
+ case "fWereWolfHeavyArmor": cF(1); break; // 0
+ case "fWereWolfSpear": cF(1); break; // 0
+ case "fWereWolfStrength": cF(150); break; // same
+ case "fWereWolfHealth": cF(2); break; // same
+ case "fWereWolfMysticism": cF(1); break; // 0
+ case "fWereWolfLightArmor": cF(1); break; // 0
+ case "fWereWolfWillPower": cF(1); break; // 0
+ case "fWereWolfSpeed": cF(150); break; // 90
+
+ default:
+ }
+ return result;
+ }
+
+ char[] toString()
+ {
+ if(type == VarType.Int) return format(i);
+ else if(type == VarType.Float) return format(f);
+ else if(type == VarType.String) return str;
+ else if(type == VarType.None) return "(no value)";
+ }
+
+ void load()
+ {
+ // We are apparently allowed not to have any value at all.
+ if(!esFile.hasMoreSubs())
+ {
+ if(state == LoadState.Previous)
+ writefln("Warning: Overwriting game setting %s with void value", id);
+ type = VarType.None;
+ return;
+ }
+
+ // If this is not the first record of this type to be loaded, then
+ // we do a little "trick" to avoid overwriting the current data
+ // with one of the dirty GMSTs.
+ if(state == LoadState.Previous)
+ {
+ // Load the data in a temporary game setting instead
+ GameSetting g;
+ g.state = LoadState.Unloaded;
+ g.id = id;
+ g.load();
+
+ // Only copy it if it was valid
+ if(g.type != VarType.Ignored)
+ {
+ // Don't allow a change of type, unless the setting we are
+ // overwriting is an ignored value.
+ if(g.type != type && type != VarType.Ignored)
+ esFile.fail(format("GMST changed type from %d to %d",
+ cast(int)type, cast(int)g.type));
+
+ str = g.str; // This will copy all the data no matter what
+ // type it is
+ }
+ return;
+ }
+
+ // Actually load some data
+ esFile.getSubName();
+ switch(esFile.retSubName())
+ {
+ case "STRV":
+ str = esFile.getHString();
+ type = VarType.String;
+ break;
+ case "INTV":
+ i = esFile.getHInt();
+ type = VarType.Int;
+ break;
+ case "FLTV":
+ f = esFile.getHFloat();
+ type = VarType.Float;
+ break;
+ default:
+ esFile.fail("Unwanted subrecord type");
+ }
+
+ SpecialFile spf = esFile.getSpecial();
+
+ // Check if this is one of the dirty values mentioned above. If it
+ // is, we set the VarType to Ignored. This leaves the posibility
+ // that this becomes the final var type, for example if you load a
+ // plugin with tribunal-gmst settings without loading tribunal
+ // first. (Then there would only exist dirty values for this
+ // settings, no "real" value.) But this is not likely a problem,
+ // since these empty values will never be used and we can check
+ // for them in any case.
+
+ if( ( spf != SpecialFile.Tribunal && isDirtyTribunal() ) ||
+ ( spf != SpecialFile.Bloodmoon && isDirtyBloodmoon() ) )
+ type = VarType.Ignored;
+ }
+}
+ListID!(GameSetting) gameSettings;
diff --git a/esm/loadinfo.d b/esm/loadinfo.d
new file mode 100644
index 0000000000..58ef857b9c
--- /dev/null
+++ b/esm/loadinfo.d
@@ -0,0 +1,249 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadinfo.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadinfo;
+
+import esm.imports;
+import esm.loaddial;
+import esm.loadrace;
+import esm.loadclas;
+import esm.loadfact;
+
+/*
+ * Dialogue information. These always follow a DIAL record and form a
+ * linked list
+ */
+
+struct DialInfo
+{
+ enum Gender : byte
+ {
+ Male = 0,
+ Female = 1,
+ NA = -1
+ }
+
+ align(1) struct DATAstruct
+ {
+ uint unknown1;
+ uint disposition;
+ byte rank; // Rank of NPC
+ Gender gender; // 0 - male, 1 - female, 0xff - none
+ byte PCrank; // Player rank
+ byte unknown2;
+
+ static assert(DATAstruct.sizeof==12);
+ }
+ DATAstruct data;
+
+ // The rules for whether or not we will select this dialog item.
+ struct SelectStruct
+ {
+ char[] selectRule; // This has a complicated format
+ union { float f; int i; }
+ VarType type;
+ }
+
+ // Id of prevous and next in linked list
+ char[] id, prev, next;
+
+ // Various references used in determining when to select this item.
+ Item actor;
+ Race *race;
+ Class *cls;
+ Faction* npcFaction, pcFaction;
+ bool factionLess; // ONLY select this item the NPC is not part of any faction.
+ char[] cell; // Use this to compare with current cell name
+ RegionBuffer!(SelectStruct) selects; // Selection rules
+
+ SoundIndex sound; // Sound to play when this is selected.
+ char[] response; // The text content of this info item.
+
+ // TODO: This should probably be compiled at load time?
+ char[] resultScript; // Result script (uncompiled) - is run if this
+ // dialog item is selected.
+
+ // Journal quest indices (introduced with the quest system in tribunal)
+ enum Quest
+ {
+ None, Name, Finished, Restart
+ }
+
+ bool deleted;
+
+ Quest questStatus;
+
+ void load(DialogueType tp)
+ {with(esFile){
+ id = getHNString("INAM");
+ prev = getHNString("PNAM");
+ next = getHNString("NNAM");
+
+ // Not present if deleted
+ if(isNextSub("DATA"))
+ readHExact(&data, data.sizeof);
+
+ // What follows is terrible spaghetti code, but I like it! ;-) In
+ // all seriousness, it's an attempt to make things faster, since
+ // INFO is by far the most common record type.
+
+ // subName is a slice of the original, so it changes when new sub
+ // names are read.
+ getSubName();
+ char[] subName = retSubName();
+
+ if(subName == "ONAM")
+ {
+ actor = actors.lookup(tmpHString());
+ if(isEmptyOrGetName) return;
+ } else actor.i = null;
+ if(subName == "RNAM")
+ {
+ race = cast(Race*)races.lookup(tmpHString());
+ if(isEmptyOrGetName) return;
+ } else race = null;
+ if(subName == "CNAM")
+ {
+ cls = cast(Class*)classes.lookup(tmpHString());
+ if(isEmptyOrGetName) return;
+ } else cls = null;
+
+ npcFaction = null;
+ factionLess = false;
+ if(subName == "FNAM")
+ {
+ char[] tmp = tmpHString();
+ if(tmp == "FFFF") factionLess = true;
+ else npcFaction = cast(Faction*)factions.lookup(tmp);
+
+ if(isEmptyOrGetName) return;
+ }
+ if(subName == "ANAM")
+ {
+ cell = getHString();
+ if(isEmptyOrGetName) return;
+ } else cell = null;
+ if(subName == "DNAM")
+ {
+ pcFaction = cast(Faction*)factions.lookup(tmpHString());
+ if(isEmptyOrGetName) return;
+ } else pcFaction = null;
+ if(subName == "SNAM")
+ {
+ sound = resources.lookupSound(tmpHString());
+ if(isEmptyOrGetName) return;
+ } else sound = SoundIndex.init;
+ if(subName == "NAME")
+ {
+ response = getHString();
+ if(isEmptyOrGetName) return;
+ } else response = null;
+
+ selects = null;
+ while(subName == "SCVR")
+ {
+ if(selects is null) selects = esmRegion.getBuffer!(SelectStruct)(1,1);
+ else selects.length = selects.length + 1;
+ with(selects.array()[$-1])
+ {
+ selectRule = getHString();
+ isEmptyOrGetName();
+ if(subName == "INTV") type = VarType.Int;
+ else if(subName == "FLTV") type = VarType.Float;
+ else
+ fail("INFO.SCVR must precede INTV or FLTV, not "
+ ~ subName);
+ readHExact(&i, 4);
+ }
+ if(isEmptyOrGetName) return;
+ }
+
+ if(subName == "BNAM")
+ {
+ resultScript = getHString();
+ if(isEmptyOrGetName) return;
+ } else resultScript = null;
+
+ deleted = false;
+ questStatus = Quest.None;
+
+ // Figure out how we check the owner's type
+ if(tp == Dialogue.Type.Journal)
+ {
+ if(subName == "QSTN") questStatus = Quest.Name;
+ else if(subName == "QSTF") questStatus = Quest.Finished;
+ else if(subName == "QSTR") questStatus = Quest.Restart;
+
+ if(questStatus != Quest.None) getHByte();
+ }
+ else if(subName == "DELE") {getHInt(); deleted = true;}
+ else fail("Don't know what to do with " ~ subName ~ " here.");
+ }}
+}
+
+/*
+ // We only need to put each item in ONE list. For if your NPC
+ // matches this response, then it must match ALL criteria, thus it
+ // will have to look up itself in all the lists. I think the order
+ // is well optimized in making the lists as small as possible.
+ if(this.actor.index != -1) actorDial[this.actor][parent]++;
+ else if(cell != "") cellDial[cell][parent]++;
+ else if(this.Class != -1) classDial[this.Class][parent]++;
+ else if(this.npcFaction != -1)
+ factionDial[this.npcFaction][parent]++;
+ else if(this.race != -1) raceDial[this.race][parent]++;
+ else allDial[parent]++; // Lists dialogues that might
+ // apply to all npcs.
+ */
+
+struct DialInfoList
+{
+ LoadState state;
+
+ DialInfo d;
+
+ void load(DialogueType type)
+ {
+ d.load(type);
+ }
+}
+
+// List of dialogue topics (and greetings, voices, etc.) that
+// reference other objects. Eg. raceDial is indexed by the indices of
+// all races referenced. The value of raceDial is a new AA, which is
+// basically used as a map (the int value is just a count and isn't
+// used for anything important.) The indices (or elements of the map)
+// are the dialogues that reference the given race. I use an AA
+// instead of a list or array, since each dialogue can be added lots
+// of times.
+
+/*
+int allDial[Dialogue*];
+int classDial[int][Dialogue*];
+int factionDial[int][Dialogue*];
+int actorDial[Item][Dialogue*];
+// If I look up cells on cell load, I don't have to resolve these
+// names into anything!
+int cellDial[char[]][Dialogue*];
+int raceDial[int][Dialogue*];
+*/
diff --git a/esm/loadingr.d b/esm/loadingr.d
new file mode 100644
index 0000000000..52e11defc0
--- /dev/null
+++ b/esm/loadingr.d
@@ -0,0 +1,63 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadingr.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadingr;
+import esm.imports;
+
+/*
+ * Alchemy ingredient
+ */
+
+struct Ingredient
+{
+ align(1) struct IRDTstruct
+ {
+ float weight;
+ int value;
+ int[4] effectID; // Effect, 0 or -1 means none
+ SkillEnum[4] skills; // Skill related to effect
+ Attribute[4] attributes; // Attribute related to effect
+
+ static assert(IRDTstruct.sizeof==56);
+ }
+
+ IRDTstruct data;
+
+ LoadState state;
+
+ char[] id, name;
+ MeshIndex model;
+ IconIndex icon;
+ Script *script;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNString("FNAM");
+ readHNExact(&data, data.sizeof, "IRDT");
+
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ icon = getOIcon();
+ }}
+}
+ListID!(Ingredient) ingreds;
diff --git a/esm/loadlevlist.d b/esm/loadlevlist.d
new file mode 100644
index 0000000000..a51a4725fa
--- /dev/null
+++ b/esm/loadlevlist.d
@@ -0,0 +1,201 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadlevlist.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadlevlist;
+import esm.imports;
+import esm.loadcrea;
+
+import util.random;
+
+/* Outdated comments:
+ *
+ * Leveled lists. Since these have identical layout, I only bothered
+ * to implement it once.
+ *
+ * We should later implement the ability to merge leveled lists from
+ * several files.
+ *
+ * The Item indices can point to several different lists (potions,
+ * weapons, armor, etc.) and this is a problem also for npcs,
+ * containers and cells. Deal with it in a uniform way. In fact, we
+ * won't have to make any difference between the creature and item
+ * lists.
+ *
+ * EDIT 2007.08.18: Looks like DMD is exploding from template forward
+ * references again. It works for very small cases, so I assume it
+ * should be legal in our case also, and that this is a BUG. I have
+ * cut it down and am going to report it. Moving ListItem out of the
+ * struct helped, but I get the same problem in other places, where it
+ * is not possible to fix. I should probably cut down the other case
+ * as well...
+ *
+ * EDIT 2007.09.09: In Monster we managed to cut down template
+ * forwarding with LinkedList by making the list use Value pointers
+ * instead of Node pointers. If I do the same for HashTable and move
+ * the value to the top, I JUST might be able to work around this DMD
+ * bug.
+ *
+ * UPDATE: Well, it works now, but only if you compile with DMD using
+ * the all-files-on-one-line approach. It will not work with DSSS
+ * until these bugs are gone.
+ */
+
+// Moved here for template bug reasons...
+struct _ListItem
+{
+ Item item; // Definded in records.d
+ short level;
+}
+
+struct LeveledListT(bool creature)
+{
+ char[] id;
+ LoadState state;
+
+ enum Flags
+ {
+ AllLevels = 0x01, // Calculate from all levels <= player
+ // level, not just the closest below
+ // player.
+ Each = 0x02 // Select a new item each time this
+ // list is instantiated, instead of
+ // giving several identical items
+ } // (used when a container has more
+ // than one instance of one leveled
+ // list.)
+
+ alias _ListItem ListItem;
+
+ Flags flags;
+ ubyte chanceNone; // Chance that none are selected (0-255??)
+ ListItem list[];
+
+ // Get a random creature from this list
+ Creature* instCreature(short PCLevel)
+ in
+ {
+ assert(creature);
+ }
+ body
+ {
+ int index = instIndex(PCLevel);
+ if(index == -1) return null; // No creature was selected
+
+ Creature *c = cast(Creature*)list[index].item.getPtr(ItemType.Creature);
+ assert(c != null);
+ return c;
+ }
+
+ // Get a random item from the list
+ Item *instItem(short PCLevel)
+ in
+ {
+ assert(!creature);
+ }
+ body
+ {
+ int index = instIndex(PCLevel);
+ if(index == -1) return null;
+
+ return &list[index].item;
+ }
+
+ // Get a random index in the right range
+ int instIndex(short PCLevel)
+ {
+ int top, bottom, i;
+
+ // TODO: Find out if this is indeed correct.
+ // Test if no creature is to be selected
+ if(rnd.randInt(0, 255) < chanceNone) return -1;
+
+ // Find the highest item below or equal to the Player level
+ for(i=list.length-1; i>=0; i--)
+ if(list[i].level <= PCLevel) break;
+ top = i;
+
+ // Select none if the player level is too low
+ if(top < 0) return -1;
+
+ // Now find the lowest element to select
+ if(flags & Flags.AllLevels) bottom = 0;
+ else
+ {
+ // Find the lowest index i such that item[i] has the same
+ // level as item[top].
+ do { i--; }
+ while(i>=0 && list[i].level == list[top].level);
+
+ bottom = i+1;
+ }
+
+ // Select a random item
+ return rnd.randInt(bottom, top);
+ }
+
+ void load()
+ {with(esFile){
+ flags = cast(Flags)getHNUint("DATA");
+ chanceNone = cast(ubyte) getHNByte("NNAM");
+
+ if(hasMoreSubs())
+ list = getRegion().allocateT!(ListItem)(getHNInt("INDX"));
+ else list = null;
+
+ // TODO: Merge the lists here. This can be done simply by adding
+ // the lists together, making sure that they are sorted by
+ // level. A better way might be to exclude repeated items. Also,
+ // some times we don't want to merge lists, just overwrite. Figure
+ // out a way to give the user this option.
+
+ foreach(ref ListItem l; list)
+ {
+ static if(creature)
+ {
+ getSubNameIs("CNAM");
+ l.item = actors.lookup(tmpHString());
+ }
+ else
+ {
+ getSubNameIs("INAM");
+
+ // Morrowind.esm actually contains an invalid reference,
+ // to "imperial cuirass" in the list
+ // "random_imp_armor". We just ignore it to avoid
+ // irritating warning messages.
+ char[] tmp = tmpHString();
+ if( (tmp != "imperial cuirass") || (id != "random_imp_armor")
+ || (getSpecial() != SpecialFile.Morrowind) )
+ l.item = items.lookup(tmp);
+ //l.item = items.lookup(tmpHString());
+ }
+ l.level = getHNShort("INTV");
+ }
+ }}
+}
+
+alias LeveledListT!(false) LeveledItems;
+alias LeveledListT!(true) LeveledCreatures;
+
+ListID!(LeveledCreatures) creatureLists;
+ListID!(LeveledItems) itemLists;
diff --git a/esm/loadligh.d b/esm/loadligh.d
new file mode 100644
index 0000000000..4ff11c8d24
--- /dev/null
+++ b/esm/loadligh.d
@@ -0,0 +1,82 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadligh.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+module esm.loadligh;
+import esm.imports;
+
+/*
+ * Lights
+ */
+
+struct Light
+{
+ char[] id;
+ LoadState state;
+
+ enum Flags : uint
+ {
+ Dynamic = 0x001,
+ Carry = 0x002, // Can be carried
+ Negative = 0x004, // Negative light?
+ Flicker = 0x008,
+ Fire = 0x010,
+ OffDefault = 0x020, // Off by default
+ FlickerSlow = 0x040,
+ Pulse = 0x080,
+ PulseSlow = 0x100
+ }
+
+ align(1) struct LHDTstruct
+ {
+ float weight;
+ int value;
+ int time; // Duration
+ int radius;
+ Color color;
+ Flags flags;
+
+ static assert(LHDTstruct.sizeof == 24);
+ }
+
+ LHDTstruct data;
+
+ char[] name;
+
+ MeshIndex model;
+ IconIndex icon;
+
+ Sound* sound;
+ Script* script;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNOString("FNAM");
+ icon = getOIcon();
+
+ readHNExact(&data, data.sizeof, "LHDT");
+
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ sound = getHNOPtr!(Sound)("SNAM", sounds);
+ }}
+}
+ListID!(Light) lights;
diff --git a/esm/loadlocks.d b/esm/loadlocks.d
new file mode 100644
index 0000000000..bae3e5291e
--- /dev/null
+++ b/esm/loadlocks.d
@@ -0,0 +1,77 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadlocks.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadlocks;
+import esm.imports;
+
+/*
+ * This file covers lockpicks, probes and armor repair items, since
+ * these have nearly identical structure.
+ */
+
+struct Tool
+{
+ align(1) struct Data
+ {
+ float weight;
+ int value;
+
+ float quality; // And when I say nearly identical structure, I
+ int uses; // mean perfectly identical except that these two
+ // variables are swaped for repair items. Don't ask
+ // me why.
+ }
+ static assert(Data.sizeof == 16);
+
+ Data data;
+
+ LoadState state;
+
+ char[] name, id;
+ MeshIndex model;
+ IconIndex icon;
+ Script* script;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNString("FNAM");
+
+ if(isNextSub("LKDT") || isNextSub("PBDT"))
+ readHExact(&data, data.sizeof);
+ else
+ {
+ getSubNameIs("RIDT");
+ readHExact(&data, data.sizeof);
+
+ // Swap t.data.quality and t.data.uses (sigh)
+ float tmp = *(cast(float*)&data.uses);
+ data.uses = *(cast(int*)&data.quality);
+ data.quality = tmp;
+ }
+
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ icon = getOIcon();
+ }}
+}
+ListID!(Tool) lockpicks, probes, repairs;
diff --git a/esm/loadltex.d b/esm/loadltex.d
new file mode 100644
index 0000000000..9e8dba209f
--- /dev/null
+++ b/esm/loadltex.d
@@ -0,0 +1,107 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadltex.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadltex;
+import esm.imports;
+
+/*
+ * Texture used for texturing landscape?
+ *
+ * Not sure how to store these yet. They are probably indexed by
+ * 'num', not 'id', but I don't know for sure. And num is not unique
+ * between files, so one option is to keep a separate list for each
+ * input file (that has LTEX records, of course.) We also need to
+ * resolve references to already existing land textures to save space.
+
+ * I'm not sure if it is even possible to override existing land
+ * textures, probably not. I'll have to try it, and have to mimic the
+ * behaviour of morrowind. First, check what you are allowed to do in
+ * the editor. Then make an esp which changes a commonly used land
+ * texture, and see if it affects the game.
+ */
+
+class LandTextureList : ListKeeper
+{
+ // Number of textures inserted in this file.
+ int num;
+
+ alias RegionBuffer!(TextureIndex) TextureList;
+
+ // Contains the list of land textures for each file, indexed by
+ // file. TODO: Use some handle system here too instead of raw
+ // filename?
+ HashTable!(char[], TextureList, ESMRegionAlloc) files;
+
+ // The texture list for the current file
+ TextureList current;
+
+ this()
+ {
+ num = 0;
+ endFile();
+
+ // The first file (Morrowind.esm) typically needs a little more
+ // than most others
+
+ current = esmRegion.getBuffer!(TextureIndex)(0,120);
+
+ // Overkill, just leave it as it is
+ //files.rehash(resources.esm.length + resources.esp.length);
+ }
+
+ void load()
+ {with(esFile){
+ getHNString("NAME");
+
+ int n = getHNInt("INTV");
+ if(n != num++)
+ {
+ //writefln("Warning: Wanted land texture %d, got %d", num-1, n);
+ current.length = n;
+ num = n+1;
+ }
+
+ current ~= resources.lookupTexture(getHNString("DATA"));
+ }}
+
+ void endFile()
+ {
+ if(num)
+ {
+ files[esFile.getFilename()] = current;
+ current = esmRegion.getBuffer!(TextureIndex)(0,50);
+ num = 0;
+ }
+ }
+
+ uint length() { return files.length; }
+
+ void* lookup(char[] s) { assert(0); }
+
+ // This requires the correct file to be open
+ TextureIndex lookup(int index)
+ {
+ return files[esFile.getFilename()][index];
+ }
+}
+LandTextureList landTextures;
diff --git a/esm/loadmgef.d b/esm/loadmgef.d
new file mode 100644
index 0000000000..11ffe1e313
--- /dev/null
+++ b/esm/loadmgef.d
@@ -0,0 +1,114 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadmgef.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadmgef;
+
+import esm.imports;
+
+import esm.loadweap;
+import esm.loadstat;
+
+struct MagicEffect
+{
+ enum Flags : int
+ {
+ SpellMaking = 0x0200,
+ Enchanting = 0x0400,
+ Negative = 0x0800 // A harmful effect
+ }
+
+ align(1) struct MEDTstruct
+ {
+ SpellSchool school;
+ float baseCost;
+
+ Flags flags;
+
+ int red, blue, green;
+ float speed, size, sizeCap; // Noe clue
+
+ static assert(MEDTstruct.sizeof == 36);
+ }
+
+ MEDTstruct data;
+
+ IconIndex icon;
+ TextureIndex particle;
+ Static* casting, hit, area;
+ Weapon* bolt;
+ Sound* castSound, boltSound, hitSound, areaSound;
+ char[] description;
+
+ int index;
+
+ void load()
+ {with(esFile){
+ readHNExact(&data, data.sizeof,"MEDT");
+
+ icon = getIcon();
+ particle = getOTexture("PTEX");
+
+ boltSound = getHNOPtr!(Sound)("BSND", sounds);
+ castSound = getHNOPtr!(Sound)("CSND", sounds);
+ hitSound = getHNOPtr!(Sound)("HSND", sounds);
+ areaSound = getHNOPtr!(Sound)("ASND", sounds);
+
+ casting = getHNOPtr!(Static)("CVFX", statics);
+ bolt = getHNOPtr!(Weapon)("BVFX", weapons);
+ hit = getHNOPtr!(Static)("HVFX", statics);
+ area = getHNOPtr!(Static)("AVFX", statics);
+
+ description = getHNOString("DESC");
+ }}
+}
+
+class MagicEffectList : ListKeeper
+{
+ // 0-136 in Morrowind
+ // 137 in Tribunal
+ // 138-140 in Bloodmoon (also changes 64?)
+ // 141-142 are summon effects introduced in bloodmoon, but not used
+ // there. They can be redefined in mods by setting the name in GMST
+ // sEffectSummonCreature04/05 creature id in
+ // sMagicCreature04ID/05ID.
+ MagicEffect[143] list;
+
+ override:
+
+ void load()
+ {
+ int index = esFile.getHNInt("INDX");
+
+ if(index < 0 || index >= list.length)
+ esFile.fail("Invalid magic effect number " ~ .toString(index));
+
+ list[index].load();
+ }
+
+ void endFile() {}
+
+ uint length() { return list.length; }
+
+ void* lookup(char[] s) { assert(0); }
+}
+MagicEffectList effects;
diff --git a/esm/loadmisc.d b/esm/loadmisc.d
new file mode 100644
index 0000000000..576b5ff302
--- /dev/null
+++ b/esm/loadmisc.d
@@ -0,0 +1,63 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadmisc.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadmisc;
+import esm.imports;
+
+/*
+ * Misc inventory items, basically things that have no use but can be
+ * carried, bought and sold. It also includes keys.
+ */
+
+struct Misc
+{
+ struct MCDTstruct
+ {
+ float weight;
+ int value;
+ int isKey; // There are many keys in Morrowind.esm that has this
+ // set to 0. TODO: Check what this field corresponds to
+ // in the editor.
+
+ static assert(MCDTstruct.sizeof == 12);
+ }
+ MCDTstruct data;
+
+ char[] id, name;
+
+ LoadState state;
+
+ MeshIndex model;
+ IconIndex icon;
+ Script *script;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNOString("FNAM");
+ readHNExact(&data, data.sizeof, "MCDT");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ icon = getOIcon();;
+ }}
+}
+ListID!(Misc) miscItems;
diff --git a/esm/loadnpc.d b/esm/loadnpc.d
new file mode 100644
index 0000000000..535af9195d
--- /dev/null
+++ b/esm/loadnpc.d
@@ -0,0 +1,171 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadnpc.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadnpc;
+
+import esm.imports;
+
+import esm.loadcont;
+import esm.loadrace;
+import esm.loadclas;
+import esm.loadfact;
+import esm.loadbody;
+
+/*
+ * NPC definition
+ */
+
+struct NPC
+{
+ // Services
+ enum Services : int
+ {
+ // Buys
+ Weapon = 0x00001,
+ Armor = 0x00002,
+ Clothing = 0x00004,
+ Books = 0x00008,
+ Ingredients = 0x00010,
+ Picks = 0x00020,
+ Probes = 0x00040,
+ Lights = 0x00080,
+ Apparatus = 0x00100,
+ RepairItem = 0x00200,
+ Misc = 0x00400,
+
+ // Services
+ Spells = 0x00800,
+ MagicItems = 0x01000,
+ Potions = 0x02000,
+ Training = 0x04000, // What skills?
+ Spellmaking = 0x08000,
+ Enchanting = 0x10000,
+ Repair = 0x20000
+ }
+
+ enum Flags
+ {
+ Female = 0x0001,
+ Essential = 0x0002,
+ Respawn = 0x0004,
+ Autocalc = 0x0008,
+ Skeleton = 0x0400, // Skeleton blood effect (white)
+ Metal = 0x0800 // Metal blood effect (golden?)
+ }
+
+ align(1) struct NPDTstruct52
+ {
+ short level;
+ byte strength, intelligence, willpower, agility,
+ speed, endurance, personality, luck;
+ byte skills[27];
+ //byte reputation; // Total confusion!
+ short health, mana, fatigue;
+ byte disposition;
+ byte reputation; // Was "factionID", but that makes no sense.
+ byte rank, unknown, u2;
+ int gold;
+ }
+ align(1) struct NPDTstruct12
+ {
+ short level;
+ byte disposition, reputation, rank,
+ unknown1, unknown2, unknown3;
+ int gold; // ?? Not sure
+ }
+
+ align(1) struct AIDTstruct
+ {
+ // These are probabilities
+ byte hello, u1, fight, flee, alarm, u2, u3, u4;
+ // The u's might be the skills that this NPC can train you in
+ Services services;
+
+ static assert(AIDTstruct.sizeof == 12);
+ }
+
+ static assert(NPDTstruct52.sizeof==52);
+ static assert(NPDTstruct12.sizeof==12);
+
+ union
+ {
+ NPDTstruct52 npdt52;
+ NPDTstruct12 npdt12; // Use this if npdt52.gold == -10
+ }
+
+ Flags flags;
+
+ InventoryList inventory;
+ SpellList spells;
+
+ AIDTstruct AI;
+ bool hasAI;
+
+ LoadState state;
+
+ char[] name, id;
+
+ MeshIndex model;
+ Race* race;
+ Class* cls;
+ Faction* faction;
+ Script* script;
+ BodyPart* hair, head;
+
+ void load()
+ {with(esFile){
+ npdt52.gold = -10;
+
+ model = getOMesh();
+ name = getHNOString("FNAM");
+
+ race = getHNPtr!(Race)("RNAM", races);
+ cls = getHNPtr!(Class)("CNAM", classes);
+ faction = getHNPtr!(Faction)("ANAM", factions);
+ head = getHNPtr!(BodyPart)("BNAM", bodyParts);
+ hair = getHNPtr!(BodyPart)("KNAM", bodyParts);
+
+ script = getHNOPtr!(Script)("SCRI", scripts);
+
+ getSubNameIs("NPDT");
+ getSubHeader();
+ if(getSubSize() == 52) readExact(&npdt52, npdt52.sizeof);
+ else if(getSubSize() == 12) readExact(&npdt12, npdt12.sizeof);
+ else fail("NPC_NPDT must be 12 or 52 bytes long");
+
+ flags = cast(Flags) getHNInt("FLAG");
+
+ inventory.load();
+ spells.load();
+
+ if(isNextSub("AIDT"))
+ {
+ readHExact(&AI, AI.sizeof);
+ hasAI = true;
+ }
+ else hasAI = false;
+
+ skipRecord();
+ }}
+}
+ListID!(NPC) npcs;
diff --git a/esm/loadnpcc.d b/esm/loadnpcc.d
new file mode 100644
index 0000000000..a0425116d2
--- /dev/null
+++ b/esm/loadnpcc.d
@@ -0,0 +1,104 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadnpcc.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadnpcc;
+import esm.imports;
+
+/*
+ * NPC change information (found in savegame files only). We can't
+ * read these yet.
+ *
+ * Some general observations about savegames:
+ *
+ * Magical items/potions/spells/etc are added normally as new ALCH,
+ * SPEL, etc. records, with unique numeric identifiers.
+ *
+ * Books with ability enhancements are listed in the save if they have
+ * been read.
+ *
+ * GLOB records set global variables.
+ *
+ * SCPT records do not define new scripts, but assign values to the
+ * variables of existing ones.
+ *
+ * STLN - stolen items, ONAM is the owner
+ *
+ * GAME - contains a GMDT (game data) of unknown format
+ *
+ * VFXM, SPLM, KLST - no clue
+ *
+ * PCDT - seems to contain a lot of DNAMs, strings?
+ *
+ * FMAP - MAPH and MAPD, probably map data.
+ *
+ * JOUR - the entire bloody journal in html
+ *
+ * QUES - seems to contain all the quests in the game, not just the
+ * ones you have done or begun.
+ *
+ * REGN - lists all regions in the game, even unvisited ones.
+ *
+ * The DIAL/INFO blocks contain changes to characters dialog status.
+ *
+ * Dammit there's a lot of stuff in there! Should really have
+ * suspected as much. The strategy further is to completely ignore
+ * save files for the time being.
+ *
+ * Several records have a "change" variant, like NPCC, CNTC
+ * (contents), and CREC (creature.) These seem to alter specific
+ * instances of creatures, npcs, etc. I have not identified most of
+ * their subrecords yet.
+ *
+ * Several NPCC records have names that begin with "chargen ", I don't
+ * know if it means something special yet.
+ *
+ * The CNTC blocks seem to be instances of leveled lists. When a
+ * container is supposed to contain this leveled list of this type,
+ * but is referenced elsewhere in the file by an INDX, the CNTC with
+ * the corresponding leveled list identifier and INDX will determine
+ * the container contents instead.
+ *
+ * Some classes of objects seem to be altered, and these include an
+ * INDX, which is probably an index used by specific references other
+ * places within the save file. I guess this means 'use this class for
+ * these objects, not the general class.' All the indices I have
+ * encountered so far are zero, but they have been for different
+ * classes (different containers, really) so possibly we start from
+ * zero for each class. This looks like a mess, but is probably still
+ * easier than to duplicate everything. I think WRITING this format
+ * will be harder than reading it.
+ */
+
+struct LoadNPCC
+{
+ void load(TES3File f)
+ {
+ writefln("NPC-changer: %s", f.getHNString("NAME"));
+
+ while(f.hasMoreSubs)
+ {
+ f.getSubName();
+ f.skipHSub();
+ }
+ }
+}
diff --git a/esm/loadrace.d b/esm/loadrace.d
new file mode 100644
index 0000000000..c9ea1ff12c
--- /dev/null
+++ b/esm/loadrace.d
@@ -0,0 +1,82 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadrace.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadrace;
+
+import esm.imports;
+
+import esm.loadbsgn; // Defines PowerList
+
+/*
+ * Race definition
+ */
+
+struct Race
+{
+ align(1) struct SkillBonus
+ {
+ SkillEnum skill;
+ int bonus;
+ }
+ static assert(SkillBonus.sizeof == 8);
+
+ enum Flags
+ {
+ Playable = 0x01,
+ Beast = 0x02
+ }
+
+ align(1) struct RADTstruct
+ {
+ // List of skills that get a bonus
+ SkillBonus bonus[7];
+
+ // Attribute values for male/female
+ int[2] strength, intelligence, willpower, agility,
+ speed, endurance, personality, luck, height,
+ weight;
+
+ Flags flags; // 0x1 - playable, 0x2 - beast race
+
+ static assert(RADTstruct.sizeof == 140);
+ }
+
+ RADTstruct data;
+
+ LoadState state;
+
+ char[] id, name, description;
+
+ SpellList powers;
+
+ void load()
+ {with(esFile){
+ name = getHNString("FNAM");
+ readHNExact(&data, data.sizeof, "RADT");
+
+ powers.load();
+
+ description = getHNOString("DESC");
+ }}
+}
+ListID!(Race) races;
diff --git a/esm/loadregn.d b/esm/loadregn.d
new file mode 100644
index 0000000000..2fb4dd2624
--- /dev/null
+++ b/esm/loadregn.d
@@ -0,0 +1,113 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadregn.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadregn;
+
+import esm.imports;
+
+import esm.loadlevlist;
+
+/*
+ * Region data
+ */
+
+struct Region
+{
+ align(1) struct WEATstruct
+ {
+ byte clear, // Don't know if these are probabilities or what.
+ cloudy,
+ foggy,
+ overcast,
+ rain,
+ thunder,
+ ash,
+ blight,
+ a,b;// Unknown weather, probably snow and something. Only
+ // present in file version 1.3.
+ }
+ static assert(WEATstruct.sizeof==10);
+
+ LoadState state;
+ char[] id, name;
+ WEATstruct data;
+ Color mapColor;
+
+ // Leveled list of creatures you can meet if you sleep outside in
+ // this region.
+ LeveledCreatures *sleepList;
+
+ // Sounds that are played randomly when you are in this region
+ struct SoundRef{ Sound* sound; ubyte chance; }
+
+ RegionBuffer!(SoundRef) soundList;
+
+ void load()
+ {with(esFile){
+ name = getHNString("FNAM");
+
+ if(isVer12())
+ readHNExact(&data, data.sizeof-2, "WEAT");
+ else if(isVer13())
+ readHNExact(&data, data.sizeof, "WEAT");
+ else fail("Don't know what to do in this version");
+
+ // TODO: Calculate weather probabilities here? Or sum them, or
+ // whatever?
+
+ sleepList = getHNOPtr!(LeveledCreatures)("BNAM", creatureLists);
+
+ /*
+ if(getFileType == FileType.Savegame)
+ {
+ // Probably says which weather condition this region is
+ // currently experiencing.
+ writefln("WNAM: ", getHNInt("WNAM"));
+ return
+ }
+ */
+
+ readHNExact(&mapColor, mapColor.sizeof, "CNAM");
+
+ soundList = esmRegion.getBuffer!(SoundRef)(0,20);
+
+ while(isNextSub("SNAM"))
+ {
+ char[32] buffer;
+
+ getSubHeaderIs(33);
+
+ soundList.length = soundList.length + 1;
+ with(soundList.array[$-1])
+ {
+ // Get and chop sound name
+ sound = cast(Sound*) sounds.lookup(getString(buffer));
+
+ // Get sound probability
+ getUByte(chance);
+ }
+ }
+ }}
+}
+
+ListID!(Region) regions;
diff --git a/esm/loadscpt.d b/esm/loadscpt.d
new file mode 100644
index 0000000000..3d7e0bf413
--- /dev/null
+++ b/esm/loadscpt.d
@@ -0,0 +1,140 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadscpt.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadscpt;
+
+import esm.imports;
+
+//private import std.string;
+
+/*
+ * Script
+ */
+
+struct Script
+{
+ char[] id;
+ LoadState state;
+
+ uint numShorts, numLongs, numFloats;
+ char[][] localVars;
+ ubyte[] scriptData;
+ char[] scriptText;
+
+ uint scriptDataSize, localVarSize;
+
+ void load()
+ {with(esFile){
+ /* Assume start of the header has already been read
+ getSubNameIs("SCHD");
+ getSubHeaderIs(52);
+ id = getString(32);
+ */
+
+ getUint(numShorts);
+ getUint(numLongs);
+ getUint(numFloats);
+ getUint(scriptDataSize);
+ getUint(localVarSize);
+
+ /* // In save games this is all that follows the header
+ if(getFileType == FileType.Ess)
+ {
+ getSubNameIs("SLCS");
+ skipHSubSize(12);
+
+ if(isNextSub("SLSD"))
+ skipHSub();
+
+ if(isNextSub("SLFD"))
+ skipHSub();
+
+ if(isNextSub("RNAM"))
+ skipHSubSize(4);
+
+ return;
+ }//*/
+
+ // List of local variables
+ if(isNextSub("SCVR"))
+ {
+ char[] tmp = getRegion().getString(localVarSize);
+
+ readHExact(tmp.ptr, tmp.length);
+
+ // At this point we can't use GC allocations at all, since
+ // our references are not in a root area. Thus the data
+ // could be collected while still in use.
+ localVars = getRegion().allocateT!(char[])
+ ( numShorts + numLongs + numFloats );
+
+ // The tmp buffer is a null-byte separated string list, we
+ // just have to pick out one string at a time.
+ foreach(ref char[] result; localVars)
+ {
+ result = stripz(tmp);
+ tmp = tmp[result.length+1..$];
+ }
+
+ if(tmp.length) fail("Variable table size mismatch");
+ }
+ else localVars = null;
+
+ // Script data
+ scriptData = getRegion().allocate(scriptDataSize);
+ readHNExact(scriptData.ptr, scriptData.length, "SCDT");
+
+ // Script text
+ scriptText = getHNOString("SCTX");
+ }}
+}
+
+class ScriptList : ListID!(Script)
+{
+ this(uint s) { super(s); }
+
+ override char[] getID()
+ {
+ // Script header
+ esFile.getSubNameIs("SCHD");
+ esFile.getSubHeaderIs(52);
+
+ char[] id = esFile.getString(32);
+ // TODO: Handle multiple Main scripts here. With tribunal,
+ // modders got the ability to add 'start scripts' to their mods,
+ // which is a script that is run at startup (I think.) Before
+ // that, there was only one startup script, called
+ // "Main". Although most mods have switched to using startup
+ // scripts, some legacy mods might still overwrite Main, and
+ // this can cause problems if several mods do it. I think the
+ // best course of action is to NEVER overwrite main, but instead
+ // add each with a separate unique name and add them to the
+ // start script list.
+ if(esFile.getSpecial() != SpecialFile.Morrowind && icmp(id,"Main")==0)
+ writefln("Found MAIN script in %s ", esFile.getFilename());
+
+ return id;
+ }
+}
+
+ScriptList scripts;
diff --git a/esm/loadskil.d b/esm/loadskil.d
new file mode 100644
index 0000000000..0d093ca4a7
--- /dev/null
+++ b/esm/loadskil.d
@@ -0,0 +1,85 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadskil.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadskil;
+
+import esm.imports;
+
+/*
+ * Skill information
+ */
+
+enum Specialization : int
+ {
+ Combat = 0,
+ Magic = 1,
+ Stealth = 2
+ }
+
+struct Skill
+{
+ align(1) struct SKDTstruct
+ {
+ Attribute attribute;
+ Specialization specialization; // 0 - Combat, 1 - Magic, 2 - Stealth
+ float useValue[4]; // How much skill improves. Meaning of each
+ // field depends on what skill this is.
+ }
+ static assert(SKDTstruct.sizeof == 24);
+
+ SKDTstruct data;
+
+ char[] description; // Description
+
+ void load()
+ {
+ esFile.readHNExact(&data, data.sizeof, "SKDT");
+ description = esFile.getHNOString("DESC");
+ }
+}
+
+class SkillList : ListKeeper
+{
+ // See SkillEnum in defs.d for a definition of skills
+ Skill[SkillEnum.Length] list;
+ static assert(list.length == 27);
+
+ override:
+
+ void load()
+ {
+ int index = esFile.getHNInt("INDX");
+
+ if(index < 0 || index >= list.length)
+ esFile.fail("Invalid skill number " ~ .toString(index));
+
+ list[index].load();
+ }
+
+ void endFile() {}
+
+ uint length() { return list.length; }
+
+ void *lookup(char[] s) { assert(0); }
+}
+SkillList skills;
diff --git a/esm/loadsndg.d b/esm/loadsndg.d
new file mode 100644
index 0000000000..c966c7a171
--- /dev/null
+++ b/esm/loadsndg.d
@@ -0,0 +1,66 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadsndg.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadsndg;
+import esm.imports;
+
+import esm.loadcrea;
+
+/*
+ * Sound generator. This describes the sounds a creature make.
+ */
+
+struct SoundGenerator
+{
+ enum Type
+ {
+ LeftFoot = 0,
+ RightFoot = 1,
+ SwimLeft = 2,
+ SwimRight = 3,
+ Moan = 4,
+ Roar = 5,
+ Scream = 6,
+ Land = 7
+ }
+
+ // Type
+ Type type;
+
+ char[] id;
+ LoadState state;
+
+ // Which creature this applies to, if any.
+ Creature *creature;
+
+ Sound *sound;
+
+ void load()
+ {with(esFile){
+ this.type = cast(Type)getHNInt("DATA");
+
+ creature = getHNOPtr!(Creature)("CNAM", creatures);
+ sound = getHNPtr!(Sound)("SNAM", sounds);
+ }}
+}
+ListID!(SoundGenerator) soundGens;
diff --git a/esm/loadsoun.d b/esm/loadsoun.d
new file mode 100644
index 0000000000..e06f917cf3
--- /dev/null
+++ b/esm/loadsoun.d
@@ -0,0 +1,54 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadsoun.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadsoun;
+
+import esm.imports;
+
+/*
+ * Sounds
+ */
+
+align(1) struct SOUNstruct
+{
+ ubyte volume, minRange, maxRange;
+}
+static assert(SOUNstruct.sizeof == 3);
+
+struct Sound
+{
+ LoadState state;
+ char[] id;
+
+ SOUNstruct data;
+
+ SoundIndex sound;
+
+ void load()
+ {
+ sound = esFile.getSound();
+ esFile.readHNExact(&data, data.sizeof, "DATA");
+ }
+}
+
+ListID!(Sound) sounds;
diff --git a/esm/loadspel.d b/esm/loadspel.d
new file mode 100644
index 0000000000..6eca2d03cc
--- /dev/null
+++ b/esm/loadspel.d
@@ -0,0 +1,96 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadspel.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadspel;
+import esm.imports;
+
+/*
+ * Spell
+ */
+
+struct SpellList
+{
+ RegionBuffer!(Spell*) list;
+
+ void load()
+ {
+ list = esFile.getRegion().getBuffer!(Spell*)(0,1);
+
+ while(esFile.isNextSub("NPCS"))
+ list ~= esFile.getHPtr!(Spell)(spells);
+ }
+}
+
+struct Spell
+{
+ char[] id;
+ LoadState state;
+
+ enum SpellType
+ {
+ Spell = 0, // Normal spell, must be cast and costs mana
+ Ability = 1, // Inert ability, always in effect
+ Blight = 2, // Blight disease
+ Disease = 3, // Common disease
+ Curse = 4, // Curse (?)
+ Power = 5 // Power, can use once a day
+ }
+
+ enum Flags
+ {
+ Autocalc = 1,
+ PCStart = 2,
+ Always = 4 // Casting always succeeds
+ }
+
+ align(1) struct SPDTstruct
+ {
+ SpellType type;
+ int cost;
+ Flags flags;
+
+ static assert(SPDTstruct.sizeof==12);
+ }
+
+ SPDTstruct data;
+
+ char[] name;
+ EffectList effects;
+
+ void load()
+ {with(esFile){
+ name = getHNOString("FNAM");
+ readHNExact(&data, data.sizeof, "SPDT");
+
+ effects = getRegion().getBuffer!(ENAMstruct)(0,1);
+
+ while(isNextSub("ENAM"))
+ {
+ effects.length = effects.length + 1;
+ readHExact(&effects.array[$-1], effects.array[$-1].sizeof);
+ }
+ }}
+
+}
+
+ListID!(Spell) spells;
diff --git a/esm/loadsscr.d b/esm/loadsscr.d
new file mode 100644
index 0000000000..7c1b97c41e
--- /dev/null
+++ b/esm/loadsscr.d
@@ -0,0 +1,55 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadsscr.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadsscr;
+import esm.imports;
+
+/*
+ * Startup script. I think this is simply a 'main' script that is run
+ * from the begining. The SSCR records contain a DATA identifier which
+ * is totally useless, and a NAME which is simply a script reference.
+ */
+
+struct StartScriptList
+{
+ RegionBuffer!(Script*) list;
+
+ void init()
+ {
+ // Set up the list
+ list = esmRegion.getBuffer!(Script*)(0,1);
+ }
+
+ // Load a record and add it to the list
+ void load()
+ {with(esFile){
+ getSubNameIs("DATA");
+ skipHSub();
+ list ~= getHNPtr!(Script)("NAME", scripts);
+ }}
+
+ uint length()
+ { return list.length; }
+}
+
+StartScriptList startScripts;
diff --git a/esm/loadstat.d b/esm/loadstat.d
new file mode 100644
index 0000000000..42556cb9bf
--- /dev/null
+++ b/esm/loadstat.d
@@ -0,0 +1,53 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadstat.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadstat;
+
+import esm.imports;
+
+/*
+ * Definition of static object.
+ *
+ * A stat record is basically just a reference to a nif file. Some
+ * esps seem to contain copies of the STAT entries from the esms, and
+ * the esms themselves contain several identical entries. Perhaps all
+ * statics referenced in a file is also put in the file? Since we are
+ * only reading files it doesn't much matter to us, but it would if we
+ * were writing. You can check some files later, when you decode the
+ * CELL blocks, if you want to test this hypothesis.
+ */
+
+struct Static
+{
+ char[] id;
+ LoadState state;
+
+ MeshIndex model;
+
+ void load()
+ {
+ model = esFile.getMesh();
+ }
+}
+
+ListID!(Static) statics;
diff --git a/esm/loadweap.d b/esm/loadweap.d
new file mode 100644
index 0000000000..744f928c2e
--- /dev/null
+++ b/esm/loadweap.d
@@ -0,0 +1,92 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (loadweap.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module esm.loadweap;
+import esm.imports;
+
+/*
+ * Weapon definition
+ */
+
+struct Weapon
+{
+ enum Type : short
+ {
+ ShortBladeOneHand = 0,
+ LongBladeOneHand = 1,
+ LongBladeTwoHand = 2,
+ BluntOneHand = 3,
+ BluntTwoClose = 4,
+ BluntTwoWide = 5,
+ SpearTwoWide = 6,
+ AxeOneHand = 7,
+ AxeTwoHand = 8,
+ MarksmanBow = 9,
+ MarksmanCrossbow = 10,
+ MarksmanThrown = 11,
+ Arrow = 12,
+ Bolt = 13,
+ Length
+ }
+
+ enum Flags : uint
+ {
+ Magical = 0x01,
+ Silver = 0x02
+ }
+
+ align(1) struct WPDTstruct
+ {
+ float weight;
+ int value;
+ Type type;
+ short health;
+ float speed, reach;
+ short enchant; // Enchantment points
+ ubyte[2] chop, slash, thrust; // Min and max
+ Flags flags;
+
+ static assert(WPDTstruct.sizeof == 32);
+ }
+
+ WPDTstruct data;
+
+ LoadState state;
+ char[] name, id;
+
+ MeshIndex model;
+ IconIndex icon;
+ Enchantment* enchant;
+ Script* script;
+
+ void load()
+ {with(esFile){
+ model = getMesh();
+ name = getHNOString("FNAM");
+ readHNExact(&data, data.sizeof, "WPDT");
+ script = getHNOPtr!(Script)("SCRI", scripts);
+ icon = getOIcon();
+ enchant = getHNOPtr!(Enchantment)("ENAM", enchants);
+ }}
+}
+ListID!(Weapon) weapons;
diff --git a/esm/records.d b/esm/records.d
new file mode 100644
index 0000000000..d5dd211a19
--- /dev/null
+++ b/esm/records.d
@@ -0,0 +1,219 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (records.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+module esm.records;
+
+public
+{
+ import monster.util.aa;
+ import util.regions;
+
+ import core.memory;
+ import core.resource;
+
+ import esm.filereader;
+ import esm.defs;
+ import esm.listkeeper;
+
+ import std.stdio; // Remove later
+}
+
+public import
+ esm.loadacti, esm.loaddoor, esm.loadglob, esm.loadscpt, esm.loadsoun, esm.loadgmst,
+ esm.loadfact, esm.loadstat, esm.loadspel, esm.loadalch, esm.loadappa, esm.loadarmo,
+ esm.loadbody, esm.loadench, esm.loadbook, esm.loadbsgn, esm.loadltex, esm.loadmgef,
+ esm.loadweap, esm.loadlocks,esm.loadcell, esm.loadregn, esm.loadligh, esm.loadskil,
+ esm.loadsndg, esm.loadrace, esm.loadmisc, esm.loadclot, esm.loadingr, esm.loadclas,
+ esm.loadcont, esm.loadcrea, esm.loadnpc, esm.loaddial, esm.loadinfo, esm.loadsscr,
+ esm.loadlevlist;
+
+void loadRecord(char[] recName)
+{
+ switch(recName)
+ {
+ case "ACTI": activators.load(); break;
+ case "DOOR": doors.load(); break;
+ case "GLOB": globals.load(); break;
+ case "SCPT": scripts.load(); break;
+ case "SOUN": sounds.load(); break;
+ case "GMST": gameSettings.load(); break;
+ case "FACT": factions.load(); break;
+ case "STAT": statics.load(); break;
+ case "SPEL": spells.load(); break;
+ case "ALCH": potions.load(); break;
+ case "APPA": appas.load(); break;
+ case "ARMO": armors.load(); break;
+ case "BODY": bodyParts.load(); break;
+ case "ENCH": enchants.load(); break;
+ case "BOOK": books.load(); break;
+ case "BSGN": birthSigns.load(); break;
+ case "LTEX": landTextures.load(); break;
+ case "MGEF": effects.load(); break;
+ case "WEAP": weapons.load(); break;
+ case "REPA": repairs.load(); break;
+ case "LOCK": lockpicks.load(); break;
+ case "PROB": probes.load(); break;
+ case "CELL": cells.load(); break;
+ case "REGN": regions.load(); break;
+ case "LIGH": lights.load(); break;
+ case "SKIL": skills.load(); break;
+ case "SNDG": soundGens.load(); break;
+ case "RACE": races.load(); break;
+ case "MISC": miscItems.load(); break;
+ case "CLOT": clothes.load(); break;
+ case "INGR": ingreds.load(); break;
+ case "CLAS": classes.load(); break;
+ case "CONT": containers.load(); break;
+ case "CREA": creatures.load(); break;
+ case "LEVI": itemLists.load(); break;
+ case "LEVC": creatureLists.load(); break;
+ case "NPC_": npcs.load(); break;
+ case "DIAL": dialogues.load(); break;
+ case "SSCR": startScripts.load(); break;
+ /*
+
+ // Tribunal / Bloodmoon only
+ case "SSCR": loadSSCR.load(); break;
+
+ */
+ // For save games:
+ // case "NPCC": loadNPCC;
+ // case "CNTC": loadCNTC;
+ // case "CREC": loadCREC;
+
+ // These should never be looked up
+ case "TES3":
+ case "INFO":
+ case "LAND":
+ case "PGRD":
+ esFile.fail("Misplaced record " ~ recName);
+ default:
+ esFile.fail("Unknown record type " ~ recName);
+ }
+ //*/
+}
+
+// Um, this has to be in this file for some reason.
+ListID!(Dialogue) dialogues;
+
+struct ItemT
+{
+ Item item;
+ ItemBase *i;
+
+ T* getType(T, ItemType Type)()
+ {
+ return item.getType!(T, Type)();
+ }
+
+ alias getType!(Potion, ItemType.Potion) getPotion;
+ alias getType!(Apparatus, ItemType.Apparatus) getApparatus;
+ alias getType!(Armor, ItemType.Armor) getArmor;
+ alias getType!(Weapon, ItemType.Weapon) getWeapon;
+ alias getType!(Book, ItemType.Book) getBook;
+ alias getType!(Clothing, ItemType.Clothing) getClothing;
+ alias getType!(Light, ItemType.Light) getLight;
+ alias getType!(Ingredient, ItemType.Ingredient) getIngredient;
+ alias getType!(Tool, ItemType.Pick) getPick;
+ alias getType!(Tool, ItemType.Probe) getProbe;
+ alias getType!(Tool, ItemType.Repair) getRepair;
+ alias getType!(Misc, ItemType.Misc) getMisc;
+ alias getType!(LeveledItems, ItemType.ItemList) getItemList;
+ alias getType!(Creature, ItemType.Creature) getCreature;
+ alias getType!(LeveledCreatures, ItemType.CreatureList) getCreatureList;
+ alias getType!(NPC, ItemType.NPC) getNPC;
+ alias getType!(Door, ItemType.Door) getDoor;
+ alias getType!(Activator, ItemType.Activator) getActivator;
+ alias getType!(Static, ItemType.Static) getStatic;
+ alias getType!(Container, ItemType.Container) getContainer;
+
+ static ItemT opCall(Item it)
+ {
+ ItemT itm;
+ itm.item = it;
+ itm.i = it.i;
+ return itm;
+ }
+}
+
+void endFiles()
+{
+ foreach(ListKeeper l; recordLists)
+ l.endFile();
+
+ items.endFile();
+}
+
+void initializeLists()
+{
+ recordLists = null;
+
+ // Initialize all the lists here. The sizes have been chosen big
+ // enough to hold the main ESM files and a large number of mods
+ // without rehashing.
+
+ activators = new ListID!(Activator)(1400);
+ doors = new ListID!(Door)(300);
+ globals = new ListID!(Global)(300);
+ scripts = new ScriptList(1800);
+ sounds = new ListID!(Sound)(1000);
+ gameSettings = new ListID!(GameSetting)(1600);
+ factions = new ListID!(Faction)(30);
+ statics = new ListID!(Static)(4000);
+ spells = new ListID!(Spell)(1300);
+ potions = new ListID!(Potion)(300);
+ appas = new ListID!(Apparatus)(30);
+ armors = new ListID!(Armor)(500);
+ bodyParts = new ListID!(BodyPart)(2300);
+ enchants = new ListID!(Enchantment)(1000);
+ books = new ListID!(Book)(700);
+ birthSigns = new ListID!(BirthSign)(30);
+ landTextures = new LandTextureList;
+ effects = new MagicEffectList;
+ weapons = new ListID!(Weapon)(700);
+ lockpicks = new ListID!(Tool)(10);
+ probes = new ListID!(Tool)(10);
+ repairs = new ListID!(Tool)(10);
+ cells = new CellList;
+ regions = new ListID!(Region)(20);
+ lights = new ListID!(Light)(1000);
+ skills = new SkillList;
+ soundGens = new ListID!(SoundGenerator)(500);
+ races = new ListID!(Race)(100);
+ miscItems = new ListID!(Misc)(700);
+ clothes = new ListID!(Clothing)(700);
+ ingreds = new ListID!(Ingredient)(200);
+ classes = new ListID!(Class)(100);
+ containers = new ListID!(Container)(1200);
+ creatures = new ListID!(Creature)(800);
+ itemLists = new ListID!(LeveledItems)(600);
+ creatureLists = new ListID!(LeveledCreatures)(400);
+ npcs = new ListID!(NPC)(3500);
+ dialogues = new ListID!(Dialogue)(3000);
+ startScripts.init();
+
+ hyperlinks.rehash(1600);
+
+ items.rehash(5500);
+ actors.rehash(5000);
+ cellRefs.rehash(17000);
+}
diff --git a/input/events.d b/input/events.d
new file mode 100644
index 0000000000..a4849613bc
--- /dev/null
+++ b/input/events.d
@@ -0,0 +1,320 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (events.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module input.events;
+
+import std.stdio;
+import std.string;
+
+import sound.audio;
+
+import core.config;
+
+import scene.soundlist;
+import scene.player;
+
+import ogre.bindings;
+
+import input.keys;
+import input.ois;
+
+// Debug output
+//debug=printMouse; // Mouse button events
+//debug=printMouseMove; // Mouse movement events
+//debug=printKeys; // Keypress events
+
+// TODO: Jukebox controls and other state-related data will later be
+// handled entirely in script code, as will some of the key bindings.
+
+// Are we currently playing battle music?
+bool battle = false;
+
+// Pause?
+bool pause = false;
+
+// Temporarily store volume while muted
+float muteVolume = -1;
+
+void toggleMute()
+{
+ if(muteVolume < 0)
+ {
+ muteVolume = config.mainVolume;
+ config.setMainVolume(0);
+ writefln("Muted");
+ }
+ else
+ {
+ config.setMainVolume(muteVolume);
+ muteVolume = -1;
+ writefln("Mute off");
+ }
+}
+
+// Switch between normal and battle music
+void toggleBattle()
+{
+ if(battle)
+ {
+ writefln("Changing to normal music");
+ jukebox.resumeMusic();
+ battleMusic.pauseMusic();
+ battle=false;
+ }
+ else
+ {
+ writefln("Changing to battle music");
+ jukebox.pauseMusic();
+ battleMusic.resumeMusic();
+ battle=true;
+ }
+}
+
+const float volDiff = 0.05;
+
+void musVolume(bool increase)
+{
+ float diff = -volDiff;
+ if(increase) diff = -diff;
+ config.setMusicVolume(diff + config.musicVolume);
+ writefln(increase?"Increasing":"Decreasing", " music volume to ", config.musicVolume);
+}
+
+void sfxVolume(bool increase)
+{
+ float diff = -volDiff;
+ if(increase) diff = -diff;
+ config.setSfxVolume(diff + config.sfxVolume);
+ writefln(increase?"Increasing":"Decreasing", " sound effect volume to ", config.sfxVolume);
+}
+
+void mainVolume(bool increase)
+{
+ float diff = -volDiff;
+ if(increase) diff = -diff;
+ config.setMainVolume(diff + config.mainVolume);
+ writefln(increase?"Increasing":"Decreasing", " main volume to ", config.mainVolume);
+}
+
+void takeScreenShot()
+{
+ char[] file = format("screenshot_%06d.png", config.screenShotNum++);
+ cpp_screenshot(toStringz(file));
+ writefln("Wrote '%s'", file);
+}
+
+// Mouse sensitivity
+float effMX, effMY;
+
+void updateMouseSensitivity()
+{
+ effMX = config.mouseSensX;
+ effMY = config.mouseSensY;
+ if(config.flipMouseY) effMY = -effMY;
+}
+
+void togglePause()
+{
+ pause = !pause;
+ if(pause) writefln("Pause");
+ else writefln("Pause off");
+}
+
+bool doExit = false;
+
+void exitProgram()
+{
+ doExit = true;
+}
+
+extern(C) void d_handleMouseMove(MouseState *state)
+{
+ debug(printMouseMove)
+ writefln("handleMouseMove: Abs(%s, %s, %s) Rel(%s, %s, %s)",
+ state.X.abs, state.Y.abs, state.Z.abs,
+ state.X.rel, state.Y.rel, state.Z.rel);
+
+ cpp_rotateCamera( state.X.rel * effMX,
+ state.Y.rel * effMY );
+}
+
+extern(C) void d_handleMouseButton(MouseState *state, int button)
+{
+ debug(printMouse)
+ writefln("handleMouseButton %s: Abs(%s, %s, %s)", button,
+ state.X.abs, state.Y.abs, state.Z.abs);
+
+ // For the moment, just treat mouse clicks as normal key presses.
+ d_handleKey(cast(KC) (KC.Mouse0 + button));
+}
+
+// Handle a keyboard event through key bindings. Direct movement
+// (eg. arrow keys) is not handled here, see d_frameStarted() below.
+extern(C) void d_handleKey(KC keycode, dchar text = 0)
+{
+ // Do some preprocessing on the data to account for OIS
+ // shortcommings.
+
+ // Some keys (especially international keys) have no key code but
+ // return a character instead.
+ if(keycode == 0)
+ {
+ // If no character is given, just drop this event since OIS did
+ // not manage to give us any useful data at all.
+ if(text == 0) return;
+
+ keycode = KC.CharOnly;
+ }
+
+ // Debug output
+ debug(printKeys)
+ {
+ char[] str;
+ if(keycode >= 0 && keycode < keysymToString.length)
+ str = keysymToString[keycode];
+ else str = "OUT_OF_RANGE";
+ writefln("Key %s, text '%s', name '%s'", keycode, text, str);
+ }
+
+ // Look up the key binding. We have to send both the keycode and the
+ // text.
+ Keys k = keyBindings.findMatch(keycode, text);
+
+ if(k)
+ switch(k)
+ {
+ case Keys.ToggleBattleMusic: toggleBattle(); break;
+
+ case Keys.MainVolUp: mainVolume(true); break;
+ case Keys.MainVolDown: mainVolume(false); break;
+ case Keys.MusVolUp: musVolume(true); break;
+ case Keys.MusVolDown: musVolume(false); break;
+ case Keys.SfxVolUp: sfxVolume(true); break;
+ case Keys.SfxVolDown: sfxVolume(false); break;
+ case Keys.Mute: toggleMute(); break;
+
+ case Keys.Debug: cpp_debug(0); break;
+ case Keys.ScreenShot: takeScreenShot(); break;
+ case Keys.Pause: togglePause(); break;
+ case Keys.Exit: exitProgram(); break;
+ default:
+ assert(k >= 0 && k < keyToString.length);
+ writefln("WARNING: Event %s has no effect", keyToString[k]);
+ }
+ return false;
+}
+
+// Refresh rate for sfx placements, in seconds.
+const float sndRefresh = 0.17;
+
+// Refresh rate for music fadeing, seconds.
+const float musRefresh = 0.2;
+
+float sndCumTime = 0;
+float musCumTime = 0;
+
+void initializeInput()
+{
+ // Move the camera in place
+ with(playerData.position)
+ {
+ cpp_moveCamera(position[0], position[1], position[2],
+ rotation[0], rotation[1], rotation[2]);
+ }
+
+ // TODO/FIXME: This should have been in config, but DMD's module
+ // system is on the brink of collapsing, and it won't compile if I
+ // put another import in core.config. I should probably check the
+ // bug list and report it.
+ updateMouseSensitivity();
+}
+
+float tmpTime = 0;
+int cnt;
+
+extern(C) int cpp_isPressed(int keysym);
+
+// Check if a key is currently down
+bool isPressed(Keys key)
+{
+ KeyBind *b = &keyBindings.bindings[key];
+ foreach(i; b.syms)
+ if(i != 0 && cpp_isPressed(i)) return true;
+ return false;
+}
+
+extern(C) int d_frameStarted(float time)
+{
+ /*
+ tmpTime += time;
+ cnt++;
+ if(tmpTime >= 1.0)
+ {
+ writefln("\nfps: ", cnt/tmpTime);
+ cnt = 0;
+ tmpTime = 0;
+ }
+ //*/
+
+ if(doExit) return 0;
+
+ musCumTime += time;
+ if(musCumTime > musRefresh)
+ {
+ jukebox.addTime(musRefresh);
+ battleMusic.addTime(musRefresh);
+ musCumTime -= musRefresh;
+ }
+
+ // The rest is ignored in pause mode
+ if(pause) return 1;
+
+ const float moveSpeed = 900;
+
+ // Check if the movement keys are pressed
+ float moveX = 0, moveY = 0, moveZ = 0;
+
+ if(isPressed(Keys.MoveLeft)) moveX -= moveSpeed;
+ if(isPressed(Keys.MoveRight)) moveX += moveSpeed;
+ if(isPressed(Keys.MoveForward)) moveZ -= moveSpeed;
+ if(isPressed(Keys.MoveBackward)) moveZ += moveSpeed;
+ if(isPressed(Keys.MoveUp)) moveY += moveSpeed;
+ if(isPressed(Keys.MoveDown)) moveY -= moveSpeed;
+
+ // Move camera. We only support "ghost-mode" at the moment, so we
+ // move without physics or collision detection.
+ cpp_moveCameraRel(moveX*time, moveY*time, moveZ*time);
+
+ sndCumTime += time;
+ if(sndCumTime > sndRefresh)
+ {
+ float x, y, z;
+ cpp_getCameraPos(&x, &y, &z);
+
+ soundScene.update(x,y,z);
+ sndCumTime -= sndRefresh;
+ }
+
+ return 1;
+}
+
diff --git a/input/keys.d b/input/keys.d
new file mode 100644
index 0000000000..5ad88deb5b
--- /dev/null
+++ b/input/keys.d
@@ -0,0 +1,262 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (keys.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+/*
+ * This module handles keyboard and mouse button configuration
+ */
+
+module input.keys;
+
+import std.string;
+import std.stdio;
+
+import input.ois;
+
+// List of all functions we need to map to keys. If you add new keys,
+// REMEMBER to add strings for them below as well. TODO: We should
+// redo this entire section so that we insert actual functions into
+// the system instead. That way, if we do not insert a function, the
+// key gets treated as a "non-event" key. Then we will also force the
+// definition of strings and function call to be in the same
+// place. The Keys enum can be eliminated, really. Default keysyms can
+// be added when the functions are inserted. But don't do anything
+// until you know how this will interact with script code.
+enum Keys
+ {
+ None = 0,
+
+ // Movement
+ MoveLeft, MoveRight,
+ TurnLeft, TurnRight,
+ MoveForward, MoveBackward,
+
+ // Used eg. when flying or swimming
+ MoveUp, MoveDown,
+
+ // These are handled as events, while the above are not.
+ FirstEvent,
+
+ // Sound control
+ MainVolUp, MainVolDown,
+ MusVolUp, MusVolDown,
+ SfxVolUp, SfxVolDown,
+ Mute,
+
+ // This will not be part of the finished product :)
+ ToggleBattleMusic,
+ Debug,
+
+ // Misc
+ Pause,
+ ScreenShot,
+ Exit,
+
+ Length
+ }
+
+// List of keyboard-bound functions
+char[][] keyToString;
+
+// Lookup for keyboard key names. TODO: This is currently case
+// sensitive, we should use my own AA to fix that.
+int[char[]] stringToKeysym;
+
+// Represents a key binding for a single function. Each function can
+// have any number keys bound to it, including mouse buttons. This
+// might be extended later to allow joystick buttons and other input
+// devices.
+struct KeyBind
+{
+ int syms[];
+
+ // Does the given keysym match this binding?
+ bool isMatch(int sym, char ch = 0)
+ {
+ assert(sym != 0, "don't send empty syms to isMatch");
+
+ // We don't match characters yet
+ if(sym == KC.CharOnly)
+ return false;
+
+ foreach(s; syms)
+ if(sym == s) return true;
+ return false;
+ }
+
+ // Does any of the given syms match this binding?
+ bool isMatchArray(int arr[] ...)
+ {
+ foreach(i; arr)
+ if(i!=0 && isMatch(i)) return true;
+ return false;
+ }
+
+ // Assign key bindings to this structure. Can be called multiple
+ // times or with multiple paramters (or an array) to bind multiple
+ // keys.
+ void bind(int symlist[] ...)
+ {
+ syms ~= symlist;
+ }
+
+ // Remove all bindings to this function
+ void clear()
+ {
+ syms = null;
+ }
+
+ // Remove the given syms from this binding, if found.
+ void remove(int symlist[] ...)
+ {
+ foreach(rs; symlist)
+ // Just loop though all the syms and set matching values to
+ // zero. isMatch() will ignore zeros.
+ foreach(ref s; syms)
+ if(s == rs) s = 0;
+ }
+
+ // Turn the keysym list into a comma separated list of key names
+ char[] getString()
+ {
+ char[] res = null;
+ bool notFirst = false;
+
+ foreach(int k; syms)
+ if(k != 0) // Ignore empty keysyms
+ {
+ if(notFirst) res ~= ",";
+ else notFirst = true;
+
+ res ~= keysymToString[k];
+ }
+
+ //writefln("getString returned %s", res);
+ return res;
+ }
+}
+
+KeyBindings keyBindings;
+
+// This structure holds the bindings of all the functions
+struct KeyBindings
+{
+ KeyBind[] bindings;
+
+ // Bind the given function to the given key(s)
+ void bind(Keys func, char[] key1, char[] key2 = "")
+ {
+ bind(func, getSym(key1), getSym(key2));
+ }
+
+ void bind(Keys func, int syms[] ...)
+ {
+ // Find other bindings that match this key
+ foreach(int i, ref KeyBind kb; bindings)
+ if(kb.isMatchArray(syms))
+ kb.remove(syms);
+ bindings[func].bind(syms);
+ }
+
+ // Find the function that matches the given keysym. We could
+ // optimize this, but I'm not sure it's worth it.
+ Keys findMatch(KC keysym, dchar ch)
+ {
+ int start=cast(int)Keys.FirstEvent + 1;
+ foreach(int i, ref KeyBind kb; bindings[start..$])
+ if( kb.isMatch(keysym, ch) )
+ return cast(Keys)(i+start);
+ return cast(Keys)0; // No match
+ }
+
+ static int getSym(char[] key)
+ {
+ key = strip(key);
+ if(key.length)
+ {
+ int *p = key in stringToKeysym;
+ if(p) return *p;
+ else writefln("Warning: unknown key '%s'", key);
+ }
+ return 0;
+ }
+
+ // Bind a function to a comma-separated key list (intended to be
+ // used directly with the ini file reader.)
+ void bindComma(Keys func, char[] keys)
+ {
+ int index = keys.find(',');
+ if(index != -1)
+ {
+ // Bind the first in the list
+ bind(func, keys[0..index]);
+ // Recurse on the rest
+ bindComma(func, keys[index+1..$]);
+ }
+ // Last or only element in the list
+ else bind(func, keys);
+ }
+
+ // Remove all key bindings
+ void clear()
+ {
+ foreach(ref kb; bindings)
+ kb.clear();
+ }
+
+ void initKeys()
+ {
+ // Keyboard functions
+ keyToString.length = Keys.Length;
+
+ keyToString[Keys.MoveLeft] = "Move Left";
+ keyToString[Keys.MoveRight] = "Move Right";
+ keyToString[Keys.TurnLeft] = "Turn Left";
+ keyToString[Keys.TurnRight] = "Turn Right";
+ keyToString[Keys.MoveForward] = "Move Forward";
+ keyToString[Keys.MoveBackward] = "Move Backward";
+ keyToString[Keys.MoveUp] = "Move Up";
+ keyToString[Keys.MoveDown] = "Move Down";
+
+ keyToString[Keys.MainVolUp] = "Increase Main Volume";
+ keyToString[Keys.MainVolDown] = "Decrease Main Volume";
+ keyToString[Keys.MusVolUp] = "Increase Music Volume";
+ keyToString[Keys.MusVolDown] = "Decrease Music Volume";
+ keyToString[Keys.SfxVolUp] = "Increase SFX Volume";
+ keyToString[Keys.SfxVolDown] = "Decrease SFX Volume";
+ keyToString[Keys.Mute] = "Mute Sound";
+
+ keyToString[Keys.ToggleBattleMusic] = "Toggle Battle Music";
+ keyToString[Keys.Debug] = "OGRE Test Action";
+
+ keyToString[Keys.Pause] = "Pause";
+ keyToString[Keys.ScreenShot] = "Screen Shot";
+ keyToString[Keys.Exit] = "Quick Exit";
+ //keyToString[Keys.] = "";
+
+ bindings.length = Keys.Length;
+
+ // Store all the key strings in a lookup-table
+ foreach(int k, ref char[] s; keysymToString)
+ if(s.length) stringToKeysym[s] = k;
+ }
+}
diff --git a/input/ois.d b/input/ois.d
new file mode 100644
index 0000000000..60129cadf2
--- /dev/null
+++ b/input/ois.d
@@ -0,0 +1,428 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (ois.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module input.ois;
+
+// Mouse buttons
+enum MB : int
+ {
+ Button0 = 0,
+ Left = Button0,
+ Button1 = 1,
+ Right = Button1,
+ Button2 = 2,
+ Middle = Button2,
+
+ Button3 = 3,
+ Button4 = 4,
+ Button5 = 5,
+ Button6 = 6,
+ Button7 = 7,
+
+ LastMouse
+ }
+
+ // Keyboard scan codes
+enum KC : int
+ {
+ UNASSIGNED = 0x00,
+ ESCAPE = 0x01,
+ N1 = 0x02,
+ N2 = 0x03,
+ N3 = 0x04,
+ N4 = 0x05,
+ N5 = 0x06,
+ N6 = 0x07,
+ N7 = 0x08,
+ N8 = 0x09,
+ N9 = 0x0A,
+ N0 = 0x0B,
+ MINUS = 0x0C, // - on main keyboard
+ EQUALS = 0x0D,
+ BACK = 0x0E, // backspace
+ TAB = 0x0F,
+ Q = 0x10,
+ W = 0x11,
+ E = 0x12,
+ R = 0x13,
+ T = 0x14,
+ Y = 0x15,
+ U = 0x16,
+ I = 0x17,
+ O = 0x18,
+ P = 0x19,
+ LBRACKET = 0x1A,
+ RBRACKET = 0x1B,
+ RETURN = 0x1C, // Enter on main keyboard
+ LCONTROL = 0x1D,
+ A = 0x1E,
+ S = 0x1F,
+ D = 0x20,
+ F = 0x21,
+ G = 0x22,
+ H = 0x23,
+ J = 0x24,
+ K = 0x25,
+ L = 0x26,
+ SEMICOLON = 0x27,
+ APOSTROPHE = 0x28,
+ GRAVE = 0x29, // accent
+ LSHIFT = 0x2A,
+ BACKSLASH = 0x2B,
+ Z = 0x2C,
+ X = 0x2D,
+ C = 0x2E,
+ V = 0x2F,
+ B = 0x30,
+ N = 0x31,
+ M = 0x32,
+ COMMA = 0x33,
+ PERIOD = 0x34, // . on main keyboard
+ SLASH = 0x35, // / on main keyboard
+ RSHIFT = 0x36,
+ MULTIPLY = 0x37, // * on numeric keypad
+ LMENU = 0x38, // left Alt
+ SPACE = 0x39,
+ CAPITAL = 0x3A,
+ F1 = 0x3B,
+ F2 = 0x3C,
+ F3 = 0x3D,
+ F4 = 0x3E,
+ F5 = 0x3F,
+ F6 = 0x40,
+ F7 = 0x41,
+ F8 = 0x42,
+ F9 = 0x43,
+ F10 = 0x44,
+ NUMLOCK = 0x45,
+ SCROLL = 0x46, // Scroll Lock
+ NUMPAD7 = 0x47,
+ NUMPAD8 = 0x48,
+ NUMPAD9 = 0x49,
+ SUBTRACT = 0x4A, // - on numeric keypad
+ NUMPAD4 = 0x4B,
+ NUMPAD5 = 0x4C,
+ NUMPAD6 = 0x4D,
+ ADD = 0x4E, // + on numeric keypad
+ NUMPAD1 = 0x4F,
+ NUMPAD2 = 0x50,
+ NUMPAD3 = 0x51,
+ NUMPAD0 = 0x52,
+ DECIMAL = 0x53, // . on numeric keypad
+ OEM_102 = 0x56, // < > | on UK/Germany keyboards
+ F11 = 0x57,
+ F12 = 0x58,
+ F13 = 0x64, // (NEC PC98)
+ F14 = 0x65, // (NEC PC98)
+ F15 = 0x66, // (NEC PC98)
+ KANA = 0x70, // (Japanese keyboard)
+ ABNT_C1 = 0x73, // / ? on Portugese (Brazilian) keyboards
+ CONVERT = 0x79, // (Japanese keyboard)
+ NOCONVERT = 0x7B, // (Japanese keyboard)
+ YEN = 0x7D, // (Japanese keyboard)
+ ABNT_C2 = 0x7E, // Numpad . on Portugese (Brazilian) keyboards
+ NUMPADEQUALS= 0x8D, // = on numeric keypad (NEC PC98)
+ PREVTRACK = 0x90, // Previous Track (CIRCUMFLEX on Japanese keyboard)
+ AT = 0x91, // (NEC PC98)
+ COLON = 0x92, // (NEC PC98)
+ UNDERLINE = 0x93, // (NEC PC98)
+ KANJI = 0x94, // (Japanese keyboard)
+ STOP = 0x95, // (NEC PC98)
+ AX = 0x96, // (Japan AX)
+ UNLABELED = 0x97, // (J3100)
+ NEXTTRACK = 0x99, // Next Track
+ NUMPADENTER = 0x9C, // Enter on numeric keypad
+ RCONTROL = 0x9D,
+ MUTE = 0xA0, // Mute
+ CALCULATOR = 0xA1, // Calculator
+ PLAYPAUSE = 0xA2, // Play / Pause
+ MEDIASTOP = 0xA4, // Media Stop
+ VOLUMEDOWN = 0xAE, // Volume -
+ VOLUMEUP = 0xB0, // Volume +
+ WEBHOME = 0xB2, // Web home
+ NUMPADCOMMA = 0xB3, // , on numeric keypad (NEC PC98)
+ DIVIDE = 0xB5, // / on numeric keypad
+ SYSRQ = 0xB7, // Also called print screen
+ RMENU = 0xB8, // right Alt
+ PAUSE = 0xC5, // Pause
+ HOME = 0xC7, // Home on arrow keypad
+ UP = 0xC8, // UpArrow on arrow keypad
+ PGUP = 0xC9, // PgUp on arrow keypad
+ LEFT = 0xCB, // LeftArrow on arrow keypad
+ RIGHT = 0xCD, // RightArrow on arrow keypad
+ END = 0xCF, // End on arrow keypad
+ DOWN = 0xD0, // DownArrow on arrow keypad
+ PGDOWN = 0xD1, // PgDn on arrow keypad
+ INSERT = 0xD2, // Insert on arrow keypad
+ DELETE = 0xD3, // Delete on arrow keypad
+ LWIN = 0xDB, // Left Windows key
+ RWIN = 0xDC, // Right Windows key
+ APPS = 0xDD, // AppMenu key
+ POWER = 0xDE, // System Power
+ SLEEP = 0xDF, // System Sleep
+ WAKE = 0xE3, // System Wake
+ WEBSEARCH = 0xE5, // Web Search
+ WEBFAVORITES= 0xE6, // Web Favorites
+ WEBREFRESH = 0xE7, // Web Refresh
+ WEBSTOP = 0xE8, // Web Stop
+ WEBFORWARD = 0xE9, // Web Forward
+ WEBBACK = 0xEA, // Web Back
+ MYCOMPUTER = 0xEB, // My Computer
+ MAIL = 0xEC, // Mail
+ MEDIASELECT = 0xED, // Media Select
+
+ CharOnly = 0xFF, // Set when the keysym is 0 but the
+ // character is set. This happens with many
+ // international characters or reassigned
+ // characters.
+
+ Mouse0 = 0x100, // Mouse button events can be handled as
+ Mouse1 = 0x101, // keypresses too.
+ Mouse2 = 0x102,
+ Mouse3 = 0x103,
+ Mouse4 = 0x104,
+ Mouse5 = 0x105,
+ Mouse6 = 0x106,
+ Mouse7 = 0x107,
+ }
+
+// Sigh. I guess we have to do this for Monster at some poing anyway,
+// so this work isn't completely wasted. Later we can make a generic
+// conversion between OIS-keysyms, SDL-keysyms and others to the
+// Monster keysyms. It sucks that everybody tries to reinvent the
+// wheel as often as they can, but that's the way it goes.
+
+const char[][] keysymToString =
+[
+ KC.UNASSIGNED : "UNASSIGNED",
+ KC.ESCAPE : "escape",
+ KC.N1 : "1",
+ KC.N2 : "2",
+ KC.N3 : "3",
+ KC.N4 : "4",
+ KC.N5 : "5",
+ KC.N6 : "6",
+ KC.N7 : "7",
+ KC.N8 : "8",
+ KC.N9 : "9",
+ KC.N0 : "0",
+ KC.MINUS : "minus",
+ KC.EQUALS : "equals",
+ KC.BACK : "backspace",
+ KC.TAB : "tab",
+ KC.Q : "q",
+ KC.W : "w",
+ KC.E : "e",
+ KC.R : "r",
+ KC.T : "t",
+ KC.Y : "y",
+ KC.U : "u",
+ KC.I : "i",
+ KC.O : "o",
+ KC.P : "p",
+ KC.LBRACKET : "{",
+ KC.RBRACKET : "}",
+ KC.RETURN : "enter",
+ KC.LCONTROL : "left_ctrl",
+ KC.A : "a",
+ KC.S : "s",
+ KC.D : "d",
+ KC.F : "f",
+ KC.G : "g",
+ KC.H : "h",
+ KC.J : "j",
+ KC.K : "k",
+ KC.L : "l",
+ KC.SEMICOLON : "semicolon",
+ KC.APOSTROPHE : "apostrophe",
+ KC.GRAVE : "grave",
+ KC.LSHIFT : "left_shift",
+ KC.BACKSLASH : "backslash",
+ KC.Z : "z",
+ KC.X : "x",
+ KC.C : "c",
+ KC.V : "v",
+ KC.B : "b",
+ KC.N : "n",
+ KC.M : "m",
+ KC.COMMA : "comma",
+ KC.PERIOD : "period",
+ KC.SLASH : "slash",
+ KC.RSHIFT : "right_shift",
+ KC.MULTIPLY : "numpad_mult",
+ KC.LMENU : "left_alt",
+ KC.SPACE : "space",
+ KC.CAPITAL : "capital",
+ KC.F1 : "f1",
+ KC.F2 : "f2",
+ KC.F3 : "f3",
+ KC.F4 : "f4",
+ KC.F5 : "f5",
+ KC.F6 : "f6",
+ KC.F7 : "f7",
+ KC.F8 : "f8",
+ KC.F9 : "f9",
+ KC.F10 : "f10",
+ KC.NUMLOCK : "numlock",
+ KC.SCROLL : "scroll",
+ KC.NUMPAD7 : "numpad_7",
+ KC.NUMPAD8 : "numpad_8",
+ KC.NUMPAD9 : "numpad_9",
+ KC.SUBTRACT : "numpad_minus",
+ KC.NUMPAD4 : "numpad_4",
+ KC.NUMPAD5 : "numpad_5",
+ KC.NUMPAD6 : "numpad_6",
+ KC.ADD : "numpad_plus",
+ KC.NUMPAD1 : "numpad_1",
+ KC.NUMPAD2 : "numpad_2",
+ KC.NUMPAD3 : "numpad_3",
+ KC.NUMPAD0 : "numpad_0",
+ KC.DECIMAL : "numpad_period",
+ KC.OEM_102 : "oem102",
+ KC.F11 : "f11",
+ KC.F12 : "f12",
+ KC.F13 : "f13",
+ KC.F14 : "f14",
+ KC.F15 : "f15",
+ KC.KANA : "kana",
+ KC.ABNT_C1 : "abnt_c1",
+ KC.CONVERT : "convert",
+ KC.NOCONVERT : "noconvert",
+ KC.YEN : "yen",
+ KC.ABNT_C2 : "abnt_c2",
+ KC.NUMPADEQUALS: "numpad_equals",
+ KC.PREVTRACK : "prev_track",
+ KC.AT : "at",
+ KC.COLON : "colon",
+ KC.UNDERLINE : "underline",
+ KC.KANJI : "kanji",
+ KC.STOP : "stop",
+ KC.AX : "ax",
+ KC.UNLABELED : "unlabeled",
+ KC.NEXTTRACK : "next_track",
+ KC.NUMPADENTER : "numpad_enter",
+ KC.RCONTROL : "right_control",
+ KC.MUTE : "mute",
+ KC.CALCULATOR : "calculator",
+ KC.PLAYPAUSE : "play_pause",
+ KC.MEDIASTOP : "media_stop",
+ KC.VOLUMEDOWN : "volume_down",
+ KC.VOLUMEUP : "volume_up",
+ KC.WEBHOME : "webhome",
+ KC.NUMPADCOMMA : "numpad_comma",
+ KC.DIVIDE : "numpad_divide",
+ KC.SYSRQ : "print_screen",
+ KC.RMENU : "right_alt",
+ KC.PAUSE : "pause",
+ KC.HOME : "home",
+ KC.UP : "up",
+ KC.PGUP : "page_up",
+ KC.LEFT : "left",
+ KC.RIGHT : "right",
+ KC.END : "end",
+ KC.DOWN : "down",
+ KC.PGDOWN : "page_down",
+ KC.INSERT : "insert",
+ KC.DELETE : "delete",
+ KC.LWIN : "left_win",
+ KC.RWIN : "right_win",
+ KC.APPS : "app_menu",
+ KC.POWER : "power",
+ KC.SLEEP : "sleep",
+ KC.WAKE : "wake",
+ KC.WEBSEARCH : "web_search",
+ KC.WEBFAVORITES: "web_favorites",
+ KC.WEBREFRESH : "web_refresh",
+ KC.WEBSTOP : "web_stop",
+ KC.WEBFORWARD : "web_forward",
+ KC.WEBBACK : "web_back",
+ KC.MYCOMPUTER : "my_computer",
+ KC.MAIL : "mail",
+ KC.MEDIASELECT : "media_select",
+
+
+ KC.CharOnly : "CHAR_ONLY", // Set when the keysym is 0 but the
+ // character is set. This happens
+ // with many international
+ // characters or reassigned
+ // characters in OIS (and it
+ // SUCKS.)
+
+ KC.Mouse0 : "mouse0",
+ KC.Mouse1 : "mouse1",
+ KC.Mouse2 : "mouse2",
+ KC.Mouse3 : "mouse3",
+ KC.Mouse4 : "mouse4",
+ KC.Mouse5 : "mouse5",
+ KC.Mouse6 : "mouse6",
+ KC.Mouse7 : "mouse7",
+ ];
+
+enum ComponentType : int
+ {
+ Unknown = 0,
+ Button = 1, // ie. Key, mouse button, joy button, etc
+ Axis = 2, // ie. A joystick or mouse axis
+ Slider = 3, //
+ POV = 4, // ie. Arrow direction keys
+ Vector3 = 5 // ie. WiiMote orientation
+ }
+
+align(4) struct Axis
+{
+ ComponentType type;
+ int abs, rel;
+ bool absOnly;
+}
+
+// The C++ size of Axis is 16
+static assert(Axis.sizeof == 16);
+
+struct MouseState
+{
+ /* Represents the height/width of your display area.. used if mouse
+ clipping or mouse grabbed in case of X11 - defaults to 50.. Make
+ sure to set this and change when your size changes.. */
+ int width, height;
+
+ // X Axis component
+ Axis X;
+
+ // Y Axis Component
+ Axis Y;
+
+ // Z Axis Component
+ Axis Z;
+
+ // represents all buttons - bit position indicates button down
+ int buttons;
+
+ // Button down test
+ bool buttonDown( MB button )
+ {
+ return (buttons & ( 1 << button )) != 0;
+ }
+}
+
+// Check that we match the C++ size
+static assert(MouseState.sizeof == 60);
diff --git a/morro.d b/morro.d
new file mode 100644
index 0000000000..2e96f93401
--- /dev/null
+++ b/morro.d
@@ -0,0 +1,385 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (morro.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module morro;
+
+import std.stdio;
+import std.string;
+import std.cstream;
+
+import ogre.ogre;
+import ogre.bindings;
+
+import scene.celldata;
+import scene.soundlist;
+
+import core.resource;
+import core.memory;
+import core.config;
+
+import monster.util.string;
+
+import sound.audio;
+
+import input.events;
+
+//*
+import std.gc;
+import gcstats;
+
+void poolSize()
+{
+ GCStats gc;
+ getStats(gc);
+ writefln("Pool size: ", comma(gc.poolsize));
+ writefln("Used size: ", comma(gc.usedsize));
+}
+//*/
+
+void main(char[][] args)
+{
+ bool render = true;
+ bool help = false;
+ bool resetKeys = false;
+
+ // Some examples to try:
+ //
+ // "Abandoned Shipwreck, Upper Level";
+ // "Gro-Bagrat Plantation";
+ // "Abinabi";
+ // "Abebaal Egg Mine";
+ // "Ald-ruhn, Ald Skar Inn";
+ // "Koal Cave";
+ // "Ald-ruhn, Arobar Manor Bedrooms"
+ // "Sud";
+ // "Vivec, The Lizard's Head";
+ // "ToddTest";
+
+ // Cells to load
+ char[][] cells;
+ int[] eCells;
+
+ foreach(char[] a; args[1..$])
+ if(a == "-n") render = false;
+ else if(a.begins("-e"))
+ {
+ int i = find(a,',');
+ eCells ~= atoi(a[2..i]);
+ eCells ~= atoi(a[i+1..$]);
+ }
+ else if(a == "-h") help=true;
+ else if(a == "-rk") resetKeys = true;
+ else cells ~= a;
+
+ if(cells.length + eCells.length/2 > 1 )
+ {
+ writefln("More than one cell specified, rendering disabled");
+ render=false;
+ }
+
+ initializeMemoryRegions();
+ config.initialize(resetKeys);
+ scope(exit) config.writeConfig();
+
+ if(cells.length == 0 && eCells.length == 0)
+ if(config.defaultCell.length)
+ cells ~= config.defaultCell;
+
+ if(cells.length == 1)
+ config.defaultCell = cells[0];
+
+ if(help || (cells.length == 0 && eCells.length == 0))
+ {
+ writefln("Syntax: %s [options] cell-name [cell-name]", args[0]);
+ writefln(" Options:");
+ writefln(" -n Only load, do not render");
+ writefln(" -ex,y Load exterior cell (x,y)");
+ writefln(" -rk Reset key bindings to default");
+ writefln(" -h Show this help");
+ writefln("");
+ writefln("Specifying more than one cell implies -n");
+ return;
+ }
+
+ initializeSound();
+ resources.initResources();
+
+ // Files to load
+ /*
+ const char[][] files = ["Morrowind.esm",
+ "Tribunal.esm",
+ "Bloodmoon.esm"];
+ /*/
+ const char[][] files = ["Morrowind.esm"];
+ //*/
+
+ // Add the path to the file name. The path is read from the config
+ // file.
+ char[][] esmFiles;
+ esmFiles.length = files.length;
+ foreach(int i, char[] ef; files)
+ // TODO: Quick hack, use FileFinder instead.
+ esmFiles[i] = config.esmDir ~ ef;
+
+ // Load all data from the ESM files
+ loadTESFiles(esmFiles);
+
+ CellData cd = cellList.get();
+
+ foreach(char[] cName; cells)
+ {
+ // Release the last cell data
+ cellList.release(cd);
+
+ // Get a cell data holder and load an interior cell
+ cd = cellList.get();
+
+ try cd.loadIntCell(cName);
+ catch(Exception e)
+ {
+ writefln(e);
+ writefln("\nUnable to load cell '%s'. Aborting", cName);
+ return;
+ }
+ }
+
+ for(int i; i
+ WWW: http://openmw.snaptoad.com/
+
+ This file (base.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+// Basic file structure
+module nif.base;
+
+import nif.nif;
+import nif.niffile;
+import nif.misc;
+import monster.util.string;
+
+//import nif.controller;
+//import nif.node;
+import nif.record;
+
+debug(verbose) public import std.stdio;
+
+void parseFile()
+{
+ with(nifFile)
+ { // Read header
+ int ver; // Hex-encoded version
+ char[40] head; // Header string
+
+ // Read and check header string
+ getString(head);
+
+ if(!begins(head,"NetImmerse File Format"))
+ fail("Not a NetImmerse file");
+
+ debug(verbose) writefln("Header: \"%s\"", head[0..39]);
+ debug(strict)
+ if(head[0..39] != "NetImmerse File Format, Version 4.0.0.2" || head[39] != 0x0a)
+ fail("Unrecognized header string, most likely an unsupported NIF version");
+
+ // Get binary coded version
+ ver = getInt;
+
+ debug(verbose)
+ {
+ writef("BCD version tag: ");
+ for(ubyte *b = (cast(ubyte*)&ver)+3; b > (cast(ubyte*)&ver); b--)
+ writef(*b, ".");
+ writefln(cast(ubyte)ver);
+ }
+
+ if(ver != 0x04_00_00_02)
+ fail("Don't know how to process this version");
+
+ // The number of records in the file
+ nifMesh.records = nifRegion.allocateT!(Record)(getInt);
+ debug(verbose) writefln("Number of records: ", nifMesh.records.length);
+
+ // The format for 10.0.1.0 seems to be a bit different. After the
+ // header, it contains the number of records, r (int), just like
+ // 4.0.0.2, but following that it contains a short x, followed by
+ // x strings. Then again by r shorts, one for each record, giving
+ // which of the above strings to use to identify the record. After
+ // this follows two ints (zero?) and then the record data.
+
+ // Besides giving me more work to do, this structure suggests some
+ // obvious code optimizations we can now apply to the reading
+ // structure. Since we now know in advance how many records there
+ // are of each type, we could allocate the structures in bulk. D
+ // doesn't allow this for classes directly, but we can make a
+ // custom allocator and whatnot. I doubt we save a lot of time
+ // with it, though. I'll save this for later.
+
+ // As for the record data itself, I do not know what has
+ // changed. There are some new records. I might as well have
+ // separate reading functions for the entire main structure.
+
+ // EDIT 14. feb 06: Since we are concentrating on Morrowind, let
+ // us drop other versions for now. It would be nice to support
+ // other versions (or even other mesh formats), but the priority
+ // of this is very low at the moment.
+ }
+
+ endFor:
+ foreach(int i, ref Record o; nifMesh.records)
+ {
+ // Get type string, and update the NIFFiles error message to
+ // match it.
+ char[] recName = nifFile.getString;
+ nifFile.setRec(i,recName);
+
+ debug(verbose)
+ {
+ writefln("\n%d: %s (%x)", i, recName, nifFile.position);
+ }
+
+ switch(recName)
+ {
+ // These are the record types we know how to read
+
+ // Time controllers
+ case "NiVisController": o = new NiVisController; break;
+ case "NiGeomMorpherController": o = new NiGeomMorpherController; break;
+ case "NiKeyframeController": o = new NiKeyframeController; break;
+ case "NiAlphaController": o = new NiAlphaController; break;
+ case "NiUVController": o = new NiUVController; break;
+ case "NiPathController": o = new NiPathController; break;
+ case "NiMaterialColorController": o = new NiMaterialColorController; break;
+ case "NiBSPArrayController": o = new NiBSPArrayController; break;
+ case "NiParticleSystemController": o = new NiParticleSystemController; break;
+
+ // NiNodes
+ case "RootCollisionNode":
+ case "NiBSParticleNode":
+ case "NiBSAnimationNode":
+ case "NiBillboardNode":
+ case "AvoidNode":
+ case "NiNode": o = new NiNode; break;
+
+ // Other nodes
+ case "NiTriShape": o = new NiTriShape; break;
+ case "NiRotatingParticles": o = new NiRotatingParticles; break;
+ case "NiAutoNormalParticles": o = new NiAutoNormalParticles; break;
+ case "NiCamera": o = new NiCamera; break;
+
+ // Effects
+ case "NiAmbientLight":
+ case "NiDirectionalLight": o = new Light; break;
+ case "NiTextureEffect": o = new NiTextureEffect; break;
+
+ // Properties
+ case "NiTexturingProperty": o = new NiTexturingProperty; break;
+ case "NiMaterialProperty": o = new NiMaterialProperty; break;
+ case "NiZBufferProperty": o = new NiZBufferProperty; break;
+ case "NiAlphaProperty": o = new NiAlphaProperty; break;
+ case "NiVertexColorProperty": o = new NiVertexColorProperty; break;
+ case "NiShadeProperty": o = new NiShadeProperty; break;
+ case "NiDitherProperty": o = new NiDitherProperty; break;
+ case "NiWireframeProperty": o = new NiWireframeProperty; break;
+ case "NiSpecularProperty": o = new NiSpecularProperty; break;
+
+ // Extra Data
+ case "NiVertWeightsExtraData": o = new NiVertWeightsExtraData; break;
+ case "NiTextKeyExtraData": o = new NiTextKeyExtraData; break;
+ case "NiStringExtraData": o = new NiStringExtraData; break;
+
+ case "NiGravity": o = new NiGravity; break;
+ case "NiPlanarCollider": o = new NiPlanarCollider; break;
+ case "NiParticleGrowFade": o = new NiParticleGrowFade; break;
+ case "NiParticleColorModifier": o = new NiParticleColorModifier; break;
+ case "NiParticleRotation": o = new NiParticleRotation; break;
+
+ // Data
+ case "NiFloatData": o = new NiFloatData; break;
+ case "NiTriShapeData": o = new NiTriShapeData; break;
+ case "NiVisData": o = new NiVisData; break;
+ case "NiColorData": o = new NiColorData; break;
+ case "NiPixelData": o = new NiPixelData; break;
+ case "NiMorphData": o = new NiMorphData; break;
+ case "NiKeyframeData": o = new NiKeyframeData; break;
+ case "NiSkinData": o = new NiSkinData; break;
+ case "NiUVData": o = new NiUVData; break;
+ case "NiPosData": o = new NiPosData; break;
+ case "NiRotatingParticlesData": o = new NiRotatingParticlesData; break;
+ case "NiAutoNormalParticlesData": o = new NiAutoNormalParticlesData; break;
+
+ // Other
+ case "NiSequenceStreamHelper": o = new NiSequenceStreamHelper; break;
+ case "NiSourceTexture": o = new NiSourceTexture; break;
+ case "NiSkinInstance": o = new NiSkinInstance; break;
+
+ default:
+ nifFile.fail("Unknown record type " ~ recName);
+ }
+
+ if(o !is null)
+ {
+ //nifFile.setRec(i,recName ~ " (" ~ o.toString ~ ")");
+ o.read();
+ }
+ }
+ // Clear error message.
+ nifFile.setRec(-1,"");
+
+ // The 'footer', which it seems isn't always constant after all. I
+ // have no clue what it is for though.
+ int roots = nifFile.getInt;
+ for(int i; i
+ WWW: http://openmw.snaptoad.com/
+
+ This file (controlled.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.controlled;
+
+import nif.record;
+import nif.extra;
+
+// 'Controlled' is something that has a controller
+abstract class Controlled : Extra
+{
+ Controller controller;
+
+ override:
+ void read()
+ {
+ super.read();
+ debug(verbose) writef("Controller ");
+ getIndex();
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ controller = lookup!(Controller)(list);
+ }
+}
+
+// Record with name and controller/extra data link
+abstract class Named : Controlled
+{
+ char[] name;
+
+ //Extra extra;
+ //Controller controller;
+
+ override:
+ char[] toString()
+ {
+ if(name.length)
+ return super.toString() ~ " '" ~ name ~ "'";
+ else
+ return super.toString();
+ }
+
+ void read()
+ {
+ //super.read(f);
+ name = nifFile.getString();
+ debug(verbose) writefln("Name: %s", name);
+ /*
+ debug(verbose) writef("Extra ");
+ getIndex(f);
+ debug(verbose) writef("Controller ");
+ getIndex(f);
+ */
+ super.read();
+ }
+
+ /*
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ extra = lookup!(Extra)(list);
+ controller = lookup!(Controller)(list);
+ }
+ */
+}
+
+class NiSequenceStreamHelper : Named {}
diff --git a/nif/controller.d b/nif/controller.d
new file mode 100644
index 0000000000..d65fe37aab
--- /dev/null
+++ b/nif/controller.d
@@ -0,0 +1,317 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (controller.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.controller;
+
+import nif.record;
+
+// Time controller
+abstract class Controller : Record
+{
+ Controller next;
+ ushort flags;
+ float frequency, phase; // Don't know what these do.
+ float timeStart, timeStop;
+ Controlled target;
+
+ override:
+ void read()
+ {with(nifFile){
+ super.read();
+
+ debug(verbose) writef("Next controller ");
+ getIndex();
+
+ flags = getUshort;
+
+ frequency = getFloatIs(1);
+ phase = getFloat;
+ timeStart = getFloat;
+ timeStop = getFloat;
+
+ debug(verbose) writef("Target ");
+ getIndex();
+
+ debug(verbose)
+ {
+ writefln("Flags: %x", flags);
+ writefln("Frequency: ", frequency);
+ writefln("Phase: ", phase);
+ writefln("Start time: ", timeStart);
+ writefln("Stop time: ", timeStop);
+ }
+ }}
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ next = lookup!(Controller)(list);
+ target = lookup!(Controlled)(list);
+ }
+}
+
+alias NiBSPArrayController NiParticleSystemController;
+
+class NiBSPArrayController : Controller
+{
+ override:
+ void read()
+ {with(nifFile){
+ super.read();
+
+ // At the moment, just skip it all
+
+ //*
+ // 23 floats = 92 bytes
+ // 3 ints = 12 bytes
+ // 3 shorts = 6 bytes
+ // 1 byte = 1 byte
+ // Total: 111 bytes
+ seekCur(111);
+
+ short s = wgetShort;
+
+ seekCur(15 + s*40);
+ /*/
+ float speed, speedRandom, angle, angleRandom;
+
+ speed = wgetFloat;
+ speedRandom = wgetFloat;
+ angle = wgetFloat;
+ angleRandom = wgetFloat;
+
+ wgetFloat(); // Unknown
+ wgetFloat(); // Sometimes pi
+
+ for(int i; i<10; i++)
+ wgetFloat();
+
+ wgetByte();
+
+ wgetFloat();
+ wgetFloat();
+ wgetFloat();
+
+ wgetShort();
+
+ wgetVector();
+
+ debug(verbose) writef("Emitter: ");
+ wgetInt();
+
+ wgetShort();
+
+ wgetFloat();
+
+ wgetInt();
+ wgetInt();
+
+ wgetShort();
+
+ debug(verbose) writefln("begin particle group");
+ short s = wgetShort;
+ wgetShort();
+ for(short i; i
+ WWW: http://openmw.snaptoad.com/
+
+ This file (data.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.data;
+import nif.record;
+
+class NiSourceTexture : Named
+{
+ byte external;
+ union
+ {
+ char[] filename;
+ NiPixelData data;
+ }
+
+ /* Pixel layout
+ 0 - Palettised
+ 1 - High color 16
+ 2 - True color 32
+ 3 - Compressed
+ 4 - Bumpmap
+ 5 - Default */
+ int pixel;
+
+ /* Mipmap format
+ 0 - no
+ 1 - yes
+ 2 - default */
+ int mipmap;
+
+ /* Alpha
+ 0 - none
+ 1 - binary
+ 2 - smooth
+ 3 - default (use material alpha, or multiply material with texture if present)
+ */
+ int alpha;
+
+ override:
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ if(!external)
+ data = lookup!(NiPixelData)(list);
+ }
+
+ void read()
+ {
+ super.read();
+
+ external = nifFile.getByteIs(0,1);
+ debug(verbose) writefln("Use external file: ", external);
+
+ if(external)
+ {
+ filename = nifFile.getString;
+ debug(verbose) writefln("Filename: ", filename);
+ }
+ else
+ {
+ nifFile.wgetByteIs(1);
+ debug(verbose) writef("Pixel Data ");
+ getIndex();
+ }
+
+ pixel = nifFile.getInt;
+ mipmap = nifFile.getInt;
+ alpha = nifFile.getIntIs(3);
+
+ debug(verbose)
+ {
+ writefln("Pixel layout: ", pixel);
+ writefln("Mipmap: ", mipmap);
+ writefln("Alpha: ", alpha);
+ }
+
+ nifFile.wgetByteIs(1);
+ }
+}
+
+// Common ancestor for several classes
+class ShapeData : Record
+{
+ float[] vertices, normals, colors, uvlist;
+ Vector center;
+ float radius;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ short verts = nifFile.getShort;
+ debug(verbose) writefln("Vertices: ", verts);
+
+ if(nifFile.getInt != 0)
+ {
+ debug(verbose) writefln("Reading vertices");
+ if(verts == 0) nifFile.warn("Empty vertex array");
+ vertices = nifFile.getArraySize!(float)(verts*3);
+ }
+ else nifFile.warn("No vertices found");
+
+ if(nifFile.getInt != 0)
+ {
+ debug(verbose) writefln("Reading normals");
+ normals = nifFile.getArraySize!(float)(verts*3);
+ }
+
+ center = nifFile.getVector();
+ radius = nifFile.getFloat();
+ debug(verbose)
+ {
+ writefln("Center: ", center.toString);
+ writefln("Radius: ", radius);
+ }
+
+ if(nifFile.getInt != 0)
+ {
+ debug(verbose) writefln("Reading vertex colors");
+ colors = nifFile.getArraySize!(float)(verts*4);
+ }
+
+ short uvs = nifFile.getShort;
+ debug(verbose) writefln("Number of UV sets: ", uvs);
+
+ if(nifFile.getInt != 0)
+ {
+ if(uvs == 0) nifFile.warn("Empty UV list");
+ uvlist = nifFile.getArraySize!(float)(uvs*verts*2);
+ }
+ else if(uvs != 0) nifFile.warn("UV list was unexpectedly missing");
+ }
+}
+
+class NiAutoNormalParticlesData : ShapeData
+{
+ ushort activeCount;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ nifFile.assertWarn(uvlist.length == 0, "UV coordinates are not expected in this record");
+
+ debug(verbose) writef("Active vertices: ");
+
+ // Number of active vertices (always matches count?)
+ activeCount = nifFile.wgetUshortIs(vertices.length/3);
+
+ nifFile.wgetFloat(); // Active radius (?)
+ nifFile.wgetShort(); // Number of valid entries in the following arrays
+
+ //writefln("%x", nifFile.position);
+ if(nifFile.wgetInt == 0) nifFile.warn("Particle size list is missing");
+ else
+ for(int i; i>3);
+
+ debug(verbose)
+ {
+ writefln("Red mask %8xh", rmask);
+ writefln("Green mask %8xh", gmask);
+ writefln("Blue mask %8xh", bmask);
+ writefln("Alpha mask %8xh", amask);
+ writefln("Bits per pixel: ", bpp);
+ writefln("Number of mipmaps: ", mips);
+ writefln("Bytes per pixel: ", bytes);
+ }
+
+ for(int i; i=verts || sh < 0)
+ nifFile.warn("Presumed vertex index was out of bounds");
+ s = nifFile.getArraySize!(short)(sh);
+ }
+ }
+ }
+}
+
+class NiMorphData : Record
+{
+ //float[] vertexList;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ int morphCount = nifFile.getInt;
+
+ int vertCount = nifFile.getInt;
+
+ debug(verbose)
+ {
+ writefln("Number of morphs: ", morphCount);
+ writefln("Vertices: ", vertCount);
+ }
+
+ nifFile.wgetByteIs(1);
+
+ //nifFile.getIntIs(0);
+ //nifFile.getIntIs(1);
+ //vertexList = nifFile.getArraySize!(float)(3*vertCount);
+
+ for(int i=0; i
+ WWW: http://openmw.snaptoad.com/
+
+ This file (effect.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.effect;
+import nif.record;
+
+class Effect : Node {}
+
+// Used for NiAmbientLight and NiDirectionalLight. Might also work for
+// NiPointLight and NiSpotLight?
+class Light : Effect
+{
+ float dimmer;
+ Vector ambient, diffuse, specular;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ debug(check)
+ if(flags != 2 && flags != 4) nifFile.warn("Unknown flags");
+
+ nifFile.getIntIs(1);
+ if(nifFile.getInt == 0) nifFile.warn("Light list might be missing");
+
+ dimmer = nifFile.getFloat;
+ ambient = nifFile.getVector;
+ diffuse = nifFile.getVector;
+ specular = nifFile.getVector;
+
+ debug(verbose)
+ {
+ writefln("Dimmer: ", dimmer);
+ writefln("Ambient: ", ambient.toString);
+ writefln("Diffuse: ", diffuse.toString);
+ writefln("Specular: ", specular.toString);
+ }
+ }
+}
+
+class NiTextureEffect : Effect
+{
+ NiSourceTexture texture;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ debug(check)
+ if(flags != 2 && flags != 4) nifFile.warn("Unknown flags");
+
+ int i = nifFile.wgetInt; // 0 or 1
+
+ if(i == 1) {if(nifFile.getInt == 0) nifFile.warn("List might be missing");}
+ else if(i != 0) nifFile.warn("Unknown value");
+
+ for(int j; j<3; j++)
+ {
+ nifFile.wgetFloatIs(1);
+ nifFile.wgetFloatIs(0);
+ nifFile.wgetFloatIs(0);
+ nifFile.wgetFloatIs(0);
+ }
+
+ nifFile.wgetIntIs(2);
+ nifFile.wgetIntIs(0,3);
+ nifFile.wgetIntIs(2);
+ nifFile.wgetIntIs(2);
+ debug(verbose) writef("Source Texture ");
+ getIndex();
+ nifFile.wgetByteIs(0);
+
+ nifFile.wgetFloatIs(1);
+ nifFile.wgetFloatIs(0);
+ nifFile.wgetFloatIs(0);
+ nifFile.wgetFloatIs(0);
+
+ nifFile.wgetShortIs(0);
+ nifFile.wgetShortIs(-75);
+ nifFile.wgetShortIs(0);
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ texture = lookup!(NiSourceTexture)(list);
+ }
+}
diff --git a/nif/extra.d b/nif/extra.d
new file mode 100644
index 0000000000..559266c470
--- /dev/null
+++ b/nif/extra.d
@@ -0,0 +1,210 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (extra.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.extra;
+import nif.record;
+import nif.controlled;
+
+abstract class Extra : Record
+{
+ Extra extra;
+
+ override:
+ void read()
+ {
+ super.read();
+ debug(verbose) nifFile.writef("Extra Data ");
+ getIndex();
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ extra = lookup!(Extra)(list);
+ }
+}
+
+class NiVertWeightsExtraData : Extra
+{
+ override:
+ void read()
+ {
+ super.read();
+
+ int i = nifFile.getInt;
+ short s = nifFile.getShort; // = number of vertices
+
+ if(s*4+2 != i)
+ nifFile.warn("Sizes don't add up");
+
+ debug(verbose)
+ {
+ writefln("Total bytes in record: ", i);
+ writefln("Number of vertex weights: ", s);
+ }
+
+ for(int j; j
+ WWW: http://openmw.snaptoad.com/
+
+ This file (misc.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+// This doesn't have to be part of the nif package at all.
+module nif.misc;
+
+import std.utf;
+import std.string;
+import monster.util.string;
+
+// Find an alternative to this
+char[] clean(char[] s)
+{
+ try{validate(s);}
+ catch(UtfException e)
+ {
+ return "(invalid utf-string)";
+ }
+ return s;
+}
+
+unittest
+{
+ assert(clean("abc æøå") == "abc æøå");
+}
+
+struct Vector
+{
+ float array[3];
+
+ void set(float x, float y, float z)
+ {
+ array[0] = x;
+ array[1] = y;
+ array[2] = z;
+ }
+
+ char[] toString()
+ {
+ return format(array);
+ //return format("[", array[0], ",", array[1], ",", array[2], "]");
+ }
+
+ int opEquals(ref Vector v)
+ {
+ return v.array == array;
+ }
+
+ static assert(Vector.sizeof == 4*3);
+}
+
+unittest
+{
+ Vector a, b;
+ a.set(1,2,3);
+ assert(a!=b);
+ b = a;
+ assert(a==b);
+}
+
+struct Vector4
+{
+ float array[4];
+
+ void set(float x, float y, float z, float a)
+ {
+ array[0] = x;
+ array[1] = y;
+ array[2] = z;
+ array[3] = a;
+ }
+
+ char[] toString()
+ {
+ return format(array);
+ //return format("[", array[0], ",", array[1], ",", array[2], ",", array[3], "]");
+ }
+
+ int opEquals(ref Vector4 v)
+ {
+ return v.array == array;
+ }
+
+ static assert(Vector4.sizeof == 4*4);
+}
+
+unittest
+{
+ Vector4 a, b;
+ a.set(1,2,3,5);
+ assert(a!=b);
+ b = a;
+ assert(a==b);
+}
+
+align(1)
+struct Matrix
+{
+ union
+ {
+ Vector v[3];
+ float[9] array;
+ }
+
+ char[] toString()
+ {
+ char[] res;
+ res ~= " Right: " ~ v[0].toString;
+ res ~= "\n Up: " ~ v[1].toString;
+ res ~= "\n Front: " ~ v[2].toString;
+ return res;
+ }
+ static assert(Matrix.sizeof == 3*3*4);
+}
+
+
+align(1)
+struct Transformation
+{
+ Vector pos;
+ Matrix rotation;
+ float scale;
+ Vector velocity;
+
+ char[] toString()
+ {
+ char[] res;
+ res ~= " Translation: " ~ pos.toString();
+ res ~= "\n" ~ rotation.toString();
+ res ~= "\n Scale: " ~ format(scale);
+ res ~= "\n Velocity: " ~ velocity.toString();
+ return res;
+ }
+
+ static assert(Transformation.sizeof == 5*Vector.sizeof + 4);
+}
diff --git a/nif/nif.d b/nif/nif.d
new file mode 100644
index 0000000000..fe9d3e1dcf
--- /dev/null
+++ b/nif/nif.d
@@ -0,0 +1,106 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (nif.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.nif;
+
+import nif.base;
+import nif.niffile;
+import nif.record;
+
+import core.memory;
+
+/* The NIFMesh struct will open, read and close a .nif file. Unlike
+ * NIFFile (which it uses), it stores all the data from the NIF.
+ */
+NIFMesh nifMesh;
+
+struct NIFMesh
+{
+ public:
+ Record records[];
+
+ // Read from a file
+ void open(char[] file)
+ {
+ nifFile.open(file);
+ loadData(); // Load the data
+ }
+
+ // Read from a memory slice. The optitional file name is used for
+ // error messages only.
+ void open(void[] s, char[] name = "")
+ {
+ nifFile.open(s, name);
+ loadData(); // Load the data
+ }
+
+ // Clear all loaded data
+ void clear()
+ {
+ records = null;
+
+ // Clear the region manager
+ nifRegion.freeAll();
+ }
+
+ /*
+ void fail(char[] msg)
+ {
+ throw new NIFMeshException(msg);
+ }
+ */
+
+ private void loadData()
+ {
+ // Remove any previously loaded data
+ clear();
+
+ // The actual parsing is done somewhere else.
+ nif.base.parseFile();
+
+ // Close the file
+ nifFile.close();
+
+ // Resolve inter-record references into object pointers.
+ foreach(int i, Record o; records)
+ if(o !is null)
+ {
+ nifFile.setRec(i, o);
+ o.sortOut(records);
+ }
+
+ // Let the records do consistency checks on their data.
+ foreach(int i, Record o; records)
+ if(o !is null)
+ {
+ nifFile.setRec(i, o);
+ o.check();
+ }
+
+ // Internal state check, used for debugging
+ debug(statecheck)
+ foreach(Record o; records)
+ if(o !is null) o.finalCheck();
+ }
+
+}
diff --git a/nif/niffile.d b/nif/niffile.d
new file mode 100644
index 0000000000..1b28e9ae07
--- /dev/null
+++ b/nif/niffile.d
@@ -0,0 +1,485 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (niffile.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.niffile;
+
+public import std.stream;
+public import util.regions;
+
+import std.string;
+import nif.misc;
+
+public import core.memory;
+
+// Exception class thrown by most exceptions originating within the
+// nif package
+class NIFFileException : Exception
+{
+ this(char[] msg) {super("NIFFile Exception: " ~ msg);}
+}
+
+/* The NIFFile struct is used for the task of reading from NIF
+ files. It provides specialized methods for handling common types,
+ records, etc., and also provides mechanisms for output and error
+ handling. It does not store any (non-transient) NIF data.
+ */
+NIFFile nifFile;
+
+struct NIFFile
+{
+ private:
+ Stream f; // Input stream
+ BufferedFile bf; // Used and reused for direct file input.
+ TArrayStream!(ubyte[]) ss; // For stream reading
+
+ // These are used for warnings and error messages only
+ char[] filename;
+ int recNum; // Record number
+ char[] recName; // Record name
+ Object recObj; // Record object
+ public:
+
+ /* ------------------------------------------------
+ * Object initialization and file management
+ * ------------------------------------------------
+ */
+
+ // Open a file from the file system
+ void open(char[] file)
+ {
+ close();
+
+ // Create a BufferedFile, and reuse it later.
+ if(bf is null)
+ bf = new BufferedFile();
+
+ bf.open(file);
+
+ f = bf;
+ filename = file;
+ }
+
+ // Open a memory slice, the name is used for error messages.
+ void open(void[] data, char[] name)
+ {
+ close();
+
+ // Create a TArrayStream, and reuse it
+ if(ss is null)
+ {
+ ss = new TArrayStream!(ubyte[])(cast(ubyte[])data);
+ ss.writeable = false; // Read-only
+ }
+ else
+ {
+ // TArrayStream lacks the open() method. Instead, all the
+ // members are public.
+ ss.buf = cast(ubyte[])data;
+ ss.len = ss.buf.length;
+ ss.cur = 0;
+ }
+
+ f = ss;
+ filename = name;
+ }
+
+ // Close the file handle
+ void close()
+ {
+ // Close the buffered file
+ if(f !is null && f is bf) f.close();
+ f = null;
+
+ // Zero out variables
+ filename = null;
+ recNum = 0;
+ recName = null;
+ recObj = null;
+ }
+
+ long size()
+ in
+ {
+ if(f is null) fail("Cannot get size, file is not open");
+ }
+ body
+ {
+ return f.size();
+ }
+
+ bool eof() { return f.eof(); }
+
+ void seekCur(ulong skip)
+ in
+ {
+ if(f is null) fail("Cannot seek, file is not open");
+ }
+ body
+ {
+ f.seekCur(skip);
+ }
+
+ ulong position()
+ {
+ return f.position();
+ }
+
+ /* ------------------------------------------------
+ * Error reporting
+ * ------------------------------------------------
+ */
+
+ // Used to format error messages
+ private char[] makeError(char[] str)
+ {
+ if(recNum > 0)
+ {
+ if(recName == "" && recObj !is null) recName = recObj.toString;
+ str = format("%s\n Record %d: %s", str, recNum-1, recName);
+ }
+ if(filename != "") str = format("%s\n File: %s", str, filename);
+ if(f !is null)
+ if(f.eof()) str = format("%s\n At end of file", str);
+ else str = format("%s\n Offset 0x%x", str, f.position);
+ return str ~ "\n";
+ }
+
+ void fail(char[] msg)
+ {
+ throw new NIFFileException(makeError(msg));
+ }
+
+ debug(warnstd) import std.stdio;
+
+ void warn(char[] msg)
+ {
+ debug(strict) fail("(fail on warning) " ~ msg);
+ else
+ {
+ debug(warnstd) writefln("WARNING: ", makeError(msg));
+ debug(warnlog) log.writefln("WARNING: ", makeError(msg));
+ }
+ }
+
+ void assertWarn(bool condition, char[] str)
+ { if(!condition) warn(str); }
+
+ /*
+ void assertFail(bool condition, char[] str)
+ { if(!condition) fail(str); }
+ */
+
+ // Used for error message handling, hackish.
+ void setRec(int i, char[] n)
+ {
+ recNum = i+1;
+ recName = n;
+ recObj = null;
+ }
+
+ // This variant takes an object instead of a name. That way we only
+ // need to call toString when the name is needed (which it mostly
+ // isn't.)
+ void setRec(int i, Object o)
+ {
+ recNum = i+1;
+ recObj = o;
+ recName = null;
+ }
+
+ /* ------------------------------------------------
+ * Reader method templates (skip to next section
+ * for usable methods)
+ * ------------------------------------------------
+ */
+
+ // Read a variable of a given type T
+ T getType(T)()
+ {
+ T t;
+ f.read(t);
+ return t;
+ }
+
+ // Read a variable and compare it to what we want it to be
+ template getTypeIs(T)
+ {
+ T getTypeIs(T[] pt ...)
+ {
+ T t = getType!(T)();
+ debug(check)
+ {
+ bool match;
+ foreach(T a; pt)
+ if(a == t) {match = true; break;}
+
+ if(!match)
+ {
+ char[] errMsg = format(typeid(T),
+ " mismatch: got %s, expected ", t);
+ if(pt.length > 1) errMsg ~= "one of: ";
+ foreach(T a; pt)
+ errMsg ~= format("%s ", a);
+ warn(errMsg);
+ }
+ }
+ return t;
+ }
+ }
+
+ debug(verbose)
+ {
+ // Read a variable of a given type T and print it to screen
+ template wgetType(T)
+ {
+ T wgetType()
+ {
+ T t;
+ f.read(t);
+ writefln(typeid(T), ": ", t);
+ return t;
+ }
+ }
+
+ // Read a variable and compare it to what we want it to be
+ template wgetTypeIs(T)
+ {
+ T wgetTypeIs(T pt[] ...)
+ {
+ T t = getType!(T)();
+
+ char[] wanted;
+ if(pt.length > 1) wanted = "one of: ";
+ foreach(T a; pt) wanted ~= format("%s ", a);
+
+ writefln(typeid(T), ": ", t, " (wanted %s)", wanted);
+
+ debug(check)
+ {
+ bool match;
+ foreach(T a; pt)
+ if(a == t) {match = true; break;}
+
+ if(!match)
+ {
+ warn(format(typeid(T),
+ " mismatch: got %s, expected %s", t, wanted));
+ }
+ }
+ return t;
+ }
+ }
+ }
+
+ // Fill the provided array of Ts
+ template getArrayLen(T)
+ {
+ T[] getArrayLen(T[] arr)
+ {
+ f.readExact(arr.ptr, arr.length*T.sizeof);
+ return arr;
+ }
+ }
+
+ // Set the size of the provided array to 'count', and fill it.
+ template getArraySize(T)
+ {
+ T[] getArraySize(int count)
+ {
+ T[] arr;
+ fitArray(count, T.sizeof);
+ arr = cast(T[])nifRegion.allocate(count * T.sizeof);
+ getArrayLen!(T)(arr);
+ return arr;
+ }
+ }
+
+ // Get an array of Ts preceded by an array length of type Index
+ // (assumed to be an integer type)
+ template getArray(T,Index)
+ {
+ T[] getArray()
+ {
+ // Read array length
+ Index s = getType!(Index)();
+
+ // Is array larger than file?
+ fitArray(s,T.sizeof);
+
+ // Allocate the buffer
+ T[] result = cast(T[])nifRegion.allocate(s * T.sizeof);
+
+ // Read the buffer
+ return getArrayLen!(T)(result);
+ }
+ }
+
+ /* ------------------------------------------------
+ * Reader methods
+ * ------------------------------------------------
+ */
+
+ // Strings
+ alias getArrayLen!(char) getString;
+ alias getArray!(char,int) getString;
+
+ // Other arrays
+ alias getArray!(int,int) getInts;
+
+ // Checks if an array of size elements each of size esize could
+ // possibly follow in the file.
+ void fitArray(int size, int esize)
+ in
+ {
+ assert(esize > 0);
+ }
+ body
+ {
+ if((size*esize+8) > (f.size() - f.position()) || size < 0)
+ fail(format(
+ "Array out of bounds: %d*%d + 8 bytes needed, but only %d bytes left in file",
+ size,esize,(f.size-f.position)));
+ }
+
+ // Base types
+ alias getType!(byte) getByte;
+ alias getType!(ubyte) getUbyte;
+ alias getType!(short) getShort;
+ alias getType!(ushort) getUshort;
+ alias getType!(int) getInt;
+ alias getType!(uint) getUint;
+ alias getType!(float) getFloat;
+
+ // Base types with error checking
+ alias getTypeIs!(short) getShortIs;
+ alias getTypeIs!(ushort) getUshortIs;
+ alias getTypeIs!(byte) getByteIs;
+ alias getTypeIs!(ubyte) getUbyteIs;
+ alias getTypeIs!(int) getIntIs;
+ alias getTypeIs!(uint) getUintIs;
+ alias getTypeIs!(float) getFloatIs;
+
+ debug(verbose)
+ {
+ // Base types
+ alias wgetType!(byte) wgetByte;
+ alias wgetType!(ubyte) wgetUbyte;
+ alias wgetType!(short) wgetShort;
+ alias wgetType!(ushort) wgetUshort;
+ alias wgetType!(int) wgetInt;
+ alias wgetType!(uint) wgetUint;
+ alias wgetType!(float) wgetFloat;
+
+ // Base types with error checking
+ alias wgetTypeIs!(short) wgetShortIs;
+ alias wgetTypeIs!(ushort) wgetUshortIs;
+ alias wgetTypeIs!(byte) wgetByteIs;
+ alias wgetTypeIs!(ubyte) wgetUbyteIs;
+ alias wgetTypeIs!(int) wgetIntIs;
+ alias wgetTypeIs!(uint) wgetUintIs;
+ alias wgetTypeIs!(float) wgetFloatIs;
+ }
+ else
+ {
+ // Base types
+ alias getByte wgetByte;
+ alias getUbyte wgetUbyte;
+ alias getShort wgetShort;
+ alias getUshort wgetUshort;
+ alias getInt wgetInt;
+ alias getUint wgetUint;
+ alias getFloat wgetFloat;
+
+ // Base types with error checking
+ alias getByteIs wgetByteIs;
+ alias getUbyteIs wgetUbyteIs;
+ alias getShortIs wgetShortIs;
+ alias getUshortIs wgetUshortIs;
+ alias getIntIs wgetIntIs;
+ alias getUintIs wgetUintIs;
+ alias getFloatIs wgetFloatIs;
+ }
+
+ // Vectors
+ Vector getVector()
+ {
+ Vector v;
+ getArrayLen!(float)(v.array);
+ return v;
+ }
+
+ Vector4 getVector4()
+ {
+ Vector4 v;
+ getArrayLen!(float)(v.array);
+ return v;
+ }
+
+ Vector getVectorIs(float x, float y, float z)
+ {
+ Vector v = getVector();
+ debug(check)
+ {
+ Vector u;
+ u.set(x,y,z);
+ if(v != u)
+ warn("Vector mismatch: expected " ~ u.toString ~ ", got " ~ v.toString);
+ }
+ return v;
+ }
+
+ debug(verbose)
+ {
+ Vector wgetVector()
+ {
+ Vector v = getVector();
+ writefln("vector: ", v.toString);
+ return v;
+ }
+
+ Vector4 wgetVector4()
+ {
+ Vector4 v = getVector4();
+ writefln("4-vector: ", v.toString);
+ return v;
+ }
+ }
+ else
+ {
+ alias getVector wgetVector;
+ alias getVector4 wgetVector4;
+ }
+
+ void getMatrix(ref Matrix m)
+ {
+ getArrayLen!(float)(m.array);
+ }
+
+ void getTransformation(ref Transformation t)
+ {
+ t.pos = getVector();
+ getMatrix(t.rotation);
+ t.scale = getFloat/*Is(1)*/;
+ t.velocity = getVectorIs(0,0,0);
+ }
+}
diff --git a/nif/niftool.d b/nif/niftool.d
new file mode 100644
index 0000000000..6e60d23c3d
--- /dev/null
+++ b/nif/niftool.d
@@ -0,0 +1,63 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (niftool.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.niftool;
+
+import nif.nif;
+import nif.niffile;
+import std.stdio;
+import std.utf;
+
+int main(char[][] args)
+{
+ if(args.length < 2)
+ {
+ writefln("You must specify a file");
+ return 1;
+ }
+
+ initializeMemoryRegions();
+
+ bool errors;
+
+ foreach(char[] fn; args[1..$])
+ {
+ writefln("Opening %s", fn);
+ try nifMesh.open(fn);
+ catch(NIFFileException e)
+ {
+ writefln("Got exception: %s", e);
+ errors = true;
+ }
+ catch(UtfException e)
+ {
+ writefln("Got an UTF-error: %s", e);
+ writefln("We have to ignore the rest of the file at this point. If you want to");
+ writefln("test the entire file, compile the NIF lib without verbose output.");
+ }
+ nifMesh.clear();
+ writefln();
+ }
+ if(errors) return 1;
+ return 0;
+}
diff --git a/nif/node.d b/nif/node.d
new file mode 100644
index 0000000000..639961583c
--- /dev/null
+++ b/nif/node.d
@@ -0,0 +1,324 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (node.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.node;
+
+import nif.record;
+
+// Tree node
+abstract class Node : Named
+{
+ ushort flags;
+ Transformation trafo;
+ //Extra properties[];
+ Property properties[];
+
+ Node parent;
+
+ // Bounding box info
+ bool hasBounding;
+ Vector boundPos;
+ Matrix boundRot;
+ Vector boundXYZ;
+
+ void setParent(Node p)
+ {
+ debug(veryverbose)
+ writefln("Setting parent of ", toString, " to ", p);
+
+ if(parent !is null)
+ {
+ char str[] = toString() ~ " already has parent " ~ parent.toString()
+ ~ ", but setting ";
+ if(p !is null) str ~= p.toString;
+ else str ~= "to null";
+
+ nifFile.warn(str);
+ }
+
+ parent = p;
+ }
+
+ override:
+ void read()
+ {
+ super.read();
+
+ flags = nifFile.getShort();
+ nifFile.getTransformation(trafo);
+
+ debug(verbose)
+ {
+ writefln("Flags: %x", flags);
+ writefln("Transformation:\n", trafo.toString);
+ writef("Properties: ");
+ }
+
+ properties = nifRegion.allocateT!(Property)(getIndexList());
+
+ if(nifFile.getInt != 0)
+ {
+ hasBounding = true;
+ nifFile.getIntIs(1);
+ boundPos = nifFile.getVector;
+ nifFile.getMatrix(boundRot);
+ boundXYZ = nifFile.getVector;
+ //if(name != "Bounding Box") nifFile.warn("Node name is not 'Bounding Box'");
+ debug(verbose)
+ {
+ writefln("Bounding box: ", boundPos.toString);
+ writefln(boundRot.toString);
+ writefln("XYZ: ", boundXYZ.toString);
+ }
+ }
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ lookupList!(Property)(list,properties);
+ }
+
+}
+
+class NiCamera : Node
+{
+ float left, right, top, bottom, near, far,
+ vleft, vright, vtop, vbottom, LOD;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ left = nifFile.getFloat;
+ right = nifFile.getFloat;
+ top = nifFile.getFloat;
+ bottom = nifFile.getFloat;
+ near = nifFile.getFloat;
+ far = nifFile.getFloat;
+
+ vleft = nifFile.getFloat;
+ vright = nifFile.getFloat;
+ vtop = nifFile.getFloat;
+ vbottom = nifFile.getFloat;
+
+ LOD = nifFile.getFloat;
+
+ nifFile.getIntIs(-1);
+ nifFile.getIntIs(0);
+
+ debug(verbose)
+ {
+ writefln("Camera frustrum:");
+ writefln(" Left: ", left);
+ writefln(" Right: ", right);
+ writefln(" Top: ", top);
+ writefln(" Bottom: ", bottom);
+ writefln(" Near: ", near);
+ writefln(" Far: ", far);
+
+ writefln("View port:");
+ writefln(" Left: ", vleft);
+ writefln(" Right: ", vright);
+ writefln(" Top: ", vtop);
+ writefln(" Bottom: ", vbottom);
+
+ writefln("LOD adjust: ", LOD);
+ }
+ }
+}
+
+class NiAutoNormalParticles : Node
+{
+ NiAutoNormalParticlesData data;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ debug(check)
+ {
+ if(flags & 0xffff-6)
+ nifFile.warn("Unknown flags");
+ }
+
+ debug(verbose) writef("Particle Data ");
+ getIndex();
+
+ nifFile.getIntIs(-1);
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ data = lookup!(NiAutoNormalParticlesData)(list);
+
+ debug(check)
+ {
+ if(castCheck!(NiParticleSystemController)(controller)
+ && castCheck!(NiBSPArrayController)(controller))
+ nifFile.warn("In " ~ toString ~ ": did not expect controller "
+ ~ controller.toString);
+ }
+ }
+}
+
+class NiRotatingParticles : Node
+{
+ NiRotatingParticlesData data;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ debug(check)
+ {
+ if(flags & 0xffff-6)
+ nifFile.warn("Unknown flags");
+ }
+
+ //nifFile.getIntIs(0);
+
+ debug(verbose) writef("Particle Data ");
+ getIndex();
+
+ nifFile.getIntIs(-1);
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ data = lookup!(NiRotatingParticlesData)(list);
+
+ debug(check)
+ {
+ if(castCheck!(NiParticleSystemController)(controller)
+ && castCheck!(NiBSPArrayController)(controller) )
+ nifFile.warn("In " ~ toString ~ ": did not expect controller "
+ ~ controller.toString);
+ }
+ //castWarn!(NiParticleSystemController)(controller);
+ }
+}
+
+class NiTriShape : Node
+{
+ NiTriShapeData data;
+ NiSkinInstance skin;
+
+ override:
+ void read()
+ {
+ super.read();
+
+ debug(verbose)
+ {
+ // If this is correct, it suggests how one might decode
+ // other flags. Check if this one is correct in sortOut().
+ if(flags&0x40) writefln("Mesh has no normals?");
+ }
+ debug(check)
+ {
+ if(flags & (0xffff-0x47))
+ nifFile.warn("Unknown flags were set");
+ }
+
+ //nifFile.getIntIs(0);
+ debug(verbose) writef("Mesh index: ");
+ getIndex();
+ debug(verbose) writef("Skin index: ");
+ getIndex();
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ data = lookup!(NiTriShapeData)(list);
+ skin = lookup!(NiSkinInstance)(list);
+
+ debug(check)
+ {
+ if(data is null) nifFile.warn("Data missing from NiTriShape");
+
+ if(castCheck!(NiGeomMorpherController)(controller)
+ && castCheck!(NiUVController)(controller)
+ && castCheck!(NiVisController)(controller) )
+ nifFile.warn("In " ~ toString ~ ": did not expect controller "
+ ~ controller.toString);
+
+ if((data.normals.length == 0) && (flags & 0x40 == 0) ||
+ (data.normals.length != 0) && (flags & 0x40))
+ nifFile.warn("0x40 present but normals missing, or vice versa");
+ }
+ }
+}
+
+class NiNode : Node
+{
+ Node children[];
+ Effect effects[];
+
+ override:
+ void read()
+ {
+ super.read();
+
+ debug(verbose)
+ {
+ if(flags & 1) writefln(" 0x01 Hidden");
+ if(flags & 2) writefln(" 0x02 Collision detection?");
+ if(flags & 4) writefln(" 0x04 Collision detection2 ?");
+ if(flags & 8) writefln(" 0x08 Unknown but common");
+ if(flags & 0x20) writefln(" 0x20 Unknown");
+ if(flags & 0x40) writefln(" 0x40 Unknown");
+ if(flags & 0x80) writefln(" 0x80 Unknown");
+ }
+ debug(check)
+ if(flags & 0x10) nifFile.warn("Unknown flags were set");
+
+ debug(verbose) writef("Child nodes: ");
+ children = nifRegion.allocateT!(Node)(getIndexList());
+ debug(verbose) writef("Effects: ");
+ effects = nifRegion.allocateT!(Effect)(getIndexList());
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+
+ lookupList!(Node)(list,children);
+ lookupList!(Effect)(list,effects);
+
+ if(castCheck!(NiKeyframeController)(controller)
+ && castCheck!(NiVisController)(controller)
+ && castCheck!(NiPathController)(controller))
+ nifFile.warn("In " ~ toString ~ ": did not expect controller "
+ ~ controller.toString);
+
+ foreach(Node n; children)
+ n.setParent(this);
+ }
+}
diff --git a/nif/property.d b/nif/property.d
new file mode 100644
index 0000000000..adbf1d4eb3
--- /dev/null
+++ b/nif/property.d
@@ -0,0 +1,261 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (property.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.property;
+import nif.record;
+
+abstract class Property : Named
+{
+ ushort flags;
+
+ override:
+ void read()
+ {
+ super.read();
+ flags = nifFile.getUshort;
+ debug(verbose) writefln("Flags: %x", flags);
+ }
+}
+
+// Check the specs on this one again
+class NiTexturingProperty : Property
+{
+ /* Apply mode:
+ 0 - replace
+ 1 - decal
+ 2 - modulate
+ 3 - hilight // These two are for PS2 only?
+ 4 - hilight2
+ */
+ int apply;
+
+ struct Texture
+ {
+ bool inUse;
+ NiSourceTexture texture;
+
+ /* Clamp mode (i don't understand this)
+ 0 - clampS clampT
+ 1 - clampS wrapT
+ 2 - wrapS clampT
+ 3 - wrapS wrapT
+ */
+
+ /* Filter:
+ 0 - nearest
+ 1 - bilinear
+ 2 - trilinear
+ 3, 4, 5 - who knows
+ */
+
+ int clamp, set, filter;
+ short ps2L, ps2K, unknown2;
+
+ void read(NiTexturingProperty caller)
+ {
+ if(nifFile.getInt == 0)
+ {
+ debug(verbose) writefln("No texture");
+ return;
+ }
+
+ inUse = 1;
+
+ debug(verbose) writef(" Texture ");
+ caller.getIndex();
+
+ clamp = nifFile.getIntIs(0,1,2,3);
+ filter = nifFile.getIntIs(0,1,2);
+ set = nifFile.getInt;
+
+ ps2L = nifFile.getShortIs(0);
+ ps2K = nifFile.getShortIs(-75,-2);
+
+ debug(verbose)
+ {
+ writefln(" Clamp ", clamp);
+ writefln(" Filter ", filter);
+ writefln(" Set? ", set);
+ writefln(" ps2L ", ps2L);
+ writefln(" ps2K ", ps2K);
+ }
+
+ unknown2 = nifFile.wgetShortIs(0,257);
+ }
+ }
+
+ /*
+ * This just in: The textures in this list is as follows:
+ *
+ * 0 - Base texture
+ * 1 - Dark texture
+ * 2 - Detail texture
+ * 3 - Gloss texture (never used?)
+ * 4 - Glow texture
+ * 5 - Bump map texture
+ * 6 - Decal texture
+ */
+ Texture[7] textures;
+
+ override:
+ void read()
+ {
+ super.read();
+ if(flags > 1) nifFile.warn("Unknown flags");
+
+ apply = nifFile.getInt;
+ debug(verbose) writefln("Apply mode: ", apply);
+ nifFile.getIntIs(7);
+
+ textures[0].read(this); // Base
+ textures[1].read(this); // Dark
+ textures[2].read(this); // Detail
+
+ nifFile.getIntIs(0); // Gloss
+
+ textures[4].read(this); // Glow
+ textures[5].read(this); // Bump map
+
+ if(textures[5].inUse)
+ {
+ float lumaScale = nifFile.wgetFloat;
+ float lumaOffset = nifFile.wgetFloat;
+ Vector4 lumaMatrix = nifFile.wgetVector4;
+ }
+
+ textures[6].read(this); // Decal
+ }
+
+ void sortOut(Record[] list)
+ {
+ super.sortOut(list);
+ foreach(ref Texture t; textures)
+ if(t.inUse) t.texture = lookup!(NiSourceTexture)(list);
+ }
+}
+
+class NiMaterialProperty : Property
+{
+ Vector ambient, diffuse, specular, emissive;
+ float glossiness, alpha;
+
+ override:
+ void read()
+ {
+ super.read();
+ if(flags != 1) nifFile.warn("Unknown flags");
+
+ ambient = nifFile.getVector;
+ diffuse = nifFile.getVector;
+ specular = nifFile.getVector;
+ emissive = nifFile.getVector;
+ glossiness = nifFile.getFloat;
+ alpha = nifFile.getFloat; // Use Alpha Property when this is not 1
+
+ debug(verbose)
+ {
+ writefln("Ambient: ", ambient.toString);
+ writefln("Diffuse: ", diffuse.toString);
+ writefln("Specular: ", specular.toString);
+ writefln("Emissive: ", emissive.toString);
+ writefln("Glossiness: ", glossiness);
+ writefln("Alpha: ", alpha);
+ }
+ }
+}
+class NiVertexColorProperty : Property
+{
+ /* Vertex mode:
+ 0 - source ignore
+ 1 - source emmisive
+ 2 - source amb diff
+
+ Lighting mode
+ 0 - lighting emmisive
+ 1 - lighting emmisive ambient/diffuse
+ */
+ int vertmode, lightmode;
+
+ override:
+ void read()
+ {
+ super.read();
+ if(flags != 0) nifFile.warn("Unknown flags");
+ vertmode = nifFile.getIntIs(0,1,2);
+ lightmode = nifFile.getIntIs(0,1);
+ debug(verbose)
+ {
+ writefln("Vertex mode: ", vertmode);
+ writefln("Lighting mode: ", lightmode);
+ }
+ }
+}
+
+alias NiWireframeProperty NiDitherProperty;
+alias NiWireframeProperty NiSpecularProperty;
+
+class NiWireframeProperty : Property
+{
+ override:
+ void read()
+ {
+ super.read();
+ if(flags != 1) nifFile.warn("Unknown flags");
+ }
+}
+
+class NiShadeProperty : Property
+{
+ override:
+ void read()
+ {
+ super.read();
+ if(flags != 0) nifFile.warn("Unknown flags");
+ }
+}
+
+class NiAlphaProperty : Property
+{
+ override:
+ void read()
+ {
+ super.read();
+ ubyte b = nifFile.getUbyte;
+ debug(verbose) writefln("Unknown byte: ", b);
+ }
+}
+
+class NiZBufferProperty : Property
+{
+ override:
+ void read()
+ {
+ super.read();
+ debug(verbose)
+ {
+ if(flags&1) writefln(" 0x01 ZTest");
+ if(flags&2) writefln(" 0x02 ZWrite");
+ }
+ if(flags & 0xfc) nifFile.warn("Unknown flags");
+ }
+}
diff --git a/nif/record.d b/nif/record.d
new file mode 100644
index 0000000000..61c08efe68
--- /dev/null
+++ b/nif/record.d
@@ -0,0 +1,302 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (record.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module nif.record;
+
+public
+{
+ import util.regions;
+
+ import nif.base;
+ import nif.controller;
+ import nif.controlled;
+ import nif.node;
+ import nif.data;
+ import nif.extra;
+ import nif.property;
+ import nif.effect;
+
+ import nif.niffile;
+ import nif.misc;
+
+ import std.string;
+}
+
+// Base class for all NIF records
+abstract class Record
+{
+ protected:
+ // List of dependency indices. We throw this away after the entire
+ // file is loaded and the dependencies have been converted to object
+ // pointers.
+ RegionBuffer!(int) depList;
+
+ debug(statecheck)
+ {
+ // An internal 'state', this check is only intended for
+ // debugging purposes (hence the surrounding debug block ;-)
+ int state;
+ /* 0 - nothing has been done
+ * 1 - record has been read
+ * 2 - sortOut has been called
+ * 3 - check has been called
+ */
+ }
+ public:
+
+ new(uint sz)
+ {
+ // After eliminating all GC dependence, this is no longer
+ // needed.
+ //return nifRegion.allocateGC(sz).ptr;
+
+ return nifRegion.allocate(sz).ptr;
+ }
+
+ delete(void *p) { assert(0); }
+
+ debug(statecheck)
+ final void finalCheck()
+ {
+ debug(veryverbose)
+ writefln("Final check on ", this);
+ assert(state==3);
+ }
+
+ // Read record data from file f. All indices are stored temporarily
+ // as integers, as they appear in the file.
+ void read()
+ {
+ // Allocate the dependency list. 50 should be enough entries.
+ depList = nifRegion.getBuffer!(int)(0,50);
+
+ debug(veryverbose)
+ writefln("Reading ", this, " at offset %x", nifFile.position);
+ debug(statecheck) assert(state++ == 0);
+ }
+
+ // Sort out dependencies between records. Called after all records
+ // have been read from file. Paramter 'list' contains all records in
+ // the order they appear in the file. Used to convert integer
+ // indices into object pointers, and checking that all types are
+ // correct. Can also be used for data checks than only require this
+ // objects dependencies.
+ void sortOut(Record[] list)
+ {
+ debug(veryverbose) writefln("Sorting out ", this);
+ debug(statecheck) assert(state++ == 1);
+ }
+
+ // Consistancy check. Called after all dependencies have been
+ // sorted. Can be used for checking that vertex counts are the same
+ // in different records, etc. It checks the depList array.
+ void check()
+ {
+ debug(veryverbose) writefln("Consistancy check on ", this);
+
+ // Check that all dependencies have been resolved. delList is
+ // successively sliced down to zero when indices are looked up.
+ assert(depList.length == 0);
+
+ debug(statecheck) assert(state++ == 2);
+ }
+
+ // Convert an integer record index to an object pointer of type T.
+ template lookup(T: Record)
+ {
+ T lookup(Record[] list)
+ {
+ // Get the next dependency from the list
+ int i = depList[0];
+ depList = depList[1..depList.length()];
+
+ debug(verbose)
+ {
+ T t = lookupCast!(T)(i, list);
+ debug(veryverbose)
+ {
+ writef(" Resolved ", i, " to ");
+ if(t is null) writefln("(null)");
+ else writefln(t);
+ }
+ return t;
+ }
+ else
+ return lookupCast!(T)(i, list);
+ }
+ }
+
+ template lookupList(T: Record)
+ {
+ void lookupList(Record[] list, T[] deps)
+ {
+ foreach(ref T t; deps)
+ t = lookup!(T)(list);
+ }
+ }
+
+ // Reads an index (int) and adds it to the list of
+ // dependencies. These can be returned later by lookup().
+ void getIndex()
+ {
+ depList ~= nifFile.getInt;
+ debug(verbose) writefln("Index ", depList[depList.length()-1]);
+ }
+
+ int getIndexList()
+ {
+ int[] l = nifFile.getInts;
+ int num;
+
+ // Only add non-null references
+ foreach(int i; l)
+ if(i != -1)
+ {
+ depList ~= i;
+ num++;
+ }
+
+ //depList ~= l;
+
+ debug(verbose)
+ {
+ writef("Index list: ");
+ foreach(int i; l)
+ writef(i, " ");
+ writefln("(%d kept)", num);
+ }
+
+ return num;
+ }
+}
+
+// Lookup with casting and error checking.
+template lookupCast(T: Record)
+{
+ T lookupCast(int i, Record[] list)
+ {
+ if(i == -1) return null;
+
+ if(i < 0 || i >= list.length)
+ nifFile.fail(format("Record index %d out of bounds (got %d records.)",
+ i, list.length));
+
+ Record r = list[i];
+
+ if(r is null)
+ {
+ nifFile.warn("Referenced an unknown record type");
+ return null;
+ }
+
+ T t = cast(T)r;
+ if(t is null)
+ nifFile.fail("Cannot convert " ~ r.toString() ~ " to " ~
+ (cast(TypeInfo_Class)typeid(T)).info.name);
+
+ return t;
+ }
+}
+
+template castCheck(T: Record)
+{
+ bool castCheck(Record r)
+ {
+ if(r !is null && cast(T)r is null) return true;
+ else return false;
+ }
+}
+
+template castWarn(T: Record)
+{
+ debug(check)
+ {
+ void castWarn(Record r)
+ {
+ if(castCheck!(T)(r))
+ nifFile.warn("Could not cast " ~ r.toString() ~ " to " ~
+ (cast(TypeInfo_Class)typeid(T)).info.name);
+ }
+ }
+ else
+ void castWarn(Record r) {};
+}
+
+class NiSkinInstance : Record
+{
+ NiSkinData data;
+ NiNode root;
+ NiNode bones[];
+
+ override:
+ void read()
+ {
+ super.read();
+
+ debug(verbose) writef("Skin data ");
+ getIndex();
+
+ debug(verbose) writef("Scene root ");
+ getIndex();
+
+ debug(verbose) writef("Bone ");
+ bones = nifRegion.allocateT!(NiNode)(getIndexList());
+ }
+
+ void sortOut(Record[] l)
+ {
+ super.sortOut(l);
+ data = lookup!(NiSkinData)(l);
+ root = lookup!(NiNode)(l);
+ lookupList!(NiNode)(l,bones);
+ }
+
+ void check()
+ {
+ super.check();
+ debug(check) if(data !is null)
+ {
+ if(bones.length != data.weights.length)
+ nifFile.warn(format("NiSkinInstance has ", bones.length,
+ " bones, but NiSkinData has ", data.weights.length));
+
+ /*
+ int meshCount = root.getFirstMesh(r).data.vertices.length/3;
+
+ foreach(int i, NiSkinData.Weight[] wl; data.weights)
+ {
+ if(bones[i] is null)
+ r.warn("Bone object missing!");
+
+ if(meshCount < data.weights[i].length)
+ r.warn(format("More skin weights than vertices in bone %d (%d < %d)",
+ i, meshCount, data.weights[i].length));
+
+ foreach(ref NiSkinData.Weight w; data.weights[i])
+ if(w.vertex >= meshCount)
+ r.warn("Skin weight vertex index out of bounds");
+ }
+ */
+ }
+ }
+}
diff --git a/ogre.cfg b/ogre.cfg
new file mode 100644
index 0000000000..90dc405237
--- /dev/null
+++ b/ogre.cfg
@@ -0,0 +1,7 @@
+Render System=OpenGL Rendering Subsystem
+
+[OpenGL Rendering Subsystem]
+FSAA=0
+Full Screen=No
+RTT Preferred Mode=FBO
+Video Mode=800 x 600
diff --git a/ogre/bindings.d b/ogre/bindings.d
new file mode 100644
index 0000000000..5624695bea
--- /dev/null
+++ b/ogre/bindings.d
@@ -0,0 +1,171 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (bindings.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module ogre.bindings;
+
+import nif.misc; // for Transformation
+import ogre.ogre; // for Placement
+
+import core.resource;
+
+/*
+ * This module is the interface to OGRE from D. Since OGRE is written
+ * in C++, all the code that deals directly with the graphics engine
+ * is packaged in a bunch of C++ functions. These functions are
+ * exported from C++ through the C calling convention, and imported
+ * here.
+ *
+ * Note that the C calling convension is not in any way type
+ * safe. This is convenient, as it allows us to send pointers as one
+ * type and recieve them as another, without casting, but also
+ * dangerous since it opens for some nasty bugs.
+ */
+
+// Represents a pointer to a Node in the OGRE engine. We never use
+// these directly in D code, only pass them back to the C++ code.
+typedef void* NodePtr;
+
+// Pointer to a manual loader class in C++.
+typedef void* ManualLoader;
+
+extern(C):
+
+// Do engine configuration. Returns 0 if we should continue, 1 if
+// not.
+int cpp_configure();
+
+// Sets up the window
+void cpp_initWindow();
+
+// Set up an empty scene.
+void cpp_makeScene();
+
+// Set the ambient light and "sunlight"
+void cpp_setAmbient(float r, float g, float b,
+ float rs, float gs, float bs);
+
+// Set fog color and view distance
+void cpp_setFog(float rf, float gf, float bf,
+ float flow, float fhigh);
+
+// Create a simple sky dome
+int cpp_makeSky();
+
+// Enter main rendering loop
+void cpp_startRendering();
+
+// Cleans up after ogre
+void cpp_cleanup();
+
+// Create a manual loader for textures
+ManualLoader cpp_createTexture(
+ char* name, // Resource name
+ TextureIndex ti); // Texture index
+
+// Inserts texture from memory data. This isn't used anymore and will
+// be removed.
+void cpp_loadMemImage(
+ char* name, // Name to give the resource
+ char* type, // Image type (eg. "dds")
+ void* data, // Pointer to file data
+ uint size, // Size
+ void* texture); // Texture resource
+
+// Gets a child SceneNode from the root node, then detatches it to
+// hide it from view. Used for creating the "template" node associated
+// with a NIF mesh.
+NodePtr cpp_getDetachedNode();
+
+// Create a copy of the given scene node, with the given coordinates
+// and rotation.
+NodePtr cpp_insertNode(NodePtr base, char* name,
+ Placement *pos, float scale);
+
+// Create a (very crappy looking) plane to simulate the water level
+void cpp_createWater(float level);
+
+// Creates a scene node as a child of 'parent', then translates and
+// rotates it according to the data in 'trafo'.
+NodePtr cpp_createNode(
+ char *name, // Name to give the node
+ Transformation *trafo, // Transformation
+ NodePtr parent, // Parent node
+ int noRot); // If 1, don't rotate node
+
+// Create a light with the given diffuse color. Attach it to SceneNode
+// 'parent'.
+NodePtr cpp_attachLight(char* name, NodePtr parent,
+ float r, float g, float b,
+ float radius);
+
+// Create the specified material
+void cpp_createMaterial(char *name, // Name to give resource
+ float *ambient, // Ambient RBG value
+ float *diffuse,
+ float *specular,
+ float *emissive,// Self illumination
+ float glossiness,// Same as shininess?
+ float alpha, // Use this in all alpha values?
+ char *texture); // Texture name
+
+// Creates a mesh and gives it a bounding box. Also creates an entity
+// and attached it to the given SceneNode 'owner'.
+void cpp_createMesh(
+ char* name, // Name of the mesh
+ int numVerts, // Number of vertices
+ float* vertices, // Vertex list
+ float* normals, // Normal list
+ float* colors, // Vertex colors
+ float* uvs, // Texture coordinates
+ int numFaces, // Number of faces*3
+ short* faces, // Faces
+ float radius, // Bounding sphere
+ char* material, // Material name, if any
+
+ // Bounding box
+ float minX,float minY,float minZ,
+ float maxX,float maxY,float maxZ,
+
+ NodePtr owner // Scene node to attach to.
+ );
+
+// Save a screen shot to the given file name
+void cpp_screenshot(char *filename);
+
+// Camera controll
+void cpp_rotateCamera(float x, float y);
+void cpp_moveCamera(float x, float y, float z, float r1, float r2, float r3);
+void cpp_getCameraPos(float *x, float *y, float *z);
+void cpp_moveCameraRel(float x, float y, float z);
+
+// Do some debug action. Check the menu for today's specials!
+void cpp_debug(int i);
+
+// CALLBACKS:
+
+// Called when a texture wants to be loaded. Not used anymore.
+void d_loadTexture(TextureIndex ti, void *tex)
+{
+ assert(0);
+ resources.loadTexture(ti, tex);
+}
diff --git a/ogre/cpp_bsaarchive.cpp b/ogre/cpp_bsaarchive.cpp
new file mode 100644
index 0000000000..bb63bcdfa5
--- /dev/null
+++ b/ogre/cpp_bsaarchive.cpp
@@ -0,0 +1,137 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (cpp_bsaarchive.cpp) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+// This file inserts an archive manager for .bsa files into the ogre
+// resource loading system. It uses callbacks to D to interact with
+// the d-based bsa handling code. The D bindings are in
+// core/resource.d.
+
+// Callbacks to D code
+
+// Does the file exist in the archives?
+extern "C" int d_bsaExists(const char *filename);
+
+// Open a file. Return the pointer and size.
+extern "C" void d_bsaOpenFile(const char *filename,
+ void **retPtr, unsigned int *retSize);
+
+
+// This archive does not cover one .bsa file, instead it interacts
+// with the all the loaded bsas and treates them as one archive.
+class BSAArchive : public Archive
+{
+public:
+
+ BSAArchive(const String& name)
+ : Archive(name, "BSA") {}
+ ~BSAArchive() {}
+
+ bool isCaseSensitive(void) const { return false; }
+
+ // The archives are already loaded in D code, and they are never
+ // unloaded.
+ void load() {}
+ void unload() {}
+
+ DataStreamPtr open(const String& filename) const
+ {
+ //std::cout << "open(" << filename << ")\n";
+ void *ptr;
+ unsigned int size;
+
+ // Open the file
+ d_bsaOpenFile(filename.c_str(), &ptr, &size);
+
+ return DataStreamPtr(new MemoryDataStream(ptr, size));
+ }
+
+ // This is never called as far as I can see.
+ StringVectorPtr list(bool recursive = true, bool dirs = false)
+ {
+ std::cout << "list(" << recursive << ", " << dirs << ")\n";
+ StringVectorPtr ptr = StringVectorPtr(new StringVector());
+ return ptr;
+ }
+ // Also never called.
+ FileInfoListPtr listFileInfo(bool recursive = true, bool dirs = false)
+ {
+ std::cout << "listFileInfo(" << recursive << ", " << dirs << ")\n";
+ FileInfoListPtr ptr = FileInfoListPtr(new FileInfoList());
+ return ptr;
+ }
+
+ // After load() is called, find("*") is called once. It doesn't seem
+ // to matter that we return an empty list, exists() gets called on
+ // the correct files anyway.
+ StringVectorPtr find(const String& pattern, bool recursive = true,
+ bool dirs = false)
+ {
+ //std::cout << "find(" << pattern << ", " << recursive
+ // << ", " << dirs << ")\n";
+ StringVectorPtr ptr = StringVectorPtr(new StringVector());
+ return ptr;
+ }
+
+ // Gets called once for each of the ogre formats, *.program,
+ // *.material etc. We can ignore that.
+ FileInfoListPtr findFileInfo(const String& pattern, bool recursive = true,
+ bool dirs = false)
+ {
+ //std::cout << "findFileInfo(" << pattern << ", " << recursive
+ // << ", " << dirs << ")\n";
+
+
+ FileInfoListPtr ptr = FileInfoListPtr(new FileInfoList());
+ //std::cout << "BSAArchive::findFileInfo is not implemented!\n";
+ return ptr;
+ }
+
+ // Check if the file exists.
+ bool exists(const String& filename)
+ {
+ //std::cout << "exists(" << filename << ")\n";
+ return d_bsaExists(filename.c_str()) != 0;
+ }
+};
+
+// An archive factory for BSA archives
+class BSAArchiveFactory : public ArchiveFactory
+{
+public:
+ virtual ~BSAArchiveFactory() {}
+
+ const String& getType() const
+ {
+ static String name = "BSA";
+ return name;
+ }
+
+ Archive *createInstance( const String& name )
+ {
+ return new BSAArchive(name);
+ }
+
+ void destroyInstance( Archive* arch) { delete arch; }
+};
+
+BSAArchiveFactory mBSAFactory;
diff --git a/ogre/cpp_framelistener.cpp b/ogre/cpp_framelistener.cpp
new file mode 100644
index 0000000000..818e180fa3
--- /dev/null
+++ b/ogre/cpp_framelistener.cpp
@@ -0,0 +1,161 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (cpp_framelistener.cpp) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+
+// Callbacks to D code.
+
+// Called once each frame
+extern "C" int d_frameStarted(float time);
+
+// Handle events
+extern "C" void d_handleKey(int keycode, unsigned int text);
+extern "C" void d_handleMouseMove(const OIS::MouseState *state);
+extern "C" void d_handleMouseButton(const OIS::MouseState *state, int button);
+
+// Frame listener, passed to Ogre. The only thing we use this for is
+// to capture input and pass control to D code.
+class MorroFrameListener: public FrameListener
+{
+
+public:
+ // Start of frame
+ bool frameStarted(const FrameEvent& evt)
+ {
+ if(mWindow->isClosed())
+ return false;
+
+ // Capture keyboard and mouse events
+ mKeyboard->capture();
+ mMouse->capture();
+
+ return d_frameStarted(evt.timeSinceLastFrame);
+ }
+};
+
+// Recieves input events and sends them to the D event handler.
+class InputListener : public OIS::KeyListener, public OIS::MouseListener
+{
+public:
+ bool keyPressed( const OIS::KeyEvent &arg )
+ {
+ /*
+ std::cout << "KeyPressed {" << arg.key
+ << ", " << ((OIS::Keyboard*)(arg.device))->getAsString(arg.key)
+ << "} || Character (" << (char)arg.text << ")\n";
+ //*/
+ d_handleKey(arg.key, arg.text);
+ return true;
+ }
+
+ bool mouseMoved( const OIS::MouseEvent &arg )
+ {
+ /*
+ const OIS::MouseState& s = arg.state;
+ std::cout << "MouseMoved: Abs("
+ << s.X.abs << ", " << s.Y.abs << ", " << s.Z.abs << ") Rel("
+ << s.X.rel << ", " << s.Y.rel << ", " << s.Z.rel << ")\n";
+ */
+ d_handleMouseMove(&arg.state);
+ return true;
+ }
+
+ bool mousePressed( const OIS::MouseEvent &arg, OIS::MouseButtonID id )
+ {
+ /*
+ const OIS::MouseState& s = arg.state;
+ std::cout << "Mouse button #" << id << " pressed. Abs("
+ << s.X.abs << ", " << s.Y.abs << ", " << s.Z.abs << ") Rel("
+ << s.X.rel << ", " << s.Y.rel << ", " << s.Z.rel << ")\n";
+ */
+ d_handleMouseButton(&arg.state, id);
+ return true;
+ }
+
+ // Unused
+ bool keyReleased( const OIS::KeyEvent &arg ) { return true; }
+ bool mouseReleased( const OIS::MouseEvent &arg, OIS::MouseButtonID id ) { return true; }
+};
+
+MorroFrameListener mFrameListener;
+InputListener mInput;
+
+// Functions called from D during event handling
+
+extern "C" int cpp_isPressed(int keysym)
+{
+ return mKeyboard->isKeyDown((OIS::KeyCode)keysym);
+}
+
+// Dump screen contents to file
+extern "C" void cpp_screenshot(char* filename)
+{
+ mWindow->writeContentsToFile(filename);
+
+ //This doesn't work, I think I have to set up an overlay or
+ //something first and display the text manually.
+ //mWindow->setDebugText(String("Wrote ") + filename);
+}
+
+// Rotate camera as result of mouse movement
+extern "C" void cpp_rotateCamera(float x, float y)
+{
+ mCamera->yaw(Degree(-x));
+ mCamera->pitch(Degree(-y));
+}
+
+// Get current camera position
+extern "C" void cpp_getCameraPos(float *x, float *y, float *z)
+{
+ Vector3 pos = mCamera->getPosition();
+ *x = pos[0];
+ *y = -pos[2];
+ *z = pos[1];
+}
+
+// Move and rotate camera in place. Transforms Morrowind coordinates
+// to OGRE coordinates.
+extern "C" void cpp_moveCamera(float x, float y, float z,
+ float r1, float r2, float r3)
+{
+ mCamera->setPosition(Vector3(x,z,-y));
+
+ // Rotation is probably not correct, but for now I have no reference
+ // point. Fix it later when we teleport from one cell to another, so
+ // we have something to compare against.
+
+ // Rotate around X axis
+ Quaternion xr(Radian(-r1), Vector3::UNIT_X);
+ // Rotate around Y axis
+ Quaternion yr(Radian(r3+3.14), Vector3::UNIT_Y);
+ // Rotate around Z axis
+ Quaternion zr(Radian(-r2), Vector3::UNIT_Z);
+
+ // Rotates first around z, then y, then x
+ mCamera->setOrientation(xr*yr*zr);
+}
+
+// Move camera relative to its own axis set
+extern "C" void cpp_moveCameraRel(float x, float y, float z)
+{
+ mCamera->moveRelative(Vector3(x,y,z));
+}
diff --git a/ogre/cpp_interface.cpp b/ogre/cpp_interface.cpp
new file mode 100644
index 0000000000..bf2486d591
--- /dev/null
+++ b/ogre/cpp_interface.cpp
@@ -0,0 +1,755 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (cpp_interface.cpp) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+//-----------------------------------------------------------------------
+// E X P O R T E D F U N C T I O N S
+//-----------------------------------------------------------------------
+
+extern "C" void cpp_cleanup()
+{
+ // Kill the input systems. This will reset input options such as key
+ // repetition.
+ mInputManager->destroyInputObject(mKeyboard);
+ mInputManager->destroyInputObject(mMouse);
+ OIS::InputManager::destroyInputSystem(mInputManager);
+
+ // Kill OGRE
+ if (mRoot)
+ {
+ delete mRoot;
+ mRoot = 0;
+ }
+}
+
+extern "C" int cpp_configure()
+{
+ mRoot = new Root();
+
+ // Add the BSA archive manager before reading the config file.
+ ArchiveManager::getSingleton().addArchiveFactory( &mBSAFactory );
+
+ // Load resource paths from config file
+ ConfigFile cf;
+ cf.load("resources.cfg");
+
+ // Go through all sections & settings in the file
+ ConfigFile::SectionIterator seci = cf.getSectionIterator();
+
+ String secName, typeName, archName;
+ while (seci.hasMoreElements())
+ {
+ secName = seci.peekNextKey();
+ ConfigFile::SettingsMultiMap *settings = seci.getNext();
+ ConfigFile::SettingsMultiMap::iterator i;
+ for (i = settings->begin(); i != settings->end(); ++i)
+ {
+ typeName = i->first;
+ archName = i->second;
+ ResourceGroupManager::getSingleton().addResourceLocation(
+ archName, typeName, secName);
+ }
+ }
+
+ // Show the configuration dialog and initialise the system
+ // You can skip this and use root.restoreConfig() to load configuration
+ // settings if you were sure there are valid ones saved in ogre.cfg
+
+ //if(mRoot->showConfigDialog())
+ if(mRoot->restoreConfig())
+ return 0;
+ return 1;
+}
+
+// Initialize window. This will create and show the actual window.
+extern "C" void cpp_initWindow()
+{
+ std::cout << "cpp_initWindow()\n";
+
+ // Initialize OGRE.
+ mWindow = mRoot->initialise(true);
+
+ // Set up the input system
+
+ using namespace OIS;
+
+ size_t windowHnd;
+ mWindow->getCustomAttribute("WINDOW", &windowHnd);
+
+ std::ostringstream windowHndStr;
+ ParamList pl;
+
+ windowHndStr << windowHnd;
+ pl.insert(std::make_pair(std::string("WINDOW"), windowHndStr.str()));
+
+ // Non-exclusive mouse and keyboard input
+ /*
+#if defined OIS_WIN32_PLATFORM
+ pl.insert(std::make_pair(std::string("w32_mouse"),
+ std::string("DISCL_FOREGROUND" )));
+ pl.insert(std::make_pair(std::string("w32_mouse"),
+ std::string("DISCL_NONEXCLUSIVE")));
+ pl.insert(std::make_pair(std::string("w32_keyboard"),
+ std::string("DISCL_FOREGROUND")));
+ pl.insert(std::make_pair(std::string("w32_keyboard"),
+ std::string("DISCL_NONEXCLUSIVE")));
+#elif defined OIS_LINUX_PLATFORM
+ pl.insert(std::make_pair(std::string("x11_mouse_grab"),
+ std::string("true")));
+ pl.insert(std::make_pair(std::string("x11_mouse_hide"),
+ std::string("true")));
+ pl.insert(std::make_pair(std::string("x11_keyboard_grab"),
+ std::string("true")));
+ pl.insert(std::make_pair(std::string("XAutoRepeatOn"),
+ std::string("false")));
+#endif
+ */
+
+ mInputManager = InputManager::createInputSystem( pl );
+
+ const bool bufferedKeys = true;
+ const bool bufferedMouse = true;
+
+ // Create all devices
+ mKeyboard = static_cast(mInputManager->createInputObject
+ ( OISKeyboard, bufferedKeys ));
+ mMouse = static_cast(mInputManager->createInputObject
+ ( OISMouse, bufferedMouse ));
+
+ unsigned int width, height, depth;
+ int left, top;
+ mWindow->getMetrics(width, height, depth, left, top);
+
+ // Set mouse region
+ const MouseState &ms = mMouse->getMouseState();
+ ms.width = width;
+ ms.height = height;
+
+ // Register the input listener
+ mKeyboard -> setEventCallback( &mInput );
+ mMouse -> setEventCallback( &mInput );
+
+ std::cout << "cpp_initWindow finished\n";
+}
+
+// Make a scene, set the given ambient light
+extern "C" void cpp_makeScene()
+{
+ // Get the SceneManager, in this case a generic one
+ mSceneMgr = mRoot->createSceneManager(ST_GENERIC);
+
+ // Create the camera
+ mCamera = mSceneMgr->createCamera("PlayerCam");
+
+ mCamera->setNearClipDistance(5);
+
+ // Create one viewport, entire window
+ vp = mWindow->addViewport(mCamera);
+ // Give the backround a healthy shade of green
+ vp->setBackgroundColour(ColourValue(0,0.1,0));
+
+ // Alter the camera aspect ratio to match the viewport
+ mCamera->setAspectRatio(Real(vp->getActualWidth()) / Real(vp->getActualHeight()));
+
+ // Set default mipmap level (NB some APIs ignore this)
+ TextureManager::getSingleton().setDefaultNumMipmaps(5);
+
+ // Load resources
+ ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
+
+ // Add the frame listener
+ mRoot->addFrameListener(&mFrameListener);
+
+ // Turn the entire scene (represented by the 'root' node) -90
+ // degrees around the x axis. This makes Z go upwards, and Y go into
+ // the screen. This is the orientation that Morrowind uses, and it
+ // automagically makes everything work as it should.
+ SceneNode *rt = mSceneMgr->getRootSceneNode();
+ root = rt->createChildSceneNode();
+ root->pitch(Degree(-90));
+}
+
+extern "C" void cpp_makeSky()
+{
+ mSceneMgr->setSkyDome( true, "Examples/CloudySky", 5, 8 );
+}
+
+extern "C" Light* cpp_attachLight(char *name, SceneNode* base,
+ float r, float g, float b,
+ float radius)
+{
+ Light *l = mSceneMgr->createLight(name);
+
+ l->setDiffuseColour(r,g,b);
+
+ // Pulled these numbers out of nowhere. Looks OK-ish. I can polish
+ // it later.
+ l->setAttenuation(8*radius, 0, 0.008, 0.0);
+
+ // base might be null, sometimes lights don't have meshes
+ if(base) base->attachObject(l);
+
+ return l;
+}
+
+extern "C" void cpp_setAmbient(float r, float g, float b, // Ambient light
+ float rs, float gs, float bs) // "Sunlight"
+{
+ ColourValue c = ColourValue(r, g, b);
+ mSceneMgr->setAmbientLight(c);
+
+ // Create a "sun" that shines light downwards. It doesn't look
+ // completely right, but leave it for now.
+ Light *l = mSceneMgr->createLight("Sun");
+ l->setDiffuseColour(rs, gs, bs);
+ l->setType(Light::LT_DIRECTIONAL);
+ l->setDirection(0,-1,0);
+}
+
+extern "C" void cpp_setFog(float rf, float gf, float bf, // Fog color
+ float flow, float fhigh) // Fog distance
+{
+ ColourValue fogColor( rf, gf, bf );
+ mSceneMgr->setFog( FOG_LINEAR, fogColor, 0.0, flow, fhigh );
+
+ // Don't render what you can't see anyway
+ mCamera->setFarClipDistance(fhigh + 10);
+
+ // Leave this out for now
+ //vp->setBackgroundColour(fogColor);
+}
+
+extern "C" void cpp_startRendering()
+{
+ mRoot->startRendering();
+}
+
+// Copy a scene node and all its children
+void cloneNode(SceneNode *from, SceneNode *to, char* name)
+{
+ to->setPosition(from->getPosition());
+ to->setOrientation(from->getOrientation());
+ to->setScale(from->getScale());
+
+ SceneNode::ObjectIterator it = from->getAttachedObjectIterator();
+ while(it.hasMoreElements())
+ {
+ // We can't handle non-entities. To be honest I have no idea
+ // what dynamic_cast does or if it's correct here. I used to be
+ // a C++ person but after discovering D I dropped C++ like it
+ // was red hot iron and never looked back.
+ Entity *e = dynamic_cast (it.getNext());
+ if(e)
+ {
+ e = e->clone(String(name) + ":" + e->getName());
+ to->attachObject(e);
+ }
+ }
+
+ // Recursively clone all child nodes
+ SceneNode::ChildNodeIterator it2 = from->getChildIterator();
+ while(it2.hasMoreElements())
+ {
+ cloneNode((SceneNode*)it2.getNext(), to->createChildSceneNode(), name);
+ }
+}
+
+// Supposed to insert a copy of the node, for now it just inserts the
+// actual node.
+extern "C" SceneNode *cpp_insertNode(SceneNode *base, char* name,
+ float *pos, float scale)
+{
+ //std::cout << "cpp_insertNode(" << name << ")\n";
+ SceneNode *node = root->createChildSceneNode(name);
+
+ // Make a copy of the node
+ cloneNode(base, node, name);
+
+ // pos points to a Placement struct, which has the format
+ // float x, y, z; // position
+ // float r1, r2, r3; // rotation
+
+ node->setPosition(pos[0], pos[1], pos[2]);
+
+ // Rotate around X axis
+ Quaternion xr(Radian(-pos[3]), Vector3::UNIT_X);
+
+ // Rotate around Y axis
+ Quaternion yr(Radian(-pos[4]), Vector3::UNIT_Y);
+
+ // Rotate around Z axis
+ Quaternion zr(Radian(-pos[5]), Vector3::UNIT_Z);
+
+ // Rotates first around z, then y, then x
+ node->setOrientation(xr*yr*zr);
+
+ node->setScale(scale, scale, scale);
+
+ return node;
+}
+
+// Create the water plane. It doesn't really resemble "water" yet
+// though.
+extern "C" void cpp_createWater(float level)
+{
+ // Create a plane aligned with the xy-plane.
+ MeshManager::getSingleton().createPlane("water",
+ ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
+ Plane(Vector3::UNIT_Z, level),
+ 150000,150000
+ //,20,20,true,1,5,5,Vector3::UNIT_Z
+ );
+ Entity *ent = mSceneMgr->createEntity( "WaterEntity", "water" );
+ root->createChildSceneNode()->attachObject(ent);
+ ent->setCastShadows(false);
+}
+
+// Manual loader for meshes. Reloading individual meshes is too
+// difficult, and not worth the trouble. Later I should make one
+// loader for each NIF file, and whenever it is invoked it should
+// somehow reload the entire file. How this is to be done when some of
+// the meshes might be loaded and in use already, I have no
+// idea. Let's just ignore it for now.
+
+class MeshLoader : public ManualResourceLoader
+{
+public:
+
+ void loadResource(Resource *resource)
+ {
+ }
+} dummyLoader;
+
+ // TODO/FIXME/DEBUG (MURDER/DEATH/KILL)
+String LASTNAME;
+
+// Load the contents of a mesh
+extern "C" void cpp_createMesh(
+ char* name, // Name of the mesh
+ int numVerts, // Number of vertices
+ float* vertices, // Vertex list
+ float* normals, // Normal list
+ float* colors, // Vertex colors
+ float* uvs, // Texture coordinates
+ int numFaces, // Number of faces*3
+ unsigned short* faces, // Faces
+ float radius, // Bounding sphere
+ char* material, // Material
+ // Bounding box
+ float minX,float minY,float minZ,
+ float maxX,float maxY,float maxZ,
+ SceneNode *owner
+ )
+{
+ //std::cerr << "Creating mesh " << name << "\n";
+
+ MeshPtr msh = MeshManager::getSingleton().createManual(name, "Meshes",
+ &dummyLoader);
+
+ Entity *e = mSceneMgr->createEntity(name, name);
+
+ owner->attachObject(e);
+ //msh->setSkeletonName(name);
+
+ // Create vertex data structure
+ msh->sharedVertexData = new VertexData();
+ msh->sharedVertexData->vertexCount = numVerts;
+
+ /// Create declaration (memory format) of vertex data
+ VertexDeclaration* decl = msh->sharedVertexData->vertexDeclaration;
+
+ int nextBuf = 0;
+ // 1st buffer
+ decl->addElement(nextBuf, 0, VET_FLOAT3, VES_POSITION);
+
+ /// Allocate vertex buffer of the requested number of vertices (vertexCount)
+ /// and bytes per vertex (offset)
+ HardwareVertexBufferSharedPtr vbuf =
+ HardwareBufferManager::getSingleton().createVertexBuffer(
+ VertexElement::getTypeSize(VET_FLOAT3),
+ numVerts, HardwareBuffer::HBU_STATIC_WRITE_ONLY);
+
+ /// Upload the vertex data to the card
+ vbuf->writeData(0, vbuf->getSizeInBytes(), vertices, true);
+
+ /// Set vertex buffer binding so buffer 0 is bound to our vertex buffer
+ VertexBufferBinding* bind = msh->sharedVertexData->vertexBufferBinding;
+ bind->setBinding(nextBuf++, vbuf);
+
+ // The lists are read in the same order that they appear in NIF
+ // files, and likely in memory. Sequential reads might possibly
+ // avert an occational cache miss.
+
+ // normals
+ if(normals)
+ {
+ //std::cerr << "+ Adding normals\n";
+ decl->addElement(nextBuf, 0, VET_FLOAT3, VES_NORMAL);
+ vbuf = HardwareBufferManager::getSingleton().createVertexBuffer(
+ VertexElement::getTypeSize(VET_FLOAT3),
+ numVerts, HardwareBuffer::HBU_STATIC_WRITE_ONLY);
+
+ vbuf->writeData(0, vbuf->getSizeInBytes(), normals, true);
+
+ bind->setBinding(nextBuf++, vbuf);
+ }
+
+ // vertex colors
+ if(colors)
+ {
+ //std::cerr << "+ Adding vertex colors\n";
+ // Use render system to convert colour value since colour packing varies
+ RenderSystem* rs = Root::getSingleton().getRenderSystem();
+ RGBA colorsRGB[numVerts];
+ RGBA *pColour = colorsRGB;
+ for(int i=0; iconvertColourValue(ColourValue(colors[0],colors[1],colors[2], colors[3]),
+ pColour++);
+ colors += 4;
+ }
+
+ decl->addElement(nextBuf, 0, VET_COLOUR, VES_DIFFUSE);
+ /// Allocate vertex buffer of the requested number of vertices (vertexCount)
+ /// and bytes per vertex (offset)
+ vbuf = HardwareBufferManager::getSingleton().createVertexBuffer(
+ VertexElement::getTypeSize(VET_COLOUR),
+ numVerts, HardwareBuffer::HBU_STATIC_WRITE_ONLY);
+ /// Upload the vertex data to the card
+ vbuf->writeData(0, vbuf->getSizeInBytes(), colorsRGB, true);
+
+ /// Set vertex buffer binding so buffer 1 is bound to our colour buffer
+ bind->setBinding(nextBuf++, vbuf);
+ }
+
+ if(uvs)
+ {
+ //std::cerr << "+ Adding texture coordinates\n";
+ decl->addElement(nextBuf, 0, VET_FLOAT2, VES_TEXTURE_COORDINATES);
+ vbuf = HardwareBufferManager::getSingleton().createVertexBuffer(
+ VertexElement::getTypeSize(VET_FLOAT2),
+ numVerts, HardwareBuffer::HBU_STATIC_WRITE_ONLY);
+
+ vbuf->writeData(0, vbuf->getSizeInBytes(), uvs, true);
+
+ bind->setBinding(nextBuf++, vbuf);
+ }
+
+ // Create the submesh that holds triangle data
+ SubMesh* sub = msh->createSubMesh(name);
+ sub->useSharedVertices = true;
+
+ if(numFaces)
+ {
+ //std::cerr << "+ Adding faces\n";
+ /// Allocate index buffer of the requested number of faces
+ HardwareIndexBufferSharedPtr ibuf = HardwareBufferManager::getSingleton().
+ createIndexBuffer(
+ HardwareIndexBuffer::IT_16BIT,
+ numFaces,
+ HardwareBuffer::HBU_STATIC_WRITE_ONLY);
+
+ /// Upload the index data to the card
+ ibuf->writeData(0, ibuf->getSizeInBytes(), faces, true);
+
+ /// Set parameters of the submesh
+ sub->indexData->indexBuffer = ibuf;
+ sub->indexData->indexCount = numFaces;
+ sub->indexData->indexStart = 0;
+ }
+
+ // Create a material with the given texture, if any.
+
+ // If this mesh has a material, attach it.
+ if(material) sub->setMaterialName(name);
+
+ /*
+ // Assign this submesh to the given bone
+ VertexBoneAssignment v;
+ v.boneIndex = ((Bone*)bone)->getHandle();
+ v.weight = 1.0;
+
+ std::cerr << "+ Assigning bone index " << v.boneIndex << "\n";
+
+ for(int i=0; i < numVerts; i++)
+ {
+ v.vertexIndex = i;
+ sub->addBoneAssignment(v);
+ }
+ */
+ /// Set bounding information (for culling)
+ msh->_setBounds(AxisAlignedBox(minX,minY,minZ,maxX,maxY,maxZ));
+
+ //std::cerr << "+ Radius: " << radius << "\n";
+ msh->_setBoundingSphereRadius(radius);
+}
+
+extern "C" void cpp_createMaterial(char *name, // Name to give
+ // resource
+
+ float *ambient, // Ambient RBG
+ // value
+ float *diffuse,
+ float *specular,
+ float *emissive, // Self
+ // illumination
+
+ float glossiness,// Same as
+ // shininess?
+
+ float alpha, // Use this in all
+ // alpha values?
+
+ char* texture) // Texture
+{
+ MaterialPtr material = MaterialManager::getSingleton().create(
+ name,
+ ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
+
+ // This assigns the texture to this material. If the texture
+ // name is a file name, and this file exists (in a resource
+ // directory), it will automatically be loaded when needed. If
+ // not, we should already have inserted a manual loader for the texture.
+ if(texture)
+ material->getTechnique(0)->getPass(0)->createTextureUnitState(texture);
+
+ // Set bells and whistles
+ material->setAmbient(ambient[0], ambient[1], ambient[2]);
+ material->setDiffuse(diffuse[0], diffuse[1], diffuse[2], alpha);
+ material->setSpecular(specular[0], specular[1], specular[2], alpha);
+ material->setSelfIllumination(emissive[0], emissive[1], emissive[2]);
+ material->setShininess(glossiness);
+
+ // Enable transparancy? This is just guesswork. Perhaps we
+ // should use NiAlphaProperty or something instead. This looks
+ // like crap so it's not enabled right now.
+
+ //material->setSceneBlending(SBT_TRANSPARENT_ALPHA);
+
+ // Temporary, just to store the name of one valid material.
+ LASTNAME = material->getName();
+}
+
+extern "C" SceneNode *cpp_getDetachedNode()
+{
+ SceneNode *node = root->createChildSceneNode();
+ root->removeChild(node);
+ return node;
+}
+
+extern "C" SceneNode* cpp_createNode(
+ char *name,
+ float *trafo,
+ SceneNode *parent,
+ int noRot)
+{
+ //std::cout << "cpp_createNode(" << name << ")";
+ SceneNode *node = parent->createChildSceneNode(name);
+ //std::cout << " ... done\n";
+
+ // First is the translation vector
+
+ // TODO should be "if(!noRot)" only for exterior cells!? Yay for
+ // consistency. Apparently, the displacement of the base node in NIF
+ // files must be ignored for meshes in interior cells, but not for
+ // exterior cells. Or at least that's my hypothesis, and it seems
+ // work. There might be some other NIF trickery going on though, you
+ // never know when you're reverse engineering someone else's file
+ // format. We will handle this later.
+ if(!noRot)
+ node->setPosition(trafo[0], trafo[1], trafo[2]);
+
+ // Then a 3x3 rotation matrix.
+ if(!noRot)
+ node->setOrientation(Quaternion(Matrix3(trafo[3], trafo[4], trafo[5],
+ trafo[6], trafo[7], trafo[8],
+ trafo[9], trafo[10], trafo[11]
+ )));
+
+ // Scale is at the end
+ node->setScale(trafo[12],trafo[12],trafo[12]);
+
+ return node;
+}
+
+// Callback to make D insert a texture.
+extern "C" void d_loadTexture(void* tl, Resource *r);
+
+// Manual loader for textures
+class TextureLoader : public ManualResourceLoader
+{
+ void *textureIndex;
+
+public:
+
+ TextureLoader(char* name, void* ti) : textureIndex(ti)
+ { createTexture(name); }
+ ~TextureLoader() {}
+
+ void createTexture(char *name)
+ {
+ // Completely disable these textures for now.
+
+ return;
+ // Create a manually loaded texture. Dimensions etc are dummy
+ // values that will be overwritten later.
+ TexturePtr texture = TextureManager::getSingleton().createManual
+ (
+ name, // Name
+ "General", // Group
+ TEX_TYPE_2D, // Texture type
+ 1, 1, // Dimensions
+ 0, // Mipmaps
+ //PF_DXT1, // Pixel format
+ PF_UNKNOWN, // Pixel format
+ TU_DEFAULT,
+ this);
+
+ // Now unload this dummy image. This forces the image to be
+ // reloaded when needed.
+ texture->unload();
+ }
+
+ void loadResource(Resource *resource)
+ {
+ // This makes the D code insert the image.
+ d_loadTexture(textureIndex, resource);
+ }
+};
+
+// Create a new manually loaded texture, and return the manual loader
+extern "C" TextureLoader *cpp_createTexture(
+ char *name,
+ void *textureIndex)
+{
+ return new TextureLoader(name, textureIndex);
+}
+
+int hasLoad = 0;
+
+extern "C" void cpp_loadMemImage(
+ char* name, // Name to give the resource
+ char* type, // Image type (eg. "dds")
+ void* data, // Pointer to file data
+ unsigned int size, // Size
+ Texture* txt)// Texture
+{
+
+ DataStreamPtr m(new MemoryDataStream(name, data, size));
+ Image i;
+ i.load(m, type);
+
+ txt->setHeight(i.getHeight());
+ txt->setWidth(i.getWidth());
+ std::cout << "New width: " << txt->getWidth()
+ << " source: " << txt->getSrcWidth() << "\n";
+
+ // _loadImages requires a list of image pointers
+ ConstImagePtrList lst(1, &i);
+ txt->_loadImages(lst);
+
+ /*
+ if(hasLoad != 15)
+ {
+ hasLoad = 15;
+
+ std::cout << "cpp_loadMemImage: name=" << name << " type=" << type
+ << " data size=" << size << "\n";
+ std::cout << "Data stream size=" << m->size() << "\n";
+ std::cout << "Image: buffer size=" << i.getSize() << " dimensions="
+ << i.getWidth() << "x" << i.getHeight() << " mipmaps="
+ << i.getNumMipmaps() << "\n";
+ std::cout << "Image format: " << i.getFormat() << "\n";
+
+ //const char* filename = "output.png";
+ //i.save(filename);
+ //std::cout << "Saved file to " << filename << "\n";
+ }
+ */
+}
+
+/* Code currently not in use
+
+// We need this later for animated meshes. Boy will this be a mess.
+extern "C" void* cpp_setupSkeleton(char* name)
+{
+ SkeletonPtr skel = SkeletonManager::getSingleton().create(
+ name, "Closet", true);
+
+ skel->load();
+
+ // Create all bones at the origin and unrotated. This is necessary
+ // since our submeshes each have their own model space. We must
+ // move the bones after creating an entity, then copy this entity.
+ return (void*)skel->createBone();
+}
+
+// Use this later when loading textures directly from NIF files
+extern "C" void cpp_createTexture(char* name, uint width, uint height)
+{
+ TexturePtr texture = TextureManager::getSingleton().createManual(
+ name, // name
+ "ManualTexture", // group
+ TEX_TYPE_2D, // type
+ width, hight, // width & height
+ 0, // number of mipmaps
+ PF_BYTE_BGRA, // pixel format
+ TU_DEFAULT); // usage; should be TU_DYNAMIC_WRITE_ONLY_DISCARDABLE for
+ // textures updated very often (e.g. each frame)
+
+ // Get the pixel buffer
+ HardwarePixelBufferSharedPtr pixelBuffer = texture->getBuffer();
+
+ // Lock the pixel buffer and get a pixel box
+ pixelBuffer->lock(HardwareBuffer::HBL_NORMAL); // for best performance use HBL_DISCARD!
+ const PixelBox& pixelBox = pixelBuffer->getCurrentLock();
+
+ uint8* pDest = static_cast(pixelBox.data);
+
+ // Fill in some pixel data. This will give a semi-transparent blue,
+ // but this is of course dependent on the chosen pixel format.
+ for (size_t j = 0; j < 256; j++)
+ for(size_t i = 0; i < 256; i++)
+ {
+ *pDest++ = 255; // B
+ *pDest++ = 0; // G
+ *pDest++ = 0; // R
+ *pDest++ = 127; // A
+ }
+
+ // Unlock the pixel buffer
+ pixelBuffer->unlock();
+
+ // Create a material using the texture
+ MaterialPtr material = MaterialManager::getSingleton().create(
+ "DynamicTextureMaterial", // name
+ ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
+
+ material->getTechnique(0)->getPass(0)->createTextureUnitState("DynamicTexture");
+ material->getTechnique(0)->getPass(0)->setSceneBlending(SBT_TRANSPARENT_ALPHA);
+}
+
+extern "C" void *cpp_insertBone(char* name, void* rootBone, int index)
+{
+ return (void*) ( ((Bone*)rootBone)->createChild(index) );
+}
+*/
diff --git a/ogre/cpp_ogre.cpp b/ogre/cpp_ogre.cpp
new file mode 100644
index 0000000000..ac786d7860
--- /dev/null
+++ b/ogre/cpp_ogre.cpp
@@ -0,0 +1,58 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (cpp_ogre.cpp) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+using namespace Ogre;
+
+RenderWindow* mWindow;
+Root *mRoot;
+SceneManager *mSceneMgr;
+Camera *mCamera;
+Viewport *vp;
+
+OIS::InputManager *mInputManager;
+OIS::Mouse *mMouse;
+OIS::Keyboard *mKeyboard;
+
+// Root node for all objects added to the scene. This is rotated so
+// that the OGRE coordinate system matches that used internally in
+// Morrowind.
+SceneNode *root;
+
+// Include the other parts of the code, and make one big object file.
+#include "cpp_framelistener.cpp"
+#include "cpp_bsaarchive.cpp"
+#include "cpp_interface.cpp"
+#include "cpp_overlay.cpp"
diff --git a/ogre/cpp_overlay.cpp b/ogre/cpp_overlay.cpp
new file mode 100644
index 0000000000..4db52df952
--- /dev/null
+++ b/ogre/cpp_overlay.cpp
@@ -0,0 +1,79 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (cpp_overlay.cpp) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+OverlayManager *om;
+Overlay *owl;
+PanelOverlayElement *cont;
+
+int visible;
+
+void crap(char *p, ColourValue v)
+{
+ std::cerr << p << ": " << v.getAsRGBA() << "\n";
+}
+
+extern "C" void cpp_debug(int i)
+{
+ std::cerr << "Running cpp_debug(" << i << ")\n";
+
+ if(om)
+ {
+ if(visible)
+ {
+ visible = 0;
+ owl->hide();
+ }
+ else
+ {
+ visible = 1;
+ owl->show();
+ }
+
+ return;
+ }
+
+ om = OverlayManager::getSingletonPtr();
+
+ owl = om->create("HUD");
+
+ OverlayElement *e = om->createOverlayElementFromFactory("Panel", "Hopp");
+ cont = (PanelOverlayElement*)e;
+
+ cont->setDimensions(0.3, 0.5);
+ cont->setPosition(0.1, 0.2);
+ cont->setMaterialName(LASTNAME);
+
+ /*
+ * I suggest creating a custom material based on a "live" texture,
+ * where we can specify alpha transparency and draw everything
+ * ourselves. (Reinvent the wheel at every chance! :)
+ */
+ MaterialPtr material = MaterialManager::getSingleton().getByName(LASTNAME);
+ std::cerr << "Material " << LASTNAME << "\n";
+ Pass* pass = material->getTechnique(0)->getPass(0);
+ pass->setSceneBlending(SBT_TRANSPARENT_ALPHA);
+
+ owl->add2D(cont);
+ owl->show();
+ visible = 1;
+}
diff --git a/ogre/meshloader.d b/ogre/meshloader.d
new file mode 100644
index 0000000000..1a42989913
--- /dev/null
+++ b/ogre/meshloader.d
@@ -0,0 +1,309 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (meshloader.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module ogre.meshloader;
+
+import std.stdio;
+import std.stream;
+
+import nif.nif;
+import nif.record;
+
+import core.resource;
+import ogre.bindings;
+
+import util.uniquename;
+
+/*
+ There are some problems that will have to be looked into later:
+
+ - Some meshes crash Ogre when shadows are turned on. (Not tested in
+ newer versions of Ogre). Shadows are completely disabled for now.
+ - There are obviously some boundry problems, some times the mesh
+ disappears even though some part of it is inside the screen. This
+ is especially a problem with animated meshes, since the animation
+ might step outside the original bounding box.
+ */
+
+MeshLoader meshLoader;
+
+struct MeshLoader
+{
+ // Not sure how to handle the bounding box, just ignore it for now.
+
+ char[] baseName; // NIF file name. Used in scene node names etc. so
+ // that we can easier identify where they came
+ // from.
+
+ // Load a NIF mesh. Assumes nifMesh is already opened. This creates
+ // a "template" scene node containing this mesh, and removes it from
+ // the main scene. This node can later be "cloned" so that multiple
+ // instances of the object can be inserted into the world without
+ // inserting the mesh more than once.
+ NodePtr loadMesh(char[] name)
+ {
+ baseName = name;
+
+ // Check if the first record is a node
+ Node n = cast(Node) nifMesh.records[0];
+
+ if(n is null)
+ {
+ // TODO: Figure out what to do in this case, we should
+ // probably throw.
+ writefln("NIF '%s' IS NOT A MESH", name);
+ return null;
+ }
+
+ // Get a fresh SceneNode and detatch it from the root. We use this
+ // as the base for our mesh.
+ NodePtr base = cpp_getDetachedNode();
+
+ // Recursively insert nodes (don't rotate the first node)
+ insertNode(n, base, true);
+
+ return base;
+ }
+
+ private:
+
+ void insertNode(Node data, NodePtr parent, bool noRot = false)
+ {
+ // Skip hidden nodes for now.
+ if(data.flags & 0x01) return;
+
+ // Create a scene node, move and rotate it into place. The name
+ // must be unique, however we might have to recognize some special
+ // names later, in order to attach arms and legs on NPCs
+ // etc. Always ignore transformation of the first node? This is a
+ // problem, I don't know when to do this and when not to. Neither
+ // is always right. Update: It looks like we should always set
+ // noRot to false in exterior cells.
+ NodePtr node = cpp_createNode(UniqueName(data.name).ptr, &data.trafo,
+ parent, cast(int)noRot);
+
+ // Handle any general properties here
+
+ // Call functions that do node-specific things, like handleNiNode
+ // or handleNiTriShape.
+ {
+ NiNode n = cast(NiNode)data;
+ if(n !is null)
+ // Handle the NiNode, and any children it might have
+ handleNiNode(n, node);
+ }
+
+ {
+ NiTriShape n = cast(NiTriShape)data;
+ if(n !is null)
+ // Trishape, with a mesh
+ handleNiTriShape(n, node);
+ }
+ }
+
+ void handleNiNode(NiNode data, NodePtr node)
+ {
+ // Handle any effects here
+
+ // In the cases where meshes have skeletal animations, we must
+ // insert the children as bones in a skeleton instead, like we
+ // originally did for all nodes. Update: A much better way is to
+ // first insert the nodes normally, and then create the
+ // skeleton. The nodes can then be moved one by one over to the
+ // appropriate bones.
+
+ // Loop through children
+ foreach(Node n; data.children)
+ insertNode(n, node);
+ }
+
+ void handleNiTriShape(NiTriShape shape, NodePtr node)
+ {
+ char[] texture;
+ char[] material;
+ char[] newName = UniqueName(baseName);
+ NiMaterialProperty mp;
+
+ // Scan the property list for textures
+ foreach(Property p; shape.properties)
+ {
+ // NiTexturingProperty block
+ {
+ NiTexturingProperty t = cast(NiTexturingProperty) p;
+ if(t !is null && t.textures[0].inUse)
+ {
+ // Ignore all other options for now
+ NiSourceTexture st = t.textures[0].texture;
+ if(st.external)
+ {
+ // Find the resource for this texture
+ TextureIndex ti = resources.lookupTexture(st.filename);
+ // Insert a manual loader into OGRE
+ // ti.load();
+
+ // Get the resource name. We use getNewName to get
+ // the real texture name, not the lookup
+ // name. NewName has been converted to .dds if
+ // necessary, to match the file name in the bsa
+ // archives.
+ texture = ti.getNewName();
+ }
+ else
+ {
+ // Internal textures
+ texture = "BLAH";
+ writefln("Internal texture, cannot read this yet.");
+ writefln("Final resource name: '%s'", texture);
+ }
+ continue;
+ }
+ }
+
+ // NiMaterialProperty block
+ {
+ NiMaterialProperty tmp = cast(NiMaterialProperty) p;
+ if(tmp !is null)
+ {
+ if(mp !is null) writefln("WARNING: More than one material!");
+ mp = tmp;
+ material = newName;
+ continue;
+ }
+ //writefln("Unknown property found: ", p);
+ }
+ }
+ //if(!texture.length) writefln("No texture found");
+
+ // Get a pointer to the texture name
+ char* texturePtr;
+ if(texture.length) texturePtr = toStringz(texture);
+ else texturePtr = null;
+
+ // Create the material
+ if(material.length)
+ cpp_createMaterial(material.ptr, mp.ambient.array.ptr, mp.diffuse.array.ptr,
+ mp.specular.array.ptr, mp.emissive.array.ptr,
+ mp.glossiness, mp.alpha, texturePtr);
+ else if(texturePtr)
+ {
+ // Texture, but no material. Make a default one.
+ writefln("WARNING: Making default material for %s", texture);
+ float[3] zero;
+ float[3] one;
+ zero[] = 0.0;
+ one[] = 1.0;
+
+ cpp_createMaterial(newName.ptr, one.ptr, one.ptr, zero.ptr, zero.ptr, 0.0, 1.0,
+ texturePtr);
+ }
+
+ with(shape.data)
+ {
+ //writefln("Number of vertices: ", vertices.length);
+
+ float *normalsPtr;
+ float *colorsPtr;
+ float *uvsPtr;
+ short *facesPtr;
+
+ // Point pointers into the correct arrays, if they are present. If
+ // not, the pointers retain their values of null.
+ if(normals.length) normalsPtr = normals.ptr;
+ if(colors.length) colorsPtr = colors.ptr;
+ if(uvlist.length) uvsPtr = uvlist.ptr;
+ if(triangles.length) facesPtr = triangles.ptr;
+
+ float
+ minX = float.infinity,
+ minY = float.infinity,
+ minZ = float.infinity,
+ maxX = -float.infinity,
+ maxY = -float.infinity,
+ maxZ = -float.infinity;
+
+ // Calculate the bounding box. TODO: This is really a hack.
+ for( int i; i < vertices.length; i+=3 )
+ {
+ if( vertices[i] < minX ) minX = vertices[i];
+ if( vertices[i+1] < minY ) minY = vertices[i+1];
+ if( vertices[i+2] < minZ) minZ = vertices[i+2];
+
+ if( vertices[i] > maxX) maxX = vertices[i];
+ if( vertices[i+1] > maxY) maxY = vertices[i+1];
+ if( vertices[i+2] > maxZ) maxZ = vertices[i+2];
+ }
+
+ cpp_createMesh(newName.ptr, vertices.length, vertices.ptr,
+ normalsPtr, colorsPtr, uvsPtr, triangles.length, facesPtr,
+ radius, material.ptr, minX, minY, minZ, maxX, maxY, maxZ,
+ node);
+ }
+ }
+}
+/*
+ // Create a skeleton and get the root bone (index 0)
+ BonePtr bone = cpp_setupSkeleton(name);
+
+ // Reset the bone index. The next bone to be created has index 1.
+ boneIndex = 1;
+ // Create a mesh and assign the skeleton to it
+ MeshPtr mesh = cpp_setupMesh(name);
+
+ // Loop through the nodes, creating submeshes, materials and
+ // skeleton bones in the process.
+ handleNode(node, bone, mesh);
+
+ // Create the "template" entity
+ EntityPtr entity = cpp_createEntity(name);
+
+ // Loop through once again, this time to set the right
+ // transformations on the entity's SkeletonInstance. The order of
+ // children will be the same, allowing us to reference bones using
+ // their boneIndex.
+ int lastBone = boneIndex;
+ boneIndex = 1;
+ transformBones(node, entity);
+ if(lastBone != boneIndex) writefln("WARNING: Bone number doesn't match");
+
+ if(!hasBBox)
+ cpp_setMeshBoundingBox(mesh, minX, minY, minZ, maxX, maxY, maxZ);
+
+ return entity;
+}
+void handleNode(Node node, BonePtr root, MeshPtr mesh)
+{
+ // Insert a new bone for this node
+ BonePtr bone = cpp_insertBone(node.name, root, boneIndex++);
+
+}
+
+void transformBones(Node node, EntityPtr entity)
+{
+ cpp_transformBone(entity, &node.trafo, boneIndex++);
+
+ NiNode n = cast(NiNode)node;
+ if(n !is null)
+ foreach(Node nd; n.children)
+ transformBones(nd, entity);
+}
+*/
diff --git a/ogre/ogre.d b/ogre/ogre.d
new file mode 100644
index 0000000000..7685d75a4f
--- /dev/null
+++ b/ogre/ogre.d
@@ -0,0 +1,133 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (ogre.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module ogre.ogre;
+
+import core.resource;
+
+import ogre.bindings;
+import util.uniquename;
+import std.stdio;
+
+import esm.defs;
+
+class OgreException : Exception
+{
+ this(char[] msg) {super("OgreException: " ~ msg);}
+
+ static void opCall(char[] msg)
+ {
+ throw new OgreException(msg);
+ }
+}
+
+// Place a mesh in the 3D scene graph, at the given
+// location/scale. Returns a node pointer to the inserted object.
+NodePtr placeObject(MeshIndex mesh, Placement *pos, float scale)
+{
+ // Get a scene node for this object. mesh.getNode() will either load
+ // it from file or BSA archive, or give us a handle if it is already
+ // loaded.
+
+ // This must be called BEFORE UniqueName below, because it might
+ // possibly use UniqueName itself and overwrite the data
+ // there. (That was a fun bug to track down...)
+ NodePtr node = mesh.getNode();
+
+ // Let us insert a copy
+ char[] name = UniqueName(mesh.getName);
+ return cpp_insertNode(node, name.ptr, pos, scale);
+}
+
+NodePtr attachLight(NodePtr parent, Color c, float radius)
+{
+ return cpp_attachLight(UniqueName("_light").ptr, parent,
+ c.red/255.0, c.green/255.0, c.blue/255.0,
+ radius);
+}
+
+// If 'true' then we must call cpp_cleanup() on exit.
+bool ogreSetup = false;
+
+// Make sure we clean up
+static ~this()
+{
+ cleanupOgre();
+}
+
+// Loads ogre configurations, creats the root, etc.
+void setupOgre()
+{
+ // Later we will send some config info from core.config along with
+ // this function
+ if(cpp_configure()) OgreException("Configuration abort");
+
+ cpp_initWindow();
+
+ // We set up the scene manager in a separate function, since we
+ // might have to do that for every new cell later on, and handle
+ // exterior cells differently, etc.
+ cpp_makeScene();
+
+ ogreSetup = true;
+}
+
+void setAmbient(Color amb, Color sun, Color fog, float density)
+{
+ cpp_setAmbient(amb.red/255.0, amb.green/255.0, amb.blue/255.0,
+ sun.red/255.0, sun.green/255.0, sun.blue/255.0);
+
+ // Calculate fog distance
+ // TODO: Mesh with absolute view distance later
+ float fhigh = 4500 + 9000*(1-density);
+ float flow = 200 + 2000*(1-density);
+
+ cpp_setFog(fog.red/255.0, fog.green/255.0, fog.blue/255.0, 200, fhigh);
+}
+
+// Jump into the OGRE rendering loop. Everything should be loaded and
+// done before calling this.
+void startRendering()
+{
+ // Kick OGRE into gear
+ cpp_startRendering();
+}
+
+// Cleans up after OGRE. Resets things like screen resolution and
+// mouse control.
+void cleanupOgre()
+{
+ if(ogreSetup)
+ cpp_cleanup();
+
+ ogreSetup = false;
+}
+
+// Gives the placement of an item in the scene (position and
+// orientation). It must have this exact structure since we also use
+// it when reading ES files.
+align(1) struct Placement
+{
+ float[3] position;
+ float[3] rotation;
+}
diff --git a/plugins.cfg b/plugins.cfg
new file mode 100644
index 0000000000..31deb6ef6b
--- /dev/null
+++ b/plugins.cfg
@@ -0,0 +1,13 @@
+# Defines plugins to load
+
+# Define plugin folder
+PluginFolder=/usr/lib/OGRE
+
+# Define plugins
+Plugin=RenderSystem_GL
+Plugin=Plugin_ParticleFX
+Plugin=Plugin_BSPSceneManager
+Plugin=Plugin_OctreeSceneManager
+# Plugin=Plugin_CgProgramManager
+
+
diff --git a/resources.cfg b/resources.cfg
new file mode 100644
index 0000000000..86843e6572
--- /dev/null
+++ b/resources.cfg
@@ -0,0 +1,23 @@
+# Resource locations to be added to the 'boostrap' path
+# This also contains the minimum you need to use the Ogre example framework
+[Bootstrap]
+#Zip=ogre/media/packs/OgreCore.zip
+
+# Resource locations to be added to the default path
+[General]
+#FileSystem=media
+#FileSystem=ogre/media/fonts
+#FileSystem=ogre/media/materials/programs
+#FileSystem=ogre/media/materials/scripts
+#FileSystem=ogre/media/materials/textures
+#FileSystem=ogre/media/models
+#FileSystem=ogre/media/overlays
+#FileSystem=ogre/media/particle
+#FileSystem=ogre/media/gui
+#Zip=ogre/media/packs/cubemap.zip
+#Zip=ogre/media/packs/cubemapsJS.zip
+#Zip=ogre/media/packs/dragon.zip
+#Zip=ogre/media/packs/fresneldemo.zip
+#Zip=ogre/media/packs/ogretestmap.zip
+#Zip=ogre/media/packs/skybox.zip
+BSA=internal
diff --git a/scene/celldata.d b/scene/celldata.d
new file mode 100644
index 0000000000..c095cec8ac
--- /dev/null
+++ b/scene/celldata.d
@@ -0,0 +1,597 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (celldata.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module scene.celldata;
+
+import std.stdio;
+
+public import esm.esmmain;
+
+import core.memory;
+
+import util.reglist;
+
+import ogre.ogre;
+import ogre.bindings;
+
+import sound.audio;
+
+import scene.player;
+
+// Base properties common to all live objects. Currently extremely
+// sketchy.
+struct LiveObjectBase
+{
+ // Should this stuff be in here?
+ bool disabled; // Disabled in game
+ bool deleted; // Deleted relative to plugin file
+
+ // Used for objects created in-game, like custom potions or
+ // enchanted items. These can be completely deleted. It is also used
+ // for creatures created from leveled lists.
+ bool transient;
+
+ // Is this a door that teleports to another cell?
+ bool teleport;
+
+ // Scale, 1.0 is normal.
+ float scale;
+
+ // Owner of an object / activator
+ char[] owner;
+
+ // A global variable? Don't know what it's used for.
+ char[] global;
+
+ // Reference to a soul trapped creature?
+ char[] soul;
+
+ // Faction owner? Rank?
+ char[] cnam;
+ int indx;
+
+ // Magic value / health / uses of an item?
+ float xchg;
+
+ // ?? See comment below
+ int intv, nam9;
+
+ // Destination for a door
+ Placement destPos;
+ char[] destCell;
+
+ // Lock level?
+ int fltv;
+
+ // For locked doors and containers
+ char[] key, trap;
+
+ // Position in 3D world
+ Placement pos;
+
+ // ??
+ byte unam;
+
+ // Don't even get me started on script-related issues
+}
+
+// Generic version of a "live" object
+struct GenLive(T)
+{
+ // This HAS to be a pointer, since we load the LOB after copying the
+ // LiveWhatever into the linked list.
+ LiveObjectBase *base;
+ T *m;
+}
+
+alias GenLive!(Static) LiveStatic;
+alias GenLive!(NPC) LiveNPC;
+alias GenLive!(Activator) LiveActivator;
+alias GenLive!(Potion) LivePotion;
+alias GenLive!(Apparatus) LiveApparatus;
+alias GenLive!(Ingredient) LiveIngredient;
+alias GenLive!(Armor) LiveArmor;
+alias GenLive!(Weapon) LiveWeapon;
+alias GenLive!(Book) LiveBook;
+alias GenLive!(Clothing) LiveClothing;
+alias GenLive!(Tool) LiveTool;
+alias GenLive!(Creature) LiveCreature;
+alias GenLive!(Door) LiveDoor;
+alias GenLive!(Misc) LiveMisc;
+alias GenLive!(Container) LiveContainer;
+
+struct LiveLight
+{
+ LiveObjectBase *base;
+ Light *m;
+
+ NodePtr lightNode;
+ SoundInstance *loopSound;
+
+ // Lifetime left, in seconds?
+ float time;
+}
+
+class CellData
+{
+ private:
+ RegionManager reg;
+
+ public:
+
+ InteriorCell *inCell;
+ ExteriorCell *exCell;
+
+ // Ambient light data
+ AMBIStruct ambi;
+
+ // Water height
+ int water;
+
+ // Linked lists that hold references to all the objects in the cell
+ RegionList!(LiveStatic) statics;
+ RegionList!(LiveMisc) miscItems;
+ RegionList!(LiveLight) lights;
+ RegionList!(LiveLight) statLights;
+ RegionList!(LiveNPC) npcs;
+ RegionList!(LiveContainer) containers;
+ RegionList!(LiveDoor) doors;
+ RegionList!(LiveActivator) activators;
+ RegionList!(LivePotion) potions;
+ RegionList!(LiveApparatus) appas;
+ RegionList!(LiveIngredient) ingredients;
+ RegionList!(LiveArmor) armors;
+ RegionList!(LiveWeapon) weapons;
+ RegionList!(LiveBook) books;
+ RegionList!(LiveTool) tools;
+ RegionList!(LiveClothing) clothes;
+ RegionList!(LiveCreature) creatures;
+
+ this(RegionManager r)
+ {
+ reg = r;
+ killCell(); // Make sure all data is initialized.
+ }
+
+ // Kills all data and initialize the object for reuse.
+ void killCell()
+ {
+ inCell = null;
+ exCell = null;
+
+ // Reset the lists
+ statics.init(reg);
+ miscItems.init(reg);
+ lights.init(reg);
+ statLights.init(reg);
+ npcs.init(reg);
+ containers.init(reg);
+ doors.init(reg);
+ activators.init(reg);
+ potions.init(reg);
+ appas.init(reg);
+ ingredients.init(reg);
+ armors.init(reg);
+ weapons.init(reg);
+ books.init(reg);
+ tools.init(reg);
+ clothes.init(reg);
+ creatures.init(reg);
+
+ // Write some statistics
+ //writefln(reg);
+
+ reg.freeAll();
+ }
+
+ // Load an exterior cell
+ void loadExtCell(int x, int y)
+ {
+ exCell = cells.getExt(x, y);
+
+ writefln("Name: %s", exCell.name);
+ writefln("Region: %s", exCell.region.id);
+
+ esFile.restoreContext(exCell.context, reg);
+
+ // Always for exterior cells
+ water = 0;
+
+ Color mapColor;
+ if(esFile.isNextSub("NAM5"))
+ esFile.readHExact(&mapColor, mapColor.sizeof);
+
+ loadReferences();
+
+ const float cellWidth = 8192;
+
+ // TODO/FIXME: This is temporary
+ playerData.position.position[0] = x*cellWidth;
+ playerData.position.position[1] = y*cellWidth;
+ playerData.position.position[2] = 6000;
+ playerData.position.rotation[] = 0;
+ }
+
+ // Load an interior cell
+ void loadIntCell(char[] cName)
+ {
+ inCell = cells.getInt(cName);
+ /*
+ writefln("Cell id: '%s'", cName);
+ writefln("Cell name: '%s'", cell.id);
+ */
+
+ esFile.restoreContext(inCell.context, reg);
+
+ // TODO: Read this crap in loadcell.d
+ if(esFile.isNextSub("INTV") || esFile.isNextSub("WHGT"))
+ water = esFile.getHInt();
+ //writefln("Water height: ", water);
+
+ if(inCell.flags & CellFlags.QuasiExt)
+ {
+ Region* reg = esFile.getHNOPtr!(Region)("RGNN", regions);
+ if(reg) writefln("Cell has region %s", reg.id);
+ else writefln("No region");
+ // Determine weather from this region
+ }
+ else
+ {
+ // Only for interior cells
+ esFile.readHNExact(&ambi, ambi.sizeof, "AMBI");
+ }
+
+ /*
+ writefln("Ambient light: ", ambi.ambient.array);
+ writefln("Sunlight: ", ambi.sunlight.array);
+ writefln("Fog color: ", ambi.ambient.array);
+ writefln("Fog density: ", ambi.fogDensity);
+ */
+ loadReferences();
+ }
+
+ private void loadReferences()
+ {
+ with(esFile)
+ {
+
+ // Now read all the references
+ while(hasMoreSubs)
+ {
+ // Number of references in the cell? Maximum once in each
+ // cell, but not always at the beginning, and not always
+ // right. In other words, completely useless. Strange
+ // people...
+ getHNOInt("NAM0", 0);
+
+ int refnum = getHNInt("FRMR"); // Reference number
+ char[] refr = getHNString("NAME"); // ID of object
+
+ // Used internally for optimizing
+ bool container;
+ bool door;
+ bool stat;
+ bool activator;
+
+ // Identify the referenced object by looking up the id
+ // string 'ref' in the global database of all
+ // cell-referencable objects.
+ Item itm = cellRefs.lookup(refr);
+ ItemT it = ItemT(itm);
+
+ // Create a new base object that holds all our reference
+ // data.
+ LiveObjectBase *base = reg.newT!(LiveObjectBase)();
+
+ // These should be ordered according to how commonly they
+ // occur and how large the reference lists are.
+
+ // Static mesh - probably the most common
+ if(Static *s = it.getStatic())
+ {
+ LiveStatic ls;
+ ls.m = s;
+ ls.base = base;
+ statics.insert(ls);
+ stat = true;
+ }
+ // Misc items are also pretty common
+ else if(Misc *m = it.getMisc())
+ {
+ LiveMisc ls;
+ ls.m = m;
+ ls.base = base;
+ miscItems.insert(ls);
+ }
+ // Lights and containers too
+ else if(Light *m = it.getLight())
+ {
+ LiveLight ls;
+ ls.m = m;
+ ls.base = base;
+
+ ls.time = m.data.time;
+
+ if(m.data.flags&Light.Flags.Carry)
+ lights.insert(ls);
+ else
+ statLights.insert(ls);
+ }
+ else if(Container *c = it.getContainer())
+ {
+ LiveContainer ls;
+ ls.m = c;
+ ls.base = base;
+ containers.insert(ls);
+ container = true;
+ }
+ // From here on I doubt the order will matter much
+ else if(Door *d = it.getDoor())
+ {
+ LiveDoor ls;
+ ls.m = d;
+ ls.base = base;
+ doors.insert(ls);
+ door = true;
+ }
+ // Activator?
+ else if(Activator *a = it.getActivator())
+ {
+ LiveActivator ls;
+ ls.m = a;
+ ls.base = base;
+ activators.insert(ls);
+ activator = true;
+ }
+ // NPC?
+ else if(NPC *n = it.getNPC())
+ {
+ LiveNPC ls;
+ ls.m = n;
+ ls.base = base;
+ npcs.insert(ls);
+ }
+ else if(Potion *p = it.getPotion())
+ {
+ LivePotion ls;
+ ls.m = p;
+ ls.base = base;
+ potions.insert(ls);
+ }
+ else if(Apparatus *m = it.getApparatus())
+ {
+ LiveApparatus ls;
+ ls.m = m;
+ ls.base = base;
+ appas.insert(ls);
+ }
+ else if(Ingredient *m = it.getIngredient())
+ {
+ LiveIngredient ls;
+ ls.m = m;
+ ls.base = base;
+ ingredients.insert(ls);
+ }
+ else if(Armor *m = it.getArmor())
+ {
+ LiveArmor ls;
+ ls.m = m;
+ ls.base = base;
+ armors.insert(ls);
+ }
+ else if(Weapon *m = it.getWeapon())
+ {
+ LiveWeapon ls;
+ ls.m = m;
+ ls.base = base;
+ weapons.insert(ls);
+ }
+ else if(Book *m = it.getBook())
+ {
+ LiveBook ls;
+ ls.m = m;
+ ls.base = base;
+ books.insert(ls);
+ }
+ else if(Clothing *m = it.getClothing())
+ {
+ LiveClothing ls;
+ ls.m = m;
+ ls.base = base;
+ clothes.insert(ls);
+ }
+ else if(Tool *m = it.getPick())
+ {
+ LiveTool ls;
+ ls.m = m;
+ ls.base = base;
+ tools.insert(ls);
+ }
+ else if(Tool *m = it.getProbe())
+ {
+ LiveTool ls;
+ ls.m = m;
+ ls.base = base;
+ tools.insert(ls);
+ }
+ else if(Tool *m = it.getRepair())
+ {
+ LiveTool ls;
+ ls.m = m;
+ ls.base = base;
+ tools.insert(ls);
+ }
+ else if(Creature *c = it.getCreature())
+ {
+ LiveCreature ls;
+ ls.m = c;
+ ls.base = base;
+ creatures.insert(ls);
+ }
+ else if(LeveledCreatures *l = it.getCreatureList)
+ {
+ // Create a creature, based on current player level.
+ LiveCreature ls;
+ ls.m = l.instCreature(playerData.level);
+ if(ls.m != null)
+ {
+ ls.base = base;
+ creatures.insert(ls);
+ }
+ }
+ else fail(format(" UNKNOWN REFERENCE! Type ", cast(int)it.i.type));
+
+ // Now that the object has found it's place, load data
+ // into base.
+
+ with(*base)
+ {
+ // ALL variables must be initialized here
+ disabled = false;
+ deleted = false;
+ transient = false;
+ teleport = false;
+
+ // Scale
+ scale = getHNOFloat("XSCL", 1.0);
+
+ // Statics only need the position data. Skip the
+ // unneeded calls to isNextSub() as an optimization.
+ if(stat) goto readpos;
+
+ // An NPC that owns this object (and will get angry if
+ // you steal it)
+ owner = getHNOString("ANAM");
+
+ // ??? I have no idea, link to a global variable
+ global = getHNOString("BNAM");
+
+ // ID of creature trapped in a soul gem (?)
+ soul = getHNOString("XSOL");
+
+ // ?? CNAM has a faction name, might be for
+ // objects/beds etc belonging to a faction.
+ cnam = getHNOString("CNAM");
+
+ // INDX might be PC faction rank required to use the
+ // item? Sometimes is -1.
+ if(cnam.length) indx = getHNInt("INDX");
+
+ // Possibly weapon health, number of uses left or
+ // weapon magic charge?
+ xchg = getHNOFloat("XCHG", 0.0);
+
+ // I have no idea, these are present some times, often
+ // along with owner (ANAM) and sometimes otherwise. Is
+ // NAM9 is always 1? INTV is usually one, but big for
+ // lights. Perhaps something to do with remaining
+ // light "charge". I haven't tried reading it as a
+ // float in those cases.
+ intv = getHNOInt("INTV", 0);
+ nam9 = getHNOInt("NAM9", 0);
+
+ // Present for doors that teleport you to another
+ // cell.
+ if(door && isNextSub("DODT"))
+ {
+ teleport = true;
+ readHExact(&destPos, destPos.sizeof);
+
+ // Destination cell (optitional?)
+ destCell = getHNOString("DNAM");
+ }
+
+ if(door || container)
+ {
+ // Lock level (I think)
+ fltv = getHNOInt("FLTV", 0);
+
+ // For locked doors and containers
+ key = getHNOString("KNAM");
+ trap = getHNOString("TNAM");
+ }
+
+ if(activator)
+ {
+ // Occurs ONCE in Morrowind.esm, for an activator.
+ unam = getHNOByte("UNAM", 0);
+
+ // Occurs in Tribunal.esm, eg. in the cell
+ // "Mournhold, Plaza Brindisi Dorom", where it has
+ // the value 100.
+ fltv = getHNOInt("FLTV", 0);
+ }
+
+ readpos:
+ // Position of this object within the cell
+ readHNExact(&pos, Placement.sizeof, "DATA");
+
+ // TODO/FIXME: Very temporary. Set player position at
+ // the first door we find.
+ if(door && !playerData.posSet)
+ {
+ playerData.posSet = true;
+ playerData.position = pos;
+ }
+ }
+ }
+
+ // Skip this? Large chance that the same file will be used for
+ // the next cell load, and the rest of our system can handle
+ // that without closing or reopening the file.
+
+ //close(); // Close the file
+ }
+ }
+}
+
+CellFreelist cellList;
+
+// Structure used as a free list for cell data objects and their
+// respective regions.
+struct CellFreelist
+{
+ // We love static arrays! And 100 cells should be enough for
+ // everybody :)
+ CellData[100] list;
+ uint next;
+
+ // TODO: Figure out a good size to use here as well.
+ CellData get()
+ {
+ if(next) return list[--next];
+
+ // Since these are reused, there's no waste in allocating on the
+ // stack here. Also, this is only semi-runtime (executed when
+ // loading a cell), thus a rare GC slow down is non-critical.
+ return new CellData(new RegionManager("CELL"));
+ }
+
+ void release(CellData r)
+ {
+ assert(next < list.length);
+
+ r.killCell();
+ list[next++] = r;
+ }
+}
diff --git a/scene/player.d b/scene/player.d
new file mode 100644
index 0000000000..8c29085810
--- /dev/null
+++ b/scene/player.d
@@ -0,0 +1,48 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (player.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module scene.player;
+
+import ogre.ogre;
+
+/*
+ * Contains essential data about the player and other current game
+ * data. This will be moved to script code.
+ */
+
+PlayerData playerData;
+
+struct PlayerData
+{
+ // Position and rotation. The rotation is not updated continuously,
+ // only the position.
+ Placement position;
+
+ // Just an example value, we use it for resolving leveled lists
+ // (lists that determine eg. what creatures to put in a cave based
+ // on what level the player is.)
+ short level = 5;
+
+ // Temp. way of selecting start point - used in celldata
+ bool posSet = false;
+}
diff --git a/scene/soundlist.d b/scene/soundlist.d
new file mode 100644
index 0000000000..c90828369b
--- /dev/null
+++ b/scene/soundlist.d
@@ -0,0 +1,72 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (soundlist.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module scene.soundlist;
+
+import esm.loadsoun;
+import sound.audio;
+
+SoundList soundScene;
+
+// Has a list over all sounds currently playing in the
+// scene. Currently only holds static, looping sounds, mainly
+// torches. Later I will have to decide what to do with play-once
+// sounds. Do I bother to update their position, or do I assume that
+// once it starts playing, it is short enough that it doesn't matter?
+// The last option is probably not viable, since some sounds can be
+// very long.
+struct SoundList
+{
+ // TODO: This is really just a test, a hack. Will be replaced by a
+ // list or similar later.
+ SoundInstance list[50];
+ int index = 0;
+
+ // Get a sound instance from a Sound struct
+ static SoundInstance getInstance(Sound *s, bool loop=false)
+ {
+ const distFactor = 40.0; // Just guessing, really.
+
+ SoundInstance inst = s.sound.getInstance();
+ inst.setParams(s.data.volume/255.0,
+ s.data.minRange*distFactor,
+ s.data.maxRange*distFactor,
+ loop);
+ return inst;
+ }
+
+ SoundInstance *insert(Sound *snd, bool loop=false)
+ {
+ if(index == 50) return null;
+
+ SoundInstance *s = &list[index++];
+ *s = getInstance(snd, loop);
+ return s;
+ }
+
+ void update(float x, float y, float z)
+ {
+ foreach(ref s; list[0..index])
+ s.setPlayerPos(x,y,z);
+ }
+}
diff --git a/sound/audiere.d b/sound/audiere.d
new file mode 100644
index 0000000000..a30d35c767
--- /dev/null
+++ b/sound/audiere.d
@@ -0,0 +1,72 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (audiere.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+// This file exposes the C++ functions that deal directly with the
+// sound library. The functions are implemented in cpp_audiere.cpp
+
+module sound.audiere;
+
+// A sound resource is the handle that represents the resource,
+// ie. the file.
+typedef void* AudiereResource;
+
+// A sound instance is an instance of this resource. We can have
+// several instances of the same file playing at once. Each instance
+// has its own volume, file position, pitch, etc.
+typedef void* AudiereInstance;
+
+extern(C)
+{
+ // Open the music device. Returns 0 on success, 1 on failure.
+ int cpp_openDevice();
+
+ // Open a new sound resource. Returns null on failure.
+ AudiereResource cpp_openSound(char* filename);
+
+ // Close a resource.
+ void cpp_closeSound(AudiereResource sound);
+
+ // Create an instance of a sound.
+ AudiereInstance cpp_createInstance(AudiereResource sound);
+
+ // Create an instance by streaming directly from file, and play it.
+ AudiereInstance cpp_playStream(char* filename, float volume);
+
+ // Destroy a previously created instance
+ void cpp_destroyInstance(AudiereInstance instance);
+
+ // Is this instance currently playing?
+ int cpp_isPlaying(AudiereInstance sound);
+
+ // Adjust parameters for this instance.
+ void cpp_setParams(AudiereInstance sound, float volume, float pan);
+
+ // Play a sound.
+ void cpp_playSound(AudiereInstance sound);
+
+ // Set repeat mode on
+ void cpp_setRepeat(AudiereInstance sound);
+
+ // Stop a sound
+ void cpp_stopSound(AudiereInstance sound);
+}
diff --git a/sound/audio.d b/sound/audio.d
new file mode 100644
index 0000000000..97f1d1a854
--- /dev/null
+++ b/sound/audio.d
@@ -0,0 +1,46 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (audio.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module sound.audio;
+
+public import sound.sfx;
+public import sound.music;
+
+import sound.audiere;
+
+class SoundException : Exception
+{
+ this(char[] caller, char[] msg) { super(caller ~ " SoundException: " ~ msg); }
+}
+
+MusicManager jukebox;
+MusicManager battleMusic;
+
+void initializeSound()
+{
+ if(cpp_openDevice())
+ throw new SoundException("initializeSound()",
+ "Failed to initialize music device");
+ jukebox.initialize("Main");
+ battleMusic.initialize("Battle");
+}
diff --git a/sound/cpp_audiere.cpp b/sound/cpp_audiere.cpp
new file mode 100644
index 0000000000..64abdabcf8
--- /dev/null
+++ b/sound/cpp_audiere.cpp
@@ -0,0 +1,118 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (cpp_audiere.cpp) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+#include
+#include
+using namespace audiere;
+using namespace std;
+
+/*
+ * Sound, using Audiere. Creates a very simple C interface for
+ * opening, closing and manipulating sounds.
+ */
+
+AudioDevicePtr device;
+
+extern "C" int cpp_openDevice()
+{
+ device = OpenDevice("");
+ if (!device) return 1;
+ return 0;
+}
+
+// Opens a new sample buffer from a file
+extern "C" SampleBuffer *cpp_openSound(char* filename)
+{
+ SampleSourcePtr sample = OpenSampleSource(filename);
+ if(!sample) return NULL;
+ SampleBufferPtr buf = CreateSampleBuffer(sample);
+ buf->ref();
+ return buf.get();
+}
+
+// Delete a sample buffer
+extern "C" void cpp_closeSound(SampleBuffer *buf)
+{
+ buf->unref();
+}
+
+// Get an output stream from a sample buffer.
+extern "C" OutputStream *cpp_createInstance(SampleBuffer *buf)
+{
+ SampleSourcePtr sample = buf->openStream();
+ if(!sample) return NULL;
+
+ OutputStreamPtr sound = OpenSound(device, sample, false);
+
+ sound->ref();
+ return sound.get();
+}
+
+// Stream a file directly. Used for music.
+extern "C" OutputStream *cpp_playStream(char* filename, float volume)
+{
+ OutputStreamPtr sound = OpenSound(device, filename, true);
+ if(sound)
+ {
+ sound->ref();
+ sound->setVolume(volume);
+ sound->play();
+ }
+ return sound.get();
+}
+
+extern "C" void cpp_destroyInstance(OutputStream *sound)
+{
+ sound->unref();
+}
+
+extern "C" int cpp_isPlaying(OutputStream *sound)
+{
+ if(sound && sound->isPlaying()) return 1;
+ return 0;
+}
+
+extern "C" void cpp_setParams(OutputStream *sound, float vol, float pan)
+{
+ if(sound)
+ {
+ sound->setVolume(vol);
+ sound->setPan(pan);
+ }
+}
+
+extern "C" void cpp_playSound(OutputStream *sound)
+{
+ sound->play();
+}
+
+extern "C" void cpp_setRepeat(OutputStream *sound)
+{
+ sound->setRepeat(true);
+}
+
+// Stop the sound
+extern "C" void cpp_stopSound(OutputStream *sound)
+{
+ if(sound) sound->stop();
+}
diff --git a/sound/music.d b/sound/music.d
new file mode 100644
index 0000000000..0f7516e9ff
--- /dev/null
+++ b/sound/music.d
@@ -0,0 +1,277 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (music.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module sound.music;
+
+import sound.audiere;
+import sound.audio;
+
+import core.config;
+import core.resource;
+
+// Simple music player, has a playlist and can pause/resume music.
+struct MusicManager
+{
+ private:
+
+ // How often we check if the music has died.
+ const float pollInterval = 2.0;
+
+ // Max file size to load. Files larger than this are streamed.
+ const int loadSize = 1024*1024;
+
+ // How much to add to the volume each second when fading
+ const float fadeInRate = 0.10;
+ const float fadeOutRate = 0.35;
+
+ // Volume
+ float volume, maxVolume;
+
+ // Time since last time we polled
+ float sumTime;
+
+ char[] name;
+
+ void fail(char[] msg)
+ {
+ throw new SoundException(name ~ " Jukebox", msg);
+ }
+
+ // Used when randomizing playlist
+ struct rndListStruct
+ {
+ char[] entry;
+ bool used;
+ }
+ rndListStruct[] rndList;
+
+ // TODO: How do we handle the play list? Randomize should be an
+ // option. We could also support things like M3U files, etc.
+ char[][] playlist;
+ int index; // Index of next song to play
+
+ bool musicOn;
+ AudiereInstance music;
+
+ // Which direction are we currently fading, if any
+ enum Fade { None = 0, In, Out }
+ Fade fading;
+
+ public:
+
+ // Initialize the jukebox
+ void initialize(char[] name)
+ {
+ this.name = name;
+ musicOn = false;
+ updateVolume();
+ }
+
+ // Get the new volume setting.
+ void updateVolume()
+ {
+ maxVolume = config.calcMusicVolume();
+
+ if(!musicOn) return;
+
+ // Adjust volume up to new setting, unless we are in the middle of
+ // a fade. Even if we are fading, though, the volume should never
+ // be over the max.
+ if(fading == Fade.None || volume > maxVolume) volume = maxVolume;
+ cpp_setParams(music, volume, 0.0);
+ }
+
+ // Give a music play list
+ void setPlaylist(char[][] pl)
+ {
+ playlist = pl;
+ index = 0;
+
+ randomize();
+ }
+
+ // Randomize playlist. An N^2 algorithm, but our current playlists
+ // are small. Improve it later if you really have to. If the
+ // argument is true, then we don't want the old last to be the new
+ // first.
+ private void randomize(bool checklast = false)
+ {
+ if(playlist.length < 2) return;
+
+ char[] last = playlist[0];
+
+ int left = playlist.length;
+ rndList.length = left;
+
+ foreach(int i, ref s; rndList)
+ {
+ s.used = false;
+ s.entry = playlist[i];
+ }
+
+ while(left--)
+ {
+ int index = rnd.randInt(0,left);
+ int i = 0;
+ foreach(ref s; rndList)
+ {
+ // Skip the ones we have used already
+ if(s.used) continue;
+
+ // Is this the correct index?
+ if(i == index)
+ {
+ s.used = true;
+ playlist[left] = s.entry;
+ break;
+ }
+ i++;
+ }
+ }
+
+ // Check that we don't replay the previous song, if the caller
+ // requested this.
+ if(checklast && playlist[0] == last)
+ {
+ playlist[0] = playlist[$-1];
+ playlist[$-1] = last;
+ }
+ }
+
+ // Skip to the next track
+ void playNext()
+ {
+ // If music is disabled, do nothing
+ if(!musicOn) return;
+
+ // Kill current track
+ if(music) cpp_destroyInstance(music);
+ music = null;
+
+ // No tracks to play?
+ if(!playlist.length) return;
+
+ // End of list? Randomize and start over
+ if(index == playlist.length)
+ {
+ randomize(true);
+ index = 0;
+ }
+
+ // Make sure the string is null terminated
+ assert(*(playlist[index].ptr+playlist[index].length) == 0);
+
+ music = cpp_playStream(playlist[index].ptr, volume);
+
+ if(!music) fail("Unable to start music track " ~ playlist[index]);
+
+ index++;
+ }
+
+ // Start playing the jukebox
+ void enableMusic()
+ {
+ if(!config.useMusic) return;
+
+ sumTime = 0;
+ musicOn = true;
+ volume = maxVolume;
+ fading = Fade.None;
+ playNext();
+ }
+
+ // Disable music
+ void disableMusic()
+ {
+ if(music) cpp_destroyInstance(music);
+ music = null;
+ musicOn = false;
+ }
+
+ // Pause current track
+ void pauseMusic()
+ {
+ fading = Fade.Out;
+ }
+
+ // Resume. Can also be called in place of enableMusic for fading in.
+ void resumeMusic()
+ {
+ if(!config.useMusic) return;
+
+ sumTime = 0;
+ volume = 0.0;
+ fading = Fade.In;
+ musicOn = true;
+ if(music) cpp_playSound(music);
+ else playNext();
+ }
+
+ // Add time since last frame to the counter. If the accumulated time
+ // has passed the polling interval, then check if the music has
+ // died. The Audiere library has a callback functionality, but it
+ // turned out not to be terribly reliable. Sometimes it was never
+ // called at all. So we have to poll at regular intervals .. :( This
+ // function is also used for fading.
+ void addTime(float time)
+ {
+ if(!musicOn) return;
+ sumTime += time;
+ if(sumTime > pollInterval)
+ {
+ sumTime = 0;
+ if(!cpp_isPlaying(music)) playNext();
+ }
+
+ if(fading)
+ {
+ // Fade the volume
+ if(fading == Fade.In)
+ {
+ volume += fadeInRate * time;
+ if(volume >= maxVolume)
+ {
+ fading = Fade.None;
+ volume = maxVolume;
+ }
+ }
+ else
+ {
+ assert(fading == Fade.Out);
+ volume -= fadeOutRate * time;
+ if(volume <= 0.0)
+ {
+ fading = Fade.None;
+ volume = 0.0;
+
+ // We are done fading out, disable music.
+ cpp_stopSound(music);
+ musicOn = false;
+ }
+ }
+
+ // Set the new volume
+ cpp_setParams(music, volume, 0.0);
+ }
+ }
+}
diff --git a/sound/sfx.d b/sound/sfx.d
new file mode 100644
index 0000000000..683609ff7f
--- /dev/null
+++ b/sound/sfx.d
@@ -0,0 +1,202 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (sfx.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module sound.sfx;
+
+import sound.audiere;
+import sound.audio;
+
+import core.config;
+import core.resource;
+
+import std.string;
+
+// Handle for a sound resource. This struct represents one sound
+// effect file (not a music file, those are handled differently
+// because of their size.) From this handle we may get instances,
+// which may be played and managed independently of each other. TODO:
+// Let the resource manager worry about only opening one resource per
+// file, when to kill resources, etc.
+struct SoundFile
+{
+ AudiereResource res;
+ char[] name;
+ bool loaded;
+
+ private int refs;
+
+ private void fail(char[] msg)
+ {
+ throw new SoundException(format("SoundFile '%s'", name), msg);
+ }
+
+ // Load a sound resource.
+ void load(char[] file)
+ {
+ // Make sure the string is null terminated
+ assert(*(file.ptr+file.length) == 0);
+
+ name = file;
+
+ loaded = true;
+ refs = 0;
+
+ res = cpp_openSound(file.ptr);
+
+ if(!res) fail("Failed to open sound file " ~ file);
+ }
+
+ // Get an instance of this resource.
+ SoundInstance getInstance()
+ {
+ SoundInstance si;
+ si.owner = this;
+ si.inst = cpp_createInstance(res);
+ if(!si.inst) fail("Failed to instantiate sound resource");
+ refs++;
+
+ return si;
+ }
+
+ // Return the sound instance when you're done with it
+ private void returnInstance(AudiereInstance inst)
+ {
+ refs--;
+ cpp_destroyInstance(inst);
+ if(refs == 0) unload();
+ }
+
+ // Unload the resource.
+ void unload()
+ {
+ loaded = false;
+ cpp_closeSound(res);
+ }
+}
+
+struct SoundInstance
+{
+ AudiereInstance inst;
+ SoundFile *owner;
+ float volume, min, max;
+ float xx, yy, zz; // 3D position
+ bool playing;
+ bool repeat;
+
+ // Return this instance to the owner
+ void kill()
+ {
+ owner.returnInstance(inst);
+ }
+
+ // Start playing a sound.
+ void play()
+ {
+ playing = true;
+ cpp_playSound(inst);
+ if(repeat) cpp_setRepeat(inst);
+ }
+
+ // Go buy a cookie
+ void stop()
+ {
+ cpp_stopSound(inst);
+ playing = false;
+ }
+
+ // Set parameters such as max volume and range
+ void setParams(float volume, float minRange, float maxRange, bool repeat=false)
+ in
+ {
+ assert(volume >= 0 && volume <= 1.0, "Volume out of range");
+ }
+ body
+ {
+ this.volume = volume;
+ min = minRange;
+ max = maxRange;
+ this.repeat = repeat;
+ playing = false;
+ }
+
+ // Set 3D position of sound
+ void setPos(float x, float y, float z)
+ {
+ xx = x;
+ yy = y;
+ zz = z;
+ }
+
+ // Currently VERY experimental, panning disabled. At some point we
+ // will likely switch to OpenAL with built-in 3D sound and dump this
+ // entirely.
+ void setPlayerPos(float x, float y, float z)
+ {
+ //writef("{%s %s %s} ", x, y, z);
+ // Distance squared
+ x -= xx;
+ y -= yy;
+ z -= zz;
+ //writef("[%s %s %s] ", x, y, z);
+ float r2 = (x*x + y*y + z*z);
+ //writefln(r2, " (%s)", max*max);
+
+ // If outside range, disable
+ if(r2 > max*max)
+ {
+ // We just moved out of range
+ if(playing)
+ {
+ //writefln("Out of range");
+ stop();
+ }
+ }
+ else
+ {
+ // We just moved into range
+ if(!playing)
+ {
+ //writefln("In range!");
+ play();
+ }
+ }
+
+ if(!playing) return;
+
+ // Invert distance
+ if(r2 < 1) r2 = 1;
+ else r2 = 1/r2;
+
+ float vol = 2*r2*min*min;
+ float pan = 0;//80*x*r2;
+
+ //writefln("x=%s, vol=%s, pan=%s", x, vol, pan);
+
+ if(vol>1.0) vol = 1.0;
+ if(pan<-1.0) pan = -1.0;
+ else if(pan > 1.0) pan = 1.0;
+ //writefln("vol=", vol, " volume=", vol*volume*config.calcSfxVolume());
+
+ cpp_setParams(inst, vol*volume*config.calcSfxVolume(), pan);
+ }
+}
diff --git a/util/random.d b/util/random.d
new file mode 100644
index 0000000000..75853aff3a
--- /dev/null
+++ b/util/random.d
@@ -0,0 +1,155 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (random.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module util.random;
+
+private import std.date;
+private import std.random;
+
+abstract class Random
+{
+ static const double scale = 1.0/uint.max;
+
+ // Initialize from current time
+ this() { initialize(); }
+
+ // Initialize from parameter
+ this(long seed) { initialize(seed); }
+
+ // Reinitialize with seed
+ abstract void initialize(long newSeed);
+
+ // Produce random numbers between 0 and uint.max
+ abstract uint rand();
+
+ // Default is to initialize using current time as seed
+ void initialize() { initialize(getUTCtime()); }
+
+ // Produce a uniform random number between 0 and 1
+ double random()
+ {
+ return rand() * scale;
+ }
+
+ // Return a uniform random number between a and b. Works for the
+ // both the cases a < b and a > b.
+ double random(double a, double b)
+ in
+ {
+ // We only disallow nan parameters
+ assert(a <>= b);
+ }
+ out(result)
+ {
+ // Result must be in range m < result < M, where m=min(a,b) and M=max(a,b)
+ if(b > a) assert( (a < result) && (result < b) );
+ else if(a > b) assert( (b < result) && (result < a) );
+ }
+ body
+ { return random()*(b - a) + a; }
+
+ // Return a random integer between a and b, inclusive.
+ int randInt(int a, int b)
+ out(result)
+ {
+ // Result must be in range m <= result <= M, where m=min(a,b) and M=max(a,b)
+ if(b >= a) assert( (a <= result) && (result <= b) );
+ else if(a > b) assert( (b <= result) && (result <= a) );
+ }
+ body
+ {
+ if(a>b) return cast(int)(rand() % (a-b+1)) + b;
+ else if(b>a) return cast(int)(rand() % (b-a+1)) + a;
+ else return a;
+ }
+
+ // Allow using "function call" syntax:
+ //
+ // Random ran = new Random1;
+ // double d = ran(); // Calls ran.random()
+ // d = ran(a,b); // Calls ran.random(a,b);
+ double opCall() { return random(); }
+ double opCall(double a, double b) { return random(a, b); }
+
+ // Return the seed originally given the object
+ long getSeed() {return origSeed;}
+
+protected:
+ long origSeed; // Value used to seed the generator
+}
+
+// Uses the standard library generator
+class DRand : Random
+{
+ // Initialize from current time
+ this() { super(); }
+
+ // Initialize from parameter
+ this(long seed) { super(seed); }
+
+ uint rand() { return std.random.rand(); }
+
+ void initialize(long newSeed)
+ {
+ origSeed = newSeed;
+ rand_seed(newSeed, 0);
+ }
+
+ alias Random.initialize initialize;
+
+ unittest
+ {
+ struct tmp { import std.stdio; }
+ alias tmp.writef writef;
+ alias tmp.writefln writefln;
+
+ writefln("Unittest for class DRand");
+
+ DRand ran = new DRand;
+ writefln("Seed (from time) = ", ran.getSeed());
+
+ // Take a look at some numbers on screen
+ writefln("Some random numbers in [0,1]:");
+ for(int i=0; i<10; i++)
+ writef(" ", ran());
+
+ ran = new DRand(0);
+ writefln("\nNew seed (preset) = ", ran.getSeed());
+
+ // Take a look at some numbers on screen
+ writefln("Some random numbers in [0,1]:");
+ for(int i=0; i<10; i++)
+ writef(" ", ran());
+
+ // Check that all interfaces work (compile time)
+ ran();
+ ran(1,2);
+ ran.random();
+ ran.random(3,4);
+ ran.initialize();
+ ran.initialize(10);
+ ran.randInt(-3,5);
+
+ writefln("\nEnd of unittest for class DRand\n");
+ }
+}
diff --git a/util/regions.d b/util/regions.d
new file mode 100644
index 0000000000..802e8e7196
--- /dev/null
+++ b/util/regions.d
@@ -0,0 +1,644 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (regions.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module util.regions;
+
+private import std.gc;
+private import std.string;
+private import std.c.stdlib;
+
+private import monster.util.string;
+
+alias std.c.stdlib.malloc malloc;
+
+class RegionManagerException : Exception
+{
+ this(char[] msg, char[] name)
+ { super( format("Memory Region Manager '%s': %s", name, msg ) ); }
+}
+
+// I tried this as an inner class, and it didn't even remotely work. I
+// think inner class support in DMD sucks quite a bit. Works ok now
+// (but still had some problems - template support in general is still
+// pretty patchy.)
+
+// UPDATE: The above comment was written a long time ago, it might
+// work now. But if it ain't broke, don't fix it.
+
+// A resizable array using a region for memory allocation. These can
+// safely be resized back and forth without wasting large amounts of
+// memory.
+class RegionBuffer(T)
+{
+ final:
+ private:
+ T[] buffer;
+ T[] inUse;
+
+ int reserve = 20;
+
+ RegionManager reg;
+
+ public:
+
+ // Size is initial size, reserve gives an indicator of the minimum
+ // amount to increase the array with at once.
+ this(RegionManager m, int size = 0, int reserve = 0)
+ {
+ reg = m;
+ if(reserve) this.reserve = reserve;
+ if(size) alloc(size);
+ }
+
+ new(uint size, RegionManager r)
+ {
+ return r.allocate(size).ptr;
+ }
+
+ delete(void *p) { assert(0); }
+
+ // Check if the buffer can hold 'size' more elements. If not,
+ // increase it. NOTE: This works even if data = null, and inUse
+ // is not. This allows us to make copy-on-resize slices.
+ private void alloc(uint size)
+ {
+ if(inUse.length + size <= buffer.length)
+ {
+ inUse = buffer[0..inUse.length+size];
+ return;
+ }
+
+ // Allocate a new array, with more entries than requested
+ buffer = reg.allocateT!(T)(buffer.length + size + reserve);
+
+ // Copy the old data
+ buffer[0..inUse.length] = inUse;
+
+ // Set up the array in use
+ inUse = buffer[0..(inUse.length+size)];
+ }
+
+ T opIndex(int i)
+ {
+ return inUse[i];
+ }
+ T opIndexAssign(T val, int i) { return inUse[i] = val; }
+
+ RegionBuffer opSlice(int x, int y)
+ {
+ RegionBuffer b = new(reg) RegionBuffer(reg,0,reserve);
+ b.inUse = inUse[x..y];
+ return b;
+ }
+
+ RegionBuffer opSlice() { return opSlice(0,inUse.length); }
+
+ RegionBuffer opCatAssign(T t)
+ {
+ alloc(1);
+ inUse[$-1] = t;
+ return this;
+ }
+
+ RegionBuffer opCatAssign(T[] t)
+ {
+ alloc(t.length);
+ inUse[($-t.length)..$] = t;
+ return this;
+ }
+
+ RegionBuffer opCatAssign(RegionBuffer t)
+ {
+ alloc(t.inUse.length);
+ inUse[($-t.inUse.length)..$] = t.inUse;
+ return this;
+ }
+
+ RegionBuffer opCat(T t) { return this.dup() ~= t; }
+ RegionBuffer opCat(T[] t) { return this.dup() ~= t; }
+ RegionBuffer opCat(RegionBuffer t) { return this.dup() ~= t; }
+
+ RegionBuffer dup()
+ {
+ RegionBuffer b = new(reg) RegionBuffer(reg, inUse.length, reserve);
+ b.inUse[] = inUse[];
+ return b;
+ }
+
+ uint length() { return inUse.length; }
+
+ void length(uint size)
+ {
+ // Grow array
+ if(size > inUse.length) alloc(size - inUse.length);
+
+ // Shrink array
+ else inUse = inUse[0..size];
+ }
+
+ // For direct access
+ T[] array() { return inUse; }
+}
+
+alias RegionBuffer!(char) RegionCharBuffer;
+alias RegionBuffer!(int) RegionIntBuffer;
+
+/*
+ * Region manager, a mark-sweep memory manager. You use it to allocate
+ * a lot of buffers, and when you are done with them you deallocate
+ * them all in one fell sweep.
+ *
+ */
+class RegionManager
+{
+ final:
+ private:
+ // Identifying name, used for error messages.
+ char[] name;
+
+ // Use a default buffer size of one meg. Might change later.
+ const uint defaultBufferSize = 1024*1024;
+
+ // The size to use for new buffer.
+ uint bufferSize;
+
+ // Current amount of space that is 'lost' in unused end-of-buffer
+ // areas. Since we have proceeded to other buffers, this space will
+ // remain unused until freeAll is called.
+ uint lost;
+
+ ubyte[][] buffers; // Actual memory buffers
+ void *gcRanges[]; // List of ranges added to gc
+
+ ubyte[] left; // Slice of what is left of the current buffer
+
+ int currentBuffer; // Index into the next unused buffer
+ int currentRange; // Index into the next unused gcRanges entry
+
+ void fail(char[] msg)
+ {
+ throw new RegionManagerException(msg, name);
+ }
+
+ // We have run out of space, add a new buffer. I want this to be an
+ // exception rather than the rule, the default bufferSize should be
+ // large enough to prevent this from being necessary in most cases.
+ void nextBuffer()
+ {
+ /*
+ if(currentBuffer != 0)
+ writefln("WARNING: adding new buffer, number ", currentBuffer);
+ */
+
+ // We should never have to increase the number of buffers!
+ if(currentBuffer >= buffers.length) fail("Out of buffers");
+
+ // Set up the buffer (if it is not already allocated.)
+ //buffers[currentBuffer].length = bufferSize;
+ if(buffers[currentBuffer].length != bufferSize)
+ {
+ assert(buffers[currentBuffer].length == 0);
+ ubyte *p = cast(ubyte*)malloc(bufferSize);
+ if(!p) fail("Malloc failed");
+ buffers[currentBuffer] = p[0..bufferSize];
+ }
+
+ // Remember the amount of space we just lost
+ lost += left.length;
+
+ // The entire buffer is available to us
+ left = buffers[currentBuffer];
+
+ // Point to the next unused buffer
+ currentBuffer++;
+ }
+
+ public:
+
+ this(char[] name = "", uint bufferSize = defaultBufferSize)
+ {
+ this.name = name;
+ this.bufferSize = bufferSize;
+
+ // Pointers are cheap. Let's preallocate these arrays big enough
+ // from the start. It's inflexible, but on purpose. We shouldn't
+ // NEED to grow them later, so we don't.
+ buffers.length = 100;
+ gcRanges.length = 10000;
+
+ freeAll();
+ }
+
+ ~this()
+ {
+ // Don't leave any loose ends dangeling for the GC.
+ freeAll();
+
+ // Kill everything
+ foreach(ubyte[] arr; buffers)
+ free(arr.ptr);
+
+ delete buffers;
+ delete gcRanges;
+ }
+
+ // Allocates an array from the region.
+ ubyte[] allocate(uint size)
+ {
+ if(size > bufferSize)
+ fail(format("Tried to allocate %d, but maximum allowed allocation size is %d",
+ size, bufferSize));
+
+ // If the array cannot fit inside this buffer, get a new one.
+ if(size > left.length) nextBuffer();
+
+ ubyte[] ret = left[0..size];
+
+ left = left[size..$];
+
+ //writefln("Allocated %d, %d left in buffer", size, left.length);
+
+ return ret;
+ }
+
+ // Allocate an array and add it to the GC as a root region. This
+ // should be used for classes and other data that might contain
+ // pointers / class references to GC-managed data.
+ ubyte[] allocateGC(uint size)
+ {
+ if(currentRange >= gcRanges.length)
+ fail("No more available GC ranges");
+
+ ubyte[] ret = allocate(size);
+
+ // Add it to the GC
+ void *p = ret.ptr;
+ std.gc.addRange(p, p+ret.length);
+ gcRanges[currentRange++] = p;
+
+ return ret;
+ }
+
+ // Allocate an array of a specific type, eg. to allocate 4 ints, do
+ // int[] array = allocateT!(int)(4);
+ template allocateT(T)
+ {
+ T[] allocateT(uint number)
+ {
+ return cast(T[])allocate(number * T.sizeof);
+ }
+ }
+
+ alias allocateT!(int) getInts;
+ alias allocateT!(char) getString;
+
+ template newT(T)
+ {
+ T* newT()
+ {
+ return cast(T*)allocate(T.sizeof);
+ }
+ }
+
+ template allocateGCT(T)
+ {
+ T[] allocateGCT(uint number)
+ {
+ return cast(T[])allocateGC(number * T.sizeof);
+ }
+ }
+
+ // Copies an array of a given type
+ template copyT(T)
+ {
+ T[] copyT(T[] input)
+ {
+ T[] output = cast(T[]) allocate(input.length * T.sizeof);
+ output[] = input[];
+ return output;
+ }
+ }
+
+ alias copyT!(int) copy;
+ alias copyT!(char) copy;
+
+ // Copies a string and ensures that it is null-terminated
+ char[] copyz(char[] str)
+ {
+ char[] res = cast(char[]) allocate(str.length+1);
+ res[$-1] = 0;
+ res = res[0..$-1];
+ res[] = str[];
+ return res;
+ }
+
+ // Resets the region manager, but does not deallocate the
+ // buffers. To do that, delete the object.
+ void freeAll()
+ {
+ lost = 0;
+ currentBuffer = 0;
+ left = null;
+
+ // Free the ranges from the clutch of the GC's hand.
+ foreach(inout void *p; gcRanges[0..currentRange])
+ if(p)
+ {
+ std.gc.removeRange(p);
+ p = null;
+ }
+
+ currentRange = 0;
+ }
+
+ // Number of used buffers, including the current one
+ uint usedBuffers() { return currentBuffer; }
+
+ // Total number of allocated buffers
+ uint totalBuffers()
+ {
+ uint i;
+
+ // Count number of allocated buffers
+ while(i < buffers.length && buffers[i].length) i++;
+
+ return i;
+ }
+
+ // Total number of allocated bytes
+ uint poolSize()
+ {
+ return bufferSize * totalBuffers();
+ }
+
+ // Total number of bytes that are unavailable for use. (They might
+ // not be used, as such, if they are at the end of a buffer but the
+ // next buffer is in use.)
+ uint usedSize()
+ {
+ return currentBuffer*bufferSize - left.length;
+ }
+
+ // The total size of data that the user has requested.
+ uint dataSize()
+ {
+ return usedSize() - lostSize();
+ }
+
+ // Number of lost bytes
+ uint lostSize()
+ {
+ return lost;
+ }
+
+ // Total amount of allocated space that is not used
+ uint wastedSize()
+ {
+ return poolSize() - dataSize();
+ }
+
+ // Give some general info and stats
+ char[] toString()
+ {
+ return format("Memory Region Manager '%s':", name,
+ "\n pool %s (%d blocks)", comma(poolSize), totalBuffers,
+ "\n used %s", comma(usedSize),
+ "\n data %s", comma(dataSize),
+ "\n wasted %s (%.2f%%)", comma(wastedSize),
+ poolSize()?100.0*wastedSize()/poolSize():0,
+ "\n lost %s (%.2f%%)", comma(lost), usedSize()?100.0*lost/usedSize:0);
+ }
+
+ // Get a RegionBuffer of a given type
+ template getBuffer(T)
+ {
+ RegionBuffer!(T) getBuffer(int size = 0, int reserve = 0)
+ {
+ return new(this) RegionBuffer!(T)(this,size,reserve);
+ }
+ }
+
+ alias getBuffer!(char) getCharBuffer;
+ alias getBuffer!(int) getIntBuffer;
+}
+
+unittest
+{
+ RegionManager r = new RegionManager("UT", 100);
+
+ // Test normal allocations first
+ assert(r.poolSize == 0);
+ assert(r.usedSize == 0);
+ assert(r.dataSize == 0);
+ assert(r.lostSize == 0);
+ assert(r.wastedSize == 0);
+
+ ubyte [] arr = r.allocate(30);
+ void *p = arr.ptr;
+
+ assert(p == r.buffers[0].ptr);
+ assert(arr.length == 30);
+ assert(r.poolSize == 100);
+ assert(r.usedSize == 30);
+ assert(r.dataSize == 30);
+ assert(r.lostSize == 0);
+ assert(r.wastedSize == 70);
+
+ arr = r.allocate(70);
+ assert(arr.ptr == p + 30);
+ assert(arr.length == 70);
+ assert(r.poolSize == 100);
+ assert(r.usedSize == 100);
+ assert(r.dataSize == 100);
+ assert(r.lostSize == 0);
+ assert(r.wastedSize == 0);
+
+ // Overflow the buffer
+ p = r.allocate(2).ptr;
+ assert(p == r.buffers[1].ptr);
+ assert(r.poolSize == 200);
+ assert(r.usedSize == 102);
+ assert(r.dataSize == 102);
+ assert(r.lostSize == 0);
+ assert(r.wastedSize == 98);
+
+ // Overflow the buffer and leave lost space behind
+ r.freeAll();
+ assert(r.poolSize == 200);
+ assert(r.usedSize == 0);
+ assert(r.dataSize == 0);
+ assert(r.lostSize == 0);
+ assert(r.wastedSize == 200);
+
+ r.allocate(1);
+ r.allocate(100);
+ assert(r.poolSize == 200);
+ assert(r.usedSize == 200);
+ assert(r.dataSize == 101);
+ assert(r.lostSize == 99);
+ assert(r.wastedSize == 99);
+
+ // Try to allocate a buffer that is too large
+ bool threw = false;
+ try r.allocate(101);
+ catch(RegionManagerException e)
+ {
+ threw = true;
+ }
+ assert(threw);
+
+ // The object should still be in a valid state.
+
+ // Try an allocation with roots
+ assert(r.currentRange == 0);
+ arr = r.allocateGC(50);
+ assert(r.poolSize == 300);
+ assert(r.usedSize == 250);
+ assert(r.dataSize == 151);
+ assert(r.lostSize == 99);
+ assert(r.wastedSize == 149);
+ assert(r.currentRange == 1);
+ assert(r.gcRanges[0] == arr.ptr);
+
+ int[] i1 = r.allocateGCT!(int)(10);
+ assert(i1.length == 10);
+ assert(r.poolSize == 300);
+ assert(r.usedSize == 290);
+ assert(r.dataSize == 191);
+ assert(r.lostSize == 99);
+ assert(r.currentRange == 2);
+ assert(r.gcRanges[1] == i1.ptr);
+
+ r.freeAll();
+ assert(r.currentRange == 0);
+ assert(r.poolSize == 300);
+ assert(r.usedSize == 0);
+ assert(r.dataSize == 0);
+ assert(r.lostSize == 0);
+
+ // Allocate some floats
+ float[] fl = r.allocateT!(float)(24);
+ assert(fl.length == 24);
+ assert(r.poolSize == 300);
+ assert(r.usedSize == 96);
+ assert(r.dataSize == 96);
+ assert(r.lostSize == 0);
+
+ // Copy an array
+ r.freeAll();
+ char[] stat = "hello little guy";
+ assert(r.dataSize == 0);
+ char[] copy = r.copy(stat);
+ assert(copy == stat);
+ assert(copy.ptr != stat.ptr);
+ copy[0] = 'a';
+ copy[$-1] = 'a';
+ assert(stat != copy);
+ assert(stat == "hello little guy");
+ assert(r.dataSize == stat.length);
+
+ // Test copyz()
+ r.freeAll();
+ stat = "ABC";
+ char *pp = cast(char*) r.copyz(stat).ptr;
+ assert(pp[2] == 'C');
+ assert(pp[3] == 0);
+ copy = r.copyz(stat);
+ assert(cast(char*)copy.ptr - pp == 4);
+ assert(pp[4] == 'A');
+ copy[0] = 'F';
+ assert(pp[3] == 0);
+ assert(pp[4] == 'F');
+
+ // Test of the buffer function
+ r.freeAll();
+ RegionBuffer!(int) b = r.getBuffer!(int)();
+ assert(b.inUse.length == 0);
+ assert(b.reserve == 20);
+
+ b.reserve = 5;
+
+ assert(b.length == 0);
+ b ~= 10;
+ b ~= 13;
+
+ assert(b.length == 2);
+ assert(b[0] == 10);
+ assert(b[1] == 13);
+ assert(b.buffer.length == 6);
+ p = b.buffer.ptr;
+
+ b.length = 0;
+ b.length = 6;
+ assert(p == b.buffer.ptr);
+ assert(b.length == 6);
+
+ b.length = 3;
+ assert(p == b.buffer.ptr);
+ assert(b.length == 3);
+
+ b[2] = 167;
+
+ b.length = 7;
+ assert(p != b.buffer.ptr); // The buffer was reallocated
+ assert(b.length == 7);
+
+ assert(b[2] == 167);
+
+ i1 = new int[5];
+ foreach(int v, inout int i; i1)
+ i = v;
+
+ p = b.buffer.ptr;
+ RegionBuffer!(int) a = b ~ i1;
+ assert(p != a.buffer.ptr); // A new buffer has been allocated
+ assert(p == b.buffer.ptr); // B should be unchanged
+ assert(a.length == b.length + i1.length);
+
+ for(int i=0; i < b.length; i++)
+ assert(a[i] == b[i]);
+
+ for(int i=0; i < i1.length; i++)
+ assert(a[i+7] == i);
+
+ // Make sure the arrays are truly different
+ a[5] = b[5] + 2;
+ assert(a[5] != b[5]);
+
+ // Make a slice
+ a = b[2..5];
+ assert(a.inUse.ptr == b.inUse.ptr+2);
+ a[1] = 4;
+ assert(a[1] == b[3]);
+ b[3] = -12;
+ assert(a[1] == b[3]);
+
+ a.length = a.length + 1;
+ assert(a.inUse.ptr != b.inUse.ptr+2);
+ a[1] = 4;
+ assert(a[1] != b[3]);
+ b[3] = -12;
+ assert(a[1] != b[3]);
+
+ r.freeAll();
+}
diff --git a/util/reglist.d b/util/reglist.d
new file mode 100644
index 0000000000..111c405f0e
--- /dev/null
+++ b/util/reglist.d
@@ -0,0 +1,215 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (reglist.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module util.reglist;
+
+import util.regions;
+
+import std.string;
+
+class RegionListException : Exception
+{
+ this(char[] msg)
+ {
+ super("RegionListException: " ~ msg);
+ }
+}
+
+/*
+ * Internal structure used below
+ */
+
+struct _aaNode(Value)
+{
+ Value value;
+
+ _aaNode* next; // Next node
+ _aaNode* prev; // Previous node
+}
+
+/*
+ * Very simple linked list that uses a specified region for
+ * allocation.
+ */
+
+struct RegionList(Value)
+{
+ private:
+ alias _aaNode!(Value) Node;
+
+ Node *head; // This is the head of the linked list (first element)
+ Node *tail; // New nodes are inserted here
+ uint totalNum; // Number of elements
+
+ RegionManager reg;
+
+ // Throw an exception
+ void fail(char[] msg)
+ {
+ msg = format("RegionList!(%s) exception: %s", typeid(Value).toString, msg);
+ throw new RegionListException(msg);
+ }
+
+ public:
+
+
+ alias Node* Iterator;
+
+ // Equivalent to calling remove() on all elements
+ void clear()
+ {
+ head = tail = null;
+ totalNum = 0;
+ }
+
+ // Reset the object and assign a new region manager
+ void init(RegionManager reg)
+ {
+ clear();
+ this.reg = reg;
+ }
+
+ Iterator insert(Value v)
+ {
+ Node* p = createNode();
+
+ if(tail)
+ {
+ // Insert node at the end of the list
+ assert(head != null);
+ tail.next = p;
+ }
+ else
+ {
+ // This is the first element to be inserted
+ assert(head == null);
+ head = p;
+ }
+ p.prev = tail;
+ tail = p;
+
+ p.value = v;
+
+ return p;
+ }
+
+ void remove(Iterator p)
+ {
+ // Remove from the list
+ if(p.next) p.next.prev = p.prev;
+ else // We're the tail
+ {
+ assert(tail == p);
+ tail = p.prev;
+ }
+
+ if(p.prev) p.prev.next = p.next;
+ else // We're head
+ {
+ assert(head == p);
+ head = p.next;
+ }
+
+ totalNum--;
+ }
+
+ // Create a new node and return it's pointer
+ private Node* createNode()
+ {
+ Node *p = cast(Node*) reg.allocate(Node.sizeof).ptr;
+
+ // Initialize pointers
+ p.next = null;
+ p.prev = null;
+
+ totalNum++;
+
+ return p;
+ }
+
+ // Loop through the nodes in the order they were inserted
+ int opApply(int delegate(ref Value v) del)
+ {
+ Node *p = head;
+ uint safeGuard = 0;
+ while(p != null)
+ {
+ assert(safeGuard++ < totalNum);
+ int i = del(p.value);
+ if(i) return i;
+ p = p.next;
+ }
+ return 0;
+ }
+
+ // Loop through the nodes in the order they were inserted
+ int opApply(int delegate(ref int ind, ref Value v) del)
+ {
+ Node *p = head;
+ int ind = 0;
+ while(p != null)
+ {
+ assert(ind < totalNum);
+ int i = del(ind, p.value);
+ ind++;
+ if(i) return i;
+ p = p.next;
+ }
+ return 0;
+ }
+
+ // Number of elements
+ uint length() { return totalNum; }
+}
+
+/*
+unittest
+{
+ RegionManager r = new RegionManager();
+
+ RegionList!(float) ll;
+ ll.reg = r;
+
+ assert(ll.length == 0);
+ ll.Iterator it = ll.insert(10.4);
+ writefln(r);
+ assert(ll.length == 1);
+ ll.insert(23);
+ it = ll.insert(6.3);
+ ll.insert(-1000);
+
+ writefln(r);
+ assert(ll.length == 4);
+
+ foreach(float f; ll) writefln(f);
+
+ ll.remove(it);
+
+ assert(ll.length == 3);
+
+ assert(r.dataSize() == 12*4);
+
+ foreach(int i, float f; ll) writefln(i, " ", f);
+}
+import std.stdio;
+*/
diff --git a/util/uniquename.d b/util/uniquename.d
new file mode 100644
index 0000000000..d3da6a0f90
--- /dev/null
+++ b/util/uniquename.d
@@ -0,0 +1,96 @@
+/*
+ OpenMW - The completely unofficial reimplementation of Morrowind
+ Copyright (C) 2008 Nicolay Korslund
+ Email: < korslund@gmail.com >
+ WWW: http://openmw.snaptoad.com/
+
+ This file (uniquename.d) is part of the OpenMW package.
+
+ OpenMW is distributed as free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License
+ version 3, as published by the Free Software Foundation.
+
+ 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
+ version 3 along with this program. If not, see
+ http://www.gnu.org/licenses/ .
+
+ */
+
+module util.uniquename;
+
+import std.stdio;
+
+// Simple auxiliary code for creating unique names in succession,
+// eg. "Screenshot_0000000001.png", "Screenshot_0000000002.png", etc..
+// WARNING: Uses static storage, so do NOT use persistent references
+// or slices to the strings produced here. Intended only for OGRE
+// calls, where copies are always made anyway.
+
+struct UniqueName
+{
+ static this()
+ {
+ buffer.length = 100;
+ number = buffer[0..10];
+ number[] = "0000000000";
+ }
+
+ static char[] opCall(char[] addon = "")
+ {
+ // Point p at last digit
+ char *p = &number[$-1];
+ char *pbeg = number.ptr;
+ while(p != pbeg-1)
+ {
+ if(*p == '9')
+ {
+ // Carry over
+ *p = '0';
+ p--;
+ }
+ else
+ {
+ // Increase this digit and exit
+ (*p)++;
+ break;
+ }
+ }
+ assert(p != pbeg-1); // Overflow
+
+ int totLen = 10 + addon.length;
+ if(totLen >= buffer.length) totLen = buffer.length - 1;
+ if(totLen > 10) buffer[10..totLen] = addon;
+ buffer[totLen] = 0; // String must null-terminate
+
+ //writefln("UniqueName result=%s", buffer[0..totLen]);
+
+ return buffer[0..totLen];
+ }
+
+ private:
+ static char[] buffer;
+ static char[] number;
+}
+
+unittest
+{
+ assert(UniqueName() == "0000000001");
+ assert(UniqueName() == "0000000002");
+ assert(UniqueName() == "0000000003");
+ assert(UniqueName() == "0000000004");
+ assert(UniqueName() == "0000000005");
+ assert(UniqueName() == "0000000006");
+ assert(UniqueName("blah") == "0000000007blah");
+
+ assert(UniqueName() == "0000000008");
+ assert(UniqueName() == "0000000009");
+ assert(UniqueName() == "0000000010");
+ assert(UniqueName() == "0000000011");
+ assert(UniqueName(" bottles of beer on the wall") ==
+ "0000000012 bottles of beer on the wall");
+}