From 055d1b1dd6a0e073cd1ae3727fc26b81b8f21f22 Mon Sep 17 00:00:00 2001 From: nkorslund Date: Sun, 22 Jun 2008 18:32:58 +0000 Subject: [PATCH] Added trunk git-svn-id: https://openmw.svn.sourceforge.net/svnroot/openmw/trunk@4 ea6a568a-9f4f-0410-981a-c910a81bb256 --- GPL3.txt | 674 +++++++++++++++++++++ INSTALL.txt | 232 ++++++++ Makefile | 33 ++ bored.d | 364 ++++++++++++ bsa/bsafile.d | 358 ++++++++++++ bsa/bsatool.d | 129 ++++ cells.txt | 1134 ++++++++++++++++++++++++++++++++++++ core/config.d | 276 +++++++++ core/filefinder.d | 224 +++++++ core/inifile.d | 228 ++++++++ core/memory.d | 60 ++ core/resource.d | 538 +++++++++++++++++ dsss.conf | 43 ++ esm/defs.d | 137 +++++ esm/esmmain.d | 117 ++++ esm/esmtool.d | 357 ++++++++++++ esm/filereader.d | 736 +++++++++++++++++++++++ esm/imports.d | 44 ++ esm/listkeeper.d | 367 ++++++++++++ esm/loadacti.d | 53 ++ esm/loadalch.d | 74 +++ esm/loadappa.d | 67 +++ esm/loadarmo.d | 143 +++++ esm/loadbody.d | 90 +++ esm/loadbook.d | 62 ++ esm/loadbsgn.d | 53 ++ esm/loadcell.d | 371 ++++++++++++ esm/loadclas.d | 90 +++ esm/loadclot.d | 83 +++ esm/loadcont.d | 106 ++++ esm/loadcrea.d | 127 ++++ esm/loadcrec.d | 59 ++ esm/loaddial.d | 291 +++++++++ esm/loaddoor.d | 49 ++ esm/loadench.d | 70 +++ esm/loadfact.d | 106 ++++ esm/loadglob.d | 61 ++ esm/loadgmst.d | 305 ++++++++++ esm/loadinfo.d | 249 ++++++++ esm/loadingr.d | 63 ++ esm/loadlevlist.d | 201 +++++++ esm/loadligh.d | 82 +++ esm/loadlocks.d | 77 +++ esm/loadltex.d | 107 ++++ esm/loadmgef.d | 114 ++++ esm/loadmisc.d | 63 ++ esm/loadnpc.d | 171 ++++++ esm/loadnpcc.d | 104 ++++ esm/loadrace.d | 82 +++ esm/loadregn.d | 113 ++++ esm/loadscpt.d | 140 +++++ esm/loadskil.d | 85 +++ esm/loadsndg.d | 66 +++ esm/loadsoun.d | 54 ++ esm/loadspel.d | 96 +++ esm/loadsscr.d | 55 ++ esm/loadstat.d | 53 ++ esm/loadweap.d | 92 +++ esm/records.d | 219 +++++++ input/events.d | 320 ++++++++++ input/keys.d | 262 +++++++++ input/ois.d | 428 ++++++++++++++ morro.d | 385 ++++++++++++ morro.ini | 44 ++ nif/base.d | 211 +++++++ nif/controlled.d | 90 +++ nif/controller.d | 317 ++++++++++ nif/data.d | 806 +++++++++++++++++++++++++ nif/effect.d | 110 ++++ nif/extra.d | 210 +++++++ nif/misc.d | 156 +++++ nif/nif.d | 106 ++++ nif/niffile.d | 485 +++++++++++++++ nif/niftool.d | 63 ++ nif/node.d | 324 +++++++++++ nif/property.d | 261 +++++++++ nif/record.d | 302 ++++++++++ ogre.cfg | 7 + ogre/bindings.d | 171 ++++++ ogre/cpp_bsaarchive.cpp | 137 +++++ ogre/cpp_framelistener.cpp | 161 +++++ ogre/cpp_interface.cpp | 755 ++++++++++++++++++++++++ ogre/cpp_ogre.cpp | 58 ++ ogre/cpp_overlay.cpp | 79 +++ ogre/meshloader.d | 309 ++++++++++ ogre/ogre.d | 133 +++++ plugins.cfg | 13 + resources.cfg | 23 + scene/celldata.d | 597 +++++++++++++++++++ scene/player.d | 48 ++ scene/soundlist.d | 72 +++ sound/audiere.d | 72 +++ sound/audio.d | 46 ++ sound/cpp_audiere.cpp | 118 ++++ sound/music.d | 277 +++++++++ sound/sfx.d | 202 +++++++ util/random.d | 155 +++++ util/regions.d | 644 ++++++++++++++++++++ util/reglist.d | 215 +++++++ util/uniquename.d | 96 +++ 100 files changed, 19865 insertions(+) create mode 100644 GPL3.txt create mode 100644 INSTALL.txt create mode 100644 Makefile create mode 100644 bored.d create mode 100644 bsa/bsafile.d create mode 100644 bsa/bsatool.d create mode 100644 cells.txt create mode 100644 core/config.d create mode 100644 core/filefinder.d create mode 100644 core/inifile.d create mode 100644 core/memory.d create mode 100644 core/resource.d create mode 100644 dsss.conf create mode 100644 esm/defs.d create mode 100644 esm/esmmain.d create mode 100644 esm/esmtool.d create mode 100644 esm/filereader.d create mode 100644 esm/imports.d create mode 100644 esm/listkeeper.d create mode 100644 esm/loadacti.d create mode 100644 esm/loadalch.d create mode 100644 esm/loadappa.d create mode 100644 esm/loadarmo.d create mode 100644 esm/loadbody.d create mode 100644 esm/loadbook.d create mode 100644 esm/loadbsgn.d create mode 100644 esm/loadcell.d create mode 100644 esm/loadclas.d create mode 100644 esm/loadclot.d create mode 100644 esm/loadcont.d create mode 100644 esm/loadcrea.d create mode 100644 esm/loadcrec.d create mode 100644 esm/loaddial.d create mode 100644 esm/loaddoor.d create mode 100644 esm/loadench.d create mode 100644 esm/loadfact.d create mode 100644 esm/loadglob.d create mode 100644 esm/loadgmst.d create mode 100644 esm/loadinfo.d create mode 100644 esm/loadingr.d create mode 100644 esm/loadlevlist.d create mode 100644 esm/loadligh.d create mode 100644 esm/loadlocks.d create mode 100644 esm/loadltex.d create mode 100644 esm/loadmgef.d create mode 100644 esm/loadmisc.d create mode 100644 esm/loadnpc.d create mode 100644 esm/loadnpcc.d create mode 100644 esm/loadrace.d create mode 100644 esm/loadregn.d create mode 100644 esm/loadscpt.d create mode 100644 esm/loadskil.d create mode 100644 esm/loadsndg.d create mode 100644 esm/loadsoun.d create mode 100644 esm/loadspel.d create mode 100644 esm/loadsscr.d create mode 100644 esm/loadstat.d create mode 100644 esm/loadweap.d create mode 100644 esm/records.d create mode 100644 input/events.d create mode 100644 input/keys.d create mode 100644 input/ois.d create mode 100644 morro.d create mode 100644 morro.ini create mode 100644 nif/base.d create mode 100644 nif/controlled.d create mode 100644 nif/controller.d create mode 100644 nif/data.d create mode 100644 nif/effect.d create mode 100644 nif/extra.d create mode 100644 nif/misc.d create mode 100644 nif/nif.d create mode 100644 nif/niffile.d create mode 100644 nif/niftool.d create mode 100644 nif/node.d create mode 100644 nif/property.d create mode 100644 nif/record.d create mode 100644 ogre.cfg create mode 100644 ogre/bindings.d create mode 100644 ogre/cpp_bsaarchive.cpp create mode 100644 ogre/cpp_framelistener.cpp create mode 100644 ogre/cpp_interface.cpp create mode 100644 ogre/cpp_ogre.cpp create mode 100644 ogre/cpp_overlay.cpp create mode 100644 ogre/meshloader.d create mode 100644 ogre/ogre.d create mode 100644 plugins.cfg create mode 100644 resources.cfg create mode 100644 scene/celldata.d create mode 100644 scene/player.d create mode 100644 scene/soundlist.d create mode 100644 sound/audiere.d create mode 100644 sound/audio.d create mode 100644 sound/cpp_audiere.cpp create mode 100644 sound/music.d create mode 100644 sound/sfx.d create mode 100644 util/random.d create mode 100644 util/regions.d create mode 100644 util/reglist.d create mode 100644 util/uniquename.d diff --git a/GPL3.txt b/GPL3.txt new file mode 100644 index 000000000..94a9ed024 --- /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 000000000..2ec5974e4 --- /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 000000000..dc81b8a7d --- /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 000000000..46f1c466d --- /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 000000000..ddb293c17 --- /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 000000000..b8282548a --- /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 000000000..4cff4a985 --- /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 000000000..1182b93b7 --- /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 000000000..2727fa5d5 --- /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 000000000..3bb1f414d --- /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 000000000..5b622c3f6 --- /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 000000000..210e5cbe7 --- /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 000000000..7d96aeb2a --- /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 000000000..f333557a1 --- /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 000000000..1ceb6d4f9 --- /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 000000000..b6cfd1d22 --- /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 000000000..1040d98cc --- /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 000000000..85ca7b6de --- /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 000000000..3e6006fc1 --- /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 000000000..3d8a5ac1d --- /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 000000000..6e0bfee22 --- /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 000000000..24f2b3019 --- /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 000000000..822ed3e76 --- /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 000000000..a8c865d5b --- /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 000000000..0dd503c83 --- /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 000000000..2009c7860 --- /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 000000000..e70ffdecb --- /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 000000000..a088b0807 --- /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 000000000..fcfe4b76f --- /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 000000000..2ce223b1f --- /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 000000000..3c7e7cddb --- /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 000000000..22bc806a1 --- /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 000000000..3a882d3b7 --- /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 000000000..d7a234b1e --- /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 000000000..32ee3c770 --- /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 000000000..213cf6613 --- /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 000000000..58ef857b9 --- /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 000000000..52e11defc --- /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 000000000..a51a4725f --- /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 000000000..4ff11c8d2 --- /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 000000000..bae3e5291 --- /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 000000000..9e8dba209 --- /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 000000000..11ffe1e31 --- /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 000000000..576b5ff30 --- /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 000000000..535af9195 --- /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 000000000..a0425116d --- /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 000000000..c9ea1ff12 --- /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 000000000..2fb4dd262 --- /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 000000000..3d7e0bf41 --- /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 000000000..0d093ca4a --- /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 000000000..c966c7a17 --- /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 000000000..e06f917cf --- /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 000000000..6eca2d03c --- /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 000000000..7c1b97c41 --- /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 000000000..42556cb9b --- /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 000000000..744f928c2 --- /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 000000000..d5dd211a1 --- /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 000000000..a4849613b --- /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 000000000..5ad88deb5 --- /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 000000000..60129cadf --- /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 000000000..2e96f9340 --- /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 000000000..d65fe37aa --- /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 000000000..559266c47 --- /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 000000000..fe9d3e1dc --- /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 000000000..1b28e9ae0 --- /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 000000000..6e60d23c3 --- /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 000000000..639961583 --- /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 000000000..adbf1d4eb --- /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 000000000..61c08efe6 --- /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 000000000..90dc40523 --- /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 000000000..5624695be --- /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 000000000..bb63bcdfa --- /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 000000000..818e180fa --- /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 000000000..bf2486d59 --- /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 000000000..ac786d786 --- /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 000000000..4db52df95 --- /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 000000000..1a4298991 --- /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 000000000..7685d75a4 --- /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 000000000..31deb6ef6 --- /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 000000000..86843e657 --- /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 000000000..c095cec8a --- /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 000000000..8c2908581 --- /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 000000000..c90828369 --- /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 000000000..a30d35c76 --- /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 000000000..97f1d1a85 --- /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 000000000..64abdabcf --- /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 000000000..0f7516e9f --- /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 000000000..683609ff7 --- /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 000000000..75853aff3 --- /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 000000000..802e8e719 --- /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 000000000..111c405f0 --- /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 000000000..d3da6a0f9 --- /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"); +}